Skip to content

Commit dc62fc1

Browse files
committed
fix: revert all commits back to bab1ccc
BREAKING CHANGE: Removes compatibilities added up to v3.0.0
1 parent 2242335 commit dc62fc1

File tree

3 files changed

+16
-100
lines changed

3 files changed

+16
-100
lines changed

README.md

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -99,13 +99,7 @@ const otpUri = getTOTPAuthUri({
9999
const code = await getCodeFromUser()
100100

101101
// now verify the code:
102-
const isValid = await verifyTOTP({
103-
otp: code,
104-
secret,
105-
period,
106-
digits,
107-
algorithm,
108-
})
102+
const isValid = await verifyTOTP({ otp: code, secret, period, digits, algorithm })
109103

110104
// if it's valid, save the secret, period, digits, and algorithm to the database
111105
// along with who it belongs to and use this info to verify the user when they

index.js

Lines changed: 11 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -37,61 +37,41 @@ const DEFAULT_PERIOD = 30
3737
* @param {string} [options.charSet='0123456789'] - The character set to use, defaults to the numbers 0-9.
3838
* @returns {Promise<string>} The generated HOTP.
3939
*/
40-
export async function generateHOTP(
40+
async function generateHOTP(
4141
secret,
4242
{
4343
counter = 0,
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)
5858
const hashBytes = new Uint8Array(signature)
59-
// offset is always the last 4 bits of the signature; its value: 0-15
59+
60+
// Use more bytes for longer OTPs
61+
const bytesNeeded = Math.ceil((digits * Math.log2(charSet.length)) / 8)
6062
const offset = hashBytes[hashBytes.length - 1] & 0xf
6163

64+
// Convert bytes to BigInt for larger numbers
6265
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])
66+
for (let i = 0; i < Math.min(bytesNeeded, hashBytes.length - offset); i++) {
67+
hotpVal = (hotpVal << 8n) | BigInt(hashBytes[offset + i])
8568
}
8669

8770
let hotp = ''
8871
const charSetLength = BigInt(charSet.length)
8972
for (let i = 0; i < digits; i++) {
9073
hotp = charSet.charAt(Number(hotpVal % charSetLength)) + hotp
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
74+
hotpVal = hotpVal / charSetLength
9575
}
9676

9777
return hotp
@@ -126,7 +106,7 @@ async function verifyHOTP(
126106
algorithm = DEFAULT_ALGORITHM,
127107
charSet = DEFAULT_CHAR_SET,
128108
window = DEFAULT_WINDOW,
129-
} = {},
109+
} = {}
130110
) {
131111
for (let i = counter - window; i <= counter + window; ++i) {
132112
if (

index.test.js

Lines changed: 4 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,7 @@
11
import assert from 'node:assert'
22
import { test } from 'node:test'
33
import base32Encode from 'base32-encode'
4-
import base32Decode from 'base32-decode'
5-
import {
6-
generateTOTP,
7-
getTOTPAuthUri,
8-
verifyTOTP,
9-
generateHOTP,
10-
} from './index.js'
4+
import { generateTOTP, getTOTPAuthUri, verifyTOTP } from './index.js'
115

126
test('OTP can be generated and verified', async () => {
137
const { secret, otp, algorithm, period, digits } = await generateTOTP()
@@ -26,7 +20,7 @@ test('options can be customized', async () => {
2620
digits: 8,
2721
secret: base32Encode(
2822
new TextEncoder().encode(Math.random().toString(16).slice(2)),
29-
'RFC4648',
23+
'RFC4648'
3024
).toString(),
3125
charSet: 'abcdef',
3226
}
@@ -139,67 +133,15 @@ test('OTP with digits > 6 should not pad with first character of charSet', async
139133
assert.match(
140134
otp,
141135
new RegExp(`^[${charSet}]{12}$`),
142-
'OTP should be 12 characters from the charSet',
136+
'OTP should be 12 characters from the charSet'
143137
)
144138

145139
// The first 6 characters should not all be 'A' (first char of charSet)
146140
const firstSixChars = otp.slice(0, 6)
147141
assert.notStrictEqual(
148142
firstSixChars,
149143
'A'.repeat(6),
150-
'First 6 characters should not all be A',
144+
'First 6 characters should not all be A'
151145
)
152146
}
153147
})
154-
155-
test('generateHOTP works with maximum HMAC offset value', async () => {
156-
await assert.doesNotReject(async () => {
157-
// These specific secret and counter values will cause offset to be 15
158-
const secret = '6YY3NUMNTQ73NRH3'
159-
const counter = 57988074
160-
await generateHOTP(base32Decode(secret, 'RFC4648'), {
161-
counter,
162-
digits: 12, // trigger the use of the 64bit htopVal
163-
algorithm: 'SHA-1',
164-
charSet: '0123456789',
165-
})
166-
})
167-
})
168-
169-
test('20 digits OTP should not pad with first character of charSet regardless of the charSet length', async () => {
170-
const longCharSet = 'ABCDEFGHIJKLMNPQRSTUVWXYZ123456789'
171-
const shortCharSet = 'ABCDEFGHIJK'
172-
173-
async function generate20DigitCodeWithCharSet(charSet) {
174-
const iterations = 100
175-
let allOtps = []
176-
177-
for (let i = 0; i < iterations; i++) {
178-
const { otp } = await generateTOTP({
179-
algorithm: 'SHA-256',
180-
charSet,
181-
digits: 20,
182-
period: 60 * 30,
183-
})
184-
allOtps.push(otp)
185-
186-
// Verify the OTP only contains characters from the charSet
187-
assert.match(
188-
otp,
189-
new RegExp(`^[${charSet}]{20}$`),
190-
'OTP should be 20 characters from the charSet',
191-
)
192-
193-
// The first 6 characters should not all be 'A' (first char of charSet)
194-
const firstSixChars = otp.slice(0, 6)
195-
assert.notStrictEqual(
196-
firstSixChars,
197-
'A'.repeat(6),
198-
'First 6 characters should not all be A',
199-
)
200-
}
201-
}
202-
203-
await generate20DigitCodeWithCharSet(shortCharSet)
204-
await generate20DigitCodeWithCharSet(longCharSet)
205-
})

0 commit comments

Comments
 (0)