docs: полный аудит приложения 2026-07-07 (6 направлений, с file:line)

This commit is contained in:
b-dev-mobile
2026-07-07 11:27:33 +00:00
parent 6a07f1f40a
commit 1641ad8ab8
+252
View File
@@ -0,0 +1,252 @@
# Полный аудит f7cloud_mobile — 2026-07-07
> Автор: `b-dev-mobile` (ячейка B). Метод: 6 параллельных агентов-исследователей по
> направлениям (архитектура, безопасность, сеть, производительность/UX, качество кода,
> сборка+vendored-форк). Все находки — с `file:line`.
>
> **Объём:** ~38 500 строк собственного Kotlin в 178 файлах (app/core/feature) +
> ~92 600 строк vendored-форка talk-android (~704 файла). Тестов — **0**. Статанализа — нет. CI — нет.
## Общий вердикт
Приложение функционально зрелое (8 работающих модулей, дисциплинированные ViewModel+StateFlow,
чистый `.gitignore` без секретов, ноль TODO-долга), но построено как «долгий прототип»: релизной
сборки физически не существует, безопасность на уровне dev-стенда, инфраструктура качества
отсутствует, в календарной логике — баги порчи данных.
---
## 🔴 Критическое (до любой раздачи пользователям)
### C1. Release-сборки не существует
- Нигде нет `signingConfig`/`storeFile` (grep по всем `*.gradle` пуст) → `assembleRelease` даёт
неподписанный APK (`INSTALL_PARSE_FAILED_NO_CERTIFICATES`).
- `app/build.gradle:28-33``minifyEnabled true` + `shrinkResources true`, но
`app/proguard-rules.pro`**1 строка (только комментарий)**; `vendor/.../proguard-rules.pro`
всё закомментировано; у `:vendor:talk-app` нет `consumerProguardFiles`; R8 full mode включён
по умолчанию (AGP 8). Точки отказа при R8: LoganSquare `$$JsonObjectMapper` (vendor build.gradle:265),
WebRTC JNI `org.webrtc.**` (:333), Retrofit/gson-модели, EventBus `@Subscribe`, Parceler,
kotlinx.serialization. Вероятен отказ ещё на этапе сборки (missing classes: conscrypt, RxJava `sun.misc`).
- Проект живёт на debug-APK 130 МБ.
### C2. TLS выключается галочкой навсегда (MITM)
- `core/network/.../UnsafeSsl.kt:12-23` — trust-all `X509TrustManager` (пустые checkServerTrusted/
checkClientTrusted) + `hostnameVerifier { _, _ -> true }`.
- Применяется ко всем клиентам: `NetworkFactory.kt:18`, `LoginFlowClient.kt:82,168,216` (передача
логина/пароля при QR-логине). В WebView продублировано: `OfficeWebViewClient.kt:91-92`
(`onReceivedSslError → proceed()`).
- Включается чекбоксом «Доверять сертификату (dev)» `AppScaffold.kt:880-888`, сохраняется в prefs
(`AuthStore.kt:13`) навсегда, молча применяется ко всем последующим сессиям, без предупреждения.
- Fix: убрать из release (`BuildConfig.DEBUG`), заменить на certificate pinning, диалог-предупреждение.
### C3. Пароль аккаунта открытым текстом + бэкапится
- `core/auth/.../AuthStore.kt:6,12``app_password` в обычном SharedPreferences (`f7_auth`,
MODE_PRIVATE). EncryptedSharedPreferences/MasterKey/Keystore в проекте нет вообще (grep пуст).
- `app/src/main/AndroidManifest.xml:25``allowBackup="true"` без `dataExtractionRules`
`f7_auth` (пароль) и `f7_app_lock` (pin_hash) уходят в облачный/adb-бэкап.
- Пароль передаётся через Intent extras: `MailScreen.kt:128-131` → MailComposeActivity (утечка в recents/dumpsys).
- Logout (`AppScaffold.kt:175-180`) — только локальная очистка; серверная ревокация
`DELETE /ocs/v2.php/core/apppassword` НЕ вызывается — токен живёт на сервере.
### C4. ANR-риск на входящем звонке
- `feature/talk-native/.../TalkVendorUserSync.kt:23``runBlocking(Dispatchers.IO)` с сетью/БД.
- Вызывается из `F7MobileApp.onCreate` (`F7MobileApp.kt:41` — блокирует холодный старт) и из
`TalkNativeCallLauncher.launchRoomCall``MainActivity.onCreate:105,120` (клик «принять звонок» на main).
- `F7MobileApp : F7cloudTalkApplication` (F7MobileApp.kt:27) — `super.onCreate()` инициализирует
весь vendor-стек (Dagger, Room+SQLCipher, WorkManager) на холодном старте.
### C5. Тихая порча календарных данных (ICS)
- **Два расходящихся парсера дат**: `CalendarIcs.parseIcsInstant` (`CalendarIcs.kt:176-193`) парсит
`...T100000Z` как ЛОКАЛЬНОЕ время; `CalDavClient.parseIcsInstant` (`CalDavClient.kt:584-602`) — как
UTC. `TZID` игнорируют оба (regex `CalendarIcs.kt:47` отрезает параметры). → события со сдвигом зоны,
по-разному в календаре и задачах.
- **ATTENDEE-парсер сломан**: `CalendarIcs.parseLines` (`:195-206`) кладёт `ATTENDEE;CN=...;PARTSTAT=...`
под один ключ `"ATTENDEE"` с отрезанными параметрами, а `parseAttendeeLine` (`:208-216`) ищет
`CN=`/`PARTSTAT=` в ключе, где их уже нет → участники схлопываются в одного, статусы дефолтные.
При редактировании события это PUT'ится на сервер (порча данных).
- **unescape неверного порядка**: `CalendarIcs.kt:268-269``\n` заменяется до `\\`.
- `CalDavClient.parseVTodo` (`:925-951`) работает без line-unfolding (длинная свёрнутая SUMMARY обрезается).
- vCard: паттерн `N[^:]*:` (`CardDavClient.kt:32-41`) заматчит и `NOTE:`/`NICKNAME:`.
### C6. GPL-3.0 нарушается уже сейчас
- У корневого репо **нет LICENSE/COPYING** (в корне только CHANGELOG/INTEGRATION/NOTICE/PROJECT-STATUS/
README). Vendored GPL-код линкуется в один APK → всё приложение — производная работа под GPL-3.0-or-later.
- Копирайты Nextcloud **заменены** на «F7cloud contributors»: `vendor/.../AndroidManifest.xml:4`
(`SPDX-FileCopyrightText: 2017-2024 F7cloud...`). GPLv3 §4 требует сохранять оригинальные уведомления.
- Раздача даже debug-APK — уже distribution; архив исходников к сборкам не публикуется.
- Проприетарный Firebase слинкован в GPL-APK (`vendor build.gradle:337`, `core/push`). У ячейки есть
свой `f7push` — основной flavor можно сделать без FCM.
---
## 🟠 Архитектура
- **God-composable**: `app/.../ui/AppScaffold.kt:112-671` — 920 строк, ~35 `mutableStateOf`/34
`rememberSaveable` в одной функции: роутер табов + логин (`:675-760`) + push-роутинг (`:358-383`) +
FCM-регистрация (`:299-309`) + permissions + QR. Рекомпозиция корня на любое изменение.
- Связь app→feature — самодельные «nonce-каунтеры»: `filesUploadRequest++`, `mailPushRequest++`
(`AppScaffold.kt:159-168`) — теряют события при гонках. Лучше SharedFlow.
- Слой пробивается в UI: `AppSheets.kt:109-150` (NotificationsSheet дёргает repo из composable),
`MailScreen.kt:83-89` (composable сам собирает OkHttpClient).
- **DI нет** (Hilt/Koin отсутствуют, Dagger только в vendor): ~25 `object`-синглтонов с сетью
(`CalDavClient.kt:80` — object на 1050 строк), inline-фабрики VM в composable (`MailScreen.kt:75-81`).
- Утечка контекста: `app/.../OfficeWebViewPool.kt:12-40` держит WebView с Activity-контекстом до 15 мин.
- **navigation-compose:2.8.9 подключён, но НЕ используется** (нет NavHost): навигация — ручной
`when(activeTab)` (`AppScaffold.kt:535-624`) + boolean-флаги + свой BackHandler (`:428-447`).
Deep links снаружи не работают (нет intent-filter со схемой; `f7cloud://talk` в `MainActivity.kt:79`
недостижим из браузера). `SavedStateHandle` — 0 вхождений: process death теряет внутриэкранное состояние.
- **Мёртвый код**: `feature/widgets` (538 строк, 4 файла) НЕ в `settings.gradle` — не компилируется;
`core/data`, `core/ui` — пустые заготовки в `settings.gradle:23,28`.
- Топ god-файлов: `MailComponents.kt` 1898, `TalkComponents.kt` 1591, `FilesComponents.kt` 1457,
`CalendarComponents.kt` 1362, `TasksComponents.kt` 1066, `CalDavClient.kt` 1050, `MailViewModel.kt` 1012,
`MailRepository.kt` 991, `AppScaffold.kt` 920.
## 🟠 Сетевой слой
- **Новый `OkHttpClient` на каждый запрос**: `NetworkFactory.kt:14-20` — фабрика строит новый
`.build()`; вызывается в начале каждого метода репозитория (`MailRepository.kt:635-641`,
`TalkRepository.kt:331-332`, `FilesRepository.kt:20,62,79...`, ещё 31 call-site). Нулевое
переиспользование соединений (TLS-handshake на каждый запрос), утечка ExecutorService.
`SupportRepository.kt:340` — вообще без callTimeout.
- HTTP-кэша (`.cache()`) нет; ETag/If-Modified-Since не используется (только If-Match при записи CalDAV).
- **Обработка 401 скопирована ~60 раз** (`if (code == 401) throw UnauthorizedException()` в каждом
методе) вместо interceptor'а. OCS-конверт распарсен независимо 4 раза (`TalkRepository.getOcsResponse:367`,
`WidgetsRepository.getOcsData:124`, `NotificationsRepository:32`, `AppMenuRepository:20`) при живом
`OcsJson.kt`. Заголовок `OCS-APIRequest` вручную в 37 местах. DAV-хелперы (`localTag/readText/toDavPath/
resolveHref`) скопированы трижды (`DavClient.kt:242`, `CalDavClient.kt:867/1024`, `CardDavClient.kt:288`).
- **Парсинг 100% ручной** org.json (`optString` — 206 вызовов, молчаливые дефолты) + ICS/vCard регэксами.
- `F7PushRegistrar.kt:53-77` — сырой `HttpURLConnection`, дублирует Basic-auth и **игнорирует
trustAllCerts** (push сломан на self-signed).
- **Инкрементальной синхронизации DAV нет**: ни sync-token/sync-collection, ни CTag. Контакты качают
**все vCard с base64-фото** и потом `.take(500)` (`CardDavClient.kt:142-157`). Календарь делает
`listCalendars` PROPFIND перед каждой операцией (`CalendarRepository.kt:188-204`); задачи —
`listTaskLists` на каждый toggle (`TasksRepository.kt:138-145`).
- `WidgetsRepository.kt:65``withTimeoutOrNull(2000)` поверх блокирующего `execute()` (отмена не
прерывает сокет). Talk long-poll (`TalkRepository.kt:26-41`) — readTimeout == серверному таймауту (гонка).
- Main-safety на соглашении: репозитории `suspend`, но без `withContext(Dispatchers.IO)` внутри
(`MailRepository`, `TalkRepository.kt:58`); `TasksRepository`/`CalendarRepository`/`AppMenuRepository`
вообще не suspend. Спасает только дисциплина вызывающих (`viewModelScope.launch(Dispatchers.IO)`).
## 🟠 Производительность и UX
- **APK ~130 МБ**: `settings.gradle:35-36` включает весь vendor как библиотеку (`feature/talk-native`
`api project(':vendor:talk-app')`). WebRTC `android-talk-webrtc:132.6834.0` (vendor build.gradle:333) —
`libjingle_peerconnection_so.so` на **4 ABI** (~30-45 МБ × ABI ≈ 60-70 МБ); sqlcipher (:281),
conscrypt (:238) — тоже × 4 ABI; `emoji2-bundled` (~10 МБ); 48 локалей vendor. Ни `splits`, ни
`abiFilters`, ни `bundle{}`, ни `resConfigs` нигде нет.
`abiFilters 'arm64-v8a','armeabi-v7a'` + `resConfigs 'ru','en'` + рабочий R8 = 130 → **30-45 МБ**.
- Compose: ключи чата Talk включают `$index` (`TalkScreen.kt:597-604`) — новое сообщение пересоздаёт
весь список; `formatEnvelopeDate` (`MailComponents.kt:644,1480-1507`) создаёт 2×Calendar +
SimpleDateFormat на каждую строку/рекомпозицию; `F7RelativeTime.kt:22``DateTimeFormatter.ofPattern`
на каждый вызов. `derivedStateOf` — 3 шт. на проект.
- Иконки UI грузятся **с сервера как SVG по сети** (`FilesComponents.kt:265,310,325,356`,
`MailComponents.kt:650` — звезда в каждой строке письма, ~15+ мест) → офлайн списки лысеют.
Coil diskCache = 2% cacheDir (`F7MobileApp.kt:79-94`), `crossfade(false)`.
- Офлайн-кэш: только контакты (Room, с TTL 10 мин — образец), файлы (огрызок: имя+флаг для корня,
`FilesRepository.kt:40-53`), почта (свой JSON-файловый кэш `MailCacheRepository.kt` — третий
самописный механизм). Календарь/задачи/Deck/поддержка/виджеты офлайн — пустой экран.
- `runBlocking` на старте (см. C4). SplashScreen API не используется (белый экран на Android 8-11).
- **Тёмной темы нет**: `F7Theme.kt:29-36` всегда `F7LightScheme`, нет `isSystemInDarkTheme`/`values-night`;
+ 77 инлайновых `Color(0xFF...)` мимо `F7Colors` (`FilesComponents.kt:305`, `SupportComponents.kt:64-66`...).
- **Локализация невозможна**: `stringResource` — 6 раз, в `strings.xml` 15 строк; 73+ русских строк
захардкожены в `Text("...")`.
- Pull-to-refresh только в Файлах (`FilesScreen.kt:219`). Ошибки местами Toast без retry
(`MailScreen.kt:348,405`). Skeleton/shimmer нет (51 CircularProgressIndicator).
`contentDescription=null` в 66 местах (в т.ч. кликабельные иконки). WindowSizeClass/sw600dp — нет.
## 🟠 Качество кода и процессы
- **0 тестов** (нет `src/test`/`src/androidTest` ни в одном модуле). Мешают: 40+ `object` с логикой,
репозитории без интерфейсов, `LocalDate.now()`/`ZoneId.systemDefault()` внутри логики
(`TasksGrouping.kt:23`, `F7RelativeTime.kt:15`, `CalendarIcs.kt:121,184`) — Clock не инжектируется.
Готовые кандидаты на чистый JUnit: `CalendarIcs`, `TasksGrouping`, `LoginFlowClient`.
- Обработка ошибок: 223 `runCatching` (~65 глотают через getOrNull/getOrDefault), ~121 `error()` как
flow-control (нетипизированный IllegalStateException — «нет сети» и «битый JSON» неразличимы),
молчаливые catch (`CardDavClient.kt:437`, `DavClient.kt:237,252`, `F7PushRegistrar.kt:87-89` — пустое
тело). Логирования почти нет (35 Log.*/println на проект).
- `!!` — 70 шт. (UI-гонки: `MailSettingsScreens.kt:557-558` `draft!!`, `TalkComponents.kt:876`).
QR-логин ломается на пароле с `&` (`LoginFlowClient.kt:196+`, лимит `size > 3`). Курсор пагинации
почты обрезает Long→Int (`MailRepository.kt:54-62`). `loadThread``getOrDefault(emptyList())`
(сеть-ошибка = «переписки нет», `MailRepository.kt:88-93`).
- Дубли: относительное время — 4 реализации (`F7RelativeTime.kt`, `TalkFormatting.kt:94`,
`MailComponents.kt:1480`, `SupportComponents.kt:68`); открытие файла с тостом дословно скопировано
(`SupportFileIO.kt:56-66` == `LocalFileOpener.kt:14-25`); `serverUrl.trimEnd('/')` — 40+ мест.
- Магия: 77 `Color(0x...)` вне designsystem; ~1087 `.dp` без spacing-токенов; 52 захардкоженных
`ocs/v2.php|remote.php` пути (нет `ApiPaths`); таймауты литералами (`F7PushRegistrar.kt:54-55`
`15000`, `TalkAttachmentUploader.kt:49-51` 5/5/6 мин).
- **lint/detekt/ktlint не настроены** (нет `lint{}`, baseline, `.editorconfig`).
- **Version catalog отсутствует** (`gradle/libs.versions.toml` нет): coroutines 1.8.1/1.9.0/1.10.1/1.10.2,
Coil 2.6.0/2.7.0 (при Coil 3.x актуальном), два Compose BOM (свой 2025.02 vs vendor 2026.01),
Firebase тремя декларациями (bom 33.7.0 / messaging 25.0.1 / 24.0.1), zxing 3.3.0 (2017 г.),
guava целиком (`app/build.gradle:101`). `org.json:json:20240303` дублирует платформенный (бесполезен).
- `gradle.properties`: `parallel=false` (тормоз 20 модулей), `enableJetifier=true` (legacy),
`nonTransitiveRClass=false`; тестовые `NC_TEST_SERVER_*` — мусор от vendor.
- Git: `.gitignore` полный (build/, local.properties, *.apk, keystores, google-services.json закрыты —
секретов в репо НЕТ). LFS не используется; `design/` — 1.66 МБ (28 PNG); vendor закоммичен целиком
(1484 файла, pack 6.95 МБ) без своей git-истории.
## Vendored-форк talk-android (v23.0.0)
- Подключён только модуль app форка (`settings.gradle:37-38`), режим library (`f7cloudAsLibrary=true`,
flavor `f7`). `feature:talk-native` — тонкая обёртка (8 файлов, 364 строки), реально используется
<10% vendor (звонковый стек: CallActivity, CallForegroundService, F7cloudTalkApplication, F7TalkUserSync).
Чат — свой Compose-модуль `feature:talk` (OCS напрямую). Но компилируется и в APK попадает весь vendor.
- Ребрендинг пакетов полный: `com.nextcloud.talk``ru.f7cloud.talk` (609/704 файла). Остатки
`com.nextcloud` (34 файла) легитимны (внешняя библиотека nextcloud-android-common:ui, mimeType, Transifex).
- **Git-истории форка НЕТ** (один коммит «Initial import», у vendor нет своего `.git`) → обновление на
новый апстрим = повторный ручной rename 700 файлов. Рекомендация: отдельный git-репо с ветками
`upstream` (чистый v23) / `f7` (rename-скрипт + патчи коммитами).
- `build.f7cloud.gradle:6``DEBUG=true` захардкожен и для release (debug-логи vendor в проде).
- `README.F7CLOUD.md:20` — «not yet compiled» устарел (модуль давно в графе).
- Cleartext: `vendor/.../network_security_config.xml:9``cleartextTrafficPermitted="true"` + доверие
user-CA; наследуется в app. Fix: переопределить свой networkSecurityConfig.
- Совместимость v23-клиент ↔ сервер spreed 22 (Talk API v4): vendored-клиент договаривается по
capabilities (`ApiUtils.kt:100-131` conversation-v4→v1; `CapabilitiesUtil.kt:336` SERVER_VERSION_MIN=17) —
безопасно, фичи 23 без поддержки просто не включатся. НО собственный `feature:talk` хардкодит `/api/v4`
без capability-гейтов (`TalkRepository.kt:17,37,46...`) — для spreed 22 ок, регресс-тест звонков на
реальном 22 обязателен.
## GPL-3.0 — что сделать (см. C6)
1. `LICENSE` (GPL-3.0-or-later) в корень; свой код остаётся вашим (копирайт ваш), лицензируется под GPL.
2. Вернуть копирайты Nextcloud в изменённых заголовках vendor (свои — добавлять рядом).
3. Публиковать архив исходников с каждым распространяемым APK (шаг CI, git-тег в Gitea).
4. Firebase+GPL: основной flavor без FCM на базе `f7push`/UnifiedPush, FCM — опциональный flavor.
5. Бренд/дизайн GPL НЕ затрагивает (это товарный знак) — тема и UI сохраняются полностью.
---
## План действий (приоритизированный)
### Этап 0 — санитария релиза (дни) = п.1 плана mail/035
1. Keystore + `signingConfig` из env/CI secrets; proguard-правила (webrtc/LoganSquare/EventBus/
`-dontwarn`) + `consumerProguardFiles` у vendor; до готовности — `minifyEnabled false` как страховка.
2. `abiFilters 'arm64-v8a','armeabi-v7a'` + `resConfigs 'ru','en'` → минус ~половина APK.
3. Безопасность-минимум: trust-all → только debug; `allowBackup="false"`; `AuthStore`
EncryptedSharedPreferences с миграцией; ревокация app password при logout; `FLAG_SECURE` на логин/PIN.
4. GPL: LICENSE в корень, вернуть копирайты Nextcloud, публикация исходников с APK.
5. `gradle.properties`: `parallel=true`, `nonTransitiveRClass=true`, убрать jetifier; удалить мёртвые
`core/data`, `core/ui`, решить судьбу `feature/widgets`.
### Этап 1 — фундамент (2-3 недели)
6. CI (Gitea Actions, инфра B): lint+тесты → assembleDebug на push → по тегу подписанный release +
исходники; versionCode из CI.
7. Единый `OkHttpClient` (пул, `.cache(20MB)`, interceptor 401, OCS-заголовки); `withContext(IO)`
внутрь репозиториев; убрать `runBlocking` из старта и пути звонка.
8. Первые 10 unit-тестов (ICS-парсер первым — зафиксировать баги C5, потом чинить); detekt+ktlint
с baseline; version catalog.
9. Починить баги данных: единый ICS-парсер с TZID/ATTENDEE/unescape; QR-пароль с `&`; курсор почты.
### Этап 2 — систематика (1-2 месяца)
10. DI (Hilt / ручной AppContainer), интерфейсы репозиториев; разбор AppScaffold на MainViewModel +
LoginScreen; navigation-compose по-настоящему или удалить.
11. kotlinx.serialization вместо org.json; sync-token/CTag для DAV; кэш календаря/задач; локальные иконки.
12. UX: pull-to-refresh везде, единый F7ErrorState+retry, SplashScreen, строки→strings.xml→тёмная тема.
13. Vendor: отдельный git-репо (upstream/f7 ветки); flavor без Firebase на базе f7push.
## Что хорошо (сохранять)
ViewModel-слой единообразен (viewModelScope.launch(IO), без блокировок main кроме talk-bootstrap);
просмотр HTML-писем безопасен (`MailMessageBodyView.kt:69` — JS off, domStorage off, фильтр запросов);
кэш контактов с TTL — образец; секретов в git нет; ключи LazyColumn почти везде на месте; ребрендинг
vendor чистый; собственная пагинация почты работает; биометрия корректна (`AppLockGate.kt:67-73`).