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 in runtimeConfig.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

ScenarioModeFunctions
Local caches (cookie / localStorage / sessionStorage)AES-CBC + random IVencryptData / decryptData
.bnc encrypted images / text, WebSocket messagesAES-ECB (legacy-backend compat)decryptBase64 / decryptBinary / decryptHex
Unified request encryption (under request())AES-CBC + HMAC-SHA256 dynamic keyencryptDynamic / 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)
FunctionSignatureNotes
encryptData(data: object, key: string) => stringdata is JSON.stringify-ed first; key is a UTF-8 key
decryptData(data: string, key: string) => stringFirst 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 on ArrayBuffer directly, built on asmcrypto.js (asm.js), 3–5× faster than CryptoJS with lower memory usage; this is what SwImage uses 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' }))
FunctionSignatureNotes
decryptBase64(base64: string, key: string) => stringconsole.error + returns '' on failure
decryptBinary(buffer: ArrayBuffer, key: string) => ArrayBufferClient only (depends on TextEncoder / asmcrypto)

⚠️ Calling decryptBinary during SSR crashes (AES-001). It must be wrapped in onMounted / an IS_CLIENT guard / 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)
FunctionSignatureNotes
decryptHex(data: string, key: string) => stringBoth 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:

  1. Each request calls generateRequestId() to produce a UUID v4 that doubles as a request header and the key-derivation parameter.
  2. The final AES key = HMAC-SHA256(hex bytes of requestId, masterKey) — even if the masterKey leaks, historical ciphertexts cannot be replayed and re-decrypted.
  3. 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 }
FunctionSignatureNotes
encryptDynamic(data: object, requestId: string, key: string, zip: boolean, encoding?: 'base64' | 'binary') => string | Uint8ArrayIV 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') => unknownBoth requestId and encoding must match the ones used for encryption
generateRequestId() => stringRFC 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 / decryptDynamic are low-level dependencies of request(); business code should not call them directly — always go through request().
  • 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.