213 lines
5.4 KiB
TypeScript
213 lines
5.4 KiB
TypeScript
/**
|
|
* 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<string, CryptoKey>;
|
|
}
|
|
|
|
// ============================================================================
|
|
// EncryptedDocBridge
|
|
// ============================================================================
|
|
|
|
export class EncryptedDocBridge {
|
|
#crypto: DocCrypto;
|
|
#spaceKeys = new Map<string, CryptoKey>();
|
|
#initialized = false;
|
|
|
|
constructor() {
|
|
this.#crypto = new DocCrypto();
|
|
}
|
|
|
|
/**
|
|
* Initialize from WebAuthn PRF output.
|
|
* Call this after EncryptID authentication returns prfOutput.
|
|
*/
|
|
async initFromAuth(prfOutput: ArrayBuffer): Promise<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<CryptoKey> {
|
|
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<CryptoKey> {
|
|
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
|
|
}
|
|
}
|