@@ -329,6 +329,125 @@ 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+ // use a pincode without leading whitespace — node's HTTP parser trims
375+ // header values, so the fixture's " 031-45-154" can't round-trip and
376+ // any wrong value with the same leading space hits the wrong-length
377+ // branch on the server, not the constant-time same-length comparison.
378+ const info = AccessoryInfo . create ( serverUsername ) ;
379+ // @ts -expect-error: private access
380+ info . setupID = Accessory . _generateSetupID ( ) ;
381+ info . displayName = "Outlet" ;
382+ info . category = 7 ;
383+ info . pincode = "031-45-154" ;
384+
385+ server = new HAPServer ( info ) ;
386+ server . allowInsecureRequest = true ;
387+ const [ port ] = await bindServer ( server ) ;
388+
389+ const wrongPincode = "999-99-999" ; // same length as "031-45-154", different content
390+ try {
391+ await axios . put (
392+ `http://localhost:${ port } /characteristics` ,
393+ { characteristics : [ { aid : 1 , iid : 9 , value : true } ] } ,
394+ { httpAgent, headers : { authorization : wrongPincode } } ,
395+ ) ;
396+ fail ( "Expected CONNECTION_AUTHORIZATION_REQUIRED response" ) ;
397+ } catch ( error ) {
398+ expect ( error ) . toBeInstanceOf ( AxiosError ) ;
399+ expect ( error . response ?. status ) . toBe ( HAPPairingHTTPCode . CONNECTION_AUTHORIZATION_REQUIRED ) ;
400+ }
401+ } ) ;
402+
403+ test ( "PUT /characteristics with correct pincode should be authorized" , async ( ) => {
404+ // use a pincode without leading whitespace — HTTP header values get trimmed
405+ // by node's parser so the global test fixture's " 031-45-154" can't round-trip.
406+ const info = AccessoryInfo . create ( serverUsername ) ;
407+ // @ts -expect-error: private access
408+ info . setupID = Accessory . _generateSetupID ( ) ;
409+ info . displayName = "Outlet" ;
410+ info . category = 7 ;
411+ info . pincode = "031-45-154" ;
412+
413+ server = new HAPServer ( info ) ;
414+ server . allowInsecureRequest = true ;
415+ const [ port ] = await bindServer ( server ) ;
416+
417+ server . on ( HAPServerEventTypes . SET_CHARACTERISTICS , ( connection , writeRequest , callback ) => {
418+ callback ( undefined , { characteristics : writeRequest . characteristics . map ( c => ( { aid : c . aid , iid : c . iid , status : HAPStatus . SUCCESS } ) ) } ) ;
419+ } ) ;
420+
421+ const response = await axios . put (
422+ `http://localhost:${ port } /characteristics` ,
423+ { characteristics : [ { aid : 1 , iid : 9 , value : true } ] } ,
424+ { httpAgent, headers : { authorization : info . pincode } } ,
425+ ) ;
426+ // a successful insecure PUT returns 204 NO_CONTENT (or 200 if there's a response body)
427+ expect ( response . status ) . toBeGreaterThanOrEqual ( 200 ) ;
428+ expect ( response . status ) . toBeLessThan ( 300 ) ;
429+ } ) ;
430+
431+ test ( "POST /resource with wrong-length authorization should not crash timingSafeEqual" , async ( ) => {
432+ server = new HAPServer ( accessoryInfoUnpaired ) ;
433+ server . allowInsecureRequest = true ;
434+ const [ port ] = await bindServer ( server ) ;
435+
436+ try {
437+ await axios . post (
438+ `http://localhost:${ port } /resource` ,
439+ { "resource-type" : "image" , "image-width" : 100 , "image-height" : 100 } ,
440+ { httpAgent, headers : { authorization : "short" } } ,
441+ ) ;
442+ fail ( "Expected CONNECTION_AUTHORIZATION_REQUIRED response" ) ;
443+ } catch ( error ) {
444+ expect ( error ) . toBeInstanceOf ( AxiosError ) ;
445+ expect ( error . response ?. status ) . toBe ( HAPPairingHTTPCode . CONNECTION_AUTHORIZATION_REQUIRED ) ;
446+ expect ( error . response ?. data ) . toEqual ( { status : HAPStatus . INSUFFICIENT_PRIVILEGES } ) ;
447+ }
448+ } ) ;
449+ } ) ;
450+
332451 describe ( "error argument in pairing debug logs (fix f12ed233)" , ( ) => {
333452 let originalEnable : string ;
334453 let originalLog : ( ...args : unknown [ ] ) => void ;
0 commit comments