How to Convert ArrayBuffers to Base64 (Web Crypto Storage)
You just successfully implemented the Web Crypto API. You encrypted your user's text, you created the Initialization Vector (IV), and you have the raw ciphertext ready to go. You call JSON.stringify() to send the data to your backend, but when you check your database, the payload is completely empty: {}.
What happened? The Web Crypto API outputs raw binary data in the form of ArrayBuffer objects. JSON (JavaScript Object Notation) is a text-based format. It simply does not know how to handle raw binary memory streams, so it silently strips them out.
The Solution: Base64 Encoding
To safely transport cryptographic data across HTTP requests and store it in PostgreSQL or MongoDB, we must convert the raw binary into a safe, text-based string. The industry standard for this is Base64 Encoding.
Base64 takes binary data and translates it into a string containing only safe ASCII characters (A-Z, a-z, 0-9, +, and /).
Encoding: ArrayBuffer to Base64
Here is the highly optimized Vanilla JavaScript utility to take the output of your AES-GCM encryption and convert it into a safe string for your database. We use a Uint8Array to read the bytes, and the native window.btoa() function to generate the Base64 string.
/**
* Converts an ArrayBuffer into a Base64 string.
* @param {ArrayBuffer} buffer - The raw cryptographic output
* @returns {string} - Safe Base64 string for JSON transport
*/
function bufferToBase64(buffer) {
// 1. Create a typed array to view the raw memory buffer
const bytes = new Uint8Array(buffer);
// 2. Iterate through the bytes and convert them to binary characters
let binary = '';
for (let i = 0; i < bytes.byteLength; i++) {
binary += String.fromCharCode(bytes[i]);
}
// 3. Convert the binary string to Base64 natively
return window.btoa(binary);
}
// Usage Example:
// const encryptedBuffer = await window.crypto.subtle.encrypt(...);
// const safeStringForDatabase = bufferToBase64(encryptedBuffer);
Decoding: Base64 back to ArrayBuffer
When a user clicks your secure link, the frontend downloads the Base64 string from your backend. However, the crypto.subtle.decrypt() function will immediately reject a string. It requires an ArrayBuffer.
We must reverse the process using window.atob() before attempting decryption.
/**
* Converts a Base64 string back into an ArrayBuffer for decryption.
* @param {string} base64 - The string fetched from the database
* @returns {ArrayBuffer} - The raw memory buffer ready for Web Crypto
*/
function base64ToBuffer(base64) {
// 1. Decode the Base64 string back to binary characters
const binaryString = window.atob(base64);
// 2. Create an empty typed array of the exact required length
const bytes = new Uint8Array(binaryString.length);
// 3. Fill the typed array with the byte values
for (let i = 0; i < binaryString.length; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
// 4. Return the underlying ArrayBuffer
return bytes.buffer;
}
// Usage Example:
// const rawBuffer = base64ToBuffer(databaseData.ciphertextBase64);
// const plaintext = await window.crypto.subtle.decrypt(..., rawBuffer);
Seamless Data Transport
In ZeroKey, every single Initialization Vector (IV), PBKDF2 Salt, and ciphertext block is processed through these exact Base64 utility functions.
This guarantees that our encrypted payloads can be securely serialized via JSON, transmitted over Vercel API routes, and stored cleanly as TEXT fields in our Supabase PostgreSQL database without any risk of data corruption or encoding loss.
Conclusion
JavaScript is incredibly flexible, but it has strict boundaries when crossing the bridge from cryptographic math to network transport. By strictly managing your data types and wrapping your `ArrayBuffers` in Base64 strings, you ensure your cryptographic architecture remains robust, predictable, and perfectly compatible with modern REST APIs.