go / crockford base32

I use Crockford's Base32 when I want a short identifier that a human will type or read aloud: email confirmation codes, invite tokens, short URL slugs.

Alphabet

0123456789ABCDEFGHJKMNPQRSTVWXYZ

It drops I, L, O, and U to remove visual ambiguity and avoid accidental obscenities. On decode, any case is accepted, I and L map to 1, and O maps to 0, so users can type what they think they see.

For contrast, esbuild emits standard RFC 4648 Base32 (uppercase AZ plus 27) for build artifacts like app-2H67SL6V.css (see cmd/esbuild). That alphabet is fine for filenames a CDN serves; it's not meant for humans.

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 confusables to their canonical forms
// and strips formatting hyphens.
var normalize = strings.NewReplacer(
	"i", "1", "I", "1",
	"l", "1", "L", "1",
	"o", "0", "O", "0",
	"-", "",
)

// Decode parses any case, with optional hyphens
// and the I/L→1, O→0 substitutions Crockford allows.
func Decode(s string) ([]byte, error) {
	return enc.DecodeString(strings.ToUpper(normalize.Replace(s)))
}

Confirmation codes

For an email confirmation flow, generate 5 random bytes, encode to 8 characters, and group with a hyphen for readability:

import "crypto/rand"

// NewCode returns an 8-character Crockford Base32 code
// formatted as XXXX-XXXX.
func NewCode() string {
	var b [5]byte
	if _, err := rand.Read(b[:]); err != nil {
		panic(err)
	}
	s := Encode(b[:])
	return s[:4] + "-" + s[4:]
}
NewCode() // "2H67-SK6V"
NewCode() // "JBTS-FCY2"

40 bits gives ~1 trillion possibilities, plenty for a one-time code with a short TTL and a small wrong-guess limit.

Verify with constant-time comparison:

import "crypto/subtle"

func Verify(input, want string) bool {
	got, err := Decode(input)
	if err != nil {
		return false
	}
	exp, _ := Decode(want)
	return subtle.ConstantTimeCompare(got, exp) == 1
}

Decode tolerates the user typing o for 0, lowercase letters, and stray hyphens, so 2h67-sk6v, 2H67SK6V, and 2H67-SK6V all verify the same.

When to use

When not to use

← All articles