export class ObjectEncryptor { // Private method to get key material from password private static async getKeyMaterial(password: string): Promise { const encoder = new TextEncoder(); return await crypto.subtle.importKey( "raw", encoder.encode(password), { name: "PBKDF2" }, false, ["deriveBits", "deriveKey"] ); } // Private method to derive encryption key private static async deriveKey( keyMaterial: CryptoKey, salt: Uint8Array ): Promise { return await crypto.subtle.deriveKey( { name: "PBKDF2", salt: salt, iterations: 100000, hash: "SHA-256", }, keyMaterial, { name: "AES-GCM", length: 256 }, true, ["encrypt", "decrypt"] ); } // Encrypt method for any serializable object static async encrypt(obj: T, password: string): Promise { // Generate a random salt and initialization vector const salt = crypto.getRandomValues(new Uint8Array(16)); const iv = crypto.getRandomValues(new Uint8Array(12)); // Convert object to JSON string const jsonString = JSON.stringify(obj); const encoder = new TextEncoder(); const data = encoder.encode(jsonString); // Derive the key const keyMaterial = await this.getKeyMaterial(password); const key = await this.deriveKey(keyMaterial, salt); // Encrypt the data const encryptedContent = await crypto.subtle.encrypt( { name: "AES-GCM", iv: iv }, key, data ); // Combine salt, iv, and encrypted data const encryptedData = new Uint8Array( salt.length + iv.length + encryptedContent.byteLength ); encryptedData.set(salt); encryptedData.set(iv, salt.length); encryptedData.set( new Uint8Array(encryptedContent), salt.length + iv.length ); // Convert to base64 for easy storage/transmission return btoa( Array.from(new Uint8Array(encryptedData)) .map((b) => String.fromCharCode(b)) .join("") ); } // Decrypt method that returns the original object type static async decrypt( encryptedBase64: string, password: string ): Promise { // Convert base64 back to Uint8Array const encryptedData = new Uint8Array( atob(encryptedBase64) .split("") .map((char) => char.charCodeAt(0)) ); // Extract salt and iv const salt = encryptedData.slice(0, 16); const iv = encryptedData.slice(16, 28); const data = encryptedData.slice(28); // Derive the key const keyMaterial = await this.getKeyMaterial(password); const key = await this.deriveKey(keyMaterial, salt); // Decrypt the data const decryptedContent = await crypto.subtle.decrypt( { name: "AES-GCM", iv: iv }, key, data ); // Convert decrypted data back to object const decoder = new TextDecoder(); return JSON.parse(decoder.decode(decryptedContent)); } }