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 吞例外回傳空字串,避免快取格式漂移時拋錯阻塞業務。