@@ -56,22 +56,44 @@ async function generateHOTP(
5656 )
5757 const signature = await crypto . subtle . sign ( 'HMAC' , key , byteCounter )
5858 const hashBytes = new Uint8Array ( signature )
59-
60- // Use more bytes for longer OTPs
61- const bytesNeeded = Math . ceil ( ( digits * Math . log2 ( charSet . length ) ) / 8 )
59+ // offset is always the last 4 bits of the signature; its value: 0-15
6260 const offset = hashBytes [ hashBytes . length - 1 ] & 0xf
6361
64- // Convert bytes to BigInt for larger numbers
6562 let hotpVal = 0n
66- for ( let i = 0 ; i < Math . min ( bytesNeeded , hashBytes . length - offset ) ; i ++ ) {
67- hotpVal = ( hotpVal << 8n ) | BigInt ( hashBytes [ offset + i ] )
63+ // the original specification allows any amount of digits between 4 and 10,
64+ // so stay on the 32bit number if the digits are less then or equal to 10.
65+ if ( digits <= 10 ) {
66+ // stay compatible with the authenticator apps and only use the bottom 32 bits of BigInt
67+ hotpVal =
68+ 0n |
69+ ( BigInt ( hashBytes [ offset ] & 0x7f ) << 24n ) |
70+ ( BigInt ( hashBytes [ offset + 1 ] ) << 16n ) |
71+ ( BigInt ( hashBytes [ offset + 2 ] ) << 8n ) |
72+ BigInt ( hashBytes [ offset + 3 ] )
73+ } else {
74+ // otherwise create a 64bit value from the hashBytes
75+ hotpVal =
76+ 0n |
77+ ( BigInt ( hashBytes [ offset ] & 0x7f ) << 56n ) |
78+ ( BigInt ( hashBytes [ offset + 1 ] ) << 48n ) |
79+ ( BigInt ( hashBytes [ offset + 2 ] ) << 40n ) |
80+ ( BigInt ( hashBytes [ offset + 3 ] ) << 32n ) |
81+ ( BigInt ( hashBytes [ offset + 4 ] ) << 24n ) |
82+ // we have only 20 hashBytes; if offset is 15 these indexes are out of the hashBytes
83+ // fallback to the bytes at the start of the hashBytes
84+ ( BigInt ( hashBytes [ ( offset + 5 ) % 20 ] ) << 16n ) |
85+ ( BigInt ( hashBytes [ ( offset + 6 ) % 20 ] ) << 8n ) |
86+ BigInt ( hashBytes [ ( offset + 7 ) % 20 ] )
6887 }
6988
7089 let hotp = ''
7190 const charSetLength = BigInt ( charSet . length )
7291 for ( let i = 0 ; i < digits ; i ++ ) {
7392 hotp = charSet . charAt ( Number ( hotpVal % charSetLength ) ) + hotp
74- hotpVal = hotpVal / charSetLength
93+
94+ // Ensures hotpVal decreases at a fixed rate, independent of charSet length.
95+ // 10n is compatible with the original TOTP algorithm used in the authenticator apps.
96+ hotpVal = hotpVal / 10n
7597 }
7698
7799 return hotp
@@ -149,8 +171,8 @@ export async function generateTOTP({
149171 charSet = DEFAULT_CHAR_SET ,
150172} = { } ) {
151173 const otp = await generateHOTP ( base32Decode ( secret , 'RFC4648' ) , {
152- counter : getCounter ( period ) ,
153- digits,
174+ counter : getCounter ( Number ( period ) ) ,
175+ digits : Number ( digits ) ,
154176 algorithm,
155177 charSet,
156178 } )
0 commit comments