/** * @bytelyst/field-encrypt — Envelope encryption * * DEK lifecycle: generate → wrap with MEK → store. * On use: load wrapped DEK → unwrap with MEK → cache → use for AES-GCM. */ import type { KeyProvider, DekStore, WrappedDek } from './types.js'; import { generateAesKey } from './aes-gcm.js'; import { DekCache } from './key-cache.js'; /** * Build a deterministic DEK ID from userId + context. * Format: `dek_{userId}_{context}` */ export function buildDekId(userId: string, context: string): string { return `dek_${userId}_${context}`; } /** * Get or create a DEK for the given scope. * * 1. Check cache → return if found * 2. Check DEK store → unwrap + cache if found * 3. Generate new DEK → wrap → store → cache → return */ export async function getOrCreateDek( dekId: string, keyProvider: KeyProvider, dekStore: DekStore, cache: DekCache ): Promise { // 1. Cache hit const cached = cache.get(dekId); if (cached) { cache.recordHit(); return cached; } cache.recordMiss(); // 2. DEK store hit — unwrap + cache const stored = await dekStore.get(dekId); if (stored) { const dek = await keyProvider.unwrapKey(stored.wrappedKey, stored.mekVersion); cache.set(dekId, dek); return dek; } // 3. Generate new DEK → wrap → store → cache const dek = generateAesKey(); const { wrappedKey, mekVersion } = await keyProvider.wrapKey(dek); const wrappedDek: WrappedDek = { dekId, wrappedKey, mekVersion, createdAt: new Date().toISOString(), }; await dekStore.put(wrappedDek); cache.set(dekId, dek); return dek; } /** * Re-wrap all DEKs after MEK rotation. * * Reads each wrapped DEK, unwraps with old MEK, wraps with new MEK, stores updated. */ export async function rewrapAllDeks( oldKeyProvider: KeyProvider, newKeyProvider: KeyProvider, dekStore: DekStore, cache: DekCache, onProgress?: (completed: number, total: number) => void ): Promise { const dekIds = await dekStore.listIds(); let completed = 0; for (const dekId of dekIds) { const stored = await dekStore.get(dekId); if (!stored) continue; // Unwrap with old MEK const rawDek = await oldKeyProvider.unwrapKey(stored.wrappedKey, stored.mekVersion); // Wrap with new MEK const { wrappedKey, mekVersion } = await newKeyProvider.wrapKey(rawDek); // Store updated wrapped DEK const updated: WrappedDek = { dekId, wrappedKey, mekVersion, createdAt: stored.createdAt, }; await dekStore.put(updated); // Invalidate cache entry so it gets re-unwrapped with new MEK next time cache.invalidate(dekId); completed++; onProgress?.(completed, dekIds.length); } return completed; }