最近因为在写微信支付相关的代码,所以不可避免的涉及到加密解密的问题。而很多js的许多加密解密算法需要自行寻找,我也没有在网上找到一篇针对微信支付这个问题的综合类博客,所以在这里叙述一下我自己关于AEAD_AES_256_GCM解密的一个JS解决方案,并列举一下收集到的资料,防止大家走弯路。
本篇文章针对的具体问题如下:
微信支付通知API:https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/pay/transactions/chapter3_11.shtml
回调报文解密文档:https://wechatpay-api.gitbook.io/wechatpay-api-v3/qian-ming-zhi-nan-1/zheng-shu-he-hui-tiao-bao-wen-jie-mi
这个名称应该是分三部分看的
密文 = 加密算法(密钥,明文)
。也就是说密文是由加密算法、密钥、明文
共同产生的,而AES指的是一种加密算法,256指的是密钥的长度为256位(32B)。所以,AES-256指的是使用AES算法和32字节长度密钥的一种加密方式,与之相似的还有AES-128和AES-192。向量
来进行初始化,这就使得我们在加密的过程中,不仅仅需要有一个密钥,还需要一个计数器的初始化向量(Initialization vector , iv)和一个附加数据。加密
和验证加密解密过程的结果是否正确
这两个操作,那么他就是一种AEAD的算法。上面说的AES-256-GCM就是一种AEAD加密算法,所以在我的浅薄理解中,AEAD-AES-256-GCM和AES-256-GCM应该是一种东西吧。(不确定,欢迎讨论)下述是微信支付提供的参数,当然还有商户平台上提供的32位密钥(表示为key)
"resource" : {
"algorithm":"AEAD_AES_256_GCM",
"ciphertext": "...",
"nonce": "...",
"original_type":"transaction",
"associated_data": ""
}
JS的话我是在npm上找到的一个库——node-aes-gcm,链接:https://www.npmjs.com/package/node-aes-gcm。这个库只有两个函数,加密与解密。函数签名如下:
function encrypt(key, iv, plaintext, add): { ciphertext: Buffer, auth_tag: Buffer}
function decrypt(key, iv, ciphertext, aad, auth_tag): { plaintext: Buffer, auth_ok: Boolean }
参数解释:
resource.ciphertext = ciphertext + auth_tag
因为微信官方并没有给出JS的实现方法,所以我这里使用python的实现示例进行比较验证。实验思路为:
// cnpm install node-aes-gcm
> gcm = require('node-aes-gcm')
{ encrypt: [Function], decrypt: [Function] }
> key = new Buffer([0xfe,0xff,0xe9,0x92,0x86,0x65,0x73,0x1c,0x6d,0x6a,0x8f,0x94,0x67,0x30,0x83,0x08])
<Buffer fe ff e9 92 86 65 73 1c 6d 6a 8f 94 67 30 83 08>
> iv = new Buffer([0xca,0xfe,0xba,0xbe,0xfa,0xce,0xdb,0xad,0xde,0xca,0xf8,0x88])
<Buffer ca fe ba be fa ce db ad de ca f8 88>
> plaintext = new Buffer([0xd9,0x31,0x32,0x25,0xf8,0x84,0x06,0xe5,0xa5,0x59,0x09,0xc5,0xaf,0xf5,0x26,0x9a,0x86,0xa7,0xa9,0x53,0x15,0x34,0xf7,0xda,0x2e,0x4c,0x30,0x3d,0x8a,0x31,0x8a,0x72,0x1c,0x3c,0x0c,0x95,0x95,0x68,0x09,0x53,0x2f,0xcf,0x0e,0x24,0x49,0xa6,0xb5,0x25,0xb1,0x6a,0xed,0xf5,0xaa,0x0d,0xe6,0x57,0xba,0x63,0x7b,0x39,0x1a,0xaf,0xd2,0x55])
<Buffer d9 31 32 25 f8 84 06 e5 a5 59 09 c5 af f5 26 9a 86 a7 a9 53 15 34 f7 da 2e 4c 30 3d 8a 31 8a 72 1c 3c 0c 95 95 68 09 53 2f cf 0e 24 49 a6 b5 25 b1 6a ed ...>
// 在这里指定附加数据为空,生成密文和MAC
> e = gcm.encrypt(key, iv, plaintext, new Buffer([]))
{ ciphertext: <Buffer 42 83 1e c2 21 77 74 24 4b 72 21 b7 84 d0 d4 9c e3 aa 21 2f 2c 02 a4 e0 35 c1 7e 23 29 ac a1 2e 21 d5 14 b2 54 66 93 1c 7d 8f 6a 5a ac 84 aa 05 1b a3 0b ...>,
auth_tag: <Buffer 4d 5c 2a f3 27 cd 64 a6 2c f3 5a bd 2b a6 fa b4> }
> d = gcm.decrypt(key, iv, e.ciphertext, new Buffer([]), e.auth_tag)
{ plaintext: <Buffer d9 31 32 25 f8 84 06 e5 a5 59 09 c5 af f5 26 9a 86 a7 a9 53 15 34 f7 da 2e 4c 30 3d 8a 31 8a 72 1c 3c 0c 95 95 68 09 53 2f cf 0e 24 49 a6 b5 25 b1 6a ed ...>,
auth_ok: true }
# pip install cryptography
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
# 将16进制字符串转为buffer作为key
>>> key = bytes.fromhex('feffe9928665731c6d6a8f9467308308')
>>> key
b'\xfe\xff\xe9\x92\x86es\x1cmj\x8f\x94g0\x83\x08'
# 将16进制字符串转为buffer作为initVector
>>> iv = bytes.fromhex('cafebabefacedbaddecaf888')
>>> iv
b'\xca\xfe\xba\xbe\xfa\xce\xdb\xad\xde\xca\xf8\x88'
# 附加数据暂时为空
>>> ad = bytes.fromhex('')
>>> ad
b''
# 密文为e.ciphertext与e.auth_tag的拼接
>>> ciphertext = bytes.fromhex('42831ec2217774244b7221b784d0d49ce3aa212f2c02a4e035c17e2329aca12e21d514b25466931c7d8f6a5aac84aa051ba30b396a0aac973d58e091473f5985' + '4d5c2af327cd64a62cf35abd2ba6fab4');
# python解密结果与js的解密结果相同,验证成功
>>> aesgcm = AESGCM(key)
>>> aesgcm.decrypt(iv, ciphertext, ad)
b'\xd912%\xf8\x84\x06\xe5\xa5Y\t\xc5\xaf\xf5&\x9a\x86\xa7\xa9S\x154\xf7\xda.L0=\x8a1\x8ar\x1c<\x0c\x95\x95h\tS/\xcf\x0e$I\xa6\xb5%\xb1j\xed\xf5\xaa\r\xe6W\xbac{9\x1a\xaf\xd2U'
因为商户号还没注册完,所以本段代码的可用性还没有得到验证
const AUTH_KEY_LENGTH = 16; // B
const { id ,create_time, event_type, resource_type, resource, summary } = ctx.request.body;
const { algorithm, ciphertext, associated_data , nonce, original_type } = resource;
const key_bytes = Buffer.from(miniprogramConfig.api_v3_key_32, 'utf8');
const nonce_bytes = Buffer.from(nonce, 'utf8');
const associated_data_bytes = Buffer.from(associated_data, 'utf8');
const ciphertext_bytes = Buffer.from(ciphertext, 'base64');
const cipherdata_length = ciphertext_bytes.length - AUTH_KEY_LENGTH;
const cipherdata_bytes = ciphertext_bytes.slice(0, cipherdata_length);
const auth_tag_bytes = ciphertext_bytes.slice(cipherdata_length, ciphertext_bytes.length);
const result = gcm.decrypt(key_bytes, nonce_bytes, cipherdata_bytes, associated_data_bytes, auth_tag_bytes);
const {plain_text, auth_ok } = result;
const notify_msg = JSON.parse(plain_text.toString());