# How encryption works

The full flow, from your first key to a message on someone else’s screen.

This is the long version of end-to-end encryption: every key, who holds it, and what happens at each step, with the code you can read on GitHub.

## The big picture

A message is locked on the device that writes it and unlocked only on the devices of the people in the chat. Everything in between, including our servers, carries bytes that mean nothing to it.

1. **You write a message** (your device): Plain text exists only here.
2. **Sealed with the chat’s key** (your device): AES-256-GCM with a fresh nonce. Nothing readable leaves the device.
3. **Carried as ciphertext** (our server): Stored and delivered as bytes. The server never holds a chat key or a private key.
4. **Opened on their device** (their device): Their private key opens their copy of the chat key, and the chat key opens the message.

_Colour shows where each step runs. Only the amber box is ours._

Two layers do the work. A chat has one key that locks its messages. That chat key is then sealed separately to each member’s personal key pair, so it can be handed to the right people over a network we run without us ever seeing it.

## The keys, and who holds them

| Key | What it is | Where it lives | What our server sees |
| --- | --- | --- | --- |
| Identity key pair | A P-256 key pair made on your device. One per account. | The private half stays on your devices. The public half is published so people can seal to you. | The public half only |
| Recovery code | 256 random bits made on your device. Older accounts were shown it once; the phone now keeps it to itself. | On your device, or written down by you. Never sent anywhere. | Nothing |
| Wrapped identity | Your private key encrypted by the recovery code. | Stored on our server so a new device can get it back. | Ciphertext against a 2^256 keyspace. It cannot open it. |
| Chat key | One AES-256 key per chat, with a version number. | Sealed separately to every member’s identity key. | Sealed copies only |
| File key | A random 32-byte key for one file. | Sealed inside the message under the chat key. | Never |
| Link key | A throwaway key a new device makes for one sign-in. | Memory of the new device only. | Nothing. It travels by QR code, not through the server. |

## Your first key

The first time an account opens secure messages, the device builds everything itself.

1. **Ask for your vault** (your device): A brand-new account has none.
2. **Make an identity key pair** (your device): A P-256 pair from the browser’s or phone’s own crypto. The private half is not exportable by the page.
3. **Make a recovery code** (your device): 256 random bits. Nobody types it in, picks it or can shorten it.
4. **Wrap the private key with the code** (your device): HKDF-SHA256 over the code and a per-vault salt gives an AES-256 key, and AES-GCM seals the private key under it.
5. **Store the public key and the wrapped blob** (our server): Safe for us to hold forever, because without the code it is noise.
6. **Keep the key locally** (your device): This browser keeps the identity in IndexedDB, locked by a device key the page cannot export.

> If two devices set up at the same moment, the server keeps whichever arrived first and the other one switches to the normal sign-in path.

## Sending a message

1. **Find the chat’s key** (your device): Ask the server for the newest chat key version and open your own sealed copy with your private key.
- _The chat has no key yet_
   1. **Get the roster** (your device): The public identity key of every member.
   2. **Mint the chat key** (your device): A new random AES-256 key, version 1.
   3. **Seal a copy to each member** (your device): A one-time ECDH exchange with their public key, hashed into an AES-GCM key. The recipient’s id is bound in.
   4. **Store the envelopes** (our server): One sealed copy per member.
- _The chat already has one_
   1. **Reuse it** (your device): Nothing to mint. If someone joined or left since, the key is replaced first (see below).
3. **Encrypt the message** (your device): AES-256-GCM, fresh 12-byte nonce. The chat id, key version and your user id are bound in as associated data.
4. **Receive ciphertext, nonce and key version** (our server): The body field is empty. It checks that every member can read that key version, then stores and delivers it. Pushes carry the ciphertext too, so the phone decrypts the notification itself.
5. **Open and read** (their device): Fetch the envelope for the version the message names, open it, decrypt. Wrong chat, wrong sender or a changed byte and it refuses to open.

> **Why the associated data matters.** A message is bound to its chat, its key version and its sender. The server cannot move it into another chat or put a different name on it without the check failing.

## Signing in on a new device

Signing in proves who you are. It does not carry your message key. A new device has to be given the key, and there are exactly two ways.

1. **Sign in** (new device): A phone scan on the web; the desktop app also takes Google. The account is open, but this device holds no key yet.
- _One scan_
   1. **Make a throwaway key pair** (new device): The QR carries a sign-in token and its public half. The private half stays in this tab’s memory.
   2. **Phone scans the QR** (your device): It reads the key off the camera, never from the server. Both screens show the same six digits.
   3. **Phone seals your identity to that key** (your device): The identity envelope is posted for the new device.
   4. **Relay the envelope** (our server): Handed out once. It cannot open it.
   5. **Open and adopt** (new device): Your key arrives, history decrypts.
- _Recovery code (older accounts, web)_
   1. **Paste the code** (new device): Case and spacing do not matter.
   2. **Send back the wrapped blob** (our server): The same ciphertext stored at setup.
   3. **Unwrap locally** (new device): HKDF from the code, AES-GCM open. The code never leaves the device.

> **Why the QR is safe.** The key that seals your identity is read off the camera, so the server cannot swap it. All it relays is a sealed envelope for a key it never saw. The six digits are there so a swapped QR is visible before you approve.

## Groups and key changes

Groups use the same scheme as DMs. The only new problem is that the set of people changes under the key.

| What happens | What the chat does |
| --- | --- |
| Someone joins | They hold no envelope, so the chat gets a new key version. They read from the moment they joined, not the history before it. |
| Someone leaves or is removed | The old key is retired and a new one is sealed to the people left. Everyone still has a valid copy of the old key, so the retirement is what forces the change. |
| Someone starts fresh | Their old messages stay sealed for good. Chats are re-keyed from both sides so nobody keeps sealing to a key they can no longer open. |
| Someone has no key yet | They cannot be added to an encrypted group until they open AbabilX once. Otherwise the group would stall for everybody. |
| Someone is added back | They start again from that moment, as new. |

A message is always read with the key version it names, never just the newest. That is how older messages stay readable after the key has moved on.

## Files

1. **Pick a fresh key for the file** (your device): 32 random bytes, used for this file only.
2. **Encrypt in chunks** (your device): 64 KiB blocks of AES-256-GCM. The chunk number and a last-chunk flag are bound in, so blocks cannot be reordered or cut off. The result is padded to a size bucket.
3. **Store an opaque object** (our server): A generic binary file. It sees roughly how big, never the name or type.
4. **Seal the file’s key with the message** (your device): The key, real name, type and size go under the chat key, beside the message.
5. **Open, download, decrypt** (their device): Open the sealed details, fetch the object, decrypt it on this device.

> **Older files.** Files sent before file encryption reached your app stay readable by our storage until they are deleted. Workspace channels and flocks (webhook feeds) are never end-to-end encrypted.

## Checking nobody is in the middle

One question cryptography cannot answer from the inside: is this public key really your friend’s? Our server hands out public keys, so a hostile one could hand out its own. Safety numbers are how two people check.

- Each person’s key is hashed 5,200 times with SHA-512, so grinding a look-alike key is expensive.
- The two results become one 60-digit number that is the same for both of you.
- The digits are identical on web, desktop and phone, and tests pin the same vector on each.
- Your app remembers each contact’s key and warns in the chat when it changes, because nobody compares digits twice.

## What this does not do

- Metadata is visible to us: who is in a chat, when messages are sent, and how big they are.
- There is no forward secrecy for ordinary chats. Someone who got a chat key could read what it sealed. Ghost Chat adds it, because its keys are throwaway.
- A modified client or a camera pointed at a screen cannot be stopped by any of this.
- Who is in a group is still our server’s word, so check safety numbers with people you care about.
- Lose every device and your local backup, and old messages cannot be opened by anyone. That is on purpose: the alternative is a key we hold.

---
Source: https://chat.ababilx.com/docs/how-encryption-works
Last updated: 2026-10-19
