@@ -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