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