- Byte-exact TCE codec (encoder/decoder, canonical numbers, vectors-tested) - Ed25519 identities with bech32m trust1… addresses - Relay client: challenge/auth handshake, request feed, responses, transparent re-auth on 401 - iced GUI (Material-dark, Solana-style palette): tab shell, approval prompts with native toasts on Linux, address book, history, settings, password change - Portable single-file vault (identity + book + history + settings) - Live 2FA roundtrip examples (send_2fa, await_2fa)
15 KiB
15 KiB
OPENCODE — контекст проекта Niko Trust GUI
Этот файл — постоянная память контекста. Обновлять при каждом значимом решении/изменении, чтобы не терять суть между сессиями.
Что это
Niko Trust GUI — лёгкий GUI-клиент (Rust, iced) к trust-релею
(~/niko_trust, Go, git.n1ko.dev/Niko/niko_trust). Один бинарник, Linux + Windows.
Для людей, которые не разбираются в криптографии.
Product-флоу (история юзера):
- Юзер запускает GUI.
- Первый запуск: создание identity (ed25519), установка пароля, согласие на автозагрузку.
- Юзер копирует свой адрес (
trust1q...) и вставляет его в нужную программу/сервис (например, на свою почту как 2FA). - При входе в сервис сервис создаёт
ApprovalRequestна этот адрес. - GUI периодически опрашивает релей и при новом запросе шлёт нативную нотификацию с кнопками Approve/Deny.
- Юзер жмёт кнопку → GUI подписывает
ApprovalResponseи кладёт его в релей. - Сервис видит ответ (
GET /v1/responses?request=<id>) и пускает/не пускает юзера.
Ключевые пути/ресурсы
| Что | Где |
|---|---|
| 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":"..."}).- Инструкция юзера: если найден баг в сервере — остановиться и сообщить.
Ключевые решения и почему
- GUI = iced (выбрано юзером, а не egui). Elm-style async (Subscription), нативный вид, хорошо для poll-потока. Минус: больше кода.
- Seed хранится зашифрованным паролем:
argon2id(KDF) →XChaCha20-Poly1305(AEAD), файл0600. Юзер вводит пароль при каждом запуске. Безопасность важна: seed подписывает Approval. - Windows-уведомления с кнопками через
winrt-notification; fallback (если COM/регистрация нестабильны) — toast без кнопок + попап в приложении с Approve/Deny. Linux —notify-rust(libnotify), кнопки из коробки. - HTTP =
ureq+ rustls — лёгкий, без openssl, один бинарник, blocking в фоновом потоке под icedSubscription. - Верификация подписей — на клиенте.
PUT /v1/objectsНЕ проверяет подписи (INV-5: релей отвечает "кто сказал", не "верить ли"). Значит GUI обязан: строго декодировать, валидировать pubkey, проверить ed25519 по встроенному в TCE ключу, применить правила объекта (скев ±120 с, lifetime запроса ≤60 с). - TCE — byte-exact. Ноль нормализации строк, canonical uvarint, сортировка карт по сырым байтам, decimal-числа без float. Проверка — сверка с frozen vectors.
- Только клиент держит приватный ключ (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":"<hex>"}(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>или?token=; сессия 30 мин. - Scope для чтения всего:
"read"или"*"(или точныйread:requestsи т.д.); 401/403. GET /v1/config(audience),/v1/healthz,/v1/readyz,/v1/metrics.
Auth-поток клиента
GET /v1/config→ audience.POST /v1/auth/challenge→ 32-байт challenge (hex).- Собрать
AuthAssertion{identity=mypub, challenge, scope:"*", audience, created_at=now}. - Подписать TCE,
POST /v1/auth/assertс Envelope{tce, signature}→session_token. - Дальше read-эндпоинты с Bearer. Сессия живёт 30 мин; при 401 → пере-auth.
Ответ на запрос
- GET
/v1/requests?recipient=<мой адрес>&limit=N. Каждый item = Envelope{tce, signature}+ object view. - Строго декодировать ApprovalRequest из
tce; извлечь sender, action, message, nonce, created_at, expires_at; вычислить request_hash = sha256(tce). - Проверить подпись: address.ValidatePubKey(sender) + ed25519.Verify(sender, tce, signature).
- Проверить время: now ∈ [created_at−120s, expires_at+120s].
- Показать: message + sender-address + действие. NOT как endorsement от релея.
- Approve →
ApprovalResponse{request_hash, responder=mypub, decision=1, created_at=now, nonce=rand16}→ подписать →POST /v1/objects. Deny → decision=0. - Один ответ на запрос — сервер отклонит второй (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.
Статус
- Изучен сервер-репо, протокол, API, векторы (source review + read-only smoke GET: healthz/readyz/config ok, claims без токена → честный 401). Видимых багов сервера нет.
- OPENCODE.md создан.
- Скаффолд Cargo (feature
gui= iced;cargo test --no-default-features= быстрое ядро без iced). - 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 чист.
- 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 чист. - relay + live-тест. Готово:
src/relay.rs+tests/live_relay.rs(envNIKO_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)в конфиге агента). - 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). - 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-gategui(не зависят от iced). - UI iced (последний блок). Готово:
src/main.rs(модульgui, featuregui; вне gui — стабmainс ошибкой). Сборка: iced 0.14 + фичаtokio(default-бэкендthread-poolимеет пустойtime, без tokio/smol нетiced::time::every). Экраны: lock (первый запуск = создание identity, иначе разблокировка; поле relay URL, defaulthttps://trust.n1ko.dev) и main (заголовок с адресом/релеем, статус-строка, карточка запроса с Approve/Deny, история). Poll-loop:Subscriptiontime::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<u8>(точные байты запроса нужны для request_hash при ответе; re-encode не byte-exact). Проверено:cargo build+clippy --all-targetsчистые (и с gui, и--no-default-features), 10 lib + 3 tce + 3 live тестов зелёные, live-тесты прогнаны против productionNIKO_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установлен; кросс-сборка отдельно.