aes Encryption
The framework ships two groups of AES utilities: aes (fixed key, serving local caches and .bnc encrypted assets) and aes-dynamic (dynamically derived keys, serving the unified request encryption). 8 exports in total, all auto-imported.
⚠️ Encryption ≠ security barrier: the keys used by these utilities (
cacheAesKey/apiKey) live inruntimeConfig.public.*and are injected into the client bundle — one line of JS in DevTools can read them. Client-side encryption is obfuscation only (against casual onlookers / low-effort scrapers); real security relies on HTTPS + backend authentication. See Client-Side Encryption Boundary.
Three Scenarios at a Glance
| Scenario | Mode | Functions |
|---|---|---|
| Local caches (cookie / localStorage / sessionStorage) | AES-CBC + random IV | encryptData / decryptData |
.bnc encrypted images / text, WebSocket messages | AES-ECB (legacy-backend compat) | decryptBase64 / decryptBinary / decryptHex |
Unified request encryption (under request()) | AES-CBC + HMAC-SHA256 dynamic key | encryptDynamic / decryptDynamic / generateRequestId |
Most business code does not need to call these directly: caching goes through useLocal / useSession (transparent encryption built in), HTTP goes through request(), and .bnc images go through SwImage / useDecryptedAsset (internally guarded). Direct calls mainly appear in custom encrypted-asset loading and similar extension scenarios.
encryptData / decryptData (local cache, CBC)
CBC roundtrip (random IV, ciphertext differs each time)
A random 16-byte IV is prepended to the ciphertext and Base64-encoded together, so every encryption yields a different result. When decryption fails (cache format drift, wrong key), decryptData silently returns an empty string '' instead of throwing — a one-off cache simply expires naturally.
// Encrypt an object → Base64 string
const ciphertext = encryptData({ uid: 123, name: 'alice' }, cacheAesKey)
// Decrypt → original object ('' on failure)
const user = decryptData(ciphertext, cacheAesKey)
| Function | Signature | Notes |
|---|---|---|
encryptData | (data: object, key: string) => string | data is JSON.stringify-ed first; key is a UTF-8 key |
decryptData | (data: string, key: string) => string | First 16 bytes are the IV; silently returns '' on failure |
decryptBase64 / decryptBinary (.bnc assets, ECB)
Both decrypt .bnc encrypted assets; they differ in the performance path:
decryptBase64(base64, key)— base64 in, base64 out, kept for the legacy flow.decryptBinary(buffer, key)— operates onArrayBufferdirectly, built on asmcrypto.js (asm.js), 3–5× faster than CryptoJS with lower memory usage; this is whatSwImageuses in performance-sensitive paths.
// High-performance path: fetch → decrypt → 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' }))
| Function | Signature | Notes |
|---|---|---|
decryptBase64 | (base64: string, key: string) => string | console.error + returns '' on failure |
decryptBinary | (buffer: ArrayBuffer, key: string) => ArrayBuffer | Client only (depends on TextEncoder / asmcrypto) |
⚠️ Calling
decryptBinaryduring SSR crashes (AES-001). It must be wrapped inonMounted/ anIS_CLIENTguard / a client-only component:// ❌ Synchronous call at setup top level — crashes during SSR const plain = decryptBinary(await res.arrayBuffer(), imgKey) // ✅ Call inside onMounted onMounted(async () => { const plain = decryptBinary(await res.arrayBuffer(), imgKey) })
decryptHex (WebSocket messages, ECB)
Decrypts hex-format ciphertext (the key is hex too) and returns the JSON.parse-d value. WebSocketUtil uses it internally to decrypt Blob messages from the server; business code rarely calls it directly.
const payload = decryptHex(event.data, wsHexKey)
| Function | Signature | Notes |
|---|---|---|
decryptHex | (data: string, key: string) => string | Both data and key are hex; console.error + returns '' on failure |
encryptDynamic / decryptDynamic / generateRequestId (dynamic key)
The low-level dependency of request(), compatible with the PHP backend's dynamic-key protocol:
- Each request calls
generateRequestId()to produce a UUID v4 that doubles as a request header and the key-derivation parameter. - The final AES key = HMAC-SHA256(hex bytes of requestId, masterKey) — even if the masterKey leaks, historical ciphertexts cannot be replayed and re-decrypted.
- With
zip: true, the plaintext is gzipped (pako) before encryption; the response side does the reverse.
// Sending an encrypted request (protocol illustration only;
// business code should always go through request(), never hand-roll this)
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 }
| Function | Signature | Notes |
|---|---|---|
encryptDynamic | (data: object, requestId: string, key: string, zip: boolean, encoding?: 'base64' | 'binary') => string | Uint8Array | IV sits at the head of the ciphertext. Overloaded on encoding: omitted / 'base64' returns string, 'binary' returns Uint8Array (you only get the union back when the encoding you pass is itself a union type) |
decryptDynamic | (data: string | ArrayBuffer | Uint8Array, requestId: string, key: string, zip: boolean, encoding?: 'base64' | 'binary') => unknown | Both requestId and encoding must match the ones used for encryption |
generateRequestId | () => string | RFC 4122 UUID v4; relies on crypto.getRandomValues (available in browsers + Nitro) |
Caveats
- ECB mode is not a secure design; it is kept only for legacy-backend compatibility — use the CBC variants for new capabilities. The
crypto.subtle(AES-GCM) migration has been shelved (framework#2 is closed: the backend encryption cannot change; to be re-evaluated when the backend framework ships a major upgrade), but still do not introduce the ECB functions in new business code. encryptDynamic/decryptDynamicare low-level dependencies ofrequest(); business code should not call them directly — always go throughrequest().- All decryption entry points swallow errors via try/catch and return an empty string, so cache-format drift never throws and blocks the business flow.