'use strict'; // Secrets at rest: VNC passwords and agent keys are AES-256-GCM encrypted. // Tokens that we only ever need to *compare* (invite tokens, agent keys as // presented by a client) are stored as SHA-256 and checked in constant time. const crypto = require('crypto'); const fs = require('fs'); const path = require('path'); const config = require('./config'); const KEY_FILE = path.join(path.dirname(config.dbPath), 'encryption.key'); let keyCache = null; // A stable 32-byte key. Prefer ENCRYPTION_KEY from the environment; otherwise // generate one next to the database so a bare `npm start` still works and stays // decryptable across restarts. function key() { if (keyCache) return keyCache; let material = config.encryptionKey; if (!material) { fs.mkdirSync(path.dirname(KEY_FILE), { recursive: true }); if (fs.existsSync(KEY_FILE)) { material = fs.readFileSync(KEY_FILE, 'utf8').trim(); } else { material = crypto.randomBytes(32).toString('hex'); fs.writeFileSync(KEY_FILE, material, { mode: 0o600 }); console.warn(`[crypto] ENCRYPTION_KEY not set — generated one at ${KEY_FILE}. Back it up.`); } } keyCache = crypto.createHash('sha256').update(material, 'utf8').digest(); return keyCache; } function encrypt(plaintext) { if (plaintext === null || plaintext === undefined || plaintext === '') return null; const iv = crypto.randomBytes(12); const cipher = crypto.createCipheriv('aes-256-gcm', key(), iv); const ct = Buffer.concat([cipher.update(String(plaintext), 'utf8'), cipher.final()]); const tag = cipher.getAuthTag(); return `v1.${iv.toString('base64')}.${tag.toString('base64')}.${ct.toString('base64')}`; } function decrypt(blob) { if (!blob) return null; const parts = String(blob).split('.'); if (parts.length !== 4 || parts[0] !== 'v1') return null; try { const decipher = crypto.createDecipheriv('aes-256-gcm', key(), Buffer.from(parts[1], 'base64')); decipher.setAuthTag(Buffer.from(parts[2], 'base64')); return Buffer.concat([decipher.update(Buffer.from(parts[3], 'base64')), decipher.final()]).toString('utf8'); } catch { // Wrong key or tampered ciphertext — treat as absent rather than crashing a session. return null; } } // URL-safe random token, 32 bytes of entropy. function randomToken(bytes = 32) { return crypto.randomBytes(bytes).toString('base64url'); } function sha256(value) { return crypto.createHash('sha256').update(String(value), 'utf8').digest('hex'); } function timingSafeEqualHex(a, b) { const ba = Buffer.from(String(a || ''), 'hex'); const bb = Buffer.from(String(b || ''), 'hex'); if (ba.length !== bb.length || ba.length === 0) return false; return crypto.timingSafeEqual(ba, bb); } function uuid() { return crypto.randomUUID(); } module.exports = { encrypt, decrypt, randomToken, sha256, timingSafeEqualHex, uuid };