Proposed Support for Base64 in JavaScript

Introduction

This page documents a stage 3 proposal for native base64 and hex encoding and decoding for binary data in JavaScript, and includes a non-production polyfill you can experiment with in the browser's console.

The proposal would provide methods for encoding and decoding Uint8Arrays as base64 and hex strings.

Feedback on the proposal's repository is appreciated.

Specification text for the proposal is available here.

API

Basic usage

Encoding a Uint8Array to a base64 string:

let arr = new Uint8Array([72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100]);
console.log(arr.toBase64());
// 'SGVsbG8gV29ybGQ='

Decoding a base64 string to a Uint8Array:

let string = 'SGVsbG8gV29ybGQ=';
console.log(Uint8Array.fromBase64(string));
// Uint8Array([72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100])

Encoding a Uint8Array to a hex string:

let arr = new Uint8Array([72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100]);
console.log(arr.toHex());
// '48656c6c6f20576f726c64'

Decoding a hex string to a Uint8Array:

let string = '48656c6c6f20576f726c64';
console.log(Uint8Array.fromHex(string));
// Uint8Array([72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100])

Options

The base64 methods take an optional options bag which allows specifying the alphabet as either "base64" (the default) or "base64url" (the URL-safe variant).

The base64 decoder also allows specifying the behavior for the final chunk with lastChunkHandling. Recall that base64 decoding operates on chunks of 4 characters at a time, but the input may have some characters which don't fit evenly into such a chunk of 4 characters. This option determines how the final chunk of characters should be handled. The three options are "loose" (the default), which treats the chunk as if it had any necessary = padding (but throws if this is not possible, i.e. there is exactly one extra character); "strict", which enforces that the chunk has exactly 4 characters (counting = padding) and that overflow bits are 0; and "stop-before-partial", which stops decoding before the final chunk unless the final chunk has exactly 4 characters.

The hex methods do not have any options.

let array = new Uint8Array([251, 255, 191]);
console.log(array.toBase64({ alphabet: 'base64' }));
// '+/+/'
console.log(array.toBase64({ alphabet: 'base64url' }));
// '-_-_'

console.log(Uint8Array.fromBase64('SGVsbG8g\nV29ybG R'));
// works, despite whitespace, missing padding, and non-zero overflow bits

try {
  Uint8Array.fromBase64('SGVsbG8gV29ybGR=', { lastChunkHandling: 'strict' });
} catch {
  console.log('with lastChunkHandling: "strict", overflow bits are rejected');
}
try {
  Uint8Array.fromBase64('SGVsbG8gV29ybGQ', { lastChunkHandling: 'strict' });
} catch {
  console.log('with lastChunkHandling: "strict", overflow bits are rejected');
}

Writing to an existing Uint8Array

The Uint8Array.prototype.setFromBase64 method allows writing to an existing Uint8Array. Like the TextEncoder encodeInto method, it returns a { read, written } pair.

This method takes an optional final options bag with the same options as above.

let target = new Uint8Array(7);
let { read, written } = target.setFromBase64('Zm9vYmFy');
console.log({ target, read, written });
// { target: Uint8Array([102, 111, 111, 98, 97, 114, 0]), read: 8, written: 6 }

Uint8Array.prototype.setFromHex is the same except for hex.

let target = new Uint8Array(6);
let { read, written } = target.setFromHex('deadbeef');
console.log({ target, read, written });
// { target: Uint8Array([222, 173, 190, 239, 0, 0]), read: 8, written: 4 }

Streaming

There is no support for streaming. However, it can be implemented in userland.