diff --git a/kiloclaw/src/auth/middleware.ts b/kiloclaw/src/auth/middleware.ts index 65501823a..3deeb400a 100644 --- a/kiloclaw/src/auth/middleware.ts +++ b/kiloclaw/src/auth/middleware.ts @@ -104,9 +104,22 @@ export async function internalApiMiddleware(c: Context, next: Next) { return c.json({ error: 'Forbidden' }, 403); } - if (apiKey !== secret) { + if (!timingSafeEqual(apiKey, secret)) { return c.json({ error: 'Forbidden' }, 403); } return next(); } + +function timingSafeEqual(a: string, b: string): boolean { + const encoder = new TextEncoder(); + const aBytes = encoder.encode(a); + const bBytes = encoder.encode(b); + + if (aBytes.length !== bBytes.length) { + // Compare a against itself so the timing is constant regardless of length mismatch + crypto.subtle.timingSafeEqual(aBytes, aBytes); + return false; + } + return crypto.subtle.timingSafeEqual(aBytes, bBytes); +} diff --git a/kiloclaw/src/test-setup.ts b/kiloclaw/src/test-setup.ts new file mode 100644 index 000000000..80ab269f1 --- /dev/null +++ b/kiloclaw/src/test-setup.ts @@ -0,0 +1,17 @@ +import { timingSafeEqual } from 'node:crypto'; + +// Polyfill crypto.subtle.timingSafeEqual for Vitest (Node environment). +// This API is available natively in Cloudflare Workers but not in Node.js. +if (!crypto.subtle.timingSafeEqual) { + Object.defineProperty(crypto.subtle, 'timingSafeEqual', { + value(a: ArrayBuffer | ArrayBufferView, b: ArrayBuffer | ArrayBufferView): boolean { + const bufA = ArrayBuffer.isView(a) + ? Buffer.from(a.buffer, a.byteOffset, a.byteLength) + : Buffer.from(a); + const bufB = ArrayBuffer.isView(b) + ? Buffer.from(b.buffer, b.byteOffset, b.byteLength) + : Buffer.from(b); + return timingSafeEqual(bufA, bufB); + }, + }); +} diff --git a/kiloclaw/vitest.config.ts b/kiloclaw/vitest.config.ts index bfef76e28..58e6c65a9 100644 --- a/kiloclaw/vitest.config.ts +++ b/kiloclaw/vitest.config.ts @@ -5,6 +5,7 @@ export default defineConfig({ name: 'unit', globals: true, environment: 'node', + setupFiles: ['src/test-setup.ts'], include: ['src/**/*.test.ts'], coverage: { provider: 'v8',