armor64: safe, strict and stable textual encoding of byte streams

Safety

Encoded streams rely only on ASCII printable characters and can be used unescaped in:

Strictness

There is only one armor64 encoding for a given byte stream.
If streams are equal, encoded streams are equal, and vice-versa.
Encoders and decoders are required to enforce this rule.
An encoded stream can be validated during decoding or in one pass.

Stability

armor64 is stable in two ways:

FAQ

Why not base64?

base64 is effectively not a specific encoding but rather a family of encodings.
=-padding is often possible, sometimes required. Many libraries do not offer unpadded variants.
Some variants allow newlines, some don't.
Some variants use a URL-safe alphabet, some don't.
Few variants specify a canonical encoding, fewer mandate that it be used.
None of the variants use an alphabet that preserves natural byte order.

This can lead to ambiguity and confusion for developers, for example when picking implementations or documenting formats.

armor64 uniquely identifies a specification for encoding and decoding bounded byte streams.
armor64.org provides test cases and links to community implementations.

What about Postel's law?

In general, an implementation should be conservative in its sending behavior, and liberal in its receiving behavior.

We could not better Martin Thomson's 2015 Internet-Draft on the topic.

Why reorder the base64url alphabet?

We want byte streams to be ordered like their encoding in the natural byte order.

As a result, situations where one side uses armor64, the other base64 are very unlikely to go unnoticed.
Had the alphabets been shared, problems could have arisen too late.

Why are newlines forbidden?

We want the encoding of byte streams to be unique, and we want to avoid escaping in URLs, JSON-based protocols, etc.

Too often, most byte streams remain comparatively short during development and in test cases.
In base64, only long byte streams involve newlines.
As a result, mishandling of newlines in transport or decoders can be discovered too late.

Specification

Encoding

Input streams are consumed from left to right in blocks of 6 bits.
If fewer than 6 bits are left at the end of the input stream, they are right-padded with 0 bits to form the last block.
Each block is converted to output a single byte according to the following table:

....00 ....01 ....10 ....11
0000.. 000000- (0x2D) 0000010 (0x30) 0000101 (0x31) 0000112 (0x32)
0001.. 0001003 (0x33) 0001014 (0x34) 0001105 (0x35) 0001116 (0x36)
0010.. 0010007 (0x37) 0010018 (0x38) 0010109 (0x39) 001011A (0x41)
0011.. 001100B (0x42) 001101C (0x43) 001110D (0x44) 001111E (0x45)
0100.. 010000F (0x46) 010001G (0x47) 010010H (0x48) 010011I (0x49)
0101.. 010100J (0x4A) 010101K (0x4B) 010110L (0x4C) 010111M (0x4D)
0110.. 011000N (0x4E) 011001O (0x4F) 011010P (0x50) 011011Q (0x51)
0111.. 011100R (0x52) 011101S (0x53) 011110T (0x54) 011111U (0x55)
1000.. 100000V (0x56) 100001W (0x57) 100010X (0x58) 100011Y (0x59)
1001.. 100100Z (0x5A) 100101_ (0x5F) 100110a (0x61) 100111b (0x62)
1010.. 101000c (0x63) 101001d (0x64) 101010e (0x65) 101011f (0x66)
1011.. 101100g (0x67) 101101h (0x68) 101110i (0x69) 101111j (0x6A)
1100.. 110000k (0x6B) 110001l (0x6C) 110010m (0x6D) 110011n (0x6E)
1101.. 110100o (0x6F) 110101p (0x70) 110110q (0x71) 110111r (0x72)
1110.. 111000s (0x73) 111001t (0x74) 111010u (0x75) 111011v (0x76)
1111.. 111100w (0x77) 111101x (0x78) 111110y (0x79) 111111z (0x7A)

Decoding

Input streams are consumed from left to right and each byte is turned into a block of 6 bits according to the previous table.
When encountering a byte that do not appear in the conversion table, the input stream is rejected.
Those blocks form a stream consumed from left to right to output a byte every 8 bits.
At the end of the input stream, if any of the remaining bits are not 0, the input stream is rejected.

Implementations

Test cases

The following should encode and decode back to itself:

Input Output
Empty string Empty string
JP H_-
Hello, World! H5KgQ5wg74SjRalZ7F
armor64 is safe, strict, and stable. It is specified and easy to test. Do not settle for lesser encodings. NM8hQr7qC10dRm0nNLO_A10nS68dNrFg754iO10nS54XQ5Ji73_o75_n76CkOLCdOa__O10WQaFVOL4nTH0oQm0oOMCoAX03Qm0iQrFVRqKoS5l_75OjRX0gOMCnOM7VOLtYQqGdQaSnAV

The following should fail to decode:

Input Reason
space Not in alphabet.
carriage return Not in alphabet.
newline Not in alphabet.
__== = not in alphabet.