'use strict'; const crypto = require('crypto'); /** * In-memory store for VHL manifests and encrypted documents. * * Each entry has a TTL; expired entries are cleaned up lazily. * * WARNING: state is lost on process restart. Replace with Redis or a * persistent store for production deployments. */ const manifests = new Map(); // manifestId → { manifest, pinHash: string|null, expiresAt: number } const documents = new Map(); // documentId → { payload: Buffer, expiresAt: number } /** Generates a cryptographically random URL-safe ID (22 chars). */ function generateId() { return crypto.randomBytes(16).toString('base64url'); } /** * Derives a SHA-256 hash of a PIN. * Stored instead of the plaintext PIN. * @param {string} pin * @returns {string} base64url hash */ function hashPin(pin) { return crypto.createHash('sha256').update(String(pin)).digest('base64url'); } // --- Documents --- /** * Stores a serialized encrypted document and returns its ID. * @param {Buffer} payload - Output of vhlCrypto.serializeEncrypted(). * @param {number} ttlSeconds * @returns {string} documentId */ function storeDocument(payload, ttlSeconds) { const id = generateId(); documents.set(id, { payload, expiresAt: Date.now() + ttlSeconds * 1000, }); scheduleCleanup(); return id; } /** * Retrieves an encrypted document by ID. * Returns null if not found or expired. * @param {string} id * @returns {Buffer|null} */ function getDocument(id) { const entry = documents.get(id); if (!entry) return null; if (Date.now() > entry.expiresAt) { documents.delete(id); return null; } return entry.payload; } // --- Manifests --- /** * Stores a manifest and returns its ID. * @param {object} manifest - Plain manifest object (e.g. { files: [...] }). * @param {number} ttlSeconds * @param {string} [pin] - If provided, access requires this PIN. * @returns {string} manifestId */ function storeManifest(manifest, ttlSeconds, pin) { const id = generateId(); manifests.set(id, { manifest, pinHash: pin ? hashPin(pin) : null, expiresAt: Date.now() + ttlSeconds * 1000, }); scheduleCleanup(); return id; } /** * Retrieves a manifest by ID, optionally validating a PIN. * * @param {string} id * @param {string} [pin] - Required when the manifest was stored with a PIN. * @returns {object|null} The manifest, or null if not found / expired. * @throws {Error} err.status = 401 if PIN is required but not provided. * @throws {Error} err.status = 403 if PIN is incorrect. */ function getManifest(id, pin) { const entry = manifests.get(id); if (!entry) return null; if (Date.now() > entry.expiresAt) { manifests.delete(id); return null; } if (entry.pinHash) { if (!pin) { const err = new Error('PIN requerido para acceder a este manifest'); err.status = 401; throw err; } if (hashPin(pin) !== entry.pinHash) { const err = new Error('PIN incorrecto'); err.status = 403; throw err; } } return entry.manifest; } // --- Cleanup --- let _cleanupTimer = null; /** * Schedules a one-shot cleanup run 60 seconds from now (if not already scheduled). * Removes all entries whose TTL has expired. */ function scheduleCleanup() { if (_cleanupTimer) return; _cleanupTimer = setTimeout(() => { _cleanupTimer = null; const now = Date.now(); for (const [id, entry] of manifests) { if (now > entry.expiresAt) manifests.delete(id); } for (const [id, entry] of documents) { if (now > entry.expiresAt) documents.delete(id); } }, 60_000); // Allow the process to exit even if cleanup hasn't fired if (_cleanupTimer.unref) _cleanupTimer.unref(); } module.exports = { storeDocument, getDocument, storeManifest, getManifest };