const DB_NAME = "pram-crypto"; const STORE_NAME = "keys"; const KEY_NAME = "pram-master-key"; function openDB(): Promise { return new Promise((resolve, reject) => { const request = indexedDB.open(DB_NAME, 1); request.onerror = () => reject(request.error); request.onsuccess = () => resolve(request.result); request.onupgradeneeded = () => { const db = request.result; if (!db.objectStoreNames.contains(STORE_NAME)) { db.createObjectStore(STORE_NAME); } }; }); } async function getFromStore(db: IDBDatabase, key: string): Promise { return new Promise((resolve, reject) => { const tx = db.transaction(STORE_NAME, "readonly"); const store = tx.objectStore(STORE_NAME); const request = store.get(key); request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error); }); } async function putToStore(db: IDBDatabase, key: string, value: Uint8Array): Promise { return new Promise((resolve, reject) => { const tx = db.transaction(STORE_NAME, "readwrite"); const store = tx.objectStore(STORE_NAME); const request = store.put(value, key); request.onsuccess = () => resolve(); request.onerror = () => reject(request.error); }); } async function getOrCreateMasterKey(): Promise { const db = await openDB(); const stored = await getFromStore(db, KEY_NAME); if (stored) { return crypto.subtle.importKey( "raw", stored.buffer as ArrayBuffer, { name: "AES-GCM" }, false, ["encrypt", "decrypt"], ); } const key = await crypto.subtle.generateKey( { name: "AES-GCM", length: 256 }, true, ["encrypt", "decrypt"], ); const exported = await crypto.subtle.exportKey("raw", key); await putToStore(db, KEY_NAME, new Uint8Array(exported)); return key; } interface EncryptedData { iv: string; data: string; } export async function encryptText(plaintext: string): Promise { const key = await getOrCreateMasterKey(); const iv = crypto.getRandomValues(new Uint8Array(12)); const encoded = new TextEncoder().encode(plaintext); const ciphertext = await crypto.subtle.encrypt( { name: "AES-GCM", iv }, key, encoded, ); const combined: EncryptedData = { iv: arrayBufferToBase64(iv), data: arrayBufferToBase64(ciphertext), }; return JSON.stringify(combined); } export async function decryptText(encrypted: string): Promise { const key = await getOrCreateMasterKey(); const parsed: EncryptedData = JSON.parse(encrypted); const iv = base64ToArrayBuffer(parsed.iv); const ciphertext = base64ToArrayBuffer(parsed.data); const decrypted = await crypto.subtle.decrypt( { name: "AES-GCM", iv }, key, ciphertext, ); return new TextDecoder().decode(decrypted); } function arrayBufferToBase64(buffer: ArrayBuffer | Uint8Array): string { const bytes = new Uint8Array(buffer); let binary = ""; for (let i = 0; i < bytes.byteLength; i++) { binary += String.fromCharCode(bytes[i]); } return btoa(binary); } function base64ToArrayBuffer(base64: string): ArrayBuffer { const binary = atob(base64); const bytes = new Uint8Array(binary.length); for (let i = 0; i < binary.length; i++) { bytes[i] = binary.charCodeAt(i); } return bytes.buffer; }