Skip to main content

Decrypting Messages: Opening the Package

Mental Model: Unwrapping the Gift

Imagine receiving a beautifully wrapped gift:

  1. The gift comes in multiple layers of wrapping paper
  2. Each layer is sealed and has its own lock
  3. To open it, you must unwrap layer by layer in reverse order
  4. 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)

LayerWhat It DoesWhat It Returns
ML-KEM LayerDecapsulate post-quantum keyDecrypted N-1
Signal LayerDerive message key, decryptDecrypted N-2
DH LayerDerive key, decryptDecrypted N-1
AES LayerDecrypt with IV and keyPlaintext 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

  1. Check layer order:

    console.log('My layers:', manager.layers.map(l => l.name))
    console.log('Sender layers:', encryptedPayload.metadata.layerOrder)
    // Should be the same!
  2. Verify keys:

    console.log('DH public key matches?', ...)
    console.log('Password correct?', ...)
  3. 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:

  1. You get a box wrapped in red paper, then blue paper, then green paper
  2. To find your gift, you must unwrap the green paper first, then blue, then red
  3. If you try to unwrap green first (innermost), it won't work—it's stuck to the next layer!
  4. 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