fix: use constant-time auth token comparison (INFRA-21) (#30)

Replace plain token !== secrets.authToken checks in the unlock and
lockdown handlers with crypto.timingSafeEqual, guarding for unequal
buffer lengths first (timingSafeEqual throws on different lengths).
Prevents timing side-channel leakage of the auth token. Handler
signatures, event shape, and return contract are unchanged.
This commit is contained in:
Adam Moussa 2026-06-05 17:26:12 -04:00 • committed by GitHub
parent 0f27b2553a
commit fe04a199de
2 changed files with 18 additions and 2 deletions

View file

@ -2,9 +2,17 @@ import {
SSMClient,
GetParameterCommand,
} from "@aws-sdk/client-ssm";
import { timingSafeEqual } from "node:crypto";
const ssm = new SSMClient({});
function tokensMatch(provided: string, expected: string): boolean {
const a = Buffer.from(provided);
const b = Buffer.from(expected);
// timingSafeEqual throws on unequal-length buffers; check length first.
return a.length === b.length && timingSafeEqual(a, b);
}
let cachedAuthToken: string | undefined;
let cachedApiKey: string | undefined;
@ -105,7 +113,7 @@ export async function handler(event: {
return xmlResponse(500, textScreenXml("Error", "Internal error"));
}
if (token !== secrets.authToken) {
if (!tokensMatch(token, secrets.authToken)) {
console.log(JSON.stringify({ action: "lockdown", status: "rejected", reason: "invalid_token", sourceIp }));
return { statusCode: 403, body: JSON.stringify({ error: "Forbidden" }) };
}

View file

@ -2,9 +2,17 @@ import {
SSMClient,
GetParameterCommand,
} from "@aws-sdk/client-ssm";
import { timingSafeEqual } from "node:crypto";
const ssm = new SSMClient({});
function tokensMatch(provided: string, expected: string): boolean {
const a = Buffer.from(provided);
const b = Buffer.from(expected);
// timingSafeEqual throws on unequal-length buffers; check length first.
return a.length === b.length && timingSafeEqual(a, b);
}
let cachedAuthToken: string | undefined;
let cachedApiKey: string | undefined;
let cachedDoorId: string | undefined;
@ -51,7 +59,7 @@ export async function handler(event: {
return { statusCode: 500, body: JSON.stringify({ error: "Internal error" }) };
}
if (token !== secrets.authToken) {
if (!tokensMatch(token, secrets.authToken)) {
console.log(JSON.stringify({ action: "unlock_attempt", status: "rejected", reason: "invalid_token", sourceIp }));
return { statusCode: 403, body: JSON.stringify({ error: "Forbidden" }) };
}