DEV Community

Cover image for Shared-Key Cryptosystems in JavaScript: A Practical Guide
Daniel Keya
Daniel Keya

Posted on

Shared-Key Cryptosystems in JavaScript: A Practical Guide

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

This is solved in practice by:

  1. Diffie-Hellman key exchange — mathematically derive a shared secret over a public channel
  2. 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);
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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. "
Enter fullscreen mode Exit fullscreen mode

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!
Enter fullscreen mode Exit fullscreen mode

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);
})();
Enter fullscreen mode Exit fullscreen mode

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:

  1. Shared-key cryptosystems use the same key to encrypt and decrypt
  2. They're fast and efficient, ideal for bulk data
  3. The Web Crypto API gives you production-grade AES-GCM in the browser and Node.js
  4. Always use a random, unique IV per encryption
  5. 🔐 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)