import crypto from 'node:crypto'; import fs from 'node:fs'; import path from 'node:path'; export const SESSION_COOKIE = 'fdc_session'; export const SESSION_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000; // Ohne SESSION_SECRET wird einmalig ein Geheimnis im Datenverzeichnis erzeugt, // damit Sitzungen einen Neustart überleben. export function loadSessionSecret(config) { if (config.sessionSecret) return config.sessionSecret; const file = path.join(config.dataDir, 'session-secret'); try { return fs.readFileSync(file, 'utf8').trim(); } catch { const secret = crypto.randomBytes(32).toString('hex'); fs.mkdirSync(config.dataDir, { recursive: true }); fs.writeFileSync(file, secret, { mode: 0o600 }); return secret; } } // Die PIN fließt in die Signatur ein: wer die PIN ändert, meldet alle Sitzungen ab. function sign(secret, pin, payload) { return crypto.createHmac('sha256', secret).update(`${payload}|${pin}`).digest('base64url'); } function safeEqual(a, b) { const bufA = Buffer.from(a); const bufB = Buffer.from(b); return bufA.length === bufB.length && crypto.timingSafeEqual(bufA, bufB); } export function createToken(secret, pin, now = Date.now()) { const payload = String(now + SESSION_MAX_AGE_MS); return `${payload}.${sign(secret, pin, payload)}`; } export function verifyToken(secret, pin, token, now = Date.now()) { if (!pin || typeof token !== 'string') return false; const [payload, signature] = token.split('.'); if (!payload || !signature) return false; if (!safeEqual(signature, sign(secret, pin, payload))) return false; return Number(payload) > now; } export function pinMatches(expected, given) { if (!expected || typeof given !== 'string') return false; const hash = (value) => crypto.createHash('sha256').update(value).digest(); return crypto.timingSafeEqual(hash(expected), hash(given)); } // Nur ein Nutzer: ein globaler Zähler reicht und greift auch hinter dem Proxy. export function createLoginLimiter({ maxFailures = 10, windowMs = 15 * 60 * 1000 } = {}) { let failures = []; return { isBlocked(now = Date.now()) { failures = failures.filter((time) => now - time < windowMs); return failures.length >= maxFailures; }, recordFailure(now = Date.now()) { failures.push(now); }, reset() { failures = []; }, }; } export function readCookie(req, name) { const header = req.headers.cookie; if (!header) return undefined; for (const part of header.split(';')) { const index = part.indexOf('='); if (index === -1) continue; if (part.slice(0, index).trim() === name) { return decodeURIComponent(part.slice(index + 1).trim()); } } return undefined; }