File tree Expand file tree Collapse file tree
Expand file tree Collapse file tree Original file line number Diff line number Diff line change @@ -88,6 +88,34 @@ describe("HOTP and TOTP Generation Tests", () => {
8888 expect ( isValid ) . toBe ( false ) ;
8989 } ) ;
9090
91+ it ( "should check every TOTP window candidate without returning on first match" , async ( ) => {
92+ vi . resetModules ( ) ;
93+ const sign = vi . fn ( async ( ) => {
94+ const buffer = new ArrayBuffer ( 20 ) ;
95+ const result = new Uint8Array ( buffer ) ;
96+ result [ 3 ] = 1 ;
97+ return buffer ;
98+ } ) ;
99+ vi . doMock ( "./hmac" , ( ) => ( {
100+ createHMAC : ( ) => ( {
101+ sign,
102+ } ) ,
103+ } ) ) ;
104+ try {
105+ const { createOTP : createMockedOTP } = await import ( "./otp" ) ;
106+
107+ const isValid = await createMockedOTP ( "1234567890" ) . verify ( "000001" , {
108+ window : 1 ,
109+ } ) ;
110+
111+ expect ( isValid ) . toBe ( true ) ;
112+ expect ( sign ) . toHaveBeenCalledTimes ( 3 ) ;
113+ } finally {
114+ vi . doUnmock ( "./hmac" ) ;
115+ vi . resetModules ( ) ;
116+ }
117+ } ) ;
118+
91119 it ( "should generate a valid QR code URL" , ( ) => {
92120 const secret = "1234567890" ;
93121 const issuer = "my-site.com" ;
Original file line number Diff line number Diff line change @@ -5,6 +5,19 @@ import type { SHAFamily } from "./type";
55const defaultPeriod = 30 ;
66const defaultDigits = 6 ;
77
8+ /**
9+ * loops over `expected.length` so timing never depends on input length
10+ *
11+ * @internal
12+ */
13+ function constantTimeEqualOTP ( input : string , expected : string ) : boolean {
14+ let difference = input . length ^ expected . length ;
15+ for ( let i = 0 ; i < expected . length ; i ++ ) {
16+ difference |= input . charCodeAt ( i ) ^ expected . charCodeAt ( i ) ;
17+ }
18+ return difference === 0 ;
19+ }
20+
821async function generateHOTP (
922 secret : string ,
1023 {
@@ -66,16 +79,15 @@ async function verifyTOTP(
6679) {
6780 const milliseconds = period * 1000 ;
6881 const counter = Math . floor ( Date . now ( ) / milliseconds ) ;
82+ let matched = false ;
6983 for ( let i = - window ; i <= window ; i ++ ) {
7084 const generatedOTP = await generateHOTP ( secret , {
7185 counter : counter + i ,
7286 digits,
7387 } ) ;
74- if ( otp === generatedOTP ) {
75- return true ;
76- }
88+ matched = constantTimeEqualOTP ( otp , generatedOTP ) || matched ;
7789 }
78- return false ;
90+ return matched ;
7991}
8092
8193/**
You can’t perform that action at this time.
0 commit comments