'use strict'; /** * NetOps To-Do — innebygd autentisering (valgfri, AUTH_MODE=app). * * Passord hashes med scrypt (Node-kjerne, ingen avhengigheter). Sesjoner er * en signert, langlevd cookie: `base64url(user).exp.HMAC-SHA256` — ingen * server-side sesjonslager nødvendig, og en restart logger ingen ut så lenge * hemmeligheten overlever (env eller persistert fil). * * Brukere lagres i data/auth.json: { "": { password: "scrypt$..", updated } } */ const fs = require('fs'); const path = require('path'); const crypto = require('crypto'); function hashPassword(password) { const salt = crypto.randomBytes(16); const hash = crypto.scryptSync(String(password), salt, 64); return `scrypt$${salt.toString('hex')}$${hash.toString('hex')}`; } function verifyPassword(password, stored) { if (typeof stored !== 'string') return false; const [alg, saltHex, hashHex] = stored.split('$'); if (alg !== 'scrypt' || !saltHex || !hashHex) return false; let expected, actual; try { expected = Buffer.from(hashHex, 'hex'); actual = crypto.scryptSync(String(password), Buffer.from(saltHex, 'hex'), expected.length); } catch { return false; } return expected.length === actual.length && crypto.timingSafeEqual(actual, expected); } class Auth { constructor(dataDir, { ttlDays = 30, secret = process.env.AUTH_SECRET || '' } = {}) { this.dataDir = dataDir; this.usersFile = path.join(dataDir, 'auth.json'); this.secretFile = path.join(dataDir, 'auth.secret'); this.ttlMs = Math.max(1, ttlDays) * 86400000; this.users = {}; this.secret = secret; } load() { fs.mkdirSync(this.dataDir, { recursive: true }); try { const d = JSON.parse(fs.readFileSync(this.usersFile, 'utf8')); if (d && typeof d === 'object' && !Array.isArray(d)) this.users = d; } catch { /* ingen brukere ennå */ } // Hemmelighet: env > persistert fil > nygenerert (persistert). if (!this.secret) { try { this.secret = fs.readFileSync(this.secretFile, 'utf8').trim(); } catch { /* genereres under */ } } if (!this.secret) { this.secret = crypto.randomBytes(32).toString('hex'); try { fs.writeFileSync(this.secretFile, this.secret, { mode: 0o600 }); } catch (e) { console.error('Kunne ikke lagre auth.secret:', e.message); } } } _save() { const tmp = this.usersFile + '.tmp'; fs.writeFileSync(tmp, JSON.stringify(this.users, null, 2) + '\n', { mode: 0o600 }); fs.renameSync(tmp, this.usersFile); } hasPassword(user) { return !!(user && this.users[user] && this.users[user].password); } anyPassword() { return Object.values(this.users).some(u => u && u.password); } setPassword(user, password) { if (!user) throw new Error('Mangler bruker'); if (typeof password !== 'string' || password.length < 6) throw new Error('Passord må være minst 6 tegn'); this.users[user] = { ...(this.users[user] || {}), password: hashPassword(password), updated: new Date().toISOString() }; this._save(); } verify(user, password) { const u = this.users[user]; return !!(u && u.password && verifyPassword(password, u.password)); } // Signert sesjons-token. user → ".." makeToken(user) { const payload = `${Buffer.from(String(user)).toString('base64url')}.${Date.now() + this.ttlMs}`; const sig = crypto.createHmac('sha256', this.secret).update(payload).digest('base64url'); return `${payload}.${sig}`; } verifyToken(token) { if (typeof token !== 'string') return null; const parts = token.split('.'); if (parts.length !== 3) return null; const payload = `${parts[0]}.${parts[1]}`; const expected = crypto.createHmac('sha256', this.secret).update(payload).digest('base64url'); const a = Buffer.from(parts[2]); const b = Buffer.from(expected); if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) return null; const exp = Number(parts[1]); if (!exp || Date.now() > exp) return null; try { return Buffer.from(parts[0], 'base64url').toString('utf8') || null; } catch { return null; } } ttlSeconds() { return Math.floor(this.ttlMs / 1000); } } module.exports = { Auth, hashPassword, verifyPassword };