From 78de978dcdccde5bbeba86ffe35686fb393553a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Sun, 25 Oct 2026 19:58:40 +0100 Subject: [PATCH] fix(recovery): check a pasted token key against its slow fingerprint The kit's check compared keyed SHA-256 digests of the pasted value and the configured key, which code scanning still reads as a fast hash of a password. It now compares the scrypt fingerprints in constant time. --- src/lib/recovery/kit.ts | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/lib/recovery/kit.ts b/src/lib/recovery/kit.ts index 0141858e9..721b9e603 100644 --- a/src/lib/recovery/kit.ts +++ b/src/lib/recovery/kit.ts @@ -3,7 +3,7 @@ * stored confirmation covers, recording a confirmation, and "Check my copy". * The rules themselves are pure and live in `kit-rules.ts`. */ -import { createHmac, randomBytes, timingSafeEqual } from "node:crypto"; +import { timingSafeEqual } from "node:crypto"; import { prisma } from "@/lib/db"; import { envValue } from "@/lib/env"; @@ -29,9 +29,7 @@ import { type KitKeyKind, type RecoveryKit, } from "@/lib/recovery/kit-rules"; - -/** Keys the constant-time comparison below; never leaves the process. */ -const COMPARE_KEY = randomBytes(32); +import { fingerprintSecretText } from "@/lib/database-backup/dump-core.mjs"; /** Every key the kit lists on this server right now, without a verdict. */ export async function listKitKeys( @@ -168,11 +166,10 @@ export function verifyKitKey(input: { case "token": { const configured = envValue("API_TOKEN_HMAC_KEY"); if (!configured || input.id !== KIT_KEY_SOURCES.token) return false; - // Keyed digests under a key of this process, so the comparison is - // constant time whatever the lengths and nothing derived from the - // secret outlives the call. + // The slow fingerprint the kit shows, compared in constant time: equal + // lengths whatever was pasted, and no fast digest of the secret. const digest = (s: string) => - createHmac("sha256", COMPARE_KEY).update(s).digest(); + Buffer.from(fingerprintSecretText(s), "utf8"); return timingSafeEqual(digest(value), digest(configured)); } }