当前位置: 首页 > 工具软件 > node-gcm > 使用案例 >

微信支付v3 AEAD_AES_256_GCM解密JS版本代码及验证 javascript

厍光霁
2023-12-01

最近因为在写微信支付相关的代码,所以不可避免的涉及到加密解密的问题。而很多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

AEAD_AES_256_GCM 加密算法解析

这个名称应该是分三部分看的

  • AES-256 加密算法:我们首先需要知道的是加密的一个过程,一个简单的加密满足如下的公式:密文 = 加密算法(密钥,明文)。也就是说密文是由加密算法、密钥、明文共同产生的,而AES指的是一种加密算法,256指的是密钥的长度为256位(32B)。所以,AES-256指的是使用AES算法和32字节长度密钥的一种加密方式,与之相似的还有AES-128和AES-192。
    参考链接:https://blog.csdn.net/qq_28205153/article/details/55798628
  • AES-256-GCM: 在加密算法中会存在一个问题,就是如何判定密文和密钥有没有被篡改。也就是说,中间人可以获取并修改密文,导致接收方解密后的明文出错,而所谓的GCM就是来解决这个问题的。
    • GCM解决问题的方式是给加密完成的密文添加一个MAC值(Message Authentication Code),而消息验证码的产生是通过加密后的密文进行运算得来的,这样接收方可以通过验证MAC值是否相等来判断消息是否有篡改(如果看不懂请自行搜索Mac相关的知识查漏补缺)。
    • GCM需要一个计数器和一个附加消息来完成算法,算法的具体细节可以参考下方连接。并且,这个计数器需要一个初始值向量来进行初始化,这就使得我们在加密的过程中,不仅仅需要有一个密钥,还需要一个计数器的初始化向量(Initialization vector , iv)和一个附加数据。
      参考链接:https://blog.csdn.net/andylau00j/article/details/79269303
  • AEAD-AES-256-GCM:AEAD(Authenticated Encryption with Associated Data)指的是一种加密形式,如果一个加密算法可以同时完成加密验证加密解密过程的结果是否正确这两个操作,那么他就是一种AEAD的算法。上面说的AES-256-GCM就是一种AEAD加密算法,所以在我的浅薄理解中,AEAD-AES-256-GCM和AES-256-GCM应该是一种东西吧。(不确定,欢迎讨论)
    参考链接:https://zhuanlan.zhihu.com/p/28566058

node-aes-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 } 

参数解释:

  • key:密钥,支持16, 24, 32位,对应我们上述的AES-128, AES-192, AES-256 算法。在微信支付中就是商户平台上的32位密钥。
  • iv:初始化GCM算法计数器的向量(initialization vector),在参数中即随机串nonce
  • plaintext:需要加密的明文,即我们需要获取的数据
  • add:GCM算法中使用的附加数据,即微信支付参数中的associated_data
    而关于这个库中的密文和微信支付中的密文表达的含义是不一样的。我们上面提到AES-256-GCM算法其实是在加密后产生了一个MAC值用于验证密钥和密文的可靠性,然后将其与密文拼接起来。我们可以观察到的是,node-ase-gcm库的加密算法产生了两个返回值,一个是ciphertext,一个是auth_tag,这两个就对应着加密后的密文和MAC值;而微信支付中的resource.ciphertext只有一个密文参数,这个参数是将密文和MAC拼接起来的结果。所以:resource.ciphertext = ciphertext + auth_tag

实验验证

因为微信官方并没有给出JS的实现方法,所以我这里使用python的实现示例进行比较验证。实验思路为:

  • 使用JS库进行加密,得到密文和MAC
  • 拼接起来之后使用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());
 类似资料: