引言
最近我在写一个接入各个渠道商的游戏聚合sdk, 主要就是进行登录的校验以及支付相关验签, 和支付回调接口。其中涉及很多校验的代码,这里就简单提一下, 更多要写一下酷派的rsa解码, 简直不要太恶心
md5/hmac
大部分渠道商的签名校验都是以md5/hmac来进行的
例子1
假如现在小米接收到了用户的充值, 然后异步通知我们sdk服务器用户的充值,他发出以下数据
{
"data": {
"status": "SUCCESS",
"cp_order_no": "mc_ud8293jdf",
"orderid": "xiaomi_ud8293jdf",
"price": 12,
"sumit_time": "20181211078455",
"productid": "mc_2342323",
"extra": "155",
},
"sign": "df789eufd8f923jhd829jfd98"
}
来告知充值ok了, 为了防止有人恶意篡改数据造成损失, 我们需要对传输的数据进行校验。
一般的校验方式很简单, 就是讲传输数据按照key的ASCII顺序排列,从a到z的顺序, 将key=value
这样的一对用&或者用空字符串来连接成一个baseStr
, 将上列数据处理后就是这样的
cp_order_no=mc_ud8293jdf&extra=155&orderid=xiaomi_ud8293jdf&.....sumit_time=20181211078455
如果用md5
sign大概就是md(baseStr
+appkey
), 这里有可能要hex编码或者base64编码格式
appkey是应用接入渠道(小米,应用宝等)分配的,私密的东西
用nodejs代码表示就是
const data = `${baseStr}&${appkey}`
crypto.createHash('md5').update(data).digest('hex')
// 或者
crypto.createHash('md5').update(data).digest('base64')
如果用hmac
一般就是
crypto.createHmac('sha1', appkey).update(baseStr).digest('hex')
// 或者
crypto.createHmac('sha1', appkey).update(baseStr).digest('base64')
再讲自己计算出的sign
和渠道商传递过来的sign
进行对比, 如果不同就说明数据有问题。
这都相当简单, 主要有些可能的urlencode或者忘记加""符号等等的小错误, 多多调试的ok了
接下来主要讲讲恶心的酷派校验
有不少的渠道商使用rsa
进行校验的, 比如华为, 同样很简单
例如使用RSAWITHSHA1
const verify = crypto.createVerify('RSA-SHA1');
verify.update(Buffer.from(_baseStr, 'utf8'));
// key是渠道商给的Rsa public key 可以是一个.pem文件
return verify.verify(key, sign, 'base64');
一般也就是这样就ok了
但是酷派的就比较特殊, 他的文档简而言之就一句话:你自己去看我给的代码示例, 他给的demo只有java php c++ net, 就是没有node的, 所以我就慢慢把他java的代码翻译过来, 以下是过程:
他给数据是这样的
const reqJson = `{\"exorderno\":\"iVk4eRZknftx4vAJm5VE\",\"transid\":\"02115061814204200016\",\"waresid\":1,${nbsp
}\"appid\":\"3000962200\",\"feetype\":0,\"money\":1,\"count\":1,\"result\":0,\"transtype\":0,${nbsp
}\"transtime\":\"2015-06-18 14:20:59\",\"cpprivate\":\"cp private info!!\",\"paytype\":401}`;
const _sign = '56b10877c6ecf3fa3c4805ca8b6f26a8 5fd39828d76b54faf8a034e4d509150b 2519141767960a2e1bfd27b04dbcc8b2';
reqJson
是返回数据
然后假设我们appkey
是这样的
const appkey = `RkIwNTlFM0Y5RTEzNTA5NDcxNEMxMkY1OTREQUQxM0VFNEEwRTI2N01UZ3hNamd6T0RRek1ERTVOR${nbsp
}Gd4T0RreU9Ua3JNVGsxTlRBME5EQXlNakF5TmpRM056RTVPRE13TkRZNE5ESTJOekUxTWpVMk5EUXdOREEz`;
接下来一些东西, 我也看不懂, 酷派Demo代码中说base64.decode(appkey)
就可以得到类似23942398+2342389479239428
的一个字符串,然后+号前的是privatekey
, +号后面的是modkey
,(什么!rsa秘钥的私密就得到了!?)
// 获取privatekey和modkey
String decodeBaseStr = Base64.decode(key);
然后我用nodejs的base64.decode方法试了几遍都不行, 方法如下
const str = Buffer.from(key, 'base64').toString()
卡了我好一会之后我才发现他的Base64.decode
是自己写的方法,点进去后看到他的decode
方法原来是这样的
// java
if (cryptoStr.length() < 40)
return "";
try {
String tempStr = new String(decode(cryptoStr.getBytes("UTF-8")));
String result = tempStr.substring(40, tempStr.length());
return new String(decode(result.getBytes("UTF-8")));
} catch (java.lang.ArrayIndexOutOfBoundsException ex) {
return "";
}
这尼玛我被骗了, 然后我按照他的这个鬼用nodejs写完是这样的
// nodejs
const _str1 = Buffer.from(key, 'base64').toString().substring(40);
const _str2 = Buffer.from(_str2, 'base64').toString();
// _str2 : 18128384301948189299+195504402202647719830468426715256440407
就这样我就拿到privatekey
和mod
了
稍微了解过rsa加密算法的人都有印象
rsa的加密解密公式大概是这样的
这里C1
就是酷派给的_sign按' '
分割后得到暗文, d
就是得到的privatekey
, n
就是我们得到的mod
, 按照这个计算式就可以得到明文M1 M2 M3
了, 上面图片中的数都比较小, 可是我们得到的数可是很大的,如同一亿的一亿次方再模99123123482348230942390这样的,妈的我一个不是科班的不太懂这个东西该怎么解, 而且这个数是不是太大了。
后来查来查去看到说有啥指数循环节的东东可以将指数降低来运算, 像酷派Demo里是java BigInteger
有方法powmod
,可以直接得结果, 我查了一会后来在Stack Overflow找到nodejs的big-integer库也有powmod方法,开心!
然后java的byteArray
又然我卡了半天
// java
private static byte[][] dencodeRSA(BigInteger[] encodeM, BigInteger d,
BigInteger n) {
byte[][] dencodeM = new byte[encodeM.length][];
int i;
int lung = encodeM.length;
for (i = 0; i < lung; i++) {
dencodeM[i] = encodeM[i].modPow(d, n).toByteArray();
}
return dencodeM;
}
这里取模后的很大的数他转为bypteArrry
, 之后用new String(bypteArrry, 'UTF-8')
就得到结果了
我直接用
// nodejs
bigInt.modPow(_p, _m).toString()
得到的结果不一样
我是直接转为字符串了, 他是转byteArray
再转utf8格式的字符串, 我想了半天node的Buffer
怎么整这个问题。
Buffer
有个方法Buffer.from(array
是可以用字节数组转为字符串的, 可是我得不到字节数组。
然后我又看了一会big-integer
库的方法, 终于找到他有个toArray(radix: number)
方法, 参数是可以指定进制, 而字节的radix就是256啊, 然后我输入进去, 阿哈!, 就得到了一个字节数组了, 再将其放入到Buffer.from()
里, 终于成功得到了与他的java一样的结果了。
完整代码
/**
* 解密酷派rsa
* @param sign 酷派传递的sign
* @param p 通过appkey解得的privatekey
* @param m 通过appkey解得的mode
*/
function decrypt(sign: string, p: string, m: string): string{
const keys = sign.split(' ');
if (!_.isArray(keys)) {
throw new BadParamsException('密文不符合要求!!!');
}
const bigIntArr = keys.map(item => {
return bigint(item, 16);
});
const _p = bigint(p);
const _m = bigint(m);
let str = '';
bigIntArr.forEach(item => {
const tmp = item.modPow(_p, _m).toArray(256);
str = str + Buffer.from(tmp.value).toString('utf8')
.replace(/\r/g, '')
.replace(/\n/g, '');
});
return str.trim();
}
虽然酷派这个手机品牌最近也不火了, 但是接入他渠道还真是不简单。 希望在他的渠道能多挣点钱,别然我太伤心;