# `encryption/key-ring/key-ring.ts` _(no submodules)_ ## Purpose `KeyRing` is the multi-user wrapped-key store at the heart of `@suaveplan/drive-sharing`. It owns the partition master key, holds per-user data keys (DEKs) wrapped under that master key, and exposes the lifecycle operations needed to add, retrieve, rotate, remove, export, and import wrapped entries. Every operation emits a lifecycle event so callers can persist to durable storage and audit changes; events deliberately carry only `userId` (no key material). ## Features - AAD-bound wrap / unwrap via `wrapKey` / `unwrapKey` from `encryption/envelope`. The AAD is per-user (`drive-sharing/v1/keyring/`) so a wrapped key cannot be moved between users without producing an AEAD failure. - `rotateKey` rewraps a single user's entry; `rotateMasterKey` rewraps every entry atomically. Both zeroize old buffers via `@suaveplan/crypto-utils`'s `zeroize`. - Versioned JSON-like `export` / `import`. The export carries `keyring_format_version: 1`; unknown versions throw `SHARE_KEYRING_FORMAT_UNSUPPORTED`. - Hard-cap on entries (`KEYRING_MAX_ENTRIES = 100_000`) prevents pathological bulk-add denial-of-service. - Event bus (`key:added`, `key:removed`, `key:rotated`) with `on(event, listener) => unsubscribe`. ## Usage: Basic Example ```ts import { KeyRing } from "@suaveplan/drive-sharing/encryption/key-ring"; import { userId } from "@suaveplan/drive-sharing/types/common"; const ring = new KeyRing(); const dek = new Uint8Array(32).fill(0xab); await ring.addKey(userId("alice"), dek); const recovered = await ring.getKey(userId("alice")); ``` ## Usage: Advanced Example ```ts import { KeyRing } from "@suaveplan/drive-sharing/encryption/key-ring"; import { userId } from "@suaveplan/drive-sharing/types/common"; const masterKey = new Uint8Array(32).fill(0x01); const ring = new KeyRing({ masterKey }); const off = ring.on("key:rotated", ({ userId }) => { console.log("rotated:", userId); }); await ring.addKey(userId("alice"), new Uint8Array(32).fill(0x10)); await ring.rotateKey(userId("alice"), new Uint8Array(32).fill(0x20)); // Backup / restore round-trip. const blob = ring.export(); const restored = new KeyRing({ masterKey }); await restored.import(blob); off(); ``` ## API Reference ### `class KeyRing` - `constructor(config?: KeyRingConfig)` — `masterKey` defaults to a fresh 32-byte CSPRNG buffer. - `hasKey(userId): boolean` - `listKeys(): UserId[]` - `addKey(userId, key): Promise` — wraps and stores. Emits `key:added`. - `getKey(userId): Promise` - `removeKey(userId): Promise` — emits `key:removed` only when an entry is actually removed. - `rotateKey(userId, newKey): Promise` — replaces the entry, emits `key:rotated`, increments `share.key.rotate_total`. - `rotateMasterKey(newMasterKey): Promise` — rewraps every entry atomically. - `export(): { keyring_format_version: 1; entries: UserWrappedKey[] }` - `import(payload): Promise` — Zod-validated. - `on(event, listener): () => void` ### `interface UserWrappedKey` `{ userId, wrappedKey }`. The export shape stores arrays of these. ### `interface KeyRingConfig` `{ masterKey?: Uint8Array }`. Master key length must be exactly 32 bytes. ### Events - `key:added` — `{ userId }` - `key:removed` — `{ userId }` - `key:rotated` — `{ userId }` ## Implementation Notes - The constructor copies the supplied `masterKey` so the caller's buffer can be zeroized independently. `rotateMasterKey` writes the new key into the same buffer slot via `Uint8Array.set` so external references to the slot are invalidated rather than left dangling. - `export` deliberately produces a plain object (not a JSON string) so the caller can pipe it through their preferred serializer (e.g. `serialize` for tagged JSON). Validation lives in `KeyRingExportSchema`. - The hard cap `KEYRING_MAX_ENTRIES = 100_000` matches the OpenSpec scalability table. Above this, the spec calls for sharded ring storage which is out of scope for this package. - `rotateKey` zeroizes the caller-supplied `newKey` on success — this matches the spec's "no plaintext key in caller's heap after success" guarantee. If the caller wants to retain the bytes they should pass a copy. - The `aadFor` derivation uses ASCII text (`drive-sharing/v1/keyring/`) rather than raw bytes so the AAD is human-readable in audit logs that capture it.