The Sufec protocol
Sufec: Simple User Friendly Encrypted Chat.
Secure messengers have been popping up like crazy in recent years. Matrix. Signal. Threema. Wire. Session. Briar. Tox. Jami. I can't even list all the ones I investigated in my search before deciding to make another. Sufec is necessary because all of these have fallen short in one or more ways:
- Requiring a phone. A secure messenger shouldn't require any specific type of device, but especially not a phone.
- Involving a central server. A secure messenger should have no single point of failure or control.
- Being too complex, resulting in a lack of robust implementations.
- Having privacy/security flaws, such as a lack of forward secrecy and deniability.
One thing Sufec tries to do that most secure messengers don't is replace email. You can message a Sufec user without having to "send a contact request" or "invite them to a DM"; "conversations" or "rooms" are not even first-class things.
Besides the general impetus to minimize the number of applications necessary for digital life, a pressing reason to want to replace email is that it's ubiquitously used for sensitive things such as password reset codes. That our society's standard is to send such things unencrypted is a travesty.
Sufec has no provisions for unencrypted messaging; we think that belongs in a different protocol.
What Sufec is not
- Ideal for all use cases
- A drop-in replacement for what you currently use
- A magic spell that perfectly guarantees whatever your notion of privacy happens to be
- Reviewed by experts in security and cryptography
Sufec should provide:
- Authentication: when you receive a message, you have proof that it was sent by who it says.
- Forward secrecy: if an attacker compromises your long-term private key, they shouldn't be able to read any messages sent before they did that.
- Self-healing: if an attacker compromises ephemeral keys, they shouldn't be able to read any messages sent after they did that.
- Forgeability: if an attacker compromises message plaintext, they shouldn't be able to prove anything about it to a third party, not even that a message was sent.
- Metadata protection: Sufec should minimize the amount of metadata that an attacker can gather by recording network traffic, even if the attacker controls the homeservers.
- Resilience: it should be difficult for an attacker to prevent communication by attacking or controlling homeservers.
Sufec is formally a federated protocol, similar to email. A user has a *homeserver* through which they receive messages, and a long-term public/private key pair, whose public part is their ID. There are no human-readable usernames as there are in email; your Sufec address is `<id>@<server>`, not `<name>@<server>`.
Unless otherwise stated, all numbers are serialized in big-endian format.
Each Sufec message is encrypted with a key derived from the long-term key pair and an ephemeral key pair from each party (this is based on the Signal handshake and is supposed to have the same properties):
Explanation of the Signal handshake
- You generate an ephemeral keypair for receiving and publish the public part on your homeserver.
- When you want to send someone a message, you generate an ephemeral keypair for sending, and ask their homeserver for their ephemeral public key.
- You derive 3 shared secrets from the 4 keypairs: both ephemeral keys combined, and each party's ephemeral key combined with the other party's long-term key.
- These three are XORed to derive one symmetric key, which is used to encrypt the message, along with a randomly generated nonce that is prepended to the ciphertext.
(This is where the handshake differs from Signal, by the simplistic use of XOR instead of a special key derivation function. I conjecture that XOR is as secure as any other way of combining them, since all inputs are secret, it's irreversible, and produces no statistical anomalies such as non-uniform probability distribution.)
Connecting to a homeserver
Every interaction with a homeserver starts by connecting to it over TCP port 49002. The server sends its public key for transport encryption, which the client handles with TOFU (trust on first use) policy: remember the public key when connecting to a server for the first time, and alert the user on subsequent connections to the same server if the key ever changes.
While reading the sections for each type of conversation you can have with a homeserver after receiving its public key, bear in mind:
- All client-server communication is half-duplex.
- After a session key is sent, all transmissions are encrypted with it, and each time it's used to encrypt, the nonce is incremented by one as a little-endian number, beginning at 0. The nonce is shared across client and server, meaning: if a client uses the nonce 0, and the server uses 1 and 2, the client's next transmission will use 3.
- Wherever possible, things being sent at the same time are encrypted as part of the same payload.
- Transmissions with a non-constant length (these will be pointed out) are preceded with a separately encrypted 4-byte number telling the length of the plaintext of the following transmission, so the remote end can know how many bytes to read before trying to decrypt.
- Servers respond to any error conditions (such as the client sending a message that cannot be decrypted, or running out of disk space) by closing the connection. Since there aren't really any points in this protocol where such errors can be expected, sophisticated diagnostics would not be worthwhile.
Sending a message
1. Send a byte with value 1 to indicate you are connecting to send.
2. Randomly generate a symmetric session key, anonymously encrypt it to the server's public key, and send. This doesn't authenticate the client because sending is meant to be anonymous. From this point on, all transmissions are encrypted with the session key.
3. Send the ID of the recipient (preceded by its length).
4. The server concatenates the ephemeral keys of each device the recipient has linked, encrypts that with the session key, and sends it (preceded by its length). If the recipient ID is not found, the server should respond with 0 for the length, indicating "0 linked devices", and the client should abort and show an error message.
5. For each of those keys, do 6-7.
6. Encrypt the message with the key (as described in the Handshake section), then create the following concentation:
- 4-byte length of your address
- your address
- your ephemeral public key used for this message
- the nonce used for the inner ciphertext
- the ciphertext of the message itself
7. Anonymously encrypt this concatenation to the recipient's ID (so their homeserver can't read the metadata), then send the result. Only prepend the length for the first copy, since they must all be the same length.
8. Once the server receives such a payload for each key it gave you, it sends an arbitrary 1-byte receipt to indicate delivery was successful.
1. Send a byte with value 0 to indicate you are connecting to receive.
2. Encrypt your ID anonymously to the server's key and send that.
3. Do a handshake with your ID and the server's key to arrive at a symmetric session key. From this point on, all transmissions are encrypted with the session key.
4. Generate a new ephemeral keypair for receiving, and send your device ID followed by your new ephemeral key.
5. The server will send you any messages that were stored for you. The precise payload is the anonymously encrypted concatenation as received by the server in Sending a message steps 6-7 (preceded by its length).
6. After each message, you send back an arbitrary 1-byte receipt to indicate the receipt was successful.
The connection stays open and the server will send any new messages that arrive.
When an unrecognized client connects to receive, the server should treat this as a registration and create the mailbox.
Add a device
To add a new device to your account, simply copy over the long-term private key and have the new device generate its own device ID. The new device can then connect to the homeserver in receiving mode and on seeing the unrecognized device ID, it will treat this as a newly linked device. From then on incoming messages will be stored until both devices have downloaded them.
Remove a device
You can use any device that has your private key to tell your homeserver to forget about another.
1. Send a byte with value 2 to indicate you are connecting to forget a device.
2. Performs steps 2-3 from Receiving messages.
3. Send the device ID you want to forget.
4. The server sends back an arbitrary 1-byte receipt to indicate the device was forgotten.
This is *not* a secure response to a lost or stolen device. Such a device, since it has your private key, would be able to re-add itself to your account, and even if it couldn't, it would still be able to send on your behalf, which your homeserver has no control over. If a device with your long-term private key is lost, you should generate a new identity.
The intended use of this feature is to tell your homeserver to stop storing messages for a device you don't or won't possess anymore, for example if you have a hard drive failure, if you reinstall your operating system and forgot to back up the relevant files, or if you are wiping a device preparing to give or sell it.
A Message is serialized as, in sequence:
- 1-byte number of recipients other than the one reading the message
- the address of each other recipient (each one preceded by its length)
- 1-byte number of included hashes of previous messages sent by other group members
- timestamp followed by hash of each of those
- a type indicator byte
- message content
If the type is 0, the message is plain text.
If the type is 1, the message is a file, and there are these additional fields before the file content:
- 1-byte length of the filename
In a group chat, each participant should, for each received message, include a hash of it with their next outgoing message, confirming that they received it. This is necessary to prevent a group member from spoofing the recipient list, secretly sending different messages to different people. For example, if A, B, and C are in a group chat and A sends a message to B that includes C in the recipient list but doesn't actually send the message to C, or sends a different message to C, then B will notice that they never receive a hash of that message from C, and C will notice that they never receive one from B.
In chats with only two members, this is not necessary so the number of included hashes can just be set to 0.
The exact input to hash is the serialized sender's address followed by the type indicator byte and message content.
An address is serialized as the ID followed by the homeserver name. The homeserver name can be a domain name, a dotted-decimal IPv4 address, or a bracketed colon-separated IPv6 address.
Sufec is based on libsodium, so:
- Keys are 32-byte Curve25519 keys.
- The encryption arranged by the handshake is the "box" construct (X25519, XSalsa20, Poly1305).
- Anonymous encryption is the "sealed box" construct (X25519, Blake2b, XSalsa20, Poly1305).
- Hash is SHA-512.
- Timestamps are represented as an 8-byte number of milliseconds since the epoch (beginning of 1970).
- Device IDs are arbitrary 4-byte strings.
Group chats are implemented similarly to email: you just send your message to each recipient, and in the Message to each one, you include the list of *other* recipients. Client-side, messages with the same set of recipients can be sorted into "rooms", providing a UX not too different from what we expect from chat apps.
There is no moderation, no invite/leave/kick/ban commands, no questions about how to agree on what members are in a group.
One downside of most forms of federation is that you depend on your homeserver to interact with the network at all, and switching to a new one can be quite difficult since you need to message all your contacts and explain that you have a new address. One of the benefits of being `<id>@<server>` instead of `<name>@<server>` is that your ID is unspoofable, so when a Sufec client receives a messages from a recognized ID at a different server, it should automatically update its knowledge of that user's address (whereas in email, anybody could've made an account on a different server with the same username as your contact and tried to impersonate them). There is no need for out-of-band verification of your new identity.
Peer to peer usage
Although Sufec is a federated protocol, we intend it to be usable in a peer to peer way by having a client act as its own homeserver, and having `<id>@<ip address>` as an address. This is part of the reason for the emphasis on homeserver-independence: someone can use it both ways, even in the same conversation, with minimal friction.
Linking a phone
Of course, one of the most convenient ways to link a phone is to have a desktop client that can show a QR code, and for every mobile client to be able to do this with every desktop client, we need a standard format for the data in the QR code. Our standard is the private key followed by the address, serialized as they are on the network.
Let's be straight up: Sufec is not going to have all the features getting cargo-culted into every modern chat app. Matrix tried, and look how that turned out (there's only one client that actually implements all the features and it's... not good). Omitted features include, but are not limited to:
- Editing, because the standard of sending your correction prefixed with an asterisk is adequate.
- Deleting, because once a message leaves your device, there is no way to enforce deletion or to know whether someone has already read it.
- Replies. This can be mostly replicated as a client-side feature: clients can show a "reply" button on messages that fills the textbox with a plaintext quote of the sort used in email (`> `), and treat that syntax with color or special formatting (a la qTox).
- Reactions, because they are an ugly special case of replies and just aren't necessary.
- Any kind of profile information. Display names are client-side, as in SMS.
- Audio/video calls, because this belongs in a separate protocol/app such as Jitsi.
Omitted security and privacy properties
Note some properties one might expect that are absent:
- If you use the same server as other users in a group you participate in, the server can probably figure out that you are in a group together (because it can see each member of the group that it hosts being messaged at the same time). Therefore it's still desirable to use a homeserver hosted by a trusted party even though they can't access any message content.
- There is no forward secrecy on the transport layer, only on the end-to-end layer.
Currently there is:
A Rust library
A TUI client
A Java library (intended only for Android)
An Android client
a GTK client
A homeserver running most of the time with open registration: yujiri.xyz.
Todo list for the project
FAQ (these have not actually been asked)
Why not TLS for transport encryption?
- TLS is too complicated; most of its features are not wanted here (extensive metadata on certificates, signature chains, expiration, cipher negotiation).
- TLS would require an additional library dependency.
Why not encrypt the intent indicator byte?
The conversation flows are sufficiently different that it would be trivial for a network eavesdropper to tell the difference anyway.
Why not use randomly generated nonces on the transport layer?
That would allow one nonce to be used twice. A network attacker, such as an evil ISP, could record certain messages, such as the 1-byte receipt you send after downloading a message successfully, and then hijack the connection and send the same receipt after each subsequent message, emptying your mailbox without letting you download the others.
Another solution would be to send not a 1-byte receipt but a hash of the downloaded message, but that would be more expensive (computation and data size) and have no relative benefit.
subscribe via RSS