The Sovereignty Stack: Implementing Argon2 and AES-GCM ​
đź“… Published on March 26, 2026
In Part 1 of this series, we discussed the philosophy of Data Sovereignty—the radical idea that your data should be under your own cryptographic control, regardless of whose cloud it sits in. We established that basic hashing isn't enough anymore, and introduced our two pillars of defense: Argon2 (The Forge) and AES-GCM (The Vault).
Now, let’s move from the why to the how.
If you want to build a system where the user—not the platform—truly owns their data, you have to embrace a Zero-Knowledge Architecture. The golden rule of this architecture is simple: The server must be blind. If you send a user's plain-text password to your backend API to do the encryption there, you have already broken the promise of sovereignty.
To achieve true digital independence, the cryptography must happen on the client’s device (in their browser or mobile app). Here is the technical blueprint for making that happen using modern JavaScript.
Phase 1: Transforming the Password ​
A standard user password like MySuperSecretPassword2026! is a terrible encryption key. It lacks the mathematical randomness (entropy) required to lock down data securely. We need to stretch that "soft" password into a hardened, 256-bit cryptographic key.
To do this locally in the browser, developers typically rely on WebAssembly (WASM) ports of Argon2id.
Unlike older algorithms that only test a computer's processing speed, Argon2id is "Memory-Hard." It forces the device to allocate a massive chunk of RAM to derive the key. If a hacker tries to brute-force this later, they can't just use fast, cheap processors; they are forced to buy expensive memory for every single guess, making the attack financially destructive.
The Key Derivation Logic ​
In your JavaScript frontend, the logic requires a unique Salt (a random sequence of bytes) to ensure that even if two users have the same password, their derived keys will be completely different.
javascript
// 1. Generate a secure random Salt
const salt = window.crypto.getRandomValues(new Uint8Array(16));
// 2. Memory-hard Argon2id function
const derivedKey = await argon2.hash({
pass: userPassword,
salt: salt,
time: 3, // Iterations
mem: 65536, // Forces the device to use 64MB of RAM
parallelism: 4, // How many CPU cores to utilize
hashLen: 32, // A 256-bit key
type: argon2.Argon2id
});Note: This process is intentionally heavy. It might take half a second to run in the browser—that tiny delay is exactly what keeps the data safe.
Phase 2: Locking the Data ​
Now that we have forged a high-strength key on the user's device, we move to the encryption phase using the browser's native Web Crypto API.
We use AES-256-GCM (Advanced Encryption Standard in Galois/Counter Mode). GCM is crucial because it provides Authenticated Encryption. It doesn't just scramble the data; it generates an "Authentication Tag"—think of it as a digital wax seal. If a malicious actor (or a corrupted database) alters even a single bit of the encrypted data, the seal breaks, and the decryption process will intentionally fail.
The Encryption Logic ​
To use AES-GCM, you must generate an Initialization Vector (IV). This is a random starting point for the encryption math. Never reuse an IV with the same key.
javascript
// 1. Generate a unique Initialization Vector (12 bytes is standard for GCM)
const iv = window.crypto.getRandomValues(new Uint8Array(12));
// 2. Import our forged key into the Web Crypto API
const cryptoKey = await window.crypto.subtle.importKey(
"raw", derivedKey, "AES-GCM", false, ["encrypt"]
);
// 3. Encrypt the plain-text data of user
const encodedData = new TextEncoder().encode(userPrivateData);
const encryptedBuffer = await window.crypto.subtle.encrypt(
{ name: "AES-GCM", iv: iv },
cryptoKey,
encodedData
);Note: In the JavaScript Web Crypto API, the resulting
encryptedBufferautomatically appends the Authentication Tag to the end of the ciphertext.
Phase 3: The Sovereign Packet ​
The heavy lifting is done. The user's device holds an encrypted blob of data, and the plain-text password is wiped from the local memory.
Now, what actually gets sent to your database? You cannot just save the ciphertext. To decrypt this data later, the client will need the exact same Salt and IV that were used during the encryption process.
Your frontend will package these non-secret ingredients together with the scrambled data into a "Sovereign Packet" to send via your API:
| Field | Purpose | Is it Secret? |
|---|---|---|
| Version | Algorithm version (e.g., v1-argon2id-aesgcm) | Public |
| Salt | The random bytes used by Argon2 | Public |
| IV (Nonce) | The starting point used by AES | Public |
| Ciphertext & Tag | The scrambled data and its wax seal | Unreadable (Encrypted) |
| Master Password | The user's actual password | NEVER LEAVES DEVICE |
When you structure your system this way, your server simply becomes a dumb hard drive. If your database is ever breached, the attackers walk away with random Salts, IVs, and unbreakable ciphertext. Because your server never saw the user's password, there is nothing to leak.
We are moving away from a world where we "trust" a company to protect us, and moving toward a world where we trust the math. By moving the cryptography to the edge—right into the user's hands—we are no longer just writing code; we are building digital independence.
