# Task 06: Авторизация, TLS и обращение с секретами (hermes-android) **Repo:** `ochenstarik-ui/hermes-android` **Assigned to:** Antigravity (режим оркестратора, два кодера) **Priority:** CRITICAL (перехват кода авторизации, утечка секретов между хостами) **Date:** 2026-08-24 **Base SHA:** результат задания 05 — указать фактический SHA при выдаче **Зависимость:** задания 01–05 приняты. Пункт 1 продолжает `SEC-01`, вторая половина которого была сознательно отложена из задания 01. ## Роли и протокол | Роль | Модель | Что делает | |---|---|---| | Оркестратор | Antigravity | Разбивает работу, маршрутизирует, принимает результат | | Кодер 1 | Gemini Flash 3.7 high | Реализация §Scope пунктов 1–6 | | Кодер 2 | Gemini Pro high | Независимая проверка, доводка, **и полностью пункт 7** | Пункт 7 (синхронизация контекста) отдан кодеру 2 намеренно: он требует проектного решения и оценки модели угроз, а не механической правки. Кодер 1 к нему не приступает. **Раунд 1 (кодер 1).** Тесты из §Required tests для пунктов 1–6, фиксация падения на base SHA, затем код. Нерешённое — в `OPEN QUESTIONS`. **Раунд 2 (кодер 2).** Независимое воспроизведение падения, проход §Anti-checklist с явными отметками, доводка пунктов 1–6, затем спецификация и реализация пункта 7. Findings по шкале. **Раунд 3 (оркестратор).** Приёмка при фактическом выводе команд. Пункт 7 принимается только вместе с письменной спецификацией того, что именно передаётся между хостами. ## Проблема **1. TLS к LAN-хосту не работает.** Задание 01 открыло cleartext-путь, но у локального хоста сертификат самоподписанный, trust anchors не настроены — вариант с `https://` по-прежнему не соединяется. Пользователь вынужден оставаться на http. **2. PKCE через loopback-сокет (`SEC-02`).** `PkceLoopbackAuthManager.kt:35,41,62` — `ServerSocket(0, 1, 127.0.0.1)` доступен любому приложению на устройстве без разрешений. Первый подключившийся выигрывает единственный `accept()`: сторонняя программа срывает вход или пытается перехватить код. RFC 8252 для мобильных предписывает custom scheme или App Links. Плюс `accept()` — блокирующий вызов внутри `withContext(Dispatchers.IO)`, не реагирует на отмену: поток и слушающий сокет живут до трёх минут после ухода пользователя. **3. Инъекция HTML в колбэк-страницу (`SEC-03`).** `PkceLoopbackAuthManager.kt:114,164` — параметр `error` подставляется в HTML без экранирования; страница отдаётся браузеру от имени `http://127.0.0.1:порт`. **4. URL авторизации не проверяется (`SEC-04`).** `PkceLoopbackAuthManager.kt:48-54` — `validateUrlScheme` вызывается только при обмене кода; сам `authUrl` открывается как есть, весь OAuth-редирект может пойти по открытому http. **5. Тикет в query-строке (`SEC-05`).** `JsonRpcGatewayClient.kt:96-98` — `"$wsUrl${sep}ticket=$ticket"` без `URLEncoder`. Тикет с `&`, `#`, `+`, `/` ломает URL; секрет попадает в логи сервера и прокси. **6. Отказ Keystore роняет приложение (`SEC-07`).** `TokenVault.kt:24-38` превращает любое исключение в `SecurityException`, `HermesAppContainer.kt:31` создаёт хранилище лениво — первый доступ к токенам на проблемном устройстве роняет процесс без пути восстановления. Библиотека `security-crypto:1.1.0-alpha06` — альфа, помеченная в Jetpack как устаревшая. Токены пишутся через `apply()`: при падении процесса свежий refresh-token теряется. Смежно (`NET-08`): обновление токена опирается на поиск подстроки «401» в тексте `IOException` (`HermesHostRuntime.kt:162,179,205`), причём в сообщение подклеивается тело ответа — «401» из произвольного текста сервера вызовет ложный разлогин. `NativeAuthTokens` не поддерживает `expires_in`. **7. Пароли видны в снимках экрана (`SEC-08`).** `FLAG_SECURE` не выставляется нигде, хотя `ClarifyDialog.kt:104-115` принимает sudo-пароль и API-ключи. У поля нет password-типа клавиатуры — IME может сохранить введённое в словарь. **8. Синхронизация контекста отправляет всю историю (`SEC-06`).** `UnifiedContextBuilder.kt:46-53` — при первом переключении `startIndex = 0`, уезжает весь таймлайн; если курсор не найден (`indexOfFirst == -1`), тоже 0, то есть полная пересылка повторно. Редактирование секретов — пять регулярок, не покрывающих AWS-ключи, блоки приватных ключей, строки подключения, содержимое `.env`. Вывод одного хоста подставляется в промпт другого как инструкции — канал кросс-хостовой prompt-инъекции. ## Scope **1. Доверие сертификату хоста.** Отпечаток сертификата фиксируется при сопряжении и закрепляется за хостом; последующие соединения проверяют его. Смена отпечатка — явный экран подтверждения с тем же весом, что и смена endpoint в `PairingPreviewDialog`. Где хранится отпечаток и что происходит при несовпадении — описать в отчёте до кода. `OkHttpClient` конфигурируется на хост, а не один на всех: сейчас `HermesRestClient` создаётся единожды в контейнере. **2. Редирект авторизации (`SEC-02`).** Перевести на `hermes://auth-callback` (или App Link) с обработкой в `onNewIntent`, убрать loopback-сокет. Проверить, что `state` и PKCE-верификатор переживают пересоздание Activity — иначе вход будет ломаться при возврате из браузера на устройствах с активным killer'ом фоновых процессов. Если контракт хоста жёстко требует loopback и не принимает custom scheme — это фиксируется в `OPEN QUESTIONS`, а loopback остаётся с двумя обязательными доработками: `accept()` в `runInterruptible` с закрытием сокета в `invokeOnCancellation`, и повторный `accept()` при первом подключении с неверным `state` вместо провала всего входа. **3. Экранирование колбэк-страницы (`SEC-03`).** Значение параметра в HTML не подставлять вообще; отдавать статический текст по классу ошибки. **4. Проверка схемы `authUrl` (`SEC-04`).** Прогонять через ту же проверку, что и обмен кода, до открытия браузера. **5. Тикет вне query (`SEC-05`).** Передавать заголовком или через `Sec-WebSocket-Protocol`. Если контракт хоста принимает только query — кодировать значение и зафиксировать ограничение в `OPEN QUESTIONS` как требующее изменения на стороне хоста. **6. Хранилище токенов и ошибки HTTP (`SEC-07`, `NET-08`).** `SecurityException` при инициализации обрабатывать как «хранилище повреждено»: очистить, потребовать повторный вход, не падать. Секреты писать через `commit()`. Ввести типизированный `HermesHttpException(code, body)`; сравнивать код, а не искать подстроку. Тело ответа в сообщение исключения не подклеивать. Поддержать `expires_in` с пересчётом в абсолютное время. Замену `security-crypto` на прямой `KeyStore` рассмотреть и обосновать выбор в отчёте; если решено остаться на библиотеке — зафиксировать версию и причину. **7. Синхронизация контекста (`SEC-06`) — только кодер 2.** Сначала письменная спецификация в отчёте: что именно передаётся на второй хост, в каком объёме, каким каналом, что пользователь видит перед передачей. Только после неё — код. Требования: курсор по идентификатору сообщения с фолбэком на «последние N», а не на всю историю; передача отдельным системным сообщением, не склейкой с пользовательским промптом; вместо чёрного списка регулярок — явное решение о том, что вообще допускается пересылать; пользователю показывается объём и состав передаваемого до отправки. Правку README о «синхронизации без полной пересылки» привести в соответствие с фактическим поведением. ## Do not change - Формат payload сопряжения и `hermes://pair` — задание 07. - Схему БД — задание 02; хранение отпечатка добавляется миграцией, схема версионируется по правилам задания 02. - Транспортные гонки — задание 01, принято. - Мёртвый слой — задание 10. ## Anti-checklist 1. Отпечаток проверяется при первом соединении, но при смене молча перезаписывается — TOFU без защиты от подмены. Проверить ветку несовпадения. 2. Custom scheme добавлен, а loopback-сокет остался в коде и всё ещё открывается. Проверить, что сокет больше не создаётся. 3. `state`/verifier хранятся в поле ViewModel — после пересоздания Activity вход ломается. Проверить сценарий возврата из браузера после убийства процесса. 4. Экранирование добавлено через ручную замену `<` и `>` — обойдётся через кавычки в атрибуте. Требование: значение не подставляется вовсе. 5. Тикет перенесён в заголовок, но старая ветка с query оставлена «на совместимость» и продолжает использоваться по умолчанию. 6. `SecurityException` перехвачен и проглочен: приложение не падает, но и не сообщает пользователю, что требуется повторный вход. 7. `HermesHttpException` введён, а в `HermesHostRuntime` остались `contains("401")`. Проверить поиском по файлу все три места. 8. `FLAG_SECURE` ставится на всю Activity навсегда — ломает штатные скриншоты во всём приложении. Требование: только на время показа диалога. 9. Пункт 7 сделан кодером 1 в обход §Роли, без спецификации. 10. Спецификация пункта 7 написана после кода и описывает то, что получилось, а не то, что решено. 11. В отчёте `green` для незапущенной команды (`AGENTS.md §3`). ## Definition of Done - Соединение с хостом по `https` с самоподписанным сертификатом работает после подтверждения отпечатка; подмена сертификата даёт явный отказ и экран подтверждения. - Вход не использует слушающий сокет на устройстве (или, при обоснованном исключении, отменяем и переживает чужое подключение). - Параметр `error` не может внести разметку в колбэк-страницу. - `authUrl` с http отклоняется, если cleartext для хоста не разрешён. - Тикет не появляется в URL WebSocket-соединения. - Отказ Keystore не роняет приложение; пользователю показывается требование повторного входа. - Ложный «401» в теле ответа не приводит к очистке токенов. - Sudo-диалог не попадает в снимки экрана и в список недавних задач. - Второй хост получает объём контекста, соответствующий письменной спецификации; README ей не противоречит. - Все существующие тесты зелёные, ни один не удалён и не ослаблен. ## Required tests `core/auth/AuthRedirectTest.kt` — `state` и verifier переживают пересоздание; неверный `state` отклоняется. `core/auth/CallbackEscapingTest.kt` — значение `error` не попадает в вывод. Обязан падать на base SHA. `core/auth/AuthUrlSchemeTest.kt` — http-`authUrl` при `allowCleartext = false` отклонён. Обязан падать на base SHA. `core/network/TicketTransportTest.kt` — тикет отсутствует в URL; спецсимволы не ломают соединение. Обязан падать на base SHA. `core/security/VaultFailureTest.kt` — отказ инициализации даёт состояние «нужен повторный вход», не исключение. `core/network/HttpErrorTypingTest.kt` — тело ответа со строкой «401» при коде 500 не вызывает очистку токенов. Обязан падать на base SHA. `core/sync/ContextSyncPolicyTest.kt` — объём передаваемого соответствует спецификации; потерянный курсор не приводит к полной пересылке. Обязан падать на base SHA. Проверка отпечатка и `FLAG_SECURE` — на устройстве; без устройства пункты `UNVERIFIED`. ## Required verification ```text ./gradlew --no-daemon testDebugUnitTest ./gradlew --no-daemon connectedDebugAndroidTest ./gradlew --no-daemon lint ./gradlew --no-daemon assembleDebug ``` ## Result `agents/antigravity/done/TASK-2026-08-24-06-auth-and-secrets.md` — разделы `## Кодер 1`, `## Кодер 2 (review + доработка + пункт 7)`, `## Спецификация синхронизации контекста`, `## Вердикт оркестратора`. CRITICAL не закрывается по заявлению исполнителя: нужен независимый re-review с fix SHA.