chore: initial commit — recon artifacts + design spec + Phase 1 plan

This commit is contained in:
2026-05-16 21:58:10 +10:00
commit d6eeb0cc50
20 changed files with 338613 additions and 0 deletions
+40
View File
@@ -0,0 +1,40 @@
# Recon artifacts (large binaries, not for code review / not shareable)
recon/artifacts/
recon/patched/
*.apk
*.xapk
*.mitm
*.har
*.dmp
# Secrets
.env
.env.*
!.env.example
minio.env
*.key
*.pem
*.p12
secrets/
# Python
__pycache__/
*.pyc
.venv/
venv/
.pytest_cache/
# Node
node_modules/
dist/
build/
.vite/
*.log
# OS
.DS_Store
Thumbs.db
# IDE
.idea/
.vscode/
+25
View File
@@ -0,0 +1,25 @@
# PremiumDriverApp
Замена вендорного Android-приложения для водителей PremiumPark, с привязкой к Premium CRM (TaxiDashboard).
## Контекст
Текущее приложение для водителей поставляется подрядчиком 1С на условиях абонентки. Подрядчик заморозил разработку. Цель проекта — построить собственное приложение, сохранив совместимость со старым функционалом (балансы, штрафы, платежи) и добавив новые функции (эксплуатация авто).
## Структура
```
PremiumDriverApp/
├── docs/superpowers/specs/ Дизайн-документы (spec'и) по этапам
├── recon/ Подпроект «Разведка» — изучение старого приложения
│ ├── artifacts/ APK, декомпил, дампы трафика (не в git)
│ ├── scripts/ mitmproxy фильтры, frida-скрипты
│ └── findings/ Карта API, диаграммы, отчёты
└── README.md
```
## Текущая фаза
**Подпроект «Разведка»** — см. [docs/superpowers/specs/2026-05-11-driver-app-recon-design.md](docs/superpowers/specs/2026-05-11-driver-app-recon-design.md).
Следующие подпроекты («Новое приложение», «BFF», «Платёжный канал») запускаются после утверждения отчёта разведки.
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,305 @@
# Driver App Recon — Design Doc
- **Дата:** 2026-05-11
- **Статус:** Draft, ожидает review владельца
- **Владелец:** vladtechno@gmail.com (PremiumPark)
- **Родительский проект:** PremiumDriverApp
- **Экосистема:** Premium CRM (TaxiDashboard)
---
## 1. Контекст и проблема
У PremiumPark есть Android-приложение для водителей проката, поставляемое подрядчиком 1С на условиях абонентской платы. Текущее приложение умеет:
- авторизовать водителя (логин/пароль),
- показывать остаток баланса и список штрафов,
- принимать платежи через приложение (детали неизвестны, выяснит разведка).
Все оплаты от водителей принимает юр.лицо PremiumPark. Подрядчик прекратил доработки приложения, оставаясь при этом получателем абонентской платы. Никаких функций эксплуатации авто (ТО, ремонты, фото повреждений, заявки в техслужбу) в приложении нет.
Стратегическая цель — построить собственное приложение, интегрированное с Premium CRM, и постепенно вытащить интеллектуальную собственность из 1С, сняв зависимость от подрядчика.
**Настоящий документ описывает только подпроект «Разведка»** — фазу изучения существующего приложения, предшествующую любой разработке. Подпроект «Новое приложение» имеет отдельный дизайн-док, который пишется после завершения разведки.
## 2. Цели и не-цели
### Цели
1. Получить полную карту HTTP/HTTPS-эндпоинтов, к которым ходит существующее приложение.
2. Документировать поток аутентификации и lifecycle сессии/токенов.
3. Документировать платёжный поток end-to-end: провайдер, инициация, токенизация, фискализация (54-ФЗ), связь с балансом в 1С.
4. Документировать механизм обновлений (poll / push / FCM) — как водитель узнаёт о новом штрафе.
5. Идентифицировать защитные механизмы (TLS pinning, обфускация, root detection, request signing, Play Integrity).
6. Принять решение go/no-go по подпроекту «Новое приложение» на основе фактов.
### Не-цели
- Не разрабатываем код нового приложения, BFF или платёжного канала.
- Не модифицируем APK и не публикуем «свой клиент».
- Не подключаем mitmproxy к боевому трафику реальных водителей — работаем только на тестовом аккаунте, заведённом в 1С специально для разведки.
- Не передаём APK, дампы, decompiled-исходники или отчёты третьим лицам.
- Не реализуем платёжный канал в этом подпроекте.
## 3. Юридическая рамка
- Декомпиляция выполняется на собственном устройстве/эмуляторе пользователя, с собственным тестовым аккаунтом, для целей совместимости и переноса данных — попадает под ст. 1280 ГК РФ (декомпилирование программы для ЭВМ для interoperability).
- PremiumPark является плательщиком абонентской платы за продукт и владельцем данных (балансы, штрафы, платежи водителей).
- Артефакты разведки хранятся локально, в `recon/artifacts/`, не публикуются в git (см. `.gitignore`).
- В случае официального запроса от подрядчика — разведка приостанавливается, артефакты архивируются.
## 4. Технический стенд
### 4.1 Хост
- Windows 11 (рабочая машина владельца)
- Python 3.10+ (для mitmproxy и frida-tools)
- Android Studio с Emulator и Platform-tools (`adb`)
- Свободное место: ≥30 GB (эмулятор + APK + дампы + декомпил)
### 4.2 Эмулятор Android
- **AVD:** Pixel 6 или Pixel 4
- **System Image:** Google APIs (НЕ Google Play) — это критично, иначе `adb root` недоступен и не получится положить mitmproxy CA в системные сертификаты
- **API Level:** 33 или 34
- **Архитектура:** x86_64 (для совместимости с frida-server)
При обнаружении сильной анти-эмуляторной защиты допустимо переключение на физическое Android-устройство с Magisk; это указано как fallback в плане исполнения.
### 4.3 Инструменты
| Инструмент | Назначение | Установка |
|---|---|---|
| `jadx-gui` | Декомпиляция APK в Java | github.com/skylot/jadx/releases |
| `apktool` | Распаковка манифеста и ресурсов | scoop install apktool |
| `mitmproxy` | Перехват HTTPS-трафика | `pip install mitmproxy` |
| `frida-tools` | Динамические хуки | `pip install frida-tools` |
| `objection` | Frida-обёртка с готовыми bypass'ами | `pip install objection` |
| `adb` | Управление эмулятором | Android SDK Platform-tools |
Конкретные команды установки и шаги настройки идут в план исполнения (writing-plans), не в спеку.
### 4.4 Файловая структура проекта
```
PremiumDriverApp/
├── README.md
├── .gitignore # игнорирует recon/artifacts/
├── docs/superpowers/specs/
│ └── 2026-05-11-driver-app-recon-design.md # этот документ
└── recon/
├── artifacts/ # НЕ в git
│ ├── original.apk # вытащенный APK
│ ├── decompiled/ # вывод jadx
│ ├── unpacked/ # вывод apktool
│ └── flows/ # mitmproxy дампы (*.mitm)
├── scripts/
│ ├── frida-bypass-pinning.js # шаблон обхода TLS pinning
│ ├── mitm-filter.py # фильтр mitmproxy для шумовых хостов
│ └── setup-emulator.ps1 # подготовка AVD
└── findings/
├── 00-final-report.md # финальный отчёт + go/no-go
├── 01-static-analysis.md
├── 02-network-capture.md
├── 03-auth-flow.md
├── 04-payment-flow.md
├── 05-realtime.md
└── 06-defenses.md
```
## 5. Этапы разведки
### Этап 1. Статический анализ APK
**Цель:** понять, что в принципе делает приложение, найти константы (URL, ключи провайдеров, package id), оценить уровень защиты.
**Действия:**
1. Получить APK: `adb shell pm path <package>``adb pull <path> recon/artifacts/original.apk`
2. Декомпиляция: `jadx-gui recon/artifacts/original.apk``recon/artifacts/decompiled/`
3. Распаковка манифеста: `apktool d original.apk -o recon/artifacts/unpacked/`
4. Извлечь из manifest и кода:
- `package` name, version, target SDK, min SDK
- permissions (особенно `INTERNET`, `RECEIVE_SMS`, `ACCESS_FINE_LOCATION`, payment-related)
- `network_security_config.xml` (cleartext policy, pinning declarations)
- встроенные base-URL — grep по `https://`
- сторонние SDK — поиск пакетов `ru.yoomoney`, `ru.tinkoff`, `com.google.firebase`, `okhttp3`, `retrofit2`
5. Оценить обфускацию: имена классов читаемые → R8/ProGuard не агрессивный; одно-/двухбуквенные имена → агрессивный.
6. Найти root/emulator detection: grep по `RootBeer`, `isDebuggerConnected`, `build.fingerprint`, `Build.PRODUCT`.
**Deliverable:** `recon/findings/01-static-analysis.md` со следующими разделами:
- Package metadata
- Permissions
- Network config
- Встроенные URL-ы и хосты
- Сторонние SDK
- Уровень обфускации
- Защитные механизмы (предварительный список)
### Этап 2. Перехват сетевого трафика
**Цель:** получить полный лог HTTP/HTTPS-запросов в обычных пользовательских сценариях.
**Действия:**
1. Подготовить эмулятор:
- `emulator -avd Pixel_6_API_34 -writable-system -no-snapshot`
- `adb root` → доступ к /system
- Установить mitmproxy CA в `/system/etc/security/cacerts/` (имя файла — hash сертификата + `.0`)
2. Прописать прокси: `Settings → Network → WiFi → Modify → Proxy: manual → 10.0.2.2:8080`
3. Запустить mitmproxy на хосте: `mitmweb --listen-port 8080`
4. Установить приложение: `adb install recon/artifacts/original.apk`
5. Прогнать сценарии (каждый — отдельным mitmproxy save):
- **S1. Вход:** запуск приложения → ввод логина/пароля → попадание на главный экран
- **S2. Просмотр баланса:** открытие экрана баланса, ожидание 60 сек (захватить полл, если есть)
- **S3. Просмотр штрафов:** открытие списка штрафов, открытие одного штрафа
- **S4. Экран платежа (без оплаты):** перейти к оплате, выбрать позицию, дойти до выбора способа оплаты, остановиться
- **S5. Инициация платежа:** довести до момента подтверждения, **но не платить реальной картой**
- **S6. Выход:** logout
6. Сохранить flow'ы: `recon/artifacts/flows/S1-login.mitm`, `S2-balance.mitm`, …
**При TLS pinning:**
- Запустить frida-server в эмуляторе.
- `objection --gadget <package> explore``android sslpinning disable`
- Если не помогает — кастомный Frida-скрипт (шаблон в `recon/scripts/frida-bypass-pinning.js`)
**Deliverable:** `recon/findings/02-network-capture.md` с таблицей всех увиденных эндпоинтов:
| Endpoint | Method | Auth | Сценарий | Запрос (схема) | Ответ (схема) | Частота |
|---|---|---|---|---|---|---|
И ссылки на исходные mitm-файлы для воспроизведения.
### Этап 3. Глубокое погружение в auth
**Цель:** понять формат и lifecycle токена, наличие подписей.
**Действия:**
1. Распарсить токен (jwt.io если JWT) — header, payload, claims, alg, iss, exp.
2. Проверить, есть ли request signing (HMAC, подпись тела) — искать в декомпиле использование `Mac.getInstance` / `Signature`.
3. Сэмплировать сессии:
- повторный логин → тот же токен или новый?
- есть ли refresh-токен и отдельный refresh-эндпоинт?
- что происходит при 401?
4. Построить sequence diagram (Mermaid).
**Deliverable:** `recon/findings/03-auth-flow.md`
### Этап 4. Платёжная подсистема (центральный этап)
**Цель:** разобрать поток оплаты end-to-end, выявить провайдера и механизм фискализации.
**Гипотезы, которые проверяем:**
- H1. Приложение использует SDK провайдера (YooKassa / Tinkoff Acquiring / СберPay) и общается напрямую с провайдером после получения init-токена от 1С.
- H2. Приложение прокидывает данные в 1С, а 1С общается с провайдером (приложение видит только наши эндпоинты).
- H3. Гибрид: init через 1С → провайдер через SDK → колбэк в 1С.
**Действия:**
1. В декомпиле найти SDK провайдера (по package name).
2. В трафике (этап 2, сценарий S5) изолировать запросы к провайдерским доменам vs к 1С.
3. Проследить токенизацию: куда уходят данные карты — в провайдер напрямую или в 1С (если последнее — большой ред-флаг по PCI DSS).
4. Понять чек 54-ФЗ:
- кто формирует — 1С / провайдер / ОФД напрямую?
- где водитель видит чек (push, email, экран приложения)?
5. Корреляция платёж ↔ баланс:
- до платежа `GET /balance` → X
- инициация → колбэк → когда `GET /balance` возвращает X + delta? (сразу/асинхронно/после явного pull)
6. Sequence diagram (Mermaid) с участниками: App, 1С, Provider, ОФД.
**Безопасность разведки:** не доводить до фактического списания. Останавливаемся на моменте, когда приложение запрашивает у провайдера init-токен и формирует webview/redirect — этого достаточно для понимания флоу.
**Deliverable:** `recon/findings/04-payment-flow.md`
### Этап 5. Push / realtime
**Цель:** понять, как водитель узнаёт о новом штрафе/начислении.
**Действия:**
- Проверить наличие FCM в манифесте (`com.google.firebase.MESSAGING_EVENT`).
- Если FCM — посмотреть payload (требует регистрации в FCM или симуляции).
- Если нет FCM — проверить наличие long-polling эндпоинта в трафике.
- Если возможно — попросить ответственного администратора 1С создать тестовый штраф для тестового водителя и засечь, как именно эта инфа приходит.
**Deliverable:** `recon/findings/05-realtime.md`
### Этап 6. Защитные механизмы (фоном)
В течение всех этапов фиксировать в `recon/findings/06-defenses.md`:
- TLS pinning — есть / нет / способ обхода
- Root/emulator detection — есть / нет / срабатывает ли на нашем стенде
- Request signing / HMAC — есть / нет / откуда берётся ключ
- Play Integrity API — есть / нет
- Антидебаг и анти-Frida хуки
### Этап 7. Финальный отчёт
**Deliverable:** `recon/findings/00-final-report.md`. Структура:
1. **Executive Summary** (1 страница, для бизнес-обсуждения)
2. **Полная карта API** (агрегированная таблица из этапа 2)
3. **Auth-flow** (Mermaid + текст)
4. **Payment-flow** (Mermaid + текст, ключевая часть)
5. **Realtime** (Mermaid + текст)
6. **Защитные механизмы и их преодоление**
7. **Оценка трудозатрат на «Новое приложение»** (грубая, в неделях разработчика)
8. **Рекомендация go/no-go**
9. **Открытые риски и неизвестные**
## 6. Критерии go / no-go
### Go — двигаемся в подпроект «Новое приложение»
- API эндпоинты и аутентификация поняты, стабильны (нет признаков частой смены контракта).
- Платёжный поток разобран, и видна возможность построить параллельный канал (свой эквайринг → свой backend → свой чек).
- Защитные механизмы преодолимы статически (т.е. мы понимаем, как мимикрировать или обойти без runtime-вмешательства).
- В трафике нет признаков hardware-bound криптографии, доступной только подрядчику.
### No-go — пересмотр стратегии
- API использует interactive challenge, который нельзя автоматизировать (CAPTCHA, манипуляция UI и пр.).
- Платёжный поток жёстко зависит от 1С на стороне сервера, и параллельный канал требует доступа к серверным обработкам.
- Используется криптография, привязанная к серверному секрету подрядчика (например, server-issued tokens с проверкой подписи приложения).
- Уровень защиты несоразмерен ценности — реверс займёт больше, чем оригинальная разработка с нуля.
### В случае no-go — альтернативы
1. **Прямые переговоры с подрядчиком** о публикации API (на руках уже будет отчёт разведки — рычаг в диалоге).
2. **Companion App** — приложение **только с новыми функциями** (блок B: эксплуатация авто), старое приложение остаётся для балансов/платежей. Реверс не нужен.
3. **Полная замена 1С** — крупный проект, не на этом цикле.
## 7. Open questions (на разведку)
Заполняются по ходу. Начальный список:
- Package name приложения — будет известно из APK на этапе 1.
- Платёжный провайдер — выясняется на этапе 4.
- Частота поллинга баланса — выясняется на этапе 2.
- Используется ли FCM — выясняется на этапе 5.
- Уровень обфускации — выясняется на этапе 1.
## 8. Следующий шаг
После утверждения этой спеки владельцем:
1. Создаём подробный план исполнения через `writing-plans` skill — пошагово, с чёткими DoD по каждому этапу.
2. Скрипты-заготовки в `recon/scripts/`: `setup-emulator.ps1`, `frida-bypass-pinning.js`, `mitm-filter.py`.
3. README с инструкцией «как развернуть стенд на пустой машине».
4. Запускаем этап 1 (статический анализ).
---
## Приложение A. Контактные точки с экосистемой Premium CRM
Этот подпроект ничего не пишет в боевую инфраструктуру (PremiumCRM, PostgreSQL, n8n, ASR-стек) и ничего не читает оттуда. Разведка изолирована — все артефакты локальны.
Контактные точки появятся **только** в подпроекте «Новое приложение»:
- FastAPI backend PremiumCRM получит новые роуты `/api/v1/driver/*`.
- PostgreSQL получит новые таблицы (maintenance, repairs, tickets, photos, driver_external_ids).
- Дейв (user_id=26) может стать author'ом авто-комментариев в новых сущностях.
- Telegram-бот @p_park_bot — потенциальный канал доставки OTP при логине нового приложения (TBD на этапе дизайна нового приложения).
@@ -0,0 +1,397 @@
# Premium Механик — PWA Design Doc
- **Дата:** 2026-05-16
- **Статус:** Draft, ожидает review владельца
- **Владелец:** vladtechno@gmail.com (PremiumPark)
- **Родительский проект:** PremiumDriverApp
- **Экосистема:** Premium CRM (TaxiDashboard) на crm.pptaxi.ru
- **Предшественник:** реверс-разведка приложения «Механик» NaughtySoft (см. `01-static-analysis.md` + перехват трафика)
---
## 1. Контекст и цель
Сотрудники-механики PremiumPark сейчас работают через приложение «Механик» вендора NaughtySoft (`com.naughtysoft.ttc`, Xamarin/.NET). Все данные осмотров уходят в 1С вендора, доступ к ним из PremiumCRM невозможен без прохода через вендорские API.
Через разведку (см. `02-network-capture.md`) подтверждён формат backend-API вендора (REST на `api.ttcontrol.naughtysoft.ru`). Эндпоинты осмотров (`/api/vehicletechinspection/byvehicle`, `/api/vehiclestate/history`, `/api/image/<id>`, `/api/inspectionvideo`) подтверждают, что **модель данных осмотров с координатами повреждений и фото уже существует и нам полностью видна**.
**Цель проекта:** построить **собственное** приложение «Premium Механик» (PWA), которое:
1. Полностью заменяет вендорское приложение для сотрудников-механиков.
2. Хранит данные осмотров **в нашей PostgreSQL** в PremiumCRM, не отправляя их к вендору.
3. Поддерживает основные кейсы: съёмка фото авто, обводка повреждений на фото, метки повреждений с координатами, сравнение с прошлым осмотром при приёмке.
4. Развёртывается без RuStore / App Store — деплой = `git push`.
**Долгосрочная цель:** отвязать парк от 1С вендора, начав с самой ценной модели данных (осмотры с фото).
## 2. Цели и не-цели
### Цели
1. Делать осмотр авто: 4-8 фото по сторонам, метки и обводки повреждений на каждой фото.
2. При приёмке машины обратно от водителя — показать прошлые повреждения для сравнения «было / стало».
3. Все данные осмотра хранятся в PostgreSQL PremiumCRM (новая БД-схема), без участия вендора.
4. Доступ для авторизованных сотрудников-механиков (через существующий auth PremiumCRM).
5. Работает на Android-планшетах механиков, на iPad/iPhone, на десктопе — без раздельных сборок.
### Не-цели
- Не делаем приложение для водителя в рамках этого подпроекта (отдельная фаза).
- Не интегрируем фактическую интеграцию обратно в 1С вендора (наоборот — уходим от неё).
- Не делаем нативные APK / IPA в первой версии. Только PWA. Возможен Capacitor-fallback в фазе 3 при необходимости.
- Не делаем offline-first в MVP (механик работает с WiFi в парке). Service Worker — задел на будущее.
- Не делаем real-time чат / уведомления через push на iOS (сложно, неприоритетно).
- Не импортируем существующие осмотры из вендора в первой версии (отдельная задача миграции данных).
## 3. Технологический стек
### Frontend
| Слой | Выбор | Обоснование |
|---|---|---|
| Build/Dev | **Vite + TypeScript** | Быстрая итерация, типизация. Стек, уже знакомый команде. |
| UI Framework | **React 18** | Команда уже работает с React (SuperLanding на Astro + React islands). |
| UI Library | **shadcn/ui + Tailwind CSS** | Совпадает с design preferences (cream palette, Inter only, без жёлтого, без emoji). |
| State Client | **TanStack Query (React Query)** | Кэширование fetches, optimistic updates, retries. |
| Local State | **Zustand** | Минималистичный store без boilerplate. |
| Routing | **React Router 7** | Standard в React экосистеме. |
| Forms | **React Hook Form + Zod** | Типизированная валидация, лёгкий контроль. |
| Canvas/Annotation | **Konva.js + react-konva** | Лучший выбор для photo annotations: layers, touch, gestures, undo/redo. |
| HTTP | **Native fetch + ky или axios** | TBD на старте. |
| PWA | **vite-plugin-pwa** | Service Worker, web manifest, install prompt — из коробки. |
| QR-сканер | **html5-qrcode** | Для сканирования госномера-стикера машины. |
### Backend
| Слой | Выбор |
|---|---|
| Расширение | Новый модуль `app/mechanic/` в FastAPI PremiumCRM |
| Базовый URL | `/api/v1/mechanic/*` |
| ORM | SQLAlchemy (используется в TaxiDashboard) |
| Миграции | Alembic |
| Auth | Существующий PremiumCRM (JWT cookie + bcrypt PIN) — переиспользуем |
| Permissions | Существующая dept-based система permissions (см. memory: `project_taxidashboard_permissions`) |
| Файловое хранилище | **MinIO** на 100.64.0.12 (контейнер `taxi-minio`). Bucket **`pp-inspections`** (уже создан). Endpoint `s3.pptaxi.ru` (HTTPS), admin UI `s3-admin.pptaxi.ru`. Креды в `/opt/sites/taxi-dashboard/minio.env`. |
| S3 client | **`aiobotocore`** — переиспользуем существующую интеграцию из TaxiDashboard (она же используется для фото осмотров уже). |
| Upload-схема | **Pre-signed POST** — фронтенд льёт фото напрямую в MinIO, минуя FastAPI. Backend выдаёт presigned URL + поля. |
| Read-схема | Существующий nginx-proxy `/media/<key>` на VDS (уже работает для других фото TaxiDashboard) ИЛИ pre-signed GET с TTL 1 час. Решим на этапе implementation. |
| Image-processing | Pillow для thumbnails и EXIF strip — запускается celery-task'ом после `confirm` upload'а. |
| Дополнительно | **Только MinIO**, без локального дубля на FS (решение владельца). Расхождение с существующим dual-write-паттерном в TaxiDashboard оправдано тяжестью медиа осмотров (10-20 фото на осмотр) — забивать диск VDS дублями нецелесообразно. |
### Развёртывание
| Хост | Назначение |
|---|---|
| 100.64.0.12 (PremiumCRM VDS) | Backend + DB + статика фото |
| Caddy | Reverse proxy + TLS + raw уролы для фото |
| Поддомен | `mechanic.pptaxi.ru` либо `crm.pptaxi.ru/mechanic/` (TBD, см. open question) |
## 4. Модель данных
Новые таблицы в существующей `taxi_dashboard` PostgreSQL:
```sql
-- Осмотр
CREATE TABLE inspections (
id BIGSERIAL PRIMARY KEY,
vehicle_id BIGINT NOT NULL REFERENCES vehicles(id),
type VARCHAR(32) NOT NULL, -- 'handover'|'return'|'periodic'|'ad-hoc'
performed_by INTEGER NOT NULL REFERENCES users(id),
driver_id INTEGER REFERENCES users(id),
started_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
finished_at TIMESTAMPTZ,
status VARCHAR(16) NOT NULL DEFAULT 'in_progress', -- 'in_progress'|'completed'|'cancelled'
notes TEXT,
prev_inspection_id BIGINT REFERENCES inspections(id), -- ссылка на предыдущий осмотр того же авто
meta JSONB DEFAULT '{}'::jsonb
);
CREATE INDEX idx_inspections_vehicle_started ON inspections(vehicle_id, started_at DESC);
-- Фото осмотра
CREATE TABLE inspection_photos (
id BIGSERIAL PRIMARY KEY,
inspection_id BIGINT NOT NULL REFERENCES inspections(id) ON DELETE CASCADE,
side VARCHAR(16) NOT NULL, -- 'front'|'rear'|'left'|'right'|'interior'|'vin'|'odometer'|'wheel-fl'|'wheel-fr'|'wheel-rl'|'wheel-rr'|'free'
slot_index INT DEFAULT 0,
storage_key TEXT NOT NULL, -- путь относительно /opt/premiumcrm/uploads/
annotated_key TEXT, -- путь к фото с canvas-обводкой (опц.)
thumb_key TEXT,
width INT,
height INT,
bytes BIGINT,
taken_at TIMESTAMPTZ DEFAULT NOW(),
display_order INT DEFAULT 0
);
-- Метка повреждения (на фото и/или на схеме авто)
CREATE TABLE damage_markers (
id BIGSERIAL PRIMARY KEY,
inspection_id BIGINT NOT NULL REFERENCES inspections(id) ON DELETE CASCADE,
photo_id BIGINT REFERENCES inspection_photos(id), -- NULL если только на схеме
side VARCHAR(16), -- сторона авто
x DECIMAL(6,4), -- normalized 0..1 в координатах фото или схемы
y DECIMAL(6,4),
polygon JSONB, -- опционально: координаты обведённого контура [{x,y}, ...]
damage_type VARCHAR(32), -- 'scratch'|'dent'|'paint'|'crack'|'missing'|'rust'|'glass'
severity VARCHAR(16), -- 'cosmetic'|'minor'|'moderate'|'severe'
description TEXT,
carried_over_from_id BIGINT REFERENCES damage_markers(id), -- если унаследовано от прошлого осмотра
resolved BOOLEAN DEFAULT FALSE, -- было ли устранено
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_damage_markers_inspection ON damage_markers(inspection_id);
```
**Ключевая идея для дифа «было/стало»:**
- При создании return-inspection backend копирует все `damage_markers` из `prev_inspection_id` со ссылкой `carried_over_from_id`. Механик может либо подтвердить «есть» (оставить), либо отметить «устранено» (`resolved=true`), либо добавить новые маркеры. На фронте отображается две группы: «было раньше» (полупрозрачные) + «новые» (яркие).
## 5. API контракт
Все endpoints под `/api/v1/mechanic/`. Auth — через существующий JWT cookie PremiumCRM. Permission gate: `mechanic:inspect` (новое право в матрице, добавляется в модуль permissions).
### Машины
```
GET /vehicles?q=<гос-номер|VIN>&limit=20
→ [{id, license_plate, vin, make, model, year, current_driver, last_inspection_at}]
GET /vehicles/{id}
→ детали авто + список последних осмотров
GET /vehicles/{id}/last-inspection
→ последний завершённый осмотр (для дифа при приёмке)
```
### Осмотры
```
POST /inspections
body: {vehicle_id, type, driver_id?}
→ {id, ...} создан в статусе in_progress; если type='return', копирует markers из prev
GET /inspections/{id}
→ полная карточка с photos + markers + meta
PATCH /inspections/{id}
body: {status?, notes?, finished_at?}
POST /inspections/{id}/photos/upload-url
body: {side, slot_index, content_type, size}
→ {photo_id, presigned_url, fields, storage_key, expires_at}
# фронт льёт сам файл напрямую в MinIO через presigned_url
POST /photos/{photo_id}/confirm
body: {actual_size?, width?, height?}
→ {id, storage_key, thumb_key, status: 'processing'|'ready'}
# после confirm backend ставит celery-task на thumbnail и EXIF strip
POST /inspections/{id}/markers
body: {photo_id?, side, x, y, polygon?, damage_type, severity, description}
→ {id, ...}
PATCH /markers/{id}
body: {damage_type?, severity?, description?, polygon?, resolved?}
DELETE /markers/{id}
POST /photos/{id}/annotated/upload-url
body: {content_type, size}
→ {presigned_url, fields, storage_key, expires_at}
# аналогично: фронт льёт annotated-версию напрямую в MinIO
POST /photos/{id}/annotated/confirm
→ {id, annotated_key, status: 'ready'}
```
### Чтение фото
```
GET /photos/{id} → 302 Redirect на pre-signed S3 URL (TTL 1 час)
GET /photos/{id}/thumb → 302 Redirect на pre-signed S3 URL thumbnail
GET /photos/{id}/annotated → 302 Redirect на pre-signed S3 URL annotated
```
### Меня (текущий механик)
```
GET /me
→ {id, name, dept, permissions: [...], avatar_url}
```
## 6. UI экраны и поток
### Экран 1. Login
PIN-вход в PremiumCRM (как уже работает в TaxiDashboard).
### Экран 2. Главная — выбор машины
- Поле поиска по госномеру/VIN
- Кнопка «Сканировать QR» (стикер на лобовом стекле машины с госномером)
- Список последних 10 машин, с которыми работал этот механик
- На каждой карточке: гос-номер, модель, водитель, дата последнего осмотра
### Экран 3. Карточка авто
- Резюме (гос-номер, VIN, модель, цвет, год)
- Текущий водитель (если в аренде)
- **Кнопки:** «Передача водителю» (handover) | «Приёмка от водителя» (return) | «Плановый осмотр» (periodic) | «Свободный осмотр» (ad-hoc)
- **Прошлые осмотры** — лента (превью фото + метки)
### Экран 4. Новый осмотр — список «слотов» фото
Карточка-grid: 4 обязательных + 4 опциональных:
- Перед, Зад, Левый, Правый (обязательные)
- VIN, Одометр, Салон, Свободное фото
- Каждый слот: пустой → камера UI; заполнен → миниатюра + индикатор количества меток
- В режиме `return` каждая карточка слота показывает «было N меток, добавь новые / подтверди / отметь устранённые»
### Экран 5. Камера
- Полноэкранный preview (через `getUserMedia()` если поддерживается, иначе `<input type="file" capture="environment">`)
- Overlay-направляющая под текущий слот (силуэт стороны авто для guidance)
- Снять → preview → принять/перенять
### Экран 6a. Vehicle Scheme (zone-навигация)
- Интерактивная SVG-схема авто с тремя проекциями: top-down (вид сверху), боковая (left/right), интерьер
- Каждая зона (бампер, капот, дверь, крыша, диск, лобовое и т.д.) — кликабельный hot-spot SVG `<path>`
- При тапе на зону → переход в Экран 5 (Камера) с предзаполненным `side`/`slot_index`
- Поверх схемы — полупрозрачные метки прошлых повреждений (для return-режима): красные точки на тех зонах, где были повреждения
- Прогресс осмотра: подсветка зон, у которых уже есть фото (зелёный outline)
### Экран 6b. Annotation Editor (главная фишка)
- Фото на весь экран, поверх — Konva canvas
- Tool panel:
- 🔘 **Маркер** — тап создаёт точку-метку
- ✏️ **Обводка** — рисуем замкнутый контур (полигон) вокруг повреждения
- ↩️ Undo / 🗑️ Erase
- 🎨 Тип/severity выбор (4 типа × 4 severity)
- При тапе на маркер — modal с типом, severity, описанием
- В `return`-режиме старые маркеры показываются полупрозрачными; тап → «есть/устранено»
### Экран 7. Обзор осмотра
- Карточка-grid всех фото + список всех маркеров + общие notes
- Кнопка «Завершить осмотр» → finished_at, status=completed
### Экран 8. Подтверждение (опц.)
- Подпись водителя (canvas) — опционально
- Сохранение, переход на главную
## 7. Архитектура frontend
```
src/
├── api/
│ ├── client.ts # настройка fetch/axios + auth interceptor
│ ├── inspections.ts # endpoints через TanStack Query
│ ├── vehicles.ts
│ └── photos.ts
├── components/
│ ├── ui/ # shadcn-сгенерированные
│ ├── camera/
│ │ ├── CameraCapture.tsx
│ │ └── PhotoSlot.tsx
│ ├── annotation/
│ │ ├── AnnotationCanvas.tsx # Konva-based
│ │ ├── ToolPalette.tsx
│ │ └── MarkerModal.tsx
│ ├── vehicle-scheme/
│ │ ├── VehicleScheme.tsx # SVG interactive scheme
│ │ ├── schemes/ # SVG-файлы по типу кузова (sedan, hatch, suv)
│ │ │ ├── sedan-top.svg
│ │ │ ├── sedan-side.svg
│ │ │ └── interior.svg
│ │ └── HotSpot.tsx # переиспользуемая зона-кнопка
│ └── shared/
├── pages/
│ ├── Login.tsx
│ ├── Home.tsx
│ ├── VehicleCard.tsx
│ ├── InspectionStart.tsx
│ ├── InspectionEditor.tsx
│ └── InspectionReview.tsx
├── store/ # Zustand store(s)
│ ├── inspectionDraft.ts
│ └── auth.ts
├── lib/
│ ├── pwa.ts # SW registration
│ └── coords.ts # norm/denorm coords
├── App.tsx
└── main.tsx
```
## 8. Архитектура backend (модуль в PremiumCRM)
```
app/mechanic/
├── __init__.py
├── router.py # /api/v1/mechanic/ роуты
├── models.py # SQLAlchemy: Inspection, InspectionPhoto, DamageMarker
├── schemas.py # Pydantic
├── service.py # бизнес-логика (start_inspection, finalize, copy_markers, ...)
├── storage.py # сохранение файлов на VDS + thumbnails
├── deps.py # auth + permission deps
└── migrations/
└── 20260516_mechanic_init.py # Alembic
```
## 9. Фазы реализации
### Фаза 1. MVP — Skeleton (3-4 нед)
Только основной flow без annotation, для первичного теста UX:
- Backend: модуль `mechanic`, миграции, endpoints вэхикулов и осмотров, фото upload (без annotation)
- Frontend: Login → Home → Vehicle → Inspection (создать, выбрать тип, добавить фото в 4 слота, добавить просто маркер-точку без сложной обводки) → Review → Complete
- Дифф с прошлым: список маркеров прошлого осмотра, чекбоксы «есть/устранено»
### Фаза 2. Annotation Editor (+2-3 нед)
- Konva-based canvas-overlay с инструментами обводки (polygon)
- Сохранение annotated-фото отдельным URL
- Старые маркеры/обводки полупрозрачно поверх новой фотографии для сравнения
### Фаза 3. Polish (+1-2 нед)
- Service Worker + offline queue (фото снимаются на устройство, загружаются по WiFi)
- QR-сканер стикеров
- Push-уведомления для админа (через web push с FCM-фолбэком или Telegram-бота)
- Дашборд статистики в админ-части PremiumCRM
### Фаза 4 (опционально). Capacitor wrapper
- Если в production столкнёмся с ограничениями iOS Safari camera или offline
- Тот же codebase → native APK + IPA
- ~1-2 недели
## 10. Связь с разведкой
- Endpoints, обнаруженные у вендора (`api.ttcontrol.naughtysoft.ru/api/vehicletechinspection/byvehicle` и пр.), могут быть использованы **отдельно** для **миграции данных** — одноразовый импорт прошлых осмотров вендора в нашу базу. Это отдельный sub-project (не входит в MVP).
- В первой версии работаем только с НОВЫМИ осмотрами, создаваемыми в PremiumMechanic — старые остаются в системе вендора и используются для справки до момента миграции.
## 11. Решения и открытые вопросы
### Решения (по итогам ревью владельца)
| # | Решение |
|---|---|
| Поддомен | **`mechanic.pptaxi.ru`** (отдельный поддомен). DNS уже настроен владельцем. На VDS 100.64.0.12 в Caddyfile добавляется блок с TLS через ACME. |
| Подпись водителя | **Не нужна** в MVP. Из спеки исключена. |
| 2D-схема авто | **Нужна обязательно с MVP.** Делаем SVG-схему сами: top-down + боковая проекция + интерьер. Не привязываемся к ассетам вендора (в APK schemes как раздельных файлов нет, у них всё через WebView + либо embedded в Xamarin DLL, либо отдельная фотография крупным планом). У нас SVG-первый подход с zone-based hot-spots. |
### Замечания по разведке вендорского редактора
- Layout `vehicleinspectiondetaildrawinglayout.xml` подтверждает: вендор использует `<android.webkit.WebView>` для редактора рисования повреждений. То есть **наша PWA-стратегия = технически тот же подход, что у вендора**, только без обёртки Xamarin.
- Заголовок WebView в layout = «Левая дверь» — у вендора **zone-based** детализация (кнопка → крупный план зоны → редактирование).
- ObjectID формата `660e7abfd13231f1dd6e8a4d` в `vehicleId` доказывает, что у вендора на бэке **MongoDB**, а не 1С. Имя `taksi.0nalog.com:1703` (виденное в Driver-приложении) — отдельный 1С-узел, не основной storage. Это означает: имитировать «1С-структуру» нам не надо — у вендора и так нет 1С на ключевом пути данных осмотров.
### Остающиеся open questions
1. **Permissions:** добавить новое право `mechanic:inspect` или ограничиться существующим `dept=Тех. служба`? — выбор на этапе implementation.
2. **Storage:** решено использовать **MinIO**, **только** MinIO без локального FS-дубля (разрешено отклониться от dual-write паттерна для тяжёлых медиа). Параметры: bucket `pp-inspections`, endpoint `s3.pptaxi.ru` HTTPS, креды в `/opt/sites/taxi-dashboard/minio.env`. Существующая `aiobotocore`-интеграция переиспользуется. Остаётся **операционно**: настроить CORS на bucket для allowed origins (`https://mechanic.pptaxi.ru`, `https://crm.pptaxi.ru`, dev `http://localhost:5173`) — задача в плане реализации.
3. **Связь с CheckAuto:** опционально показывать в карточке машины автоматические проверки водителя — после MVP.
4. **Импорт истории осмотров от вендора:** через VendorBridge sub-project — после MVP.
## 12. Следующий шаг
После утверждения этой спеки:
1. Создать подробный план реализации Фазы 1 через `writing-plans` skill.
2. Сгенерировать миграции БД + scaffold бэкенд-модуля.
3. Скаффолд React-проекта в `mechanic-pwa/frontend/` с базовой страницей логина.
4. На итерациях по 2-3 экрана.
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 926 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

+283
View File
@@ -0,0 +1,283 @@
# Этап 1. Статический анализ — отчёт
- **Дата:** 2026-05-16
- **Аналитик:** vladtechno@gmail.com + Claude
- **Источники:** локальные APK в `recon/artifacts/`, jadx 1.5.5 декомпиляция
---
## 1. Что разбирали
Два приложения одного вендора **NaughtySoft**:
| Приложение | APK | Версия | Назначение |
|---|---|---|---|
| «Водитель» | `com.naughtysoft.TtcDriver` (XAPK split) | 1.2.313 (build 323) | Основное приложение для водителей: балансы, штрафы, платежи, обращения, осмотры |
| «Механик» | `com.naughtysoft.ttc` | 1.8.13 (build 165) | Приложение для механиков парка: фото машин в 1С |
Оба от одного вендора (`com.naughtysoft.*`), технологический стек **разный** (см. §3) — признак того, что вендор находится в процессе технологической миграции и не унифицирует разработку.
---
## 2. Метаданные APK
### Водитель (`com.naughtysoft.TtcDriver`)
| Параметр | Значение |
|---|---|
| Version | 1.2.313 (build 323) |
| Target SDK | 36 (Android 16) |
| Min SDK | 24 (Android 7) |
| Main Activity | `com.naughtysoft.TtcDriver.MainActivity` |
| Permissions | INTERNET, CAMERA, RECORD_AUDIO, STORAGE, WAKE_LOCK, POST_NOTIFICATIONS, FCM (`c2dm.permission.RECEIVE`), AD_ID, ACCESS_NETWORK_STATE |
| Permissions, которых **НЕТ** | GPS (ACCESS_FINE/COARSE_LOCATION), SMS, READ_CONTACTS, READ_PHONE_STATE, BIND_NOTIFICATION_LISTENER |
| `android:usesCleartextTraffic` | **true** (приложение умеет HTTP без TLS) |
| `network_security_config.xml` | **отсутствует** (TLS pinning через XML не настроен) |
| Распространение | XAPK с App Bundle (base + split: arm64_v8a, en, mdpi, zh) |
| Native ABI | **только arm64_v8a** (нет x86_64, нет armeabi-v7a) |
Отсутствие GPS-разрешений = приложение пассивное, не трекает позицию водителя.
Отсутствие SMS-разрешений = OTP-перехвата автоматического нет, логин классический.
### Механик (`com.naughtysoft.ttc`)
| Параметр | Значение |
|---|---|
| Version | 1.8.13 (build 165) |
| Target SDK | 33 (Android 13) |
| Min SDK | 19 (Android 4.4) |
| Application Label | «Механик» |
| Permissions | INTERNET, CAMERA, RECORD_AUDIO, ACCESS_FINE_LOCATION, ACCESS_COARSE_LOCATION, STORAGE, VIBRATE, NETWORK_STATE, WIFI_STATE |
| Native ABI | только arm64_v8a |
В Механике в отличие от Водителя есть **GPS** и используется **Xamarin** (см. §3).
---
## 3. Технологические стеки
### Водитель — Flutter (Dart AOT)
Подтверждено наличием:
- `lib/arm64-v8a/libflutter.so` — Flutter engine (11 МБ)
- `lib/arm64-v8a/libapp.so`**Dart AOT snapshot** с бизнес-логикой (9.6 МБ)
- Java-пакеты: `io/flutter/`, `dev/flutter/pigeon`, `_COROUTINE/`, Kotlin runtime
- Flutter-плагины: `dev/fluttercommunity/plus`, `com/baseflow/permissionhandler`, `com/jrai/flutter_keyboard_visibility`, `io/scer/pdfx`, `studio/midoridesign/gal`, `io/scer/pdf_renderer`, `dev/fluttered/map_launcher`
**Что это значит для реверса:**
- Бизнес-логика приложения — **в Dart, скомпилированном AOT в `libapp.so`**. jadx-декомпиляция Java/Kotlin даёт только тонкие plugin wrappers, не бизнес-код.
- Все строковые константы (URL, paths, ключи) хранятся в `libapp.so` как plain ASCII → извлекаемы через `grep -a`/`strings` (см. §4).
- HTTP-запросы Flutter делает через стандартный Dart `HttpClient` → видимы в mitmproxy как обычный HTTPS (без custom низкоуровневых трюков).
- Дальнейший реверс Dart-кода (поведение, проверки) — через **reFlutter** (требует пересборки snapshot) или динамический анализ через mitmproxy. На статическом этапе ограничиваемся извлечёнными строками.
### Механик — Xamarin (.NET/Mono)
Подтверждено наличием:
- Java-пакеты: `xamarin/`, `mono/`, `crc64xxxxxxxxxxxxxxxx/` (CRC64-обфускация имён классов — характерный паттерн Xamarin)
- `assets/assemblies/assemblies.blob` (4 МБ) — упакованный контейнер всех .NET DLL
- `assets/assemblies/assemblies.manifest` (1.6 КБ) — манифест содержимого blob
**Что это значит для реверса:**
- jadx даёт ноль информации о логике Механика (всё в C# DLL).
- Для распаковки нужен **pyxamstore** (распаковать blob → отдельные DLL) + **dnSpy/ILSpy** для C# декомпиляции.
- Это **отдельный поток работы**, отложен. Приоритет — водитель.
**Вывод о стеке вендора:** Xamarin (Mechanic) сейчас deprecated Microsoft'ом в пользу .NET MAUI. Driver переписан на Flutter — современный стек. Вендор находится в процессе технологической миграции, что согласуется с фактом «прекратил доработки» — ресурсы ушли на перенос/переход, а не на обновление наших фич.
---
## 4. Сетевая инфраструктура
Все извлечены из `libapp.so` водительского приложения через `grep -aoE 'https?://...'`.
### Бэкенды
| Endpoint | Назначение |
|---|---|
| **`https://api.ttcontrol.naughtysoft.ru/api`** | Главный API вендора NaughtySoft. Основной канал общения приложения с сервером. |
| **`https://taksi.0nalog.com:1703/`** | Нестандартный порт **1703** — классический паттерн **HTTP-сервиса 1С Предприятие**. С большой вероятностью это сам сервер 1С PremiumPark, на котором работает конфигурация вендора. Имя `0nalog` (вместо `nalog`) — возможный obscurity-приём. |
| **`https://ttcdriver.firebaseio.com`** | Firebase Realtime Database. Помимо FCM-пушей, часть данных может синхронизироваться через Realtime DB. Project ID: `ttcdriver`. |
### Архитектурная гипотеза
```
Driver App (Flutter)
├──► api.ttcontrol.naughtysoft.ru (HTTPS, основное взаимодействие)
│ │
│ └──► (на бэке вендора, вероятно) ──► taksi.0nalog.com:1703 (1С)
├──► taksi.0nalog.com:1703 (возможно, прямые запросы — нужно подтвердить на этапе 2)
├──► ttcdriver.firebaseio.com (Realtime DB + FCM регистрация)
└──► FCM (Firebase Cloud Messaging — push-уведомления о штрафах и пр.)
```
Подтвердить факт прямых обращений на `0nalog:1703` (или это идёт только через прокси `api.ttcontrol`) — задача этапа 2 (mitmproxy).
### Карта API endpoints
Извлечены пути `/v1/...` и `/v3/...` из `libapp.so`. Префикс домена — `api.ttcontrol.naughtysoft.ru/api`.
| Endpoint | Назначение (гипотеза) |
|---|---|
| `/v1/GetBalance` | Остаток баланса водителя |
| `/v1/GetFines` | Список штрафов |
| `/v1/GetChecks` | Чеки 54-ФЗ |
| `/v1/GetPayroll` | Зарплата/начисления v1 |
| `/v3/GetPayroll` | Зарплата/начисления v3 (новее) |
| **`/v1/CreatePayment`** | **Инициация платежа** |
| `/v1/GetPaymentDetails/` | Детали платежа |
| **`/v1/BanksSBP`** | **Список банков СБП** — платежи через Систему Быстрых Платежей |
| `/v1/ChangeQIWIWalletCardNumber` | QIWI Wallet (legacy — QIWI закрылся в 2024, мёртвый код) |
| `/v1/CreateAppeal` | Создать обращение/заявку |
| `/v1/GetAppealHistory` | История обращений водителя |
| `/v1/AddMessageAppeal` | Добавить сообщение к обращению |
| **`/v1/AddInspection`** | **Добавить осмотр (фото повреждений)** |
| **`/v1/FOTO/`** | **Endpoint для фотографий** |
| `/v1/AttractedAdd` | Добавить привлечённого (реферал?) |
| `/v1/AttractedList` | Список привлечённых |
| `/v1/OnLine` | Heartbeat / online status |
| `/v1/GetStatus/` | Статус (требует расшифровки на этапе 2) |
| `/v1/Sell` | Продать что-то (требует расшифровки) |
| `/v1/GetListOfQuestions` | Список вопросов (FAQ / скрининг) |
### Аутентификация
Найдены характерные строки в `libapp.so`:
- `Authorization` — заголовок HTTP
- `Bearer` — Bearer-токен
- `apiKey` — упоминание (вероятно Firebase API key)
- `authorization` (lowercase) — тоже встречается
**Гипотеза:** стандартная Bearer-token аутентификация. Логин → получение access_token → дальше во всех запросах `Authorization: Bearer <token>`. Подтвердить и понять lifecycle (refresh? expiry?) — задача этапа 3.
---
## 5. Защитные механизмы
### TLS pinning
- **На уровне Android system (network_security_config.xml):** отсутствует — конфиг не найден в декомпиле.
- **Cleartext traffic разрешён** — приложение умеет HTTP без TLS (вероятно для `taksi.0nalog.com:1703`).
- **Программный TLS pinning в Dart-коде:** возможен, но в `libapp.so` явных маркеров pinning'а (типа `setTrustedRoots`, custom SecurityContext с pinned certs) поверхностный grep не выявил. Проверится на этапе 2: если mitmproxy сразу читает трафик — pinning'а нет; если нет — нужен reFlutter/Frida.
**Прогноз:** на этапе 2 mitmproxy будет работать **без обхода TLS pinning**. Это сильно упрощает разведку.
### Root/Emulator detection
Не сканировалось специально (требует анализа Dart-кода в `libapp.so`). На этапе 2 проверим эмпирически — запустится ли приложение на эмуляторе. Учитывая что:
- Приложение Flutter (без agressive anti-debug по умолчанию)
- Нет premium-фич, ориентированных на security (банковский класс, антифрод)
— вероятность блокирующего detection'а низкая.
### Request signing / HMAC
Маркеров явного HMAC (типа `Mac.getInstance("HmacSHA256")`) в Kotlin-стороне не найдено (большая часть Java-кода — Flutter engine + plugins, без бизнес-логики). В Dart возможна реализация, но это нестандартный паттерн для Flutter приложений — обычно полагаются на Bearer + TLS.
### Прочее
- **Play Integrity API** — не используется (нет соответствующих библиотек в декомпиле).
- **App attestation / SafetyNet** — отсутствует.
---
## 6. Сторонние SDK и зависимости
### Из Kotlin/Java декомпиляции водителя
- `com.google.firebase.*` — Firebase (FCM, аналитика)
- `com.google.android.gms.*` — Google Play Services
- `dev.flutter.pigeon` — Flutter Pigeon (Dart ↔ Native bridges)
- `io.flutter.*` — Flutter engine
- `com.baseflow.permissionhandler` — permission_handler plugin
- `com.jrai.flutter_keyboard_visibility` — keyboard visibility plugin
- `io.scer.pdfx`, `io.scer.pdf_renderer` — PDF rendering plugins
- `studio.midoridesign.gal` — gal plugin (сохранение в галерею)
- `dev.fluttercommunity.plus` — Flutter community plugins
- `dev.fluttered.map_launcher` — map_launcher plugin (открытие карт)
- `com.github.rmtmckenzie.native_device_orientation` — ориентация устройства
- `com.softmaestri.notification` — кастомные нотификации (другой вендор?)
- `com.getkeepsafe.relinker` — динамическая загрузка native libraries
- `org.apache.commons` — Apache Commons
- `org.chromium.support_lib_boundary` — WebView boundary library
### **НЕ найдено** (важно):
- **`ru.yoomoney.*`** — нет ЮКассы SDK
- **`ru.tinkoff.*`** — нет Tinkoff Acquiring SDK
- **`com.yandex.payments.*`** — нет Yandex Pay
- **`com.sberbank.*`** — нет Сбер SDK
- `com.squareup.okhttp3` / `retrofit2` — нет (HTTP идёт через Dart)
- `dagger`, `hilt` — нет DI-фреймворков (Flutter сам по себе их не требует)
**Вывод по платежам:** на Kotlin-стороне платёжных SDK провайдеров нет. Платёж через `/v1/CreatePayment` скорее всего возвращает **deep link** (типа `https://qr.nspk.ru/<id>` для СБП или URL веб-страницы провайдера), и Flutter открывает его через стандартный intent. Это **отличный сценарий для миграции** — точно повторяемо в нашем приложении без необходимости лицензировать чьи-то SDK.
---
## 7. Промежуточные гипотезы
1. **Архитектура двухслойная**: NaughtySoft API (`api.ttcontrol`) — фасад, под капотом стоит 1С (`taksi.0nalog:1703`). Подтвердить на этапе 2: смотреть, идут ли с приложения **прямые** запросы на `0nalog:1703` (тогда оба слоя независимы), или только на `api.ttcontrol` (тогда `0nalog` — публично доступный backend 1С только в декомпиле, а реально приложение его не дёргает).
2. **Платежи через СБП + deep links**, без нативных SDK провайдеров → **легко повторимо** в нашем будущем приложении.
3. **Фото-функция (`/v1/AddInspection`, `/v1/FOTO/`) уже есть на бэке** — она просто не выдана водителям в UI текущего приложения. Если на этапе 2 удастся снять трафик этих endpoints (через Механика или через намеренный вызов в Driver если кнопка где-то есть), мы получим готовый протокол для нашего блока B без дизайна с нуля.
4. **QIWI код мёртв** — индикатор того, что подрядчик не зачищает мёртвый функционал. Подтверждает «прекратили доработки».
5. **TLS pinning слабый или отсутствует** → mitmproxy будет работать в этап 2 «из коробки».
---
## 8. Open questions для следующих этапов
- Какой формат запроса/ответа на `/v1/CreatePayment`? (этап 2)
- Использует ли приложение `/api/...` префикс или `/v1/...` идут напрямую от корня? (этап 2)
- Использует ли водитель `taksi.0nalog.com:1703` напрямую или только через прокси `api.ttcontrol`? (этап 2)
- Программный TLS pinning в Dart — есть? (этап 2, эмпирически)
- Refresh-токен и его lifecycle — какой? (этап 3)
- Что приходит в FCM-сообщении при штрафе? Полный объект или только триггер «обнови данные»? (этап 5)
- Что лежит в `assemblies.blob` Механика? Те же endpoints или другие? (отдельный поток)
---
## 9. Блокеры и риски
| # | Риск | Митигация |
|---|---|---|
| R1 | **ARM-only ABI** в обоих APK — на x86_64 эмуляторе `adb install` падает с `INSTALL_FAILED_NO_MATCHING_ABIS` | Для этапа 2 либо: (a) создать AVD x86_64 API 33+ с включённой ARM-translation, (b) использовать физическое Android-устройство, (c) использовать ARM-эмулятор (медленный) |
| R2 | Программный TLS pinning в Dart-коде возможен, но не подтверждён на статике | Проверить эмпирически на этапе 2; если есть — обойти через **reFlutter** |
| R3 | Механик на Xamarin требует отдельного тулинга (pyxamstore + dnSpy) для разбора C# логики | Отдельный поток работы; приоритет — водитель |
| R4 | Часть Dart-логики (валидация, формат запросов) невидима без динамики | Перенесено на этап 2 (mitmproxy) |
---
## 10. Артефакты
| Файл | Что |
|---|---|
| `recon/artifacts/original-driver.xapk` | Исходный XAPK водителя из APKPure |
| `recon/artifacts/driver-xapk/` | Распакованный XAPK (base APK + splits + manifest.json) |
| `recon/artifacts/original-mechanic.apk` | APK Механика |
| `recon/artifacts/decompiled/driver/` | jadx-декомпиляция водителя (4331 классов, 25 errors ≈ 0.6%) |
| `recon/artifacts/decompiled/mechanic/` | jadx-декомпиляция Механика (даёт только Xamarin glue, не C# логику) |
| `recon/artifacts/native/driver/lib/arm64-v8a/libapp.so` | Извлечённый Dart AOT snapshot водителя — главный источник URL и endpoints |
---
## 11. Следующий шаг (этап 2)
Все необходимые данные для разворачивания этапа 2 готовы:
- Список endpoints для отслеживания — есть
- Базовые URL'ы — есть
- Подтверждено отсутствие XML-pinning'а — mitmproxy будет работать
- Топология FCM известна — этап 5 пойдёт от Firebase project `ttcdriver`
**Перед этапом 2 нужно решить ARM-блокер** (R1): выбрать ARM-эмулятор / x86_64 с ARM translation / физическое устройство. Это **открытый пункт для пользователя**.
После этого по плану: установка приложения на стенд → mitmproxy + системный CA → проигрыш сценариев S1-S6 (см. §5 design-doc'а) → каталог реальных запросов/ответов.
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
+46
View File
@@ -0,0 +1,46 @@
"""Read mitmproxy flow file, print unique endpoints by host/method/path."""
import sys, collections
from mitmproxy import io as miio
from mitmproxy.exceptions import FlowReadException
flow_file = sys.argv[1]
filter_host = sys.argv[2] if len(sys.argv) > 2 else None
stats = collections.Counter()
endpoints = collections.defaultdict(set)
hosts = collections.Counter()
status_codes = collections.Counter()
with open(flow_file, "rb") as f:
reader = miio.FlowReader(f)
try:
for flow in reader.stream():
if not hasattr(flow, "request"):
continue
req = flow.request
host = req.pretty_host
if filter_host and filter_host not in host:
continue
hosts[host] += 1
key = (req.method, req.path.split("?")[0])
endpoints[host].add(key)
stats[(req.method, host, req.path.split("?")[0])] += 1
if flow.response:
status_codes[flow.response.status_code] += 1
except FlowReadException as e:
print(f"[warn] flow truncated: {e}", file=sys.stderr)
print("=== Hosts ===")
for h, c in hosts.most_common():
print(f" {c:5d} {h}")
print()
print("=== Endpoints grouped by host ===")
for h in sorted(endpoints.keys()):
print(f"\n--- {h} ---")
for method, path in sorted(endpoints[h]):
count = stats[(method, h, path)]
print(f" [{count:3d}x] {method:6s} {path}")
print()
print("=== Status codes ===")
for code, c in status_codes.most_common():
print(f" {code}: {c}")
+43
View File
@@ -0,0 +1,43 @@
"""Dump request URL + response body for flows matching a filter substring."""
import sys, json
from mitmproxy import io as miio
from mitmproxy.exceptions import FlowReadException
flow_file = sys.argv[1]
filter_substr = sys.argv[2]
max_dumps = int(sys.argv[3]) if len(sys.argv) > 3 else 1
dumps = 0
with open(flow_file, "rb") as f:
reader = miio.FlowReader(f)
try:
for flow in reader.stream():
if not hasattr(flow, "request"):
continue
url = flow.request.pretty_url
if filter_substr not in url:
continue
if not flow.response:
continue
dumps += 1
print(f"\n=== {flow.request.method} {url} ===")
print(f"Status: {flow.response.status_code}")
print(f"Content-Type: {flow.response.headers.get('Content-Type', '?')}")
print("--- response (first 4000 chars) ---")
try:
body = flow.response.get_text(strict=False)
if body:
try:
parsed = json.loads(body)
body = json.dumps(parsed, indent=2, ensure_ascii=False)
except (json.JSONDecodeError, ValueError):
pass
print(body[:4000])
else:
print("(binary or empty)")
except Exception as e:
print(f"[error decoding body: {e}]")
if dumps >= max_dumps:
break
except FlowReadException as e:
print(f"[warn] flow truncated: {e}", file=sys.stderr)
+47
View File
@@ -0,0 +1,47 @@
"""Scan a binary blob for MZ markers, validate as PE, dump each as separate file."""
import struct, sys, pathlib
blob_path = pathlib.Path(sys.argv[1])
out_dir = pathlib.Path(sys.argv[2])
out_dir.mkdir(parents=True, exist_ok=True)
data = blob_path.read_bytes()
def pe_total_size(buf, off):
"""Compute size of PE image at offset using section table."""
# MZ at off; PE header offset at off+0x3C
if off + 0x40 > len(buf):
return None
pe_off = struct.unpack_from("<I", buf, off + 0x3C)[0]
pe_start = off + pe_off
if pe_start + 24 > len(buf) or buf[pe_start:pe_start+4] != b"PE\x00\x00":
return None
num_sections = struct.unpack_from("<H", buf, pe_start + 6)[0]
optional_header_size = struct.unpack_from("<H", buf, pe_start + 20)[0]
sections_start = pe_start + 24 + optional_header_size
last_end = sections_start + num_sections * 40
for i in range(num_sections):
s = sections_start + i * 40
raw_size = struct.unpack_from("<I", buf, s + 16)[0]
raw_off = struct.unpack_from("<I", buf, s + 20)[0]
end = off + raw_off + raw_size
last_end = max(last_end, end)
return last_end - off
i = 0
n = 0
while True:
j = data.find(b"MZ", i)
if j == -1:
break
sz = pe_total_size(data, j)
if sz and 4096 < sz < 5_000_000:
out = out_dir / f"plain_{j:08x}.dll"
out.write_bytes(data[j:j+sz])
print(f"[ok] offset=0x{j:08x} size={sz} -> {out.name}")
n += 1
i = j + sz
else:
i = j + 2
print(f"\nExtracted {n} plain PE files into {out_dir}")
+22
View File
@@ -0,0 +1,22 @@
"""Search a binary as UTF-16 LE, return regex matches."""
import re, sys, pathlib, glob
pattern = sys.argv[1]
paths = sys.argv[2:]
rx = re.compile(pattern)
found = {}
for pat in paths:
for p in glob.glob(pat):
try:
data = pathlib.Path(p).read_bytes().decode("utf-16-le", errors="ignore")
except Exception as e:
print(f"[skip] {p}: {e}", file=sys.stderr)
continue
for m in rx.findall(data):
key = m if isinstance(m, str) else m[0]
found.setdefault(key, []).append(p)
for key in sorted(found.keys()):
files = sorted(set(pathlib.Path(f).name for f in found[key]))
print(f"{','.join(files):20s} {key}")
+49
View File
@@ -0,0 +1,49 @@
"""
Distill XABA/XALZ assemblies.blob into individual .NET assemblies.
Format ref (reverse-engineered): https://github.com/dotnet/android/blob/main/src/Xamarin.Android.Build.Tasks/Utilities/AssemblyStore.cs
"""
import struct, sys, pathlib, lz4.block
if len(sys.argv) != 3:
print("Usage: unpack_xaba.py <assemblies.blob> <out_dir>")
sys.exit(1)
blob_path = pathlib.Path(sys.argv[1])
out_dir = pathlib.Path(sys.argv[2])
out_dir.mkdir(parents=True, exist_ok=True)
data = blob_path.read_bytes()
assert data[:4] == b"XABA", f"not a XABA blob, got {data[:4]!r}"
# Scan for XALZ markers (each = one compressed assembly)
i = 0
extracted = 0
while True:
idx = data.find(b"XALZ", i)
if idx == -1:
break
# XALZ: 4 magic + 4 descriptor_index + 4 uncompressed_size + N lz4_data
desc_idx = struct.unpack_from("<I", data, idx + 4)[0]
uncompressed = struct.unpack_from("<I", data, idx + 8)[0]
payload_start = idx + 12
# Find next XALZ to bound payload (or EOF)
next_idx = data.find(b"XALZ", payload_start)
payload_end = next_idx if next_idx != -1 else len(data)
compressed = data[payload_start:payload_end]
try:
decompressed = lz4.block.decompress(compressed, uncompressed_size=uncompressed)
except Exception as e:
# Try without explicit size as fallback
try:
decompressed = lz4.block.decompress(compressed)
except Exception as e2:
print(f"[skip] desc={desc_idx} decompress failed: {e2}")
i = payload_start
continue
out_file = out_dir / f"{desc_idx:03d}.dll"
out_file.write_bytes(decompressed)
extracted += 1
print(f"[ok] desc={desc_idx:03d} size={len(decompressed)} -> {out_file.name}")
i = payload_start
print(f"\nExtracted {extracted} assemblies into {out_dir}")