# OPENCODE — контекст проекта Niko Trust GUI Этот файл — постоянная память контекста. Обновлять при каждом значимом решении/изменении, чтобы не терять суть между сессиями. ## Что это **Niko Trust GUI** — лёгкий GUI-клиент (Rust, **iced**) к trust-релею (`~/niko_trust`, Go, `git.n1ko.dev/Niko/niko_trust`). Один бинарник, Linux + Windows. Для людей, которые не разбираются в криптографии. **Product-флоу (история юзера):** 1. Юзер запускает GUI. 2. Первый запуск: создание identity (ed25519), установка пароля, согласие на автозагрузку. 3. Юзер копирует свой адрес (`trust1q...`) и вставляет его в нужную программу/сервис (например, на свою почту как 2FA). 4. При входе в сервис сервис создаёт `ApprovalRequest` на этот адрес. 5. GUI периодически опрашивает релей и при новом запросе шлёт **нативную нотификацию с кнопками Approve/Deny**. 6. Юзер жмёт кнопку → GUI подписывает `ApprovalResponse` и кладёт его в релей. 7. Сервис видит ответ (`GET /v1/responses?request=`) и пускает/не пускает юзера. ## Ключевые пути/ресурсы | Что | Где | |---|---| | GUI-репо (этот) | `/home/niko/niko_trust_gui` | | Сервер-репо (Go) | `/home/niko/niko_trust` | | Протокол (нормативный) | `~/niko_trust/docs/PROTOCOL.md` | | HTTP API | `~/niko_trust/docs/API.md` | | Trust-модель | `~/niko_trust/docs/TRUST-MODEL.md` | | Адреса | `~/niko_trust/docs/ADDRESS.md` | | Frozen test vectors (эталон) | `~/niko_trust/testdata/vectors/tce_vectors.json` | | Go-эталон кодирования | `~/niko_trust/internal/protocol/encode.go`, `internal/tce/encoder.go` | ## Серверы - **Production:** `https://trust.n1ko.dev` - **Тест сейчас:** `http://192.168.31.155:5743` — значение по умолчанию в конфиге GUI. Настройка переключает на production-пресет. - **`audience` для подписей не хардкодим** — берём из `GET /v1/config` (`{"audience":"..."}`). - Инструкция юзера: **если найден баг в сервере — остановиться и сообщить.** ## Ключевые решения и почему 1. **GUI = iced** (выбрано юзером, а не egui). Elm-style async (Subscription), нативный вид, хорошо для poll-потока. Минус: больше кода. 2. **Seed хранится зашифрованным паролем**: `argon2id` (KDF) → `XChaCha20-Poly1305` (AEAD), файл `0600`. Юзер вводит пароль при каждом запуске. Безопасность важна: seed подписывает Approval. 3. **Windows-уведомления с кнопками** через `winrt-notification`; **fallback** (если COM/регистрация нестабильны) — toast без кнопок + попап в приложении с Approve/Deny. Linux — `notify-rust` (libnotify), кнопки из коробки. 4. **HTTP = `ureq` + rustls** — лёгкий, без openssl, один бинарник, blocking в фоновом потоке под iced `Subscription`. 5. **Верификация подписей — на клиенте.** `PUT /v1/objects` НЕ проверяет подписи (INV-5: релей отвечает "кто сказал", не "верить ли"). Значит GUI обязан: строго декодировать, валидировать pubkey, проверить ed25519 по встроенному в TCE ключу, применить правила объекта (скев ±120 с, lifetime запроса ≤60 с). 6. **TCE — byte-exact.** Ноль нормализации строк, canonical uvarint, сортировка карт по сырым байтам, decimal-числа без float. Проверка — сверка с frozen vectors. 7. **Только клиент держит приватный ключ** (server никогда не импортит signer; INV-1). У нас то же правило: no server-side signing. ## Rust-эквиваленты Go | Go | Rust | |---|---| | `crypto/ed25519` (RFC8032, детерм.) | `ed25519-dalek` | | `filippo.io/edwards25519` (ValidatePubKey) | `curve25519-dalek` | | `btcutil/bech32` (bech32m) | crate `bech32` (Bech32m) | | `crypto/sha256` (object_id) | `sha2` | | `crypto/rand` (nonce/seed) | `rand` + `getrandom` | | argon2id + XChaCha20-Poly1305 | crates `argon2`, `chacha20poly1305` | | HTTP | `ureq` (rustls) | ## Wire-протокол (сводка, детали в PROTOCOL.md) - Magic 21 байт `trust.n1ko.dev/tce/1\0`, затем object_tag (1 байт), version uvarint (=1). - Теги: 0x01 Identity, 0x02 Claim, 0x03 Revocation, 0x04 ApprovalRequest, 0x05 ApprovalResponse, 0x06 AuthAssertion. - `object_id = hex(sha256(tce))`. Подпись = Ed25519 над точными TCE-байтами (не над JSON). - Identity-поле: `uvarint(address_version=0) || uvarint(32) || pubkey(32)` → `00 20 <32>`. - Строки: `uvarint(len) || utf8`, без контроля, без нормализации, без BOM. - uvarint canonical (без `81 00`). Таймстампы: uvarint, диапазон 1e9..4102444800, `0` разрешён только как "no expiry". - Значения: 0x00 null, 0x01 false, 0x02 true, 0x03 string, 0x04 number(canonical decimal text). - Карты: `uvarint(count) || (enc_bytes(key)||value)*`, ключи по `[a-z][a-z0-9]*([._-][a-z0-9]+)*`, сортировка bytewise ascending, дубли запрещены. - Адрес: bech32m, hrp `trust`, payload = `0x00||pubkey33`, строгий decode (lowercase, no pad bits). - Полные лимиты полей и объектов — PROTOCOL.md §6.3 (зашиты в encoder/decoder). ### Поля объектов (строгий порядок!) - **IdentityRegistration (0x01):** identity, alias(≤64), created_at - **Claim (0x02):** issuer, subject, claims(map≥1), created_at, expires_at(0=noexp), serial, nonce(16) - **Revocation (0x03):** issuer, claim_id(32), reason(≤256), created_at, nonce(16) - **ApprovalRequest (0x04):** sender, recipient, action(≤128), payload(map,может быть пустой), message(≤256), created_at, expires_at(≤ created_at+60s!), nonce(16) - **ApprovalResponse (0x05):** request_hash(32), responder, decision(uvarint 0|1), created_at, nonce(16) - **AuthAssertion (0x06):** identity, challenge(32), scope(≤32), audience(≤128), created_at ### API-эндпоинты (детали API.md) - `POST /v1/auth/challenge` → `{"challenge":""}` (rate 30/мин/IP) - `POST /v1/auth/assert` (Envelope AuthAssertion) → `{"session_token","identity","scope"}` - `POST /v1/objects` (Envelope) → `{"object_id"}`; idempotent; 1 response/request (422 иначе) - `GET /v1/objects/{id}`, `GET /v1/claims?subject=`, `GET /v1/requests?recipient=`, `GET /v1/responses?request=`, `GET /v1/revocations?claim=` + `limit`/`offset` - Read-эндпоинты: `Authorization: Bearer ` или `?token=`; сессия 30 мин. - Scope для чтения всего: `"read"` или `"*"` (или точный `read:requests` и т.д.); 401/403. - `GET /v1/config` (audience), `/v1/healthz`, `/v1/readyz`, `/v1/metrics`. ### Auth-поток клиента 1. `GET /v1/config` → audience. 2. `POST /v1/auth/challenge` → 32-байт challenge (hex). 3. Собрать `AuthAssertion{identity=mypub, challenge, scope:"*", audience, created_at=now}`. 4. Подписать TCE, `POST /v1/auth/assert` с Envelope `{tce, signature}` → `session_token`. 5. Дальше read-эндпоинты с Bearer. Сессия живёт 30 мин; при 401 → пере-auth. ### Ответ на запрос 1. GET `/v1/requests?recipient=<мой адрес>&limit=N`. Каждый item = Envelope `{tce, signature}` + object view. 2. Строго декодировать ApprovalRequest из `tce`; извлечь sender, action, message, nonce, created_at, expires_at; вычислить request_hash = sha256(tce). 3. Проверить подпись: address.ValidatePubKey(sender) + ed25519.Verify(sender, tce, signature). 4. Проверить время: now ∈ [created_at−120s, expires_at+120s]. 5. Показать: message + sender-address + действие. NOT как endorsement от релея. 6. Approve → `ApprovalResponse{request_hash, responder=mypub, decision=1, created_at=now, nonce=rand16}` → подписать → `POST /v1/objects`. Deny → decision=0. 7. Один ответ на запрос — сервер отклонит второй (422). ## Тестовые векторы (эталон) - Партии: NikoCraft seed=`01`×32, Niko seed=`02`×32 (указы в vectors: pubkey, address). - Проверки: encoder даёт тот же hex; object_id совпадает; dalek verify подписи ok; decoder→re-encode тождество; адреса совпадают; таблица canonical numbers; выборочные rejects на decoder. ## Статус - [x] Изучен сервер-репо, протокол, API, векторы (source review + read-only smoke GET: healthz/readyz/config ok, claims без токена → честный 401). Видимых багов сервера нет. - [x] OPENCODE.md создан. - [x] Скаффолд Cargo (feature `gui` = iced; `cargo test --no-default-features` = быстрое ядро без iced). - [x] tce (encoder/decoder/number) + address (свой bech32m) + signer (ed25519-dalek 3, is_weak+verify_strict) + protocol (6 объектов). **Все frozen vectors byte-exact: encode→hex, object_id, подпись (dalek воспроизводит детерменированные подписи), decode→re-encode, reject-набор, числа, адреса.** clippy чист. - [x] keyring (argon2id + XChaCha20-Poly1305, файл 0600). **Готово**: `src/keyring.rs`, `FileFormat{v,kdf,m,t,p,salt,nonce,cipher}` (base64), `aead` добавлен в deps (0.6), `XNonce::from`, 5 unit-тестов (roundtrip, wrong-password, kdf-params), clippy чист. - [x] relay + live-тест. **Готово**: `src/relay.rs` + `tests/live_relay.rs` (env `NIKO_RELAY`). Auth-рукопожатие (config→audience, challenge, AuthAssertion{scope:"*"}, assert→Bearer), `fetch_requests` (строгая декодировка + validate_pubkey + verify + recipient must = me, пропускает ядовитые конверты), `respond` (проверка окна запроса ±120с, request_hash=sha256(tce), POST objects), `store`. **3 live-теста против `192.168.31.155:5743` проходят:** roundtrip (ответ принят, 2-й ответ на тот же запрос → 422), wrong-recipient отфильтрован, forged-подпись отброшена. ВАЖНО: у ureq 3.4 API другой (http::Response + body_mut().read_json()/read_to_string(), `http_status_as_error(false)` в конфиге агента). - [x] inbox/verify. **Готово**: `src/inbox.rs` — `Inbox` (в памяти): dedup по request_id (каждый запрос всплывает один раз), история `HistoryItem{request_id_hex, sender(Address), action, message, created_at, expires_at, outcome}`, `in_window(created,expires,now)` с ±120s skew (§13.1), outcome Pending/Approved/Denied/TimedOut/Stale. `observe()` возвращает только actionable (`Pending`); истёкшие/из будущего записываются в историю как TimedOut/Stale. `Address` получил `PartialEq, Eq`. 3 unit-теста (dedup, window+outcomes, outcome_changes). - [x] notify + autostart. **Готово**: `src/notify.rs` — `ApprovalNotification{sender,action,message}`, `show()` (notify-rust, кнопки Approve/Deny, ждёт через отдельный поток), `PromptHandle::wait_timeout`, `PromptOutcome{Approved,Denied,Ignored}`, `to_decision()`, `interactive_supported()` = `dbus_stack().is_some()`. `src/autostart.rs` — обёртка `auto-launch` (XDG autostart на Linux, registry на Windows), `APP_NAME="niko-trust"`, `enable/disable/is_enabled`. Smoke-тест `examples/smoke.rs` отработал на этой машине: dbus есть, уведомление показывается (кнопки кликнуть некому — демона нет, outcome=Ignored после таймаута), autostart is_enabled=false. Модули `notify`/`autostart` вне feature-gate `gui` (не зависят от iced). - [x] UI iced (последний блок). **Готово**: `src/main.rs` (модуль `gui`, feature `gui`; вне gui — стаб `main` с ошибкой). Сборка: iced 0.14 + фича `tokio` (default-бэкенд `thread-pool` имеет пустой `time`, без tokio/smol нет `iced::time::every`). Экраны: lock (первый запуск = создание identity, иначе разблокировка; поле relay URL, default `https://trust.n1ko.dev`) и main (заголовок с адресом/релеем, статус-строка, карточка запроса с Approve/Deny, история). Poll-loop: `Subscription` `time::every(5s)` → `Relay::fetch_requests` → `Inbox::observe` → очередь (`VecDeque`) → текущий `Prompt`. Нативные нотификации: `notify::show` в отдельном потоке, outcome в UI через `futures::channel::mpsc` + глобальный слот `PROMPT_RX` + `Subscription::run_with(request_id, |_| iced::stream::channel(...))` (инфраструктура `iced::stream`/`futures` реэкспортируется iced'ом). Ответ: `Relay::respond` в `Task::perform`, при истечении окна (проверка `in_window`) помечается TimedOut. **Изменено в ядре**: в `IncomingRequest` добавлено поле `tce: Vec` (точные байты запроса нужны для request_hash при ответе; re-encode не byte-exact). Проверено: `cargo build`+`clippy --all-targets` чистые (и с gui, и `--no-default-features`), 10 lib + 3 tce + 3 live тестов зелёные, **live-тесты прогнаны против production `NIKO_RELAY=https://trust.n1ko.dev` — проходят**, GUI запускается (без падений, первый запуск = экран создания). ## Команды - `cargo build` / `cargo test` / `cargo clippy` в `/home/niko/niko_trust_gui`. - Live-интеграционные тесты: env `NIKO_RELAY=https://trust.n1ko.dev` (или `http://192.168.31.155:5743`). - Windows: target `x86_64-pc-windows-gnu` установлен; кросс-сборка отдельно.