Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 9 additions & 6 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,17 +77,20 @@ export async function generateHOTP(
BigInt(hashBytes[offset + 4]) << 24n |

// we have only 20 hashBytes; if offset is 15 these indexes are out of the hashBytes
// fallback to zero
BigInt(hashBytes[offset + 5] ?? 0n) << 16n |
BigInt(hashBytes[offset + 6] ?? 0n) << 8n |
BigInt(hashBytes[offset + 7] ?? 0n)
// fallback to the bytes at the start of the hashBytes
BigInt(hashBytes[(offset + 5) % 20]) << 16n |
BigInt(hashBytes[(offset + 6) % 20]) << 8n |
BigInt(hashBytes[(offset + 7) % 20])
}

let hotp = ''
const charSetLength = BigInt(charSet.length)
for (let i = 0; i < digits; i++) {
hotp = charSet.charAt(Number(hotpVal % charSetLength)) + hotp
hotpVal = hotpVal / charSetLength
hotp = charSet.charAt(Number(hotpVal % charSetLength)) + hotp

// Ensures hotpVal decreases at a fixed rate, independent of charSet length.
// 10n is compatible with the original TOTP algorithm used in the authenticator apps.
hotpVal = hotpVal / 10n
}

return hotp
Expand Down
40 changes: 40 additions & 0 deletions index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -161,3 +161,43 @@ test('generateHOTP works with maximum HMAC offset value', async () => {
});
});
})

test('20 digits OTP should not pad with first character of charSet regardless of the charSet length', async () => {
const longCharSet = 'ABCDEFGHIJKLMNPQRSTUVWXYZ123456789'
const shortCharSet = 'ABCDEFGHIJK'

async function generate20DigitCodeWithCharSet(charSet) {

const iterations = 100
let allOtps = []

for (let i = 0; i < iterations; i++) {
const { otp } = await generateTOTP({
algorithm: 'SHA-256',
charSet,
digits: 20,
period: 60 * 30,
})
allOtps.push(otp)

// Verify the OTP only contains characters from the charSet
assert.match(
otp,
new RegExp(`^[${charSet}]{20}$`),
'OTP should be 20 characters from the charSet'
)

// The first 6 characters should not all be 'A' (first char of charSet)
const firstSixChars = otp.slice(0, 6)
assert.notStrictEqual(
firstSixChars,
'A'.repeat(6),
'First 6 characters should not all be A'
)
}
}

await generate20DigitCodeWithCharSet(shortCharSet);
await generate20DigitCodeWithCharSet(longCharSet);

})