Conversation
decodeSecret enforced no length. Its only size test was len(key) == 0, so any secret that decoded to at least one byte was accepted and used as an HMAC key. "MZXW6YTB" decodes to 5 bytes, and a 40-bit TOTP secret is enumerable. secretLen has been declared as 20 three functions away the whole time and nothing compared against it. Enroll always mints secretLen bytes, so nothing this package generates was affected. The exposure is a secret arriving from outside: migrated from another implementation, pasted by a user, or restored from a store that truncated it. Short inputs did fail before, which is what made this easy to miss, and they failed for the wrong reason. The encoding was chosen by len(s)%8 != 0, so a length that is not a multiple of 8 took the padded encoding and was rejected as malformed. That reads like a length control and is not one: every length that is a multiple of 8 took the unpadded branch and decoded cleanly at any size. The tests are built around that trap, and the four that go red under the old check are exactly the multiple-of-8 cases. The encoding branch is gone with it, and that is a consequence rather than a separate decision. 20 bytes is exactly four 40-bit base32 groups, so it encodes to 32 characters with no padding and the padded and unpadded forms of a valid secret are byte-identical. Measured before removing it: the branch could no longer select an encoding for anything the length check would accept. Also caps the encoded input before decoding rather than after. Closes #357 Signed-off-by: Jose <75870284+Jaro-c@users.noreply.github.com>
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.
decodeSecretenforced no length. Its only size test waslen(key) == 0, so any secret that decoded to at least one byte was accepted and used as an HMAC key.secretLenhas been declared as 20 three functions away the whole time and nothing compared against it.Measured on develop at 6396536:
MZXW6YTBAAAAAAAAMZXW6YTBMZXW6YTBA 5-byte secret is a 40-bit keyspace, and
generateTOTPproduced codes from it without complaint.Enrollalways mintssecretLenbytes, so nothing this package generates was affected. The exposure is a secret arriving from outside: migrated from another TOTP implementation, pasted by a user, or restored from a store that truncated it.The trap, because it is what the tests are built around
Short inputs did fail before this, and they failed for the wrong reason. The encoding was selected by
len(s)%8 != 0, so a length that is not a multiple of 8 got the padded encoding and was rejected as malformed base32. That reads like a length control and is not one. Every length that is a multiple of 8 took the unpadded branch and decoded cleanly at whatever size it produced.The four subtests that go red when the old
len(key) == 0check is restored are exactly the multiple-of-8 cases, which is what makes them a test of the length check rather than of the encoding heuristic.The encoding branch is gone too
That is a consequence of the length check, not a separate decision, and I measured it before removing anything: 20 bytes is exactly four 40-bit base32 groups, so it encodes to 32 characters with no padding and the padded and unpadded forms of a valid secret are byte-identical. The branch could therefore no longer select an encoding for any input the length check would accept. Leaving it would have left a tolerance that tolerates nothing.
The encoded input is also capped before decoding rather than after.
Provenance
Found by a MiniMax-M3 pass over the package. Its conclusion was right and its reproduction was wrong: it offered
MYas an accepted one-byte secret, andMYis rejected, by the padding heuristic above. Running its example alone would have closed this as a false positive.Closes #357