Готовность ChatOffline 1.0

Офлайн-мессенджер для Android и iPhone: Bluetooth LE mesh + локальный Wi‑Fi, сквозное шифрование (Noise XX, Ed25519, X25519), личные и групповые чаты, файлы, голосовые, звонки по Wi‑Fi. Аудит ветки release/1.0 (b95df392 → beta3), тесты на ASUS ROG Phone 5 (Android 11) и iPhone 12 Pro Max.

~2 790автотестов, 0 падений
≈260 тыс.строк Kotlin + Swift
~75 %готовность к закрытой бете
8дефектов исправлено сегодня (3 UI, 4 безопасности, 1 файлы)
Тестовая сборка для Android: скачать ChatOffline 1.0.0-beta3
35 МБ, Android 8.0+, без магазинных сертификатов. Для iPhone сборки нет: iOS не запускает приложения без подписи Apple; на iPhone доступна веб-версия chatoffline.ru/app через интернет.

Вердикт

Ядро сделано и хорошо проверено. До реального запуска не хватает полевых доказательств и дистрибуции. Протокол, криптография, доставка с подтверждениями, файлы и группы реализованы на обеих платформах и закрыты тысячами тестов. Android↔Android и Android↔iPhone по BLE уже проходили на железе.

Что мешает выпустить бету:

  • нет большой полевой матрицы: 100 сообщений, 100 разрывов, курьер через третий телефон, батарея без кабеля;
  • нет подписи и каналов раздачи: Apple Developer Program и TestFlight, Play Console;
  • все четыре найденных риска безопасности закрыты сегодня; остался открытый ID в локальном Wi‑Fi (DNS-SD);
  • ветка main отстала на 96 коммитов, у репозитория нет удалённого адреса и живого CI.

Готовность по областям

ОбластьГотовностьСтатус
Протокол, крипто, шифрованное хранилище
92 %
BLE-транспорт Android
85 % файлы по BLE починены в сентябре
BLE-транспорт iOS (фон)
65 % ограничения iOS в фоне
Личные чаты, доставка, ACK, outbox
88 %
Знакомство (Подключить → Принять), QR/SAS
85 %
Файлы, фото, голосовые
75 % нет матрицы на 100 файлов
Группы (Круги)
70 % нет проверки на 20 участниках
Звонки по Wi‑Fi
60 % перепроверки не было с августа
Курьер (доставка через третьего)
55 % на железе не проверялся
Интерфейс Android
82 %
Интерфейс iOS
72 %
Подпись, магазины, политика приватности
35 % нужен аккаунт Apple
Desktop / Web / интернет-доставка
вне 1.0

Что проверено сегодня

Всё прогнано заново на текущем коде, а не взято из старых отчётов.

ПроверкаРезультатКомментарий
Android unit :app:testDebugUnitTest731 / 0698 существующих + 33 новых (инициалы, приём Noise, лимитеры, сумка курьера, приватность рекламы BLE/LAN/Wi‑Fi Direct, экран «Рядом»)
KMP :sharedLogic:desktopTest797 / 01 пропущен
Desktop :desktopApp:test422 / 0только на JDK 21; на JDK 17 падает UnsupportedClassVersionError
iOS swift test880 / 02 пропущены; +13 новых (приём Noise, сумка курьера, приватность рекламы и DNS-SD)
Android lint0 ошибок237 предупреждений, в основном deprecated API BLE и EncryptedSharedPreferences
Сборка release APKOKbeta2 — 74 МБ; beta3 — 35 МБ после отказа от x86-библиотек для эмуляторов. Conformance-векторы Android↔iOS PASS полностью (с account-v2 и identity-transfer)
beta3 на ROG Phone 5: «Отменить запрос» и «Определяем имя…»PASSУстановлен публично скачанный APK поверх beta2. Полный цикл: «Отменить запрос» → карточка вернулась в «Подключить» → новая заявка → «Ждём ответ». После перезапуска сосед 2 с показан как «Определяем имя…», затем «Demo». Одно нажатие сразу после разблокировки экрана не сработало (система гасит первое касание), повтор прошёл
Soak release-сборки, экран выключен, 20 минPASSROG Phone 5: процесс не перезапускался, память 58–64 МБ без роста, падений 0
iOS под симуляторOKбез подписи: экран «Хранилище недоступно» (−34018), это ожидаемо; с ad-hoc подписью: обычный онбординг
ROG Phone 5: онбординг → Чаты → Рядом → Звонки → Ещё → НастройкиPASSmesh стартует сам, без лишних нажатий
ROG Phone 5: обнаружение соседа по BLEPASSвиден реальный узел «Demo»
ROG Phone 5: «Подключить» → запрос знакомстваотправленстатус «Ждём ответ»; принять должна вторая сторона
Mac ↔ Android по Wi‑Fi (LAN): обнаружение, Noise XX, взаимный SASPASSштатный раннер run-desktop-android-ble-file-physical.sh: 22/22 наблюдения «есть / подключён / аутентифицирован»
Mac ↔ Android только по BLE: файл 64 КиБ в обе стороныPASSMac→Android 70 с (572 GATT-записи, 0 сбоев), Android→Mac 29 с; SHA-256 совпал (f8a38b49…), ACK аутентифицирован, Wi‑Fi не использовался. Сборка уже с исправлением Noise (cfc2d8a0). Запуск из Terminal.app: так macOS выдала разрешение Bluetooth
Эфир BLE до/после: сканер Mac ловит рекламу ChatOffPASSROG Phone (новая сборка): service_data=нет, пропадает из эфира при остановке приложения. Соседний «Demo» на старой сборке рядом продолжает слать постоянный ID 87fba713… — наглядно прежняя утечка
Mac ↔ Android по BLE после правки рекламыPASS ×2Сначала Android→Mac падал и на старой сборке. Причина найдена: Android не подтверждал опоздавший дубликат части файла, если приём уже завершён, и Mac ждал подтверждения до таймаута. Исправлено в ae8eb321, два прогона подряд прошли в обе стороны (ble-test-2120, 2125)
iPhone 12: запуск установленной сборкиBLOCKEDпрофиль подписи истёк; Xcode сообщает «Your session has expired» и отклоняет вход в Apple ID даже после повторного ввода пароля
Android↔iPhone: сообщения, файлы, звонокждёт iPhoneначнётся сразу, как Xcode создаст профиль

Найдено на телефоне и исправлено

Коммит 657c8630 в release/1.0. Проверено на ROG Phone 5, unit 702/0, lint 0 ошибок.

1. Фиолетовые карточки и чипы

В теме не были заданы роли secondaryContainer и surfaceContainer*. Карточки «Рядом» и «Звонки», выбранные чипы и слайдер брали базовую фиолетовую палитру Material 3, а не зелёный бренд. Теперь заданы все контейнерные роли для светлой и тёмной схем.

2. Кнопка «Новый чат» — «+» размером с точку

Стандартные 24dp отступов у кнопки 56dp сжимали иконку почти до нуля. Теперь отступ нулевой, иконка 28dp.

3. Аватар всегда «CO»

Было зашито в коде; теперь инициалы из имени («ROG5» → «RO», «Анна Петрова» → «АП»). Плюс онбординг больше не уезжает под статус-бар (edge-to-edge).

До → после

Чаты до исправления
До: «+» точкой, аватар CO
Чаты после исправления
После
Рядом до
До: серо-фиолетовая карточка
Рядом после
После: зелёная
Настройки до
До: фиолетовые чипы
Настройки после
После

Интерфейс и цвета

Палитра (Android = iOS = Web)

Night #07110E — фон
NightSurface #0C1A16
NightRaised #142821
Mint #70F5B6 — акцент
Forest #0F7C52 — акцент светлой
Paper #F4F8F5
Warning #F2C66D
Danger #FF746D

Mint на Night даёт контраст около 13:1, на светлой теме Forest на Paper около 5:1, то есть AA проходит. Ниже, в разделе «Ещё», пять шрифтовых стилей, масштаб текста 85–200 %, темы «Системная/Светлая/Тёмная».

Навигация Android

Шапка бренда (логотип, «Помощь», профиль), нижняя панель Чаты · Рядом · Звонки. Профиль ведёт в «Ещё»: Группы, Передачи, Устройства, Публичный канал, Настройки. Онбординг из одного экрана: имя и объяснение разрешений.

Онбординг Android
Онбординг
Звонки
Звонки
Ещё
Ещё
Онбординг iOS
iOS: онбординг
iOS ошибка хранилища
iOS: экран восстановления

Что ещё улучшить в интерфейсе

ПроблемаГдеПредложениеПриоритет
Два входа в профиль: иконка в шапке и аватар на «Чатах» ведут в одно местоAndroid, BrandBar + HomeScreenОставить один аватар с инициалами в шапкеP2
Карточка «Рядом» не показывает силу сигнала и канал (BLE/Wi‑Fi)Android/iOSИконка канала и «близко / далеко»P2
Пустой экран чатов не ведёт за рукуЧатыКнопка «Найти людей рядом» прямо в пустом состоянииP3
iOS-онбординг короче Android: нет объяснения разрешенийiOSТе же два блока, что на AndroidP2
Экран восстановления хранилища показывает сырой код unexpectedStatus(-34018)iOSПрятать под «Подробнее»P3
Первые секунды после запуска сосед показан как «без имени», пока не придёт подписанный ANNOUNCE (замер 6–11 с, сразу после установки до минуты)РядомСделано в beta3: «Определяем имя…» до прихода подписанного ANNOUNCEготово
Статус «Ждём ответ» без «Отменить»РядомСделано в beta3: кнопка «Отменить запрос», как на iOSготово

Алгоритмы и протокол

Транспорт BLE
  • Обе роли одновременно: GATT-сервер и клиент, собственный сервис c0ffee01-….
  • MTU 517, кадр ≤ 512 байт (фикс сентября), заголовок 10 байт, до 128 кадров на пакет.
  • До 4 исходящих соединений, PHY 2M, L2CAP CoC для объёмных данных (API 29+).
  • Очередь отправки с приоритетами: CONTROL > TEXT > FILE_CONTROL > PREVIEW > FILE_CHUNK. Текст обгоняет файлы.
  • Параллельно LAN: DNS-SD + TCP + Noise XX + SAS.
Маршрутизация
  • Flood с TTL: 7 для сообщений, 3 для ANNOUNCE; на ретрансляции джиттер 5–280 мс.
  • Дедупликация: LRU на 1000 отпечатков.
  • Fanout: при ≤ 4 соседях — всем, иначе ⌈log₂n⌉+1, выбор детерминированный.
  • Курьер (spray-and-wait): 3 копии соседям; сумка носителя на 64 конверта и 24 ч, только шифротекст.
  • Outbox: повторы через 20 с, 40 с … 10 мин, хранится 24 ч, удаляется по сквозному ACK.
Криптография
  • Noise_XX_25519_ChaChaPoly_SHA256 — личные сообщения с forward secrecy.
  • Ed25519 — подписанный ANNOUNCE и публичные сообщения.
  • Sealed box (X25519 + HKDF + ChaCha20-Poly1305) — для офлайн-почты и курьера.
  • Файлы: ChaCha20-Poly1305 на каждый чанк, AAD связывает файл, индекс и SHA-256.
  • Ключи хранятся в AndroidKeyStore / Keychain (ThisDeviceOnly). SAS — 6 цифр.
Лимиты защиты от злоупотреблений
  • Входящих 600/с с одного адреса, ретрансляции 80/с суммарно.
  • ANNOUNCE не чаще раза в 4 с, handshake не чаще раза в 1,5 с.
  • До 32 Noise-сессий, тело сообщения ≤ 4096 байт, до 32 фрагментов.
  • Пропускная способность: GATT 20–60 КБ/с на канал. Сеть рассчитана на десятки узлов, максимум на сотни.

Риски, подтверждённые в коде

РискПочему важноЧто сделать
Любой сбой расшифровки рвёт Noise-сессию (MeshEngine.kt:3972), а sender_id в заголовке не аутентифицированЗлоумышленник в радиусе BLE может поддельными кадрами постоянно сбрасывать чужие сессии. Доставка уйдёт в sealed без forward secrecyСделано на Android и iOS: окно nonce 16 (только вперёд), сбойный кадр отбрасывается, сессия пересоздаётся после 8 сбоев без успеха за 20 с. Следующий шаг — явный nonce в протоколе v2исправлено cfc2d8a0
Постоянный 8-байтный peer ID в рекламе BLEТелефон можно отслеживать по эфируСделано: в рекламе остался только UUID сервиса на всех клиентах (как всегда было на iOS), личность — только после соединения по подписанному ANNOUNCE. Проверено сканером эфира с Mac. В локальном Wi‑Fi (DNS-SD) ID и имя пока видныисправлено 040a24f7
TokenBucketRateLimiter и MinIntervalGate на HashMap без вытесненияРост памяти от потока поддельных отправителейСделано: LRU на 256 ключей, как на iOSисправлено cfc2d8a0
Сумка курьера при заполнении отвергает новые конвертыСпамер может занять её на 24 чСделано на Android, iOS и Desktop: не больше 16 конвертов на адресата, полная сумка вытесняет самый старый неотправленный конверт самой большой группыисправлено 256b8a38
ACCESS_FINE_LOCATION без maxSdkVersionЛишний вопрос про геолокацию и замечания модерации PlayСделано в beta3: maxSdkVersion="32", на Android 13+ разрешение не объявляетсяготово
iOS в фоне рекламирует только UUID в overflow areaСвёрнутый iPhone плохо виден другимЧестно сообщать в UI «держите приложение открытым»; проверить на полеP2

Что нужно, чтобы проект заработал реально

От владельца (не код)
  • Apple ID в Xcode: войти заново. Без этого iPhone-сборку не установить. Обходные пути для iPhone без подписи: (а) веб-версия chatoffline.ru/app в Safari — работает через интернет и связывается с Android-телефоном, но не умеет Bluetooth; (б) платный Apple Developer + TestFlight; других способов у iOS нет.
  • Apple Developer Program ($99/год): TestFlight, App ID без experimental.
  • Google Play Console ($25): закрытое тестирование, листинг, политика приватности уже написана.
  • 3–4 телефона на один стенд и 2–3 часа на полевые прогоны, включая один телефон без кабеля для замера батареи.
  • Резервная копия кода: в Gitea на GX10 создан приватный репозиторий admin/chatoff, remote gitea уже добавлен; отправку веток должен запустить владелец (git push gitea main release/1.0 claude/project-analysis-testing-4c91c4).
Инженерно (2–3 недели)
  • Влить release/1.0 в main: main отстал на 96 коммитов.
  • P1 по Noise-сессии, P2 по лимитерам, сумке курьера и отслеживанию по BLE — сделано 16.10; осталось: убрать ID из объявления в локальном Wi‑Fi.
  • Полевая матрица: 100 сообщений в обе стороны, 100 разрывов, курьер A→C→B ×3, 100 файлов SHA-256, 20 звонков по 5 мин.
  • Уменьшить APK — сделано в beta3: 35 МБ вместо 74.
  • Сделать iOS-онбординг таким же подробным, как на Android; добавить «Отменить» у заявки.

План по фазам

Основа — docs/PLAN.md от 18.09. Статусы обновлены по сегодняшней проверке, сроки рассчитаны на 2 инженеров и QA.

Фаза 0 — Зафиксировать продукт готово ~90 %

Ветка release/1.0, документация STATUS/PLAN, iOS Release собирается, Gradle требует JDK 21.

Осталось: git-remote, живой CI, влить в main. Срок: 1–2 дня.

Фаза 1 — Надёжная связь и доставка код готов, поле частично

Автостарт mesh, баннер состояния, reconnect-ACK (12/12 на железе), знакомство Android→iPhone, LAN keepalive, panic wipe, честный статус Bluetooth на iOS.

Приёмка не закрыта: 100 сообщений, 100 разрывов, курьер ×3, 9 из 10 знакомств быстрее 30 с. Срок: 1 неделя полевых прогонов.

Фаза 2 — Понятный интерфейс ~85 %

Список чатов с фильтрами, статусы с раскрытием маршрута, карточка контакта, словарь без жаргона, RU/EN, доступность, Maestro-флоу. Сегодня исправлены цвета контейнеров, кнопка и аватар.

Осталось: единый вход в профиль, iOS-онбординг, «Отменить» у заявки, тест на 8 людях без инструкций. Срок: 1 неделя.

Фаза 3 — Файлы, звонки, батарея ~65 %

Файлы по BLE Android↔Android починены, EXIF-очистка на iOS, автоприём малых файлов, разрешения перед звонком, час soak без утечек памяти.

Осталось: 100 файлов SHA-256, 20 звонков Android↔iPhone, батарея без кабеля, уменьшение APK. Срок: 1–2 недели.

Фаза 3.5 — Безопасность (новая) ~75 %

Сделано 16.10: окно nonce и устойчивость Noise-сессии к подделкам (Android + iOS), LRU-лимитеры. Также 16.10: квоты и справедливое вытеснение в сумке курьера на всех трёх клиентах. И: из рекламы BLE убран постоянный ID. Осталось: явный nonce в протоколе v2, ID в объявлении локальной сети, maxSdkVersion у геолокации.

Срок: 1 неделя. Нужно до закрытой беты: P1 касается приватности переписки.

Фаза 4 — Релиз-кандидат и закрытая бета ~30 %

Готово: политика приватности, «Скопировать отчёт для поддержки» (без телеметрии), сверка SECURITY.md с кодом.

Осталось: Apple Developer + TestFlight, Play internal → closed, 3–4 недели наблюдения, crash-free ≥ 99 %. Срок: 2 недели + наблюдение.

Фаза 5 — После запуска вне 1.0

Интернет-звонки и push, Desktop (macOS/Windows; 415 тестов уже есть), Web/PWA, синхронизация устройств, разбиение MeshEngine (5 746 строк) и MeshController (11 000+ строк) на сервисы.

Рабочий макет: основной сценарий

Интерактивный прототип с цветами из текущей темы и предложенными улучшениями: единый аватар, «Отменить» у заявки, раскрываемый маршрут. Нажимайте вкладки, «Подключить», отправляйте сообщения и файл.

12:30BT ▂▄▆ 86%
∞ChatOffline?АП

Журнал событий протокола

Состояния сообщения как в приложении: в очереди (outbox, шифротекст) → отправлено (ушло в радио) → доставлено (сквозной ACK). Пока связи нет, сообщения ждут в outbox и уходят при её возвращении, без дублей.

Симулятор mesh: flood, TTL и курьер

Модель алгоритма из RelayPolicy/Fanout: узлы в радиусе ретранслируют пакет, TTL уменьшается, дубликаты отбрасывает seen-set. Если пути нет, sealed-конверт уезжает с курьером (spray-and-wait, 3 копии), и курьер передаёт его при встрече.

Нажмите «Отправить».