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

Where I do not use it

← All articles