Javascript ArrayBuffer to URLEncodedBase64
Context
When working with Passkeys (Web Authentication API), CredentialsContainer APIs accept ArrayBuffer in the options argument of
the CredentialsContainer.create(options)
method. However, most FIDO servers uses JSON for their request and reponse bodies.
Sending binary data over JSON requires us to use binary to string encoding techniques. The widely adopt one is Base64 encoding. In this specific case, the FIDO server that I am using use Base64URL encoding.
Excerpt from the FIDO server DTO definition
type AuthenticatorAttestationResponse struct {
AuthenticatorResponse
AttestationObject URLEncodedBase64 `json:"attestationObject"`
Transports []string `json:"transports,omitempty"`
}
Key Terms
ArrayBuffer to Base64 URL Conversion
function arrayBufferToBase64URL(buffer: ArrayBuffer): string {
const uint8Array = new Uint8Array(buffer);
const binaryString = [...uint8Array.values()]
.map((byte) => String.fromCodePoint(byte))
.join("");
const base64EncodedString = btoa(binaryString);
const base64URLEncodedString = base64EncodedString
.replaceAll("+", "-")
.replaceAll("/", "_")
.replace(/=+$/, "");
return base64URLEncodedString;
}
The ArrayBuffer
object is used to represent a generic raw binary data buffer.
In order to make sense of its content, we have to put on a data view.
Here, we accomplished this by creating a new Uint8Array()
passing in the ArrayBuffer.
Uint8Array enables us to look into the binary data as an array of 8-bit unsigned integers. Unsigned means that we will get only positive number with this view. All 8 bits are used to represent the magnitude of the value. Therefore the range of the possible values are 0 up to 255 with increment of 1.
Next, we iterate through all integer values of uint8Array
and transform each integer value into a string of one character. Then, join all characters into a long string representing every elements in the uint8Array
and save it into the binaryString
variable.
Values from 0 to 255 cover ASCII characters and also extended ASCII characters. These characters can be represented in JS string as JS string encoding is UTF-16 (or UCS2 depending on implementation).
Next, we use btoa()
to convert our binaryString
into Base64 encoded string and save it into the base64EncodedString
variable. btoa()
accepts a single argument of type string. Each character in the string is treated as a byte in binary corresponsing to its codepoint. The codepoint can be retrieved from String.codePointAt(index)
method. This is a reverse of what we did with String.fromCharCode(integer)
method when we build the binaryString
.
Next, we replace '/'
(forward slash) character with '_'
(underscore). And replace '+'
(plus) character with '-'
(minus). Optionally, we remove the padding '='
from the final encoded string. These steps transform a Base64 encoded string to a Base64 URL encoded string as per the spec.
Sidenote
You might be wondering why do we have to convert binary data to string of characters that represent the binary data. And later convert it to Base64 with btoa()
. While it is possible to convert directly from ArrayBuffer or Uint8Array to Base64 by writing our own encoder, the performance is subpar comparing to the usage of btoa()
.
As I researched different approaches to accomplish the conversion, these steps are the most common and performant.
Conclusion
I hope this article sheds some light on the what could be obscured in regards to what each line of code does and the reason behind such a roundabout way to convert something. Happy learning and moving your body. Bye. 😘