Cryptography sounds intimidating — but once you understand the core idea behind shared-key (symmetric) cryptosystems, you'll wonder why you ever found it mysterious. In this post, we'll break down what it is, how it works, and how to implement it practically in JavaScript using the Web Crypto API.
What Is a Shared-Key Cryptosystem?
A shared-key cryptosystem (also called a symmetric-key cryptosystem) is one where the same key is used to both encrypt and decrypt data. Think of it like a physical padlock — whoever has the key can lock and unlock it.
Plaintext → [ Encrypt with Key K ] → Ciphertext → [ Decrypt with Key K ] → Plaintext
This is in contrast to asymmetric cryptography (like RSA), which uses a public/private key pair. Symmetric encryption is:
- Fast — ideal for large amounts of data
- Efficient — lower computational overhead
- ⚠️ Key distribution problem — both parties must securely share the same key
Popular shared-key algorithms include:
- AES (Advanced Encryption Standard) — the gold standard today
- ChaCha20 — modern, fast, used in TLS 1.3
- 3DES — legacy, being phased out
The Key Exchange Problem
Here's the fundamental challenge: if Alice and Bob want to communicate secretly using a shared key, how do they exchange that key without Eve intercepting it?
Alice ──── "Here's our secret key!" ────→ Eve intercepts ──→ Bob
This is solved in practice by:
- Diffie-Hellman key exchange — mathematically derive a shared secret over a public channel
- Asymmetric encryption — encrypt the symmetric key with a public key, then decrypt with a private key (hybrid encryption, used in TLS)
For this post, we'll focus on the encryption/decryption mechanics assuming the key is already securely shared.
Hands-On: AES-GCM in JavaScript
The Web Crypto API (window.crypto.subtle) is built into modern browsers and Node.js 18+. It gives us access to AES-GCM — AES in Galois/Counter Mode — which provides both encryption and authentication (it detects tampering).
Step 1: Generate a Shared Key
async function generateKey() {
const key = await crypto.subtle.generateKey(
{
name: "AES-GCM",
length: 256, // 256-bit key — strong!
},
true, // extractable: allows export
["encrypt", "decrypt"]
);
return key;
}
const sharedKey = await generateKey();
console.log("Key generated:", sharedKey);
Step 2: Encrypt a Message
AES-GCM requires a random IV (Initialization Vector) for every encryption. The IV doesn't need to be secret — just unique per operation.
async function encrypt(key, plaintext) {
const iv = crypto.getRandomValues(new Uint8Array(12)); // 96-bit IV for AES-GCM
const encodedText = new TextEncoder().encode(plaintext);
const ciphertext = await crypto.subtle.encrypt(
{ name: "AES-GCM", iv },
key,
encodedText
);
// Return both IV and ciphertext — the IV is needed for decryption
return { iv, ciphertext };
}
const message = "Hello, Bob! This is our secret message. ";
const { iv, ciphertext } = await encrypt(sharedKey, message);
console.log("Encrypted:", ciphertext);
Why bundle the IV with the ciphertext?
The IV is required for decryption. It's not secret — it just must be unique. Sending it alongside the ciphertext is standard practice.
Step 3: Decrypt the Message
async function decrypt(key, iv, ciphertext) {
const decryptedBuffer = await crypto.subtle.decrypt(
{ name: "AES-GCM", iv },
key,
ciphertext
);
return new TextDecoder().decode(decryptedBuffer);
}
const decryptedMessage = await decrypt(sharedKey, iv, ciphertext);
console.log("Decrypted:", decryptedMessage);
// → "Hello, Bob! This is our secret message. "
Exporting and Importing Keys
In real applications, you'll need to serialize the key — to send it over a wire, store it in a database, or share it between sessions.
// Export the key to raw bytes
async function exportKey(key) {
const exported = await crypto.subtle.exportKey("raw", key);
return new Uint8Array(exported);
}
// Import raw bytes back into a CryptoKey
async function importKey(rawKeyBytes) {
return crypto.subtle.importKey(
"raw",
rawKeyBytes,
{ name: "AES-GCM", length: 256 },
true,
["encrypt", "decrypt"]
);
}
const rawKey = await exportKey(sharedKey);
console.log("Raw key bytes:", rawKey);
const importedKey = await importKey(rawKey);
const testDecrypt = await decrypt(importedKey, iv, ciphertext);
console.log("Decrypted with imported key:", testDecrypt);
// ✅ Still works!
Putting It All Together: A Complete Example
// ============================================================
// Shared-Key Cryptosystem Demo — AES-GCM with Web Crypto API
// ============================================================
const Crypto = {
async generateKey() {
return crypto.subtle.generateKey(
{ name: "AES-GCM", length: 256 },
true,
["encrypt", "decrypt"]
);
},
async encrypt(key, plaintext) {
const iv = crypto.getRandomValues(new Uint8Array(12));
const encoded = new TextEncoder().encode(plaintext);
const ciphertext = await crypto.subtle.encrypt(
{ name: "AES-GCM", iv },
key,
encoded
);
return { iv, ciphertext };
},
async decrypt(key, iv, ciphertext) {
const buffer = await crypto.subtle.decrypt(
{ name: "AES-GCM", iv },
key,
ciphertext
);
return new TextDecoder().decode(buffer);
},
};
// --- Simulation ---
(async () => {
// Alice and Bob agree on a shared key (assume secure exchange)
const sharedKey = await Crypto.generateKey();
// Alice encrypts
const secret = "The treasure is buried under the old oak tree. 🌳";
const { iv, ciphertext } = await Crypto.encrypt(sharedKey, secret);
console.log("Alice sends encrypted message.");
// Bob decrypts
const revealed = await Crypto.decrypt(sharedKey, iv, ciphertext);
console.log("Bob reads:", revealed);
})();
Security Considerations ⚠️
Before you ship anything crypto-related, keep these in mind:
| Practice | Why It Matters |
|---|---|
| Never reuse an IV | Reusing an IV with the same key completely breaks AES-GCM security |
| Use AES-GCM over AES-CBC | GCM provides authentication; CBC does not (vulnerable to padding oracle attacks) |
| Don't roll your own crypto | Stick to well-audited APIs like Web Crypto |
| Key length: 256-bit | 128-bit is technically secure, but 256-bit is the modern standard |
| Protect your keys | An encrypted message is only as safe as the key protecting it |
When to Use Shared-Key vs. Asymmetric Cryptography
| Shared-Key (Symmetric) | Asymmetric | |
|---|---|---|
| Speed | Very fast | Slow |
| Key management | Single shared secret | Public/private pair |
| Best for | Bulk data encryption | Key exchange, digital signatures |
| Examples | AES, ChaCha20 | RSA, ECC |
In practice, most systems use both: asymmetric crypto to securely exchange a symmetric key, then symmetric crypto for the actual data. This is exactly how TLS/HTTPS works.
Recap
Here's what we covered:
- Shared-key cryptosystems use the same key to encrypt and decrypt
- They're fast and efficient, ideal for bulk data
- The Web Crypto API gives you production-grade AES-GCM in the browser and Node.js
- Always use a random, unique IV per encryption
- 🔐 Never reuse IVs, and never build your own crypto primitives
Cryptography in JavaScript has never been more accessible — and with the Web Crypto API, you have no excuse to use weak homebrew solutions. Go build something secure!
Found this helpful? Drop a ❤️ or leave a comment below. Got questions about key exchange or asymmetric crypto? Let me know — that's a great follow-up post topic!
Top comments (0)