142 lines
3.9 KiB
JavaScript
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 };
|