Initial Commit
This commit is contained in:
@@ -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.
|
||||
Reference in New Issue
Block a user