Advanced Cryptography in Wasm
Introduction
In the companion lesson on cryptographic hashing, we covered one-way functions. This lesson goes further into the building blocks of encryption -- reversible transformations that protect data confidentiality. We implement three progressively more sophisticated ciphers in pure Rust and explain the principles behind modern algorithms like AES.
Why Wasm for cryptography?
WebAssembly has several properties that make it suitable for cryptographic operations:
| Property | Benefit for crypto |
|---|---|
| Predictable execution | No JIT compiler reoptimizations that could leak timing info |
| No garbage collection | No GC pauses that create timing side channels |
| Constant-time operations | Easier to write constant-time code than in JS |
| Linear memory model | No pointer-chasing overhead, cache-friendly |
| Sandboxed | Code cannot access memory outside its linear memory |
| Portable | Same binary runs identically on all platforms |
Important caveat: Wasm is better than JavaScript for crypto, but not perfect. Wasm memory can still be inspected by the host, and the Web Crypto API should be preferred for production use when it supports your algorithm.
Cipher taxonomy
Ciphers
├── Symmetric (same key encrypts and decrypts)
│ ├── Stream ciphers (encrypt byte-by-byte)
│ │ ├── XOR cipher (trivial)
│ │ ├── RC4 (deprecated)
│ │ └── ChaCha20 (modern)
│ └── Block ciphers (encrypt fixed-size blocks)
│ ├── Feistel networks
│ │ ├── DES (deprecated, 56-bit key)
│ │ ├── 3DES (legacy)
│ │ └── Blowfish / Twofish
│ └── Substitution-permutation networks
│ └── AES (current standard, 128/192/256-bit)
└── Asymmetric (public key encrypts, private key decrypts)
├── RSA
├── Diffie-Hellman / ECDH
└── Ed25519 (signatures)XOR cipher: the simplest encryption
The XOR cipher is the foundation of all modern stream ciphers. It works because XOR is its own inverse:
Encrypt: plaintext XOR key = ciphertext
Decrypt: ciphertext XOR key = plaintext
Proof:
A XOR B XOR B = A (XOR is self-inverse)
Truth table for XOR:
0 XOR 0 = 0
0 XOR 1 = 1
1 XOR 0 = 1
1 XOR 1 = 0The key is repeated cyclically across the data:
Plaintext: H e l l o , W e b
Key: S E C R E T S E C R
XOR: 1B 20 2F 3E 2A 78 73 12 26 30Why XOR alone is insecure: With a repeating key, frequency analysis can recover the key. Modern stream ciphers (ChaCha20) use XOR but generate the key stream from a CSPRNG, making each "key byte" effectively random.
Caesar cipher: substitution fundamentals
The Caesar cipher shifts each letter by a fixed amount in the alphabet. With shift=3, A becomes D, B becomes E, etc.
Alphabet: A B C D E F G H I J K L M N O P Q R S T U V W X Y Z
Shift 13: N O P Q R S T U V W X Y Z A B C D E F G H I J K L M
ROT13 ("HELLO") = "URYYB"ROT13 (shift=13) is its own inverse because 13 + 13 = 26 (the alphabet length). This is used for spoiler text in forums.
Why Caesar is insecure: Only 25 possible keys -- an attacker can try all of them in microseconds. Letter frequency analysis also breaks it instantly (E is the most common English letter).
The Feistel network: how real block ciphers work
A Feistel network is the structure behind DES, Blowfish, and many other block ciphers. Its key insight is that encryption and decryption use the same code -- you just reverse the key order.
Feistel round (encrypt):
┌─────────┐ ┌─────────┐
│ Left │ │ Right │
└────┬─────┘ └────┬─────┘
│ │
│ ┌────┴─────┐
│ │ f(R, K) │ ← Round function with round key
│ └────┬─────┘
│ │
▼ XOR ▼
├──────────────⊕
│ │
▼ ▼
┌─────────┐ ┌─────────┐
│ new Right│ │ new Left │ ← Swap: new_left = old_right
└─────────┘ └─────────┘ new_right = old_left XOR f(R, K)The round function f(R, K) does NOT need to be reversible! The Feistel structure guarantees reversibility regardless of what f does. This is the mathematical elegance that made it so popular.
Decryption: Run the same algorithm but with round keys in reverse order. The XOR operations undo themselves.
Block cipher modes: ECB vs CBC
A block cipher encrypts fixed-size blocks (e.g., 64 or 128 bits). To encrypt longer messages, you need a mode of operation:
ECB (Electronic Codebook) -- DO NOT USE
Each block is encrypted independently with the same key:
ECB Mode:
Block 1 ──▶ [Encrypt(K)] ──▶ Cipher 1
Block 2 ──▶ [Encrypt(K)] ──▶ Cipher 2
Block 3 ──▶ [Encrypt(K)] ──▶ Cipher 3
Problem: identical plaintext blocks → identical ciphertext blocks
Patterns in the data are preserved!This is the famous "ECB penguin" problem -- encrypting an image in ECB mode preserves the outline of the image because identical pixel blocks produce identical cipher blocks.
CBC (Cipher Block Chaining) -- Better
Each block is XORed with the previous ciphertext block before encryption:
CBC Mode:
IV ─────────────┐
▼
Block 1 ──XOR──▶ [Encrypt(K)] ──▶ Cipher 1 ──┐
▼
Block 2 ──────────────────────XOR──▶ [Encrypt(K)] ──▶ Cipher 2 ──┐
▼
Block 3 ──────────────────────────────────────XOR──▶ [Encrypt(K)] ──▶ Cipher 3CBC requires an Initialization Vector (IV) -- a random value for the first block. Identical plaintext blocks produce different ciphertext because the chaining makes each block depend on all previous blocks.
AES: the modern standard
AES (Advanced Encryption Standard) uses a Substitution-Permutation Network (SPN) instead of a Feistel network:
AES-128 structure (10 rounds):
┌──────────────┐
│ 128-bit block │
├──────────────┤
│ SubBytes │ ← Non-linear byte substitution (S-box)
│ ShiftRows │ ← Circular shift of rows
│ MixColumns │ ← Matrix multiplication in GF(2^8)
│ AddRoundKey │ ← XOR with round key
├──────────────┤
│ ... repeat │ × 10 rounds
├──────────────┤
│ Ciphertext │
└──────────────┘AES is not practical to implement from scratch for a playground demo (the S-box alone is a 256-byte lookup table), but the RustCrypto ecosystem provides production-ready implementations.
Diffie-Hellman key exchange
Diffie-Hellman allows two parties to establish a shared secret over an insecure channel:
Alice Bob
───── ───
Choose secret a Choose secret b
Compute A = g^a mod p Compute B = g^b mod p
──── send A ────▶
◀──── send B ────
Compute s = B^a mod p Compute s = A^b mod p
Both get: s = g^(ab) mod p
Eavesdropper sees: g, p, A, B
Cannot compute: s (discrete logarithm problem)The RustCrypto ecosystem
For production cryptography in Rust/Wasm, use the RustCrypto crates:
| Crate | Purpose | Wasm compatible |
|---|---|---|
aes |
AES block cipher | Yes |
chacha20poly1305 |
Authenticated encryption | Yes |
rsa |
RSA encryption/signatures | Yes |
ed25519-dalek |
Ed25519 signatures | Yes |
x25519-dalek |
X25519 key exchange | Yes |
sha2 |
SHA-256/512 hashing | Yes |
argon2 |
Password hashing | Yes |
All of these are pure Rust with no C dependencies, so they compile to Wasm without modification.
Digital signatures
Digital signatures prove that a message came from a specific sender and was not tampered with:
Signing (private key):
Message ──▶ [Hash] ──▶ [Sign with private key] ──▶ Signature
Verification (public key):
Message ──▶ [Hash] ──▶ ┐
├──▶ [Verify] ──▶ Valid/Invalid
Signature ─────────────▶ ┘Common algorithms: RSA-PSS, Ed25519, ECDSA. Ed25519 is preferred for new systems due to its speed, small key/signature size, and resistance to implementation pitfalls.
Try it
Extend the code to:
- Implement CBC mode for the Feistel cipher by chaining blocks with XOR
- Add a brute-force Caesar cracker that tries all 25 shifts and scores results by English letter frequency
- Implement a Vigenere cipher (polyalphabetic substitution) which is a stronger version of Caesar
- Demonstrate why ECB mode is insecure by encrypting a repeating pattern and showing that ciphertext blocks repeat