这篇文章是记录一个我遇到的一个困扰了我整晚的坑,事情得从兔小巢的密文传递登录态说起。
最近写的一个项目想接入兔小巢,后端是node,由于使用企业微信的OAuth2登录,而兔小巢不支持企业微信登录(只支持微信和QQ),所以就需要给兔小巢传递一个自定义的登录态,但看了兔小巢官方文档,并未提供js或ts实现的示例。
所以这里我用ts写了一个简单的demo,抛砖引玉:
export const encryptTxcData = async ({
productId,
productPrivateKey,
userData,
}: {
productId: string
productPrivateKey: string
userData: Record<string, unknown>
}): Promise<string> => {
const key = productPrivateKey.padEnd(16, '=')
const iv = (productId + productPrivateKey).padEnd(16, '=')
const data = JSON.stringify(userData)
const enc = new TextEncoder()
const keyBuffer = enc.encode(key)
const ivBuffer = enc.encode(iv)
const dataBuffer = enc.encode(data)
// const blockSize = 16
// const paddingSize = blockSize - (dataBuffer.byteLength % blockSize)
// const paddedData = new Uint8Array(dataBuffer.byteLength + paddingSize)
// paddedData.set(dataBuffer)
// paddedData.fill(paddingSize, dataBuffer.byteLength)
const cryptoKey = await globalThis.crypto.subtle.importKey(
'raw',
keyBuffer,
{ name: 'AES-CBC', length: 128 },
false,
['encrypt']
)
const encryptedData = await globalThis.crypto.subtle.encrypt(
{ name: 'AES-CBC', iv: ivBuffer },
cryptoKey,
dataBuffer,
)
const encryptedString = String.fromCharCode(...new Uint8Array(encryptedData))
const base64EncryptedString = globalThis.btoa(encryptedString)
const result = base64EncryptedString.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
return result
}
这里我是用web crypto api的SubtleCrypto实现的,不依赖啥工具库,所以应该不挑运行环境,在node或deno设置浏览器都能跑,具体可以看看MDN文档对此的介绍。
其实生成密文的步骤在兔小巢的文档也讲的很清楚了,啥语言实现起来应该都不过二三十行,所以我也懒得放github了。
不过相信你也注意到了上面demo中被注释掉的一段代码,这是什么情况呢?
这里还是先引用一下兔小巢文档内对加密算法的说明:
算法模式: AES-128-CBC;
算法密钥
key:产品密钥, 不足16位则右侧用”=“补足;算法初始向量
iv:产品ID+产品密钥, 不足16位则右侧用”=“补足;算法选用 CBC 模式,数据采用 PKCS#7 填充:K 为秘钥字节数,Buf 为待加密的内容,N 为其字节数。Buf 需要被填充为 K 的整数倍。在 Buf 的尾部填充(K - N%K)个字节,每个字节的内容 是(K - N%K)。
其中提到了数据采用PKCS#7填充(PKCS7 - wikipedia),但是我看MDN文档没提及subtle.encrypt()如何设置数据填充方式,而且参考了文档里提供的python和php的demo都是自己实现了PKCS#7,所以,我理所当然地觉得用js也要自己实现PKCS#7,其实这倒也不难,是的,就是demo中注释掉那段。
然后我就玉玉了,根本跑不起来……🙃
直到看到stackoverflow上这条问题:What padding does window.crypto.subtle.encrypt use for AES-CBC
里面的高赞回答提到W3C标准已经指定了WebCryptoAPI的AES-CBC模式的默认填充方式就是PKCS#7
When operating in CBC mode, messages that are not exact multiples of the AES block size (16 bytes) can be padded under a variety of padding schemes. In the Web Crypto API, the only padding mode that is supported is that of PKCS#7, as described by Section 10.3, step 2, of [RFC2315].
结案了,不需要自己实现PKCS#7,但也无法指定其它填充方式,就这么简单
但是包括MDN文档在内的资料都没介绍这个标准😡(为什么呢?)
于是我之前就深深栽入这个坑里了🥲