const SESSION_KEY_STORAGE_KEY = 'vncmail:session-encryption-key'; const ALGORITHM = 'AES-GCM'; let _available: boolean | null = null; export function isEncryptionAvailable(): boolean { if (_available !== null) return _available; try { if (typeof window === 'undefined') { _available = false; return false; } if (!window.crypto || !window.crypto.subtle) { _available = false; return false; } _available = true; return true; } catch { _available = false; return false; } } function getOrCreateSessionKey(): Promise { if (!isEncryptionAvailable()) return Promise.resolve(null); try { let raw = sessionStorage.getItem(SESSION_KEY_STORAGE_KEY); if (!raw) { const keyBytes = new Uint8Array(32); crypto.getRandomValues(keyBytes); raw = btoa(String.fromCharCode(...keyBytes)); sessionStorage.setItem(SESSION_KEY_STORAGE_KEY, raw); } const keyData = Uint8Array.from(atob(raw), (c) => c.charCodeAt(0)); return crypto.subtle.importKey('raw', keyData, { name: ALGORITHM }, false, [ 'encrypt', 'decrypt', ]); } catch { return Promise.resolve(null); } } let _cachedKey: CryptoKey | null | undefined; async function getKey(): Promise { if (_cachedKey !== undefined) return _cachedKey; _cachedKey = await getOrCreateSessionKey(); return _cachedKey; } function invalidateKey(): void { _cachedKey = undefined; } export async function encryptValue(plaintext: string): Promise { if (!isEncryptionAvailable()) { console.warn('[localStorage crypto] Web Crypto unavailable, storing in plaintext'); return plaintext; } const key = await getKey(); if (!key) { console.warn('[localStorage crypto] Failed to derive key, storing in plaintext'); return plaintext; } try { const iv = crypto.getRandomValues(new Uint8Array(12)); const encoded = new TextEncoder().encode(plaintext); const ciphertext = await crypto.subtle.encrypt({ name: ALGORITHM, iv }, key, encoded); const combined = new Uint8Array(iv.length + new Uint8Array(ciphertext).length); combined.set(iv); combined.set(new Uint8Array(ciphertext), iv.length); return btoa(String.fromCharCode(...combined)); } catch (err) { console.warn('[localStorage crypto] Encryption failed:', err); return plaintext; } } export async function decryptValue(ciphertext: string): Promise { if (!isEncryptionAvailable()) { return ciphertext; } const key = await getKey(); if (!key) { return ciphertext; } try { const combined = Uint8Array.from(atob(ciphertext), (c) => c.charCodeAt(0)); if (combined.length < 13) return null; const iv = combined.slice(0, 12); const data = combined.slice(12); const decrypted = await crypto.subtle.decrypt({ name: ALGORITHM, iv }, key, data); return new TextDecoder().decode(decrypted); } catch { return null; } } export function resetSessionKey(): void { try { sessionStorage.removeItem(SESSION_KEY_STORAGE_KEY); } catch { /* noop */ } invalidateKey(); }