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.
This commit is contained in:
Marc-André Bombeck committed 2026-10-25 19:58:40 +01:00
1 parent 3939c9c0a5
commit 78de978dcd
1 file changed
+5 -8
+5 -8
View File
@@ -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));
}
}