import crypto from 'crypto'; // Get encryption key from environment or generate a default one const ENCRYPTION_KEY = process.env.ENCRYPTION_KEY || 'phosphat-report-default-key-32b'; // Must be 32 characters const ALGORITHM = 'aes-256-cbc'; const IV_LENGTH = 16; // For AES, this is always 16 // Ensure the key is exactly 32 bytes function getEncryptionKey(): Buffer { const key = ENCRYPTION_KEY.padEnd(32, '0').substring(0, 32); return Buffer.from(key, 'utf8'); } /** * Encrypts a plain text string * @param text - The plain text to encrypt * @returns Encrypted string in format: iv:encryptedData */ export function encrypt(text: string): string { if (!text) return ''; try { const key = getEncryptionKey(); const iv = crypto.randomBytes(IV_LENGTH); const cipher = crypto.createCipheriv(ALGORITHM, key, iv); let encrypted = cipher.update(text, 'utf8', 'hex'); encrypted += cipher.final('hex'); // Return iv and encrypted data separated by ':' return iv.toString('hex') + ':' + encrypted; } catch (error) { console.error('Encryption error:', error); throw new Error('Failed to encrypt data'); } } /** * Decrypts an encrypted string * @param encryptedText - The encrypted text in format: iv:encryptedData * @returns Decrypted plain text string */ export function decrypt(encryptedText: string): string { if (!encryptedText) return ''; try { const key = getEncryptionKey(); // Check if this is the new format with IV if (isEncrypted(encryptedText)) { const textParts = encryptedText.split(':'); if (textParts.length !== 2) { throw new Error('Invalid encrypted text format'); } const iv = Buffer.from(textParts[0], 'hex'); const encryptedData = textParts[1]; const decipher = crypto.createDecipheriv(ALGORITHM, key, iv); let decrypted = decipher.update(encryptedData, 'hex', 'utf8'); decrypted += decipher.final('utf8'); return decrypted; } // Check if this is legacy format else if (isLegacyEncrypted(encryptedText)) { try { return legacyDecrypt(encryptedText); } catch (legacyError) { console.warn('Failed to decrypt legacy format, treating as plain text:', legacyError); // If we can't decrypt legacy format, assume it's plain text return encryptedText; } } // If it doesn't match any encrypted format, assume it's plain text else { return encryptedText; } } catch (error) { console.error('Decryption error:', error); // If decryption fails completely, return the original text // This prevents the app from crashing if there's corrupted data console.warn('Returning original text due to decryption failure'); return encryptedText; } } /** * Encrypts sensitive mail settings * @param settings - Mail settings object * @returns Mail settings with encrypted password */ export function encryptMailSettings(settings: { host: string; port: number; secure: boolean; username: string; password: string; fromName: string; fromEmail: string; }) { return { ...settings, password: encrypt(settings.password) }; } /** * Decrypts sensitive mail settings * @param settings - Mail settings object with encrypted password * @returns Mail settings with decrypted password */ export function decryptMailSettings(settings: { host: string; port: number; secure: boolean; username: string; password: string; fromName: string; fromEmail: string; }) { try { return { ...settings, password: decrypt(settings.password) }; } catch (error) { console.error('Failed to decrypt mail settings password:', error); // Return settings with empty password if decryption fails // This prevents the app from crashing return { ...settings, password: '' }; } } /** * Legacy decrypt function for backward compatibility with old encryption method * @param encryptedText - The encrypted text * @returns Decrypted plain text string */ function legacyDecrypt(encryptedText: string): string { try { const key = getEncryptionKey(); // Try to use the old createDecipher method if available (for local development) // This is a fallback for data encrypted with the old method if (typeof (crypto as any).createDecipher === 'function') { const decipher = (crypto as any).createDecipher(ALGORITHM, key); let decrypted = decipher.update(encryptedText, 'hex', 'utf8'); decrypted += decipher.final('utf8'); return decrypted; } else { // If old method is not available, we can't decrypt old format throw new Error('Cannot decrypt legacy format in this Node.js version'); } } catch (error) { throw error; } } /** * Validates if a string is encrypted (contains ':' separator) * @param text - Text to validate * @returns True if text appears to be encrypted */ export function isEncrypted(text: string): boolean { return text.includes(':') && text.split(':').length === 2; } /** * Checks if encrypted text is in legacy format (no IV separator) * @param text - Text to check * @returns True if text appears to be in legacy format */ export function isLegacyEncrypted(text: string): boolean { // Legacy format doesn't have ':' separator or has different pattern return !text.includes(':') && text.length > 0 && /^[a-f0-9]+$/i.test(text); } /** * Safely encrypts a password only if it's not already encrypted * @param password - Password to encrypt * @returns Encrypted password */ export function safeEncryptPassword(password: string): string { if (isEncrypted(password)) { return password; // Already encrypted } return encrypt(password); } /** * Migrates legacy encrypted data to new format * @param encryptedText - Potentially legacy encrypted text * @returns Re-encrypted text in new format */ export function migrateEncryption(encryptedText: string): string { if (!encryptedText) return ''; try { // If it's already in new format, return as-is if (isEncrypted(encryptedText)) { // Verify it can be decrypted and re-encrypt to ensure consistency const decrypted = decrypt(encryptedText); return encrypt(decrypted); } // If it's in legacy format, decrypt and re-encrypt if (isLegacyEncrypted(encryptedText)) { const decrypted = legacyDecrypt(encryptedText); return encrypt(decrypted); } // If it's plain text, encrypt it return encrypt(encryptedText); } catch (error) { console.error('Migration error:', error); // If migration fails, try to encrypt as plain text try { return encrypt(encryptedText); } catch (encryptError) { console.error('Failed to encrypt during migration:', encryptError); return encryptedText; } } } /** * Test encryption/decryption functionality * @param testText - Text to test with * @returns Test results */ export function testEncryption(testText: string = 'test-password-123') { try { const encrypted = encrypt(testText); const decrypted = decrypt(encrypted); return { success: decrypted === testText, original: testText, encrypted: encrypted, decrypted: decrypted, isValid: decrypted === testText, format: 'new' }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : 'Unknown error', format: 'unknown' }; } }