@@ -44,50 +44,54 @@ export async function generateHOTP(
4444 digits = DEFAULT_DIGITS ,
4545 algorithm = DEFAULT_ALGORITHM ,
4646 charSet = DEFAULT_CHAR_SET ,
47- } = { }
47+ } = { } ,
4848) {
4949 const byteCounter = intToBytes ( counter )
5050 const key = await crypto . subtle . importKey (
5151 'raw' ,
5252 secret ,
5353 { name : 'HMAC' , hash : algorithm } ,
5454 false ,
55- [ 'sign' ]
55+ [ 'sign' ] ,
5656 )
5757 const signature = await crypto . subtle . sign ( 'HMAC' , key , byteCounter )
58- const hashBytes = new Uint8Array ( signature )
59- // offset is always the last 4 bits of the signature; its value: 0-15
60- const offset = hashBytes [ hashBytes . length - 1 ] & 0xf
58+ const hashBytes = new Uint8Array ( signature )
59+ // offset is always the last 4 bits of the signature; its value: 0-15
60+ const offset = hashBytes [ hashBytes . length - 1 ] & 0xf
6161
62- let hotpVal = 0n
63- if ( digits === 6 ) {
64- // stay compatible with the authenticator apps and only use the bottom 32 bits of BigInt
65- hotpVal = 0n |
66- BigInt ( hashBytes [ offset ] & 0x7f ) << 24n |
67- BigInt ( hashBytes [ offset + 1 ] ) << 16n |
68- BigInt ( hashBytes [ offset + 2 ] ) << 8n |
69- BigInt ( hashBytes [ offset + 3 ] )
70- } else {
71- // otherwise create a 64bit value from the hashBytes
72- hotpVal = 0n |
73- BigInt ( hashBytes [ offset ] & 0x7f ) << 56n |
74- BigInt ( hashBytes [ offset + 1 ] ) << 48n |
75- BigInt ( hashBytes [ offset + 2 ] ) << 40n |
76- BigInt ( hashBytes [ offset + 3 ] ) << 32n |
77- BigInt ( hashBytes [ offset + 4 ] ) << 24n |
78-
79- // 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 )
84- }
62+ let hotpVal = 0n
63+ if ( digits === 6 ) {
64+ // stay compatible with the authenticator apps and only use the bottom 32 bits of BigInt
65+ hotpVal =
66+ 0n |
67+ ( BigInt ( hashBytes [ offset ] & 0x7f ) << 24n ) |
68+ ( BigInt ( hashBytes [ offset + 1 ] ) << 16n ) |
69+ ( BigInt ( hashBytes [ offset + 2 ] ) << 8n ) |
70+ BigInt ( hashBytes [ offset + 3 ] )
71+ } else {
72+ // otherwise create a 64bit value from the hashBytes
73+ hotpVal =
74+ 0n |
75+ ( BigInt ( hashBytes [ offset ] & 0x7f ) << 56n ) |
76+ ( BigInt ( hashBytes [ offset + 1 ] ) << 48n ) |
77+ ( BigInt ( hashBytes [ offset + 2 ] ) << 40n ) |
78+ ( BigInt ( hashBytes [ offset + 3 ] ) << 32n ) |
79+ ( BigInt ( hashBytes [ offset + 4 ] ) << 24n ) |
80+ // we have only 20 hashBytes; if offset is 15 these indexes are out of the hashBytes
81+ // fallback to the bytes at the start of the hashBytes
82+ ( BigInt ( hashBytes [ ( offset + 5 ) % 20 ] ) << 16n ) |
83+ ( BigInt ( hashBytes [ ( offset + 6 ) % 20 ] ) << 8n ) |
84+ BigInt ( hashBytes [ ( offset + 7 ) % 20 ] )
85+ }
8586
8687 let hotp = ''
8788 const charSetLength = BigInt ( charSet . length )
8889 for ( let i = 0 ; i < digits ; i ++ ) {
8990 hotp = charSet . charAt ( Number ( hotpVal % charSetLength ) ) + hotp
90- hotpVal = hotpVal / charSetLength
91+
92+ // Ensures hotpVal decreases at a fixed rate, independent of charSet length.
93+ // 10n is compatible with the original TOTP algorithm used in the authenticator apps.
94+ hotpVal = hotpVal / 10n
9195 }
9296
9397 return hotp
@@ -122,7 +126,7 @@ async function verifyHOTP(
122126 algorithm = DEFAULT_ALGORITHM ,
123127 charSet = DEFAULT_CHAR_SET ,
124128 window = DEFAULT_WINDOW ,
125- } = { }
129+ } = { } ,
126130) {
127131 for ( let i = counter - window ; i <= counter + window ; ++ i ) {
128132 if (
0 commit comments