| const DB_NAME = "pram-crypto"; |
| const STORE_NAME = "keys"; |
| const KEY_NAME = "pram-master-key"; |
|
|
| function openDB(): Promise<IDBDatabase> { |
| 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<Uint8Array | undefined> { |
| 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<void> { |
| 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<CryptoKey> { |
| 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<string> { |
| 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<string> { |
| 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; |
| } |
|
|