port-nimara-client-portal/server/utils/encryption.ts

118 lines
3.5 KiB
TypeScript

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);