Files
jmap-webmail/lib/auth/crypto.ts
T
Matthieu MALVACHE 6a49514f7a feat(auth): add "Remember me" session persistence and simplify 2FA UX
Basic Auth sessions now persist across page refreshes via an encrypted
httpOnly cookie (AES-256-GCM). The feature is opt-in: admin sets
SESSION_SECRET env var, user checks "Remember me" at login.

Rework the 2FA input from a checkbox+animated panel to a discreet
"I have a 2FA code" text link that reveals the TOTP field on click.
2026-02-26 00:45:26 +01:00

51 lines
1.9 KiB
TypeScript

import { createCipheriv, createDecipheriv, randomBytes, createHash } from 'node:crypto';
import { logger } from '@/lib/logger';
const ALGORITHM = 'aes-256-gcm';
const IV_LENGTH = 12;
const TAG_LENGTH = 16;
function getKey(): Buffer {
const secret = process.env.SESSION_SECRET;
if (!secret) throw new Error('SESSION_SECRET not configured');
return createHash('sha256').update(secret).digest();
}
export function encryptSession(serverUrl: string, username: string, password: string): string {
const key = getKey();
const iv = randomBytes(IV_LENGTH);
const cipher = createCipheriv(ALGORITHM, key, iv);
const payload = JSON.stringify({ v: 1, serverUrl, username, password });
const encrypted = Buffer.concat([cipher.update(payload, 'utf8'), cipher.final()]);
const tag = cipher.getAuthTag();
return Buffer.concat([iv, tag, encrypted]).toString('base64');
}
export function decryptSession(token: string): { serverUrl: string; username: string; password: string } | null {
try {
const key = getKey();
const data = Buffer.from(token, 'base64');
if (data.length < IV_LENGTH + TAG_LENGTH) return null;
const iv = data.subarray(0, IV_LENGTH);
const tag = data.subarray(IV_LENGTH, IV_LENGTH + TAG_LENGTH);
const encrypted = data.subarray(IV_LENGTH + TAG_LENGTH);
const decipher = createDecipheriv(ALGORITHM, key, iv);
decipher.setAuthTag(tag);
const decrypted = Buffer.concat([decipher.update(encrypted), decipher.final()]);
const parsed = JSON.parse(decrypted.toString('utf8'));
if (parsed.v !== 1 || !parsed.serverUrl || !parsed.username || !parsed.password) return null;
return { serverUrl: parsed.serverUrl, username: parsed.username, password: parsed.password };
} catch (error) {
logger.warn('Session decryption failed', {
error: error instanceof Error ? error.message : 'Unknown error',
});
return null;
}
}