import { encodeBase64, decodeBase64 } from "@std/encoding"; /** * Encrypted data structure with key version support */ export interface EncryptedData { keyVersion: number; salt: string; iv: string; data: string; } /** * Generate a random salt (16 bytes recommended for PBKDF2) */ function generateSalt(): Uint8Array { return crypto.getRandomValues(new Uint8Array(16)); } /** * Generate a random IV (12 bytes recommended for AES-GCM) */ function generateIV(): Uint8Array { return crypto.getRandomValues(new Uint8Array(12)); } /** * Derive a key from a password using PBKDF2 */ async function deriveKey( password: string, salt: Uint8Array, keyVersion: number = 1 ): Promise { const encoder = new TextEncoder(); const keyMaterial = await crypto.subtle.importKey( "raw", encoder.encode(password), { name: "PBKDF2" }, false, ["deriveBits", "deriveKey"] ); // Adjust iterations based on key version (for future rotation) const iterations = keyVersion === 1 ? 100000 : 200000; return crypto.subtle.deriveKey( { name: "PBKDF2", salt: new Uint8Array(salt), iterations, hash: "SHA-256" }, keyMaterial, { name: "AES-GCM", length: 256 }, true, ["encrypt", "decrypt"] ); } /** * Encrypt data using AES-GCM with key version support */ export async function encrypt( plaintext: string, password:string, keyVersion: number = 1 ): Promise { const encoder = new TextEncoder(); const salt = generateSalt(); const iv = generateIV(); const key = await deriveKey(password, salt, keyVersion); const encrypted = await crypto.subtle.encrypt( { name: "AES-GCM", iv: new Uint8Array(iv) }, key, encoder.encode(plaintext) ); return { keyVersion, salt: encodeBase64(salt), iv: encodeBase64(iv), data: encodeBase64(new Uint8Array(encrypted)) }; } /** * Decrypt data using AES-GCM with key version support */ export async function decrypt(encryptedData: EncryptedData, password:string): Promise { const decoder = new TextDecoder(); const salt = decodeBase64(encryptedData.salt); const iv = decodeBase64(encryptedData.iv); const data = decodeBase64(encryptedData.data); const key = await deriveKey(password, salt, encryptedData.keyVersion); try { const decrypted = await crypto.subtle.decrypt( { name: "AES-GCM", iv: new Uint8Array(iv) }, key, data ); return decoder.decode(decrypted); } catch (_error) { throw new Error("Decryption failed: Invalid data or key"); } } /** * Generate a secure random encryption key */ export function generateEncryptionKey(): string { const bytes = crypto.getRandomValues(new Uint8Array(32)); return encodeBase64(bytes); }