Skip to main content

Sending Messages: The Package Delivery

Mental Model: The Mailing System

Imagine you want to send a package across the country:

  1. You pack your item in a box
  2. The courier picks it up
  3. It passes through various transit hubs (sorting centers)
  4. At each hub, it gets sorted and labeled
  5. Finally, the courier delivers it to the recipient

CascadingCipher is like that mailing system:

  • Your message = the item
  • Each cipher layer = a transit hub
  • Encryption = sorting and labeling
  • Decryption = opening the package

The CascadingCipherManager is the mail carrier who knows the route!

The Encryption Flow

Step 1: Manager Receives Your Message

Step 2: Bottom-Up Encryption (Cascading)

Each layer processes the message in order:

Important: Encryption flows downstream—from the first layer to the last!


How It Works: The Encryption Process

The Manager's Job

The CascadingCipherManager orchestrates the encryption:

async encrypt(plaintext: string): Promise<EncryptedPayload> {
// 1. Convert plaintext to bytes
const plaintextBytes = new TextEncoder().encode(plaintext)

// 2. Start with the first layer
let currentPayload: Uint8Array = plaintextBytes

// 3. Pass through each layer in order
for (const layer of this.layers) {
currentPayload = await layer.encrypt(currentPayload)
}

// 4. Wrap in an EncryptedPayload
const encryptedPayload: EncryptedPayload = {
ciphertext: currentPayload,
metadata: {
layerOrder: this.layers.map(l => l.name),
timestamps: Date.now()
}
}

return encryptedPayload
}

Each layer knows how to encrypt its data:

// Example: AES Layer
async encrypt(data: Uint8Array): Promise<Uint8Array> {
// 1. Generate a random IV (initialization vector)
const iv = crypto.getRandomValues(new Uint8Array(12))

// 2. Derive encryption key from password or DH secret
const key = await this.deriveKey()

// 3. Encrypt the data
const encrypted = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv },
key,
data
)

// 4. Return IV + ciphertext
return new Uint8Array([...iv, ...new Uint8Array(encrypted)])
}

Full Example: Encrypting a Message

const manager = new CascadingCipherManager()

// Add layers in order: bottom (AES) → top (ML-KEM)
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
}))

// Encrypt a message
const plaintext = 'Hello secure world!'
const encrypted = await manager.encrypt(plaintext)

console.log('Encrypted:', encrypted)

// Output:
// {
// ciphertext: Uint8Array(1024),
// metadata: {
// layerOrder: ['AES', 'DH', 'Signal', 'MLKEM'],
// timestamps: 1238472938472
// }
// }

What Each Layer Does

LayerWhat It DoesWhat It Returns
AES LayerEncrypt with password/DH keyIV + ciphertext
DH LayerDerive key, encryptKey-encrypted ciphertext
Signal LayerRatchet message keyMessage-encrypted ciphertext
ML-KEM LayerPost-quantum key encapsulationPost-quantum ciphertext

Each layer takes the previous layer's output and encrypts it again!


Visualizing the Encryption Flow


Why Layers Must Be in Order

The Foundation Rule

Layers must be applied in a specific order:

LAYER 1 (Foundation) → LAYER 2 (Context) → LAYER 3 (Post-Quantum)

If you flip the order:

ML-KEM (top) → Signal → DH (bottom)

Then ML-KEM encrypts plaintext (bad!) instead of the post-quantum protected message!

Why Foundation First?

Foundation layers (AES, DH) provide the core encryption:

  1. AES: Encrypts with a password-derived key
  2. DH: Derives the key from the shared secret

Context layers (Signal, MLS) add forward secrecy:

  1. Signal: Ratchets the key per message
  2. MLS: Handles group membership changes

Post-quantum layers (ML-KEM) add quantum resistance:

  1. ML-KEM: Protects against quantum computers

The "Building Blocks" Metaphor

Think of building a house:

  1. Foundation (Layer 1): Concrete base (AES/DH) essential for everything
  2. Walls (Layer 2): Structure (Signal/MLS) shape the space
  3. Roof (Layer 3): Weather protection (ML-KEM) covers everything

You can't put the roof on before the walls, or walls before the foundation!


Complete Example: Sending a Secure Email

async function sendSecureEmail() {
// 1. Set up Alice's manager
const aliceManager = new CascadingCipherManager()

// Generate keys
const aliceKeyPair = await crypto.subtle.generateKey(
{ name: "ECDH", namedCurve: "P-256" },
true,
["deriveKey", "deriveBits"]
)

const bobKeyPair = await crypto.subtle.generateKey(
{ name: "ECDH", namedCurve: "P-256" },
true,
["deriveKey", "deriveBits"]
)

const mlkemAlice = await MLKEM768.keyGen()
const mlkemBob = await MLKEM768.keyGen()

// Add layers in correct order
await aliceManager.addLayer(new AESCipherLayer({ password: 'alice-secret' }))
await aliceManager.addLayer(new DHCipherLayer({
myPrivateKey: aliceKeyPair.privateKey,
theirPublicKey: bobKeyPair.publicKey
}))
await aliceManager.addLayer(new SignalCipherLayer({
myKeyPair: aliceKeyPair,
theirPublicKey: bobKeyPair.publicKey
}))
await aliceManager.addLayer(new MLKEMCipherLayer({
myKeyPair: mlkemAlice,
theirPublicKey: mlkemBob.publicKey
}))

// 2. Encrypt the email
const email = {
to: 'bob@example.com',
from: 'alice@example.com',
subject: 'Project Alpha Proposal',
body: `Dear Bob,

Attached is our proposal for Project Alpha. We've done the market research,
and I think this is a strong path forward.

Please review and let me know if you have any concerns.

Best,
Alice`

const encrypted = await aliceManager.encrypt(JSON.stringify(email))

// 3. Send encrypted payload (to Alice and Bob)
console.log('Encrypted payload:', encrypted)

// 4. Alice stores her copy and sends to Bob
await saveToSent(encrypted)
await sendToBob(encrypted)

// Bob decrypts it (next chapter)
console.log('\n📧 Encrypted email sent to Bob!')
}

Quiz Time!

🧠 What happens if you don't add layers to the manager?

Nothing! The manager has no layers, so encrypt() returns the plaintext unchanged. You must add at least one layer to encrypt anything, or the message stays in plain text!

🧠 Why does encryption flow from Layer 1 to Layer N (not the reverse)?

Because each layer builds on the previous one. Layer 1 provides the base encryption (AES), Layer 2 provides the key (DH), etc. If you layered ML-KEM first, it would encrypt plaintext with a post-quantum key, but then you lose the benefit of the layered approach (each layer's output becomes the next layer's input).

🧠 What's the metadata field for?

It tracks which layers were used and when, so the receiver knows the correct decryption order. Without it, the receiver wouldn't know whether the ciphertext was encrypted with AES→DH→Signal→ML-KEM or ML-KEM→Signal→DH→AES (which wouldn't work!).

🧠 Can you add the same layer twice?

You can, but it doesn't improve security and wastes performance. Encrypting twice with the same layer (e.g., AES→AES) is redundant—the second encryption doesn't add benefits over the first. The layers should be different and complementary.


Can You Explain to a 5-Year-Old?

Imagine sending a letter to a friend:

  1. You write your letter (plaintext)
  2. You put it in an envelope and seal it (Layer 1: AES)
  3. You give it to the courier who adds their own seal (Layer 2: DH)
  4. The courier passes it through sorting centers, each adding their own envelope (Layer 3: Signal)
  5. Finally, a magic envelope protects it from robots (Layer 4: ML-KEM)

Your friend receives a stack of envelopes, unwraps each in reverse order, and finds your letter inside!


Key Takeaways

Manager orchestrates encryption: Passes through layers in order

Encryption flows downstream: Layer 1 → Layer 2 → ... → Layer N

Each layer encrypts previous output: Cascading effect

Order matters: Foundation first (AES/DH), then context layers

Metadata tracks layers: Receiver knows decryption order

Manager returns EncryptedPayload: Ciphertext + metadata

Redundant layers don't help: Use complementary layers, not same layer twice