diff --git a/server/identity.js b/server/identity.js index dc8f592..e64ef6b 100644 --- a/server/identity.js +++ b/server/identity.js @@ -23,10 +23,25 @@ // A route bills against `creditKey`; for tier it uses the stored row tier // (cloud) or `license.tier` (license) — see identityTier(). +import { timingSafeEqual } from "crypto"; import { getConfigSnapshot } from "./config.js"; import { resolveLicense } from "./keysat-client.js"; import { getCreditKey } from "./credits.js"; +// Constant-time string compare for the shared operator key, so a token +// guess can't be tuned byte-by-byte off response timing. Mirrors the +// helper in admin-auth.js. A length mismatch returns false early — +// length isn't the secret, and timingSafeEqual requires equal length. +function constantTimeEqual(a, b) { + if (typeof a !== "string" || typeof b !== "string") return false; + if (a.length !== b.length) return false; + try { + return timingSafeEqual(Buffer.from(a), Buffer.from(b)); + } catch { + return false; + } +} + // user-ids come from Recaps (base64url / hex account ids). Constrain the // charset so a header can't smuggle a path-ish or oversized key into the // ledger. @@ -40,7 +55,7 @@ export async function resolveIdentity(req) { const cfg = await getConfigSnapshot(); const expected = (cfg.relay_cloud_operator_key || "").trim(); const presented = (req.header("X-Recap-Operator-Key") || "").trim(); - if (!expected || !presented || presented !== expected) { + if (!expected || !presented || !constantTimeEqual(presented, expected)) { const e = new Error( "X-Recap-User-Id requires a valid X-Recap-Operator-Key" ); @@ -81,7 +96,7 @@ export async function verifyOperatorKey(req) { const cfg = await getConfigSnapshot(); const expected = (cfg.relay_cloud_operator_key || "").trim(); const presented = (req.header("X-Recap-Operator-Key") || "").trim(); - return !!expected && !!presented && presented === expected; + return !!expected && !!presented && constantTimeEqual(presented, expected); } // The tier to bill/quota at for a resolved identity.