Decrypting Messages: Opening the Package
Mental Model: Unwrapping the Gift
Imagine receiving a beautifully wrapped gift:
- The gift comes in multiple layers of wrapping paper
- Each layer is sealed and has its own lock
- To open it, you must unwrap layer by layer in reverse order
- First remove the outermost paper, then the next, and so on
Decryption is like unwrapping a gift:
- Ciphertext = multiple layers of encrypted data
- You must remove layers in reverse order (top → bottom)
- Each layer's key opens the lock for that layer
- Finally, you get the plaintext inside
The Decryption Flow
Step 1: Manager Receives Encrypted Payload
Step 2: Top-Down Decryption (Reverse Cascading)
Each layer decrypts the previous layer's output in reverse order:
Important: Decryption flows upstream—from the last layer to the first!
How It Works: The Decryption Process
The Manager's Job
The CascadingCipherManager orchestrates the decryption in reverse:
async decrypt(encryptedPayload: EncryptedPayload): Promise<string> {
// 1. Start with the ciphertext
let currentPayload: Uint8Array = encryptedPayload.ciphertext
// 2. Reverse iterate through layers (from N to 1)
for (let i = this.layers.length - 1; i >= 0; i--) {
const layer = this.layers[i]
currentPayload = await layer.decrypt(currentPayload)
}
// 3. Convert bytes to strings
return new TextDecoder().decode(currentPayload)
}
Each layer knows how to decrypt its data:
// Example: AES Layer
async decrypt(encryptedData: Uint8Array): Promise<Uint8Array> {
// 1. Extract IV (first 12 bytes)
const iv = encryptedData.slice(0, 12)
const ciphertext = encryptedData.slice(12)
// 2. Derive decryption key from password or DH secret
const key = await this.deriveKey()
// 3. Decrypt the data
const decrypted = await crypto.subtle.decrypt(
{ name: 'AES-GCM', iv },
key,
ciphertext
)
// 4. Return decrypted bytes
return new Uint8Array(decrypted)
}
Full Example: Decrypting a Message
const manager = new CascadingCipherManager()
// Add layers in the same order as encryption
await manager.addLayer(new AESCipherLayer({ password: 'secret' }))
await manager.addLayer(new DHCipherLayer({
myPrivateKey: myPrivateKey,
theirPublicKey: theirPublicKey
}))
await manager.addLayer(new SignalCipherLayer({
myKeyPair,
theirPublicKey,
rootKey
}))
await manager.addLayer(new MLKEMCipherLayer({
myKeyPair,
theirPublicKey
}))
// Decrypt the message (previously encrypted)
const decrypted = await manager.decrypt(encryptedPayload)
console.log('Decrypted:', decrypted)
// "Hello secure world!"
What Each Layer Does (in Reverse)
| Layer | What It Does | What It Returns |
|---|---|---|
| ML-KEM Layer | Decapsulate post-quantum key | Decrypted N-1 |
| Signal Layer | Derive message key, decrypt | Decrypted N-2 |
| DH Layer | Derive key, decrypt | Decrypted N-1 |
| AES Layer | Decrypt with IV and key | Plaintext bytes |
Each layer removes its protection, revealing the next layer's ciphertext!
Visualizing the Decryption Flow
The Critical Rule: Layers Must Match
Sender's Stack
AES → DH → Signal → ML-KEM (encryption)
Receiver's Stack
ML-KEM → Signal → DH → AES (decryption)
Critical: The receiver's layers must be the same and in the same order (decrypted in reverse)!
What If They Don't Match?
// Sender: AE → DH → Signal → ML-KEM
const aliceManager = new CascadingCipherManager()
await aliceManager.addLayer(new AESCipherLayer())
await aliceManager.addLayer(new DHCipherLayer(...))
await aliceManager.addLayer(new SignalCipherLayer(...))
await aliceManager.addLayer(new MLKEMCipherLayer(...))
const encrypted = await aliceManager.encrypt(message)
// Receiver: AES → DH → ML-KEM (missing Signal!)
const bobManager = new CascadingCipherManager()
await bobManager.addLayer(new AESCipherLayer())
await bobManager.addLayer(new DHCipherLayer(...))
await bobManager.addLayer(new MLKEMCipherLayer(...))
const decrypted = await bobManager.decrypt(encrypted)
// Error: Decryption fails! Can't decrypt Signal layer
Bob can't decrypt because the Signal layer is missing!
How to Ensure Matching
Always use the metadata!
// Receiver checks metadata
const receivedOrder = encryptedPayload.metadata.layerOrder
// ['AES', 'DH', 'Signal', 'MLKEM']
// Receiver adds layers in the same order
const manager = new CascadingCipherManager()
for (const layerName of receivedOrder) {
const layer = this.getLayerInstance(layerName)
await manager.addLayer(layer)
}
// Now decrypt (order matches!)
const decrypted = await manager.decrypt(encryptedPayload)
Complete Example: Receiving a Secure Email
async function receiveSecureEmail() {
// 1. Bob receives the encrypted email from Alice
const encrypted = await receiveFromAlice()
console.log('Received encrypted email')
// 2. Bob sets up his manager (same layers as Alice)
const bobManager = new CascadingCipherManager()
// Bob's keys (same as Alice setup)
const bobKeyPair = await crypto.subtle.generateKey(
{ name: "ECDH", namedCurve: "P-256" },
true,
["deriveKey", "deriveBits"]
)
const aliceKeyPair = await crypto.subtle.generateKey(
{ name: "ECDH", namedCurve: "P-256" },
true,
["deriveKey", "deriveBits"]
)
const mlkemBob = await MLKEM768.keyGen()
const mlkemAlice = await MLKEM768.keyGen() // Alice's public key
// Add layers in the SAME ORDER as Alice
await bobManager.addLayer(new AESCipherLayer({
password: 'bob-secret' // Bob's password!
}))
await bobManager.addLayer(new DHCipherLayer({
myPrivateKey: bobKeyPair.privateKey,
theirPublicKey: aliceKeyPair.publicKey
}))
await bobManager.addLayer(new SignalCipherLayer({
myKeyPair: bobKeyPair,
theirPublicKey: aliceKeyPair.publicKey
}))
await bobManager.addLayer(new MLKEMCipherLayer({
myKeyPair: mlkemBob,
theirPublicKey: mlkemAlice.publicKey
}))
// 3. Bob decrypts the email
const decrypted = await bobManager.decrypt(encrypted)
const email = JSON.parse(decrypted)
console.log('\n📧 Decrypted email:')
console.log('To:', email.to)
console.log('From:', email.from)
console.log('Subject:', email.subject)
console.log('Body:', email.body)
return email
}
Handling Decryption Failures
Common Failures
try {
const decrypted = await manager.decrypt(encrypted)
} catch (error) {
console.error('Decryption failed:', error)
// Check common issues:
// 1. Incorrect password (AES layer)
// 2. Wrong DH keys (DH layer)
// 3. Message key not found (Signal layer)
// 4. Invalid ciphertext (corruption or tampering)
// For debugging:
console.error('Layer:', error.layer)
console.error('Error type:', error.type)
console.error('Details:', error.details)
}
Debugging Steps
-
Check layer order:
console.log('My layers:', manager.layers.map(l => l.name))
console.log('Sender layers:', encryptedPayload.metadata.layerOrder)
// Should be the same! -
Verify keys:
console.log('DH public key matches?', ...)
console.log('Password correct?', ...) -
Check for corruption:
console.log('Ciphertext length:', encrypted.ciphertext.length)
console.log('Metadata valid?', encryptedPayload.metadata)
Quiz Time!
🧠 Why does decryption happen in reverse (from N to 1)?
Because encryption happened in the opposite order (1 to N)! The ciphertext is encrypted: Layer 1 → Layer 2 → ... → Layer N. To decrypt, you must remove Layer N first, then N-1, ... down to Layer 1. It's like unwrapping gift paper—you remove the outermost layer first.
🧠 What happens if you try to decrypt in the wrong order?
Decryption fails! Each layer's ciphertext is encrypted with a specific layer's key. If you try to decrypt Layer 1's ciphertext with Layer 2's key, it won't work. You must follow the encryption path in reverse.
🧠 How does the receiver know which layers were used?
From the metadata! The EncryptedPayload includes metadata.layerOrder which lists the layer names in the order they were applied. The receiver uses this to add the same layers for decryption.
🧠 Can the receiver decode without the private keys for DH/ML-KEM?
No! They need the corresponding private keys to derive the decryption keys. If Eve intercepts the encrypted message but doesn't have one of the private keys, she can't decrypt that layer (and thus the whole message stays encrypted).
Details
🧠 What if the message is corrupted (tallampered)?
The AES-GCM layer throws an error ("authentication tag mismatch"). This is a good thing—it proves the message was tampered with and refuses to decrypt. You'd catch the error and discard the message.
Can You Explain to a 5-Year-Old?
Imagine receiving a gift with many layers:
- You get a box wrapped in red paper, then blue paper, then green paper
- To find your gift, you must unwrap the green paper first, then blue, then red
- If you try to unwrap green first (innermost), it won't work—it's stuck to the next layer!
- You follow the reverse order of wrapping to finally reach your present
Decryption is the same, but the gift paper is locked and needs special keys to undo each layer!
Key Takeaways
✅ Manager orchestrates decryption: Passes through layers in reverse
✅ Decryption flows upstream: Layer N → Layer N-1 → ... → Layer 1
✅ Each layer decrypts previous output: Reverse cascading
✅ Order must match: Sender's order = Receiver's order (decrypted reverse)
✅ Metadata tracks layers: Receiver knows which layers to add
✅ Decryption fails if mismatched: Wrong keys or order → error
✅ Tamper detection: AES-GCM rejects modified messages