
The Server Can't Read Your Direct Messages
Direct messages shipped unencrypted and said so. Two weeks later they were encrypted on your machine, and twice along the way it quietly stopped working.
Your server can't read your direct messages. It doesn't have the key. Messages are encrypted on your machine before they leave it, and what the server stores is something it has no way to open. There's no setting for this and no way to turn it off.
That wasn't true when direct messages shipped in 1.7. Every conversation had a line above it then, saying the server could read what was in it. That line was honest, and it took about two weeks to make it unnecessary.
Twice along the way, encryption stopped working for whole groups of people and nothing on screen said so.
How it works
Every message gets its own key. That key is then encrypted separately for each person in the conversation, using the public key they published, and those encrypted copies travel with the message. The server stores all of it and can open none of it.
Attachments work the same way. They used to go up as ordinary files, get a thumbnail made, and be served to anyone with the link, so a private conversation could have public photographs in it. Now the file is encrypted before it's uploaded and its key sits inside the message.
Gryt only encrypts when everyone in the conversation has published a key it will accept. Encrypting for four people out of five leaves the fifth staring at a conversation they can't read while everyone else sees it working, which is worse than not encrypting at all. When it can't encrypt, the line above the messages comes back and says why.
Half of every signature would have been rejected
The desktop app used the browser's built-in crypto. The phone can't: crypto.subtle
doesn't exist in React Native, so mobile was already using @noble/curves. Running
two implementations of the same format is how you get two apps that can't read each
other's messages, with each sender seeing their own text just fine. So the desktop
moved to noble as well.
Nothing about the message format changed, so this should have been invisible. The test vectors failed anyway.
ECDSA has two valid signatures for every message: s, and order - s. Noble rejects
the high one by default. That's a sensible default for a blockchain, where a
signature you can rewrite and keep valid means a transaction you can replay under a
second id. Gryt doesn't identify anything by its signature, so the default is wrong
here.
The browser's crypto doesn't normalise, and neither does JOSE. So about half of all ES256 signatures come out high. Leaving noble's default on would have rejected roughly half of every client's published keys, at random, with "the signature does not check out". The other half would have worked perfectly.
A bug like that survives a lot of testing. It would have turned up months later as
"encryption sometimes doesn't work", with nothing anyone could reproduce. It only
came out now because the one test vector happened to have a high s, which was luck.
The check signs 24 of them now and fails unless at least one comes out high, so a
future run can't pass on a lucky sample.
Twice, encryption silently stopped working
Nobody who was already a member ever published a key. Publishing ran from the code that handles joining a server. A client that already has a token never joins again. It reconnects instead, and reconnecting runs different code. So encryption worked for anyone who joined a server after the feature shipped, and for nobody who was already in one.
Nothing failed. No key left the device, nothing errored, and the message box said the other person hadn't published a key yet, which was true. That made it look like their problem rather than a bug.
An account signed in on two devices had it switched off as well. Your client remembers two things about everyone you talk to: their message key, and a fingerprint of the key that vouched for it. If either changes, your client stops encrypting, because a key that changed is what a swapped key looks like.
Your message key comes from your seed, so every device you own produces the same one. The signing key didn't work that way. Each device generated its own, so a laptop and a phone signed into one account vouched for the same message key with two different signatures. Whichever spoke last was what the server held, and everyone else watched the fingerprint flip back and forth. Their clients refused to encrypt, which is exactly what they should have done. The people most likely to own two devices were the ones it was off for.
There was a test meant to catch the first one. It checked that both code paths called the publish function, and both of them did. The function returned early before sending anything. The test reads the function itself now, and fails if anything can return before the key goes out.
Comparing a code in person
Remembering someone's key catches that key changing later. It can't tell you the first one was ever theirs, because the server is what introduced the two of you, and anything you check through the server has the same problem.
The only way around it is for the two of you to compare a short code somewhere the server isn't: on the phone, or standing in the same room.
The code is built from all four values involved, both message keys and both fingerprints. Change any one of them and the two codes come out different, and the server can't do anything about that, because it never sees you compare. The four are sorted before they're hashed, so both of you get the same code without having to agree who counts as first. Two honest people reading out codes that didn't match would conclude they were being attacked, which is worse than not checking at all.
The code is digits rather than words. Your identity backup is already 24 words, and eight more words next to them would read as a second recovery phrase: something to keep secret and type into a box. This is the opposite. You read it out loud to someone.
The button says "read this to them", and afterwards the card says "compared and matched". That's what the two of you did, rather than something Gryt is claiming to have checked for you. The mark disappears the moment either key changes. If you compared a code last year and their key has changed since, that comparison tells you nothing.
A warning that was almost always wrong
For a while, joining a server could tell you it was showing a message key that wasn't yours, and that you should treat your direct messages as readable by it.
It went off because of timing. Your client publishes its key when it joins, and the first member list usually arrives before that has landed, so for about a second the key genuinely doesn't match. The warning had no dismiss button and no expiry, so that one second stayed on screen for the rest of the session.
It also pointed at the wrong thing. A hostile server can cause this. What actually caused it, nearly every time, was signing in on a second device.
It waits five seconds now, disappears as soon as the keys agree, and mentions the second device first. That doesn't fix the cause. The cause was that a second device made a whole new identity, and the message password in 1.9 is what fixes that.
Channels are not encrypted
Channels aren't encrypted, and don't claim to be. A direct message is between two people who picked each other. A channel is a room someone administers: people join it, leave it and get removed from it, and the server has to hand the history to whoever turned up yesterday. That's a different problem and this doesn't solve it.
So if you're hosting: your server can read what's said in its channels. It can't read a direct message between two of its members.
1.7.0 is the release this shipped in, and the code is in client and @gryt/crypto if you want to read it rather than take my word for it.