Skip to content

Commit 3bf1701

Browse files
authored
fix: prevent certain offset values to address out of hashBytes (#23)
1 parent 09f5f48 commit 3bf1701

File tree

2 files changed

+34
-15
lines changed

2 files changed

+34
-15
lines changed

index.js

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ 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-
async function generateHOTP(
40+
export async function generateHOTP(
4141
secret,
4242
{
4343
counter = 0,
@@ -56,28 +56,31 @@ async function generateHOTP(
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; 0-15
60-
const offset = hashBytes[hashBytes.length-1] & 0xf
59+
// offset is always the last 4 bits of the signature; its value: 0-15
60+
const offset = hashBytes[hashBytes.length - 1] & 0xf
6161

6262
let hotpVal = 0n
6363
if (digits === 6) {
6464
// stay compatible with the authenticator apps and only use the bottom 32 bits of BigInt
6565
hotpVal = 0n |
66-
BigInt((hashBytes[offset] & 0x7f)) << 24n |
67-
BigInt((hashBytes[offset + 1])) << 16n |
68-
BigInt((hashBytes[offset + 2])) << 8n |
66+
BigInt(hashBytes[offset] & 0x7f) << 24n |
67+
BigInt(hashBytes[offset + 1]) << 16n |
68+
BigInt(hashBytes[offset + 2]) << 8n |
6969
BigInt(hashBytes[offset + 3])
7070
} else {
7171
// otherwise create a 64bit value from the hashBytes
7272
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-
BigInt((hashBytes[offset + 5])) << 16n |
79-
BigInt((hashBytes[offset + 6])) << 8n |
80-
BigInt(hashBytes[offset + 7])
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)
8184
}
8285

8386
let hotp = ''

index.test.js

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

67
test('OTP can be generated and verified', async () => {
78
const { secret, otp, algorithm, period, digits } = await generateTOTP()
@@ -145,3 +146,18 @@ test('OTP with digits > 6 should not pad with first character of charSet', async
145146
)
146147
}
147148
})
149+
150+
151+
test('generateHOTP works with maximum HMAC offset value', async () => {
152+
await assert.doesNotReject( async() => {
153+
// These specific secret and counter values will cause offset to be 15
154+
const secret = '6YY3NUMNTQ73NRH3';
155+
const counter = 57988074;
156+
await generateHOTP(base32Decode(secret, 'RFC4648'),{
157+
counter,
158+
digits: 12, // trigger the use of the 64bit htopVal
159+
algorithm: 'SHA-1',
160+
charSet: '0123456789',
161+
});
162+
});
163+
})

0 commit comments

Comments
 (0)