Готовность 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.
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:testDebugUnitTest | 731 / 0 | 698 существующих + 33 новых (инициалы, приём Noise, лимитеры, сумка курьера, приватность рекламы BLE/LAN/Wi‑Fi Direct, экран «Рядом») |
KMP :sharedLogic:desktopTest | 797 / 0 | 1 пропущен |
Desktop :desktopApp:test | 422 / 0 | только на JDK 21; на JDK 17 падает UnsupportedClassVersionError |
iOS swift test | 880 / 0 | 2 пропущены; +13 новых (приём Noise, сумка курьера, приватность рекламы и DNS-SD) |
| Android lint | 0 ошибок | 237 предупреждений, в основном deprecated API BLE и EncryptedSharedPreferences |
| Сборка release APK | OK | beta2 — 74 МБ; beta3 — 35 МБ после отказа от x86-библиотек для эмуляторов. Conformance-векторы Android↔iOS PASS полностью (с account-v2 и identity-transfer) |
| beta3 на ROG Phone 5: «Отменить запрос» и «Определяем имя…» | PASS | Установлен публично скачанный APK поверх beta2. Полный цикл: «Отменить запрос» → карточка вернулась в «Подключить» → новая заявка → «Ждём ответ». После перезапуска сосед 2 с показан как «Определяем имя…», затем «Demo». Одно нажатие сразу после разблокировки экрана не сработало (система гасит первое касание), повтор прошёл |
| Soak release-сборки, экран выключен, 20 мин | PASS | ROG Phone 5: процесс не перезапускался, память 58–64 МБ без роста, падений 0 |
| iOS под симулятор | OK | без подписи: экран «Хранилище недоступно» (−34018), это ожидаемо; с ad-hoc подписью: обычный онбординг |
| ROG Phone 5: онбординг → Чаты → Рядом → Звонки → Ещё → Настройки | PASS | mesh стартует сам, без лишних нажатий |
| ROG Phone 5: обнаружение соседа по BLE | PASS | виден реальный узел «Demo» |
| ROG Phone 5: «Подключить» → запрос знакомства | отправлен | статус «Ждём ответ»; принять должна вторая сторона |
| Mac ↔ Android по Wi‑Fi (LAN): обнаружение, Noise XX, взаимный SAS | PASS | штатный раннер run-desktop-android-ble-file-physical.sh: 22/22 наблюдения «есть / подключён / аутентифицирован» |
| Mac ↔ Android только по BLE: файл 64 КиБ в обе стороны | PASS | Mac→Android 70 с (572 GATT-записи, 0 сбоев), Android→Mac 29 с; SHA-256 совпал (f8a38b49…), ACK аутентифицирован, Wi‑Fi не использовался. Сборка уже с исправлением Noise (cfc2d8a0). Запуск из Terminal.app: так macOS выдала разрешение Bluetooth |
| Эфир BLE до/после: сканер Mac ловит рекламу ChatOff | PASS | ROG 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 ошибок.
В теме не были заданы роли secondaryContainer и surfaceContainer*. Карточки «Рядом» и «Звонки», выбранные чипы и слайдер брали базовую фиолетовую палитру Material 3, а не зелёный бренд. Теперь заданы все контейнерные роли для светлой и тёмной схем.
Стандартные 24dp отступов у кнопки 56dp сжимали иконку почти до нуля. Теперь отступ нулевой, иконка 28dp.
Было зашито в коде; теперь инициалы из имени («ROG5» → «RO», «Анна Петрова» → «АП»). Плюс онбординг больше не уезжает под статус-бар (edge-to-edge).
До → после






Интерфейс и цвета
Палитра (Android = iOS = Web)
Mint на Night даёт контраст около 13:1, на светлой теме Forest на Paper около 5:1, то есть AA проходит. Ниже, в разделе «Ещё», пять шрифтовых стилей, масштаб текста 85–200 %, темы «Системная/Светлая/Тёмная».
Навигация Android
Шапка бренда (логотип, «Помощь», профиль), нижняя панель Чаты · Рядом · Звонки. Профиль ведёт в «Ещё»: Группы, Передачи, Устройства, Публичный канал, Настройки. Онбординг из одного экрана: имя и объяснение разрешений.





Что ещё улучшить в интерфейсе
| Проблема | Где | Предложение | Приоритет |
|---|---|---|---|
| Два входа в профиль: иконка в шапке и аватар на «Чатах» ведут в одно место | Android, BrandBar + HomeScreen | Оставить один аватар с инициалами в шапке | P2 |
| Карточка «Рядом» не показывает силу сигнала и канал (BLE/Wi‑Fi) | Android/iOS | Иконка канала и «близко / далеко» | P2 |
| Пустой экран чатов не ведёт за руку | Чаты | Кнопка «Найти людей рядом» прямо в пустом состоянии | P3 |
| iOS-онбординг короче Android: нет объяснения разрешений | iOS | Те же два блока, что на Android | P2 |
Экран восстановления хранилища показывает сырой код unexpectedStatus(-34018) | iOS | Прятать под «Подробнее» | P3 |
| Первые секунды после запуска сосед показан как «без имени», пока не придёт подписанный ANNOUNCE (замер 6–11 с, сразу после установки до минуты) | Рядом | Сделано в beta3: «Определяем имя…» до прихода подписанного ANNOUNCE | готово |
| Статус «Ждём ответ» без «Отменить» | Рядом | Сделано в beta3: кнопка «Отменить запрос», как на iOS | готово |
Алгоритмы и протокол
- Обе роли одновременно: 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, remotegiteaуже добавлен; отправку веток должен запустить владелец (git push gitea main release/1.0 claude/project-analysis-testing-4c91c4).
- Влить
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+ строк) на сервисы.
Рабочий макет: основной сценарий
Интерактивный прототип с цветами из текущей темы и предложенными улучшениями: единый аватар, «Отменить» у заявки, раскрываемый маршрут. Нажимайте вкладки, «Подключить», отправляйте сообщения и файл.
Журнал событий протокола
Состояния сообщения как в приложении: в очереди (outbox, шифротекст) → отправлено (ушло в радио) → доставлено (сквозной ACK). Пока связи нет, сообщения ждут в outbox и уходят при её возвращении, без дублей.
Симулятор mesh: flood, TTL и курьер
Модель алгоритма из RelayPolicy/Fanout: узлы в радиусе ретранслируют пакет, TTL уменьшается, дубликаты отбрасывает seen-set. Если пути нет, sealed-конверт уезжает с курьером (spray-and-wait, 3 копии), и курьер передаёт его при встрече.
Нажмите «Отправить».