Day Vision

Reading Everyday,Extending Vision

node.js Rsa解码过程(以酷派支付校验为例)

2022-09-16 06:35:52


这都相当简单, 主要有些可能的urlencode或者忘记加""符号等等的小错误, 多多调试的ok了接下来主要讲讲恶心的酷派校验有不少的渠道商使用rsa进行校验的, 比如华为, 同样很简单例如使用RSAWITHSHA1const verify


引言

最近我在写一个接入各个渠道商的 游戏聚合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的加密解密公式大概是这样的

rsa.jpg

这里 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<integer>).toString('utf8') 是可以用字节数组转为字符串的, 可是我得不到字节数组。
然后我又看了一会 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();
}

虽然酷派这个手机品牌最近也不火了, 但是接入他渠道还真是不简单。 希望在他的渠道能多挣点钱,别然我太伤心;