import { decrypt, encrypt, generateKey } from "./index.ts"; export interface EncryptionKey { id: string; secret: string; active?: boolean; createdAt?: number; } export interface EncryptionKeyring { active(): EncryptionKey; get(id: string): EncryptionKey | undefined; keys(): EncryptionKey[]; rotate(key?: EncryptionKey): Promise; remove(id: string): boolean; } const KEY_ID = /^[A-Za-z0-9._-]{1,64}$/; function validateKey(key: EncryptionKey): void { if (!KEY_ID.test(key.id)) { throw new TypeError(`Invalid encryption key id: ${key.id}`); } if (!key.secret.trim()) { throw new TypeError(`Encryption key '${key.id}' has an empty secret`); } } export function createKeyring(initial: EncryptionKey[]): EncryptionKeyring { const values = new Map(); let activeCount = 0; for (const key of initial) { validateKey(key); if (values.has(key.id)) { throw new Error(`WRN-ENCRYPTION-KEYRING-DUPLICATE: ${key.id}`); } if (key.active) activeCount++; values.set(key.id, { ...key }); } if (!values.size) throw new Error("WRN-ENCRYPTION-KEYRING-EMPTY"); if (activeCount > 1) throw new Error("WRN-ENCRYPTION-KEYRING-MULTIPLE-ACTIVE"); const activeInternal = (): EncryptionKey => { const key = [...values.values()].find((entry) => entry.active) ?? [...values.values()].at(-1); if (!key) throw new Error("WRN-ENCRYPTION-ACTIVE-KEY-MISSING"); return key; }; return { active: () => ({ ...activeInternal() }), get(id) { const key = values.get(id); return key ? { ...key } : undefined; }, keys: () => [...values.values()].map((key) => ({ ...key })), async rotate(key) { const next = key ?? { id: `key-${Date.now().toString(36)}`, secret: await generateKey(), createdAt: Date.now(), }; validateKey(next); for (const current of values.values()) current.active = false; const stored = { ...next, active: true }; values.set(stored.id, stored); return { ...stored }; }, remove(id) { if (!values.has(id)) return false; if (values.size <= 1) { throw new Error("WRN-ENCRYPTION-KEYRING-LAST-KEY"); } if (activeInternal().id === id) { throw new Error("WRN-ENCRYPTION-KEYRING-ACTIVE-REMOVE"); } return values.delete(id); }, }; } /** Versioned payload: `wrn1..`. */ export async function seal(plaintext: string, keyring: EncryptionKeyring): Promise { const key = keyring.active(); return `wrn1.${key.id}.${await encrypt(plaintext, key.secret)}`; } export async function open(sealed: string, keyring: EncryptionKeyring): Promise { const match = /^wrn1\.([A-Za-z0-9._-]{1,64})\.(.+)$/.exec(sealed); if (!match) throw new Error("WRN-ENCRYPTION-PAYLOAD-VERSION"); const key = keyring.get(match[1]!); if (!key) throw new Error(`WRN-ENCRYPTION-KEY-NOT-FOUND: ${match[1]}`); return decrypt(match[2]!, key.secret); } export function sealedKeyId(sealed: string): string | null { return /^wrn1\.([A-Za-z0-9._-]{1,64})\./.exec(sealed)?.[1] ?? null; } export function needsRotation(sealed: string, keyring: EncryptionKeyring): boolean { return sealedKeyId(sealed) !== keyring.active().id; }