Launch Запустить

Talkee — Security & Encryption

Federated end-to-end encrypted messenger. Your messages, your server, your keys.

Talkee — безопасность и шифрование

Федеративный 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'ом длины сервер даже не знает кто написал какое сообщение какого размера. Всё шифрование происходит на вашем устройстве до того, как что-либо покидает его.

What Talkee Protects

Что защищает Talkee

DataProtectionStatus
Private messages (P2P)MLS (RFC 9420), post-quantum ciphersuitePQ-Encrypted
Group messagesMLS (RFC 9420) — same protocol, N membersPQ-Encrypted
Message sender identitySealed inside MLS payload; server stores no sender_idSealed
Group chat namesMLS-exporter-derived key, ciphertext on serverEncrypted
File names, MIME typesTravel inside MLS payload; server sees only "encrypted"Encrypted
Message & file lengthBucketed padding hides exact byte countsPadded
Voice & video callsSRTP / DTLS 1.2 + Insertable Streams frame encryptionEncrypted
Call signalling (SDP, ICE)MLS exporter, key rotates with group epochEncrypted
Files & imagesXChaCha20-Poly1305 (client-side before upload)Encrypted
History sync bundles (new device)libsodium sealed_box to device's X25519 pubkeyEncrypted
Reactions (who reacted with which emoji)Sealed per-chat under an MLS-exporter-derived key; server stores only opaque bytesSealed
Read state (who has read what)Never reaches the server — per-chat unread counters live client-side in the sealed IDB vaultClient-only
Password at rest (server)Argon2id hash — no encrypted-blob decrypt-check trickHashed
Local IndexedDB (MLS state + message cache)XChaCha20-Poly1305 under an Argon2id-derived password key; MK only in memory while the tab is aliveEncrypted at rest
Federation envelopesmTLS + Ed25519 signature over payloadEncrypted + signed
ДанныеЗащитаСтатус
Личные сообщения (P2P)MLS (RFC 9420), post-quantum ciphersuitePQ-шифровано
Групповые сообщенияMLS (RFC 9420) — тот же протокол, N участниковPQ-шифровано
Личность отправителяЗапечатана в MLS payload; сервер не хранит sender_idSealed
Имена групповых чатовКлюч из 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 envelopesmTLS + Ed25519 подпись payloadШифр + подпись

Cryptographic Stack

Криптографический стек

PurposeAlgorithm / ConstructionStandard
Group messagingMLS, ciphersuite MLS_256_MLKEM1024_CHACHA20POLY1305_SHA512_Ed25519 (61452)RFC 9420
Post-quantum KEMML-KEM-1024 (Kyber-1024)FIPS 203
Key exchange (history-sync sealed_box)X25519 (Curve25519 ECDH) + XSalsa20-Poly1305RFC 7748, libsodium
Symmetric encryption (files, at-rest)XChaCha20-Poly1305IETF draft
Authenticated encryption (MLS application)ChaCha20-Poly1305RFC 8439
Call-signalling keyMLS exporter → HKDF("talkee-webrtc-signaling-v1", 32 bytes)RFC 9420 §8
Reaction-sealing keyMLS exporter → "talkee-reaction-v1" (32 bytes); epoch-rotatingRFC 9420 §8
Key derivationHKDF-SHA512RFC 5869
Server-side password hashingArgon2id (memory-hard) — genuine hash, not an encrypted-blob checkRFC 9106
Local IDB vault — key derivationArgon2id 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 wrapXChaCha20-Poly1305 seal of a random 32-byte MK under the Argon2id KEK (24-byte nonce, 16-byte tag)IETF draft
Local IDB vault — payload encryptionXChaCha20-Poly1305 with fresh 24-byte random nonce per IDB row; MK held in memory onlyIETF draft
Digital signaturesEd25519 (device identity, federation envelopes)RFC 8032
HashingBLAKE3 (session tokens, recovery-key hash)BLAKE3 spec
Recovery mnemonicBIP-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.2RFC 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 AADNIST SP 800-38D
Media-frame key derivationMLS 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 TraefikRFC 8446
НазначениеАлгоритм / конструкцияСтандарт
Групповое сообщениеMLS, ciphersuite MLS_256_MLKEM1024_CHACHA20POLY1305_SHA512_Ed25519 (61452)RFC 9420
Post-quantum KEMML-KEM-1024 (Kyber-1024)FIPS 203
Обмен ключами (history-sync sealed_box)X25519 (Curve25519 ECDH) + XSalsa20-Poly1305RFC 7748, libsodium
Симметричное шифрование (файлы, at-rest)XChaCha20-Poly1305IETF draft
Authenticated encryption (MLS application)ChaCha20-Poly1305RFC 8439
Ключ сигналинга звонковMLS exporter → HKDF("talkee-webrtc-signaling-v1", 32 байта)RFC 9420 §8
Ключ запечатывания реакцийMLS exporter → "talkee-reaction-v1" (32 байта); ротируется с epochRFC 9420 §8
Key derivationHKDF-SHA512RFC 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 keyXChaCha20-Poly1305 seal случайного 32-байтового MK под Argon2id KEK (24-байтовый nonce, 16-байтовый tag)IETF draft
Локальный IDB vault — шифрование payloadXChaCha20-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.2RFC 3711, RFC 6347
Медиа звонков (E2E frame layer)AES-256-GCM для каждого закодированного кадра, 12-байтовый IV (4 байта salt + 8 байт монотонный счётчик), 16-байтовый tag, заголовок кодека как AADNIST SP 800-38D
Деривация ключа медиа-кадровMLS exporter → HKDF-SHA256 (salt talkee-media-e2e-v1, info talkee-frame-key, 32 байта)RFC 5869
TLS (транспорт)TLS 1.3 — навязывается браузером и TraefikRFC 8446

How It Works

Как это работает

Account Creation

Создание аккаунта

1
Device identityEvery install (PWA on your laptop, PWA on your phone, each browser tab) generates a unique client_device_id + Ed25519 and X25519 keypairs locally. Private keys never leave the device.
2
Password hashingThe server stores only an Argon2id hash of your password — memory-hard, resistant to GPU / ASIC brute-force. No password, no password-derived wrapping key, nothing reversible lands in the database.
3
Recovery mnemonicA 12-word BIP-39 phrase is shown once on screen. The server stores only its hash; if you lose your password, the mnemonic proves possession and lets you set a new one. Server cannot derive the mnemonic from the hash.
4
Per-device MLS KeyPackage poolThe MLS worker generates ~10 KeyPackages for this device and uploads them (ML-KEM-1024 + ChaCha20-Poly1305 + Ed25519 post-quantum ciphersuite). Others can invite this specific device into a group instantly. The pool refills every 12 hours and when it drops below 3.
1
Identity устройстваКаждая установка (PWA на ноуте, PWA на телефоне, отдельная вкладка) генерирует уникальный client_device_id + пары Ed25519 и X25519 локально. Приватные ключи никогда не покидают устройство.
2
Хэш пароляВ БД попадает только Argon2id-hash пароля — memory-hard, устойчивый к brute-force на GPU / ASIC. Ни пароль, ни производный от пароля wrapping-ключ, ни что-либо обратимое на сервере не хранится.
3
Recovery-мнемоникаФраза из 12 слов BIP-39 показывается один раз на экране. Сервер хранит только её hash; если пароль утерян, мнемоника доказывает владение и даёт установить новый. Из hash'а мнемонику вернуть нельзя.
4
Пул MLS KeyPackage для устройстваMLS worker генерирует ~10 KeyPackage для этого устройства и загружает их (post-quantum ciphersuite: ML-KEM-1024 + ChaCha20-Poly1305 + Ed25519). Другие могут мгновенно пригласить именно это устройство в группу. Пул пополняется каждые 12 часов и при остатке меньше 3.

Sending a Message (1-on-1 or Group)

Отправка сообщения (1-на-1 или группа)

1
MLS group establishmentWhen a chat is created, the initiator fetches one KeyPackage per device of every member and builds the MLS group locally. Every LeafNode gets its own ratchet; Welcomes are addressed precisely — never broadcast.
2
Sealed-sender envelopeThe client wraps the plaintext in {_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.
3
Length padding + MLS application messageThe worker pads the plaintext to the next bucket (64 B → 256 B → 1 KiB → 4 KiB → 16 KiB → 64 KiB), then derives a per-message key from the ratchet and encrypts. Forward secrecy and post-compromise security are guaranteed — even the sender cannot re-decrypt an old message.
4
Server relay (opaque)The server routes the ciphertext to each member's devices, stores it for up to 7 days (purge_stale_ciphertext then nulls it), and authenticates neither content nor sender. MLS keyschedule prevents forgery.
5
Adding / removing a memberAdd → Commit advances the MLS epoch; new member can decrypt from that point, not before. Remove → Commit removes the LeafNode and re-keys the group; the departing member loses access on the next Commit.
1
Создание MLS-группыПри создании чата инициатор запрашивает по одному KeyPackage на каждое устройство каждого участника и локально строит MLS-группу. У каждого LeafNode — свой ratchet; Welcome адресованы точечно, не broadcast'ом.
2
Sealed-sender envelopeКлиент оборачивает plaintext в {_v:1, sender, text, file_key?, …} и шифрует это MLS'ом. Сервер получает ciphertext без какой-либо привязки к отправителю и сохраняет строку с sender_id = NULL. Кто написал — узнают только получатели.
3
Padding длины + MLS application messageWorker паддит plaintext до следующей корзины (64 B → 256 B → 1 KiB → 4 KiB → 16 KiB → 64 KiB), затем выводит per-message ключ из ratchet'а и шифрует. Forward secrecy и post-compromise security гарантированы — даже отправитель не может повторно расшифровать старое сообщение.
4
Relay через сервер (opaque)Сервер маршрутизирует ciphertext на устройства всех участников, хранит до 7 дней (purge_stale_ciphertext потом nullит), не верифицирует ни содержимое, ни отправителя. MLS keyschedule препятствует подделке.
5
Добавление / удаление участникаAdd → Commit сдвигает MLS epoch; новый участник расшифровывает только с этого момента. Remove → Commit удаляет LeafNode и перегенерирует ключ группы; покидающий участник теряет доступ на следующем Commit.

Voice & Video Calls

Голосовые и видеозвонки

1
MLS-only signalling E2ESDP offers and ICE candidates are encrypted before hitting the server with a key derived from the chat's MLS exporter (label 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.
2
Epoch-rotating keyBecause the exporter changes every MLS epoch (e.g. on member add / remove), the signalling key rotates automatically. A one-time compromise of the key doesn't unlock past or future signalling.
3
Transport securityAudio and video run over SRTP with DTLS 1.2 key exchange, peer-to-peer when possible. A TURN relay is used only when NAT / firewall rules force it — and the relay is always your own coturn, shipped in the same Docker compose bundle as the home server. Talkee does not fall back to any public / third-party TURN.
4
AES-256-GCM frame E2E on top of SRTPEvery encoded media frame is wrapped in AES-256-GCM before SRTP ever sees it, using a 12-byte IV (4-byte per-call salt + 8-byte monotonic counter) and a 16-byte auth tag. The codec header (1 byte Opus / 10 bytes VP8/H264) is kept as AAD so routers and decoders can still parse the frame. The key comes from the MLS exporter via HKDF-SHA256 (salt 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.
1
Только MLS сигналинг E2ESDP offers и ICE candidates шифруются до отправки на сервер ключом из MLS exporter (label talkee-webrtc-signaling-v1). Static-DH fallback удалён — запуск звонка в свежем чате сначала синхронно поднимает MLS-группу (~200 мс один раз), затем каждый следующий звонок идёт по готовому пути.
2
Ключ, ротирующийся с epochПоскольку exporter меняется каждый MLS epoch (напр. при add / remove участника), ключ сигналинга ротируется автоматически. Разовая компрометация ключа не раскрывает ни прошлый, ни будущий сигналинг.
3
Transport securityАудио и видео идут по SRTP с обменом ключами через DTLS 1.2, peer-to-peer где возможно. TURN relay поднимается только когда NAT / firewall вынуждает — и это всегда свой coturn, поставляемый в том же Docker-compose бандле, что и home-сервер. Fallback на публичные / сторонние TURN отсутствует.
4
AES-256-GCM frame E2E поверх SRTPКаждый закодированный медиа-кадр оборачивается в AES-256-GCM до того, как его увидит SRTP: 12-байтовый IV (4 байта per-call salt + 8 байт монотонный счётчик), 16-байтовый auth-tag. Заголовок кодека (1 байт Opus / 10 байт VP8/H264) остаётся в AAD, чтобы роутеры и декодеры могли парсить кадр. Ключ берётся из MLS exporter через HKDF-SHA256 (salt talkee-media-e2e-v1, info talkee-frame-key) и подключается через Insertable Streams API. Скомпрометированный TURN или даже вредоносный SFU видят только шифротекст — SRTP+DTLS это внешний конверт, а реальную конфиденциальность даёт AES-GCM.

Local IDB Vault — encryption at rest

Локальный IDB Vault — шифрование at-rest

1
KEK from your login passwordAt login, while the password is still in memory for a single function scope, the client runs Argon2id (memory-hard, 64 MiB, 3 iterations) over the password plus a per-install 16-byte salt to derive a 32-byte KEK. The password itself is never written to disk and leaves memory the moment the function returns.
2
Master key sealed under KEKA random 32-byte master key (MK) is generated once per device. MK is sealed with XChaCha20-Poly1305 under the KEK and stored in a small IDB record alongside the salt. The KEK is zeroed from memory; only the MK stays, and only in RAM.
3
Every sensitive IDB write is wrappedDecrypted message cache, MLS group state, LeafNode private keys, PathSecrets, ratchet state — all go through 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.
4
Close the tab and the key is goneMK lives only in the page's JS heap. Closing the tab, closing the browser, or rebooting wipes it. The next time you open the PWA, you enter your password — Argon2id re-derives the KEK, unseals MK, and the cache becomes readable again. No password, no data.
5
Sign out = full nuke“Sign out” in Settings closes every IDB handle, deletes the vault record, the MLS state DB, and the message cache DB, and then reloads. Next login starts from a blank device and re-syncs history from other devices via sealed bundles.
1
KEK из пароля логинаПри логине, пока пароль ещё в памяти в скоупе одной функции, клиент прогоняет Argon2id (memory-hard, 64 МиБ, 3 итерации) над паролем + per-install 16-байтовый salt и получает 32-байтовый KEK. Сам пароль никогда не пишется на диск и покидает память, как только функция вернула управление.
2
Master key sealed под KEKСлучайный 32-байтовый master key (MK) генерируется один раз на устройство. MK запечатывается XChaCha20-Poly1305 под KEK и сохраняется в маленькой IDB-записи вместе с salt. KEK обнуляется в памяти; остаётся только MK — и только в RAM.
3
Любая чувствительная IDB-запись шифруетсяКеш расшифрованных сообщений, MLS group state, приватники LeafNode, PathSecrets, ratchet state — всё проходит через wrap(MK) до попадания на диск: свежий 24-байтовый nonce на запись, 16-байтовый auth-tag, никакого key reuse. Атакующий, снявший дамп IDB, видит только шифротекст.
4
Закрыл вкладку — ключа нетMK живёт только в JS-heap страницы. Закрыл вкладку, закрыл браузер, перезагрузил ОС — он стирается. При следующем открытии PWA ты вводишь пароль, Argon2id пересчитывает KEK, распечатывает MK, и кеш снова читаем. Нет пароля — нет данных.
5
Sign out = полный nuke«Sign out» в Settings закрывает все IDB-handle, удаляет vault-запись, MLS state DB и message cache DB, потом перезагружает страницу. Следующий логин стартует с чистого устройства и подтягивает историю с других устройств через sealed bundles.

Multi-Device

Multi-Device

1
Per-device LeafNodeEvery install of the same account holds its own MLS LeafNode inside every chat it's a member of. Credentials embed user_id:device_id so two devices of the same user are distinguishable to the MLS tree.
2
Add-proposal rolloutWhen a fresh device registers, any already-synced device of the same user automatically adds the new LeafNode to every existing group via a standard MLS Commit. No pairing codes, no QR — password login is the authenticator.
3
Revocation cascadeRevoking a device from Settings triggers an 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.
1
LeafNode на каждое устройствоКаждая установка аккаунта держит собственный MLS LeafNode внутри каждого чата, где она состоит. Credentials содержат user_id:device_id — два устройства одного пользователя различимы для MLS-дерева.
2
AddProposal rolloutКогда регистрируется новое устройство, любое уже синхронизированное устройство того же пользователя автоматически добавляет новый LeafNode во все существующие группы обычным MLS Commit. Никаких pairing-кодов, никакого QR — аутентификатор это пароль.
3
Каскад revokeRevoke устройства из Settings триггерит ON DELETE CASCADE по sessions, пулу KeyPackage, MLS group state и pending handshakes — одна транзакция, один DB commit. Отозванное устройство получает мгновенный WS-kick и logout.

Encrypted History Sync

Зашифрованная синхронизация истории

1
Package + sealA device that already holds the chat's plaintext cache packages the recent messages into a JSON bundle and seals it with crypto_box_seal against the new device's X25519 public key (libsodium anonymous-sender AEAD).
2
Server is a dumb pipeThe sealed bundle travels through /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.
3
Unseal & mergeThe new device unseals with its private X25519 key and merges the plaintext into its local cache. History appears in seconds after first login; MLS forward-secrecy is preserved on the wire because past MLS ciphertexts are still undecryptable.
1
Package + sealУстройство, у которого уже есть plaintext-кэш чата, упаковывает последние сообщения в JSON-bundle и запечатывает его crypto_box_seal под X25519-pubkey нового устройства (anonymous-sender AEAD libsodium).
2
Сервер — тупая трубаЗапечатанный bundle идёт через /v1/device-sync/bundle с лимитом 4 MiB. Сервер видит только (from_device_id, to_device_id, chat_id, непрозрачный ciphertext). Bundles истекают через 24 часа через purge_stale_sync_bundles.
3
Unseal & mergeНовое устройство распечатывает своим X25519 приватником и мёрджит plaintext в свой локальный кэш. История появляется за секунды после первого логина; MLS forward-secrecy на wire сохранена, потому что прошлые MLS ciphertext'ы всё равно нерасшифруемы.

What the Server Knows

Что знает сервер

We believe in transparency. Here is exactly what the server can and cannot see:

Мы за прозрачность. Вот точный список того, что сервер может и не может увидеть:

DataServer can see?Why
Message contentNoMLS-encrypted on your device
File contentNoXChaCha20 before upload
File names & MIME typesNoInside the MLS payload; server stores the placeholder "encrypted"
Group chat namesNoSealed with an MLS-exporter key before upload
Message senderNoSealed-sender: sender_id is NULL for every encrypted row; real identity lives inside the MLS payload
Exact message / file sizeNoPlaintext is padded to size buckets before encryption
Call audio / videoNoSRTP peer-to-peer; frame E2E on supported browsers
Call signalling (SDP / ICE)NoSealed with the MLS exporter secret — rotates every epoch
Your passwordNoOnly an Argon2id hash ever reaches the database
Your recovery mnemonicNoNever transmitted — only shown once on your screen; server stores a hash for proof-of-possession
Device private keysNoGenerated locally, held in IndexedDB + MLS worker memory
Group membership rosterYesRequired for delivery fan-out
Who chats with whomYesRequired for routing (chat metadata: ids, timestamps)
TimestampsYesRequired for ordering
Your username & domainYesRequired for account lookup
Device public keys (Ed25519, X25519)YesNeeded 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 он больше не знает кто именно из участников написал какое сообщение — чат для сервера становится анонимным набором, а не размеченной лентой.

Key Properties

Ключевые свойства

Post-quantum protection (ML-KEM-1024)

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.

Forward secrecy & post-compromise security

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.

Sealed sender

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.

Encrypted group names & file names

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.

Length-bucketed padding

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.

Per-device MLS LeafNodes

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.

Encrypted history sync

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.

Web Worker key isolation

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.

Encryption at rest — local IDB vault

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.

PWA swipe-safe persistence

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.

No persistent admin audit trail

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.

Cross-instance end-to-end encryption via MLS federation

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.

Persistent, pinned server identities

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.

Open, standard cryptography

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.

Post-quantum защита (ML-KEM-1024)

Каждая MLS-группа устанавливает shared secret через ML-KEM-1024 (FIPS 203) в дополнение к классическому X25519. Даже если атакующий сохранит сегодняшний трафик и сломает X25519 на будущем квантовом компьютере («harvest now, decrypt later»), ML-KEM остаётся стойким против всех известных классических и квантовых атак.

Forward secrecy и post-compromise security

MLS обеспечивает и то, и другое. Ключ каждого сообщения уничтожается после расшифровки — компрометация одного устройства не раскрывает прошлый трафик. После следующей ротации ключа она же не раскрывает и будущий.

Sealed sender

Сервер никогда не сохраняет sender_id для зашифрованных сообщений. Каждая зашифрованная строка попадает в БД с sender_id = NULL; реальный отправитель живёт внутри MLS-payload и виден только участникам чата после расшифровки. Запрос от регулятора или утечка БД дадут анонимную timeline сообщений на чат — не размеченный кто-кому-написал.

Зашифрованные имена групп и файлов

Имена групповых чатов запечатаны ключом из MLS exporter (ротируется на каждый epoch). Имена файлов и MIME-типы едут внутри MLS-payload; в БД blob хранит только placeholder "encrypted" и application/octet-stream. Сервер не отличит «Q3 Финансовый отчёт.pdf» от мема.

Padding длины по корзинам

Сообщения: 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'ов схлопываются в один наблюдаемый размер.

MLS LeafNode на каждое устройство

Каждая установка (браузер, вкладка, PWA на телефоне) генерирует свой client_device_id + пары Ed25519 + X25519 и имеет независимый LeafNode в MLS-дереве. Revoke устройства (Настройки → Ваши устройства) каскадит на сервере: его sessions, пул KeyPackage, group state и pending handshakes удаляются в одной транзакции. Потерянное устройство ничего нового в группе расшифровать не сможет.

Зашифрованная синхронизация истории

Когда регистрируется новое устройство, любое уже синхронизированное устройство того же аккаунта упаковывает plaintext-историю чата и запечатывает её под X25519-ключ нового устройства (libsodium crypto_box_seal). Сервер переправляет непрозрачный ciphertext; расшифровать может только целевое устройство. Bundles истекают через 24 часа.

Изоляция ключей в Web Worker

Весь приватный MLS-материал живёт внутри изолированного Web Worker со своей IndexedDB. XSS-дыра в основной странице не добирается до групповых или подписных ключей; worker экспонирует только encrypt / decrypt / rotate RPC.

Шифрование at-rest — локальный IDB vault

Каждая чувствительная IndexedDB-запись — MLS group state, кеш расшифрованных сообщений, PathSecrets — обёрнута в XChaCha20-Poly1305 ключом, который живёт только в памяти, пока жива вкладка. Сам ключ запечатан под KEK из Argon2id от вашего пароля логина. Закрыл вкладку — ключ испарился: при следующем открытии PWA пароль пересчитает всё заново. Нет пароля — нет кеша, даже при физическом доступе к профилю браузера.

Swipe-safe персистентность в PWA

Каждая state-mutating операция MLS пишет обновлённое состояние группы в IndexedDB (завёрнутое под vault-ключ) синхронно перед возвратом, а расшифрованные plaintext'ы попадают в durable cache до того как UI их отрисует. Пользователь, который свайпнул приложение посреди операции, ничего не теряет — следующий запуск восстанавливает ровно то состояние ratchet'а, которое было.

Нет persistent audit-trail администраторских действий

Действия админа логируются только в эфемерные структурированные логи — в БД нет таблицы с записью «админ X заблокировал юзера Y в момент Z». Когда оператор сворачивает инстанс, очистка логов удаляет след. Намеренно: durable audit trail — forensic liability в момент, когда сервер изымают.

Cross-instance end-to-end шифрование через MLS-федерацию

Одна 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.

Персистентные, пиннинг'уемые identity серверов

У каждого инстанса 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, в открытых библиотеках, публично.

Capabilities

Возможности

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 namesAvailable
Encrypted file names & MIME typesAvailable
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 devicesAvailable
Voice calls (P2P, SRTP + MLS-exporter signalling)Available
Video calls (P2P, SRTP + MLS-exporter signalling)Available
End-to-end encrypted file sharingAvailable
Frame-level E2E for calls (Insertable Streams)Available
Web Push notificationsAvailable
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 appsPlanned
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В планах

Honest Caveats

Честные оговорки

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 на диске — непрозрачный шифротекст до тех пор, пока ты снова не введёшь пароль.