152 lines
3.9 KiB
TypeScript
152 lines
3.9 KiB
TypeScript
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.createCipher(ALGORITHM, key);
|
|
|
|
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();
|
|
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.createDecipher(ALGORITHM, key);
|
|
let decrypted = decipher.update(encryptedData, 'hex', 'utf8');
|
|
decrypted += decipher.final('utf8');
|
|
|
|
return decrypted;
|
|
} catch (error) {
|
|
console.error('Decryption error:', error);
|
|
throw new Error('Failed to decrypt data');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
}) {
|
|
return {
|
|
...settings,
|
|
password: decrypt(settings.password)
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
|
|
/**
|
|
* 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);
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
};
|
|
} catch (error) {
|
|
return {
|
|
success: false,
|
|
error: error instanceof Error ? error.message : 'Unknown error'
|
|
};
|
|
}
|
|
} |