Compare operator key in constant time
resolveIdentity and verifyOperatorKey compared the shared relay_cloud_operator_key with ===/!==, which short-circuits on the first differing byte — a timing oracle on a high-value key. Use a timingSafeEqual-based constantTimeEqual, matching admin-auth.js.
This commit is contained in:
+17
-2
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user