4.4 KiB
4.4 KiB
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/unwrapKeyfromencryption/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. rotateKeyrewraps a single user's entry;rotateMasterKeyrewraps every entry atomically. Both zeroize old buffers via@suaveplan/crypto-utils'szeroize.- Versioned JSON-like
export/import. The export carrieskeyring_format_version: 1; unknown versions throwSHARE_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) withon(event, listener) => unsubscribe.
Usage: Basic Example
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
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)—masterKeydefaults to a fresh 32-byte CSPRNG buffer.hasKey(userId): booleanlistKeys(): UserId[]addKey(userId, key): Promise<void>— wraps and stores. Emitskey:added.getKey(userId): Promise<Uint8Array | undefined>removeKey(userId): Promise<void>— emitskey:removedonly when an entry is actually removed.rotateKey(userId, newKey): Promise<UserWrappedKey>— replaces the entry, emitskey:rotated, incrementsshare.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
masterKeyso the caller's buffer can be zeroized independently.rotateMasterKeywrites the new key into the same buffer slot viaUint8Array.setso external references to the slot are invalidated rather than left dangling. exportdeliberately produces a plain object (not a JSON string) so the caller can pipe it through their preferred serializer (e.g.serializefor tagged JSON). Validation lives inKeyRingExportSchema.- The hard cap
KEYRING_MAX_ENTRIES = 100_000matches the OpenSpec scalability table. Above this, the spec calls for sharded ring storage which is out of scope for this package. rotateKeyzeroizes the caller-suppliednewKeyon 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
aadForderivation uses ASCII text (drive-sharing/v1/keyring/<userId>) rather than raw bytes so the AAD is human-readable in audit logs that capture it.