Replay protection and backup code hardening (v1.1.0) - #19
Merged
Conversation
verifyTotpWithDelta reports the matched time step so callers can store a last-used step per user and reject replays, which RFC 6238 requires and a bare boolean cannot support. verifyTotp keeps its boolean return: changing it to an object would make every existing truthiness check silently pass. The README login flow now shows the replay-safe pattern as the default. Backup codes: default length 8 -> 10 characters (40 -> 50 bits), digests can be keyed with HMAC-SHA-256 so a leaked database row cannot be brute-forced offline, and verifyBackupCode normalises user input (case, dashes, whitespace), compares every digest in constant time with no early exit, and returns the remaining list with the consumed digest removed so single-use is built into the API. Digests stored by v1.0.x (grouped form, unkeyed) still verify via a legacy fallback. Closes #7, closes #8
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #7 and #8, the two security findings from the pre-launch review. Version is bumped to 1.1.0, so merging this publishes to npm through the new release pipeline.
verifyTotpWithDelta (#7)
New function returning
{ valid, delta, step }.stepis the absolute time step the code matched; storing it per user and requiring each login to beat it prevents replay within the drift window, which RFC 6238 requires. The README login flow now shows this as the default pattern, with atotp_last_stepcolumn.Deliberate decision:
verifyTotpkeeps returning a boolean. Changing its return type to an object would makeif (await verifyTotp(...))always pass for existing users, a silent auth bypass. The rich result lives in a new function instead, so nothing existing can break.Every candidate in the window is still computed and compared in constant time with no early exit. If a code matches more than one step (a 1 in 10^digits coincidence), the earliest step is reported.
Backup codes (#8)
generateBackupCodes({ key })uses HMAC-SHA-256, so leaked digests are useless without the key, matching the vault's threat modelverifyBackupCode(input, hashed, { key }): normalises case, dashes, and whitespace, compares every stored digest in constant time with no early exit, and returns{ valid, remaining }with the consumed digest removed, so the single-use rule is built into the API rather than left to the integratorTests
116 passing, 16 new: delta and step values across the window, the last-used-step replay pattern end to end, keyed vs unkeyed digests, input normalisation, single-use consumption including duplicate digests, and a v1.0.x legacy digest regression test.