hermes-android/agy-work/TASK-2026-08-24-06-auth-and-secrets.md

17 KiB
Raw Blame History

Task 06: Авторизация, TLS и обращение с секретами (hermes-android)

Repo: ochenstarik-ui/hermes-android Assigned to: Antigravity (режим оркестратора, два кодера) Priority: CRITICAL (перехват кода авторизации, утечка секретов между хостами) Date: 2026-08-24 Base SHA: результат задания 05 — указать фактический SHA при выдаче Зависимость: задания 0105 приняты. Пункт 1 продолжает SEC-01, вторая половина которого была сознательно отложена из задания 01.

Роли и протокол

Роль Модель Что делает
Оркестратор Antigravity Разбивает работу, маршрутизирует, принимает результат
Кодер 1 Gemini Flash 3.7 high Реализация §Scope пунктов 16
Кодер 2 Gemini Pro high Независимая проверка, доводка, и полностью пункт 7

Пункт 7 (синхронизация контекста) отдан кодеру 2 намеренно: он требует проектного решения и оценки модели угроз, а не механической правки. Кодер 1 к нему не приступает.

Раунд 1 (кодер 1). Тесты из §Required tests для пунктов 16, фиксация падения на base SHA, затем код. Нерешённое — в OPEN QUESTIONS. Раунд 2 (кодер 2). Независимое воспроизведение падения, проход §Anti-checklist с явными отметками, доводка пунктов 16, затем спецификация и реализация пункта 7. Findings по шкале. Раунд 3 (оркестратор). Приёмка при фактическом выводе команд. Пункт 7 принимается только вместе с письменной спецификацией того, что именно передаётся между хостами.

Проблема

1. TLS к LAN-хосту не работает. Задание 01 открыло cleartext-путь, но у локального хоста сертификат самоподписанный, trust anchors не настроены — вариант с https:// по-прежнему не соединяется. Пользователь вынужден оставаться на http.

2. PKCE через loopback-сокет (SEC-02). PkceLoopbackAuthManager.kt:35,41,62ServerSocket(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-54validateUrlScheme вызывается только при обмене кода; сам 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.ktstate и 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

./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.