Skip to content

Commit 8ad1ab0

Browse files
bwp91claude
andcommitted
test: cover constant-time pincode comparison
Adds five HAPServer tests verifying the fix from 12aea01: the insecure PUT /characteristics and POST /resource paths must compare the authorization header against the configured pincode using crypto.timingSafeEqual with a length check first. Coverage: - missing authorization → CONNECTION_AUTHORIZATION_REQUIRED - wrong-length authorization → rejected (without the length guard timingSafeEqual would throw RangeError) - same-length wrong pincode → rejected - correct pincode → authorized - /resource endpoint also rejects wrong-length auth Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 36267cc commit 8ad1ab0

2 files changed

Lines changed: 109 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ All notable changes to `@homebridge/hap-nodejs` will be documented in this file.
1818
- test: cover aid.iid format validation
1919
- test: cover camera stream start TLV parsing guards
2020
- test: cover error argument in pairing debug logs
21+
- test: cover constant-time pincode comparison
2122

2223
### Homebridge Dependencies
2324

src/lib/HAPServer.spec.ts

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,114 @@ describe("HAPServer", () => {
329329
});
330330
});
331331

332+
describe("constant-time pincode comparison (fix 12aea013)", () => {
333+
test("PUT /characteristics with no authorization header should be rejected", async () => {
334+
server = new HAPServer(accessoryInfoUnpaired);
335+
server.allowInsecureRequest = true;
336+
const [port] = await bindServer(server);
337+
338+
try {
339+
await axios.put(
340+
`http://localhost:${port}/characteristics`,
341+
{ characteristics: [{ aid: 1, iid: 9, value: true }] },
342+
{ httpAgent },
343+
);
344+
fail("Expected CONNECTION_AUTHORIZATION_REQUIRED response");
345+
} catch (error) {
346+
expect(error).toBeInstanceOf(AxiosError);
347+
expect(error.response?.status).toBe(HAPPairingHTTPCode.CONNECTION_AUTHORIZATION_REQUIRED);
348+
expect(error.response?.data).toEqual({ status: HAPStatus.INSUFFICIENT_PRIVILEGES });
349+
}
350+
});
351+
352+
test("PUT /characteristics with wrong-length authorization should not crash timingSafeEqual", async () => {
353+
server = new HAPServer(accessoryInfoUnpaired);
354+
server.allowInsecureRequest = true;
355+
const [port] = await bindServer(server);
356+
357+
// 5 bytes — different length to the 11-byte pincode. Without the
358+
// length guard added in 12aea013, crypto.timingSafeEqual throws
359+
// RangeError and the connection would be killed.
360+
try {
361+
await axios.put(
362+
`http://localhost:${port}/characteristics`,
363+
{ characteristics: [{ aid: 1, iid: 9, value: true }] },
364+
{ httpAgent, headers: { authorization: "short" } },
365+
);
366+
fail("Expected CONNECTION_AUTHORIZATION_REQUIRED response");
367+
} catch (error) {
368+
expect(error).toBeInstanceOf(AxiosError);
369+
expect(error.response?.status).toBe(HAPPairingHTTPCode.CONNECTION_AUTHORIZATION_REQUIRED);
370+
}
371+
});
372+
373+
test("PUT /characteristics with same-length wrong pincode should be rejected", async () => {
374+
server = new HAPServer(accessoryInfoUnpaired);
375+
server.allowInsecureRequest = true;
376+
const [port] = await bindServer(server);
377+
378+
const wrongPincode = " 999-99-999"; // same length as " 031-45-154", different content
379+
try {
380+
await axios.put(
381+
`http://localhost:${port}/characteristics`,
382+
{ characteristics: [{ aid: 1, iid: 9, value: true }] },
383+
{ httpAgent, headers: { authorization: wrongPincode } },
384+
);
385+
fail("Expected CONNECTION_AUTHORIZATION_REQUIRED response");
386+
} catch (error) {
387+
expect(error).toBeInstanceOf(AxiosError);
388+
expect(error.response?.status).toBe(HAPPairingHTTPCode.CONNECTION_AUTHORIZATION_REQUIRED);
389+
}
390+
});
391+
392+
test("PUT /characteristics with correct pincode should be authorized", async () => {
393+
// use a pincode without leading whitespace — HTTP header values get trimmed
394+
// by node's parser so the global test fixture's " 031-45-154" can't round-trip.
395+
const info = AccessoryInfo.create(serverUsername);
396+
// @ts-expect-error: private access
397+
info.setupID = Accessory._generateSetupID();
398+
info.displayName = "Outlet";
399+
info.category = 7;
400+
info.pincode = "031-45-154";
401+
402+
server = new HAPServer(info);
403+
server.allowInsecureRequest = true;
404+
const [port] = await bindServer(server);
405+
406+
server.on(HAPServerEventTypes.SET_CHARACTERISTICS, (connection, writeRequest, callback) => {
407+
callback(undefined, { characteristics: writeRequest.characteristics.map(c => ({ aid: c.aid, iid: c.iid, status: HAPStatus.SUCCESS })) });
408+
});
409+
410+
const response = await axios.put(
411+
`http://localhost:${port}/characteristics`,
412+
{ characteristics: [{ aid: 1, iid: 9, value: true }] },
413+
{ httpAgent, headers: { authorization: info.pincode } },
414+
);
415+
// a successful insecure PUT returns 204 NO_CONTENT (or 200 if there's a response body)
416+
expect(response.status).toBeGreaterThanOrEqual(200);
417+
expect(response.status).toBeLessThan(300);
418+
});
419+
420+
test("POST /resource with wrong-length authorization should not crash timingSafeEqual", async () => {
421+
server = new HAPServer(accessoryInfoUnpaired);
422+
server.allowInsecureRequest = true;
423+
const [port] = await bindServer(server);
424+
425+
try {
426+
await axios.post(
427+
`http://localhost:${port}/resource`,
428+
{ "resource-type": "image", "image-width": 100, "image-height": 100 },
429+
{ httpAgent, headers: { authorization: "short" } },
430+
);
431+
fail("Expected CONNECTION_AUTHORIZATION_REQUIRED response");
432+
} catch (error) {
433+
expect(error).toBeInstanceOf(AxiosError);
434+
expect(error.response?.status).toBe(HAPPairingHTTPCode.CONNECTION_AUTHORIZATION_REQUIRED);
435+
expect(error.response?.data).toEqual({ status: HAPStatus.INSUFFICIENT_PRIVILEGES });
436+
}
437+
});
438+
});
439+
332440
describe("error argument in pairing debug logs (fix f12ed233)", () => {
333441
let originalEnable: string | undefined;
334442
let originalLog: (...args: unknown[]) => void;

0 commit comments

Comments
 (0)