go / crockford base32
I use Crockford's Base32 when a person types or reads an identifier aloud. An email confirmation code is the usual case.
Alphabet
0123456789ABCDEFGHJKMNPQRSTVWXYZ
The alphabet drops I, L, and O because they look like 1 and 0.
It drops U to prevent accidental obscenities.
The decoder accepts both cases.
It maps I and L to 1, and it maps O to 0.
A person can type what they see, and the code still decodes.
esbuild uses the standard RFC 4648 Base32 alphabet
(A to Z plus 2 to 7) for content hashes in file names
such as app-2H67SL6V.css
(see web / cdn).
That alphabet is correct for a file name that a CDN serves.
I do not use it for a code that a person types.
Encoding
Go's encoding/base32 accepts a custom alphabet:
package crockford
import (
"encoding/base32"
"strings"
)
const alphabet = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"
var enc = base32.NewEncoding(alphabet).WithPadding(base32.NoPadding)
// Encode returns the Crockford Base32 encoding of src.
func Encode(src []byte) string {
return enc.EncodeToString(src)
}
// normalize maps each confusable letter to its digit
// and removes hyphens.
var normalize = strings.NewReplacer(
"i", "1", "I", "1",
"l", "1", "L", "1",
"o", "0", "O", "0",
"-", "",
)
// Decode accepts both cases and optional hyphens.
// It applies the I/L to 1 and O to 0 substitutions.
func Decode(s string) ([]byte, error) {
return enc.DecodeString(strings.ToUpper(normalize.Replace(s)))
}
Confirmation codes
For an email confirmation flow, I generate 5 random bytes. They encode to 8 characters. A hyphen splits the characters into two groups of 4:
import "crypto/rand"
// NewCode returns an 8-character Crockford Base32 code
// in the form XXXX-XXXX.
func NewCode() string {
var b [5]byte
rand.Read(b[:]) // never returns an error since Go 1.24
s := Encode(b[:])
return s[:4] + "-" + s[4:]
}
NewCode() // "2H67-SK6V"
NewCode() // "JBTS-FCY2"
40 bits give about 1.1 trillion possible codes. That is sufficient for a one-time code with a short expiry and a small limit on wrong guesses.
I compare codes in constant time:
import "crypto/subtle"
func Verify(input, want string) bool {
got, err := Decode(input)
if err != nil {
return false
}
exp, err := Decode(want)
if err != nil {
return false
}
return subtle.ConstantTimeCompare(got, exp) == 1
}
Decode accepts lowercase letters and missing hyphens.
So 2h67-sk6v, 2H67SK6V, and 2H67-SK6V all verify
against the same code.
Where I use it
- One-time codes that a person types from a different device
- Short invite tokens in URLs
- Short IDs in CLI output
Where I do not use it
- Opaque IDs that no person reads (I use a UUID or hex)
- Build-time content hashes (standard Base32 is correct there)
- Values that must decode to exactly the input string, because the decoder normalizes