Private keys
The sender's private keys, unlocked in their browser by half of their password.
The password is split before it leaves the device. One half signs in; the other opens a vault of private keys that the server stores and can never open. A 24-word phrase can rebuild every key the account will ever use.
Code frontend/src/lib/crypto/vault.js frontend/src/lib/crypto/password.js frontend/src/lib/keystore.js
More about this in the story →
Lock and sign
Encrypts each message with a fresh key, links it to the one before, and signs the whole envelope.
The message key is sealed twice, to the recipient and to the sender. The envelope carries its number in the conversation and the hash of the message before it, and the signature covers both, so the order is part of what was signed.
Code frontend/src/lib/crypto/envelope.js frontend/src/lib/messaging.js
More about this in the story →
Relay
Takes signed envelopes over a WebSocket and delivers them to whoever is in the conversation.
Socket.IO, with Redis between instances so a message sent to one instance reaches a recipient connected to another. It sees who talks to whom and when. It never sees what was said.
Code backend/src/realtime.js backend/src/auth.js
Chain keeper
Accepts a message only if it is properly signed and links onto the newest message in its conversation.
When two people send at the same moment only one can be next. The other is answered with a conflict and the message it missed, and its browser links onto that, signs again and resends without the person doing anything.
Code backend/src/services/chain.js
More about this in the story →
Stored messages
Locked text, sealed keys, signatures and links, in MongoDB. Nothing here opens without a private key from a browser.
Files are stored the same way, encrypted in the browser and named by the hash of their encrypted bytes. A deleted message leaves its place in the chain and loses its content.
Code backend/src/models/message.js backend/src/services/files.js
More about this in the story →
Key log
A public, append-only log of everyone's public keys, the same design as Certificate Transparency.
The server signs the head of the log and anchors it in Bitcoin. If it showed two people different keys for the same person, the proofs they each hold would not fit together.
Code backend/src/services/log.js backend/src/services/merkle.js backend/src/services/anchoring.js backend/src/models/logLeaf.js
More about this in the story →
Check and open
The recipient's browser checks the signature and the link to the previous message before it decrypts anything.
The server checks too, but that check is a courtesy. This one is the control: a forged, edited, reordered or missing message shows up here as a broken link, and the conversation says so.
Code frontend/src/lib/messaging.js frontend/src/lib/crypto/envelope.js
More about this in the story →
Whose key is this?
Checks that the key which signed a message really is the sender's, without taking the server's word for it.
Every change of key is signed by the key before it, back to the first. The browser checks that history, checks the key log vouches for it, and remembers the keys of contacts the recipient has verified in person.
Code frontend/src/lib/crypto/keyHistory.js frontend/src/lib/log.js frontend/src/lib/keys.js
More about this in the story →
Walk-through, step by step
One message, from the sender to the recipient
Follow a single message from one person's keyboard to another's screen. At each stage, notice what the server is given: enough to check and deliver the message, never enough to read or fake it.
-
Keys that never leftPrivate keys → Lock and sign
The sender's private keys are already unlocked in their browser. They were made there, and the server has only ever held a locked copy.
-
Lock, link, sign, sendLock and sign → Relay
The browser encrypts the text with a fresh key, seals that key to the recipient, adds the hash of the message before it, signs the lot and sends the envelope.
-
Is it next in line?Relay → Chain keeper
The server checks the signature and that the envelope links onto the newest message. If the other person sent at the same moment and got in first, the sender's browser is told, links onto that message, signs again and resends.
-
Stored, still lockedChain keeper → Stored messages
The envelope is saved as it arrived. The server holds the locked text and the sealed keys and can open neither.
-
DeliveredRelay → Check and open
The envelope goes to the recipient's browser over its open connection, even if that connection is to a different server instance.
-
Who signed this?Check and open → Whose key is this?
Before decrypting, the recipient's browser checks the signature and the link to the message before, and asks whether the signing key is really the sender's.
-
Proof, not the server's wordWhose key is this? → Key log
The sender's key history is checked link by link and against the public key log. Only then is the message opened and shown, with a shield beside it.
How we know: Tests send two messages at the same moment and check one lands and the other is answered with what it missed (backend/tests/realtime.test.js); browser tests tamper with a stored message and check the conversation shows the break (e2e/tests/tampering.spec.js).