@@ -56,22 +56,44 @@ async function generateHOTP(
56
56
)
57
57
const signature = await crypto . subtle . sign ( 'HMAC' , key , byteCounter )
58
58
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
62
60
const offset = hashBytes [ hashBytes . length - 1 ] & 0xf
63
61
64
- // Convert bytes to BigInt for larger numbers
65
62
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 ] )
68
87
}
69
88
70
89
let hotp = ''
71
90
const charSetLength = BigInt ( charSet . length )
72
91
for ( let i = 0 ; i < digits ; i ++ ) {
73
92
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
75
97
}
76
98
77
99
return hotp
@@ -149,8 +171,8 @@ export async function generateTOTP({
149
171
charSet = DEFAULT_CHAR_SET ,
150
172
} = { } ) {
151
173
const otp = await generateHOTP ( base32Decode ( secret , 'RFC4648' ) , {
152
- counter : getCounter ( period ) ,
153
- digits,
174
+ counter : getCounter ( Number ( period ) ) ,
175
+ digits : Number ( digits ) ,
154
176
algorithm,
155
177
charSet,
156
178
} )
0 commit comments