Federated end-to-end encrypted messenger. Your messages, your server, your keys.
Федеративный E2E-мессенджер. Ваши сообщения, ваш сервер, ваши ключи.
Zero-trust architecture: the server operator cannot read your messages, files, or calls — and with sealed-sender, encrypted group names, and length padding, the server doesn't even learn who wrote which message of what size. All encryption happens on your device before anything leaves it.
Zero-trust архитектура: оператор сервера не может прочитать ваши сообщения, файлы или звонки — а с sealed-sender, зашифрованными именами групп и padding'ом длины сервер даже не знает кто написал какое сообщение какого размера. Всё шифрование происходит на вашем устройстве до того, как что-либо покидает его.
| Data | Protection | Status |
|---|---|---|
| Private messages (P2P) | MLS (RFC 9420), post-quantum ciphersuite | PQ-Encrypted |
| Group messages | MLS (RFC 9420) — same protocol, N members | PQ-Encrypted |
| Message sender identity | Sealed inside MLS payload; server stores no sender_id | Sealed |
| Group chat names | MLS-exporter-derived key, ciphertext on server | Encrypted |
| File names, MIME types | Travel inside MLS payload; server sees only "encrypted" | Encrypted |
| Message & file length | Bucketed padding hides exact byte counts | Padded |
| Voice & video calls | SRTP / DTLS 1.2 + Insertable Streams frame encryption | Encrypted |
| Call signalling (SDP, ICE) | MLS exporter, key rotates with group epoch | Encrypted |
| Files & images | XChaCha20-Poly1305 (client-side before upload) | Encrypted |
| History sync bundles (new device) | libsodium sealed_box to device's X25519 pubkey | Encrypted |
| Reactions (who reacted with which emoji) | Sealed per-chat under an MLS-exporter-derived key; server stores only opaque bytes | Sealed |
| Read state (who has read what) | Never reaches the server — per-chat unread counters live client-side in the sealed IDB vault | Client-only |
| Password at rest (server) | Argon2id hash — no encrypted-blob decrypt-check trick | Hashed |
| Local IndexedDB (MLS state + message cache) | XChaCha20-Poly1305 under an Argon2id-derived password key; MK only in memory while the tab is alive | Encrypted at rest |
| Federation envelopes | mTLS + Ed25519 signature over payload | Encrypted + signed |
| Данные | Защита | Статус |
|---|---|---|
| Личные сообщения (P2P) | MLS (RFC 9420), post-quantum ciphersuite | PQ-шифровано |
| Групповые сообщения | MLS (RFC 9420) — тот же протокол, N участников | PQ-шифровано |
| Личность отправителя | Запечатана в MLS payload; сервер не хранит sender_id | Sealed |
| Имена групповых чатов | Ключ из MLS exporter, на сервере только шифротекст | Зашифровано |
| Имена файлов, MIME-типы | Внутри MLS payload; сервер видит только "encrypted" | Зашифровано |
| Длина сообщений и файлов | Padding по корзинам скрывает точный размер | Padded |
| Голос и видео | SRTP / DTLS 1.2 + frame encryption через Insertable Streams | Зашифровано |
| Сигналинг звонков (SDP, ICE) | MLS exporter, ключ ротируется с epoch группы | Зашифровано |
| Файлы и изображения | XChaCha20-Poly1305 на клиенте до загрузки | Зашифровано |
| History-sync bundles (новое устройство) | libsodium sealed_box на X25519 pubkey устройства | Зашифровано |
| Реакции (кто какой эмодзи поставил) | Запечатаны под ключом из MLS exporter чата; сервер хранит только непрозрачные байты | Sealed |
| Read state (кто что прочитал) | На сервер не попадает — per-chat unread-счётчики живут на клиенте в запечатанном IDB vault | Только клиент |
| Пароль на сервере | Argon2id hash — никаких зашифрованных-блобов-для-проверки | Hashed |
| Локальная IndexedDB (MLS state + кеш сообщений) | XChaCha20-Poly1305 под ключом, выведенным из пароля через Argon2id; MK в памяти только пока жива вкладка | Шифр at-rest |
| Federation envelopes | mTLS + Ed25519 подпись payload | Шифр + подпись |
| Purpose | Algorithm / Construction | Standard |
|---|---|---|
| Group messaging | MLS, ciphersuite MLS_256_MLKEM1024_CHACHA20POLY1305_SHA512_Ed25519 (61452) | RFC 9420 |
| Post-quantum KEM | ML-KEM-1024 (Kyber-1024) | FIPS 203 |
| Key exchange (history-sync sealed_box) | X25519 (Curve25519 ECDH) + XSalsa20-Poly1305 | RFC 7748, libsodium |
| Symmetric encryption (files, at-rest) | XChaCha20-Poly1305 | IETF draft |
| Authenticated encryption (MLS application) | ChaCha20-Poly1305 | RFC 8439 |
| Call-signalling key | MLS exporter → HKDF("talkee-webrtc-signaling-v1", 32 bytes) | RFC 9420 §8 |
| Reaction-sealing key | MLS exporter → "talkee-reaction-v1" (32 bytes); epoch-rotating | RFC 9420 §8 |
| Key derivation | HKDF-SHA512 | RFC 5869 |
| Server-side password hashing | Argon2id (memory-hard) — genuine hash, not an encrypted-blob check | RFC 9106 |
| Local IDB vault — key derivation | Argon2id over login password + per-install 16-byte salt → 32-byte KEK (iter=3, mem=64 MiB, lanes=1) | RFC 9106 |
| Local IDB vault — master key wrap | XChaCha20-Poly1305 seal of a random 32-byte MK under the Argon2id KEK (24-byte nonce, 16-byte tag) | IETF draft |
| Local IDB vault — payload encryption | XChaCha20-Poly1305 with fresh 24-byte random nonce per IDB row; MK held in memory only | IETF draft |
| Digital signatures | Ed25519 (device identity, federation envelopes) | RFC 8032 |
| Hashing | BLAKE3 (session tokens, recovery-key hash) | BLAKE3 spec |
| Recovery mnemonic | BIP-39 (12 words; only the hash reaches the server) | BIP-0039 |
| Padding (messages + files) | ISO/IEC 7816-4 bit padding (text) / AEAD-before-random-tail (files) | ISO/IEC 7816-4 |
| Voice / video media (transport) | SRTP + DTLS 1.2 | RFC 3711, RFC 6347 |
| Voice / video media (E2E frame layer) | AES-256-GCM per encoded frame, 12-byte IV (4-byte salt + 8-byte monotonic counter), 16-byte tag, codec header kept as AAD | NIST SP 800-38D |
| Media-frame key derivation | MLS exporter → HKDF-SHA256 (salt talkee-media-e2e-v1, info talkee-frame-key, 32 bytes) | RFC 5869 |
| TLS (transport) | TLS 1.3 — enforced by browser and Traefik | RFC 8446 |
| Назначение | Алгоритм / конструкция | Стандарт |
|---|---|---|
| Групповое сообщение | MLS, ciphersuite MLS_256_MLKEM1024_CHACHA20POLY1305_SHA512_Ed25519 (61452) | RFC 9420 |
| Post-quantum KEM | ML-KEM-1024 (Kyber-1024) | FIPS 203 |
| Обмен ключами (history-sync sealed_box) | X25519 (Curve25519 ECDH) + XSalsa20-Poly1305 | RFC 7748, libsodium |
| Симметричное шифрование (файлы, at-rest) | XChaCha20-Poly1305 | IETF draft |
| Authenticated encryption (MLS application) | ChaCha20-Poly1305 | RFC 8439 |
| Ключ сигналинга звонков | MLS exporter → HKDF("talkee-webrtc-signaling-v1", 32 байта) | RFC 9420 §8 |
| Ключ запечатывания реакций | MLS exporter → "talkee-reaction-v1" (32 байта); ротируется с epoch | RFC 9420 §8 |
| Key derivation | HKDF-SHA512 | RFC 5869 |
| Хэш пароля (серверный) | Argon2id (memory-hard) — настоящий hash, не обёртка-проверка | RFC 9106 |
| Локальный IDB vault — деривация ключа | Argon2id над паролем логина + per-install 16-байтовый salt → 32-байтовый KEK (iter=3, mem=64 МиБ, lanes=1) | RFC 9106 |
| Локальный IDB vault — обёртка master key | XChaCha20-Poly1305 seal случайного 32-байтового MK под Argon2id KEK (24-байтовый nonce, 16-байтовый tag) | IETF draft |
| Локальный IDB vault — шифрование payload | XChaCha20-Poly1305 со свежим 24-байтовым случайным nonce на каждую IDB-запись; MK только в памяти | IETF draft |
| Цифровые подписи | Ed25519 (identity устройства, federation envelope) | RFC 8032 |
| Хэширование | BLAKE3 (session-токены, hash recovery-ключа) | BLAKE3 spec |
| Recovery-мнемоника | BIP-39 (12 слов; на сервер попадает только hash) | BIP-0039 |
| Padding (сообщения + файлы) | ISO/IEC 7816-4 bit padding (текст) / AEAD-before-random-tail (файлы) | ISO/IEC 7816-4 |
| Медиа звонков (транспорт) | SRTP + DTLS 1.2 | RFC 3711, RFC 6347 |
| Медиа звонков (E2E frame layer) | AES-256-GCM для каждого закодированного кадра, 12-байтовый IV (4 байта salt + 8 байт монотонный счётчик), 16-байтовый tag, заголовок кодека как AAD | NIST SP 800-38D |
| Деривация ключа медиа-кадров | MLS exporter → HKDF-SHA256 (salt talkee-media-e2e-v1, info talkee-frame-key, 32 байта) | RFC 5869 |
| TLS (транспорт) | TLS 1.3 — навязывается браузером и Traefik | RFC 8446 |
client_device_id + Ed25519 and X25519 keypairs locally. Private keys never leave the device.client_device_id + пары Ed25519 и X25519 локально. Приватные ключи никогда не покидают устройство.{_v:1, sender, text, file_key?, …} and encrypts that with MLS. The server receives a ciphertext with no sender attribution and stores the row with sender_id = NULL. Only recipients learn who wrote it.purge_stale_ciphertext then nulls it), and authenticates neither content nor sender. MLS keyschedule prevents forgery.{_v:1, sender, text, file_key?, …} и шифрует это MLS'ом. Сервер получает ciphertext без какой-либо привязки к отправителю и сохраняет строку с sender_id = NULL. Кто написал — узнают только получатели.purge_stale_ciphertext потом nullит), не верифицирует ни содержимое, ни отправителя. MLS keyschedule препятствует подделке.talkee-webrtc-signaling-v1). No static-DH fallback exists — starting a call in a fresh chat first brings up the MLS group synchronously (~200 ms once), then every call hits the ready path.talkee-media-e2e-v1, info talkee-frame-key), wired in through the Insertable Streams API. A compromised TURN relay or even a malicious SFU sees nothing but ciphertext — SRTP+DTLS is the outer envelope, AES-GCM is the real confidentiality guarantee.talkee-webrtc-signaling-v1). Static-DH fallback удалён — запуск звонка в свежем чате сначала синхронно поднимает MLS-группу (~200 мс один раз), затем каждый следующий звонок идёт по готовому пути.talkee-media-e2e-v1, info talkee-frame-key) и подключается через Insertable Streams API. Скомпрометированный TURN или даже вредоносный SFU видят только шифротекст — SRTP+DTLS это внешний конверт, а реальную конфиденциальность даёт AES-GCM.wrap(MK) before hitting disk: fresh 24-byte random nonce per row, 16-byte auth tag, no key reuse. An attacker who dumps IDB sees only ciphertext.wrap(MK) до попадания на диск: свежий 24-байтовый nonce на запись, 16-байтовый auth-tag, никакого key reuse. Атакующий, снявший дамп IDB, видит только шифротекст.user_id:device_id so two devices of the same user are distinguishable to the MLS tree.ON DELETE CASCADE across sessions, KeyPackage pool, MLS group state and pending handshakes — one transaction, one database commit. The revoked device sees an immediate WS kick and logs out.user_id:device_id — два устройства одного пользователя различимы для MLS-дерева.ON DELETE CASCADE по sessions, пулу KeyPackage, MLS group state и pending handshakes — одна транзакция, один DB commit. Отозванное устройство получает мгновенный WS-kick и logout.crypto_box_seal against the new device's X25519 public key (libsodium anonymous-sender AEAD)./v1/device-sync/bundle capped at 4 MiB. The server only sees (from_device_id, to_device_id, chat_id, opaque ciphertext). Bundles expire after 24 hours via purge_stale_sync_bundles.crypto_box_seal под X25519-pubkey нового устройства (anonymous-sender AEAD libsodium)./v1/device-sync/bundle с лимитом 4 MiB. Сервер видит только (from_device_id, to_device_id, chat_id, непрозрачный ciphertext). Bundles истекают через 24 часа через purge_stale_sync_bundles.We believe in transparency. Here is exactly what the server can and cannot see:
Мы за прозрачность. Вот точный список того, что сервер может и не может увидеть:
| Data | Server can see? | Why |
|---|---|---|
| Message content | No | MLS-encrypted on your device |
| File content | No | XChaCha20 before upload |
| File names & MIME types | No | Inside the MLS payload; server stores the placeholder "encrypted" |
| Group chat names | No | Sealed with an MLS-exporter key before upload |
| Message sender | No | Sealed-sender: sender_id is NULL for every encrypted row; real identity lives inside the MLS payload |
| Exact message / file size | No | Plaintext is padded to size buckets before encryption |
| Call audio / video | No | SRTP peer-to-peer; frame E2E on supported browsers |
| Call signalling (SDP / ICE) | No | Sealed with the MLS exporter secret — rotates every epoch |
| Your password | No | Only an Argon2id hash ever reaches the database |
| Your recovery mnemonic | No | Never transmitted — only shown once on your screen; server stores a hash for proof-of-possession |
| Device private keys | No | Generated locally, held in IndexedDB + MLS worker memory |
| Group membership roster | Yes | Required for delivery fan-out |
| Who chats with whom | Yes | Required for routing (chat metadata: ids, timestamps) |
| Timestamps | Yes | Required for ordering |
| Your username & domain | Yes | Required for account lookup |
| Device public keys (Ed25519, X25519) | Yes | Needed to route MLS Welcomes + seal history bundles to new devices |
| Данные | Сервер видит? | Почему |
|---|---|---|
| Содержимое сообщения | Нет | Зашифровано MLS на вашем устройстве |
| Содержимое файла | Нет | XChaCha20 до загрузки |
| Имена файлов и MIME-типы | Нет | Внутри MLS payload; сервер хранит placeholder "encrypted" |
| Имена групповых чатов | Нет | Запечатаны ключом из MLS exporter до загрузки |
| Отправитель сообщения | Нет | Sealed-sender: sender_id в БД = NULL для каждой зашифрованной строки; реальный отправитель живёт внутри MLS payload |
| Точный размер сообщения / файла | Нет | Plaintext паддится до размерных корзин перед шифрованием |
| Аудио / видео звонков | Нет | SRTP peer-to-peer; frame E2E на поддерживающих браузерах |
| Сигналинг звонков (SDP / ICE) | Нет | Запечатан MLS exporter secret'ом — ротируется каждый epoch |
| Ваш пароль | Нет | В БД попадает только Argon2id hash |
| Recovery-мнемоника | Нет | Никогда не передаётся — показывается один раз на экране; сервер хранит только hash для proof-of-possession |
| Приватные ключи устройства | Нет | Генерируются локально, хранятся в IndexedDB + памяти MLS worker |
| Состав группы | Да | Нужно для fan-out доставки |
| Кто с кем общается | Да | Нужно для маршрутизации (chat-метаданные: id, timestamps) |
| Timestamps | Да | Нужно для упорядочивания |
| Username и domain | Да | Нужно для поиска аккаунта |
| Публичные ключи устройства (Ed25519, X25519) | Да | Нужно для доставки MLS Welcomes и sealed-box history bundles |
This goes beyond the metadata model used by mainstream secure messengers. The server still learns the routing graph (which accounts share a chat and when a message transits), but with sealed-sender it no longer learns which of the members wrote which message — the chat becomes an anonymous set, not a labelled timeline.
Это идёт дальше metadata-модели, принятой в популярных защищённых мессенджерах. Сервер по-прежнему видит routing graph (какие аккаунты состоят в одном чате и когда прошло сообщение), но с sealed-sender он больше не знает кто именно из участников написал какое сообщение — чат для сервера становится анонимным набором, а не размеченной лентой.
Every MLS group negotiates its shared secret with ML-KEM-1024 (FIPS 203) alongside classical X25519. Even if an adversary stores today's traffic and breaks X25519 on a future quantum computer ("harvest now, decrypt later"), ML-KEM remains unbroken under every known classical and quantum attack.
MLS gives both. Each message key is consumed after decryption — compromising one device doesn't reveal past traffic. After the next key rotation, compromise doesn't expose future traffic either.
The server never persists sender_id for encrypted messages. Every encrypted row lands in the database with sender_id = NULL; real identity lives inside the MLS-encrypted payload and only chat members learn it after decryption. A subpoena or breach against the server yields a timeline of anonymous messages per chat — not a labelled who-said-what.
Group chat names are sealed with a key derived from the MLS exporter (rotates every epoch). File names and MIME types ride inside the MLS payload; the blob row on disk stores only the placeholder "encrypted" and application/octet-stream. The server cannot tell a "Q3 Financial Results.pdf" from a meme.
Messages pad to 64 B / 256 B / 1 KiB / 4 KiB / 16 KiB / 64 KiB; files pad to 32 KiB / 128 KiB / 512 KiB / 2 MiB / 8 MiB / 32 MiB / 64 MiB. Traffic analysis sees a bucket, not an exact byte count — dozens-to-thousands of distinct plaintexts collapse into the same observable size.
Each install (browser, tab, phone PWA) generates its own client_device_id + Ed25519 + X25519 keypair and holds an independent LeafNode in the MLS tree. Revoking a device (from Settings → Your Devices) cascades on the server: its sessions, KeyPackage pool, group state and pending handshakes are wiped in one transaction. A lost device can't retroactively decrypt anything new in the group.
When a fresh device registers, any already-synced device of the same account packages the chat's plaintext history and seals it for the new device's X25519 public key (libsodium crypto_box_seal). The server relays an opaque ciphertext; only the target device can unseal. Bundles expire after 24 hours.
All MLS private material lives inside an isolated Web Worker with its own IndexedDB. A cross-site scripting flaw in the main page cannot reach group keys or device signing keys; the worker only exposes encrypt / decrypt / rotate RPCs.
Every sensitive IndexedDB record — MLS group state, decrypted message cache, PathSecrets — is wrapped in XChaCha20-Poly1305 under a master key that only exists in memory while the tab is alive. The master key itself is sealed under an Argon2id-derived key from your login password. Close the tab and the key evaporates: the next time you open the PWA, your password re-derives everything. No password, no cache — even with physical access to the browser profile.
Every state-mutating MLS op writes the updated group state to the worker's IndexedDB (wrapped under the vault key) synchronously before returning, and decrypted plaintexts reach the durable cache before the UI renders them. A user who swipes the app off-screen mid-operation loses nothing — the next launch rehydrates exactly the state the ratchet held.
Admin actions are logged only to ephemeral structured logs — no database table records "admin X blocked user Y at time Z". When the operator winds an instance down, log truncation removes the trace. Deliberate: a durable audit trail is a forensic liability the moment a server is seized.
A single MLS group spans peer boundaries. When alice@acme.talkee.eu adds bob@partner-corp.eu to a chat, both clients negotiate a post-quantum group key through MLS — the Welcome message carrying Bob's enrolment into the tree travels as a signed mls_welcome envelope between the two servers, fan-outs locally to every Bob device, and only a Bob-controlled browser (not acme's server, not partner-corp's server) can open it. Every subsequent message is ChaCha20-Poly1305 under a group key neither operator ever sees. Compromising one peer — or both — leaks nothing beyond routing metadata.
Key enablers: KeyPackages for remote users are fetched over a signed GET /s2s/mls/keybundle (Ed25519 per-request authentication, one KeyPackage per Bob device), chat identifiers are harmonised so both peers address the same MLS group by the same id, and Commit envelopes propagate across domains for ratchet rotation. The wire format is envelope-typed and each is retried from a durable inbox with idempotent handlers — a peer that's down for minutes doesn't drop a single Welcome or Commit on reconnect.
Every talkee instance has its own Ed25519 signing key, persisted on disk at first boot and never regenerated across redeploys. Peers pin that key the first time they see it; subsequent connections verify every envelope against the pinned key. A newly-deployed backend container keeps talking to the same federation graph — no re-bootstrap storms, no trust reset — and a malicious operator trying to swap an instance's identity silently will trip an "invalid signature" mismatch on every peer it talks to.
Built on RFC 9420 (MLS), FIPS 203 (ML-KEM), RFC 7748 (X25519), RFC 8032 (Ed25519), RFC 9106 (Argon2id), IETF XChaCha20-Poly1305, BIP-39 recovery mnemonics. No proprietary cryptography, no security through obscurity. Everything is peer-reviewed, library-backed, and public.
Каждая MLS-группа устанавливает shared secret через ML-KEM-1024 (FIPS 203) в дополнение к классическому X25519. Даже если атакующий сохранит сегодняшний трафик и сломает X25519 на будущем квантовом компьютере («harvest now, decrypt later»), ML-KEM остаётся стойким против всех известных классических и квантовых атак.
MLS обеспечивает и то, и другое. Ключ каждого сообщения уничтожается после расшифровки — компрометация одного устройства не раскрывает прошлый трафик. После следующей ротации ключа она же не раскрывает и будущий.
Сервер никогда не сохраняет sender_id для зашифрованных сообщений. Каждая зашифрованная строка попадает в БД с sender_id = NULL; реальный отправитель живёт внутри MLS-payload и виден только участникам чата после расшифровки. Запрос от регулятора или утечка БД дадут анонимную timeline сообщений на чат — не размеченный кто-кому-написал.
Имена групповых чатов запечатаны ключом из MLS exporter (ротируется на каждый epoch). Имена файлов и MIME-типы едут внутри MLS-payload; в БД blob хранит только placeholder "encrypted" и application/octet-stream. Сервер не отличит «Q3 Финансовый отчёт.pdf» от мема.
Сообщения: 64 B / 256 B / 1 KiB / 4 KiB / 16 KiB / 64 KiB; файлы: 32 KiB / 128 KiB / 512 KiB / 2 MiB / 8 MiB / 32 MiB / 64 MiB. Traffic analysis видит корзину, а не точный размер — десятки-тысячи разных plaintext'ов схлопываются в один наблюдаемый размер.
Каждая установка (браузер, вкладка, PWA на телефоне) генерирует свой client_device_id + пары Ed25519 + X25519 и имеет независимый LeafNode в MLS-дереве. Revoke устройства (Настройки → Ваши устройства) каскадит на сервере: его sessions, пул KeyPackage, group state и pending handshakes удаляются в одной транзакции. Потерянное устройство ничего нового в группе расшифровать не сможет.
Когда регистрируется новое устройство, любое уже синхронизированное устройство того же аккаунта упаковывает plaintext-историю чата и запечатывает её под X25519-ключ нового устройства (libsodium crypto_box_seal). Сервер переправляет непрозрачный ciphertext; расшифровать может только целевое устройство. Bundles истекают через 24 часа.
Весь приватный MLS-материал живёт внутри изолированного Web Worker со своей IndexedDB. XSS-дыра в основной странице не добирается до групповых или подписных ключей; worker экспонирует только encrypt / decrypt / rotate RPC.
Каждая чувствительная IndexedDB-запись — MLS group state, кеш расшифрованных сообщений, PathSecrets — обёрнута в XChaCha20-Poly1305 ключом, который живёт только в памяти, пока жива вкладка. Сам ключ запечатан под KEK из Argon2id от вашего пароля логина. Закрыл вкладку — ключ испарился: при следующем открытии PWA пароль пересчитает всё заново. Нет пароля — нет кеша, даже при физическом доступе к профилю браузера.
Каждая state-mutating операция MLS пишет обновлённое состояние группы в IndexedDB (завёрнутое под vault-ключ) синхронно перед возвратом, а расшифрованные plaintext'ы попадают в durable cache до того как UI их отрисует. Пользователь, который свайпнул приложение посреди операции, ничего не теряет — следующий запуск восстанавливает ровно то состояние ratchet'а, которое было.
Действия админа логируются только в эфемерные структурированные логи — в БД нет таблицы с записью «админ X заблокировал юзера Y в момент Z». Когда оператор сворачивает инстанс, очистка логов удаляет след. Намеренно: durable audit trail — forensic liability в момент, когда сервер изымают.
Одна MLS-группа живёт поверх границ инстансов. Когда alice@acme.talkee.eu добавляет bob@partner-corp.eu в чат, оба клиента договариваются о post-quantum групповом ключе через MLS — Welcome-сообщение с enrolment'ом Боба в дерево летит подписанным envelope'ом mls_welcome между двумя серверами, фан-аутится на каждое устройство Боба локально, а открыть его может только браузер Боба (не сервер acme, не сервер partner-corp). Каждое последующее сообщение — ChaCha20-Poly1305 под групповым ключом, которого ни один оператор никогда не видит. Компрометация одного peer'а — или обоих сразу — не даёт ничего кроме routing metadata.
Что это делает возможным: KeyPackages удалённых пользователей подгружаются через подписанный GET /s2s/mls/keybundle (Ed25519-аутентификация на каждый запрос, по одному KeyPackage на устройство Боба), chat_id гармонизирован — оба пира адресуют одну MLS-группу по одному id, Commit-envelope'ы пробрасываются между доменами для ротации ratchet'а. Формат wire — типизированный envelope, каждый ретраится из durable inbox с идемпотентными handler'ами: пир, который был недоступен несколько минут, на восстановлении не теряет ни одного Welcome или Commit.
У каждого инстанса talkee свой Ed25519-ключ подписи, записанный на диск при первом старте и никогда не регенерируемый при передеплое. Peer'ы пиннят этот ключ при первом handshake и дальше верифицируют каждый envelope против него. Свежий рестарт контейнера продолжает разговор с той же federation-сетью — никаких re-bootstrap штормов, никакого trust-reset — а оператор, пытающийся тихо подменить identity инстанса, поймает "invalid signature" на каждом пире.
Построено на RFC 9420 (MLS), FIPS 203 (ML-KEM), RFC 7748 (X25519), RFC 8032 (Ed25519), RFC 9106 (Argon2id), IETF XChaCha20-Poly1305, BIP-39. Никакой проприетарной крипты, никакой security-through-obscurity. Всё peer-reviewed, в открытых библиотеках, публично.
| 1-on-1 encrypted messaging (MLS, post-quantum) | Available |
| Group encrypted messaging (MLS, any size) | Available |
| Sealed sender (server doesn't learn sender) | Available |
| Encrypted group names | Available |
| Encrypted file names & MIME types | Available |
| Length-bucketed padding (messages + files) | Available |
| Self-destruct messages (TTL + burn-on-read) | Available |
| Multi-device (per-device LeafNodes, revoke cascade) | Available |
| Encrypted history sync for new devices | Available |
| Voice calls (P2P, SRTP + MLS-exporter signalling) | Available |
| Video calls (P2P, SRTP + MLS-exporter signalling) | Available |
| End-to-end encrypted file sharing | Available |
| Frame-level E2E for calls (Insertable Streams) | Available |
| Web Push notifications | Available |
| Progressive Web App (PWA, offline, swipe-safe) | Available |
| Encryption at rest — local IDB vault (Argon2id + XChaCha20-Poly1305) | Available |
| Multi-tenant federation (mTLS + Ed25519, persistent identity) | Available |
| Cross-instance E2E messaging (MLS groups across federated peers) | Available |
| Signed server-to-server KeyPackage fetch (/s2s/mls/keybundle) | Available |
| Group voice / video calls (> 2 participants) | Planned |
| Native iOS / Android apps | Planned |
| 1-на-1 шифрованные чаты (MLS, post-quantum) | Доступно |
| Групповые шифрованные чаты (MLS, любой размер) | Доступно |
| Sealed sender (сервер не знает отправителя) | Доступно |
| Зашифрованные имена групп | Доступно |
| Зашифрованные имена файлов и MIME-типы | Доступно |
| Padding длины по корзинам (сообщения + файлы) | Доступно |
| Self-destruct сообщения (TTL + burn-on-read) | Доступно |
| Multi-device (LeafNode на устройство, revoke-каскад) | Доступно |
| Зашифрованная history-sync для новых устройств | Доступно |
| Голосовые звонки (P2P, SRTP + сигналинг через MLS exporter) | Доступно |
| Видеозвонки (P2P, SRTP + сигналинг через MLS exporter) | Доступно |
| End-to-end шифрование файлов | Доступно |
| Frame-level E2E для звонков (Insertable Streams) | Доступно |
| Web Push уведомления | Доступно |
| Progressive Web App (PWA, offline, swipe-safe) | Доступно |
| Шифрование at rest — локальный IDB vault (Argon2id + XChaCha20-Poly1305) | Доступно |
| Multi-tenant federation (mTLS + Ed25519, persistent identity) | Доступно |
| Cross-instance E2E сообщения (MLS-группы между federated peer'ами) | Доступно |
| Signed server-to-server KeyPackage fetch (/s2s/mls/keybundle) | Доступно |
| Групповые голос / видео (> 2 участников) | В планах |
| Нативные приложения iOS / Android | В планах |
Conversation graph is visible to the home server. Sealed-sender hides who sent each message inside a chat, and bucketed padding hides exact size, but the fact that "user A is a member of chat X" is still stored server-side — MLS commits have to be routed somewhere. If graph confidentiality is in your threat model, layer Talkee over Tor or a mix network.
TURN relay sees IP pairs — but it's always yours. When direct peer-to-peer fails, the bundled coturn (shipped in the same Docker compose as the home server) learns both endpoints' public IPs. Talkee never falls back to a third-party TURN, so address metadata never leaves the self-hosted perimeter. Media content is opaque either way — AES-256-GCM under an MLS-derived key before SRTP.
Walk-away-from-unlocked-tab protection is the OS's job. While the tab is open and you're logged in, the master key is in the page's JS memory — someone with physical access to the unlocked browser could DevTools their way in. Talkee deliberately does not try to auto-lock the browser (that's a bad UX layer for it). Close the tab, lock your OS screen, or sign out when you step away. What Talkee does cover: once the tab is closed, the master key is gone and IDB-at-rest is opaque ciphertext until you re-type your password.
Conversation graph виден home-серверу. Sealed-sender скрывает, кто отправил каждое сообщение внутри чата, а bucketed padding — точный размер, но факт «user A состоит в chat X» остаётся на сервере — MLS-коммиты должны куда-то маршрутизироваться. Если скрытие графа общения входит в вашу threat-модель — сочетайте Talkee с Tor или mix network.
TURN relay видит пары IP — но он всегда ваш. Когда прямой peer-to-peer невозможен, coturn из того же Docker-compose бандла, что и home-сервер, узнаёт публичные IP обеих сторон. На публичный / сторонний TURN Talkee не переключается никогда, так что address-метаданные не покидают self-hosted периметр. Содержимое медиа в любом случае — шифротекст AES-256-GCM под MLS-ключом до SRTP.
Защита от «ушёл от разблокированной вкладки» — задача ОС, а не приложения. Пока вкладка открыта и вы залогинены, master key лежит в JS-памяти страницы — кто-то с физическим доступом к разблокированному браузеру через DevTools до него дотянется. Talkee намеренно не пытается auto-lock'ать браузер (это плохой слой для такой защиты). Закрыл вкладку, залочил экран ОС или нажал Sign Out, когда уходишь. Что Talkee покрывает: как только вкладка закрыта, master key исчез, и IDB на диске — непрозрачный шифротекст до тех пор, пока ты снова не введёшь пароль.