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
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
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
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
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
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
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
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
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)
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
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
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
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
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)
When to rotate keys:
- Regular Schedule:
Every 7 days: Rotate Signed PreKey
Every 30 days: Replenish One-Time PreKeys
Every 100 messages: DH ratchet in session
- 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
- 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
- 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
| 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 |
✓ 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
✗ Cannot decrypt old messages
✓ Create new identity
✓ Re-add contacts
✓ Start fresh conversations
⚠ Contacts see "new identity" warning
✓ Request from admin (encrypted)
✓ Admin sends encrypted copy
⚠ Cannot decrypt messages sent while key was lost
✗ Cannot prove identity
✗ Cannot decrypt pending messages
✓ Generate new identity
✓ Mark old identity as compromised
⚠ Must re-establish trust with contacts
Essential Rules:
- NEVER store private keys unencrypted
- ALWAYS use non-extractable Web Crypto keys when possible
- ALWAYS rotate keys when members join/leave groups
- DELETE message keys immediately after use
- BACKUP identity keys with strong encryption
- REGENERATE PreKeys regularly (weekly/monthly)
- 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!
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!
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
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
}"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"
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 │
└─────────────────────────────────────────────────────┘
Think of it like this:
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
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
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
// ===================================
// 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!"| 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 |
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!