TOTP secrets, the main event

The secret inside a 2FA enrollment QR code is Base32 in the otpauth:// URI, and every "enter this key manually" fallback shows the same string, usually grouped in fours and lowercase. Decoding one here shows you what the authenticator app actually stores: 10 or 20 random bytes, nothing more. The code changing every 30 seconds comes from hashing those bytes with the current time, a mechanism we walk through in how TOTP codes actually work.

Handle the string accordingly: the Base32 form is the second factor, and anyone who reads it can generate your codes forever. Decode secrets for debugging in a tool that does not transmit them, which is the reason this page runs entirely in your browser and keeps working offline.

Why 32 characters, and which 32

Base32 maps bytes onto 32 printable characters, 5 bits per character, 8 characters per 5 input bytes. That is a worse deal than Base64's 6 bits per character, and the trade is intentional: with only 32 symbols, the alphabet can afford to be case-insensitive and to drop every character that humans confuse with another. Base64 is for machines passing data through text channels; Base32 is for the moments a human is part of the channel, reading a key off one screen and typing it into another.

Those moments are rarer, which is why Base32 has exactly a few strongholds: TOTP secret keys, DNSSEC records, Tor onion addresses, ULIDs (in a variant), and occasional case-insensitive identifiers in filesystems and URLs.

The RFC 4648 alphabet is the letters A through Z followed by the digits 2 through 7. What is missing tells the story: no 0 to mistake for O, no 1 to mistake for I or L, no 8 to mistake for B, no 9. Decoding is defined case-insensitively, so jbswy3dp and JBSWY3DP are the same value. Padding uses = to fill the last 8-character block, exactly like Base64's, and is just as commonly stripped in practice.

The error messages on this page lean on that design: when a pasted string contains a 0 or 1, the decoder does not just say "invalid character", it points out that a letter was probably misread, because that is what a 0 in Base32 almost always means.

How to use this converter

  1. Encoding: type or paste text and the Base32 appears live, UTF-8 handled correctly. --no-pad strips the trailing =, --group-4 formats the output in blocks of four the way authenticator apps display keys.
  2. Decoding: turn on --decode and paste. Lowercase, spaces, dashes and missing padding are all repaired first, so a setup key goes in exactly as some enrollment page displayed it. What cannot be repaired is named: the offending character and its position.
  3. Binary results stay usable. A decoded payload that is not text, and a real TOTP secret is random bytes, comes back as a hex dump you can download as a file.

--hex switches both directions to the base32hex alphabet, and keeping that switch explicit is deliberate: the two alphabets overlap enough that auto-detection would guess, and a wrong guess decodes to silently wrong bytes rather than an error.

Base32 vs Base64

Base32Base64
Bits per character56
Size overhead+60%+33%
Block size5 bytes → 8 chars3 bytes → 4 chars
Case-insensitiveyesno
Confusable charactersnone by design0/O, 1/l/I all present
Typical homeTOTP keys, DNSSEC, onion addresseseverything else

The rule of thumb falls out of the table: if a human will ever retype the value, or it lives in a case-insensitive namespace like DNS, take the 60% and use Base32. Otherwise use Base64, or its URL-safe variant when the string travels in URLs.

The other Base32s

"Base32" is unfortunately a family name. Standard RFC 4648 (A–Z 2–7) is what this page speaks, and what TOTP uses. base32hex (0–9 A–V, the --hex flag) preserves sort order and lives in DNSSEC. Crockford Base32 drops I, L, O and U, folds lookalikes on input and powers ULIDs. z-base-32, from the Zooko school of human-oriented encodings, reorders the alphabet to put frequent characters on easy keystrokes and appears in some cryptographic tooling.

They do not interoperate. The same 32-symbol idea, four incompatible tables, and a string that happens to decode under the wrong one produces wrong bytes with no error. When a Base32 value refuses to decode, the first question is not "is it corrupted" but "which Base32 is this", and the alphabet cheat sheet above answers it faster than trial and error.

Base32 questions

Why do TOTP and 2FA secret keys use Base32?

Because the fallback to scanning the QR code is a human typing the key by hand, and Base32 is the encoding built for that. It is case-insensitive, and its alphabet (A–Z and 2–7) contains no 0, 1, 8 or 9, so nobody has to distinguish 0 from O or 1 from I on a phone screen. Base64 fails both tests and would save little space; hex would double the typing. The convention comes from the Google Authenticator key URI format, and every authenticator app since has followed it.

What characters are valid in a Base32 string?

A to Z and 2 to 7, plus = as padding. No 0, 1, 8 or 9, which is the whole point.

Why is my 2FA secret key invalid when I enter it manually?

Usually one mistyped lookalike character. The Base32 alphabet has no 0, 1, 8 or 9, so a "0" you read off the screen is the letter O, a "1" is I or L, and an "8" is B. Spaces and lowercase are not the problem; every sane app strips and uppercases them. Other causes: a truncated copy (secrets are commonly 16 or 32 characters), or a service that displays the key with padding while the app expects it bare. Pasting the key into a Base32 decoder like the one above tells you instantly whether it is structurally valid and which character breaks it.

How do I Base32 encode and decode in Python?

import base64, then base64.b32encode(b"data") and base64.b32decode(s). The decoder is strict by default: it wants uppercase and full padding, so real-world input needs base64.b32decode(s.upper() + "=" * (-len(s) % 8)), or pass casefold=True to accept lowercase directly. There is also a map01 parameter that translates 0 to O and 1 to I or L for input typed by humans. For base32hex, the functions are b32hexencode and b32hexdecode, available since Python 3.10.

How do I decode Base32 in JavaScript?

There is no built-in: neither browsers nor Node.js ship Base32, unlike Base64 with atob and Buffer. Options are a small npm package (hi-base32, rfc4648) or twenty lines of your own: walk the string, map each character to its 5-bit value via the alphabet, push the bits into an accumulator and emit a byte whenever 8 have collected. The engine on this page does exactly that, and since it runs client-side you can read the implementation in the page source.

What is the difference between Base32 and Base64?

Base32 encodes 5 bits per character, Base64 encodes 6, so Base32 output is about 60% larger than the input while Base64 adds only 33%. In exchange, Base32 is case-insensitive and free of lookalike and URL-hostile characters, which makes it the right pick for anything humans read aloud, retype or put in a case-insensitive system (DNS labels, some filesystems). Base64 wins wherever machines exchange data and size matters, which is most places; that is why Base32 is the niche encoding and Base64 the default one.

What is base32hex and where is it used?

The RFC 4648 §7 variant with the alphabet 0–9 A–V instead of A–Z 2–7. Its point is that the encoded string sorts in the same order as the raw bytes, a property standard Base32 does not have because its digits come after its letters. The place you meet it is DNSSEC: NSEC3 records hash owner names and encode them as base32hex labels. Outside of DNS tooling it is rare, and feeding base32hex to a standard decoder produces either an error or, worse, silently wrong bytes, since many characters are valid in both alphabets at different positions.

What is Crockford Base32?

A separate design from Douglas Crockford, not an RFC 4648 variant: alphabet 0–9 A–Z minus I, L, O and U, decoding that folds i and l to 1 and o to 0, no padding, and an optional check symbol. Its natural habitat is human-facing identifiers, most prominently ULIDs, which are 26 Crockford-Base32 characters. It is not interchangeable with standard Base32: a ULID contains digits like 0 and 1 that standard Base32 rejects, so pick the decoder that matches the producer.

How long is a TOTP secret in Base32?

16 characters carry 80 bits, 32 carry 160, and RFC 4226 wants at least 160. Neither length needs padding.