import crypto from 'crypto'; /** * Issuing and recognising the tokens that open the public intake endpoint. * * Kept apart from the routes so the rules are pure and testable directly — the * same reasoning as `uploadTypes.ts` and `keyByCallerAndEmail`, both of which * are exported for their tests because they are where the real decisions live. */ // 32 bytes — 256 bits. base64url so the value survives being pasted into a URL, // a chat message and a QR code without escaping, which is the whole point of a // link somebody is handed. const TOKEN_BYTES = 32; export function generateToken(): string { return crypto.randomBytes(TOKEN_BYTES).toString('base64url'); } /** * The digest stored against a link. * * SHA-256 rather than bcrypt, deliberately, and the reasoning is the opposite * of the one that governs passwords. A password hash is slow on purpose, * because a human password carries little entropy and has to survive an * offline dictionary attack. This is 256 bits from a CSPRNG: there is no * dictionary to try, and guessing is not a threat that slowing the hash * addresses. * * Meanwhile the digest is computed on every submission request, and the intake * endpoint is unauthenticated. A deliberately slow hash there would be a * denial-of-service surface rather than a protection — see #242, where cost-12 * bcrypt in the test suite was enough to push a request past its timeout under * load. * * No timing-safe comparison is needed. The lookup is an indexed equality match * on the digest rather than a byte-by-byte compare of the secret, and an * attacker able to mount a timing attack against a 256-bit random value would * still need the value. */ export function hashToken(token: string): string { return crypto.createHash('sha256').update(token).digest('hex'); }