import crypto from 'crypto'; /** * Encryption utilities using AES-256-GCM for local database encryption * Each user has a unique encryption key derived from their userId and a master secret */ const ALGORITHM = 'aes-256-gcm'; const KEY_LENGTH = 32; // 256 bits const IV_LENGTH = 16; // 128 bits const SALT_LENGTH = 64; const TAG_LENGTH = 16; export interface EncryptedData { encryptedData: string; iv: string; authTag: string; } /** * Generate a unique encryption key for a user * This key is generated once at first login and stored securely in electron-store * @param userId - The user's unique identifier * @returns Base64 encoded encryption key */ export function generateUserEncryptionKey(userId: string): string { // Generate a random salt for this user const salt = crypto.randomBytes(SALT_LENGTH); // Create a deterministic key based on userId and random salt // This ensures each user has a unique, strong key const key = crypto.pbkdf2Sync( userId, salt, 100000, // iterations KEY_LENGTH, 'sha512' ); // Combine salt and key for storage const combined = Buffer.concat([salt, key]); return combined.toString('base64'); } /** * Extract the actual encryption key from the stored combined salt+key * @param storedKey - Base64 encoded salt+key combination * @returns Encryption key buffer */ function extractKeyFromStored(storedKey: string): Buffer { const combined = Buffer.from(storedKey, 'base64'); // Extract key (last KEY_LENGTH bytes) return combined.subarray(SALT_LENGTH, SALT_LENGTH + KEY_LENGTH); } /** * Encrypt sensitive data using AES-256-GCM * @param data - Plain text data to encrypt * @param userKey - User's encryption key (base64) * @returns Encrypted data with IV and auth tag */ export function encrypt(data: string, userKey: string): EncryptedData { try { const key = extractKeyFromStored(userKey); const iv = crypto.randomBytes(IV_LENGTH); const cipher = crypto.createCipheriv(ALGORITHM, key, iv); let encrypted = cipher.update(data, 'utf8', 'hex'); encrypted += cipher.final('hex'); const authTag = cipher.getAuthTag(); return { encryptedData: encrypted, iv: iv.toString('hex'), authTag: authTag.toString('hex') }; } catch (error) { throw new Error(`Encryption failed: ${error instanceof Error ? error.message : 'Unknown error'}`); } } /** * Decrypt data encrypted with AES-256-GCM * @param encryptedData - Encrypted data object * @param userKey - User's encryption key (base64) * @returns Decrypted plain text */ export function decrypt(encryptedData: EncryptedData, userKey: string): string { try { const key = extractKeyFromStored(userKey); const iv = Buffer.from(encryptedData.iv, 'hex'); const authTag = Buffer.from(encryptedData.authTag, 'hex'); const decipher = crypto.createDecipheriv(ALGORITHM, key, iv); decipher.setAuthTag(authTag); let decrypted = decipher.update(encryptedData.encryptedData, 'hex', 'utf8'); decrypted += decipher.final('utf8'); return decrypted; } catch (error) { throw new Error(`Decryption failed: ${error instanceof Error ? error.message : 'Unknown error'}`); } } /** * Encrypt an object by converting it to JSON first * @param obj - Object to encrypt * @param userKey - User's encryption key * @returns Encrypted data */ export function encryptObject(obj: T, userKey: string): EncryptedData { const jsonString = JSON.stringify(obj); return encrypt(jsonString, userKey); } /** * Decrypt and parse an encrypted object * @param encryptedData - Encrypted data object * @param userKey - User's encryption key * @returns Decrypted and parsed object */ export function decryptObject(encryptedData: EncryptedData, userKey: string): T { const decrypted = decrypt(encryptedData, userKey); return JSON.parse(decrypted) as T; } /** * Hash data using SHA-256 (for non-reversible hashing like titles) * @param data - Data to hash * @returns Hex encoded hash */ export function hash(data: string): string { return crypto.createHash('sha256').update(data).digest('hex'); }