File tree Expand file tree Collapse file tree 2 files changed +49
-6
lines changed Expand file tree Collapse file tree 2 files changed +49
-6
lines changed Original file line number Diff line number Diff line change @@ -77,17 +77,20 @@ export async function generateHOTP(
7777 BigInt ( hashBytes [ offset + 4 ] ) << 24n |
7878
7979 // we have only 20 hashBytes; if offset is 15 these indexes are out of the hashBytes
80- // fallback to zero
81- BigInt ( hashBytes [ offset + 5 ] ?? 0n ) << 16n |
82- BigInt ( hashBytes [ offset + 6 ] ?? 0n ) << 8n |
83- BigInt ( hashBytes [ offset + 7 ] ?? 0n )
80+ // fallback to the bytes at the start of the hashBytes
81+ BigInt ( hashBytes [ ( offset + 5 ) % 20 ] ) << 16n |
82+ BigInt ( hashBytes [ ( offset + 6 ) % 20 ] ) << 8n |
83+ BigInt ( hashBytes [ ( offset + 7 ) % 20 ] )
8484 }
8585
8686 let hotp = ''
8787 const charSetLength = BigInt ( charSet . length )
8888 for ( let i = 0 ; i < digits ; i ++ ) {
89- hotp = charSet . charAt ( Number ( hotpVal % charSetLength ) ) + hotp
90- hotpVal = hotpVal / charSetLength
89+ hotp = charSet . charAt ( Number ( hotpVal % charSetLength ) ) + hotp
90+
91+ // Ensures hotpVal decreases at a fixed rate, independent of charSet length.
92+ // 10n is compatible with the original TOTP algorithm used in the authenticator apps.
93+ hotpVal = hotpVal / 10n
9194 }
9295
9396 return hotp
Original file line number Diff line number Diff line change @@ -161,3 +161,43 @@ test('generateHOTP works with maximum HMAC offset value', async () => {
161161 } ) ;
162162 } ) ;
163163} )
164+
165+ test ( '20 digits OTP should not pad with first character of charSet regardless of the charSet length' , async ( ) => {
166+ const longCharSet = 'ABCDEFGHIJKLMNPQRSTUVWXYZ123456789'
167+ const shortCharSet = 'ABCDEFGHIJK'
168+
169+ async function generate20DigitCodeWithCharSet ( charSet ) {
170+
171+ const iterations = 100
172+ let allOtps = [ ]
173+
174+ for ( let i = 0 ; i < iterations ; i ++ ) {
175+ const { otp } = await generateTOTP ( {
176+ algorithm : 'SHA-256' ,
177+ charSet,
178+ digits : 20 ,
179+ period : 60 * 30 ,
180+ } )
181+ allOtps . push ( otp )
182+
183+ // Verify the OTP only contains characters from the charSet
184+ assert . match (
185+ otp ,
186+ new RegExp ( `^[${ charSet } ]{20}$` ) ,
187+ 'OTP should be 20 characters from the charSet'
188+ )
189+
190+ // The first 6 characters should not all be 'A' (first char of charSet)
191+ const firstSixChars = otp . slice ( 0 , 6 )
192+ assert . notStrictEqual (
193+ firstSixChars ,
194+ 'A' . repeat ( 6 ) ,
195+ 'First 6 characters should not all be A'
196+ )
197+ }
198+ }
199+
200+ await generate20DigitCodeWithCharSet ( shortCharSet ) ;
201+ await generate20DigitCodeWithCharSet ( longCharSet ) ;
202+
203+ } )
You can’t perform that action at this time.
0 commit comments