Move docs/ package mirror into document-navigator plugin
Relocate the ~6k-file docs tree from the repo root into plugins/document-navigator/docs/ so the plugin is self-contained and portable. docs/superpowers/ (specs and plans) stays at the repo root. Moved via cp+rm rather than git mv because directory rename() fails with EPERM on this WSL /mnt DrvFs mount; git rename-detection pairs the identical-content files at commit time. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
# encrypted-archive
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## 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<Uint8Array> {
|
||||
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.
|
||||
Reference in New Issue
Block a user