# encrypted-archive _(no submodules)_ ## Purpose Wire format for AES-256-GCM-encrypted packed archives served alongside unencrypted bundles. Adopts the stable magic prefix `SP-ENC1` followed by a length-prefixed JSON header (`{ alg, keyId, nonce, aad? }`) and the GCM ciphertext+tag. The format keeps the encrypted path opt-in: bodies without the prefix flow through the loader unchanged, so consumers can ship a mixed-CDN of encrypted and unencrypted bundles without forking the loader. ## Features - Stable 8-byte magic prefix (`53 50 2D 45 4E 43 31 01`) detected via a cheap byte-equality check on the loader hot path. - Self-describing header: `alg`, `keyId`, base64-encoded `nonce`, and an optional `aad`. No fragile binary layout for the header — JSON, with a hard 64 KiB cap to prevent malicious oversize headers from blowing the buffer. - AES-256-GCM via `@suaveplan/crypto`'s `aeadEncrypt`/`aeadDecrypt`. Tag mismatch surfaces as `PackerBrowserError(LOAD_FAILED)` with the underlying `CryptoError` attached as `cause` for forensic logging. - Pure functions — no implicit RNG, no implicit key derivation. The caller supplies key + nonce; this module just shapes the bytes. ## Usage: Basic Example ```ts import { decodeEncryptedArchiveHeader, decryptEncryptedArchive, encodeEncryptedArchive, } from "@suaveplan/packer-browser"; const key = new Uint8Array(32).fill(0xaa); const nonce = new Uint8Array(12).fill(0x01); const wire = encodeEncryptedArchive( new TextEncoder().encode("hello"), "tenant-a", key, nonce ); const decoded = decodeEncryptedArchiveHeader(wire); const plaintext = decryptEncryptedArchive(decoded, key); // → Uint8Array of "hello" ``` ## Usage: Advanced Example ```ts import { decodeEncryptedArchiveHeader, decryptEncryptedArchive, hasEncryptedArchiveMagic, MemoryKeyring, } from "@suaveplan/packer-browser"; const keyring = new MemoryKeyring({ "tenant-a": new Uint8Array(32).fill(0xaa), }); async function unwrap(fetched: Uint8Array): Promise { if (!hasEncryptedArchiveMagic(fetched)) return fetched; const decoded = decodeEncryptedArchiveHeader(fetched); const key = await keyring.resolveKey(decoded.header.keyId); return decryptEncryptedArchive(decoded, key); } ``` ## API Reference - `SP_ENC1_MAGIC: Uint8Array` — the 8-byte magic prefix. - `hasEncryptedArchiveMagic(bytes: Uint8Array): boolean` — detector. - `encodeEncryptedArchive(plaintext, keyId, key, nonce, opts?): Uint8Array` — encoder. `opts.aad?: Uint8Array` is bound into the GCM tag. - `decodeEncryptedArchiveHeader(bytes: Uint8Array): DecodedEncryptedArchive` — parses the header + slices the ciphertext. Throws `LOAD_FAILED` on a malformed frame. - `decryptEncryptedArchive(decoded, key): Uint8Array` — wraps `aeadDecrypt`. Throws `LOAD_FAILED` on tag mismatch with `cause` attached. ## Implementation Notes - The header length is encoded as a 4-byte big-endian unsigned integer. Big-endian is the on-wire convention used by `@suaveplan/packer`'s manifest decoder; matching it here keeps cross-package readers uniform. - The `aad` field, when present, MUST cover the same bytes at encrypt and decrypt time — typically a hash of the surrounding request URL or manifest hash. The wire format does NOT bind `aad` automatically; the caller decides whether to include it. - A 64 KiB header cap protects against pathological adversarial inputs that could trigger a large allocation before any AEAD work has run. Realistic headers are 200–400 bytes. - AES-256-GCM was chosen over XChaCha20-Poly1305 because the loader is expected to run in browsers where AES-NI is hardware-accelerated and Web Crypto can be plugged in via the `engine` option of `aeadEncrypt`/`aeadDecrypt` for further performance.