简介

例如SM4:我国SM4分组密码算法正式成为ISO/IEC国际标准

国密算法,即国家商用密码算法。是由国家密码管理局认定和公布的密码算法标准及其应用规范,其中部分密码算法已经成为国际标准。如SM系列密码,SM代表商密,即商业密码,是指用于商业的、不涉及国家秘密的密码技术。
——国密算法介绍

这里常用的为SM2SM4算法

SM2

简介

它是基于椭圆曲线密码的公钥密码算法标准,其秘钥长度256bit,包含数字签名、密钥交换和公钥加密,用于替换RSA/DH/ECDSA/ECDH等国际算法。可以满足电子认证服务系统等应用需求,由国家密码管理局于2010年12月17号发布。
SM2采用的是ECC 256位的一种,其安全强度比RSA 2048位高,且运算速度快于RSA。
——国密算法介绍

JS实现

JS已有较为成熟的实现库,这里推荐sm-crypto,可实现SM2SM3SM4

上面链接的NPM库间接已给出示例,这里直接照搬:
密钥对

const sm2 = require('sm-crypto').sm2

let keypair = sm2.generateKeyPairHex()

publicKey = keypair.publicKey // 公钥
privateKey = keypair.privateKey // 私钥

// 默认生成公钥 130 位太长,可以压缩公钥到 66 位
const compressedPublicKey = sm2.compressPublicKeyHex(publicKey) // compressedPublicKey 和 publicKey 等价
sm2.comparePublicKeyHex(publicKey, compressedPublicKey) // 判断公钥是否等价

// 自定义随机数,参数会直接透传给 jsbn 库的 BigInteger 构造器
// 注意:开发者使用自定义随机数,需要自行确保传入的随机数符合密码学安全
let keypair2 = sm2.generateKeyPairHex('123123123123123')
let keypair3 = sm2.generateKeyPairHex(256, SecureRandom)

let verifyResult = sm2.verifyPublicKey(publicKey) // 验证公钥
verifyResult = sm2.verifyPublicKey(compressedPublicKey) // 验证公钥

加解密

const cipherMode = 1 // 1 - C1C3C2,0 - C1C2C3,默认为1

let encryptData = sm2.doEncrypt(msgString, publicKey, cipherMode) // 加密结果
let decryptData = sm2.doDecrypt(encryptData, privateKey, cipherMode) // 解密结果

encryptData = sm2.doEncrypt(msgArray, publicKey, cipherMode) // 加密结果,输入数组
decryptData = sm2.doDecrypt(encryptData, privateKey, cipherMode, {output: 'array'}) // 解密结果,输出数组

来测试一下:

const sm2 = require('sm-crypto').sm2

let keypair = sm2.generateKeyPairHex()

publicKey = keypair.publicKey // 公钥
privateKey = keypair.privateKey // 私钥

// 默认生成公钥 130 位太长,可以压缩公钥到 66 位
const compressedPublicKey = sm2.compressPublicKeyHex(publicKey) // compressedPublicKey 和 publicKey 等价
sm2.comparePublicKeyHex(publicKey, compressedPublicKey) // 判断公钥是否等价

// 自定义随机数,参数会直接透传给 jsbn 库的 BigInteger 构造器
// 注意:开发者使用自定义随机数,需要自行确保传入的随机数符合密码学安全
//let keypair2 = sm2.generateKeyPairHex('123123123123123')
//let keypair3 = sm2.generateKeyPairHex(256, SecureRandom)

let verifyResult = sm2.verifyPublicKey(publicKey) // 验证公钥
verifyResult = sm2.verifyPublicKey(compressedPublicKey) // 验证公钥

console.log(verifyResult)

const cipherMode = 1 // 1 - C1C3C2,0 - C1C2C3,默认为1

msgString = 'HELLO WORLD'

let encryptData = sm2.doEncrypt(msgString, publicKey, cipherMode) // 加密结果
let decryptData = sm2.doDecrypt(encryptData, privateKey, cipherMode) // 解密结果

console.log(encryptData)
console.log(decryptData)

//encryptData = sm2.doEncrypt(msgArray, publicKey, cipherMode) // 加密结果,输入数组
//decryptData = sm2.doDecrypt(encryptData, privateKey, cipherMode, {output: 'array'}) // 解密结果,输出数组

输出结果:

true
f378b1ea6ee7d8d1729ee38fc5bbe8ccc2c2813f7fc07bcad877169d57fa5e2212d994cbf4c4dff78bd58acc82304373013e5d2e793c0ad2aafc90ab2c65083af6762f53b00324f091f67df9cac9d851a31baee07dfc19114bc0188dcb7aa37c9c9f9bfb43dec6a0712225
HELLO WORLD

SM4

是我国自主设计的分组对称密码算法,用于替代DES/AES等国际算法。SM4算法与AES算法具有相同的密钥长度、分组长度,都是128bit。于2012年3月21日发布,适用于密码应用中使用分组密码的需求。
——国密算法介绍

SM4 NPM库中也给出了示例,同样照搬:
加密

const sm4 = require('sm-crypto').sm4
const msg = 'hello world! 我是 juneandgreen.' // 可以为 utf8 串或字节数组
const key = '0123456789abcdeffedcba9876543210' // 可以为 16 进制串或字节数组,要求为 128 比特

let encryptData = sm4.encrypt(msg, key) // 加密,默认输出 16 进制字符串,默认使用 pkcs#7 填充(传 pkcs#5 也会走 pkcs#7 填充)
let encryptData = sm4.encrypt(msg, key, {padding: 'none'}) // 加密,不使用 padding
let encryptData = sm4.encrypt(msg, key, {padding: 'none', output: 'array'}) // 加密,不使用 padding,输出为字节数组
let encryptData = sm4.encrypt(msg, key, {mode: 'cbc', iv: 'fedcba98765432100123456789abcdef'}) // 加密,cbc 模式

解密

const encryptData = '0e395deb10f6e8a17e17823e1fd9bd98a1bff1df508b5b8a1efb79ec633d1bb129432ac1b74972dbe97bab04f024e89c' // 可以为 16 进制串或字节数组
const key = '0123456789abcdeffedcba9876543210' // 可以为 16 进制串或字节数组,要求为 128 比特

let decryptData = sm4.decrypt(encryptData, key) // 解密,默认输出 utf8 字符串,默认使用 pkcs#7 填充(传 pkcs#5 也会走 pkcs#7 填充)
let decryptData = sm4.decrypt(encryptData, key, {padding: 'none'}) // 解密,不使用 padding
let decryptData = sm4.decrypt(encryptData, key, {padding: 'none', output: 'array'}) // 解密,不使用 padding,输出为字节数组
let decryptData = sm4.decrypt(encryptData, key, {mode: 'cbc', iv: 'fedcba98765432100123456789abcdef'}) // 解密,cbc 模式

实战

逆向

定点医疗机构查询-国家医保服务平台
注意:只做技术研究、交流!

可以看到需要逆向的参数有很多:
请求头:

表单:

响应结果:

一顿找(推荐搜索关键字):

本来想半扣的,毕竟这里面比如那个r就是个标准的SHA256加密,可以直接用三方库,SM不就说了,当时在半扣的过程中,越扣越多,索性全扣得了

同时需要一个参数t,直接在控制台输出t看看:

vat t ={
    "transformRequest": {},
    "transformResponse": {},
    "timeout": 30000,
    "xsrfCookieName": "XSRF-TOKEN",
    "xsrfHeaderName": "X-XSRF-TOKEN",
    "maxContentLength": -1,
    "headers": {
        "common": {
            "Accept": "application/json, text/plain, */*"
        },
        "delete": {},
        "get": {},
        "head": {},
        "post": {
            "Content-Type": "application/x-www-form-urlencoded"
        },
        "put": {
            "Content-Type": "application/x-www-form-urlencoded"
        },
        "patch": {
            "Content-Type": "application/x-www-form-urlencoded"
        },
        "Accept": "application/json",
        "Content-Type": "application/json",
        "channel": "web"
    },
    "withCredentials": false,
    "baseURL": "/ebus/fuwu/api",
    "method": "post",
    "url": "/nthl/api/CommQuery/queryFixedHospital",
    "data": {
        "addr": "",
        "regnCode": "340600",
        "medinsName": "",
        "medinsLvCode": "",
        "medinsTypeCode": "",
        "openElec": "",
        "pageNum": 5,
        "pageSize": 10
    }
}

t明显可直接写死,其中一些参数猜测可能为:

  • regnCode:地区代码(打开此网站会检测你IP位置并在控制台输出)
  • pageNum:翻页
  • pageSize:一页内容量

如下所示,打开网站在开发人员工具开启下重新刷新页面,会看到地区检测结果:

如果想要获取到其它位置,自行研究API了

将其封装进代码,最终Webpack代码6W行,如果想将生成相关的代码自己扣下来的话(因为这段代码被包含在了Webpack中)同时还要注意去掉(仅自己封装用于生成表单和请求头的代码)try-catch语句,不然少了参数都不知道。

因为生成加密参数的代码被包含在Webpack中"7d92",所以可以试试不自己扣(因为上面所有的Webpack代码已经全扣了),那么要如何调用呢?

可以知道代码在"7d92",那么先输出一下webpack("7d92")(这里是自行导出了加载器,详见Webpack文章)

console.log(webpack("7d92"))

结果:

{ a: [Getter], b: [Getter] }

选择a再输出:

console.log(webpack("7d92").a)

结果:

[Function: f]

可以看到a为一个函数,并且其名字似乎预示选对了
再调用一下函数:

console.log(webpack("7d92").a())

结果(只截取片段):

 return t.headers["x-tif-paasid"] = l.paasId,

发现报错,很明显,这里便是我们要找的加密方法,把上面的找到的t塞进去看看结果:

console.log(webpack("7d92").a(t))

结果:

{
  transformRequest: {},
  transformResponse: {},
  timeout: 30000,
  xsrfCookieName: 'XSRF-TOKEN',
  xsrfHeaderName: 'X-XSRF-TOKEN',
  maxContentLength: -1,
  headers: {
    common: { Accept: 'application/json, text/plain, */*' },
    delete: {},
    get: {},
    head: {},
    post: { 'Content-Type': 'application/x-www-form-urlencoded' },
    put: { 'Content-Type': 'application/x-www-form-urlencoded' },
    patch: { 'Content-Type': 'application/x-www-form-urlencoded' },
    Accept: 'application/json',
    'Content-Type': 'application/json',
    channel: 'web',
    'x-tif-paasid': undefined,
    'x-tif-signature': '092eeb43442f3633e5d22790695b39264f0f0834e14f60dfe99d514e95a2cf93',
    'x-tif-timestamp': 1672929556,
    'x-tif-nonce': 'Y4aUfjoK',
    contentType: 'application/x-www-form-urlencoded'
  },
  withCredentials: false,
  baseURL: '/ebus/fuwu/api',
  method: 'post',
  url: '/nthl/api/CommQuery/queryFixedHospital',
  data: '{"data":{"data":{"encData":"3DFBCA4667B978F639BB23B95DCE4CC73F6F89D76EF932292FE8BA14BC87DB17CCD20943B4DAE96380B41164D761DE9742C84A985FE3BABC31CB352556BB87C9C1495DB24A29AB6BC3A85AB7FCA00F338EE714ACFC4C924F01CF575098AEF167B7A135B72DAC6C2F3CD510E411A3C63A7D334AF16D66C5F4C5E72412B915CB27"},"appCode":"T98HPCGN5ZVVQBS8LZQNOAEXVI9GYHKQ","version":"1.0.0","encType":"SM4","signType":"SM2","timestamp":1672929556,"signData":"2+IZoksuP1rgU4LhVryfmBs1icvANuGmjR3FNk95bJyTjyqm3TK66ts3oV7L1d3Aot5RXvB7GRCK9QjnypILxw=="}}'
}

成功拿到需要的数据!

暂时放下,来看看结果如何处理。

最终找到此处:

此时e.data还是原始响应结果,并未解密:

{
    "code": 0,
    "data": {
        "signData": "+EJhLYxiMpbwjGCsEvK+wyXNoAyMKpkGnjb7tGZ3OoFNWqKzDjIeDfqE9ZSO0anEt3z4rNoFJ5lgHxeDuddVdg==",
        "encType": "SM4",
        "data": {
            "encData": "2DC8C08C5C251B04B9CCBA9436917607527A4CC8F48CC3C8D7C226EB9483D8FA03CECA79ABA4AC1CE5 ....篇幅有限,这里省略.....FCF4D05388AA6A2E4E171B6486547C2E9DB06B68A81A61409372E7E00A0D1691A2F222D7698DCAC"
        },
        "signType": "SM2",
        "appCode": "T98HPCGN5ZVVQBS8LZQNOAEXVI9GYHKQ",
        "version": "1.0.0",
        "timestamp": "1672919504404"
    },
    "message": "成功",
    "timestamp": "1672919504",
    "type": "success"
}

复制到本地备后测试使用。

同时这段代码也被包含在Webpack中,可以选择不扣,不过因为实际涉及解密的代码都集中在了一行上,这里选择半扣:

function get_data(data){
    const e = {data:data},
        c = webpack("7d92");

    return e.data.data.appCode && (e.data.data = Object(c.b)("SM4", e.data)),e.data
}

看到这里可以猜测,webpack("7d92")a负责加密,b负责解密
将备好的数据放进去看运行结果:

成功解密

简单封装下代码,准备模拟请求

模拟请求

这里继续使用Python来模拟请求,只爬取一页数据
同时要注意,从js代码获取的值需要进行一些处理才能让requests得以正确传递,
同时如果遇到UnicodeEncodeError: 'gbk' codec...问题请参照此文章:使用PyExecJS为Python爬虫提供加密参数

完整代码请见仓库 国家医疗保障服务平台-SM/gjyb

代码仅作学习交流之用,请勿用于非法用途!