File tree Expand file tree Collapse file tree 2 files changed +44
-8
lines changed Expand file tree Collapse file tree 2 files changed +44
-8
lines changed Original file line number Diff line number Diff line change @@ -56,17 +56,22 @@ async function generateHOTP(
5656 )
5757 const signature = await crypto . subtle . sign ( 'HMAC' , key , byteCounter )
5858 const hashBytes = new Uint8Array ( signature )
59- const offset = hashBytes [ 19 ] & 0xf
60- let hotpVal =
61- ( ( hashBytes [ offset ] & 0x7f ) << 24 ) |
62- ( ( hashBytes [ offset + 1 ] & 0xff ) << 16 ) |
63- ( ( hashBytes [ offset + 2 ] & 0xff ) << 8 ) |
64- ( hashBytes [ offset + 3 ] & 0xff )
59+
60+ // Use more bytes for longer OTPs
61+ const bytesNeeded = Math . ceil ( ( digits * Math . log2 ( charSet . length ) ) / 8 )
62+ const offset = hashBytes [ hashBytes . length - 1 ] & 0xf
63+
64+ // Convert bytes to BigInt for larger numbers
65+ let hotpVal = 0n
66+ for ( let i = 0 ; i < Math . min ( bytesNeeded , hashBytes . length - offset ) ; i ++ ) {
67+ hotpVal = ( hotpVal << 8n ) | BigInt ( hashBytes [ offset + i ] )
68+ }
6569
6670 let hotp = ''
71+ const charSetLength = BigInt ( charSet . length )
6772 for ( let i = 0 ; i < digits ; i ++ ) {
68- hotp = charSet . charAt ( hotpVal % charSet . length ) + hotp
69- hotpVal = Math . floor ( hotpVal / charSet . length )
73+ hotp = charSet . charAt ( Number ( hotpVal % charSetLength ) ) + hotp
74+ hotpVal = hotpVal / charSetLength
7075 }
7176
7277 return hotp
Original file line number Diff line number Diff line change @@ -114,3 +114,34 @@ test('OTP Auth URI can be generated', async () => {
114114 } )
115115 assert . match ( uri , / ^ o t p a u t h : \/ \/ t o t p \/ ( .* ) \? / )
116116} )
117+
118+ test ( 'OTP with digits > 6 should not pad with first character of charSet' , async ( ) => {
119+ const charSet = 'ABCDEFGHIJKLMNPQRSTUVWXYZ123456789'
120+ const iterations = 100
121+ let allOtps = [ ]
122+
123+ for ( let i = 0 ; i < iterations ; i ++ ) {
124+ const { otp } = await generateTOTP ( {
125+ algorithm : 'SHA-256' ,
126+ charSet,
127+ digits : 12 ,
128+ period : 60 * 30 ,
129+ } )
130+ allOtps . push ( otp )
131+
132+ // Verify the OTP only contains characters from the charSet
133+ assert . match (
134+ otp ,
135+ new RegExp ( `^[${ charSet } ]{12}$` ) ,
136+ 'OTP should be 12 characters from the charSet'
137+ )
138+
139+ // The first 6 characters should not all be 'A' (first char of charSet)
140+ const firstSixChars = otp . slice ( 0 , 6 )
141+ assert . notStrictEqual (
142+ firstSixChars ,
143+ 'A' . repeat ( 6 ) ,
144+ 'First 6 characters should not all be A'
145+ )
146+ }
147+ } )
You can’t perform that action at this time.
0 commit comments