/** * Encryption Utilities * * Provides encryption/decryption for sensitive provider credentials * Uses Web Crypto API with AES-GCM encryption */ /** * Derives an encryption key from a user session token * Uses PBKDF2 for key derivation */ async function deriveKey( password: string, salt: Uint8Array ): Promise { const encoder = new TextEncoder(); const keyMaterial = await crypto.subtle.importKey( 'raw', encoder.encode(password), 'PBKDF2', false, ['deriveBits', 'deriveKey'] ); // Ensure we have an ArrayBuffer (not SharedArrayBuffer) const saltBuffer = salt.buffer instanceof ArrayBuffer ? salt.buffer : new ArrayBuffer(salt.byteLength); if (!(salt.buffer instanceof ArrayBuffer)) { const view = new Uint8Array(saltBuffer); view.set(salt); } return crypto.subtle.deriveKey( { name: 'PBKDF2', salt: saltBuffer, iterations: 100000, hash: 'SHA-256', }, keyMaterial, { name: 'AES-GCM', length: 256 }, false, ['encrypt', 'decrypt'] ); } /** * Generates a random salt */ function generateSalt(): Uint8Array { return crypto.getRandomValues(new Uint8Array(16)); } /** * Generates a random IV (Initialization Vector) */ function generateIV(): Uint8Array { return crypto.getRandomValues(new Uint8Array(12)); } /** * Encrypts provider credentials * * @param data - The data to encrypt (provider credentials) * @param userSessionToken - User's session token for key derivation * @returns Encrypted data with salt and IV prepended (base64 encoded) */ export async function encryptProvider( data: string, userSessionToken: string ): Promise { if (!crypto.subtle) { throw new Error('Web Crypto API not available'); } try { const salt = generateSalt(); const iv = generateIV(); const key = await deriveKey(userSessionToken, salt); const encoder = new TextEncoder(); const dataBuffer = encoder.encode(data); // Ensure we have an ArrayBuffer (not SharedArrayBuffer) const ivBuffer = iv.buffer instanceof ArrayBuffer ? iv.buffer : (() => { const buf = new ArrayBuffer(iv.byteLength); new Uint8Array(buf).set(iv); return buf; })(); const encrypted = await crypto.subtle.encrypt( { name: 'AES-GCM', iv: ivBuffer, }, key, dataBuffer ); // Combine salt (16 bytes) + iv (12 bytes) + encrypted data const combined = new Uint8Array(salt.length + iv.length + encrypted.byteLength); combined.set(salt, 0); combined.set(iv, salt.length); combined.set(new Uint8Array(encrypted), salt.length + iv.length); // Convert to base64 for storage return btoa(String.fromCharCode(...combined)); } catch (error) { throw new Error(`Encryption failed: ${error instanceof Error ? error.message : String(error)}`); } } /** * Decrypts provider credentials * * @param encryptedData - Base64 encoded encrypted data with salt and IV * @param userSessionToken - User's session token for key derivation * @returns Decrypted data (original provider credentials) */ export async function decryptProvider( encryptedData: string, userSessionToken: string ): Promise { if (!crypto.subtle) { throw new Error('Web Crypto API not available'); } if (!encryptedData || typeof encryptedData !== 'string' || encryptedData.trim() === '') { throw new Error('Encrypted data is empty or invalid'); } try { // Validate base64 format before decoding // Base64 can contain A-Z, a-z, 0-9, +, /, and = for padding // Remove whitespace that might have been added const cleaned = encryptedData.trim().replace(/\s/g, ''); const base64Regex = /^[A-Za-z0-9+/]*={0,2}$/; if (!base64Regex.test(cleaned)) { throw new Error('Invalid base64 format in encrypted data. The credentials may be corrupted. Please remove and re-add this provider.'); } // Decode from base64 (use cleaned version) let decoded: string; try { decoded = atob(cleaned); } catch (e) { throw new Error(`Base64 decoding failed: ${e instanceof Error ? e.message : String(e)}. The credentials may be corrupted. Please remove and re-add this provider.`); } if (decoded.length < 28) { throw new Error('Encrypted data is too short (missing salt/IV)'); } const combined = Uint8Array.from(decoded, (c) => c.charCodeAt(0)); // Extract salt (16 bytes), iv (12 bytes), and encrypted data const salt = combined.slice(0, 16); const iv = combined.slice(16, 28); const encrypted = combined.slice(28); const key = await deriveKey(userSessionToken, salt); // Ensure we have an ArrayBuffer (not SharedArrayBuffer) const ivBuffer = iv.buffer instanceof ArrayBuffer ? iv.buffer : (() => { const buf = new ArrayBuffer(iv.byteLength); new Uint8Array(buf).set(iv); return buf; })(); const decrypted = await crypto.subtle.decrypt( { name: 'AES-GCM', iv: ivBuffer, }, key, encrypted ); const decoder = new TextDecoder(); return decoder.decode(decrypted); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); const errorName = error instanceof Error ? error.name : 'Unknown'; // Provide more detailed error message if (!errorMessage || errorMessage.trim() === '') { throw new Error(`Decryption failed: Unknown error occurred (${errorName}). The credentials may be corrupted or encrypted with a different key. Please remove and re-add this provider.`); } throw new Error(`Decryption failed: ${errorMessage}. The credentials may be corrupted. Please remove and re-add this provider.`); } } /** * Checks if Web Crypto API is available */ export function isEncryptionAvailable(): boolean { return typeof crypto !== 'undefined' && !!crypto.subtle; }