Skip to content

Instantly share code, notes, and snippets.

@mrajaeim
Last active November 20, 2025 18:52
Show Gist options
  • Select an option

  • Save mrajaeim/078b5358c1e3a3b4bcb5279662daba2a to your computer and use it in GitHub Desktop.

Select an option

Save mrajaeim/078b5358c1e3a3b4bcb5279662daba2a to your computer and use it in GitHub Desktop.

🔐 Complete Guide to E2E Encryption Concepts

🔑 KEY TYPES & CONCEPTS

1. Identity Key Pair (Long-term Identity)

What it is:

  • Your permanent cryptographic identity in the system
  • Asymmetric key pair (public + private)
  • Like your digital fingerprint/passport

Properties:

Type: Asymmetric (RSA 2048-bit or Curve25519)
Lifespan: Permanent (unless device reset)
Shareable: Public key YES, Private key NEVER
Regenerate: Only if compromised or device reset

How to generate:

const identityKeyPair = await crypto.subtle.generateKey(
  {
    name: "ECDH",
    namedCurve: "P-256"
  },
  false, // non-extractable for security
  ["deriveKey"]
);

How to store:

  • Private key: Non-extractable in Web Crypto API, or encrypted in IndexedDB with user password
  • Public key: Uploaded to server, shared with everyone

If user loses it:

  • ❌ Cannot decrypt old messages (forward secrecy)
  • ❌ Cannot prove identity to contacts
  • ✅ Can create new identity and re-register
  • ✅ Need backup recovery phrase or encrypted cloud backup

Purpose:

  • Authenticate your identity to other users
  • Establish trust relationships
  • Sign other keys to prove they belong to you

2. Signed PreKey (Medium-term)

What it is:

  • A public key signed by your Identity Key
  • Proves the PreKey actually belongs to you
  • Changed periodically (weekly/monthly)

Properties:

Type: Asymmetric (Curve25519)
Lifespan: 1-4 weeks typically
Shareable: Public key YES, Private NEVER
Regenerate: YES, regularly rotated
Signed by: Your Identity Key

How to generate:

const signedPreKey = await generateKeyPair();
const signature = await signKey(signedPreKey.public, identityKey.private);

// Upload to server
await uploadToServer({
  publicKey: signedPreKey.public,
  signature: signature,
  keyId: generateId()
});

How to store:

  • Private: Encrypted locally
  • Public: On server with signature

If user loses it:

  • ✅ Generate new one easily
  • ⚠️ Old sessions may fail to decrypt (use multiple PreKeys)

Why it exists:

  • Enables offline messaging (someone can encrypt for you while you're offline)
  • Adds forward secrecy through rotation
  • Signature prevents impersonation

3. One-Time PreKeys (Single-use)

What it is:

  • A batch of temporary public keys (e.g., 100 keys)
  • Each used exactly once, then deleted
  • Like disposable phone numbers

Properties:

Type: Asymmetric (Curve25519)
Lifespan: Single use only
Shareable: Public YES, Private NEVER
Regenerate: YES, continuously replenished
Quantity: Generate 50-100 at a time

How to generate:

const oneTimePreKeys = [];
for (let i = 0; i < 100; i++) {
  const keyPair = await generateKeyPair();
  oneTimePreKeys.push({
    id: i,
    publicKey: keyPair.public,
    privateKey: keyPair.private
  });
}

// Upload public keys to server
await uploadPreKeys(oneTimePreKeys.map(k => ({
  id: k.id,
  publicKey: k.publicKey
})));

How to store:

  • Private: Encrypted locally with key IDs
  • Public: On server, server deletes after use

If user loses them:

  • ✅ Generate new batch
  • ⚠️ Pending messages using old keys may fail

Purpose:

  • Maximum forward secrecy
  • Each conversation starts with fresh key
  • Prevents mass decryption if one key compromised

4. Session Key (Per-conversation)

What it is:

  • Symmetric key derived from key exchange
  • Unique per conversation pair (Alice ↔ Bob)
  • Used for actual message encryption

Properties:

Type: Symmetric (AES-256)
Lifespan: Entire conversation session
Shareable: NEVER (derived independently by both parties)
Regenerate: Via Double Ratchet with each message
Derived from: Identity Key + PreKeys + Ephemeral Keys

How to derive (X3DH protocol):

// Alice's side (sender)
const sharedSecret = await deriveSharedSecret([
  DH(aliceIdentity.private, bobSignedPreKey.public),
  DH(aliceEphemeral.private, bobIdentity.public),
  DH(aliceEphemeral.private, bobSignedPreKey.public),
  DH(aliceEphemeral.private, bobOneTimePreKey.public)
]);

const sessionKey = await HKDF(sharedSecret, "session-key");

How to store:

  • In memory during active session
  • Can be encrypted and saved for offline mode
  • Cleared on logout

If user loses it:

  • ❌ Cannot decrypt messages in that session
  • ✅ Can re-establish with new key exchange
  • ⚠️ Past messages permanently undecryptable

Purpose:

  • Fast symmetric encryption of messages
  • Unique per conversation prevents cross-contamination

5. Message Keys (Per-message)

What it is:

  • Unique key for each individual message
  • Derived from Session Key using ratcheting
  • Ensures each message has different encryption

Properties:

Type: Symmetric (AES-256)
Lifespan: Single message only
Shareable: NEVER
Regenerate: Automatically with each message
Derived from: Chain Key (part of Double Ratchet)

How it works (Double Ratchet):

// Chain Key ratchets forward
let chainKey = initialChainKey;

function sendMessage(plaintext) {
  // Derive message key from chain key
  const messageKey = HMAC(chainKey, "message-key");
  
  // Ratchet chain key forward
  chainKey = HMAC(chainKey, "chain-ratchet");
  
  // Encrypt message
  const ciphertext = AES_Encrypt(plaintext, messageKey);
  
  // Delete message key immediately
  messageKey.clear();
  
  return ciphertext;
}

How to store:

  • ❌ NEVER stored (derived on-demand)
  • Used immediately and discarded
  • Old message keys cannot be regenerated

If user loses it:

  • Not applicable (ephemeral by design)
  • Old messages remain encrypted forever

Purpose:

  • Forward secrecy: Past messages safe even if current key compromised
  • Each message independently secure

6. Group Master Key (For group chats)

What it is:

  • Shared symmetric key for entire group
  • All members have the same key
  • Used to derive sender chains

Properties:

Type: Symmetric (AES-256)
Lifespan: Until member added/removed
Shareable: With group members ONLY (encrypted individually)
Regenerate: YES, when membership changes
Distribution: Encrypted separately for each member

How to generate:

// Admin creates group
const groupMasterKey = await crypto.subtle.generateKey(
  {
    name: "AES-GCM",
    length: 256
  },
  true,
  ["encrypt", "decrypt"]
);

// Encrypt for each member
for (const member of members) {
  const memberPublicKey = await fetchPreKey(member.id);
  const encryptedKey = await encryptForMember(
    groupMasterKey, 
    memberPublicKey
  );
  await sendToMember(member.id, encryptedKey);
}

How to store:

  • Encrypted locally with user's keys
  • Each member stores their own copy
  • Server NEVER has it

If user loses it:

  • ❌ Cannot decrypt group messages
  • ✅ Admin can re-send encrypted copy
  • ⚠️ Security issue if lost to attacker

Why regenerate for new members:

This is critical for security! Here's why:

Scenario WITHOUT key rotation:
1. Alice, Bob, Carol in group with Key_v1
2. Carol leaves group
3. David joins, receives Key_v1
4. Problem: David can decrypt ALL old messages from when Carol was member
   - Violates privacy of Carol's past messages
   - Carol expected her messages private after leaving

Scenario WITH key rotation:
1. Alice, Bob, Carol in group with Key_v1
2. Carol leaves group
3. Generate NEW Key_v2
4. Alice and Bob receive Key_v2
5. David joins, receives Key_v2
6. ✓ David cannot decrypt old messages (needs Key_v1)
7. ✓ Carol cannot decrypt new messages (needs Key_v2)

Purpose:

  • Efficient group messaging (one encryption, many recipients)
  • Access control through key rotation
  • Forward and backward secrecy

7. Sender Chain Key (In groups)

What it is:

  • Each group member has their own chain
  • Derived from Group Master Key
  • Used to encrypt their messages to the group

Properties:

Type: Symmetric chain
Lifespan: Until group key rotates
Shareable: NEVER (but all members derive same chains)
Regenerate: With each message (ratchets forward)
Unique per: Each sender in the group

How it works:

// Alice derives her sender chain from group key
const aliceSenderChain = HKDF(
  groupMasterKey, 
  "sender-chain-" + aliceId
);

// Alice sends message
let aliceChainKey = aliceSenderChain;
const messageKey = HMAC(aliceChainKey, "message");
aliceChainKey = HMAC(aliceChainKey, "ratchet");

const encrypted = AES_Encrypt(message, messageKey);
await sendToGroup({
  senderId: aliceId,
  counter: aliceMessageCounter++,
  ciphertext: encrypted
});

// Bob derives Alice's chain and decrypts
const aliceSenderChainBobSide = HKDF(
  groupMasterKey,
  "sender-chain-" + aliceId
);
// Bob ratchets to correct counter
const bobsViewOfMessageKey = ratchetToCounter(
  aliceSenderChainBobSide,
  aliceMessageCounter
);
const decrypted = AES_Decrypt(encrypted, bobsViewOfMessageKey);

How to store:

  • Current chain state encrypted locally
  • Counter tracked per sender
  • Cleared when group key rotates

If user loses it:

  • ⚠️ Cannot decrypt messages from that sender
  • ✅ Can request sender to re-send from server
  • ✅ Group admin can rotate key and fix

Purpose:

  • Each member's messages are independently verifiable
  • Prevents impersonation within group
  • Efficient: derive keys locally without communication

8. Ephemeral Keys (Temporary)

What it is:

  • Short-lived keys generated for single session
  • Discarded immediately after use
  • Adds extra layer of forward secrecy

Properties:

Type: Asymmetric (Curve25519)
Lifespan: Single key exchange
Shareable: Public key only
Regenerate: Every new session

How to generate:

// Generate fresh ephemeral key for each conversation
const ephemeralKey = await crypto.subtle.generateKey(
  { name: "ECDH", namedCurve: "P-256" },
  false,
  ["deriveKey"]
);

// Use in key exchange
const sharedSecret = await deriveSecret(
  ephemeralKey.private,
  recipientPublicKey
);

// Delete immediately after deriving session key
ephemeralKey.private = null;

How to store:

  • ❌ NOT stored, created and destroyed
  • Only used for initial handshake

If user loses it:

  • Not applicable (ephemeral by design)

Purpose:

  • Even if long-term keys compromised, past sessions remain secure
  • Adds perfect forward secrecy

🔄 PROTOCOLS & ALGORITHMS

X3DH (Extended Triple Diffie-Hellman)

What it is:

  • Initial key agreement protocol
  • Establishes shared secret between two parties
  • Works even when recipient is offline

How it works:

Alice wants to message Bob (who is offline):

1. Alice fetches from server:
   - Bob's Identity Key (IK_B)
   - Bob's Signed PreKey (SPK_B)
   - Bob's One-Time PreKey (OPK_B) [if available]

2. Alice generates ephemeral key (EK_A)

3. Alice performs 3 or 4 Diffie-Hellman operations:
   DH1 = DH(IK_A, SPK_B)
   DH2 = DH(EK_A, IK_B)
   DH3 = DH(EK_A, SPK_B)
   DH4 = DH(EK_A, OPK_B) [if OPK available]

4. Combine all DHs:
   Shared_Secret = KDF(DH1 || DH2 || DH3 || DH4)

5. Derive Session Key from Shared_Secret

Why 3-4 exchanges:

  • More DH operations = more security
  • Even if one key type compromised, others protect secret
  • Provides multiple layers of authentication

Properties:

  • ✅ Works offline (asynchronous)
  • ✅ Mutual authentication
  • ✅ Forward secrecy through OPK
  • ✅ Deniability (no long-term signatures)

Double Ratchet Algorithm

What it is:

  • Continuously updates encryption keys during conversation
  • Two ratchets: Diffie-Hellman ratchet + Symmetric ratchet
  • Used by Signal, WhatsApp, etc.

How it works:

Symmetric Ratchet (Message Chain):
├─ Chain Key 0
│  ├─ Message Key 0 → Encrypt Message 0
│  └─ Chain Key 1
│     ├─ Message Key 1 → Encrypt Message 1
│     └─ Chain Key 2
│        └─ ...

Diffie-Hellman Ratchet (Session Updates):
Each time you send/receive, generate new DH key pair
├─ Root Key
│  ├─ DH with recipient's new public key
│  └─ New Root Key + New Chain Key

Example flow:

// Initial state
let rootKey = initialRootKey;
let sendChainKey = initialSendChain;
let receiveChainKey = initialReceiveChain;

// Send message
function send(plaintext) {
  // Symmetric ratchet forward
  const messageKey = HMAC(sendChainKey, "message");
  sendChainKey = HMAC(sendChainKey, "chain");
  
  return AES_Encrypt(plaintext, messageKey);
}

// Receive message triggers DH ratchet
function receive(ciphertext, theirNewPublicKey) {
  // DH ratchet: create new ephemeral key
  const myNewKeyPair = generateKeyPair();
  
  // Derive new root and chain keys
  const dhOutput = DH(myNewKeyPair.private, theirNewPublicKey);
  [rootKey, receiveChainKey] = KDF(rootKey, dhOutput);
  
  // Symmetric ratchet to decrypt
  const messageKey = HMAC(receiveChainKey, "message");
  receiveChainKey = HMAC(receiveChainKey, "chain");
  
  return AES_Decrypt(ciphertext, messageKey);
}

Properties:

  • ✅ Forward secrecy: Past keys can't be derived
  • ✅ Post-compromise security: Future keys safe even if current compromised
  • ✅ Out-of-order message handling
  • ✅ Self-healing: Recovers from key compromise

Sender Keys Protocol (Groups)

What it is:

  • Efficient group messaging encryption
  • Each sender broadcasts encrypted message once
  • All recipients derive decryption key

How it works:

Traditional (Pairwise) - INEFFICIENT:
Alice encrypts separately for each member:
├─ Encrypt for Bob
├─ Encrypt for Carol  
├─ Encrypt for David
└─ Encrypt for Eve
Total: 4 encryptions, 4 transmissions

Sender Keys - EFFICIENT:
Alice encrypts once:
├─ Derive Message Key from Alice's Sender Chain
├─ Encrypt message once
└─ Broadcast to all
Total: 1 encryption, 1 transmission

All members derive Alice's chain from Group Master Key

Implementation:

// Group setup: Everyone has groupMasterKey

// Alice creates her sender chain
const aliceSenderSeed = HKDF(groupMasterKey, "alice-sender");
let aliceChainKey = aliceSenderSeed;

// Alice sends message #5
function aliceSendMessage5(plaintext) {
  // Ratchet to message 5
  for (let i = 0; i < 5; i++) {
    aliceChainKey = HMAC(aliceChainKey, "ratchet");
  }
  
  const messageKey5 = HMAC(aliceChainKey, "message-5");
  const ciphertext = AES_Encrypt(plaintext, messageKey5);
  
  return {
    senderId: "alice",
    counter: 5,
    ciphertext: ciphertext
  };
}

// Bob receives and decrypts
function bobReceiveAliceMessage(message) {
  // Derive Alice's chain
  const aliceSenderSeed = HKDF(groupMasterKey, "alice-sender");
  let aliceChainKey = aliceSenderSeed;
  
  // Ratchet to message counter
  for (let i = 0; i < message.counter; i++) {
    aliceChainKey = HMAC(aliceChainKey, "ratchet");
  }
  
  const messageKey = HMAC(aliceChainKey, "message-" + message.counter);
  return AES_Decrypt(message.ciphertext, messageKey);
}

Properties:

  • ✅ Very efficient for large groups
  • ✅ Each sender authenticated
  • ✅ Works with Double Ratchet per sender
  • ⚠️ Requires key rotation when membership changes

🛡️ SECURITY PROPERTIES

Forward Secrecy

What it means:

  • Compromise of current keys doesn't expose past messages
  • Past session keys cannot be derived from current keys

How achieved:

Message Timeline:
├─ Message 1: Key_1 (deleted after use)
├─ Message 2: Key_2 (deleted after use)
├─ Message 3: Key_3 (deleted after use)
└─ Message 4: Key_4 (current)

If attacker gets Key_4:
✓ Can decrypt Message 4
✗ CANNOT decrypt Messages 1, 2, 3

Implementation:

  • One-time PreKeys (deleted after use)
  • Ephemeral keys (never stored)
  • Ratcheting (cannot reverse)
  • Immediate key deletion

Post-Compromise Security (Backward Secrecy)

What it means:

  • Even if attacker compromises current key, future messages become secure again
  • System "heals" from compromise

How achieved:

Time →
├─ Normal: Secure
├─ Attacker steals Key_5
├─ Compromise: Messages 5-7 exposed
├─ DH Ratchet: New ephemeral key generated
└─ Recovered: Messages 8+ secure again

Implementation:

  • Double Ratchet DH updates
  • Periodic key rotation
  • New ephemeral keys with each response

Deniability (Plausible Deniability)

What it means:

  • Cannot cryptographically prove who sent a message
  • Messages authenticated during session, but not provable later
  • Unlike signatures which create permanent proof

How achieved:

Traditional Signatures (NOT deniable):
Message + Digital Signature
└─ Anyone can verify signature forever
└─ Proves Alice sent message (like written signature)

MAC Authentication (Deniable):
Message + MAC(SharedKey, Message)
├─ Only recipient can verify (has SharedKey)
├─ Recipient could have created MAC themselves
└─ No proof to third party that Alice sent it

Why it matters:

  • Protects privacy
  • Messages can't be used as evidence
  • Like face-to-face conversation (you know who said it, but can't prove to others)

Key Rotation Scenarios

When to rotate keys:

  1. Regular Schedule:
Every 7 days: Rotate Signed PreKey
Every 30 days: Replenish One-Time PreKeys
Every 100 messages: DH ratchet in session
  1. Group Member Added:
Before: Group has Key_v1
│
├─ New member joins
│
Generate Key_v2
├─ Old members receive Key_v2
├─ New member receives Key_v2
│
After: Old member CANNOT access future messages
       New member CANNOT access past messages
  1. Group Member Removed:
Before: Group has Key_v3
│
├─ Member leaves/removed
│
Generate Key_v4
├─ Remaining members receive Key_v4
├─ Removed member keeps Key_v3
│
After: Removed member CANNOT decrypt new messages
  1. Suspected Compromise:
If device stolen/hacked:
├─ Generate new Identity Key from new device
├─ Upload new PreKeys
├─ Server marks old keys as revoked
└─ Contacts get new keys on next message

💾 STORAGE BEST PRACTICES

Key Storage Table:

Key Type Storage Location Encrypted? Extractable? Backup?
Identity Private Key IndexedDB or Web Crypto YES (user password) NO YES (encrypted cloud)
Identity Public Key Server NO YES Not needed
Signed PreKey Private IndexedDB YES NO YES
One-Time PreKeys Private IndexedDB YES NO Optional
Session Keys Memory / IndexedDB YES NO Optional
Message Keys NEVER STORED N/A NO NO
Group Master Key IndexedDB YES NO Via group admin
Sender Chain Keys IndexedDB YES NO Derived from group key

📱 WHAT HAPPENS IF USER LOSES...

Loses Phone (but has backup):

✓ Install app on new device
✓ Enter backup password/recovery phrase
✓ Download encrypted key backup from cloud
✓ Decrypt keys locally
✓ Register new device
✓ Continue with same identity

Loses Phone (no backup):

✗ Cannot decrypt old messages
✓ Create new identity
✓ Re-add contacts
✓ Start fresh conversations
⚠ Contacts see "new identity" warning

Loses Group Master Key:

✓ Request from admin (encrypted)
✓ Admin sends encrypted copy
⚠ Cannot decrypt messages sent while key was lost

Loses Identity Key:

✗ Cannot prove identity
✗ Cannot decrypt pending messages
✓ Generate new identity
✓ Mark old identity as compromised
⚠ Must re-establish trust with contacts

🎯 KEY TAKEAWAYS

Essential Rules:

  1. NEVER store private keys unencrypted
  2. ALWAYS use non-extractable Web Crypto keys when possible
  3. ALWAYS rotate keys when members join/leave groups
  4. DELETE message keys immediately after use
  5. BACKUP identity keys with strong encryption
  6. REGENERATE PreKeys regularly (weekly/monthly)
  7. NEVER share Group Master Key unencrypted

Security Hierarchy (strongest first):

1. Ephemeral Keys (single use, never stored)
2. Message Keys (single use, immediately deleted)
3. One-Time PreKeys (single use, can be pre-generated)
4. Session Keys (per conversation, ratcheted)
5. Sender Chain Keys (per sender in group, ratcheted)
6. Group Master Key (shared, rotated on membership change)
7. Signed PreKey (rotated weekly/monthly)
8. Identity Key (permanent, highest protection)

Remember:

  • More key types = More security layers
  • Key rotation = Forward & backward secrecy
  • Ephemeral keys = Perfect forward secrecy
  • No key storage = Cannot be stolen
  • Backup = Recovery vs Security tradeoff

This architecture ensures that even if one layer is compromised, the others provide protection!

🔑 Clarifying: Identity Key vs PreKey vs Public Key

The Confusion Explained

The problem is that ALL of these are actually "public-private key pairs", but they serve different roles:

🤔 CONFUSING TERMINOLOGY:
- "Identity Key" → Actually means "Identity Key PAIR" (has public + private)
- "PreKey" → Actually means "PreKey PAIR" (has public + private)
- "Public Key" → Generic term for the public half of any key pair

They're ALL asymmetric pairs!

Let me break it down clearly:

1. Identity Key (Pair)

Identity Key = {
  Public Part: Your permanent public identity (like your username)
  Private Part: Your permanent secret (like your password, but stronger)
}

Think of it as:

  • Your passport in the cryptographic world
  • Your permanent digital fingerprint
  • It says "This is really YOU"

Concrete example:

// Generate Identity Key PAIR
const identityKeyPair = await crypto.subtle.generateKey(
  { name: "ECDSA", namedCurve: "P-256" },
  false,
  ["sign", "verify"]
);

// identityKeyPair contains:
identityKeyPair = {
  publicKey: "04a3f2c8d9..." // Everyone can see this - it's YOUR identity
  privateKey: "5e7c2b9a..." // ONLY YOU have this - proves YOU control the identity
}

// Upload to server
await uploadToServer({
  userId: "alice@example.com",
  identityPublicKey: identityKeyPair.publicKey // <-- This is YOUR public identity
});

What you do with it:

  • Private part: Use to sign other keys (prove they belong to you)
  • Public part: Others use to verify your signatures

2. Signed PreKey (Pair)

Signed PreKey = {
  Public Part: Temporary public key (changes weekly/monthly)
  Private Part: Temporary secret key
  + Signature: Proves this PreKey belongs to the Identity Key owner
}

Think of it as:

  • A temporary phone number
  • You change it periodically for privacy
  • But you "sign" it with your passport to prove it's really yours

Why "Signed"? Because someone could upload a fake PreKey pretending to be you:

// ATTACK WITHOUT SIGNATURE:
Attacker uploads fake PreKey:
{
  userId: "alice@example.com",
  preKey: attackerPreKey.publicKey  // <-- Attacker's key!
}
// When Bob encrypts for Alice, attacker can decrypt!

// PROTECTED WITH SIGNATURE:
Alice uploads signed PreKey:
{
  userId: "alice@example.com",
  preKey: alicePreKey.publicKey,
  signature: sign(alicePreKey.publicKey, aliceIdentityKey.privateKey)
}
// Signature proves: "This PreKey was created by the owner of Identity Key"
// Bob verifies signature with Alice's Identity Public Key
// If attacker tries to fake it, signature verification fails!

Concrete example:

// Step 1: Generate a PreKey PAIR (temporary)
const preKeyPair = await crypto.subtle.generateKey(
  { name: "ECDH", namedCurve: "P-256" },
  false,
  ["deriveKey"]
);

preKeyPair = {
  publicKey: "042b8f3e1c...",  // Temporary public key
  privateKey: "7d4a9c2f..."    // Temporary private key
}

// Step 2: SIGN the PreKey with your Identity Key
const signature = await crypto.subtle.sign(
  { name: "ECDSA", hash: "SHA-256" },
  identityKeyPair.privateKey,  // <-- Using Identity Private Key to sign
  preKeyPair.publicKey          // <-- Signing the PreKey Public Key
);

// Step 3: Upload to server
await uploadToServer({
  userId: "alice@example.com",
  signedPreKey: {
    id: 42,
    publicKey: preKeyPair.publicKey,  // Others will encrypt with this
    signature: signature               // Proves it's really from Alice
  }
});

// Step 4: Bob verifies before using
const aliceIdentityPublicKey = await fetchIdentityKey("alice@example.com");
const isValid = await crypto.subtle.verify(
  { name: "ECDSA", hash: "SHA-256" },
  aliceIdentityPublicKey,        // <-- Alice's Identity Public Key
  signature,                      // <-- The signature to verify
  preKeyPair.publicKey           // <-- The PreKey being verified
);

if (isValid) {
  // Signature is valid! This PreKey really belongs to Alice
  // Safe to use it to encrypt
} else {
  // ATTACK DETECTED! This PreKey was not signed by Alice's Identity Key
}

3. "Public Key" (Generic Term)

"Public Key" is just a generic term that can refer to the public part of ANY key pair:

"Public Key" could mean:
- Identity Public Key
- PreKey Public Key  
- One-Time PreKey Public Key
- Ephemeral Public Key
- Sender Chain Public Key
- Any public half of any asymmetric pair

In the phrase "Signed PreKey", which public key is signed?

Let me break down the phrase step by step:

"public key signed by your Identity Key"
    ↓           ↓              ↓
  Which      Action         Which
  public                    Identity
  key?                      Key part?

ANSWER:
"PreKey's PUBLIC PART signed by your Identity Key's PRIVATE PART"

Visual Breakdown: The Signature Process

ALICE'S KEYS:
┌─────────────────────────────────────────────────────┐
│ Identity Key (Permanent)                            │
│ ├─ Identity Public Key:  "04a3f2c8..."  (Everyone) │
│ └─ Identity Private Key: "5e7c2b9a..."  (Alice)    │
└─────────────────────────────────────────────────────┘
                    │
                    │ Alice uses her Identity PRIVATE key to sign ↓
                    ↓
┌─────────────────────────────────────────────────────┐
│ Signed PreKey (Temporary, rotates weekly)           │
│ ├─ PreKey Public Key:  "042b8f3e..."    (Everyone) │
│ ├─ PreKey Private Key: "7d4a9c2f..."    (Alice)    │
│ └─ Signature: "8c5d3a1b..."                        │
│    └─ Created by: Sign(PreKey Public, Identity Private) │
└─────────────────────────────────────────────────────┘
                    │
                    │ Uploaded to server
                    ↓
┌─────────────────────────────────────────────────────┐
│ Server stores (publicly accessible):                │
│ ├─ Alice's Identity Public Key                     │
│ ├─ Alice's PreKey Public Key                       │
│ └─ Signature linking them                          │
└─────────────────────────────────────────────────────┘
                    │
                    │ Bob fetches Alice's keys
                    ↓
┌─────────────────────────────────────────────────────┐
│ BOB VERIFIES:                                       │
│ 1. Get Alice's Identity Public Key (trusted)       │
│ 2. Get Alice's PreKey Public Key                   │
│ 3. Get Signature                                    │
│ 4. Verify: Is signature valid?                     │
│    → Verify(PreKey Public, Signature, Identity Public) │
│ 5. If valid: This PreKey really belongs to Alice   │
│ 6. Bob can safely encrypt message for Alice        │
└─────────────────────────────────────────────────────┘

Real-World Analogy

Think of it like this:

Identity Key = Your Passport

Passport:
- Name: Alice
- Photo: [Your face]
- Signature: [Government's seal]
- Valid: Permanently (until expired)

Identity Public Key:
- Shows who you are
- Everyone can see it
- Proves your identity

Identity Private Key:
- Only you possess it
- Use it to "sign" other documents
- Like your ability to sign checks

Signed PreKey = Your Temporary Hotel Keycard

Hotel Keycard:
- Room number
- Valid: This week only
- Signed by hotel (proves it's legitimate)

PreKey Public:
- Temporary identifier
- Changes weekly for security
- Others encrypt messages with it

PreKey Private:
- You use it to decrypt
- Discarded after a week

Signature:
- Hotel's signature proving the keycard is legitimate
- In crypto: Your Identity Key's signature proving PreKey is yours

Why Sign the PreKey?

Without signature:
- Anyone could claim "This is Alice's PreKey"
- You might encrypt for wrong person
- Like anyone printing a fake hotel keycard

With signature:
- PreKey is signed by Identity Key
- Verifiable that it belongs to Alice
- Like hotel's signature on the keycard

Complete Example: From Generation to Usage

// ===================================
// ALICE SETS UP HER KEYS
// ===================================

// 1. Generate IDENTITY KEY PAIR (once, permanent)
const aliceIdentityKeyPair = await crypto.subtle.generateKey(
  { name: "ECDSA", namedCurve: "P-256" },
  false,
  ["sign", "verify"]
);

console.log("Alice's Identity Keys:");
console.log("  Public (everyone sees):", aliceIdentityKeyPair.publicKey);
console.log("  Private (Alice only):", aliceIdentityKeyPair.privateKey);

// 2. Generate PREKEY PAIR (weekly rotation)
const alicePreKeyPair = await crypto.subtle.generateKey(
  { name: "ECDH", namedCurve: "P-256" },
  false,
  ["deriveKey"]
);

console.log("Alice's PreKey Pair:");
console.log("  Public (everyone sees):", alicePreKeyPair.publicKey);
console.log("  Private (Alice only):", alicePreKeyPair.privateKey);

// 3. SIGN the PreKey's public part with Identity's private part
const signature = await crypto.subtle.sign(
  { name: "ECDSA", hash: "SHA-256" },
  aliceIdentityKeyPair.privateKey,  // ← Identity PRIVATE key (proves it's Alice)
  alicePreKeyPair.publicKey         // ← PreKey PUBLIC key (what we're signing)
);

console.log("Signature:", signature);
console.log("This signature proves: 'The PreKey belongs to owner of Identity Key'");

// 4. Upload to server
await server.upload({
  userId: "alice",
  identityPublicKey: aliceIdentityKeyPair.publicKey,
  signedPreKey: {
    id: 1,
    publicKey: alicePreKeyPair.publicKey,
    signature: signature
  }
});

// ===================================
// BOB WANTS TO MESSAGE ALICE
// ===================================

// 1. Fetch Alice's keys from server
const aliceKeys = await server.fetchKeys("alice");

console.log("Bob fetched:");
console.log("  Alice's Identity Public Key:", aliceKeys.identityPublicKey);
console.log("  Alice's PreKey Public Key:", aliceKeys.signedPreKey.publicKey);
console.log("  Signature:", aliceKeys.signedPreKey.signature);

// 2. VERIFY the signature (critical security step!)
const isValidSignature = await crypto.subtle.verify(
  { name: "ECDSA", hash: "SHA-256" },
  aliceKeys.identityPublicKey,          // ← Alice's Identity PUBLIC key
  aliceKeys.signedPreKey.signature,     // ← The signature to check
  aliceKeys.signedPreKey.publicKey      // ← The PreKey PUBLIC key being verified
);

if (!isValidSignature) {
  throw new Error("ATTACK DETECTED! PreKey signature is invalid!");
}

console.log("✓ Signature verified! This PreKey really belongs to Alice");

// 3. Now Bob can safely use Alice's PreKey to encrypt
const bobEphemeralKeyPair = await crypto.subtle.generateKey(
  { name: "ECDH", namedCurve: "P-256" },
  false,
  ["deriveKey"]
);

// Perform key exchange with Alice's PreKey
const sharedSecret = await crypto.subtle.deriveKey(
  { name: "ECDH", public: aliceKeys.signedPreKey.publicKey },
  bobEphemeralKeyPair.privateKey,
  { name: "AES-GCM", length: 256 },
  false,
  ["encrypt"]
);

// Encrypt message
const encryptedMessage = await crypto.subtle.encrypt(
  { name: "AES-GCM", iv: randomIV },
  sharedSecret,
  new TextEncoder().encode("Hello Alice!")
);

// ===================================
// ALICE RECEIVES AND DECRYPTS
// ===================================

// Alice uses her PreKey PRIVATE key to decrypt
const aliceSharedSecret = await crypto.subtle.deriveKey(
  { name: "ECDH", public: bobEphemeralKeyPair.publicKey },
  alicePreKeyPair.privateKey,  // ← Using PreKey PRIVATE part
  { name: "AES-GCM", length: 256 },
  false,
  ["decrypt"]
);

const decryptedMessage = await crypto.subtle.decrypt(
  { name: "AES-GCM", iv: randomIV },
  aliceSharedSecret,
  encryptedMessage
);

console.log("Alice decrypted:", new TextDecoder().decode(decryptedMessage));
// Output: "Hello Alice!"

Summary: Which Key Does What?

Key Type Public Part Private Part Signature
Identity Key Your permanent public identity
Everyone can see
Used to verify signatures
Your permanent secret
ONLY YOU have it
Used to create signatures
N/A (not signed by anything)
Signed PreKey Temporary public key
Everyone can see
Used for key exchange
Temporary secret key
ONLY YOU have it
Used to derive shared secrets
Created by signing PreKey's public part with Identity's private part

The Signing Relationship:

Identity Private Key 
      ↓ (signs)
PreKey Public Key
      ↓ (creates)
Signature
      ↓ (verified by)
Identity Public Key

In plain English:

  • "I (Alice) am using my Identity Private Key to sign the PreKey Public Key"
  • "This creates a signature that proves the PreKey belongs to me"
  • "Others verify this signature using my Identity Public Key"
  • "If signature is valid, they know the PreKey really belongs to Alice"

The key insight is: they're all key pairs, but the Identity Key signs the PreKey to prove ownership!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment