Comin protocol
What this page is: a description of what the Comin apps and server actually do, written from the code. It names what the server can see, what end-to-end encryption protects and what it does not, and the weak spots we know about. It is not a marketing page.
Architecture
Comin has native apps for iOS and Android. Both use the same Rust crypto core, comin-mls, a fork of the AWS mls-rs-uniffi binding on top of the unmodified mls-rs 0.55.2 library with the RustCrypto provider.
The Comin server is written in Go and acts as the MLS Delivery Service. It handles accounts, the device registry, the KeyPackage directory, relaying over WebSocket, an offline queue, encrypted media storage and push wake-ups. It never holds content keys.
Calls go through a self-hosted LiveKit SFU, and the media is end-to-end encrypted on the devices. Push goes through Apple and Google. Optional edge relay nodes forward TCP to the same server for networks that block the main address.
Message history is kept only on the devices. An encrypted message leaves the server queue as soon as the recipient device acknowledges it.
What the server knows
| Data | Server sees it | Notes |
|---|---|---|
| Message content: text, reactions, edits, deletions, read receipts, room name and description, locations, polls | No | MLS application messages. The ciphertext is queued until acknowledged, at most 30 days |
| Media files | No | AES-256-GCM ciphertext. Deleted 30 days after upload |
| Group membership and roles | Yes | Needed for delivery. MLS commits are visible to the server as PublicMessage |
| Who sends to which group, when, and how much | Yes | Sender device, group, time and ciphertext size |
| IP address | Yes, while connected | Not stored in the database. Kept in memory for rate limits and for the approximate city shown on your own Devices screen |
| Username, display name, avatar | Yes | Avatars are not end-to-end encrypted and are encrypted at rest when the server key is set |
| Online status and last seen | Yes | One timestamp per account |
| Typing indicator and call signalling | Yes | Plaintext to the server: who calls whom, audio or video. Call media is not visible |
| Welcome envelope | Partly | Chat or room, request or not, and the inviter's username. The MLS Welcome itself is encrypted |
| A message you report | Yes, by your choice | Your device decrypts it and sends a copy, up to 4,000 bytes. Kept up to 180 days |
| App log | Only if you tap “Send report” | Redacted on the device and on the server. Kept 7 days |
| Access requests | Yes | Name, email and comment from the request form. Not end-to-end encrypted |
Protected against
- The server operator, or anyone who takes over the server, reading messages, media or call content
- The server adding a listening device to a chat: that requires an MLS commit that every member processes and verifies
- A removed member or revoked device reading new messages: removal is an MLS Remove commit that rotates the keys, not a database row
- A group member impersonating another account, injecting a device under someone else's account, or rewriting history: receivers enforce these rules
Not protected against
- Metadata: membership, timing, sizes and IP addresses are visible to the server
- Key substitution at first contact, unless you compare safety numbers. Keys are pinned on first use
- The server delaying, dropping or refusing to deliver messages
- A compromised or unlocked device
Identity and devices
Accounts
- Registration needs an invitation. No phone number or email is required
- Passwords are stored as bcrypt hashes
- Session tokens are 32 random bytes. Only their SHA-256 is stored. They expire after 90 days
- “Only my circle” hides an account from search and withholds its KeyPackages from strangers, so they cannot start a chat with it
Keys
Each device has an Ed25519 MLS signature key, and each account has one Ed25519 identity key. The MLS credential of a device is userId:deviceId. The identity key signs every approved device:
"comin-device-v1" || userId || ":" || deviceId || 0x0A || deviceSigKey
At login, a device proves it holds its signature key by signing a fresh server nonce:
"comin-device-pop-v1" || username || 0x0A || deviceSigKey || nonce
New device
- Pending until approved. A new device stays pending until one of your approved devices approves it. A pending device cannot connect to chats, and its KeyPackages are not handed out
- Identity key transfer. The approving device scans a QR on the new device, or both screens show the same 6-digit code. It then sends the identity seed encrypted with HPKE (X25519, HKDF-SHA256, ChaCha20-Poly1305,
info = comin-identity-transfer-v1). The server stores the ciphertext and deletes it on first read
Lost all devices
- 24-hour wait. An access reset runs after 24 hours. Your old devices are notified by push and email during that time and can cancel it
- What it changes. The reset creates a new identity key. Your contacts see “has set up Comin again — check the safety number”
- Revocation. A revoked device is removed from every group with an MLS Remove commit. So is a device that has been inactive for 45 days
Safety numbers
You can compare a 60-digit safety number with a contact, or scan their QR code. Each half is computed from the contact's identity key and account id with 5,200 rounds of SHA-512. A verified mark is stored only on your device, together with the exact key you checked.
MLS
| Parameter | Value |
|---|---|
| Protocol | MLS 1.0, RFC 9420 |
| Cipher suite | 0x0001 MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519, the only one accepted |
| Handshake | PublicMessage, so the server observer can follow membership changes |
| Application messages | PrivateMessage |
| KeyPackages | 50 one-time plus 1 last-resort per device, 365-day lifetime, last-resort rotated every 30 days |
| Allowed proposals | Add and Remove by value in member commits. Standalone proposals and encrypted commits are rejected |
| Comin extensions | 0xF0C1: identity-key signature inside the leaf. 0xF0C2: marker that the leaf accepts external commits |
Who is behind a leaf
MLS proves only that a leaf holds its key. Comin receivers also check that the leaf belongs to the account it names:
- Before applying a commit, they check the leaf's identity signature
- After applying it, they check the server's device list against the identity key pinned for that account
- If a device cannot be verified, sending to the chat is blocked until you choose “Trust” or “Leave”. Messages from it are held
- The
fromnick inside a payload must belong to the sending leaf. Only the author can edit or delete a message
Membership changes
- Commits wait for the server. A commit is applied only after the server confirms it, so the group cannot split into different epochs
- Server role checks. The server checks roles on every commit: owners and admins can add and remove people; ordinary members can only add their own devices
- Leaving and revocation. These become removal tasks that another member executes with a Remove commit
- A user's own new device can enter that user's groups by external commit. Every receiver checks the leaf signature against the identity key it already knows. This is disabled on the server by default
Server observer
The server follows each group's public handshake with an MLS external group. It has no secrets: it verifies that commits apply and compares the cryptographic roster with its own table. Most of its checks are fail-open by default. The main defence is on the clients.
Messages
Every message is a JSON object encrypted as one MLS application message. Its kind is one of:
- content:
text,reply,image,album,video,voice,file,files,location,card,qr,game,meeting-poll - mutations:
edit,delete,reaction,read,screenshot,rename,description,accept,meeting, game moves, poll votes
Unknown kinds show as text, and unknown fields are ignored.
The server cannot see payloads, so every rule is enforced by the receiver:
- Edits are accepted for 10 minutes, plus 1 minute for clock skew
- A deletion leaves a placeholder that keeps the author and the time
- A link in a room description must be http or https and opens only after a confirmation that shows the full address
Read receipts carry no message id. They mean “read up to now”.
Link previews are built by the sender. The sender's device fetches the page directly, without cookies, so the site sees the sender's IP address. The Comin server never sees the link. The preview image travels as an encrypted file. The receiver accepts a preview only if its link appears in the message text.
A video note can carry a JPEG cover of up to 16 KB inside the message, so it can be shown before the video downloads.
Media
- Encryption. Every file gets a fresh random 256-bit key and is encrypted whole with AES-GCM: a 12-byte nonce and a 16-byte tag
- Storage. The server stores the ciphertext under a random id and, for files sent by current apps, serves it only to members of the chat. It is deleted after 30 days
- The key travels only inside the encrypted message, in the fragment of the reference:
cominblob://<blobId>?mime=<type>#<base64 key>
- Resume. Interrupted downloads resume with HTTP Range. The GCM tag over the whole file is checked at the end
- Local cache. The cache on the device holds ciphertext only
Transport
- TLS. Everything goes over TLS to
api.comin.chat: REST under/v1/and a WebSocket at/v1/ws. The session token is sent only in the Authorization header and only tocomin.chathosts. There is no certificate pinning - WebSocket frames. Group messages, Welcomes, call signals, typing and acks. Frames from the offline queue must be acknowledged. Undelivered frames are deleted after 30 days. Each device's queue is capped at 20,000 frames or 100 MB
- Feature negotiation. Newer features are negotiated per connection with
caps, so older apps never get frames they do not understand - Edge relay nodes such as
edge1.comin.chatare plain TCP pass-through to the main server. TLS for the API ends on the main server, so a node sees the IP, timing and volume of connections, but not the HTTP, tokens or frames. The app switches to a node only when the direct connection stalls
Push notifications
A message push carries the group id and the MLS ciphertext, if it fits in 3.5 KB. The notification extension on iPhone, or the background service on Android, decrypts it on the device without advancing the stored keys.
The sender's name and a preview appear only if the sender's device is verified. Otherwise you see “Comin — New message”.
Neither Apple nor Google receives the sender's name with a message: they see only that a device got a push for a chat id. A call push still carries the caller's name, but only for older app versions that cannot look it up themselves.
Calls
Audio, video and screen sharing are encrypted frame by frame on the device: LiveKit frame encryption, AES-GCM. The key comes from the chat's MLS group:
key = MLS-Exporter("comin-call-e2ee-v1", groupId, 32)
- Who knows the key. Every current member of the chat can derive it. The server and the LiveKit SFU cannot
- Rotation. When the group epoch changes, for example after someone is removed, apps switch to the new key within seconds
- No fallback. If no key is available, the call does not connect. There is never an unencrypted fallback
- Joining. To join, a device gets a one-hour LiveKit token, issued only to members of the chat. TURN runs over TLS on port 443
- Signalling. Call signalling (invite, accept, hang up, mute) is not end-to-end encrypted. The server sees who calls whom and whether it is a video call
Comin News
Comin News is a public, read-only channel and is not an MLS group. Every post is signed with Ed25519 over its id, its time and the SHA-256 hashes of its texts, image and link. The apps show only posts with a valid signature, and an image only if its hash matches.
The public key is built into the apps:
cjV4ljXZRvA1c5CYzylZ9jV7TAQD/hkYeMH23L7C3LY=
The private key is not stored on the server. The server can hide or delay a post, but cannot forge or change one.
Data on the device
| Data | iPhone | Android |
|---|---|---|
| Keys and credentials | Keychain, this device only | Android Keystore with EncryptedSharedPreferences |
| MLS state | Files with iOS Data Protection | EncryptedFile |
| Message history | SQLite under iOS Data Protection, without a separate database key | SQLCipher |
| Backups | Excluded | Disabled |
App lock with Face ID or biometrics is a screen gate and adds no encryption. Screenshot protection cannot stop a second camera.
Versioning
- REST is versioned by path
- WebSocket behaviour is negotiated with
caps - MLS features are announced in leaf capabilities
- Signed formats each carry a
-v1domain string, and a change means a new version string - New message fields are always optional
- Shared test fixtures. iOS, Android and the server are tested against the same set of wire fixtures
Known limitations
We list these ourselves so that reviewers do not have to find them.
- Metadata. The server knows who is in which chat and when they write
- Plaintext signalling. Call signalling and typing indicators reach the server in plaintext
- Push providers. Apple and Google see the chat id and the time of every push
- Open profiles. Any signed-in user can look up any user's online status, last seen, display name and avatar. Username search is by prefix
- Prior epochs stay on the device for up to a week. At most five per chat are kept for late messages. Someone who seizes the phone in that window and recorded the traffic could read messages of those epochs that the phone itself never decrypted
- The call key is shared by every current member of the chat, not only by those in the call
- One identity key per account. It lives on every device that received it, so compromising one of those devices compromises the account identity until an access reset
- Leaf signatures are not yet mandatory for new accounts, for compatibility with older app versions
- Server checks are off or fail-open by default. This applies to the server-side MLS observer and device proof of possession. The main protection is the client-side checks
- History storage. On iPhone, message history relies on iOS Data Protection without a separate database key
- No certificate pinning
- First contact trusts the key from the server. Compare safety numbers to rule out substitution
Report a vulnerability
Write to privacy@comin.chat with steps to reproduce and the app build number
Please give us reasonable time to fix the issue before disclosing it, and do not test against other people's accounts
Протокол Comin
Что это: описание того, что на самом деле делают приложения и сервер Comin, по коду. Что видит сервер, от чего защищает сквозное шифрование и от чего нет, какие слабые места мы знаем.
Что защищено
- Сообщения. Текст, реакции, правки, удаления, отметки о прочтении, название и описание комнаты шифруются на устройстве по MLS (RFC 9420). Набор шифров: MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519. Библиотека —
mls-rsот AWS без изменений - Медиа. У каждого файла свой ключ AES-256-GCM. Ключ лежит только внутри зашифрованного сообщения. Сервер хранит шифртекст 30 дней
- Звонки. Каждый кадр шифруется на устройстве ключом из MLS-группы чата. Ключ меняется со сменой состава. Без ключа звонок не соединяется
- Новое устройство попадает в чат только коммитом MLS, который видят все участники. Исключение и отзыв меняют ключи группы
- Подлинность. Приёмник проверяет подпись устройства ключом личности аккаунта и что ник отправителя принадлежит ему. Непроверенное устройство в чате блокирует отправку, пока вы не решите
Что видит сервер
- Состав чатов и роли; кто, когда и сколько пишет
- IP при подключении — в базу не пишется
- Ник, имя, аватар; статус «в сети» и время последнего визита
- Сигналы звонков и «печатает» — открытым текстом
- Копию сообщения, на которое вы пожаловались, — до 180 дней
- Журнал приложения — только по кнопке «Отправить отчёт», 7 дней
- Заявки на доступ
Слабые места, которые мы называем сами
- Метаданные: серверу виден граф общения
- Apple и Google видят id чата и время каждого пуша
- Статус «в сети», имя и аватар любого пользователя доступны любому вошедшему
- Прошлые эпохи MLS держатся на устройстве до недели, не больше пяти на чат: кто изымет телефон в этот срок и записал трафик, прочтёт сообщения этих эпох, которые телефон сам не расшифровал
- Ключ звонка знает вся группа, а не только те, кто в звонке
- Взлом устройства с главным ключом — это взлом личности аккаунта до сброса доступа
- Серверные проверки MLS по умолчанию выключены или пропускают при сомнении — главная защита на клиентах
- История на iPhone — под защитой файлов iOS, без отдельного ключа базы. Привязки сертификатов нет
- Первый контакт — доверие ключу с сервера. Подмену ловит сверка кода безопасности
Как сообщить об уязвимости
Пишите на privacy@comin.chat: шаги воспроизведения и номер сборки