Why Client-Side Encryption?
Client-side encryption means the browser encrypts data before it ever reaches the server. Even if the server is compromised or the provider is compelled by law, the server only sees ciphertext — not plaintext. This is the foundation of end-to-end encrypted apps like Signal, ProtonMail, and Bitwarden.
AES-GCM: The Right Mode
AES (Advanced Encryption Standard) is the global standard for symmetric encryption. It operates on 128-bit blocks and supports 128, 192, or 256-bit keys. The mode determines how AES handles data larger than one block:
- AES-CBC: Cipher Block Chaining — confidentiality only, no integrity check, needs padding. Vulnerable to padding oracle attacks if not handled carefully.
- AES-GCM: Galois/Counter Mode — provides both confidentiality and authentication (detects tampering). No padding needed. The modern choice for almost all use cases.
Always use AES-256-GCM for new applications.
Key Concepts
- Key: The secret 256-bit (32-byte) value used to encrypt and decrypt.
- IV / Nonce: A 96-bit (12-byte) random value used with GCM. Must be unique per encryption operation with the same key. Never reuse an IV with the same key.
- Salt: Random value mixed with a password before key derivation. Stored alongside ciphertext.
- PBKDF2: Password-Based Key Derivation Function 2 — derives a strong key from a human-readable password using many iterations.
Complete Encryption/Decryption in JavaScript
const enc = new TextEncoder();
const dec = new TextDecoder();
async function encrypt(plaintext, password) {
const salt = crypto.getRandomValues(new Uint8Array(16));
const iv = crypto.getRandomValues(new Uint8Array(12));
const keyMaterial = await crypto.subtle.importKey(
"raw", enc.encode(password), "PBKDF2", false, ["deriveKey"]
);
const key = await crypto.subtle.deriveKey(
{ name: "PBKDF2", salt, iterations: 250000, hash: "SHA-256" },
keyMaterial,
{ name: "AES-GCM", length: 256 },
false,
["encrypt"]
);
const ciphertext = await crypto.subtle.encrypt(
{ name: "AES-GCM", iv },
key,
enc.encode(plaintext)
);
// Combine salt + iv + ciphertext, encode as base64
const combined = new Uint8Array([...salt, ...iv, ...new Uint8Array(ciphertext)]);
return btoa(String.fromCharCode(...combined));
}
async function decrypt(encoded, password) {
const combined = Uint8Array.from(atob(encoded), c => c.charCodeAt(0));
const salt = combined.slice(0, 16);
const iv = combined.slice(16, 28);
const ciphertext = combined.slice(28);
const keyMaterial = await crypto.subtle.importKey(
"raw", enc.encode(password), "PBKDF2", false, ["deriveKey"]
);
const key = await crypto.subtle.deriveKey(
{ name: "PBKDF2", salt, iterations: 250000, hash: "SHA-256" },
keyMaterial,
{ name: "AES-GCM", length: 256 },
false,
["decrypt"]
);
const plaintext = await crypto.subtle.decrypt({ name: "AES-GCM", iv }, key, ciphertext);
return dec.decode(plaintext);
}
PBKDF2 Iteration Count
PBKDF2's iteration count slows down brute-force attacks on the password. OWASP recommends at least 600,000 iterations for PBKDF2-SHA256 in 2024. The example above uses 250,000 (still acceptable). As hardware gets faster, increase this count.
Security Considerations
- Never hardcode keys or passwords in source code.
- The IV must be random and unique for every encryption with the same key. Reusing an IV with AES-GCM completely breaks security.
- GCM authentication tag is included in the ciphertext output by the Web Crypto API — don't truncate it.
- Client-side encryption does not protect against XSS — if an attacker can run JavaScript in your page, they can intercept the plaintext before encryption.
Encrypt Text in Your Browser
Use ToolsVito's AES Encrypt/Decrypt tool to encrypt text with a password directly in your browser — uses AES-256 with PBKDF2 key derivation, nothing leaves your device.