This demo shows a rough implementation of the Signal Protocol's X3DH key exchange algorithm using only the WebCrypto APIs that are native to modern web browsers. This is not intended to be a secure X3DH implementation and is for demonstration purposes only.
Alice and Bob each begin by creating a "prekey bundle", which contains their long-term identity key, a signed prekey, and a set of one-time-use prekeys. The public halves of each of these keys are shared with the server and with other users.
Now that Alice has proven that Bob signed the signed prekey with his private identity key, she can safely begin the key exchange process using X3DH:
ek
: Alice generates an ephemeral ECDH key which will only
be used for this session. This acts kind of like an initialization
vector, ensuring that several of the operations in the key exchange
will have unpredictable outputs. (<aliceEphemeral>
)
d1
: Alice derives a shared Diffie-Hellman key using her
private identity key and Bob's public signed prekey (DH(<aliceIdentity>, <bobSignedPrekey>)
)
d2
: Alice derives a shared Diffie-Hellman key using her
private ephemeral key and Bob's public identity key (DH(<aliceEphemeral>, <bobIdentity>)
)
d3
: Alice derives a shared Diffie-Hellman key using her
private ephemeral key and Bob's public signed prekey. (DH(<aliceEphemeral>, <bobSignedPrekey>)
)
d4
: Alice derives a shared Diffie-Hellman key using her
private ephemeral key and Bob's public one-time prekey. (DH(<aliceEphemeral>, <bobOneTimePrekey>)
)
sk
: Alice generates an AES-256 symmetric encryption key
by concatenating all four Diffie-Hellman results and passing them to a
Hash-based key derivation function (HKDF).
This is the symmetric key that will be used to encrypt the first
message.
Alice can now encrypt her initial message and send it to Bob, along with her identity key, her ephemeral key, one of Bob's one-time prekeys, and the ciphertext itself. The ciphertext includes an “associated data” header containing both Alice's identity key and Bob's identity key.
Bob receives this message, and now begins the process of decrypting it to see its contents. (Presumably, he could also look up the profile identified by Alice's identity key and decide whether he wants to accept the message in the first place)
Bob repeats the same set of Diffie-Hellman calculations as Alice, using a combination of the public keys that Alice sent in her message and the private keys of his own. He can then generate the same symmetric key that Alice did, and decrypt her message.
If you scroll back up to where Alice calculated her X3DH keys, you’ll notice that all of Bob’s calculated Diffie-Hellman keys and symmetric key are identical to the ones that Alice calculated before. This is despite Alice not having access to any of Bob's private keys, and despite Bob not having access to any of Alice's private keys. That's the magic of key exchange!
Bob now uses his calculated symmetric key to decrypt Alice's message. Bob authenticates the message’s encryptino by validating that the “associated data” that Alice used when encrypting the message matches Alice and Bob's identity keys.
All of the work above only served to begin a session between Alice and Bob. It authenticated them with each other and provided a random initial key for their future conversations.
However, the symmetric key generated in this example will never be used again! Instead, Alice and Bob will each feed that key into the Double Ratchet Algorithm to generate new encryption keys for each message they send going forward. This will ensure that even if an attacker breaks the key for one of their messages, that attacker won’t have access to any of their other messages, either before or after the broken message.