/** * Encryption utilities for securing sensitive user data * Uses Web Crypto API with AES-GCM for authenticated encryption */ // Generate a key from a password using PBKDF2 async function deriveKey(password: string, salt: BufferSource): Promise { const encoder = new TextEncoder(); const keyMaterial = await crypto.subtle.importKey( "raw", encoder.encode(password), "PBKDF2", false, ["deriveBits", "deriveKey"] ); return crypto.subtle.deriveKey( { name: "PBKDF2", salt: salt, iterations: 100000, hash: "SHA-256", }, keyMaterial, { name: "AES-GCM", length: 256 }, false, ["encrypt", "decrypt"] ); } // Get or create encryption key from localStorage async function getEncryptionKey(): Promise { const STORAGE_KEY = "encryption_salt"; const PASSWORD_KEY = "encryption_password"; // Generate a unique password based on user's browser fingerprint // This creates a consistent key per browser/device const getBrowserFingerprint = (): string => { try { const canvas = document.createElement("canvas"); const ctx = canvas.getContext("2d"); if (ctx) { ctx.textBaseline = "top"; ctx.font = "14px 'Arial'"; ctx.textBaseline = "alphabetic"; ctx.fillStyle = "#f60"; ctx.fillRect(125, 1, 62, 20); ctx.fillStyle = "#069"; ctx.fillText("Browser fingerprint", 2, 15); ctx.fillStyle = "rgba(102, 204, 0, 0.7)"; ctx.fillText("Browser fingerprint", 4, 17); } const fingerprint = (canvas.toDataURL() || "") + (navigator.userAgent || "") + (navigator.language || "") + (screen.width || 0) + (screen.height || 0) + (new Date().getTimezoneOffset() || 0); return fingerprint; } catch (error) { // Fallback if canvas fingerprinting fails return (navigator.userAgent || "") + (navigator.language || "") + (screen.width || 0) + (screen.height || 0); } }; let salt = localStorage.getItem(STORAGE_KEY); let password = localStorage.getItem(PASSWORD_KEY); if (!salt || !password) { // Generate new salt and password const saltBytes = crypto.getRandomValues(new Uint8Array(16)); salt = Array.from(saltBytes) .map(b => b.toString(16).padStart(2, "0")) .join(""); password = getBrowserFingerprint(); localStorage.setItem(STORAGE_KEY, salt); localStorage.setItem(PASSWORD_KEY, password); } // Convert hex string back to Uint8Array const saltBytes = salt.match(/.{1,2}/g)?.map(byte => parseInt(byte, 16)) || []; const saltArray = new Uint8Array(saltBytes); return deriveKey(password, saltArray); } // Encrypt a string value export async function encryptValue(value: string): Promise { if (!value || typeof window === "undefined") return value; try { const key = await getEncryptionKey(); const encoder = new TextEncoder(); const data = encoder.encode(value); // Generate a random IV for each encryption const iv = crypto.getRandomValues(new Uint8Array(12)); const encrypted = await crypto.subtle.encrypt( { name: "AES-GCM", iv: iv, }, key, data ); // Combine IV and encrypted data const combined = new Uint8Array(iv.length + encrypted.byteLength); combined.set(iv); combined.set(new Uint8Array(encrypted), iv.length); // Convert to base64 for storage const binaryString = String.fromCharCode(...combined); return btoa(binaryString); } catch (error) { // If encryption fails, return original value (graceful degradation) return value; } } // Decrypt a string value export async function decryptValue(encryptedValue: string): Promise { if (!encryptedValue || typeof window === "undefined") return encryptedValue; try { const key = await getEncryptionKey(); // Decode from base64 const binaryString = atob(encryptedValue); const combined = Uint8Array.from(binaryString, c => c.charCodeAt(0)); // Extract IV and encrypted data const iv = combined.slice(0, 12); const encrypted = combined.slice(12); const decrypted = await crypto.subtle.decrypt( { name: "AES-GCM", iv: iv, }, key, encrypted ); const decoder = new TextDecoder(); return decoder.decode(decrypted); } catch (error) { // If decryption fails, try to return as-is (might be unencrypted legacy data) return encryptedValue; } } // Encrypt sensitive fields in a user object export async function encryptUserData(user: any): Promise { if (!user || typeof window === "undefined") return user; const encrypted = { ...user }; // Encrypt sensitive fields const sensitiveFields = ["first_name", "last_name", "phone_number", "email"]; for (const field of sensitiveFields) { if (encrypted[field]) { encrypted[field] = await encryptValue(String(encrypted[field])); } } return encrypted; } // Decrypt sensitive fields in a user object export async function decryptUserData(user: any): Promise { if (!user || typeof window === "undefined") return user; const decrypted = { ...user }; // Decrypt sensitive fields const sensitiveFields = ["first_name", "last_name", "phone_number", "email"]; for (const field of sensitiveFields) { if (decrypted[field]) { try { decrypted[field] = await decryptValue(String(decrypted[field])); } catch (error) { // If decryption fails, keep original value (might be unencrypted) } } } return decrypted; } // Check if a value is encrypted (heuristic check) function isEncrypted(value: string): boolean { // Encrypted values are base64 encoded and have a specific structure // This is a simple heuristic - encrypted values will be longer and base64-like if (!value || value.length < 20) return false; try { // Try to decode as base64 atob(value); // If it decodes successfully and is long enough, it's likely encrypted return value.length > 30; } catch { return false; } } // Smart encrypt/decrypt that handles both encrypted and unencrypted data export async function smartDecryptUserData(user: any): Promise { if (!user || typeof window === "undefined") return user; const decrypted = { ...user }; const sensitiveFields = ["first_name", "last_name", "phone_number", "email"]; for (const field of sensitiveFields) { if (decrypted[field] && typeof decrypted[field] === "string") { if (isEncrypted(decrypted[field])) { try { decrypted[field] = await decryptValue(decrypted[field]); } catch (error) { // Failed to decrypt field, keep original value } } // If not encrypted, keep as-is (backward compatibility) } } return decrypted; }