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