1. 微信支付 v3版介绍
微信支付是微信公众账号、微信开放平台和微信商户号三个主体之间的支付服务。微信支付 v3版相比于 v2版,最大的变化是在加强了支付接口安全性的同时,将业务逻辑参数以JSON格式传递,而不是像之前的XML格式。
为了在数据传输时提高安全性,微信支付 v3版增加了RSA密钥加密功能。虽然这种方式对于客户端用户是无感知的,但对于开发者来说,需要进行密钥加密和解密。
本文将介绍如何解密微信支付 v3版接口返回的加密数据,为开发者提供了解逐步实现解密逻辑的方法。
2. 解密原理
2.1 加密方法
微信支付 v3版采用了签名、加密和随机串的方式保证数据传输的安全性。这里我们需要了解以下几个参数:
nonce_str:自定义随机值,例如:"5K8264ILTKCH16CQ2502SI8ZNMTM67VS"
associated_data:对于AEAD_AES_256_GCM算法,传输中会加入额外的数据进行验证,此处为:"{\"nonce\":\"5K8264ILTKCH16CQ2502SI8ZNMTM67VS\",\"associated_data\":\"test\"}"
ciphertext:加密的报文体内容
ad_info:微信支付v3接口新增的广告信息,指用户留在微信上的时间,单位为秒。例如:"20201323:34:56"
mch_id:商户号,注意不是AppID
apiversion:接口版本号,例如:"v3"
加密方法采用的是AEAD_AES_256_GCM算法,通俗地讲就是在AES加密的基础上进行GCM模式加密。对于AES加密,是常规的互联网加密算法,核心代码如下:
/**
* 用于进行AES加密的函数
*/
function aesEncrypt(msg, key) {
const cipher = crypto.createCipheriv('aes-256-cdc', key, iv);
return Buffer.concat([cipher.update(msg), cipher.final()]);
}
而在此基础上,加入了GCM模式的加密,核心代码如下:
/**
* 用于进行AES-GCM加密的函数
*/
function aesGcmEncrypt(msg, key, associated_data = '') {
const cipher = crypto.createCipheriv(ALGORITHM_AES_256_GCM, key, iv, {
authTagLength: TAG_LENGTH_16,
});
cipher.setAAD(Buffer.from(associated_data, 'utf8'), {
plaintextLength: Buffer.byteLength(msg),
});
return Buffer.concat([cipher.update(msg), cipher.final(), cipher.getAuthTag()]);
}
2.2 解密方法
微信支付 v3版数据的解密,主要涉及两种密钥:APIv3密钥 和 敏感信息加密密钥。
APIv3密钥是微信支付v3唯一的凭据,类似于之前我们使用的AppID和AppSecret。此处我们需要用到对应的商户号mch_id、APIv3密钥商户私钥和APIv3密钥平台私钥进行解密。解密之前,我们还需要获取到微信支付v3接口的响应信息和独立随机字符串nonce_str。
为了方便解密,我们可以使用官方提供的解密类库:aesgcm.js。该类库提供SanboxEncryptor和APIv3Encryptor两个类,分别用于解密测试环境和正式环境下的数据。
解密时,我们需要进行以下步骤:
发送加密数据
获取平台证书信息
验证签名
使用APIv3密钥和敏感信息加密密钥解密数据
结果处理
步骤四是重点,这里为了方便,我们分别列出APIv3密钥和敏感信息加密密钥的解密代码:
/**
* 解密APIv3密钥
* @param {string} cipherText - 微信支付返回的加密数据
* @param {string} nonce - 随机字符串,与微信支付接口传输的一致
* @param {string} associatedData - 额外数据,与微信支付接口传输的一致
* @param {string} apiversion - 接口版本,例如:"v3"
* @param {string} mchid - 商户号
* @param {string} privateKey - APIv3密钥商户私钥
* @param {string} wechatpayPublicKey - APIv3密钥平台公钥
* @return {string} 解密后的APIv3密钥
*/
async function decryptAPIv3Key(
cipherText,
nonce,
associatedData,
apiversion,
mchid,
privateKey,
wechatpayPublicKey
) {
const localPrivateKey = await parsePrivateKey(privateKey);
const localWechatPublicKey = await parsePublicKey(wechatpayPublicKey);
const APIv3EncryptorInstance = new APIv3Encryptor(
mchid,
localPrivateKey,
localWechatPublicKey,
'/v3/decrypt',
apiversion
);
const ciphertextBuffer = Buffer.from(cipherText, 'base64');
const decryptResult = await APIv3EncryptorInstance.decryptData(
ciphertextBuffer,
Buffer.from(nonce, 'utf8'),
Buffer.from(associatedData, 'utf8')
);
return decryptResult.toString();
}
/**
* 解密敏感信息加密密钥
* @param {string} cipherText - 微信支付返回的加密数据
* @param {string} nonce - 随机字符串,与微信支付接口传输的一致
* @param {string} associatedData - 额外数据,与微信支付接口传输的一致
* @param {string} apiversion - 接口版本,例如:"v3"
* @param {string} mchid - 商户号
* @param {string} key - APIv3密钥
* @return {string} 解密后的敏感信息加密密钥
*/
async function decryptSymmetricKey(cipherText, nonce, associatedData, apiversion, mchid, key) {
const plainKey = await decryptAPIv3Key(key, nonce, associatedData, apiversion, mchid);
const decryptedBuffer = await aesgcm.decrypt(
Buffer.from(cipherText, 'base64'),
Buffer.from(plainKey, 'base64'),
Buffer.from(associatedData, 'utf8'),
Buffer.from(nonce, 'utf8')
);
return decryptedBuffer.toString('utf8');
}
3. 总结
微信支付 v3版的加密和解密方法有了重大升级,其实现方式涉及到多个参数和密钥,对于开发者要求较高。因此,我们需要借助官方提供的类库,以及对Node.js加密库crypto的深入理解,逐步构建起理解微信支付v3版的方法论,进而进行程序开发和功能优化。