Base32 Encoding in TOTP Secrets

Understand Base32 encoding for TOTP secrets. Learn why TOTP uses Base32 instead of Base64 or hex, how to encode and decode secrets, and fix common encoding issues.

Troubleshooting

Detailed Explanation

Base32 Encoding: The Bridge Between Bytes and Authenticator Apps

TOTP secrets are raw bytes, but authenticator apps and otpauth:// URIs need a text representation. Base32 encoding (RFC 4648) is the standard chosen for this purpose, and understanding it is key to debugging enrollment issues.

Why Base32 (Not Base64 or Hex)?

Base32 uses 32 characters: A-Z and 2-7

Compared to alternatives:

Encoding Characters Bytes → Text Ratio Ambiguity
Hex 0-9, A-F 1:2 0/O confusion
Base32 A-Z, 2-7 5:8 None
Base64 A-Z, a-z, 0-9, +, / 3:4 Case-sensitive, special chars

Base32 was chosen because:

  • No ambiguous characters: avoids 0/O, 1/I/l confusion
  • Case-insensitive: JBSWY3DP = jbswy3dp
  • No special characters: safe for URIs without additional encoding
  • Human-readable: users can manually type it if QR scanning fails

Encoding Process

Base32 encodes 5 bytes into 8 characters:

Input bytes:  [0x48, 0x65, 0x6C, 0x6C, 0x6F]  ("Hello")
Binary:       01001000 01100101 01101100 01101100 01101111
5-bit groups: 01001 00001 10010 10110 11000 11011 00011 01111
Base32 index: 9     1     18    22    24    27    3     15
Characters:   J     B     S     W     Y     3     D     P
Result:       JBSWY3DP

Padding

Base32 uses = for padding when the input length is not a multiple of 5 bytes. For TOTP:

  • 20 bytes (SHA-1 secret) → 32 characters, no padding needed
  • 10 bytes → 16 characters, no padding
  • Other lengths may produce trailing = signs

Important: most TOTP implementations strip padding from the URI:

Correct:   secret=JBSWY3DPEHPK3PXP
Incorrect: secret=JBSWY3DPEHPK3PXP======

Common Encoding Issues

  1. Including padding: remove all trailing = characters before putting the secret in a URI
  2. Using lowercase: while technically valid (Base32 is case-insensitive), some older apps expect uppercase
  3. Spaces in the secret: some displays add spaces for readability (JBSW Y3DP) — strip them before use
  4. Confusing Base32 with Base64: using a Base64 decoder on a Base32 string produces garbage
  5. Invalid characters: if the secret contains 0, 1, 8, or 9, it is not valid Base32

Decoding for Verification

When verifying TOTP on the server, decode the Base32 secret back to bytes before computing the HMAC:

import base64

secret_b32 = "JBSWY3DPEHPK3PXP"
# Add padding if needed
padding = 8 - (len(secret_b32) % 8)
if padding != 8:
    secret_b32 += '=' * padding
secret_bytes = base64.b32decode(secret_b32)

Use Case

Developers debugging TOTP enrollment failures often trace the problem to Base32 encoding issues. This guide is essential when a user's authenticator app shows different codes than the server expects, when migrating TOTP secrets between systems that handle encoding differently, or when building a TOTP library that needs correct encoding and decoding. Understanding Base32 at the bit level helps diagnose subtle bugs that higher-level abstractions can hide.

Try It — TOTP Generator

Open full tool