
Тот вечер я помню хорошо. Двадцать минут в звонке, пытаясь объяснить человеку как установить VPN. Через пару дней и этот способ тоже закрыли.
Но это была не единственная боль. Простой звонок другу в Москву, переписка с клиентом, групповой чат с командой - всё превратилось в логистическую задачу. Один сидит без VPN, у другого он не работает, третий не может установить нужное приложение. Люди тратят время не на разговор, а на то чтобы вообще выйти на связь.
А потом мой российский номер, к которому были привязаны Telegram и WhatsApp, «сгорел» из-за неактивности внутри РФ. Оператор просто выставил его на продажу. Через неделю кто-то купил эту симку и начал методично пытаться войти в мои аккаунты.
В какой-то момент стало понятно: это не проблема инструкций. Это проблема архитектуры. Приватная переписка не должна требовать технической подготовки. Телефон - это не вы. Это запись в базе данных оператора, которую можно купить, взломать или заблокировать.
Так появился Mist Messenger. Ниже - как это устроено, что сломалось, и почему некоторые решения были болезненными.
Почему существующие решения не работают
Telegram блокируют. VPN блокируют быстрее, чем пользователи успевают переключиться. Signal недоступен. Google Meet и Zoom работают с перебоями. Каждый раз когда появляется рабочий инструмент - находится способ его закрыть.
Причина простая: все эти инструменты видны. У каждого мессенджера есть сетевой fingerprint - уникальная подпись, по которой системы глубокой инспекции пакетов его опознают. Нативные мессенджеры с кастомными TLS-библиотеками определяются DPI по JA3/JA4 отпечатку мгновенно. Можно усложнять протокол, обфусцировать трафик, менять порты - но сам факт что ты что-то скрываешь уже делает тебя мишенью.
Mist работает в браузере и устанавливается как PWA за 5 секунд. Для систем блокировок его трафик выглядит как обычный сайт: TLS fingerprint - настоящий Chrome или Safari, тот же протокол, никакого характерного паттерна. Внутри при этом - полноценное E2EE шифрование. Невидим для файрвола, приватен для пользователя.
Это не компромисс, а архитектурное решение, которое мы не планировали. Просто повезло с выбором платформы.
Авторизация: 12 слов вместо номера телефона

В Mist аккаунт - это seed-фраза из словаря BIP39. Та же механика, что защищает криптокошельки: 12 случайных слов, из которых математически выводится ваша личность в системе. Генерируется на вашем устройстве, нигде не хранится, никуда не отправляется.
Технически из seed через HMAC-SHA256 детерминированно выводятся два ключа: ключ идентичности (ECDSA P-256) для подписей и ключ шифрования (ECDH P-256) для создания shared secret с каждым собеседником. Это две отдельные пары ключей с разным назначением. Identity key подписывает challenge при логине - Zero-Knowledge Auth: сервер отправляет случайный challenge, клиент подписывает приватным ключом, сервер проверяет подпись публичным. Пароль никогда не передаётся. Encryption key участвует в ECDH key agreement для вычисления shared secret.
Здесь меня справедливо спросят: почему HMAC-SHA256, а не Argon2id? Честный ответ - потому что seed-фраза BIP39 уже имеет 128 бит энтропии (12 слов × log₂(2048) ≈ 132 бита). Для сравнения: перебор 2¹²⁸ комбинаций на всех GPU мира займёт больше времени, чем возраст вселенной. Argon2id нужен, когда пользователь вводит слабый пароль вроде qwerty123. Когда пароль - это 12 случайных слов из словаря в 2048 - key stretching не даёт практической пользы. Тем не менее, Argon2id запланирован на v2 - не потому что это закрывает реальную атаку, а потому что это закрывает вопросы аудиторов.
Non-extractable CryptoKey: ключ, который нельзя украсть
Приватные ключи хранятся как non-extractable CryptoKey через Web Crypto API. Удивительно мало проектов это используют, хотя механизм мощный.
Когда вы создаёте ключ через crypto.subtle.generateKey() с параметром extractable: false, браузер создаёт объект CryptoKey, который можно использовать для криптографических операций (подпись, расшифровка, ECDH deriveBits), но нельзя экспортировать. Вызов crypto.subtle.exportKey() вернёт ошибку. Даже если вредоносный скрипт получит доступ к JavaScript-контексту - он не сможет вытащить приватный ключ.
На практике: XSS-атака может вызвать decryptMessage() и получить расшифрованный текст, но не может украсть сам ключ для офлайн-использования. Без ключа атакующий должен поддерживать активную сессию, а не просто один раз забрать ключ и уйти.
CryptoKey хранится в IndexedDB - единственном хранилище браузера, которое поддерживает structured clone algorithm для непримитивных объектов. localStorage и sessionStorage работают только со строками.
Шифрование: когда «как у всех» - это хорошо
В криптографии изобретать велосипед - плохая идея. E2EE в Mist - классика: ECDH P-256 для обмена ключами, AES-256-GCM для шифрования. Скучно, но надёжно.
Когда вы пишете собеседнику, ваше устройство выполняет ECDH key agreement: ваш приватный ключ × публичный ключ собеседника → shared secret (256 бит). Из этого raw секрета через SHA-256 выводится симметричный ключ AES-256. Каждое сообщение шифруется с уникальным 96-битным IV через crypto.getRandomValues(). AES-256-GCM обеспечивает одновременно шифрование и аутентификацию - модификация пакета приведёт к провалу расшифровки.
Shared secret между двумя пользователями одинаков в обе стороны (свойство ECDH: A_priv × B_pub = B_priv × A_pub). Публичные ключи хранятся на сервере. При первом контакте клиент запрашивает публичный ключ собеседника и кеширует на 5 минут. Shared secret кешируется на 30 минут, причём хранятся и возвращаются .slice() копии - защита от cache poisoning через .fill(0).
Групповые чаты: один ключ на всех не работает
В DM всё просто: два человека, один shared secret. В группе из N человек нужно N×(N-1)/2 попарных секретов - это не масштабируется.
Решение: отправитель генерирует случайный message key для каждого сообщения. Текст шифруется этим ключом через AES-256-GCM. Затем message key «оборачивается» индивидуально для каждого участника через их ECDH shared secret. Каждый получатель расшифровывает только свою копию message key, а затем - само сообщение. Сервер хранит зашифрованный текст + массив обёрнутых ключей.
Эфемерные ключи и forward secrecy
Помимо долгоживущих identity-ключей, для каждой пары собеседников создаётся эфемерная сессия - отдельная ECDH-пара, живущая 24 часа. Из эфемерного shared secret через HKDF (RFC 5869) с identity binding выводится сессионный ключ.
Если кто-то завтра получит ваш текущий ключ - прошлые сообщения останутся зашифрованными ключом, который уже уничтожен. Эфемерные ключи хранятся только в оперативной памяти и никогда не персистятся на диск.
Дополнительная защита - nonce replay protection с 5-минутным окном свежести. Использованные nonce хранятся в LRU-кеше: при заполнении 20% удаляется без полной очистки.
Здесь был баг с Safari. WebCrypto в Safari не поддерживает JWK-импорт ECDH-ключей. Пришлось конвертировать через raw координаты и собирать PKCS8 DER вручную с полным ASN.1 algorithm identifier. Задокументировано прямо в коде, потому что следующий человек потратит на это столько же времени.
Safety numbers и один исправленный bias
Для защиты от MITM реализованы safety numbers - как в Signal. Каждая пара может сравнить числовой код из своих публичных ключей.
Алгоритм: оба ключа (uncompressed P-256, 65 байт каждый) сортируются лексикографически, конкатенируются, хешируются SHA-256 дважды с разными seed - 64 байта. Из них через rejection sampling - 60 десятичных цифр.
Первая реализация использовала mod 100 - и это был bias. Байт 0–255 при делении на 100 даёт неравномерное распределение: значения 0–55 выпадают чуть чаще. Исправлено: принимаются только байты < 200 (ровно 2×100 в диапазоне 0–199). Acceptance rate: 78%. С 64 входными байтами ожидается ~50 принятых - более чем достаточно для 60 цифр.
Звонки: LiveKit, E2EE и два бага
Голосовые звонки через self-hosted LiveKit SFU. Сервер ретранслирует зашифрованные RTP-пакеты и не может их прочитать.
Для DM-звонков ключ выводится через ECDH с domain separator: SHA-256("mist-call-key-v1:" sharedSecret roomName). Domain separator гарантирует: утечка call key не компрометирует message key.
Для групповых - инициатор генерирует 32-байтный ключ и шифрует его для каждого участника через ECDH. Бэкенд хранит массив encryptedCallKeys, но принципиально не включает их в API-ответы и broadcast - только при join, и только свой.
Баг первый: инициатор раздавал ключи всем - кроме себя. При переподключении не мог получить ключ обратно. Фикс - одна строка: включить currentUserId в Set получателей.
Баг второй: бэкенд не различал DM и групповые звонки. Теперь для DM encryptedCallKeys игнорируются принципиально - ключ выводится только на клиенте.
LiveKit токены выдаются с canPublishData: false (блокирует data channels, обходящие media E2EE), TTL 15 минут, одноразовые. Rate limiting: 5 звонков в минуту.
Конференции - отдельная фича. Ссылка без регистрации, зал ожидания, хост впускает вручную. Демонстрация экрана: 1920×1080 VP9 при 8 Mbps с contentHint: 'detail' - итог: хороший аналог Zoom и Google Meet для рабочих звонков.
Стек: Bun, Hono, PostgreSQL
Бэкенд на Bun. Встроенный Bun.serve держит тысячи WebSocket-соединений без захлёбывания. Hono - лёгкий, типизированный через Zod. PostgreSQL через Prisma + pgBouncer. Атомарность через транзакции. Race conditions закрыты на уровне БД.
PWA: осознанная боль
Первое с чем сталкивается каждый - установка. Это не кнопка «Загрузить». На iPhone: открыть ссылку в Safari, нажать «Поделиться», выбрать «На экран домой». Звучит просто, но это барьер. Мы решили его в интерфейсе - при открытии в браузере появляется баннер с инструкцией для вашего устройства. Пять секунд.
Второй страх - «пропущу сообщение». Mist поддерживает push-уведомления через Service Worker: на Android сразу, на iOS после добавления на домашний экран.
iOS и микрофон: при сворачивании PWA система убивает MediaStreamTrack микрофона. Мы перебрали шесть подходов: PiP видео, echo loop, Web Audio oscillator, silent audio + MediaSession, HD fake getUserMedia - ни один не работает. Safari в standalone PWA принципиально убивает mic при background. Работает: WakeLock (экран не гаснет), автовосстановление при возврате, честный toast «не сворачивайте во время звонка». Нативное приложение - следующий этап, код готов на 80%.
Edge swipe - ещё одна боль. Safari интерпретирует свайп от края как history.back(). В SPA это ведёт к белому экрану. Решение: touchstart listener с preventDefault() в первых 20 пикселях от краёв, { passive: false }. Полностью блокирует навигационный жест.
Россия: ТСПУ, DPI и честный разговор
Mist доступен из России без VPN. TURN over TCP 443 для звонков неотличим от HTTPS. Браузерный TLS fingerprint проходит DPI. Домен напрямую на IP сервера без CDN-прослойки.
Ирония из практики: один пользователь написал «без VPN работает, а с VPN - нет». VPN маршрутизировал через страну, где IP хостера был заблокирован по другим причинам. Инструмент обхода блокировок сам стал причиной недоступности.
Почему это хрупко: ТСПУ работает на нескольких уровнях. PWA проходит сигнатурный анализ и JA3/JA4 автоматически. Но если IP попадёт в чёрный список - придётся менять. Если Россия перейдёт к модели белых списков - любой зарубежный сервис без договорённостей окажется недоступен. Пока окно открыто - Mist работает. Обещать большее было бы враньём.
Что получает пользователь

За всей этой криптографией - продукт для людей. Не абстрактный «защищённый мессенджер», а конкретные вещи которые работают каждый день.
Личные/групповые чаты, голосовые звонки с E2EE. Конференции по ссылке до 10 человек с демонстрацией экрана - участник входит без регистрации. AI-ассистент (Gemini, GPT, Grok) встроен в интерфейс и работает из России без VPN. Сообщения с таймером самоуничтожения. Публичные каналы. Инвайт-коды. Полная локализация EN/RU.
Открытый код
Весь криптографический код - в публичном репозитории: github.com/Mist-Messenger/mist-messenger-security
Вместо заключения
Технологии цензуры совершенствуются. Мы тоже. Mist строился не просто как мессенджер - а как доказательство того, что право на приватность это не привилегия, а математическая константа.
Мы продолжаем эту гонку, чтобы вам больше не приходилось объяснять близким как настроить VPN - просто чтобы сказать «привет».
Комментарии (43)

grvelvet
30.03.2026 11:17А вы потянете финансирование своего проекта? Или тихо закроетесь через пол года?

MistM Автор
30.03.2026 11:17Проект финансируется фаундерами. Инфраструктура в текущей реализации недорогая, сервер не занимается шифрованием, всё шифрование происходит на клиенте. Подписки (доп возможности к базовому функционалу) частично покрывают расходы

McKinseyBA
30.03.2026 11:17сервер не занимается шифрованием
Главное вовремя рассмотреть в вашем нике наличие буквы "s")

slinkinone
30.03.2026 11:17Согласен с вопросом проблемности архитектуры нынешних технологий - клиент<->сервер.
Но насколько я понимаю, ваш мессенджер не решает эту проблему. Такж есть сервера вашего мессенджера, которые можно определять и блокировать.
---
На мой взгляд, P2P должен получить новый виток на фоне того, что происходит в мире с технологиями. Причем без каких-то костылей в виде mesh систем и блютузов.

MistM Автор
30.03.2026 11:17P2P как идея отличная, но на практике упирается в NAT traversal, мобильные сети и энергопотребление. Держать прямое соединение между двумя телефонами за NAT оператора, задача которую даже WebRTC решает через TURN серверы, а это уже не чистый P2P. Мы не претендуем на решение проблемы «сервер можно заблокировать» - от этого нет защиты. Но разница в том что наш сервер это почтальон: он передаёт зашифрованные пакеты и не может их прочитать. Даже если его заблокируют - переписка не скомпрометирована. А переехать на другой IP - вопрос минут. Полноценный P2P мессенджер без серверов, без mesh, без костылей - я буду рад увидеть рабочую реализацию. Пока её нет, мы решаем задачу теми способами которые работают сегодня.

S-trace
30.03.2026 11:17IPv6?
Сделать десктопную версию клиента, которая будет иметь галочку "Разрешить TURN Relay" и при её включении будет прокидывать порт на роутере через UPNP, тестировать его доступность с сервера, анонсировать себя в DHT-подобной сети (тут правда тонкий момент с первым подключением, но можно через DNS анонсировать адреса раскрутки, и добавить поле где можно их руками переопределить) и TURNить трафик взамен на премиум-фичи бесплатно?

MistM Автор
30.03.2026 11:17Это интересное предложение, но на практике упирается в несколько моментов:
UPnP отключён по умолчанию у большинства провайдеров и роутеров. А где включён - это дыра в безопасности которую мы не хотим рекомендовать пользователям.
IPv6 да, снимает проблему NAT. Но проникновение IPv6 в России пока низкое, особенно в мобильных сетях.
Идея с пользовательскими relay за премиум это по сути Tor-модель. Работает, но создаёт юридические риски для тех кто ретранслирует чужой трафик. В контексте РФ это то, чем мы не хотим нагружать пользователей.
Пока проще и надёжнее держать несколько своих TURN серверов в разных юрисдикциях. Но идею с десктоп-клиентом держим в уме, он в планах

LittleHornet
30.03.2026 11:17Как раз в том и вопрос, чья именно переписка не скомпрометирована.
Скажет потом подследственный, что переписывался через это вот всё - ну и добрый день, господин майор.

santjagocorkez
30.03.2026 11:17Решение: отправитель генерирует случайный message key для каждого сообщения. Текст шифруется этим ключом через AES-256-GCM. Затем message key «оборачивается» индивидуально для каждого участника через их ECDH shared secret. Каждый получатель расшифровывает только свою копию message key, а затем - само сообщение
Так всё хорошо начиналось, и на тебе: полное раскрытие списка участников группы. Жаль.

MistM Автор
30.03.2026 11:17В v2 планируем подход Signal Private Groups, сервер хранит зашифрованный blob состояния группы и не знает состав участников. Протокол открыт, будем пробовать

roma1052
30.03.2026 11:17Шикарный кейс использования
non-extractable CryptoKey. Удивительно, как много проектов до сих пор хранят ключи вlocalStorageв виде простых строк, подставляясь под любой XSS-вектор.Особенно порадовал момент с исправлением bias в
rejection samplingдля safety numbers — такая дотошность к статистическому распределению (байт < 200) выдает серьезный инженерный подход.Вопрос к автору по поводу групповых чатов: рассматривали ли вы внедрение протокола MLS (Messaging Layer Security) в будущем? Обертка ключа (key wrapping) для каждого участника через ECDH хороша на малых группах, но при росте N до сотен участников нагрузка на отправителя и объем метаданных в БД начнут расти экспоненциально. Планируете ли оптимизировать это через древовидные структуры ключей?

MistM Автор
30.03.2026 11:17Спасибо за детальный разбор.
По MLS да, рассматриваем. Текущая схема с per-member key wrapping работает для групп до 50-100 человек: отправитель делает N операций AES-GCM wrap, сервер хранит N зашифрованных ключей на сообщение. Это O(N) на отправку и O(N) на хранение.
MLS с TreeKEM даёт O(log N) на обновление ключей за счёт древовидной структуры при 1000 участников это разница между 1000 и 10 операциями. Для нашего текущего лимита групп это оверинжиниринг, но при масштабировании станет необходимостью.

kzkvv
30.03.2026 11:17Честный ответ
Честный комментарий - читать ваш AI-слоп отвратительно. Даже если он на 80% написан человеком.
Право на приватность это не привилегия, а математическая константа
Такую воду даже гпт мне уже не выдает - вы постарались!
Пока окно открыто - Mist работает. Обещать большее было бы враньём
А кто-то называет это "маркетинг". Впрочем, нейронке виднее.

janatem
30.03.2026 11:17А где почитать про архитектуру и бизнес-модель проекта? Я вижу только ссылку на исходники криптобиблиотеки и собственно сайт-сервер.

MistM Автор
30.03.2026 11:17Подробная документация по архитектуре в процессе, скоро появится на сайте. Пока можете ознакомиться с README на гитхабе (cсылка в статье). По бизнес-модели: Free план + платные подписки Pro и Ultra

Quqas
30.03.2026 11:17Домен напрямую на IP сервера
и?
вы с каких провов тестили? это где в эрэфистане забугорное https, уже не должно содержать православный sni? и cdn не причём.

MistM Автор
30.03.2026 11:17SNI в открытом виде - да, без ECH домен виден в TLS handshake. Но блокировки по SNI в РФ работают не для всех доменов, а по реестру. Пока домена нет в реестре РКН SNI не помеха. Тестировали с МТС, Билайн, Т2 и домашних провайдеров, работает без VPN

Quqas
30.03.2026 11:17бан cdn вовсе не по sni а по ip\cidr и что характерно без судно и без реестра по беспределу
имею мнение что не тольколишьвсе cdn побанены а тупо не эрэфийские ip в заголовке
ну или шейпинг или т.п. и т.д.

DamirMur
30.03.2026 11:17Это не решает проблему "белых списков", а решает если в качестве транспорта использовать яндекс-почту, max. Шифровать-расшифровывать rsa ключами. Проблему звонков не решает, но зато работает при отключениях.

MistM Автор
30.03.2026 11:17Возможно это рабочий метод для экстремальных сценариев, но транспорт через Яндекс почту или Max создаёт новую проблему оба сервиса видят метаданные и находятся в рф юрисдикции. Это не приватный транспорт, это замена одного риска другим

DamirMur
30.03.2026 11:17Ваша связь идет через операторов связи, которые находятся в рф юрисдикции.
Система хранения трафика у операторов связи в РФ (в рамках «закона Яровой» и СОРМ-3) обязывает провайдеров хранить текстовые сообщения, голосовые вызовы, изображения, звуковые и видеофайлы пользователей до 30 суток (с ежегодным ростом емкости). Метаданные о действиях абонентов хранятся до 3 лет
Регистрация на левых симках, остальное хорошо зашифровано.

uchuum
30.03.2026 11:17Нижняя панель обрезается, благодаря этому даже в суппорт не написать.

MistM Автор
30.03.2026 11:17Спасибо за обратную связь. Какое устройство и браузер? Вы используете PWA или открытое окно в браузере?

AcckiyGerman
30.03.2026 11:17+1, на мобильном на экране регистрации после кнопки “Copy” не видно ни галочки “я сохранил” ни кнопку “Continue” и невозможно прокрутить экран - скролл не работает.
Мобильный Firefox, или на десктопе включите Responsive Mode (CTRL+SHIFT+M) и сразу обнаружите ошибку.

IfElseEndif
30.03.2026 11:17Благодарю за то, что Вы есть и что скинули сурс - очень ценно. Я сейчас разрабатываю один проект, которому КРАЙНЕ поможет знакомство с Вашим сурсом.
В образовательных целях, будет весьма своевременно и полезно.
Также, обязательно зарегистрируюсь и посмотрю Ваш проект - очень интересная штука, чем-то напоминает некоторые форумы прошлого, на которых тоже была авторизация через ключевые фразы\наборы слов, мой отец мне в детстве про такое говорил и предлагал реализовать, когда мотивировал учиться IT)

helldweller
30.03.2026 11:17Вот Вы пишите BIP39... А со своей BIP39 seed фразой я зайти не смог. Пишет Invalid or expired challenge. Это значит, что Вам важно самим генерировать ключи. Для чего?.. Для того чтобы генерировать производные ключи и иметь возможность расшифровать?

MistM Автор
30.03.2026 11:17Seed-фраза в Mist генерируется при создании аккаунта, войти с произвольной BIP39 фразой нельзя, потому что аккаунта с таким ключом просто не существует на сервере. Процесс: регистрация - BIP39 в Mist генерирует 12 слов - из них детерминированно выводятся ключи - публичный ключ отправляется на сервер. При логине сервер проверяет подпись через challenge-response если публичного ключа нет в базе, логин невозможен. Использовать свою фразу от кошелька не получится это разные системы идентификации. Зарегистрируйтесь через "Создать аккаунт", сохраните сгенерированную фразу и с ней можно входить с любого устройства в Mist.

Kirill8
30.03.2026 11:17При копировании через кнопку текст вставляется: abc defg hijkl итд. Следующим шагом запрашиваются три слова в произвольном порядке. Возможно, стоит сделать, чтобы вставлялось в следующем виде:
1. abc
2. defg
3. hijkl
?

Kirill8
30.03.2026 11:17Обязательно учитывайте, что если вами зарегистрированный канал не укладывается в представление о приемлемом, то его у вас просто-напросто удалят. Не важно, что вы там собираетесь публиковать, у владельцев ресурса есть своя логика, и они её придерживаются.
Скрытый текст
Разработчики с одной стороны выглядят адекватными (как минимум, подписку ультра подарили после нескольких баг-репортов), но с другой при таких ограничениях желание иметь дело с таким мессенджером исчезает, в итоге удалился.
Мессенджер Mist, MistM, модерация, ограничения, специфика.

nin-jin
30.03.2026 11:17Ну вот, зачем мессенджер, в котором нельзя слать дикпики?

Forcenn89
30.03.2026 11:17Я скажу больше - дикпиков ограниченно кидать можно в день и ценники конские на премы. эх, я сначала думал сделать из него корпоративный мессенджер.

MistM Автор
30.03.2026 11:17На начальном этапе мы ввели лимиты на загрузку файлов (для Free 20 в день), зашифрованные файлы весят значительно больше исходных и нагрузка на хранилище соответствующая. В будущем планируем пересматривать лимиты. По корпоративному плану вы можете написать в поддержку Mist или на почту, обсудим индивидуально.



0x200AC
Проблема в том, что https трафик к определенному "сайту" точно так же можно заблокировать. И если "классические" мессенджеры используют распределенную инфраструктуру с динамическим меняющимися серверами и CDN, которые вынуждают блокировать не по адресам, а по DPI, то ваш "Домен напрямую на IP сервера без CDN-прослойки" внесут в список заблокированных сайтов одним кликом. Как тысячи и сотни тысяч других "сайтов"
MistM Автор
Да мы осознаем этот риск. Подготовились к возможности блокировок. Резервные домены, несколько IP, быстрое переключение. Плюс PWA обновляется автоматически - пользователю не нужно качать новую версию из стора чтобы переехать на новый адрес.
0x200AC
Пока есть однозначно идентифицируемые домены/ip - их будут банить до бесконечности. Будь их хоть сотня
dimaaannn
Вероятно только меши спасут этот проклятый мир.
Концепция централизованных IP адресов и DNS себя изживает.