AES-GCM是微信支付APIv3的加解密方案之一,定义可见rfc5116,v3使用的是aead_aes_256_gcm。稍微补充一个aead的的描述,aead加密方式与其他对称加密方式主要不同的地方就是:
它每一段密文必定有对应的校验码,通过核对校验码来判断密文是否完整。
APIv3回调通知和平台证书下载文档上有介绍AES-GCM的使用场景。nodejs原生crypto模块,在处理GCM模式解密时,从变更历史上看,Node11加入了强制校验auth_tag(authentication tag)长度规则,Node10目前全系列还没有合并这个向前兼容规则,详情可见 https://github.com/nodejs/node/pull/20039 。
先上一段测试用js代码,来复现 nodejs#20039 上连带反馈的问题:
const crypto = require('crypto')const decrypt = (ciphertext, key, iv, aad = '') => { const buf = Buffer.from(ciphertext, 'base64') const tag = buf.slice(-16) const payload = buf.slice(0, -16) const decipher = crypto.createDecipheriv( 'aes-256-gcm', key, iv ).setAuthTag(tag).setAAD(Buffer.from(aad)) return Buffer.concat([ decipher.update(payload, 'hex'), decipher.final() ]).toString('utf8') }const mockupIv = 'abcdef0123456789'const mockupKey = 'abcdef0123456789abcdef0123456789'try { decrypt('', mockupKey, mockupIv) } catch {}
上述代码,在node10.15-10.24,均抛出如下不可捕获的错误(fatal error),程序会直接挂掉,在12-15之间,可以正常运行。
类似如下:
node[97219]: ../src/node_crypto.cc:3047:CipherBase::UpdateResult node::crypto::CipherBase::Update(const char *, int, unsigned char **, int *): Assertion `MaybePassAuthTagToOpenSSL()' failed. 1: 0x100d69661 node::Abort() (.cold.1) [/Users/james/.nvm/versions/node/v10.24.0/bin/node] 2: 0x10003aeb4 node_module_register [/Users/james/.nvm/versions/node/v10.24.0/bin/node] 3: 0x100039fb9 node::AddEnvironmentCleanupHook(v8::Isolate*, void (*)(void*), void*) [/Users/james/.nvm/versions/node/v10.24.0/bin/node] 4: 0x100112fae node::StringBytes::InlineDecoder::Decode(node::Environment*, v8::Local<v8::String>, v8::Local<v8::Value>, node::encoding) [/Users/james/.nvm/versions/node/v10.24.0/bin/node] 5: 0x1001119dc node::crypto::CipherBase::Update(v8::FunctionCallbackInfo<v8::Value> const&) [/Users/james/.nvm/versions/node/v10.24.0/bin/node] 6: 0x1002386c3 v8::internal::FunctionCallbackArguments::Call(v8::internal::CallHandlerInfo*) [/Users/james/.nvm/versions/node/v10.24.0/bin/node] 7: 0x100237bae v8::internal::MaybeHandle<v8::internal::Object> v8::internal::(anonymous namespace)::HandleApiCallHelper<false>(v8::internal::Isolate*, v8::internal::Handle<v8::internal::HeapObject>, v8::internal::Handle<v8::internal::HeapObject>, v8::internal::Handle<v8::internal::FunctionTemplateInfo>, v8::internal::Handle<v8::internal::Object>, v8::internal::BuiltinArguments) [/Users/james/.nvm/versions/node/v10.24.0/bin/node] 8: 0x10023728a v8::internal::Builtin_Impl_HandleApiCall(v8::internal::BuiltinArguments, v8::internal::Isolate*) [/Users/james/.nvm/versions/node/v10.24.0/bin/node] 9: 0x37d3d8d5bf3d 10: 0x37d3d8d118d5 11: 0x37d3d8d0a5c3 12: 0x37d3d8d118d5 13: 0x37d3d8d0a5c3 [1] 97218 abort npm test
上述错误日志,发生在我本地的Node10环境中。我花了几个小时,翻了好几遍github issues,最后找到了 nodejs#20039 pull requests,通读下来并反复测试了10.19-10.24版本,均无法正常捕获,这应该是上述pr没合并至Node10系列所致。
稍微分析一下,可能产生致命错误的条件:
密文为空字符串时,程序会崩
密文为 Cg==(base64空字符串) CLI会有 Warning DEP0090 弹出
(node:987) [DEP0090] DeprecationWarning: Permitting authentication tag lengths of 1 bytes is deprecated. Valid GCM tag lengths are 4, 8, 12, 13, 14, 15, 16.
微信支付官方文档在解密示例代码 常量定义了这个auth_tag长度为128位16字节,匹配rfc5116规范并且取的是最大值。
这下问题来了,万一无法正常获取到待解密字符串或者获取到的是空字符串,GCM模式校验码位又必须是16字节,业务逻辑又强依赖解密后字符串(验签证书是v3通讯强依赖)这崩掉了,着急上火的可真就是摊上事儿了!
找到问题关键点,那就打个业务逻辑补丁:应用端,对输入待解密字符串,做长度校验,长度为0的,不进入解密函数;或者可以采用如下向前兼容js patch补丁:
- ).setAuthTag(tag).setAAD(Buffer.from(aad))+ )++ // Restrict valid GCM tag length, patches for Node < 11.0.0+ // more @see https://github.com/nodejs/node/pull/20039+ const tagLen = tag.length+ if (tagLen > 16 || (tagLen < 12 && tagLen != 8 && tagLen != 4)) {+ let backport = new TypeError(`Invalid authentication tag length: ${tagLen}`)+ backport.code = 'ERR_CRYPTO_INVALID_AUTH_TAG'+ throw backport+ }+ decipher.setAuthTag(tag).setAAD(Buffer.from(aad))
上述代码取自 wechatpay-axios-plugin@aa36a56,也已随源码用例覆盖Node10-15版本,均达预期,可安全使用。
小程序云开发标配目前是Node10,不清楚云开发团队在处理消息通知及关键信息解密时,是否采用的是轻量化如nodejs原生crypto这样的解决方案,这个就需要云产品团队相关的同学进来看看,评估一下有无风险点了。
对自主对接云开发的开发者来说,建议尽快给打下业务逻辑补丁或者程序解密补丁,避免不可预期的错误发生(虽然极小概率,但支付的事,可真不是小事儿)。
建议云开发平台,能够升级一下Node10至最新lts运行时,一并建议能同时支持Node12、Node14运行时。
本文由作者授权转载
欢迎大家关注作者的专栏,作者是微信支付方面的专家大佬。
https://developers.weixin.qq.com/community/personal/oCJUsw8rTnF0BuXMAW7DKGiYN_i4