ONYX: self-hosted мессенджер с LAN-режимом — история одного инди-проекта

в 23:01, , рубрики: e2ee, flutter, lan, node.js, open source, self-hosted, x25519, криптография, мессенджер, шифрование

Когда смотришь на существующие 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": "alice",
  "timestamp": 1710000000000,
  "pubkey": "<X25519 public key, base64>"
}

Все остальные клиенты слушают этот порт и при получении пакета обновляют две таблицы:

  • username → IP-адрес источника

  • username → публичный ключ X25519

mDNS не используется — только чистый UDP broadcast. Ручной ввод IP тоже не нужен. Публичный ключ передаётся прямо в broadcast-пакете — это позволяет сразу же начать шифрованный обмен без дополнительного рукопожатия.

Передача медиафайлов в LAN

Медиафайлы идут по отдельному каналу — порт 45679 — чанками по ~32 KB. Каждый чанк шифруется независимо через AES-256-GCM, что позволяет начинать декрипт и отображение ещё до получения всего файла.

Шифрование: два слоя на эллиптических кривых

RSA в проекте нет — только современный стек на эллиптических кривых.

Схема для E2EE-чатов

  1. Обмен ключами: X25519 ECDH с эфемерной парой ключей per-session

  2. Деривация: HKDF-SHA256 с контекстной меткой (onyx-lan-v2 для LAN, отдельные метки для E2EE-чатов)

  3. Шифрование: 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, на домашнем сервере или прямо в локальной сети. Сценарии разные:

  • Локальная сеть — перекидка файлов и общение внутри офиса или домашней сети без выхода в интернет

  • Приватное сообщество — закрытая группа на своём VPS, куда люди приходят по инвайту

  • Публичный канал — хостишь канал сам, подписчики заходят и читают посты, также по инвайту

Группа — двусторонняя коммуникация, пишут все участники. Канал — однонаправленная трансляция, публикуют только администраторы, остальные читают.

Подключиться к внешнему серверу можно прямо из приложения через экран external_server_join_screen — вводишь адрес инстанса и присоединяешься к нужной группе или каналу.

Развёртывание своего внешнего канала/группы

Для этого есть отдельный серверный софт — ONYX Server

Избранное: локальные заметки и хранилище

В ONYX есть отдельная вкладка «Избранное» — это не чат с самим собой, а полноценный инструмент для личных заметок. Можно создать любое количество избранных чатов, каждый с отдельной аватаркой и названием, и использовать их как категории: пароли, идеи, сохранённые медиа, ссылки.

Всё содержимое хранится локально на устройстве — никуда не отправляется и не синхронизируется. Сервер про избранное ничего не знает.

Аккаунты: анонимность, мультиаккаунтность и удаление

Регистрация — только юзернейм и пароль. Ни номера телефона, ни почты. Это сознательное решение: минимум данных о пользователе на сервере.

Юзернейм выбирается один раз и навсегда — изменить его невозможно. Это твой постоянный идентификатор в системе. Поменять можно только отображаемое имя, но не сам username.

Мультиаккаунтность — в приложении можно зарегистрировать и держать любое количество аккаунтов, переключаться между ними без выхода.

Удаление аккаунта — в любой момент можно удалить аккаунт вместе со всеми медиафайлами и данными на сервере. Никаких следов не остаётся.

Текущее состояние

Проект в рабочей бете. Разработка продолжается. Буду рад вопросам в комментариях.

Затестить

Автор: wardcore

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js