base32.cr

RFC 4648 Base32 and base32hex encoding/decoding for Crystal. Optional padding, case-insensitive decoding, no dependencies.

base32.cr

CI

Base32 encoding and decoding for Crystal, per RFC 4648: the standard alphabet of §6 (A-Z, 2-7) and the extended-hex alphabet of §7 (0-9, A-V).

Crystal's standard library ships Base64 but not Base32. This fills that gap with the same API shape, no dependencies, and no surprises: padding is optional, decoding is case insensitive, and malformed input raises instead of quietly producing the wrong bytes.

Installation

Add the dependency to your shard.yml:

dependencies:
  base32:
    github: aluminumio/base32.cr
    version: ~> 0.1.0

Then run shards install.

Usage

require "base32"

Base32.encode("foobar")                 # => "MZXW6YTBOI======"
Base32.encode("foobar", padding: false) # => "MZXW6YTBOI"
Base32.encode(Bytes[1, 2, 3])           # => "AEBAG==="

Base32.decode("MZXW6YTBOI======")        # => Bytes[102, 111, 111, 98, 97, 114]
Base32.decode_string("MZXW6YTBOI======") # => "foobar"
Base32.decode_string("mzxw6ytboi")       # => "foobar"

The extended-hex alphabet of §7, which preserves the sort order of the underlying data, is available under the same names prefixed with hex_:

Base32.hex_encode("foobar")                 # => "CPNMUOJ1E8======"
Base32.hex_decode_string("CPNMUOJ1E8======") # => "foobar"

API

Method Returns
Base32.encode(data : String | Bytes, padding : Bool = true) String
Base32.decode(data : String | Bytes) Bytes
Base32.decode_string(data : String | Bytes) String
Base32.hex_encode(data : String | Bytes, padding : Bool = true) String
Base32.hex_decode(data : String | Bytes) Bytes
Base32.hex_decode_string(data : String | Bytes) String

Encoding accepts a String (encoded as its UTF-8 bytes) or Bytes.

Decoding accepts either case, padded or unpadded input, and steps over CR and LF so that line-wrapped input round-trips. Everything else raises Base32::Error:

Base32.decode("MZXW-YTB")  # Base32::Error: Invalid base32 character '-' (byte 0x2d)
Base32.decode("MZXW0YTB")  # Base32::Error: Invalid base32 character '0' (byte 0x30)
Base32.decode("MZX")       # Base32::Error: Invalid base32 length: 3 data characters
                           #                cannot encode a whole number of bytes
Base32.decode("MZXW6===Y") # Base32::Error: Invalid base32 input: data character 'Y'
                           #                follows the padding

Note that 0/O and 1/I/L are not treated as interchangeable. That substitution belongs to Crockford's Base32, which is a different encoding with a different alphabet; accepting it here would silently decode input that RFC 4648 says is invalid.

When to prefer Base32 over Base64

Base32 costs about 20% more characters than Base64 (8 characters per 5 bytes rather than 4 per 3). In exchange:

  • It is case insensitive. The alphabet has a single case, so decoding accepts either. Anything that may case-fold a value in transit will corrupt a mixed-case Base64 string but leave a Base32 one intact — DNS labels, email local-parts rewritten by intermediate mail servers, case-insensitive filesystems, values a human may retype or a system may normalise.
  • Its alphabet contains no delimiters. No +, /, - or _, so an encoded value can be embedded in a URL, a filename, or a structured address without escaping and without colliding with the separators around it. With padding: false the output is [A-Z2-7]+ and nothing else.

Reach for Base64 whenever size matters and the transport is byte-transparent and case-preserving. Reach for Base32 when the value has to survive a channel that is neither — a signed token in an email address, a lookup key in a hostname, an identifier a person will read aloud.

Development

$ crystal spec
$ crystal tool format --check

The specs cover the RFC 4648 §10 test vectors for both alphabets, round trips over every length up to 40 bytes, unpadded output, case-insensitive decoding, and each rejection path. Output is also verified byte-for-byte against CPython's base64.b32encode / b32hexencode.

Contributing

  1. Fork it (https://github.com/aluminumio/base32.cr/fork)
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create a new Pull Request

Licence

MIT. See LICENSE.

Repository

base32.cr

Owner
Statistic
  • 0
  • 0
  • 0
  • 0
  • 0
  • about 5 hours ago
  • August 15, 2026
License

MIT License

Links
Synced at

Sat, 15 Aug 2026 20:44:44 GMT

Languages