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 + 随机 IV | encryptData / 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) => string | data 先 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) |
⚠️
decryptBinarySSR 期调用会直接崩(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) => string | data / key 均为 hex;失败 console.error 并返回 '' |
encryptDynamic / decryptDynamic / generateRequestId(动态密钥)
request() 的底层依赖,与 PHP 后端的动态密钥协议兼容:
- 每次请求
generateRequestId()生成 UUID v4,同时作请求头与密钥派生参数。 - 最终 AES 密钥 = HMAC-SHA256(requestId 的十六进制字节, masterKey) —— 即便 masterKey 泄露,历史密文也不可重放重解。
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') => unknown | requestId 与 encoding 都必须与加密时一致 |
generateRequestId | () => string | RFC 4122 UUID v4,依赖 crypto.getRandomValues(浏览器 + Nitro 均可用) |
注意事项
- ECB 模式并非安全设计,仅为兼容旧后端保留;新能力请用 CBC 版本。
crypto.subtle(AES-GCM)迁移已搁置(framework#2 已关闭:后端加密不能换,待后端框架升级大版本时重新评估),但 ECB 函数仍勿在新业务中引入。 encryptDynamic/decryptDynamic是request()的底层依赖,业务代码不要直接调用,统一走request()。- 所有解密入口对错误一律 try/catch 吞异常返回空字符串,避免缓存格式漂移时抛错阻塞业务。