Когда смотришь на существующие self-hosted мессенджеры, часто видишь одно из двух: либо сложную инфраструктуру, которую непросто развернуть (Matrix/Synapse), либо минимализм без шифрования. ONYX — это попытка найти середину: простой в развёртывании сервер, полноценное E2E-шифрование и режим работы в локальной сети без интернета вообще.
Архитектура проекта
Компонент
Технология
Клиент
Flutter (Android, Windows, macOS, Linux)
Сервер
Node.js — Express + express-ws + ws
База данных
MariaDB + Redis (сессии, кэш)
Файловое хранилище
S3-совместимое (AWS SDK v3)
Транспорт
WebSocket (wss://) + HTTP/REST
Шифрование
X25519 + XChaCha20-Poly1305 + AES-256-GCM
LAN-режим: работа без интернета
Одна из ключевых фич ONYX — возможность общаться в локальной сети без интернета вообще. Для этого реализован собственный механизм авто-обнаружения устройств.
Протокол discovery через UDP broadcast
Каждый клиент каждые 5 секунд рассылает JSON-пакет на адрес 255.255.255.255:45678:
Все остальные клиенты слушают этот порт и при получении пакета обновляют две таблицы:
username → IP-адрес источника
username → публичный ключ X25519
mDNS не используется — только чистый UDP broadcast. Ручной ввод IP тоже не нужен. Публичный ключ передаётся прямо в broadcast-пакете — это позволяет сразу же начать шифрованный обмен без дополнительного рукопожатия.
Передача медиафайлов в LAN
Медиафайлы идут по отдельному каналу — порт 45679 — чанками по ~32 KB. Каждый чанк шифруется независимо через AES-256-GCM, что позволяет начинать декрипт и отображение ещё до получения всего файла.
Шифрование: два слоя на эллиптических кривых
RSA в проекте нет — только современный стек на эллиптических кривых.
Схема для E2EE-чатов
Обмен ключами: X25519 ECDH с эфемерной парой ключей per-session
Деривация: HKDF-SHA256 с контекстной меткой (onyx-lan-v2 для LAN, отдельные метки для E2EE-чатов)
Шифрование: XChaCha20-Poly1305 AEAD
Формат пакета:
[pubkey 32B] [nonce 12B] [ciphertext] [mac 16B]
Почему XChaCha20-Poly1305, а не AES-GCM?
AES-GCM требует аппаратного ускорения (AES-NI) для нормальной производительности. XChaCha20-Poly1305 работает с константным временем на любом железе — важно для мобильных устройств без AES-NI. Дополнительный бонус — расширенный nonce (192 бита против 96 у GCM), что снижает риск коллизий при большом количестве сообщений в одной сессии.
Для медиа в LAN используется AES-256-GCM — там чанковая передача, а аппаратное ускорение доступно на большинстве десктопов.
Мультидевайсность и E2EE
Один из сложнейших кейсов — когда у пользователя несколько устройств, и при этом нужно сохранить E2E-шифрование.
Когда новое устройство подключается к аккаунту, оно отправляет запрос на авторизацию к доверенному устройству. Доверенное устройство должно явно одобрить новое устройство — только после этого происходит обмен ключами. Это означает, что сервер никогда не имеет доступа к расшифрованному содержимому, даже при добавлении нового девайса.
Личные сообщения и мультидевайсная синхронизация
Личные чаты работают через центральный сервер с E2EE. Технически мультидевайсность устроена так: когда тебе приходит новое сообщение, сервер рассылает его на каждое твоё устройство отдельно — зашифрованное под публичный ключ конкретного девайса. То есть технически это разные зашифрованные сообщения для каждого устройства, просто с одним и тем же открытым текстом внутри.
Важное ограничение: синхронизируются только входящие сообщения. Исходящие — видны только на том устройстве, с которого отправлены. Это честный компромисс: полная двусторонняя синхронизация при E2EE требует либо отдельного механизма шифрования для копии «себе», либо хранения на сервере в расшифрованном виде — оба варианта сложнее и либо дороже, либо менее безопасны.
Почему Flutter, а не Electron или нативная разработка
Требование с самого начала — одна кодовая база для Windows, macOS, Linux и Android. Вариантов было три:
Нативная разработка: 3–5 отдельных кодовых баз, кратно больше работы и рассинхрон между платформами
Electron: кроссплатформенность есть, но Chromium в процессе — это +150–200 MB RAM на старте и DOM-рендеринг вместо нативного
Flutter: единая кодовая база, рендеринг через Skia/Impeller без DOM, реальные 60fps на анимациях
Для мессенджера с активными анимациями, медиа и чатами разница в рендеринге принципиальна. На Flutter Desktop пришлось написать 10+ отдельных модулей только под оптимизации FPS (fps_booster, fps_optimizer, fps_stimulator, message_load_optimizer, chat_preloader) — Flutter на десктопе без допиливания лагает заметно больше, чем на мобилках. Но результат — плавный UI на всех четырех платформах из одного репозитория.
Desktop-специфика
На десктопных платформах реализованы нативные интеграции, которых нет в мобильных клиентах:
Системный трей — приложение сворачивается в трей, не закрывается.
Single-instance — предотвращение нескольких копий через IPC. Повторный запуск поднимает уже открытое окно.
Кастомный titlebar — системный скрыт, своя шапка с drag-зоной
Windows-нативные уведомления — отдельный модуль, не через Flutter-оверлей
Безопасность: что ещё кроме E2EE
Помимо шифрования сообщений:
PIN-код и биометрия — Face ID / fingerprint через Flutter Secure Storage
Proxy-поддержка — proxy_manager.dart, маршрутизация через произвольный прокси
Secure storage — все чувствительные данные через защищённое хранилище ОС (Keychain / Android Keystore)
Управление активными сессиями — видны все подключённые устройства, любую сессию можно завершить удалённо, но только через доверенное устройство
Self-hosted группы и каналы
В ONYX два вида групп и каналов — принципиально разные по модели.
Встроенные группы и каналы (через сервер ONYX)
Стандартные группы и каналы работают через центральный сервер ONYX и не шифруются — это осознанный компромисс ради надёжной синхронизации между устройствами. Подходит для открытых сообществ и случаев, когда сквозное шифрование не является требованием.
Внешние группы и каналы (self-hosted)
Любой пользователь может поднять собственный инстанс — на , на домашнем сервере или прямо в локальной сети. Сценарии разные:
Локальная сеть — перекидка файлов и общение внутри офиса или домашней сети без выхода в интернет
Приватное сообщество — закрытая группа на своём VPS, куда люди приходят по инвайту
Публичный канал — хостишь канал сам, подписчики заходят и читают посты, также по инвайту
Группа — двусторонняя коммуникация, пишут все участники. Канал — однонаправленная трансляция, публикуют только администраторы, остальные читают.
Подключиться к внешнему серверу можно прямо из приложения через экран external_server_join_screen — вводишь адрес инстанса и присоединяешься к нужной группе или каналу.
Развёртывание своего внешнего канала/группы
Для этого есть отдельный серверный софт — ONYX Server
Избранное: локальные заметки и хранилище
В ONYX есть отдельная вкладка «Избранное» — это не чат с самим собой, а полноценный инструмент для личных заметок. Можно создать любое количество избранных чатов, каждый с отдельной аватаркой и названием, и использовать их как категории: пароли, идеи, сохранённые медиа, ссылки.
Всё содержимое хранится локально на устройстве — никуда не отправляется и не синхронизируется. Сервер про избранное ничего не знает.
Аккаунты: анонимность, мультиаккаунтность и удаление
Регистрация — только юзернейм и пароль. Ни номера телефона, ни почты. Это сознательное решение: минимум данных о пользователе на сервере.
Юзернейм выбирается один раз и навсегда — изменить его невозможно. Это твой постоянный идентификатор в системе. Поменять можно только отображаемое имя, но не сам username.
Мультиаккаунтность — в приложении можно зарегистрировать и держать любое количество аккаунтов, переключаться между ними без выхода.
Удаление аккаунта — в любой момент можно удалить аккаунт вместе со всеми медиафайлами и данными на сервере. Никаких следов не остаётся.
Текущее состояние
Проект в рабочей бете. Разработка продолжается. Буду рад вопросам в комментариях.