Skip to content

Instantly share code, notes, and snippets.

@nichoth
Created November 11, 2025 04:04
Show Gist options
  • Select an option

  • Save nichoth/ea858f33a110e6e0c64b9962f16b999d to your computer and use it in GitHub Desktop.

Select an option

Save nichoth/ea858f33a110e6e0c64b9962f16b999d to your computer and use it in GitHub Desktop.
Perform Diffie-Hellman Key Exchange with the web crypto API (in a browser)
// Generate X25519 key pairs
async function generateX25519KeyPair(): Promise<CryptoKeyPair> {
return await crypto.subtle.generateKey(
{
name: "X25519"
},
true, // extractable
["deriveKey", "deriveBits"]
);
}
// Derive a shared secret using X25519
async function deriveSharedSecret(
privateKey: CryptoKey,
publicKey: CryptoKey
): Promise<CryptoKey> {
return await crypto.subtle.deriveKey(
{
name: "X25519",
public: publicKey
},
privateKey,
{
name: "AES-GCM",
length: 256
},
true, // extractable
["encrypt", "decrypt"]
);
}
// Complete X25519 example
async function x25519Example(): Promise<{
aliceKeyPair: CryptoKeyPair;
bobKeyPair: CryptoKeyPair;
aliceSharedSecret: CryptoKey;
bobSharedSecret: CryptoKey;
}> {
// Alice generates her X25519 key pair
const aliceKeyPair = await generateX25519KeyPair();
// Bob generates his X25519 key pair
const bobKeyPair = await generateX25519KeyPair();
// Alice derives shared secret using her private key and Bob's public key
const aliceSharedSecret = await deriveSharedSecret(
aliceKeyPair.privateKey,
bobKeyPair.publicKey
);
// Bob derives shared secret using his private key and Alice's public key
const bobSharedSecret = await deriveSharedSecret(
bobKeyPair.privateKey,
aliceKeyPair.publicKey
);
// Export keys to verify they match
const aliceSecret = await crypto.subtle.exportKey("raw", aliceSharedSecret);
const bobSecret = await crypto.subtle.exportKey("raw", bobSharedSecret);
console.log("Alice's shared secret:", new Uint8Array(aliceSecret));
console.log("Bob's shared secret:", new Uint8Array(bobSecret));
console.log("Secrets match:",
new Uint8Array(aliceSecret).toString() === new Uint8Array(bobSecret).toString()
);
return { aliceKeyPair, bobKeyPair, aliceSharedSecret, bobSharedSecret };
}
// Run the example
x25519Example();
@nichoth
Copy link
Author

nichoth commented Nov 12, 2025

double ratchet

The Signal protocol

The Two Ratchets

1. The DHKE

On each new message, the message contains a new public key. You use your private key with the given public key to derive a new secret.

2. Symmetric-key Ratchet (KDF Chain)

After each message, both sides advance their sending or receiving chain using a key derivation function (KDF).

Each message gets its own unique symmetric encryption key (message_key_n), which is deleted after use.

  1. When a new DH public key is received -> perform a DH Ratchet step -> derive a new root key and new chain keys.
  2. For every message sent or received -> perform a Symmetric Ratchet step -> derive a new message key.

Each message is encrypted with a specific symmetric key. The message key comes from a hierarchy of derived keys. The root key and chain keys are intermediate secrets used to securely derive that per-message key.


three key levels in the Double Ratchet

  1. root key — derived from DH shared secret + previous root key — used to derive new chain keys
  2. chain key — derived from the root key — used to derive message keys (per direction)
  3. message key — derived from chain key — used to encrypt/decrypt one message — used once, then deleted

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