first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,90 @@
# `encryption/key-ring/key-ring.ts`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## 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/<userId>`) 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<void>` — wraps and stores. Emits `key:added`.
- `getKey(userId): Promise<Uint8Array | undefined>`
- `removeKey(userId): Promise<void>` — emits `key:removed` only when an entry is actually removed.
- `rotateKey(userId, newKey): Promise<UserWrappedKey>` — replaces the entry, emits `key:rotated`, increments `share.key.rotate_total`.
- `rotateMasterKey(newMasterKey): Promise<void>` — rewraps every entry atomically.
- `export(): { keyring_format_version: 1; entries: UserWrappedKey[] }`
- `import(payload): Promise<void>` — 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/<userId>`) rather than raw bytes so the AAD is human-readable in audit logs that capture it.