Gaaising's Blog

记录WebCryptoAPI遇到的坑——JS实现兔小巢密文传递登录态

Category: Tech , JavaScript , Web Publish: Update: Words: 788 (3 min read)

这篇文章是记录一个我遇到的一个困扰了我整晚的坑,事情得从兔小巢的密文传递登录态说起。

最近写的一个项目想接入兔小巢,后端是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中被注释掉的一段代码,这是什么情况呢?

这里还是先引用一下兔小巢文档内对加密算法的说明:

  1. 算法模式: AES-128-CBC;

  2. 算法密钥key产品密钥, 不足16位则右侧用”=“补足;

  3. 算法初始向量iv : 产品ID+产品密钥, 不足16位则右侧用”=“补足;

  4. 算法选用 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].

引自:W3C Recommendation

结案了,不需要自己实现PKCS#7,但也无法指定其它填充方式,就这么简单

但是包括MDN文档在内的资料都没介绍这个标准😡(为什么呢?)

于是我之前就深深栽入这个坑里了🥲

This article is licensed under CC BY-NC-SA 4.0.