mirror of
https://github.com/Sea-Haven-Industries/seahaven-door-unlock-api.git
synced 2026-09-30 03:43:11 +00:00
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:
parent
0f27b2553a
commit
fe04a199de
2 changed files with 18 additions and 2 deletions
|
|
@ -2,9 +2,17 @@ import {
|
||||||
SSMClient,
|
SSMClient,
|
||||||
GetParameterCommand,
|
GetParameterCommand,
|
||||||
} from "@aws-sdk/client-ssm";
|
} from "@aws-sdk/client-ssm";
|
||||||
|
import { timingSafeEqual } from "node:crypto";
|
||||||
|
|
||||||
const ssm = new SSMClient({});
|
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 cachedAuthToken: string | undefined;
|
||||||
let cachedApiKey: string | undefined;
|
let cachedApiKey: string | undefined;
|
||||||
|
|
||||||
|
|
@ -105,7 +113,7 @@ export async function handler(event: {
|
||||||
return xmlResponse(500, textScreenXml("Error", "Internal error"));
|
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 }));
|
console.log(JSON.stringify({ action: "lockdown", status: "rejected", reason: "invalid_token", sourceIp }));
|
||||||
return { statusCode: 403, body: JSON.stringify({ error: "Forbidden" }) };
|
return { statusCode: 403, body: JSON.stringify({ error: "Forbidden" }) };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,17 @@ import {
|
||||||
SSMClient,
|
SSMClient,
|
||||||
GetParameterCommand,
|
GetParameterCommand,
|
||||||
} from "@aws-sdk/client-ssm";
|
} from "@aws-sdk/client-ssm";
|
||||||
|
import { timingSafeEqual } from "node:crypto";
|
||||||
|
|
||||||
const ssm = new SSMClient({});
|
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 cachedAuthToken: string | undefined;
|
||||||
let cachedApiKey: string | undefined;
|
let cachedApiKey: string | undefined;
|
||||||
let cachedDoorId: string | undefined;
|
let cachedDoorId: string | undefined;
|
||||||
|
|
@ -51,7 +59,7 @@ export async function handler(event: {
|
||||||
return { statusCode: 500, body: JSON.stringify({ error: "Internal error" }) };
|
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 }));
|
console.log(JSON.stringify({ action: "unlock_attempt", status: "rejected", reason: "invalid_token", sourceIp }));
|
||||||
return { statusCode: 403, body: JSON.stringify({ error: "Forbidden" }) };
|
return { statusCode: 403, body: JSON.stringify({ error: "Forbidden" }) };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue