|
1 | 1 | import axios, { AxiosError, AxiosResponse } from "axios"; |
2 | 2 | import crypto from "crypto"; |
| 3 | +import createDebug from "debug"; |
3 | 4 | import { Agent } from "http"; |
4 | 5 | import tweetnacl from "tweetnacl"; |
5 | 6 | import { PairingStates, PairMethods, TLVValues } from "../internal-types"; |
@@ -328,6 +329,103 @@ describe("HAPServer", () => { |
328 | 329 | }); |
329 | 330 | }); |
330 | 331 |
|
| 332 | + describe("error argument in pairing debug logs (fix f12ed233)", () => { |
| 333 | + let originalEnable: string; |
| 334 | + let originalLog: (...args: unknown[]) => void; |
| 335 | + let captured: unknown[][]; |
| 336 | + |
| 337 | + beforeEach(() => { |
| 338 | + captured = []; |
| 339 | + // createDebug.disable() returns the namespaces it just disabled, so we |
| 340 | + // can faithfully restore them via createDebug.enable() in afterEach. |
| 341 | + originalEnable = createDebug.disable(); |
| 342 | + // route all enabled debug output through our capture function |
| 343 | + // eslint-disable-next-line @typescript-eslint/no-explicit-any |
| 344 | + originalLog = (createDebug as any).log; |
| 345 | + // eslint-disable-next-line @typescript-eslint/no-explicit-any |
| 346 | + (createDebug as any).log = function(this: { namespace: string }, ...args: unknown[]) { |
| 347 | + captured.push([this.namespace, ...args]); |
| 348 | + }; |
| 349 | + createDebug.enable("HAP-NodeJS:HAPServer"); |
| 350 | + }); |
| 351 | + |
| 352 | + afterEach(() => { |
| 353 | + // eslint-disable-next-line @typescript-eslint/no-explicit-any |
| 354 | + (createDebug as any).log = originalLog; |
| 355 | + createDebug.disable(); |
| 356 | + if (originalEnable) { |
| 357 | + createDebug.enable(originalEnable); |
| 358 | + } |
| 359 | + }); |
| 360 | + |
| 361 | + test("M5 decrypt failure should pass the error to the debug logger", async () => { |
| 362 | + server = new HAPServer(accessoryInfoUnpaired); |
| 363 | + const [port] = await bindServer(server); |
| 364 | + |
| 365 | + const pairSetup = new PairSetupClient(port, httpAgent); |
| 366 | + const responseM1 = await pairSetup.sendM1(); |
| 367 | + const M2 = pairSetup.parseM2(responseM1.data); |
| 368 | + const M3 = await pairSetup.prepareM3(M2, accessoryInfoUnpaired.pincode); |
| 369 | + const responseM3 = await pairSetup.sendM3(M3); |
| 370 | + pairSetup.parseM4(responseM3.data, M3); |
| 371 | + |
| 372 | + // craft an M5 with bogus ciphertext+tag (passes the >=16 length check |
| 373 | + // from 0719059b but fails MAC verification) |
| 374 | + await axios.post( |
| 375 | + `http://localhost:${port}/pair-setup`, |
| 376 | + tlv.encode( |
| 377 | + TLVValues.STATE, PairingStates.M5, |
| 378 | + TLVValues.ENCRYPTED_DATA, Buffer.alloc(48), // 32-byte payload + 16-byte tag, all zeros |
| 379 | + ), |
| 380 | + { httpAgent, responseType: "arraybuffer" }, |
| 381 | + ); |
| 382 | + |
| 383 | + // captured rows: [namespace, formatString, ...formatArgs] |
| 384 | + const m5Lines = captured.filter(line => typeof line[1] === "string" |
| 385 | + && (line[1] as string).includes("Error while decrypting and verifying M5 subTlv")); |
| 386 | + expect(m5Lines.length).toBeGreaterThan(0); |
| 387 | + |
| 388 | + // the format string contains two %s — the first is the username, the |
| 389 | + // second is the error. Without the fix only the username was passed. |
| 390 | + const line = m5Lines[0]; |
| 391 | + expect(line[2]).toBe(accessoryInfoUnpaired.username); |
| 392 | + // line[3] should be the error caught from decipher.final() — the |
| 393 | + // exact constructor varies by Node version / openssl binding, so |
| 394 | + // just assert it's a thrown error-like object with a message. |
| 395 | + expect(line[3]).toBeDefined(); |
| 396 | + expect((line[3] as Error).message).toEqual(expect.any(String)); |
| 397 | + }); |
| 398 | + |
| 399 | + test("pair-verify M3 decrypt failure should pass the error to the debug logger", async () => { |
| 400 | + server = new HAPServer(accessoryInfoPaired); |
| 401 | + const [port] = await bindServer(server); |
| 402 | + |
| 403 | + const pairVerify = new PairVerifyClient(port, httpAgent); |
| 404 | + const responseM1 = await pairVerify.sendM1(); |
| 405 | + pairVerify.parseM2(responseM1.data, serverInfoPaired); |
| 406 | + |
| 407 | + await axios.post( |
| 408 | + `http://localhost:${port}/pair-verify`, |
| 409 | + tlv.encode( |
| 410 | + TLVValues.STATE, PairingStates.M3, |
| 411 | + TLVValues.ENCRYPTED_DATA, Buffer.alloc(48), |
| 412 | + ), |
| 413 | + { httpAgent, responseType: "arraybuffer" }, |
| 414 | + ); |
| 415 | + |
| 416 | + const m3Lines = captured.filter(line => typeof line[1] === "string" |
| 417 | + && (line[1] as string).includes("M3: Failed to decrypt and/or verify")); |
| 418 | + expect(m3Lines.length).toBeGreaterThan(0); |
| 419 | + const line = m3Lines[0]; |
| 420 | + expect(line[2]).toBe(accessoryInfoPaired.username); |
| 421 | + // line[3] should be the error caught from decipher.final() — the |
| 422 | + // exact constructor varies by Node version / openssl binding, so |
| 423 | + // just assert it's a thrown error-like object with a message. |
| 424 | + expect(line[3]).toBeDefined(); |
| 425 | + expect((line[3] as Error).message).toEqual(expect.any(String)); |
| 426 | + }); |
| 427 | + }); |
| 428 | + |
331 | 429 | describe("M1 reset prevention (fix d4c81be0)", () => { |
332 | 430 | test("/pair-setup should reject a second M1 on a connection with in-progress state", async () => { |
333 | 431 | server = new HAPServer(accessoryInfoUnpaired); |
|
0 commit comments