Skip to content

Deferred: Validate Base32 padding count/length and decide the padded-vs-unpadded policy (strict mode) #19

Description

@darianmiller

Follow-on from #13. #13 added strict-mode rejection of non-zero trailing bits
(RFC 4648 section 3.5) and of data characters appearing after padding. It
intentionally deferred full padding count/length validation because that
requires a policy decision this ticket must resolve.

Deferred scope:

  • In strict mode, validate that the number of = pad characters is correct for
    the data length: numDataChars mod 8 must be in {0,2,4,5,7} (1,3,6 are
    impossible for valid Base32), and, when padding is present, the pad-run length
    must equal the expected value (0/6/4/3/1 respectively) so that
    dataChars + padChars is a multiple of 8.
  • Reject an invalid data-length remainder (1,3,6) in strict mode regardless of
    padding.
  • Decide the padded-vs-unpadded policy. Real-world TOTP secrets (e.g. Google
    Authenticator) are frequently stored UNPADDED, so requiring canonical padding
    would reject them even in strict mode. Options: (a) accept unpadded input but,
    if any = is present, require the exact correct count/position; (b) require
    full canonical padding; (c) expose it as a separate option. Pick one and
    document it.

Implementation notes:

Acceptance criteria:

  • The padded-vs-unpadded policy is decided and documented.
  • Strict rejects an incorrect = count for the data length.
  • Strict rejects an invalid data-length remainder (1,3,6).
  • A correctly-encoded secret (padded or unpadded per the chosen policy) still decodes in strict mode.
  • Lenient behavior unchanged; existing tests pass.
  • DUnit tests cover correct and incorrect pad counts and the invalid-remainder cases.

Activity

  1. darianmiller commented on Jul 21, 2026

    @darianmiller
    ContributorAuthor

    For TOTP, the governing spec is the otpauth Key URI Format (Google Authenticator's de-facto standard), and that convention stores/accepts the base32 secret unpadded. That's why real secrets in the wild are unpadded — not cause 4648 permits it freely, but because the referring spec omits it. So "unpadded is fine for TOTP" is correct, via 3.2's delegation, not in general.

    Since the applicable spec (otpauth) omits padding, the right call for this library is option (a) accept unpadded, but if any = is present, require the correct count/position. Requiring full canonical padding (option b) would reject legitimate otpauth secrets even in strict mode. (Let's pick option a)

    §3.3 also validates #12. The very next paragraph says implementations MUST reject non-alphabet characters "unless the specification referring to this document explicitly states otherwise." So the old lenient skip-everything behavior actually violated 4648's default — which is precisely what the #12 strict mode restores. (Lenient stays as an opt-out for the MIME-style "be liberal" behavior §3.3 also mentions.)

  2. changed the title [-]Validate Base32 padding count/length and decide the padded-vs-unpadded policy (strict mode)[/-] [+]Deferred: Validate Base32 padding count/length and decide the padded-vs-unpadded policy (strict mode)[/+] on Jul 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions