2026-04-27 04:25:52 +00:00

142 lines
3.9 KiB
JavaScript

'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 };