Signal Protocol Layer: The Two-Person Phone Booth
Mental Model: The Two-Person Phone Boothβ
Imagine you and your friend want to talk privately in a crowded room. You step into a phone boothβa small, enclosed space where you can talk without being heard.
Signal is like that booth, but with a magic twist:
- The booth ratchets forward with every sentence you speak
- Each ratchet burns the previous conversation (can't go back!)
- The booth's magic key changes after each word you say
Result: Complete forward secrecy. If Eve breaks into the booth now, she can only hear what you're saying right nowβnot earlier, and not future conversations.
Why Use Signal in Cascading Cipher?β
π― The Problemβ
In the DH chapter, we learned we can create a shared secret. But what about:
- Compromise of a private key β Eve decrypts all past messages
- Message ordering β Ensure messages are decrypted in the right order
- Loss of synchronization β Handle when someone goes offline
π‘οΈ The Signal Protocol Solutionβ
Signal solves these with a double ratchet:
Root Ratchet (Like a Master Key)β
- Root Key: Long-term secret from DH handshake
- Chain Key: Temporary key from the Root Key
- Message Key: Per-message encryption key
Sending Ratchet (Like a Forward-Only Counter)β
- Each message gets a new random Message Key
- After sending, the Chain Key updates (can't reverse!)
- Old Message Keys are deleted forever
Receiving Ratchet (Like a Message Queue)β
- Track which message numbers you've decrypted
- Decrypt in order, reject duplicates
- Skip old or out-of-order messages
Result: Perfect forward secrecy + compromise resilience + ordered delivery.
Where It Fits in the Stackβ
βββββββββββββββββββββββββββββββββββ
β π Signal Layer (Ratcheting) β β Per-message key rotation
βββββββββββββββββββββββββββββββββββ€
β π€ DH Layer (Key Exchange) β β Creates the root key
βββββββββββββββββββββββββββββββββββ€
β AES Layer (Foundation) β β Encrypts with each message key
βββββββββββββββββββββββββββββββββββ
Signal takes the DH secret and creates fresh keys for every message so compromise is limited to just that message.
How the Double Ratchet Worksβ
The Ratchet Chainβ
Step-by-Step Message Flowβ
1. Initial Setup (DH Handshake)β
// Alice and Bob do ECDH to create a root key
const rootKey = await deriveRootKey(aliceKp, bobKp)
// rootKey: long-lived shared secret
// Initialize the first chain key
const chainKey0 = await KDF(rootKey, "chainKey0")
2. Sending Message 1β
// Derive Message Key 1 from Chain Key 0
const messageKey1 = await KDF(chainKey0, "messageKey1")
// Encrypt Message 1 with Message Key 1
const encrypted1 = await AES_GCMEncrypt(message1, messageKey1)
// Ratchet the chain key to Chain Key 1
const chainKey1 = await KDF(chainKey0, "ratchet")
// Delete messageKey1 and chainKey0!
await secureDelete([messageKey1, chainKey0])
3. Sending Message 2β
// Derive Message Key 2 from Chain Key 1
const messageKey2 = await KDF(chainKey1, "messageKey2")
// Encrypt Message 2 with Message Key 2
const encrypted2 = await AES_GCMEncrypt(message2, messageKey2)
// Ratchet to Chain Key 2
const chainKey2 = await KDF(chainKey1, "ratchet")
// Delete messageKey2 and chainKey1!
await secureDelete([messageKey2, chainKey1])
4. Receiving and Decryptingβ
Bob receives encrypted1 first:
// Bob has the same rootKey from DH
const bobChainKey0 = await KDF(rootKey, "chainKey0")
const bobMessageKey1 = await KDF(bobChainKey0, "messageKey1")
// Decrypt Message 1
const decrypted1 = await AES_GCMDecrypt(encrypted1, bobMessageKey1)
// Ratchet Bob's chain key to stay in sync
const bobChainKey1 = await KDF(bobChainKey0, "ratchet")
await secureDelete([messageKey1, bobChainKey0])
// Track that we decrypted message 1
await storeMessageNumber(1)
5. Receiving Message 2β
// Derive Message Key 2 from Chain Key 1
const bobMessageKey2 = await KDF(bobChainKey1, "messageKey2")
// Decrypt Message 2
const decrypted2 = await AES_GCMDecrypt(encrypted2, bobMessageKey2)
// Ratchet to Chain Key 2
const bobChainKey2 = await KDF(bobChainKey1, "ratchet")
await secureDelete([messageKey2, bobChainKey1])
// Track that we decrypted message 2
await storeMessageNumber(2)
Why Can't Eve Go Back?β
The Cryptographic Arrowβ
The KDF (Key Derivation Function) is a one-way function:
Chain Key 0 --KDF--> Message Key 1
Chain Key 0 --KDF--> Chain Key 1
Given Chain Key 1, you cannot derive Chain Key 0 or Message Key 1. The chain only moves forward!
If Eve Compromises Alice's Phoneβ
Before Signal (DH + AES only):β
Eve gets Alice's private key
β Decrypts DH shared secret
β Derives AES key
β Decrypts **all** messages (past and future!)
After Signal (DH + AES + Signal ratchet):β
Eve gets Alice's private key
β Derives ROOT KEY (good)
β But can't get past chain keys (they're deleted)
β Can't derive MESSAGE KEYS (already gone)
β Decrypts **only current message**!
β Future messages? Use new Chain Key ratcheted forward
β Past messages? Message Keys deleted forever
Using the Signal Layerβ
Basic Setupβ
import { SignalCipherLayer } from '@cascading-cipher/signal-layer'
// Initialize DH keys (from previous chapter)
const myKeyPair = await crypto.subtle.generateKey(
{ name: "ECDH", namedCurve: "P-256" },
true,
["deriveKey", "deriveBits"]
)
// Create a Signal layer
const signalLayer = new SignalCipherLayer({
myKeyPair,
theirPublicKey: otherPersonPublicKey,
rootKey: sharedDHSecret
})
Sending a Messageβ
// Send a message (Signal generates a fresh key automatically!)
const plaintext = "Message with forward secrecy!"
const encrypted = await signalLayer.encrypt(
new TextEncoder().encode(plaintext)
)
// Output includes:
// - ciphertext (encrypted with fresh key)
// - message number (for ordering)
// - authentication tag (integrity)
console.log('Encrypted:', encrypted)
Receiving and Decryptingβ
// Decrypt the message
const decrypted = await signalLayer.decrypt({
ciphertext,
messageNumber: 1,
authTag
})
console.log('Decrypted:', new TextDecoder().decode(decrypted))
// "Message with forward secrecy!"
Message Numbering (Automatic)β
Signal automatically tracks message numbers:
// Alice sends message 1, 2, 3...
await signalLayer.encrypt(new Uint8Array([1])) // #1
await signalLayer.encrypt(new Uint8Array([2])) // #2
await signalLayer.encrypt(new Uint8Array([3])) // #3
// Bob receives and decrypts #1, #2, #3
const decrypted = await signalLayer.decrypt(encryptedMessage)
// Decrypts in order, rejects duplicates or out-of-order!
Integration with CascadingCipherManagerβ
import { CascadingCipherManager } from '@cascading-cipher/manager'
import { AESCipherLayer } from '@cascading-cipher/aes-layer'
import { DHCipherLayer } from '@cascading-cipher/dh-layer'
import { SignalCipherLayer } from '@cascading-cipher/signal-layer'
// Add ALL layers: DHCipherLayer provides root key
const manager = new CascadingCipherManager()
// 1. DH layer establishes the root key
await manager.addLayer(new DHCipherLayer({
myPrivateKey: myPrivateKey,
theirPublicKey: theirPublicKey
}))
// 2. Signal layer creates per-message keys from the root
await manager.addLayer(new SignalCipherLayer({
myKeyPair,
theirPublicKey,
rootKey: derivedRootKey
}))
// 3. AES encrypts with each per-message key
await manager.addLayer(new AESCipherLayer())
// Encrypt: DH root key β Signal message key β AES encryption
const encrypted1 = await manager.encrypt(
"Message 1 with forward secrecy!"
)
const encrypted2 = await manager.encrypt(
"Message 2 - completely different!"
)
// Decrypt: AES decrypt β Signal ratchets key
const decrypted1 = await manager.decrypt(encrypted1)
const decrypted2 = await manager.decrypt(encrypted2)
Complete Example: Secure Conversationβ
async function secureConversation() {
// Setup 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"]
)
// Create managers
const aliceManager = new CascadingCipherManager()
const bobManager = new CascadingCipherManager()
// Add layers (same for both)
for (const [manager, myKeys, theirKeys] of [
[aliceManager, aliceKeyPair, bobKeyPair],
[bobManager, bobKeyPair, aliceKeyPair]
]) {
await manager.addLayer(new DHCipherLayer({
myPrivateKey: myKeys.privateKey,
theirPublicKey: theirKeys.publicKey
}))
await manager.addLayer(new SignalCipherLayer({
myKeyPair: myKeys,
theirPublicKey: theirKeys.publicKey,
rootKey: await deriveRootKey(myKeys, theirKeys)
}))
await manager.addLayer(new AESCipherLayer())
}
// Alice sends 5 messages
const aliceMessages = [
"Hello Bob!",
"How are you?",
"This is message 3",
"Message 4 here",
"Final message!"
]
const encryptedMessages = []
for ( const msg of aliceMessages ) {
const encrypted = await aliceManager.encrypt(msg)
encryptedMessages.push(encrypted)
console.log(`Alice sent: ${msg}`)
}
// Bob decrypts them all
const decryptedMessages = []
for (const encrypted of encryptedMessages) {
const decrypted = await bobManager.decrypt(encrypted)
decryptedMessages.push(new TextDecoder().decode(decrypted))
}
// Verify
console.log('\nBob received:')
decryptedMessages.forEach((msg, i) => {
console.log(`${i + 1}: ${msg}`)
assert(msg === aliceMessages[i])
})
// Even if Eve compromised Alice's phone now:
// - She can get the root key (from DH)
// - But message keys for messages 1-5 are gone
// - Future messages? Bob ratchets, so they use new keys
console.log('\nβ
Perfect forward secrecy: Only current keys exposed!')
}
function deriveRootKey(keyPair1, keyPair2) {
// In real Signal, this is more complex
// For now, use ECDH to derive a shared secret
return crypto.subtle.deriveBits(
{ name: "ECDH", public: keyPair2.publicKey },
keyPair1.privateKey,
256
)
}
Security Propertiesβ
β Perfect Forward Secrecy (PFS)β
- Compromise of a private key reveals only the current message
- Past messages: keys deleted forever
- Future messages: new keys ratcheted forward
β Post-Compromise Security (PCS)β
If Eve compromises Bob's phone:
- She can decrypt only the current message Bob is sending
- Future messages: root key ratcheted β new chain/key
- Recovery: Reestablish DH handshake β new root key
β Future Secrecyβ
If Alice and Bob re-ratchet (new DH handshake):
- Eve's compromise of old DH keys reveals nothing
- Everything uses the new root key
β Message Orderingβ
Signal rejects:
- Out-of-order messages
- Duplicate messages
- Messages from the past (old message numbers)
Quiz Time!β
π§ Why can't Eve decrypt past messages even if she has the root key?
Because Signal deletes the Chain Keys and Message Keys after use! The root key is long-lived, but the actual encryption keys (Message Keys) are derived from Chain Keys, which are constantly ratcheting forward. Eve would need to derive old Chain Keys from new onesβwhich is impossible since KDF is one-way.
π§ What if Eve records an encrypted message and later learns Alice's private key?
Eve can:
- Derive the Root Key from the DH handshake (good)
- Get the CURRENT Chain Key (if she has it)
BUT she can't:
- Get the Message Key used for that past message (deleted)
- Get past Chain Keys (can't reverse the ratchet)
So that past message stays encrypted forever!
π§ Why does Signal track message numbers?
To ensure you decrypt messages in the right order and reject duplicates. If you receive message #3 but haven't decrypted #2 yet, Signal rejects it. If you receive #2 again (duplicate), Signal rejects it. This prevents replay attacks and ensures synchronization.
π§ What's the difference between forward secrecy and perfect forward secrecy?
- Forward secrecy: Compromise reveals limited information (some messages safe)
- Perfect forward secrecy: Compromise of a private key reveals only the current message (all past and future messages safe)
Signal achieves perfect forward secrecy with the double ratchet.
π§ Why not just use a fresh DH key for every message?
Because DH is slow (computationally expensive) and you still need to handle the initial handshake. Signal is more efficient: it does one DH handshake to get a root key, then uses fast KDF to derive all the message keys. You get the security of many handshakes without the cost!
Can You Explain to a 5-Year-Old?β
Imagine talking on a magic phone booth:
- You enter the booth with your friend
- The booth has a special lock that changes after every word
- After you say a word, the old lock disappears!
- Even if someone breaks in later, they only hear what you're saying right now
- Your earlier words are gone forever (the lock disappeared)
- Future words use a new lock (the ratchet moved forward)
It's like taking a photo, burning the photo, and never being able to show that moment again. Every word is its own self-destructing photo.
Key Takeawaysβ
β Double ratchet: Root key β Chain key β Message key (all one-way)
β Forward only: Keys ratchet forward, never backward
β Perfect PFS: Past and future messages safe on compromise
β Message keys deleted: Burn after use (self-destructing)
β Ordered delivery: Reject duplicates and out-of-order
β One DH β many keys: Fast but just as secure as many handshakes
β Used by: Signal, WhatsApp (3 of 4), Matrix (optionally)