Skip to main content

📝 Passing Notes in Class

Sending Messages in MLS

In 15 minutes: Learn to send and receive encrypted group messages
Prerequisite: Adding members


🎯 The Simple Story

Alice and Bob are in a secret meeting room (MLS group).

Alice wants to tell Bob: "Meeting at 2pm"

How does she do it?

She whispers in the room (encrypts with group secret K₁). Bob hears (decrypts with K₁) because he's in the room

Eve is outside the room (can't hear anything).


🧠 Mental Model

Hold this picture in your head:

Passing Notes (MLS Encrypted Messages):

Inside the room:
┌─────────────────────────────┐
│ Alice, Bob, Charlie inside │
│ Shared group secret: K₁ │
└─────────────────────────────┘

Alice wants to send: Hello:
1. Alice whispers: Hello
2. Encrypt with K₁ (group secret)
3. Send to: Bob, Charlie

Bob receives:
1. Decrypts with K₁ (he's inside)
2. Reads: Hello

Charlie receives:
1. Decrypts with K₁ (he's inside)
2. Reads: Hello

Eve (outside):
1. Can't decrypt K₁ (not in room)
2. Can't read anything

📊 See It Happen


🎭 Code: Sending a Message

Alice Encrypts

// Alice's code

// Alice wants to send message to group
const message = 'Hello everyone Meeting at 2pm';

// Encrypt for the group
const envelope = await alice.encryptMessage('team-chat', message);

// envelope contains:
// - groupId
// - ciphertext
// - timestamp

// Send envelope to transport (e.g., server)
// await sendToServer(envelope);

Bob Decrypts

// Bob's code

// Bob receives envelope from server
// const envelope = await receiveFromServer();

// Decrypt
const plaintext = await bob.decryptMessage(envelope);

console.log('From Alice:', plaintext);
// Output: Hello everyone Meeting at 2pm

🔄 What Happens Behind the Scenes?

encryptMessage() Flow

// What encryptMessage() does

await alice.encryptMessage('team-chat', 'Hello'):
1. Alice gets current group state
├─ group_id
├─ epoch
└─ K (current group secret)

2. Alice derives message key
├─ Use MLS message derivation
├─ Based on K+ epoch
└─ Get: message_key

3. Alice encrypts message
├─ plaintext: Hello
├─ key: message_key
├─ algorithm: AES-128-GCM
└─ ciphertext: C

4. Alice creates message envelope
├─ ciphertext: C
├─ timestamp: now
├─ authentication: Ed25519 signature
└─ group_id

5. Alice updates state
├─ Update leaf secret (ratcheting)
└─ Generate new leaf secret

6. Return envelope

Note: Bob does the same steps in reverse

decryptMessage() Flow

// What decryptMessage() does

await bob.decryptMessage(envelope):
1. Bob gets envelope
├─ ciphertext
├─ timestamp
└─ group_id

2. Bob verifies group
├─ Check group_id
└─ Check he's a member

3. Bob derives message key
├─ Use MLS message derivation
(same as Alice used)
├─ Based on K+ epoch
└─ Get: message_key

4. Bob decrypts message
├─ ciphertext: C
├─ key: message_key
├─ algorithm: AES-128-GCM
└─ plaintext: Hello

5. Bob verifies signature
├─ Check who sent it
└─ Prevent forgery

6. Bob updates state
├─ Update leaf secret (ratcheting)
└─ Generate new leaf secret

7. Return plaintext

🎮 Try It Yourself

Question 1: Alice encrypts "Hello" with K₁ and sends to Bob using transport. What does Bob do to read it?

Show Answer

Alice's steps:

  1. Get group secret K₁
  2. Derive message key from K₁
  3. Encrypt "Hello" = ciphertext C
  4. Send C to Bob

Bob's steps:

  1. Receive C
  2. Get group secret K₁
  3. Derive message key from K₁ (same derivation)
  4. Decrypt C = "Hello"

Answer: Bob decrypts using K₁ (same key Alice used)


Question 2: Eve intercepts an encrypted message. She doesn't have K₁. Can she read it?

Show Answer This

Eve has:

  • Ciphertext C
  • Wants to read "Hello"

Eve tries:

  1. Decryption needs message_key
  2. message_key derived from K₁
  3. Eve doesn't have K₁ (not in group)
  4. Can't derive message_key
  5. Can't decrypt C

Answer: No, Eve can't decrypt (missing K₁)


Question 3: How does MLS prevent message replay?

Show Answer

Message envelopes include:

  • timestamp
  • epoch

Each message is unique:

  • Same message in same epoch can't be replayed
  • Signatures authenticate sender
  • Timestamps prevent timing attacks

Answer: Timestamp + signatures prevent replay


💡 Complete Example

// ==========================================
// ALICE SENDS MESSAGE
// ==========================================

// Alice encrypts
const envelope = await alice.encryptMessage('team-chat', 'Meeting at 2pm');

// envelope structure:
// - groupId: Uint8Array
// - ciphertext: Uint8Array
// - timestamp: number

console.log('Encrypted message:', envelope.ciphertext);

// Send to transport (WebSocket, REST API, P2P, etc.)
// const result = await sendToServer(envelope);

// ==========================================
// BOB RECEIVES MESSAGE
// ==========================================

// Bob receives from transport
// const envelope = await receiveFromServer();

// Decrypt
const plaintext = await bob.decryptMessage(envelope);

console.log('Decrypted:', plaintext);
// Meeting at 2pm

✅ Quick Check

Can you explain MLS messaging to a 5-year-old?

Try saying this out loud:

"Sending messages in MLS is like whispering in a soundproof room. Everyone inside the room can hear you, but people outside can't hear anything. The secret is that everyone inside knows the same secret password to understand what's being said"


🎓 Key Takeaways

encryptMessage() = Encrypt with group secret
decryptMessage() = Decrypt with group secret
Message_key = Derived from K₁ + epoch
Envelope = ciphertext + metadata
Everyone in group = Can decrypt (has K₁)
People outside = Can't decrypt (no K₁)
Signatures = Authenticate sender


🎉 What You'll Learn Next

Now messaging works Let's handle members leaving:

👋 Continue: Kicking Someone Out

We'll learn how to remove members from an MLS group


Now you know how to send messages. Next: Let's remove members