aes 加解密

framework 提供两组 AES 工具:aes(固定密钥,服务本地缓存与 .bnc 加密资源)与 aes-dynamic(动态派生密钥,服务统一请求加密)。共 8 个导出,全部 auto-import、无需手动引入。

⚠️ 加密 ≠ 安全屏障:这些工具的密钥(cacheAesKey / apiKey)都在 runtimeConfig.public.*,会被注入客户端 bundle,DevTools 一行 JS 可读。客户端加密仅作 obfuscation(防旁人偶然看见 / 防 low-effort 爬虫),真正的安全靠 HTTPS + 后端鉴权。详见客户端加密的安全边界。

三套场景速查

场景模式函数
本地缓存(cookie / localStorage / sessionStorage)AES-CBC + 随机 IVencryptData / decryptData
.bnc 加密图片 / 文本、WebSocket 消息AES-ECB(兼容旧后端)decryptBase64 / decryptBinary / decryptHex
统一请求加密(request() 底层)AES-CBC + HMAC-SHA256 动态密钥encryptDynamic / decryptDynamic / generateRequestId

多数业务不需要直接调这些函数:缓存走 useLocal / useSession(内部已透明加解密)、请求走 request()、.bnc 图片走 SwImage / useDecryptedAsset(内部已守卫)。直接调用主要出现在自定义加密资源加载等扩展场景。

encryptData / decryptData(本地缓存,CBC)

CBC roundtrip(随机 IV,密文每次不同)

加载中...

随机 IV(16 字节)拼接在密文头部一并 Base64 编码,每次加密结果不同。decryptData 解密失败(缓存格式漂移、密钥不符)时静默返回空字符串 '',不抛错 —— 一次性缓存自然失效即可。

// 加密对象 → Base64 字符串
const ciphertext = encryptData({ uid: 123, name: 'alice' }, cacheAesKey)

// 解密 → 原始对象(失败返回 '')
const user = decryptData(ciphertext, cacheAesKey)
函数签名说明
encryptData(data: object, key: string) => stringdata 先 JSON.stringify 再加密;key 为 UTF-8 密钥
decryptData(data: string, key: string) => string头 16 字节为 IV;失败静默返回 ''

decryptBase64 / decryptBinary(.bnc 资源,ECB)

两者都解密 .bnc 加密资源,区别在性能路径:

  • decryptBase64(base64, key) — base64 进 base64 出,旧流程兼容用。
  • decryptBinary(buffer, key) — 直接操作 ArrayBuffer,基于 asmcrypto.js 的 asm.js 实现,比 CryptoJS 快 3-5 倍且内存占用更低,SwImage 等性能敏感场景用的就是它。
// 高性能路径:fetch → 解密 → blob URL
const res = await fetch('/uploads/cover.bnc')
const plain = decryptBinary(await res.arrayBuffer(), imgKey)
const url = URL.createObjectURL(new Blob([plain], { type: 'image/webp' }))
函数签名说明
decryptBase64(base64: string, key: string) => string失败 console.error 并返回 ''
decryptBinary(buffer: ArrayBuffer, key: string) => ArrayBuffer仅客户端可用(依赖 TextEncoder / asmcrypto)

⚠️ decryptBinary SSR 期调用会直接崩(AES-001)。必须包在 onMounted / IS_CLIENT 守卫 / client-only 组件内:

// ❌ setup 顶层同步调用,SSR 期崩
const plain = decryptBinary(await res.arrayBuffer(), imgKey)

// ✅ onMounted 内调用
onMounted(async () => {
  const plain = decryptBinary(await res.arrayBuffer(), imgKey)
})

decryptHex(WebSocket 消息,ECB)

解密 hex 格式密文(密钥也是 hex),返回 JSON.parse 后的值。WebSocketUtil 内部用它解密服务端 Blob 消息,业务一般不直接调。

const payload = decryptHex(event.data, wsHexKey)
函数签名说明
decryptHex(data: string, key: string) => stringdata / key 均为 hex;失败 console.error 并返回 ''

encryptDynamic / decryptDynamic / generateRequestId(动态密钥)

request() 的底层依赖,与 PHP 后端的动态密钥协议兼容:

  1. 每次请求 generateRequestId() 生成 UUID v4,同时作请求头与密钥派生参数。
  2. 最终 AES 密钥 = HMAC-SHA256(requestId 的十六进制字节, masterKey) —— 即便 masterKey 泄露,历史密文也不可重放重解。
  3. zip: true 时明文先 gzip(pako)再加密,解密侧反之。
// 发送加密请求(仅展示协议;业务统一走 request(),不要手拼)
const requestId = generateRequestId()
const body = encryptDynamic({ page: 1, size: 20 }, requestId, apiKey, true)
const rawText = await $fetch('/api/list', { method: 'POST', body, headers: { requestId } })
const resp = decryptDynamic(rawText, requestId, apiKey, true) as { status: 'y' | 'n'; data: unknown }
函数签名说明
encryptDynamic(data: object, requestId: string, key: string, zip: boolean, encoding?: 'base64' | 'binary') => string | Uint8Array密文 IV 在头部。已按 encoding 重载:缺省 / 'base64' 返回 string,'binary' 返回 Uint8Array(只有传入的 encoding 本身是联合类型时才拿到联合返回)
decryptDynamic(data: string | ArrayBuffer | Uint8Array, requestId: string, key: string, zip: boolean, encoding?: 'base64' | 'binary') => unknownrequestId 与 encoding 都必须与加密时一致
generateRequestId() => stringRFC 4122 UUID v4,依赖 crypto.getRandomValues(浏览器 + Nitro 均可用)

注意事项

  • ECB 模式并非安全设计,仅为兼容旧后端保留;新能力请用 CBC 版本。crypto.subtle(AES-GCM)迁移已搁置(framework#2 已关闭:后端加密不能换,待后端框架升级大版本时重新评估),但 ECB 函数仍勿在新业务中引入。
  • encryptDynamic / decryptDynamic 是 request() 的底层依赖,业务代码不要直接调用,统一走 request()。
  • 所有解密入口对错误一律 try/catch 吞异常返回空字符串,避免缓存格式漂移时抛错阻塞业务。