Add email communication system with encrypted credentials
- Add email components for composing, viewing threads, and credential setup - Implement server API endpoints for sending emails and fetching threads - Add encryption utilities for secure credential storage - Configure email settings in environment variables - Integrate email functionality into interest details modal
This commit is contained in:
117
server/utils/encryption.ts
Normal file
117
server/utils/encryption.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
import crypto from 'crypto';
|
||||
|
||||
const algorithm = 'aes-256-gcm';
|
||||
const saltLength = 64;
|
||||
const tagLength = 16;
|
||||
const ivLength = 16;
|
||||
const iterations = 100000;
|
||||
const keyLength = 32;
|
||||
|
||||
function getKey(): Buffer {
|
||||
const key = process.env.NUXT_EMAIL_ENCRYPTION_KEY;
|
||||
if (!key || key.length < 32) {
|
||||
throw new Error('NUXT_EMAIL_ENCRYPTION_KEY must be at least 32 characters long');
|
||||
}
|
||||
// Ensure key is exactly 32 bytes
|
||||
return Buffer.from(key.substring(0, 32).padEnd(32, '0'));
|
||||
}
|
||||
|
||||
export function encryptCredentials(email: string, password: string): string {
|
||||
try {
|
||||
const key = getKey();
|
||||
const iv = crypto.randomBytes(ivLength);
|
||||
const salt = crypto.randomBytes(saltLength);
|
||||
|
||||
const derivedKey = crypto.pbkdf2Sync(key, salt, iterations, keyLength, 'sha256');
|
||||
const cipher = crypto.createCipheriv(algorithm, derivedKey, iv);
|
||||
|
||||
const data = JSON.stringify({ email, password });
|
||||
const encrypted = Buffer.concat([
|
||||
cipher.update(data, 'utf8'),
|
||||
cipher.final()
|
||||
]);
|
||||
|
||||
const tag = cipher.getAuthTag();
|
||||
|
||||
// Combine salt, iv, tag, and encrypted data
|
||||
const combined = Buffer.concat([salt, iv, tag, encrypted]);
|
||||
|
||||
return combined.toString('base64');
|
||||
} catch (error) {
|
||||
throw new Error('Failed to encrypt credentials');
|
||||
}
|
||||
}
|
||||
|
||||
export function decryptCredentials(encryptedData: string): { email: string; password: string } {
|
||||
try {
|
||||
const key = getKey();
|
||||
const combined = Buffer.from(encryptedData, 'base64');
|
||||
|
||||
// Extract components
|
||||
const salt = combined.slice(0, saltLength);
|
||||
const iv = combined.slice(saltLength, saltLength + ivLength);
|
||||
const tag = combined.slice(saltLength + ivLength, saltLength + ivLength + tagLength);
|
||||
const encrypted = combined.slice(saltLength + ivLength + tagLength);
|
||||
|
||||
const derivedKey = crypto.pbkdf2Sync(key, salt, iterations, keyLength, 'sha256');
|
||||
const decipher = crypto.createDecipheriv(algorithm, derivedKey, iv);
|
||||
decipher.setAuthTag(tag);
|
||||
|
||||
const decrypted = Buffer.concat([
|
||||
decipher.update(encrypted),
|
||||
decipher.final()
|
||||
]);
|
||||
|
||||
return JSON.parse(decrypted.toString('utf8'));
|
||||
} catch (error) {
|
||||
throw new Error('Failed to decrypt credentials');
|
||||
}
|
||||
}
|
||||
|
||||
// In-memory session storage for credentials (cleared on server restart)
|
||||
const credentialCache = new Map<string, { credentials: string; timestamp: number }>();
|
||||
const CACHE_TTL = 30 * 60 * 1000; // 30 minutes
|
||||
|
||||
export function storeCredentialsInSession(sessionId: string, encryptedCredentials: string): void {
|
||||
credentialCache.set(sessionId, {
|
||||
credentials: encryptedCredentials,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
|
||||
// Clean up expired sessions
|
||||
cleanupExpiredSessions();
|
||||
}
|
||||
|
||||
export function getCredentialsFromSession(sessionId: string): string | null {
|
||||
const session = credentialCache.get(sessionId);
|
||||
|
||||
if (!session) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check if session is expired
|
||||
if (Date.now() - session.timestamp > CACHE_TTL) {
|
||||
credentialCache.delete(sessionId);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Update timestamp on access
|
||||
session.timestamp = Date.now();
|
||||
return session.credentials;
|
||||
}
|
||||
|
||||
export function clearCredentialsFromSession(sessionId: string): void {
|
||||
credentialCache.delete(sessionId);
|
||||
}
|
||||
|
||||
function cleanupExpiredSessions(): void {
|
||||
const now = Date.now();
|
||||
for (const [sessionId, session] of credentialCache.entries()) {
|
||||
if (now - session.timestamp > CACHE_TTL) {
|
||||
credentialCache.delete(sessionId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup expired sessions every 5 minutes
|
||||
setInterval(cleanupExpiredSessions, 5 * 60 * 1000);
|
||||
Reference in New Issue
Block a user