/** * EncryptID → DocCrypto Bridge * * Connects the EncryptID WebAuthn PRF output to the local-first * DocCrypto layer for client-side document encryption. * * Data flow: * WebAuthn PRF output * → DocCrypto.initFromPRF() * → deriveSpaceKey(spaceId) * → deriveDocKey(spaceKey, docId) * → AES-256-GCM encrypt/decrypt * * Usage: * import { EncryptedDocBridge } from './encryptid-bridge'; * * const bridge = new EncryptedDocBridge(); * * // After EncryptID authentication (PRF output available): * await bridge.initFromAuth(authResult.prfOutput); * * // Pass to EncryptedDocStore for a space: * const store = new EncryptedDocStore(spaceSlug, bridge.getDocCrypto()); * * // On sign-out: * bridge.clear(); */ import { DocCrypto } from './crypto'; // ============================================================================ // TYPES // ============================================================================ export interface BridgeState { initialized: boolean; /** Cached space keys for quick doc key derivation */ spaceKeys: Map; } // ============================================================================ // EncryptedDocBridge // ============================================================================ export class EncryptedDocBridge { #crypto: DocCrypto; #spaceKeys = new Map(); #initialized = false; constructor() { this.#crypto = new DocCrypto(); } /** * Initialize from WebAuthn PRF output. * Call this after EncryptID authentication returns prfOutput. */ async initFromAuth(prfOutput: ArrayBuffer): Promise { await this.#crypto.initFromPRF(prfOutput); this.#spaceKeys.clear(); this.#initialized = true; } /** * Initialize from raw key material (e.g., from EncryptIDKeyManager's * encryption key or any 256-bit key). */ async initFromKey(key: CryptoKey | Uint8Array | ArrayBuffer): Promise { await this.#crypto.init(key); this.#spaceKeys.clear(); this.#initialized = true; } /** * Get the DocCrypto instance for use with EncryptedDocStore. * Returns null if not initialized. */ getDocCrypto(): DocCrypto | undefined { return this.#initialized ? this.#crypto : undefined; } /** * Pre-derive and cache a space key for faster doc key derivation. */ async warmSpaceKey(spaceId: string): Promise { if (!this.#initialized) return; if (!this.#spaceKeys.has(spaceId)) { const key = await this.#crypto.deriveSpaceKey(spaceId); this.#spaceKeys.set(spaceId, key); } } /** * Get a cached space key (or derive it). */ async getSpaceKey(spaceId: string): Promise { if (!this.#initialized) throw new Error('Bridge not initialized'); let key = this.#spaceKeys.get(spaceId); if (!key) { key = await this.#crypto.deriveSpaceKey(spaceId); this.#spaceKeys.set(spaceId, key); } return key; } /** * Derive a document key directly (convenience for one-off operations). */ async deriveDocKey(spaceId: string, docId: string): Promise { const spaceKey = await this.getSpaceKey(spaceId); return this.#crypto.deriveDocKey(spaceKey, docId); } /** * Check if the bridge is ready for encryption operations. */ get isInitialized(): boolean { return this.#initialized; } /** * Clear all key material from memory. Call on sign-out. */ clear(): void { this.#crypto.clear(); this.#spaceKeys.clear(); this.#initialized = false; } } // ============================================================================ // Singleton for app-wide access // ============================================================================ let _bridge: EncryptedDocBridge | null = null; /** * Get the global EncryptedDocBridge singleton. */ export function getDocBridge(): EncryptedDocBridge { if (!_bridge) { _bridge = new EncryptedDocBridge(); } return _bridge; } /** * Reset the global bridge (e.g., on sign-out). */ export function resetDocBridge(): void { if (_bridge) { _bridge.clear(); _bridge = null; } } // ============================================================================ // Helper: check if a space supports client-side encryption // ============================================================================ /** * Check localStorage for whether the user has encryption enabled for a space. * This is set by the space encryption toggle in the UI. */ export function isSpaceEncryptionEnabled(spaceSlug: string): boolean { try { return localStorage.getItem(`rspace:${spaceSlug}:encrypted`) === 'true'; } catch { return false; } } /** * Set the client-side encryption flag for a space. */ export function setSpaceEncryptionEnabled(spaceSlug: string, enabled: boolean): void { try { if (enabled) { localStorage.setItem(`rspace:${spaceSlug}:encrypted`, 'true'); } else { localStorage.removeItem(`rspace:${spaceSlug}:encrypted`); } } catch { // localStorage unavailable (SSR, etc.) } } /** * Check if encrypted backup is enabled globally. */ export function isEncryptedBackupEnabled(): boolean { try { return localStorage.getItem('encryptid_backup_enabled') === 'true'; } catch { return false; } } /** * Toggle encrypted backup flag. */ export function setEncryptedBackupEnabled(enabled: boolean): void { try { if (enabled) { localStorage.setItem('encryptid_backup_enabled', 'true'); } else { localStorage.removeItem('encryptid_backup_enabled'); } } catch { // localStorage unavailable } }