diff --git a/DAO_запуск/README.md b/DAO_запуск/README.md index ac7fd93..6969f65 100644 --- a/DAO_запуск/README.md +++ b/DAO_запуск/README.md @@ -134,7 +134,7 @@ Что сделать: -- продумать и реализовать смену `root key`, `device key`, `blockchain key`; +- продумать и реализовать смену `root key`, `client key`, `blockchain key`; - описать ограничения, кто и в каком сценарии может менять каждый тип ключа; - продумать, как не потерять доступ и как обновлять доверие к новым ключам. diff --git a/DOC/libs/shine-server-bd/DOC.md b/DOC/libs/shine-server-bd/DOC.md index c95247f..aae2dfa 100644 --- a/DOC/libs/shine-server-bd/DOC.md +++ b/DOC/libs/shine-server-bd/DOC.md @@ -8,7 +8,7 @@ shine.db.SqliteDbController — один вход в БД: читает db.path, shine.db.DatabaseInitializer — разовая сборка схемы (таблицы + индексы). -shine.db.entities.* — POJO-модели строк таблиц (без логики, только поля/геттеры/сеттеры + иногда удобные методы вроде getDeviceKeyByte()). +shine.db.entities.* — POJO-модели строк таблиц (без логики, только поля/геттеры/сеттеры + иногда удобные методы вроде getClientKeyByte()). shine.db.dao.* — DAO по таблицам: ActiveSessionsDAO, SolanaUsersDAO, UserParamsDAO, IpGeoCacheDAO, BlockchainStateDAO, BlocksDAO; плюс “сервисные” DAO: UserCreateDAO — атомарная регистрация пользователя в транзакции (BEGIN IMMEDIATE + rollback/commit). diff --git a/DOC/Описание БД.md b/DOC/Описание БД.md index ac06e07..684098a 100644 --- a/DOC/Описание БД.md +++ b/DOC/Описание БД.md @@ -38,7 +38,7 @@ message_stats ⭐ solana_users login — TEXT PK — уникальный логин пользователя -device_key — TEXT NOT NULL — публичный ключ устройства (Base64(32) / HEX(64)) +client_key — TEXT NOT NULL — публичный ключ устройства (Base64(32) / HEX(64)) solana_key — TEXT NULL — публичный ключ Solana-аккаунта active_sessions @@ -61,7 +61,7 @@ login — TEXT NOT NULL, FK → solana_users(login) param — TEXT NOT NULL time_ms — INTEGER NOT NULL value — TEXT NOT NULL -device_key — TEXT NULL +client_key — TEXT NULL signature — TEXT NULL Ограничение: diff --git a/Dev_Docs/API/01_User_Registration_API.md b/Dev_Docs/API/01_User_Registration_API.md index a975d79..2f84d7e 100644 --- a/Dev_Docs/API/01_User_Registration_API.md +++ b/Dev_Docs/API/01_User_Registration_API.md @@ -35,7 +35,7 @@ "blockchainName": "anya-001", "solanaKey": "BASE64_32_PUBLIC_KEY", "blockchainKey": "BASE64_32_PUBLIC_KEY", - "deviceKey": "BASE64_32_PUBLIC_KEY", + "clientKey": "BASE64_32_PUBLIC_KEY", "bchLimit": 1000000 } } @@ -99,7 +99,7 @@ "blockchainName": "anya-001", "solanaKey": "BASE64_32_PUBLIC_KEY", "blockchainKey": "BASE64_32_PUBLIC_KEY", - "deviceKey": "BASE64_32_PUBLIC_KEY", + "clientKey": "BASE64_32_PUBLIC_KEY", "serverLastGlobalNumber": 128, "serverLastGlobalHash": "4f...ab", "serverBlockchainSizeBytes": 45212, diff --git a/Dev_Docs/API/02_Authentication_API.md b/Dev_Docs/API/02_Authentication_API.md index 65f4024..1ace107 100644 --- a/Dev_Docs/API/02_Authentication_API.md +++ b/Dev_Docs/API/02_Authentication_API.md @@ -11,7 +11,7 @@ Логика раздела такая: -- сначала клиент либо начинает создание новой сессии через `deviceKey`; +- сначала клиент либо начинает создание новой сессии через `clientKey`; - либо начинает вход в уже созданную сессию через `sessionKey`; - сервер на первом шаге выдаёт challenge/nonce; - на втором шаге клиент присылает подписанный ответ; @@ -55,7 +55,7 @@ 2. Вход в существующую сессию: `SessionChallenge` -> `SessionLogin` -`deviceKey` используется для создания новой сессии. +`clientKey` используется для создания новой сессии. `sessionKey` используется для входа в уже созданную сессию. @@ -119,7 +119,7 @@ ed25519/BASE64_PUBLIC_KEY "storagePwd": "BASE64_OR_APP_SPECIFIC_SECRET", "timeMs": 1774600000123, "authNonce": "nonce", - "deviceKey": "BASE64_DEVICE_PUBLIC_KEY", + "clientKey": "BASE64_DEVICE_PUBLIC_KEY", "signatureB64": "BASE64_SIGNATURE", "sessionType": 1, "clientPlatform": "Web", @@ -138,15 +138,15 @@ AUTH_CREATE_SESSION:{login}:{sessionKey}:{storagePwd}:{timeMs}:{authNonce} Перед проверкой подписи сервер должен: -1. взять актуальный `solana_users.device_key`; -2. сравнить его с `payload.deviceKey`; +1. взять актуальный `solana_users.client_key`; +2. сравнить его с `payload.clientKey`; 3. только потом проверять подпись. -Если ключ не совпадает, сервер возвращает ошибку `DEVICE_KEY_NOT_ACTUAL`. +Если `clientKey` не совпадает, сервер возвращает ошибку `DEVICE_KEY_NOT_ACTUAL`. На будущее: -- для ротации `device_key` желательно добавить перепроверку через Solana. +- для ротации `client_key` желательно добавить перепроверку через Solana. ### Успешный ответ @@ -172,15 +172,15 @@ AUTH_CREATE_SESSION:{login}:{sessionKey}:{storagePwd}:{timeMs}:{authNonce} - `501 / NO_LOGIN` — у пользователя на сервере не заполнен `login`. - `400 / EMPTY_STORAGE_PWD` — пустой `storagePwd`. - `400 / EMPTY_SESSION_KEY` — пустой `sessionKey`. -- `422 / UNSUPPORTED_KEY_ALGORITHM` — префикс алгоритма в `sessionKey` или `deviceKey` не поддерживается текущим сервером. -- `400 / BAD_BASE64` — неверный Base64 в `sessionKey`, `deviceKey` или `signatureB64`. +- `422 / UNSUPPORTED_KEY_ALGORITHM` — префикс алгоритма в `sessionKey` или `clientKey` не поддерживается текущим сервером. +- `400 / BAD_BASE64` — неверный Base64 в `sessionKey`, `clientKey` или `signatureB64`. - `400 / EMPTY_SIGNATURE` — пустая подпись. - `400 / TIME_SKEW` — время клиента отличается от серверного больше допустимого окна. -- `400 / NO_DEVICE_KEY` — у пользователя в БД отсутствует `deviceKey`. +- `400 / NO_DEVICE_KEY` — у пользователя в БД отсутствует `clientKey`. - `400 / EMPTY_AUTH_NONCE` — пустой `authNonce`. - `400 / AUTH_NONCE_MISMATCH` — `authNonce` не соответствует значению из `AuthChallenge`. -- `400 / EMPTY_DEVICE_KEY` — в запросе не передан `deviceKey`. -- `422 / DEVICE_KEY_NOT_ACTUAL` — `deviceKey` не совпадает с актуальной версией на сервере. +- `400 / EMPTY_DEVICE_KEY` — в запросе не передан `clientKey`. +- `422 / DEVICE_KEY_NOT_ACTUAL` — `clientKey` не совпадает с актуальной версией на сервере. - `422 / BAD_SIGNATURE` — подпись не прошла проверку. - `460 / SESSION_TYPE_MISMATCH` — `sessionType` не совпадает с типом сессии, уже опубликованным для этого `sessionKey` в Solana PDA. - `501 / SESSION_TYPE_PDA_CHECK_FAILED` — сервер не смог проверить `sessionType` по Solana PDA. @@ -314,7 +314,7 @@ SESSION_LOGIN:{sessionId}:{timeMs}:{nonce} В этом потоке: -- новое устройство не владеет `deviceKey` и не проходит обычный `CreateAuthSession`; +- новое устройство не владеет `clientKey` и не проходит обычный `CreateAuthSession`; - пароль проверяется сервером только как фильтр; - решение об одобрении принимает уже авторизованная доверенная сессия пользователя; - сервер не расшифровывает `encryptedPayload` и не становится источником приватных ключей. diff --git a/Dev_Docs/API/10_User_Params_API.md b/Dev_Docs/API/10_User_Params_API.md index 8b5e65e..f64ef99 100644 --- a/Dev_Docs/API/10_User_Params_API.md +++ b/Dev_Docs/API/10_User_Params_API.md @@ -21,7 +21,7 @@ "param": "display_name", "time_ms": 1774700000123, "value": "Alice", - "device_key": "BASE64_DEVICE_PUBLIC_KEY", + "client_key": "BASE64_DEVICE_PUBLIC_KEY", "signature": "BASE64_SIGNATURE" } } @@ -76,7 +76,7 @@ "param": "display_name", "time_ms": 1774700000123, "value": "Alice", - "device_key": "BASE64_DEVICE_PUBLIC_KEY", + "client_key": "BASE64_DEVICE_PUBLIC_KEY", "signature": "BASE64_SIGNATURE" } } @@ -116,7 +116,7 @@ "param": "display_name", "time_ms": 1774700000123, "value": "Alice", - "device_key": "BASE64_DEVICE_PUBLIC_KEY", + "client_key": "BASE64_DEVICE_PUBLIC_KEY", "signature": "BASE64_SIGNATURE" } ] @@ -126,4 +126,4 @@ ## Примечание -Имена JSON-полей `time_ms` и `device_key` сейчас соответствуют Java-модели ответа/запроса и должны передаваться именно в таком виде. +Имена JSON-полей `time_ms` и `client_key` сейчас соответствуют Java-модели ответа/запроса и должны передаваться именно в таком виде. diff --git a/Dev_Docs/Future_Features/dao_запуск/2026-06-05_esp32_hardware_wallet_device_session.md b/Dev_Docs/Future_Features/dao_запуск/2026-06-05_esp32_hardware_wallet_device_session.md index b86d377..c98a3ca 100644 --- a/Dev_Docs/Future_Features/dao_запуск/2026-06-05_esp32_hardware_wallet_device_session.md +++ b/Dev_Docs/Future_Features/dao_запуск/2026-06-05_esp32_hardware_wallet_device_session.md @@ -22,7 +22,7 @@ ESP32 становится аппаратным HSM (hardware security module): ### ESP32 (основная работа) - [ ] Инициализация WiFi (SSID/пароль в NVS) - [ ] WebSocket-клиент (`WebSocketsClient`) — постоянное соединение с сервером -- [ ] Авторизация на сервере: `AuthChallenge` → `CreateAuthSession` через `deviceKey` (уже есть в NVS), сохранить `sessionId` в NVS +- [ ] Авторизация на сервере: `AuthChallenge` → `CreateAuthSession` через `clientKey` (уже есть в NVS), сохранить `sessionId` в NVS - [ ] Обработчик входящих WebSocket-событий: JSON-парсинг, диспетчер по типу - [ ] Новые UI-экраны: «Разрешить сессию?» и «Подписать?» с кнопками Да/Нет - [ ] Расширенное хранилище ключей в NVS (произвольные именованные ключи сверх базовых трёх) diff --git a/Dev_Docs/Keys/DERIVATION.md b/Dev_Docs/Keys/DERIVATION.md index b342522..77a864e 100644 --- a/Dev_Docs/Keys/DERIVATION.md +++ b/Dev_Docs/Keys/DERIVATION.md @@ -56,7 +56,7 @@ seed(32) = SHA-256(material) |------|---------|---------------------| | root | `root.key` | Личность. Подписывает unsigned-часть PDA-записи (`RootKeyBlock`). | | blockchain | `bch.key` | Подписывает `LastBlockState` персонального блокчейна (`blockchain_public_key`). | -| device / **Solana** | `dev.key` | Ключ устройства = Solana-ключ. Fee payer и подпись Solana-транзакций; адрес кошелька = `base58(devicePub)`. См. §3. | +| device / **Solana** | `client.key` | Ключ устройства = Solana-ключ. Fee payer и подпись Solana-транзакций; адрес кошелька = `base58(clientPub)`. См. §3. | | homeserver | `homeserver.key:<имя>` | Ключ homeserver-устройства, по одному на каждый homeserver (различитель — имя). См. §4. | Полные роли каждого ключа — в `Dev_Docs/Keys/README.md`. @@ -67,16 +67,16 @@ seed(32) = SHA-256(material) Отдельного «солана-ключа» нет. На Solana работают два ключа: -- **`dev.key` (device) — пополняемый кошелёк и fee payer.** Solana-адрес = `base58(devicePub)`. +- **`client.key` (device) — пополняемый кошелёк и fee payer.** Solana-адрес = `base58(clientPub)`. Этим ключом оплачиваются и подписываются `create_user_pda` / `update_user_pda`. Пополнять SOL нужно именно на этот адрес. - **`root.key` — авторитет записи**, подписывает unsigned-часть PDA через Ed25519-инструкцию, но **не** является fee payer. Соответствует формату PDA `shine-solana/shine/doc/formats/shine-user-pda-format-v.1.0.md` §2.1 -(«create/update оплачиваются с `device_key`», «root_key — не fee payer»). +(«create/update оплачиваются с `client_key`», «root_key — не fee payer»). Кратко про роли на Solana: `root.key` — это **главный (master) ключ**: им управляют PDA-записью -(`create/update`) и через это можно заменить все остальные ключи; `dev.key` — это **пополняемый +(`create/update`) и через это можно заменить все остальные ключи; `client.key` — это **пополняемый кошелёк и плательщик комиссий**. Полное описание ролей — `Dev_Docs/Keys/README.md`. --- @@ -118,9 +118,9 @@ homeserver.key:home-b -> ключ B - `shine-UI/server-ui/js/server-ui-shared.js` — те же root/bch/dev для серверного UI (~147–160). ### Solana-ключ / адрес кошелька (UI) -- `shine-UI/js/pages/registration-payment-view.js` — `deriveUserWalletAddress`: адрес = `base58(devicePub)` (~113). -- `shine-UI/js/pages/topup-view.js` — `deviceWalletAddressFromBundle`: тот же канонический адрес из `preGeneratedKeyBundle.devicePair`. - Прежний расходящийся путь `deriveWalletFromPassword` (прямой Argon2 по `dev.key`, мимо `masterSecret`) удалён. +- `shine-UI/js/pages/registration-payment-view.js` — `deriveUserWalletAddress`: адрес = `base58(clientPub)` (~113). +- `shine-UI/js/pages/topup-view.js` — `clientWalletAddressFromBundle`: тот же канонический адрес из `preGeneratedKeyBundle.clientPair`. + Прежний расходящийся путь `deriveWalletFromPassword` (прямой Argon2 по `client.key`, мимо `masterSecret`) удалён. ### Деривация ключей (прошивка ESP32) - `ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/main-device/shine_homeserver_main/shine_homeserver_main.ino` @@ -131,7 +131,7 @@ homeserver.key:home-b -> ключ B ### Формат PDA (куда попадают ключи) - `shine-solana/shine/doc/formats/shine-user-pda-format-v.1.0.md` - — `RootKeyBlock` §6, `DeviceKeyBlock` §7, `blockchain_public_key` §9, `SessionsBlock`/`session_type=100` §13, оплата §2.1. + — `RootKeyBlock` §6, `ClientKeyBlock` §7, `blockchain_public_key` §9, `SessionsBlock`/`session_type=100` §13, оплата §2.1. ### Сервер (тестовый seed) - `SHiNE-server/src/test/java/test/it/cases/SeedDataPopulationHelper.java` `deriveKeysFromPassword` (~246) — diff --git a/Dev_Docs/Keys/README.md b/Dev_Docs/Keys/README.md index 863a9da..4afb114 100644 --- a/Dev_Docs/Keys/README.md +++ b/Dev_Docs/Keys/README.md @@ -8,9 +8,9 @@ В SHiNE у пользователя есть несколько уровней ключей: -- `root key` - главный (master) ключ пользователя: тот, кто им владеет, управляет пользовательской PDA в Solana и может заменить все остальные ключи. Это не пополняемый кошелёк (комиссии платит `device key`). +- `root key` - главный (master) ключ пользователя: тот, кто им владеет, управляет пользовательской PDA в Solana и может заменить все остальные ключи. Это не пополняемый кошелёк (комиссии платит `client key`). - `blockchain key` - ключ записи в персональный SHiNE-блокчейн пользователя. -- `device key` - общий ключ пользовательских устройств для повседневной работы, звонков, DM и мелких платежей. +- `client key` - общий ключ пользовательских устройств для повседневной работы, звонков, DM и мелких платежей. - `session key` - ключ конкретной сессии или конкретного устройства для авторизации на сервере. Главная идея: самые важные ключи можно держать на доверенном серверном или аппаратном устройстве, а обычные клиентские устройства получают только ключи, нужные для текущей работы. @@ -30,7 +30,7 @@ `root key` — это **главный (master) ключ** в следующем смысле: зная `root key`, можно управлять пользовательской PDA-записью в Solana (`create_user_pda` / `update_user_pda`) и тем самым **заменить все остальные ключи** пользователя (device, blockchain, homeserver). Поэтому компрометация `root key` равносильна компрометации всей личности пользователя. -Важно не путать авторитет и кошелёк: `root key` — это авторитет над PDA-записью, а **SOL-комиссии за create/update платит `device key`** (он же fee payer и адрес для пополнения). Подробнее о том, какой ключ за что отвечает на Solana, — в `Dev_Docs/Keys/DERIVATION.md`, §3. +Важно не путать авторитет и кошелёк: `root key` — это авторитет над PDA-записью, а **SOL-комиссии за create/update платит `client key`** (он же fee payer и адрес для пополнения). Подробнее о том, какой ключ за что отвечает на Solana, — в `Dev_Docs/Keys/DERIVATION.md`, §3. ## `blockchain key` @@ -50,9 +50,9 @@ Рабочая логика по умолчанию должна использовать последнюю актуальную ветку. Старые ветки остаются читаемыми и показывают историю смены ключей. -## `device key` +## `client key` -`device key` - общий ключ, который знают доверенные устройства пользователя. +`client key` - общий ключ, который знают доверенные устройства пользователя. Назначение: @@ -63,11 +63,11 @@ - derivation Arweave-кошелька; - оплата или подготовка добавления данных в Arweave-кошелек по отдельному протоколу. -Arweave-кошелёк должен выводиться из `device key` по протоколу: +Arweave-кошелёк должен выводиться из `client key` по протоколу: - `Dev_Docs/Протоколы/SHINE_ARWEAVE_DERIVATION_V1.md` -Если пользователь теряет только `device key`, в худшем случае ломается повседневная переписка и доступ конкретных устройств к ежедневным операциям. `root key` и `blockchain key` при правильной архитектуре остаются отдельно защищёнными. +Если пользователь теряет только `client key`, в худшем случае ломается повседневная переписка и доступ конкретных устройств к ежедневным операциям. `root key` и `blockchain key` при правильной архитектуре остаются отдельно защищёнными. ## `session key` @@ -83,7 +83,7 @@ Arweave-кошелёк должен выводиться из `device key` по - авторизация сессии на сервере; - привязка устройства к пользователю; - подтверждение запросов от конкретной сессии; -- доступ к зашифрованному `device key` после успешной авторизации. +- доступ к зашифрованному `client key` после успешной авторизации. Одна и та же сессия может быть пригодна для подключения к нескольким серверам пользователя, если архитектура конкретного пользователя это допускает. @@ -108,14 +108,14 @@ Arweave-кошелёк должен выводиться из `device key` по Обычное устройство обычно имеет: - собственный `session key`; -- зашифрованный `device key`, который открывается после авторизации; +- зашифрованный `client key`, который открывается после авторизации; - доступ к DM, звонкам и обычным пользовательским операциям. Доверенное серверное или аппаратное устройство может иметь: - `root key`; - `blockchain key`; -- `device key`; +- `client key`; - собственный `session key`. Такая сессия может подписывать операции повышенной важности по запросам пользователя. @@ -139,7 +139,7 @@ Self-message - это сообщение пользователя самому Входящее сообщение может быть зашифровано: -- `device key`; +- `client key`; - `session key`; - отдельным ключом конкретного чата; - другим ключом, который уже известен клиенту. @@ -158,12 +158,12 @@ Self-message - это сообщение пользователя самому ## Связанные документы -- `Dev_Docs/Keys/DERIVATION.md` - **источник истины по конкретной деривации** секрета и ключей (формулы Argon2id, `base64|suffix→SHA-256→Ed25519`, суффиксы `root.key`/`bch.key`/`dev.key`/`homeserver.key:<имя>`, Solana-ключ, ссылки на код). +- `Dev_Docs/Keys/DERIVATION.md` - **источник истины по конкретной деривации** секрета и ключей (формулы Argon2id, `base64|suffix→SHA-256→Ed25519`, суффиксы `root.key`/`bch.key`/`client.key`/`homeserver.key:<имя>`, Solana-ключ, ссылки на код). - `Dev_Docs/Personal_Messages/README.md` - текущая документация личных сообщений. - `Dev_Docs/Blockchain/README.md` - точка входа по форматам SHiNE-блокчейна. - `Dev_Docs/Solana_Architecture/README.md` - архитектура Solana-программ, PDA-счетов, DAO и движения средств. - `Dev_Docs/Инициализация_Solana_регистрации/README.md` - деплой и первичная инициализация Solana-регистрации. -- `Dev_Docs/Протоколы/SHINE_ARWEAVE_DERIVATION_V1.md` - derivation Arweave-кошелька из `device key`. +- `Dev_Docs/Протоколы/SHINE_ARWEAVE_DERIVATION_V1.md` - derivation Arweave-кошелька из `client key`. ## Что нужно уточнить перед реализацией @@ -172,5 +172,5 @@ Self-message - это сообщение пользователя самому - какие операции требуют `root key`, а какие достаточно подписывать `blockchain key`; - формат self-message-команд; - порядок перебора ключей при расшифровке входящих сообщений; -- правила ротации `device key` и восстановления доступа после потери устройства; +- правила ротации `client key` и восстановления доступа после потери устройства; - какие типы серверных и аппаратных сессий нужны в первой реализации. diff --git a/Dev_Docs/Pending_Features/2026-06-19_2015_esp32_pairing_requests.md b/Dev_Docs/Pending_Features/2026-06-19_2015_esp32_pairing_requests.md index 2eb2dbe..2966137 100644 --- a/Dev_Docs/Pending_Features/2026-06-19_2015_esp32_pairing_requests.md +++ b/Dev_Docs/Pending_Features/2026-06-19_2015_esp32_pairing_requests.md @@ -11,7 +11,7 @@ - на ESP32 после успешной homeserver-авторизации в `SETTINGS` появляется пункт `Device requests` первым; - `REFRESH` реально загружает активные заявки; - на экране видно две плитки, список листается вертикально; - - client-session заявка после `YES` подключается с передачей только `device key`; + - client-session заявка после `YES` подключается с передачей только `client key`; - wallet-session заявка после `YES` подключается без передачи ключей, через выпуск отдельной wallet-session; - `NO` отклоняет заявку и она исчезает из списка активных. diff --git a/Dev_Docs/Pending_Features/2026-06-21_1735_esp32_wallet_selector_and_extension_rpc.md b/Dev_Docs/Pending_Features/2026-06-21_1735_esp32_wallet_selector_and_extension_rpc.md index f5e30a2..416f383 100644 --- a/Dev_Docs/Pending_Features/2026-06-21_1735_esp32_wallet_selector_and_extension_rpc.md +++ b/Dev_Docs/Pending_Features/2026-06-21_1735_esp32_wallet_selector_and_extension_rpc.md @@ -2,20 +2,20 @@ - краткое описание: - в ESP32 заменён домашний блок `баланс + QR` на единый вход в экран кошелька; - - добавлен выбор активного кошелька `DeviceKey / RootKey / Custom`; + - добавлен выбор активного кошелька `ClientKey / RootKey / Custom`; - для browser extension добавлен первый RPC `get_wallet_public_key` через существующий `wallet-session` и `CallSignalToSession`; - в popup расширения добавлен запрос текущего кошелька и копирование `publicKeyBase58`. - что проверять: - на ESP32 после ввода секрета на главном экране видна кнопка `Кошелёк: ...`; - экран `WALLET` открывается и показывает текущий тип кошелька; - - экран `WALLET_SELECT` переключает `DeviceKey`, `RootKey` и `Custom`; + - экран `WALLET_SELECT` переключает `ClientKey`, `RootKey` и `Custom`; - для `Custom` открывается ввод имени и после сохранения derivation работает; - `Показать баланс кошелька` читает баланс именно активного кошелька; - `Показать QR-код кошелька` показывает QR и адрес именно активного кошелька; - browser extension после подключения wallet-session может запросить текущий кошелёк у ESP32; - extension показывает тип кошелька, полный `publicKeyBase58`, результат проверки через PDA и копирует ключ в буфер; - - для `dev.key` и `root.key` проверка через PDA даёт ожидаемое совпадение. + - для `client.key` и `root.key` проверка через PDA даёт ожидаемое совпадение. - ожидаемый результат: - активный кошелёк на ESP32 реально влияет на баланс, QR и ответ `get_wallet_public_key`; diff --git a/Dev_Docs/Pending_Features/2026-06-21_2045_esp32_english_ui_and_trusted_login_fix.md b/Dev_Docs/Pending_Features/2026-06-21_2045_esp32_english_ui_and_trusted_login_fix.md index 023531e..2a922b1 100644 --- a/Dev_Docs/Pending_Features/2026-06-21_2045_esp32_english_ui_and_trusted_login_fix.md +++ b/Dev_Docs/Pending_Features/2026-06-21_2045_esp32_english_ui_and_trusted_login_fix.md @@ -5,7 +5,7 @@ - Что проверять: 1. На ESP32 открыть `HOME`, `WALLET`, `SELECT WALLET`, `WALLET QR` и убедиться, что пользовательские строки на экране только на английском. - 2. Проверить сценарий выбора `DeviceKey`, `RootKey`, `Custom` и чтение баланса/QR после переключения. + 2. Проверить сценарий выбора `ClientKey`, `RootKey`, `Custom` и чтение баланса/QR после переключения. 3. В расширении открыть popup, запустить `Подключить` для логина, у которого ещё не настраивался trusted-device login. 4. Убедиться, что `StartTrustedDeviceLogin` больше не падает с `NullPointerException`. 5. После создания pairing-запроса проверить, что homeserver/UI может его увидеть и обработать как раньше. diff --git a/Dev_Docs/Pending_Features/2026-06-21_2235_wallet_provider_and_esp_sign_transaction.md b/Dev_Docs/Pending_Features/2026-06-21_2235_wallet_provider_and_esp_sign_transaction.md index 55771e1..1573d9f 100644 --- a/Dev_Docs/Pending_Features/2026-06-21_2235_wallet_provider_and_esp_sign_transaction.md +++ b/Dev_Docs/Pending_Features/2026-06-21_2235_wallet_provider_and_esp_sign_transaction.md @@ -12,7 +12,7 @@ 6. Проверить оба варианта: - `APPROVE` возвращает сайту подписанную транзакцию; - `REJECT` возвращает отказ. - 7. Проверить сценарии для `DeviceKey`, `RootKey`, `Custom`. + 7. Проверить сценарии для `ClientKey`, `RootKey`, `Custom`. - Ожидаемый результат: - сайт может подключить кошелёк через provider расширения; diff --git a/Dev_Docs/Pending_Features/2026-06-23_1245_esp32_homeserver_pda_update_fix.md b/Dev_Docs/Pending_Features/2026-06-23_1245_esp32_homeserver_pda_update_fix.md new file mode 100644 index 0000000..6da227a --- /dev/null +++ b/Dev_Docs/Pending_Features/2026-06-23_1245_esp32_homeserver_pda_update_fix.md @@ -0,0 +1,26 @@ +## Кратко + +Исправлена ESP32-ветка обновления `user_pda` для добавления `homeserver`-сессии после миграции формата PDA на `RecoveryKeyBlock`. + +## Что сделано + +- В `shine_homeserver_main.ino` синхронизирован `create/update` payload с новым форматом `shine_users`. +- В сериализацию и парсинг PDA добавлен `RecoveryKeyBlock`. +- Для ветки `Add Homeserver` добавлены промежуточные checkpoint-записи в NVS, чтобы после crash или reset было видно, на каком шаге оборвалась операция. +- В `ESP32/AGENTS.md` добавлена памятка по чтению `last_error`. + +## Что проверять + +- Зарегистрировать или использовать уже существующий аккаунт на ESP32. +- Дойти до состояния `homeserver not in PDA`. +- Нажать `Add Homeserver`. +- Если операция не успешна, считать `last_error` по USB serial и убедиться, что видна свежая запись именно по шагам `Homeserver PDA update ...`, а не старый diag. + +## Ожидаемый результат + +- `Add Homeserver` добавляет `homeserver1` в `sessions` блока `SessionsBlock`. +- Если операция падает, в NVS сохраняется свежая диагностическая запись с текущим этапом, а не устаревший лог регистрации. + +## Статус + +`pending` diff --git a/Dev_Docs/Pending_Features/2026-06-24_1825_unified_channels_list_without_stories.md b/Dev_Docs/Pending_Features/2026-06-24_1825_unified_channels_list_without_stories.md new file mode 100644 index 0000000..7e4eac9 --- /dev/null +++ b/Dev_Docs/Pending_Features/2026-06-24_1825_unified_channels_list_without_stories.md @@ -0,0 +1,28 @@ +# Общий список каналов без stories + +- Краткое описание: + вкладка `Каналы` переведена на единый список без разделения на "мои" и "подписки". + Название канала в списке теперь показывается как `login_владельца/название_канала`. + Служебный канал `stories` скрыт из списка каналов, поиска, подписки и связанных UI-сценариев. + +- Что проверять: + 1. Открыть вкладку `Каналы`. + 2. Убедиться, что сразу показывается один общий список. + 3. Проверить, что свои и чужие каналы отображаются вместе. + 4. Проверить формат названий: `ownerLogin/channelName`. + 5. Открыть свой канал и убедиться, что внутри сохраняется UI владельца. + 6. Открыть чужой канал и убедиться, что внутри сохраняется UI подписчика. + 7. Проверить, что `stories` не отображается: + - в общем списке; + - в поиске каналов; + - в подписке на канал; + - в списках выбора канала для репоста. + +- Ожидаемый результат: + - вкладка `Каналы` больше не делится на два режима; + - все видимые каналы идут единым списком; + - `stories` нигде не виден и не предлагается пользователю; + - переход в канал сохраняет корректный UI в зависимости от владельца. + +- Статус: + `pending` diff --git a/Dev_Docs/Solana/user_pda/README.md b/Dev_Docs/Solana/user_pda/README.md index 1039e38..6039f04 100644 --- a/Dev_Docs/Solana/user_pda/README.md +++ b/Dev_Docs/Solana/user_pda/README.md @@ -12,8 +12,9 @@ - логин пользователя; - неизменяемые параметры создания записи; +- публичный recovery-ключ пользователя; - корневой публичный ключ пользователя; -- ключ устройства; +- клиентский публичный ключ пользователя; - данные одного или нескольких пользовательских блокчейнов SHiNE; - серверные данные пользователя, если пользователь выступает сервером; - серверы доступа пользователя; @@ -34,9 +35,9 @@ ## 2.1. Кто оплачивает create/update PDA -- Инструкции `create_user_pda` и `update_user_pda` оплачиваются с `device_key`. +- Инструкции `create_user_pda` и `update_user_pda` оплачиваются с `client_key`. - `root_key` используется для подписи unsigned части записи через Ed25519 instruction и не является fee payer. -- Для server PDA это правило то же самое: пополнять SOL нужно на адрес `device_key`. +- Для server PDA это правило то же самое: пополнять SOL нужно на адрес `client_key`. ## 3. Общие правила кодирования @@ -85,8 +86,9 @@ UserPdaRecordV1 | block_type | Блок | Назначение | |------------|------|------------| +| `0` | `RecoveryKeyBlock` | Ключ восстановления пользователя. | | `1` | `RootKeyBlock` | Корневой ключ пользователя. | -| `2` | `DeviceKeyBlock` | Ключ устройства пользователя. | +| `2` | `ClientKeyBlock` | Клиентский ключ пользователя. | | `3` | `BlockchainRegistryBlock` | Один или несколько блокчейнов пользователя. | | `30` | `ServerProfileBlock` | Серверные данные пользователя. | | `40` | `AccessServersBlock` | Серверы доступа/relay. | @@ -97,13 +99,31 @@ UserPdaRecordV1 Правила: - неизвестный `block_type` в `format_major = 1` считается ошибкой; -- обязательные блоки: `RootKeyBlock`, `DeviceKeyBlock`, `BlockchainRegistryBlock`; +- обязательные блоки: `RecoveryKeyBlock`, `RootKeyBlock`, `ClientKeyBlock`, `BlockchainRegistryBlock`; - необязательные блоки: `ServerProfileBlock`, `AccessServersBlock`, `SessionsBlock`, `TrustedStateBlock`; - каждый обязательный блок должен встречаться ровно один раз; - порядок блоков в записи фиксируется для простоты проверки: - `RootKey`, `DeviceKey`, `BlockchainRegistry`, `ServerProfile`, `AccessServers`, `Sessions`, `TrustedState`. + `RecoveryKey`, `RootKey`, `ClientKey`, `BlockchainRegistry`, `ServerProfile`, `AccessServers`, `Sessions`, `TrustedState`. -## 6. RootKeyBlock +## 6. RecoveryKeyBlock + +Recovery-ключ нужен для будущих сценариев восстановления и ротации остальных ключей. В текущей версии он только публикуется в записи и не меняется через обычный `update_user_pda`. + +```text +RecoveryKeyBlock +- block_type: u8 = 0 +- block_version: u8 = 0 +- recovery_key: [u8; 32] +``` + +Правила: + +- при создании задается публичный recovery-ключ пользователя; +- при обновлении `recovery_key` должен совпадать с предыдущей записью; +- приватный `recovery.key` в PDA не хранится; +- отдельная ротация recovery-ключа будет отдельным форматом/сценарием в будущем. + +## 7. RootKeyBlock Смена `root_key` пока не проектируется и не реализуется. Блок фиксирует только стадию `0`. @@ -120,24 +140,24 @@ RootKeyBlock - при обновлении `root_key` должен совпадать с предыдущей записью; - ротация root-key будет отдельным форматом/сценарием в будущем. -## 7. DeviceKeyBlock +## 8. ClientKeyBlock -Смена `device_key` пока также не проектируется как отдельная ротация. В версии `0` хранится один ключ устройства. +Смена `client_key` пока также не проектируется как отдельная ротация. В версии `0` хранится один клиентский ключ пользователя. ```text -DeviceKeyBlock +ClientKeyBlock - block_type: u8 = 2 - block_version: u8 = 0 -- device_key: [u8; 32] +- client_key: [u8; 32] ``` Правила: -- при создании задается текущий публичный ключ устройства; -- при обновлении ключ устройства может быть обновлен только если это отдельно разрешено бизнес-логикой инструкции; -- история устройств и несколько устройств в этом формате не хранятся. +- при создании задается текущий клиентский публичный ключ пользователя; +- при обновлении `client_key` должен совпадать с предыдущей записью; +- история устройств и несколько клиентских ключей в этом формате не хранятся. -## 8. BlockchainRegistryBlock +## 9. BlockchainRegistryBlock Блок хранит данные пользовательских блокчейнов SHiNE. Сейчас используется один блокчейн, но структура сразу сделана как список. @@ -155,7 +175,7 @@ BlockchainRegistryBlock - в будущем можно увеличить количество записей без изменения смысла `BlockchainRecord`; - каждый `BlockchainRecord` описывает один пользовательский SHiNE-блокчейн. -## 9. BlockchainRecord +## 10. BlockchainRecord ```text BlockchainRecord @@ -191,7 +211,7 @@ BlockchainRecord Arweave `tx_id` - обычное поле внутри записи конкретного блокчейна. Solana-программа не проверяет, что такой Arweave transaction действительно существует и содержит корректные данные; это ответственность клиента/сервера/пользователя. -## 10. Правила обновления BlockchainRecord +## 11. Правила обновления BlockchainRecord При обновлении записи: @@ -229,7 +249,7 @@ last_block_signature = Ed25519(blockchain_public_key, message) Причина проверки подписи `LastBlockState`: `root_key` управляет Solana-записью пользователя, а `blockchain_public_key` подтверждает состояние конкретного пользовательского блокчейна. Подписывается не голый хэш, а связка логина, имени блокчейна, номера последнего блока, хэша последнего блока и занятого размера. -## 11. ServerProfileBlock +## 12. ServerProfileBlock Блок присутствует, если пользователь выступает сервером. @@ -255,7 +275,7 @@ ServerProfileBlock - `server_address` - строковый адрес сервера в соответствии с `address_format_type`; - `sync_servers` - логины SHiNE-пользователей, зарегистрированных как серверы, с которыми этот сервер синхронизирует блокчейн и личные сообщения. Solana-программа не обязана проверять, что эти логины действительно зарегистрированы как серверы. -## 12. AccessServersBlock +## 13. AccessServersBlock Блок хранит серверы доступа/relay для пользователя. @@ -274,7 +294,7 @@ AccessServersBlock - `access_servers` - логины пользователей системы, используемых как серверы доступа/relay. Solana-программа не обязана проверять, что эти логины действительно зарегистрированы как серверы; - точная семантика выбора сервера доступа определяется клиентской/серверной логикой SHiNE. -## 13. SessionsBlock +## 14. SessionsBlock Блок хранит опубликованные пользовательские сессии. На текущем этапе регистрация пользователя не добавляет туда записи автоматически, поэтому стандартный create/update продолжает работать с пустым списком. @@ -309,6 +329,7 @@ SessionRecord | Значение | Смысл | |----------|-------| | `1` | Обычная пользовательская сессия. | +| `50` | Кошелёк пользователя. | | `100` | Homeserver пользователя. | Правила: @@ -320,7 +341,7 @@ SessionRecord - внутри одного блока должны быть уникальны и `session_name`, и `session_pub_key`; - на текущем этапе UI и регистрация не обязаны добавлять туда записи автоматически. -## 14. TrustedStateBlock +## 15. TrustedStateBlock Пока trusted-логика не реализована полностью, поэтому блок хранит только счетчик. @@ -333,7 +354,7 @@ TrustedStateBlock Пока блок с доверенными лицами не реализуется, потому что полный формат trusted-логики еще не составлен. В будущем trusted-связи, очереди, таймеры и подтверждения должны быть вынесены в отдельный формат. -## 15. Подпись user_pda +## 16. Подпись user_pda Подписывается не вся PDA целиком, а unsigned-часть записи: @@ -354,7 +375,7 @@ Solana-программа проверяет подпись через встр Смену формата подписи сейчас не трогаем. -## 16. Регистрация пользователя +## 17. Регистрация пользователя При регистрации: @@ -372,12 +393,12 @@ Solana-программа проверяет подпись через встр - если покупается дополнительный лимит, пользователь платит комиссию за этот лимит; - вся unsigned-часть записи подписана `root_key`. -## 17. Обновление пользователя +## 18. Обновление пользователя При обновлении: - PDA должна существовать; -- `login`, `created_at_ms`, `root_key` не меняются; +- `login`, `created_at_ms`, `recovery_key`, `root_key`, `client_key` не меняются; - `record_number = previous_record_number + 1`; - `prev_record_hash` равен хэшу unsigned-части предыдущей записи; - `updated_at_ms` обновляется; @@ -387,7 +408,7 @@ Solana-программа проверяет подпись через встр - при увеличении оплаченного лимита пользователь доплачивает комиссию; - Arweave `tx_id` может быть пустым или обновленным, но его содержимое Solana не валидирует. -## 18. Отличия от старого линейного формата +## 19. Отличия от старого линейного формата Старый формат после `login` хранил поля линейно: @@ -395,8 +416,8 @@ Solana-программа проверяет подпись через встр - `root_key`; - `blockchain_key_status`; - `blockchain_key`; -- `device_key_status`; -- `device_key`; +- `client_key_status`; +- `client_key`; - `chain_number`; - `balance`; - серверные поля; @@ -407,17 +428,54 @@ Solana-программа проверяет подпись через встр Новый целевой формат сохраняет первые 9 фиксированных полей как заголовок, но дальше переходит на типизированные блоки: +- recovery-ключ становится отдельным обязательным блоком; - ключи становятся отдельными блоками; - данные блокчейна становятся расширенным блоком со своим публичным ключом, лимитом, занятым размером, вершиной цепочки и Arweave `tx_id`; - серверные данные и access-серверы отделяются от данных блокчейна; - расширение формата делается добавлением новых версий блоков или новых `block_type`, а не вставкой полей в середину линейной записи. -## 18. Что пока не входит в формат +## 20. Деривация ключей из master secret + +Сама Solana-программа не вычисляет ключи из секрета и не хранит приватные ключи. Но текущая согласованная клиентская схема деривации для публичной версии формата фиксируется здесь как reference для UI/ESP32/внешних клиентов. + +Базовая формула: + +```text +seed = SHA-256("SHiNE-key" || 0x00 || master_secret32 || 0x00 || suffix_utf8) +``` + +Где: + +- `master_secret32` — 32-байтовый master secret пользователя; +- `suffix_utf8` — строка назначения ключа. + +Согласованные suffix: + +```text +"recovery.key" +"root.key" +"blockchain.key" +"client.key" +``` + +Соответствие: + +```text +recovery.seed = SHA-256("SHiNE-key" || 0x00 || master_secret32 || 0x00 || "recovery.key") +root.seed = SHA-256("SHiNE-key" || 0x00 || master_secret32 || 0x00 || "root.key") +blockchain.seed = SHA-256("SHiNE-key" || 0x00 || master_secret32 || 0x00 || "blockchain.key") +client.seed = SHA-256("SHiNE-key" || 0x00 || master_secret32 || 0x00 || "client.key") +``` + +Далее каждая строка `seed` интерпретируется off-chain как `seed32` для отдельной пары Ed25519. + +## 21. Что пока не входит в формат Пока не проектируем: +- ротацию `recovery_key`; - ротацию `root_key`; -- сложную ротацию `device_key`; +- сложную ротацию `client_key`; - ротацию `blockchain_public_key`; - проверку содержимого Arweave transaction; - хранение полной истории пользовательского блокчейна внутри Solana; diff --git a/Dev_Docs/Solana_Architecture/README.md b/Dev_Docs/Solana_Architecture/README.md index 34b0257..e28e129 100644 --- a/Dev_Docs/Solana_Architecture/README.md +++ b/Dev_Docs/Solana_Architecture/README.md @@ -47,7 +47,7 @@ DAO в текущем виде не является отдельной Anchor- | Программа | Program ID | | --- | --- | | `shine_login_guard` | `3xkopA7cXagxzMFrKdv3NCBfV6BKiRJCk69kr27M2sRo` | -| `shine_users` | `FZS1YctoeEhCkZ5VTjsysUFAXR8CqxYztcLboXcg2Rpm` | +| `shine_users` | `3bYrnXwLc56oVPUBAjY8zTMLwHCYq29b5rUMu3b64SQJ` | | `shine_payments` | `c4yTa4JT9EtQDCBX9LmWFK6T2gp4JGsuymFbom2EudW` | Если эти адреса меняются, нужно синхронно обновить: diff --git a/Dev_Docs/Solana_Architecture/details/accounts_and_money_flow.md b/Dev_Docs/Solana_Architecture/details/accounts_and_money_flow.md index 0aafaeb..6fbe6c6 100644 --- a/Dev_Docs/Solana_Architecture/details/accounts_and_money_flow.md +++ b/Dev_Docs/Solana_Architecture/details/accounts_and_money_flow.md @@ -19,7 +19,7 @@ 5. `DAO_TREASURY_WALLET` / `dao_wallet` — казна DAO. 6. `manager_wallet` — кошелек менеджера, которому DAO выдает лимиты на создание тикетов. 7. `user root_key` — корневой ключ пользователя для подписи пользовательской записи. -8. `user device_key` — ключ устройства пользователя. +8. `user client_key` — ключ устройства пользователя. 9. `server_key` — ключ сервера пользователя, если пользователь является сервером. Текущие адреса из `programs/common/src/deploy_config.rs`: @@ -27,7 +27,7 @@ | Роль | Адрес | | --- | --- | | `SHINE_LOGIN_GUARD_PROGRAM_ID` | `3xkopA7cXagxzMFrKdv3NCBfV6BKiRJCk69kr27M2sRo` | -| `SHINE_USERS_PROGRAM_ID` | `FZS1YctoeEhCkZ5VTjsysUFAXR8CqxYztcLboXcg2Rpm` | +| `SHINE_USERS_PROGRAM_ID` | `3bYrnXwLc56oVPUBAjY8zTMLwHCYq29b5rUMu3b64SQJ` | | `SHINE_PAYMENTS_PROGRAM_ID` | `c4yTa4JT9EtQDCBX9LmWFK6T2gp4JGsuymFbom2EudW` | | `DAO_AUTHORITY` | `FUc28vNixp7F3nnkpGVt6nuJbgvJ4429v4B5wS52Df6P` | | `DAO_TREASURY_WALLET` | `FUc28vNixp7F3nnkpGVt6nuJbgvJ4429v4B5wS52Df6P` | diff --git a/Dev_Docs/Solana_Architecture/details/shine_users.md b/Dev_Docs/Solana_Architecture/details/shine_users.md index d8e7004..e268bc2 100644 --- a/Dev_Docs/Solana_Architecture/details/shine_users.md +++ b/Dev_Docs/Solana_Architecture/details/shine_users.md @@ -62,7 +62,7 @@ `UserMutableFields`: -- `device_key: Pubkey` +- `client_key: Pubkey` - `blockchain_public_key: Pubkey` - `blockchain_name: String` - `used_bytes: u64` diff --git a/Dev_Docs/Solana_Architecture/schemes/architecture.svg b/Dev_Docs/Solana_Architecture/schemes/architecture.svg index a731eb3..9ef76d3 100644 --- a/Dev_Docs/Solana_Architecture/schemes/architecture.svg +++ b/Dev_Docs/Solana_Architecture/schemes/architecture.svg @@ -32,7 +32,7 @@ Пользователь - signer, root_key, device_key + signer, root_key, client_key Покупатель тикета diff --git a/Dev_Docs/audit/Solana-audit-3-by-Claude-12июня2026.md b/Dev_Docs/audit/Solana-audit-3-by-Claude-12июня2026.md index e5a93f6..e456584 100644 --- a/Dev_Docs/audit/Solana-audit-3-by-Claude-12июня2026.md +++ b/Dev_Docs/audit/Solana-audit-3-by-Claude-12июня2026.md @@ -43,8 +43,8 @@ bump, подмена аккаунта оракула) не найдено. Вс |---|---|---|---|---|---|---| | `init_users_economy_config` | ✓ | `owner == system` + `data_is_empty` (анти-reinit) | деривация + сверка | ✓ | — | значения из `settings`, не из ввода | | `update_users_economy_config` | ✓ + `signer == DAO_AUTHORITY` | `owner == program_id` | деривация + сверка | — | — | `lamports_per_limit_step > 0` | -| `create_user_pda` | ✓ + `signer == device_key` | user_pda `owner == system` + empty; econ_config `owner == program_id` | user_pda, econ_config, inflow_vault, login_guard — все сверены | ✓ | ed25519 (record sig idx −2, last_block idx −1) | `inflow_vault` сверен с PDA `shine_payments`; login_guard сверен дважды | -| `update_user_pda` | ✓ + `signer == device_key` | user_pda `owner == program_id`; econ_config `owner == program_id` | деривация + сверка | ✓ | ed25519 + `version == old+1` + `prev_hash == hash(old)` | immutable-поля сверены с прежней записью | +| `create_user_pda` | ✓ + `signer == client_key` | user_pda `owner == system` + empty; econ_config `owner == program_id` | user_pda, econ_config, inflow_vault, login_guard — все сверены | ✓ | ed25519 (record sig idx −2, last_block idx −1) | `inflow_vault` сверен с PDA `shine_payments`; login_guard сверен дважды | +| `update_user_pda` | ✓ + `signer == client_key` | user_pda `owner == program_id`; econ_config `owner == program_id` | деривация + сверка | ✓ | ed25519 + `version == old+1` + `prev_hash == hash(old)` | immutable-поля сверены с прежней записью | ### shine_payments @@ -111,7 +111,7 @@ commit-reveal; для текущей модели — приемлемый ри - **Подмена PDA** невозможна нигде: всюду пара «деривация `find_program_address` + сверка полного адреса». Пользовательский bump не принимается, `create_program_address` с внешним bump не используется — bump-seed атаки исключены. - **Проверка владельца** при каждом чтении PDA: `read_state` и `validate_singleton_state_pda` (`shine_payments`) требуют `owner == id()`; `validate_users_economy_config_pda` и проверка `user_pda.owner == program_id` (`shine_users`) — перед десериализацией данных. - **Создаваемые PDA**: проверка `is_uninitialized` / `owner == system && data_is_empty` исключает повторную инициализацию и перезапись чужого аккаунта. -- **signer / authority**: все handler начинают с обязательного `is_signer`; привилегированные операции дополнительно сверяют ключ с авторитетом (`config.dao_wallet`, `DAO_AUTHORITY`, `allowance.manager_wallet`, `ticket.recipient_wallet`, `device_key`). +- **signer / authority**: все handler начинают с обязательного `is_signer`; привилегированные операции дополнительно сверяют ключ с авторитетом (`config.dao_wallet`, `DAO_AUTHORITY`, `allowance.manager_wallet`, `ticket.recipient_wallet`, `client_key`). - **system-program** сверяется с `system_program::ID` там, где идёт создание аккаунта/перевод; **sysvar инструкций** сверяется с `sysvar::instructions::id()` перед ed25519-интроспекцией. - **Аккаунт оракула**: пин адреса `PYTH_SOL_USD_ACCOUNT` + `owner == pyth_receiver` + `feed_id` + возраст (120 с) + доверительный интервал (10%). - **Ed25519 в `shine_users`**: относительные индексы −1/−2, `num_signatures == 1`, все три `ix_index == u16::MAX` (offset-данные внутри самой ed25519-инструкции), сверка `program_id == ed25519_program` и pubkey/signature/message по хэшу — указать на чужую инструкцию нельзя. diff --git a/Dev_Docs/Инициализация_Solana_регистрации/README.md b/Dev_Docs/Инициализация_Solana_регистрации/README.md index 98b91d1..b9457b2 100644 --- a/Dev_Docs/Инициализация_Solana_регистрации/README.md +++ b/Dev_Docs/Инициализация_Solana_регистрации/README.md @@ -12,7 +12,7 @@ ## Актуальные адреса (devnet) - `shine_users` (регистрация пользователей): - `FZS1YctoeEhCkZ5VTjsysUFAXR8CqxYztcLboXcg2Rpm` + `3bYrnXwLc56oVPUBAjY8zTMLwHCYq29b5rUMu3b64SQJ` - `shine_login_guard`: `3xkopA7cXagxzMFrKdv3NCBfV6BKiRJCk69kr27M2sRo` - `shine_payments`: @@ -22,7 +22,7 @@ - Сеть: `https://api.devnet.solana.com` - `shine_users`: - - `Program ID`: `FZS1YctoeEhCkZ5VTjsysUFAXR8CqxYztcLboXcg2Rpm` + - `Program ID`: `3bYrnXwLc56oVPUBAjY8zTMLwHCYq29b5rUMu3b64SQJ` - TX deploy: `5VzfpSirFCRqPUZfvAt3eADY9KnowW79PKZ1pCQAa2DJGiztj4dUYYXrSQNmWEhPVu6mPSDfcuHzFyEVmoKLa9DM` - `shine_login_guard`: - `Program ID`: `3xkopA7cXagxzMFrKdv3NCBfV6BKiRJCk69kr27M2sRo` @@ -64,11 +64,38 @@ anchor deploy -p shine_users - Переход на страницу: - `shine-UI/js/pages/developer-settings-view.js` +### Browser plugin wallet + +- Резолвер PDA и проверка адресов: + - `SHiNE-browser-plugin-wallet/js/lib/shine-server-resolver.js` +- Проверка текущего wallet по PDA: + - `SHiNE-browser-plugin-wallet/background.js` +- Отображение состояния в popup: + - `SHiNE-browser-plugin-wallet/popup.js` + ### Сервер - Серверные константы Solana: - `shine-server-config/src/main/java/utils/config/SolanaProgramsConfig.java` +### Solana / Anchor + +- `shine-solana/shine/Anchor.toml` +- `shine-solana/shine/programs/shine_users/src/lib.rs` (`declare_id!`) +- `shine-solana/shine/programs/shine_login_guard/src/lib.rs` (`declare_id!`) +- `shine-solana/shine/programs/shine_payments/src/lib.rs` (`declare_id!`) + +### Где ещё нужно синхронизировать адреса после нового deploy + +- UI-константы и все потребители в `shine-UI/js/*` +- browser-plugin-wallet +- серверный `SolanaProgramsConfig.java` +- Anchor-конфиг и `declare_id!` в Solana-модуле +- документы: + - `Dev_Docs/Solana/user_pda/README.md` + - `shine-solana/shine/doc/programs/shine_users.md` + - `shine-solana/shine/doc/devnet_keys_and_deploy.md` + ## Как запустить инициализацию economy PDA 1. Открыть UI. @@ -80,19 +107,19 @@ anchor deploy -p shine_users Страница сама вычисляет PDA `users_economy_config` по seed: - seed: `shine_users_economy_config` -- program: `FZS1YctoeEhCkZ5VTjsysUFAXR8CqxYztcLboXcg2Rpm` +- program: `3bYrnXwLc56oVPUBAjY8zTMLwHCYq29b5rUMu3b64SQJ` ## Кто оплачивает create/update user_pda -- И обычная регистрация `create_user_pda`, и последующее `update_user_pda` оплачиваются с `deviceKey`. +- И обычная регистрация `create_user_pda`, и последующее `update_user_pda` оплачиваются с `clientKey`. - В UI это означает, что Solana fee payer всегда берётся из `device`-ключа пользователя/сервера. - `rootKey` нужен для подписи unsigned PDA-записи, но не оплачивает транзакцию. -- Для server UI это особенно важно: перед `create` и `update` нужно пополнять именно Solana-адрес `deviceKey`. +- Для server UI это особенно важно: перед `create` и `update` нужно пополнять именно Solana-адрес `clientKey`. ## Важно - `init_users_economy_config` выполняется один раз на программу. Если PDA уже создан, повторный вызов вернёт ошибку "already initialized" (это нормальное поведение). -- Серверные приватные ключи для Solana не используются как отдельный backend-wallet: в UI/server UI транзакцию оплачивает именно `deviceKey`, а содержимое записи подписывает `rootKey`. +- Серверные приватные ключи для Solana не используются как отдельный backend-wallet: в UI/server UI транзакцию оплачивает именно `clientKey`, а содержимое записи подписывает `rootKey`. - `shine_users` внутри `create_user_pda` требует корректный адрес `shine_login_guard` для CPI-классификации логина. Несовпадение адреса приведёт к ошибке регистрации. diff --git a/Dev_Docs/Протоколы/ESP_Pairing_и_режимы_подключения.md b/Dev_Docs/Протоколы/ESP_Pairing_и_режимы_подключения.md index 2022536..5333464 100644 --- a/Dev_Docs/Протоколы/ESP_Pairing_и_режимы_подключения.md +++ b/Dev_Docs/Протоколы/ESP_Pairing_и_режимы_подключения.md @@ -4,7 +4,7 @@ ## 1. Текущие режимы -### 1. Создание новой сессии через `deviceKey` +### 1. Создание новой сессии через `clientKey` Поток: @@ -12,8 +12,8 @@ Смысл: -- новое устройство уже владеет приватным `deviceKey`; -- сервер проверяет подпись `deviceKey`; +- новое устройство уже владеет приватным `clientKey`; +- сервер проверяет подпись `clientKey`; - создаётся обычная активная сессия пользователя; - этот поток остаётся без изменений. @@ -67,7 +67,7 @@ ## 4. Чего сервер в этой версии не делает -- не передаёт приватный `deviceKey`; +- не передаёт приватный `clientKey`; - не расшифровывает `encryptedPayload`; - не проверяет криптографию содержимого payload; - не делает клиентский UI; diff --git a/Dev_Docs/Протоколы/SHINE_ARWEAVE_DERIVATION_V1.md b/Dev_Docs/Протоколы/SHINE_ARWEAVE_DERIVATION_V1.md index 81d50f5..25e48b4 100644 --- a/Dev_Docs/Протоколы/SHINE_ARWEAVE_DERIVATION_V1.md +++ b/Dev_Docs/Протоколы/SHINE_ARWEAVE_DERIVATION_V1.md @@ -3,11 +3,11 @@ Сокращение: **SAWD-v1**. ## Назначение -Из 32-байтного `deviceKey32` пользователя получить один и тот же нативный Arweave RSA-4096 JWK wallet и один и тот же Arweave address. +Из 32-байтного `clientKey32` пользователя получить один и тот же нативный Arweave RSA-4096 JWK wallet и один и тот же Arweave address. ## Вход -- `deviceKey32`: ровно 32 байта. -- Если исходный `device.key` хранится как Ed25519 PKCS8 base64, нужно извлечь последние 32 байта из PKCS8. +- `clientKey32`: ровно 32 байта. +- Если исходный `client.key` хранится как Ed25519 PKCS8 base64, нужно извлечь последние 32 байта из PKCS8. - Если используется Solana keypair JSON на 64 байта, используются только `bytes[0..31]`. ## Выход @@ -46,8 +46,8 @@ - `SMALL_PRIME_LIMIT = 10000` ## Алгоритм -1. Проверить `deviceKey32.length == 32`. -2. `masterSeed32 = HMAC-SHA256(key = UTF8(MASTER_LABEL), message = deviceKey32)`. +1. Проверить `clientKey32.length == 32`. +2. `masterSeed32 = HMAC-SHA256(key = UTF8(MASTER_LABEL), message = clientKey32)`. 3. Реализовать `deriveBytes(label, length)`: - `output = empty` - `counter = 0` diff --git a/Dev_Docs/Протоколы/Формат_взаимодействия_внешнего_кошелька_и_ESP32.md b/Dev_Docs/Протоколы/Формат_взаимодействия_внешнего_кошелька_и_ESP32.md index 137ac30..1b0c431 100644 --- a/Dev_Docs/Протоколы/Формат_взаимодействия_внешнего_кошелька_и_ESP32.md +++ b/Dev_Docs/Протоколы/Формат_взаимодействия_внешнего_кошелька_и_ESP32.md @@ -16,7 +16,7 @@ На устройстве в UI пользователь выбирает текущий активный кошелёк: -- `dev.key` +- `client.key` - `root.key` - `custom` @@ -115,7 +115,7 @@ ESP32 возвращает: - `requestId` — должен совпадать с `requestId` исходного запроса. - `ok` — признак успешного результата. - `wallet.type` — тип активного кошелька: - - `dev.key` + - `client.key` - `root.key` - `custom` - `wallet.publicKeyBase58` — публичный ключ активного кошелька в `Base58`. @@ -156,11 +156,11 @@ ESP32 возвращает: Расширение уже знает публичные ключи пользователя из Solana PDA. Поэтому оно может дополнительно проверить ответ ESP32: -- если `wallet.type = dev.key`, то `publicKeyBase58` должен совпасть с `deviceKey`, прочитанным из PDA; +- если `wallet.type = client.key`, то `publicKeyBase58` должен совпасть с `clientKey`, прочитанным из PDA; - если `wallet.type = root.key`, то `publicKeyBase58` должен совпасть с `rootKey`, прочитанным из PDA; - если `wallet.type = custom`, такой проверки по PDA в первой версии нет. -При несовпадении для `dev.key` или `root.key` расширение должно показать пользователю предупреждение, что возвращённый ключ не совпал с ожидаемым ключом из PDA. +При несовпадении для `client.key` или `root.key` расширение должно показать пользователю предупреждение, что возвращённый ключ не совпал с ожидаемым ключом из PDA. ## 8. Ожидаемое поведение UI расширения diff --git a/ESP32/AGENTS.md b/ESP32/AGENTS.md index 5afda45..0af2112 100644 --- a/ESP32/AGENTS.md +++ b/ESP32/AGENTS.md @@ -9,3 +9,20 @@ ## Синхронизация со спецификацией - При изменении экранов, кнопок, переходов, статусов или текстов обязательно обновлять соответствующую спецификацию в `ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/reference/`. + +## Сборка ESP32 + +- Основной способ проверки и прошивки скетчей для `ESP32-S3-Touch-AMOLED-2.16` - `main-device/burn.sh`. +- Не собирать эти скетчи напрямую через `arduino-cli compile` без `burn.sh`, потому что скрипт добавляет нужные локальные библиотеки и конфиги из `official-demo/examples/Arduino-v3.3.5/libraries`. +- Если сборка падает по `lv_conf.h` или `TouchDrvCSTXXX.hpp`, сначала проверять именно `burn.sh` и его `--library` пути, а не считать, что файл пропал из репозитория. + +## Диагностика ESP32 + +- Последнюю сохранённую ошибку или диагностическую запись читать с устройства через USB serial monitor на `115200`. +- Базовая команда: + `arduino-cli monitor -p /dev/ttyACM0 --config baudrate=115200` +- После подключения отправлять одну из команд: + `last_error`, `last_diag` или `reg_diag` +- Для очистки сохранённой диагностики использовать: + `clear_error` или `clear_diag` +- При падениях в ветках регистрации и обновления PDA сначала читать именно `last_error`: запись хранится в NVS и может пережить перезагрузку устройства. diff --git a/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/main-device/argon2_sd_test/argon2_sd_test.ino b/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/main-device/argon2_sd_test/argon2_sd_test.ino index cdeab1f..50dd4e2 100644 --- a/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/main-device/argon2_sd_test/argon2_sd_test.ino +++ b/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/main-device/argon2_sd_test/argon2_sd_test.ino @@ -11,7 +11,7 @@ * legacy(empty password): * secret = SHA256(base64(SHA256(password)) + "master.secret") * keyPair_i = Ed25519(SHA256(base64(secret) + "|" + suffix_i)) - * suffixes = ["root.key", "bch.key", "dev.key"] + * suffixes = ["root.key", "blockchain.key", "client.key"] * * Плата: Waveshare ESP32-S3-Touch-AMOLED-2.16 * SD : SDMMC 1-bit CLK=GPIO2, CMD=GPIO1, D0=GPIO3 @@ -116,8 +116,8 @@ static bool gKbNums = false; // ═══════════════════════════════════════════════════════════ struct KeyPair { uint8_t pub[32]; uint8_t priv[32]; }; static KeyPair gKeys[3]; -static const char * KEY_SUFFIXES[3] = {"root.key", "bch.key", "dev.key"}; -static const char * KEY_LABELS[3] = {"root.key", "bch.key", "dev.key"}; +static const char * KEY_SUFFIXES[3] = {"root.key", "blockchain.key", "client.key"}; +static const char * KEY_LABELS[3] = {"root.key", "blockchain.key", "client.key"}; static uint32_t gElapsedSec = 0; // Base58 представления (43-44 символа для 32 байт + \0) diff --git a/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/main-device/shine_homeserver_main/shine_homeserver_main.ino b/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/main-device/shine_homeserver_main/shine_homeserver_main.ino index 2a204d1..0d7482a 100644 --- a/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/main-device/shine_homeserver_main/shine_homeserver_main.ino +++ b/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/main-device/shine_homeserver_main/shine_homeserver_main.ino @@ -12,6 +12,8 @@ #include #include #include +#include +#include #include #define XPOWERS_CHIP_AXP2101 #include "XPowersLib.h" @@ -68,7 +70,7 @@ int ge25519_frombytes(ge25519_p3 *h, const unsigned char *s); #define TEXT_EDIT_PANEL_H 330 #define TEST_VERSION "SHiNE homeserver (v.0.18)" -static const char *kShineUsersProgramId = "FZS1YctoeEhCkZ5VTjsysUFAXR8CqxYztcLboXcg2Rpm"; +static const char *kShineUsersProgramId = "3bYrnXwLc56oVPUBAjY8zTMLwHCYq29b5rUMu3b64SQJ"; static const char *kShineUsersUserPdaSeedPrefix = "user_login="; static const char *kShineLoginGuardProgramId = "3xkopA7cXagxzMFrKdv3NCBfV6BKiRJCk69kr27M2sRo"; static const char *kShinePaymentsProgramId = "c4yTa4JT9EtQDCBX9LmWFK6T2gp4JGsuymFbom2EudW"; @@ -80,8 +82,10 @@ static const char *kUsersEconomyConfigSeed = "shine_users_economy_config"; static const char *kPaymentsInflowSeed = "shine_payments_inflow_vault"; static const char *kLastBlockPrefix = "SHiNE_LAST_BLOCK"; static const char *kProgramDerivedAddressMarker = "ProgramDerivedAddress"; +static const char *kDefaultShineServerLogin = "shineupme"; +static const uint8_t kBlockTypeRecoveryKey = 0; static const uint8_t kBlockTypeRootKey = 1; -static const uint8_t kBlockTypeDeviceKey = 2; +static const uint8_t kBlockTypeClientKey = 2; static const uint8_t kBlockTypeBlockchainRegistry = 3; static const uint8_t kBlockTypeServerProfile = 30; static const uint8_t kBlockTypeAccessServers = 40; @@ -229,8 +233,9 @@ struct ShinePdaUserState { std::vector accessServers; uint8_t sessionsMode = 1; uint8_t trustedCount = 0; + uint8_t recoveryKey32[32] = {}; uint8_t rootKey32[32] = {}; - uint8_t deviceKey32[32] = {}; + uint8_t clientKey32[32] = {}; uint8_t blockchainKey32[32] = {}; String blockchainName; uint64_t paidLimitBytes = 0; @@ -320,8 +325,10 @@ static int gScanResultCount = 0; static WifiViewMode gWifiViewMode = WIFI_VIEW_OVERVIEW; static String gSolanaRpcUrl = "https://api.devnet.solana.com"; -static String gShineServerUrl = "https://shineup.me"; -static String gServerStatusMessage = "Edit RPC or shine host"; +static String gShineServerLogin = kDefaultShineServerLogin; +static String gShineServerUrl; +static String gResolvedShineServerLogin; +static String gServerStatusMessage = "Edit RPC or server login"; static String gLoginValue; static String gHomeserverValue = "homeserver1"; static bool gSecretConfigured = false; @@ -345,6 +352,9 @@ static unsigned long gLastAccountCheckMs = 0; static bool gShowRegisterAccountButton = false; static bool gShowHomeserverPdaActionButton = false; static String gHomeserverPdaActionReason; +static bool gCachedAccountPdaValid = false; +static String gCachedAccountPdaLogin; +static ShinePdaUserState gCachedAccountPdaState; static String gUserPdaAddress; static String gRegistrationSignature; static String gShineStatusLine = "SHiNE: account not configured"; @@ -392,6 +402,8 @@ struct DerivedKeyInfo { static String gRootPubB58; static String gRootPrivB58; +static String gRecoveryPubB58; +static String gRecoveryPrivB58; static String gBlockchainPubB58; static String gBlockchainPrivB58; static String gDevicePubB58; @@ -407,6 +419,9 @@ static const int kWalletRpcSignalTypeRequest = 9100; static const int kWalletRpcSignalTypeResponse = 9101; static ActiveWalletSignRequest gActiveWalletSignRequest; static String gWalletSignStatusMessage; +static lv_indev_drv_t *gSecretScrollIndevDriver = nullptr; +static uint8_t gSecretScrollThrowSaved = 0; +static bool gSecretScrollThrowApplied = false; static EditContext gEditContext = EDIT_CONTEXT_NONE; static Screen gEditReturnScreen = SCREEN_HOME; @@ -517,6 +532,11 @@ static void pushU64LE(std::vector &out, uint64_t value); static void pushStrU8(std::vector &out, const String &value); static void pushFixed(std::vector &out, const uint8_t *data, size_t len); static String bytesToBase58(const uint8_t *data, size_t len); +static bool getSystemEpochMs(uint64_t &epochMsOut); +static bool ensureNtpTimeSynced(String &errorOut); +static bool resolveShineServerUrlFromLogin(const String &serverLogin, String &serverUrlOut, String &errorOut); +static String currentShineServerLoginSource(); +static bool ensureCurrentShineServerUrl(String &errorOut); static String buildBaseRpcRequest(const char *method, const String ¶msJson); static bool rpcCallSolana(const char *method, const String ¶msJson, String &payloadOut); static bool rpcResponseHasError(const String &payload); @@ -540,9 +560,10 @@ static std::vector buildLastBlockStateBytes(const String &login, static std::vector buildUnsignedCreateRecord( const String &login, const String &blockchainName, - const String &serverAddress, + const std::vector &accessServers, + const uint8_t recoveryPub[32], const uint8_t rootPub[32], - const uint8_t devicePub[32], + const uint8_t clientPub[32], const uint8_t blockchainPub[32], const uint8_t lastBlockSignature[64], uint64_t paidLimitBytes, @@ -550,9 +571,10 @@ static std::vector buildUnsignedCreateRecord( static std::vector buildCreateInstructionData( const String &login, const String &blockchainName, - const String &serverAddress, + const std::vector &accessServers, + const uint8_t recoveryPub[32], const uint8_t rootPub[32], - const uint8_t devicePub[32], + const uint8_t clientPub[32], const uint8_t blockchainPub[32], const uint8_t lastBlockSignature[64], const uint8_t rootSignature[64], @@ -566,7 +588,7 @@ static std::vector buildUpdateInstructionData(const ShinePdaUserState & const uint8_t rootSignature64[64]); static std::vector buildLegacyMessage( const uint8_t recentBlockhash[32], - const uint8_t devicePub[32], + const uint8_t clientPub[32], const uint8_t userPda[32], const uint8_t inflowVault[32], const uint8_t economyConfig[32], @@ -575,7 +597,7 @@ static std::vector buildLegacyMessage( const std::vector &createData); static std::vector buildUpdateLegacyMessage( const uint8_t recentBlockhash[32], - const uint8_t devicePub[32], + const uint8_t clientPub[32], const uint8_t userPda[32], const uint8_t inflowVault[32], const uint8_t economyConfig[32], @@ -610,6 +632,7 @@ static void loadRegisterDiagDetailsFromPrefs(); static void saveRegisterDiagDetailsToPrefs(const String &details); static void clearRegisterDiagDetailsFromPrefs(); static void saveRegisterDiag(const String &status, const String &summary, const String &details); +static void saveRegisterDiagCheckpoint(const String &summary, const String &details); static void printRegisterDiagToSerial(); static void clearRegisterDiag(); static void handleUsbSerialCommands(); @@ -877,6 +900,19 @@ static String normalizeLoginValue(const String &value) { return out; } +static bool isValidShineServerLoginValue(const String &value) { + if (value.isEmpty() || value.length() > 20) { + return false; + } + for (size_t i = 0; i < value.length(); ++i) { + char ch = value.charAt(i); + if (!(isAlphaNumeric((unsigned char)ch) || ch == '_')) { + return false; + } + } + return true; +} + static String abbreviateValue(const String &value, size_t head = 8, size_t tail = 6) { if (value.length() <= head + tail + 3) { return value; @@ -1206,10 +1242,17 @@ static void deriveKeyPairFromSecretSuffix(const uint8_t *secret32, const String if (!secret32) { return; } - String material = base64Std(secret32, 32) + "|" + suffix; + const char *prefix = "SHiNE-key"; uint8_t seed[32] = {}; uint8_t pub[32] = {}; - sha256calc(reinterpret_cast(material.c_str()), material.length(), seed); + std::vector material; + material.reserve(10 + 1 + 32 + 1 + suffix.length()); + material.insert(material.end(), prefix, prefix + strlen(prefix)); + material.push_back(0); + material.insert(material.end(), secret32, secret32 + 32); + material.push_back(0); + material.insert(material.end(), suffix.c_str(), suffix.c_str() + suffix.length()); + sha256calc(material.data(), material.size(), seed); Ed25519::derivePublicKey(pub, seed); privB58 = base58From32(seed); pubB58 = base58From32(pub); @@ -1218,6 +1261,8 @@ static void deriveKeyPairFromSecretSuffix(const uint8_t *secret32, const String static void clearDerivedKeys() { gRootPubB58 = ""; gRootPrivB58 = ""; + gRecoveryPubB58 = ""; + gRecoveryPrivB58 = ""; gBlockchainPubB58 = ""; gBlockchainPrivB58 = ""; gDevicePubB58 = ""; @@ -1233,9 +1278,10 @@ static void refreshDerivedKeys() { if (!gSecretConfigured) { return; } + deriveKeyPairFromSecretSuffix(gSecretBytes, "recovery.key", gRecoveryPubB58, gRecoveryPrivB58); deriveKeyPairFromSecretSuffix(gSecretBytes, "root.key", gRootPubB58, gRootPrivB58); - deriveKeyPairFromSecretSuffix(gSecretBytes, "bch.key", gBlockchainPubB58, gBlockchainPrivB58); - deriveKeyPairFromSecretSuffix(gSecretBytes, "dev.key", gDevicePubB58, gDevicePrivB58); + deriveKeyPairFromSecretSuffix(gSecretBytes, "blockchain.key", gBlockchainPubB58, gBlockchainPrivB58); + deriveKeyPairFromSecretSuffix(gSecretBytes, "client.key", gDevicePubB58, gDevicePrivB58); deriveKeyPairFromSecretSuffix(gSecretBytes, homeserverKeySuffix(), gHomeserverPubB58, gHomeserverPrivB58); String customName = gCustomWalletName; customName.trim(); @@ -1255,7 +1301,7 @@ static String selectedWalletDisplayName() { } case WALLET_SELECTION_DEVICE: default: - return "DeviceKey"; + return "ClientKey"; } } @@ -1267,7 +1313,7 @@ static String selectedWalletTypeCode() { return "custom"; case WALLET_SELECTION_DEVICE: default: - return "dev.key"; + return "client.key"; } } @@ -1282,7 +1328,7 @@ static String selectedWalletDerivationSuffix() { } case WALLET_SELECTION_DEVICE: default: - return "dev.key"; + return "client.key"; } } @@ -1310,6 +1356,43 @@ static String selectedWalletPrivateKeyB58() { } } +static void secretScrollBoostSet(bool enable) { + if (enable) { + lv_indev_t *indev = lv_indev_get_act(); + if (!indev || !indev->driver) { + return; + } + if (!gSecretScrollThrowApplied) { + gSecretScrollIndevDriver = indev->driver; + gSecretScrollThrowSaved = gSecretScrollIndevDriver->scroll_throw; + gSecretScrollIndevDriver->scroll_throw = (uint8_t)(gSecretScrollThrowSaved / 4); + gSecretScrollThrowApplied = true; + } + } else if (gSecretScrollThrowApplied && gSecretScrollIndevDriver) { + gSecretScrollIndevDriver->scroll_throw = gSecretScrollThrowSaved; + gSecretScrollIndevDriver = nullptr; + gSecretScrollThrowSaved = 0; + gSecretScrollThrowApplied = false; + } +} + +static void secretScrollEventCb(lv_event_t *e) { + lv_event_code_t code = lv_event_get_code(e); + if (code == LV_EVENT_SCROLL_BEGIN) { + lv_anim_t *anim = lv_event_get_scroll_anim(e); + if (anim && anim->time > 0) { + uint32_t boostedTime = (uint32_t)anim->time / 4; + if (boostedTime < 80U) { + boostedTime = 80U; + } + lv_anim_set_time(anim, boostedTime); + } + secretScrollBoostSet(true); + } else if (code == LV_EVENT_SCROLL_END || code == LV_EVENT_DELETE) { + secretScrollBoostSet(false); + } +} + static bool selectedWalletAvailable() { return !selectedWalletPublicKeyB58().isEmpty(); } @@ -1374,6 +1457,10 @@ static bool shouldPauseShineReconnectForUi() { case SCREEN_SECRET_GENERATE_INFO: case SCREEN_SECRET_GENERATE_RUNNING: case SCREEN_SECRET_GENERATE_CANCEL_CONFIRM: + case SCREEN_REGISTER_ACCOUNT_CONFIRM: + case SCREEN_REGISTER_ACCOUNT_RESULT: + case SCREEN_HOMESERVER_PDA_CONFIRM: + case SCREEN_HOMESERVER_PDA_RESULT: case SCREEN_TEXT_EDIT: return true; default: @@ -1396,6 +1483,9 @@ static void markAccountStateDirty() { gHomeserverPdaResultMessage = ""; gHomeserverPdaResultDetails = ""; gHomeserverPdaResultSuccess = false; + gCachedAccountPdaValid = false; + gCachedAccountPdaLogin = ""; + gCachedAccountPdaState = ShinePdaUserState{}; gUserPdaAddress = ""; gRegistrationSignature = ""; gRegisterConfirmMessage = ""; @@ -1559,6 +1649,16 @@ static String balanceHomeLine() { return gBalanceStatusMessage; } +static String shineServerDisplayLabel() { + if (!gShineServerUrl.isEmpty()) { + return gShineServerUrl; + } + if (!currentShineServerLoginSource().isEmpty()) { + return currentShineServerLoginSource(); + } + return "not set"; +} + static String wifiHomeRichLine() { String ssid = gWifiSavedSsid.isEmpty() ? String("not configured") : gWifiSavedSsid; if (WiFi.status() == WL_CONNECTED) { @@ -1568,7 +1668,7 @@ static String wifiHomeRichLine() { } static String shineHomeRichLine() { - String serverLabel = gShineServerUrl.isEmpty() ? String("not set") : gShineServerUrl; + String serverLabel = shineServerDisplayLabel(); if (gShineStatusLine.endsWith(" connected")) { return String("SHiNE: ") + serverLabel + " #38B26D connected#"; } @@ -1648,6 +1748,96 @@ static uint64_t shineNowMs() { return value > 0 ? (uint64_t)value : (uint64_t)millis(); } +static bool getSystemEpochMs(uint64_t &epochMsOut) { + struct timeval tv {}; + if (gettimeofday(&tv, nullptr) != 0) { + return false; + } + if (tv.tv_sec < 1700000000) { + return false; + } + epochMsOut = (uint64_t)tv.tv_sec * 1000ULL + (uint64_t)(tv.tv_usec / 1000ULL); + return true; +} + +static bool ensureNtpTimeSynced(String &errorOut) { + errorOut = ""; + uint64_t epochMs = 0; + if (getSystemEpochMs(epochMs)) { + return true; + } + configTime(0, 0, "pool.ntp.org", "time.cloudflare.com", "time.google.com"); + for (int i = 0; i < 30; ++i) { + delay(500); + if (getSystemEpochMs(epochMs)) { + return true; + } + } + errorOut = "NTP time sync failed"; + return false; +} + +static bool resolveShineServerUrlFromLogin(const String &serverLogin, String &serverUrlOut, String &errorOut) { + errorOut = ""; + serverUrlOut = ""; + String cleanLogin = normalizeLoginValue(serverLogin); + if (cleanLogin.isEmpty()) { + errorOut = "Shine server login is not set"; + return false; + } + + ShinePdaUserState serverState; + if (!readShineUserPda(cleanLogin, serverState, errorOut)) { + if (errorOut.isEmpty()) { + errorOut = "Failed to read Shine server PDA"; + } + return false; + } + if (!serverState.found) { + errorOut = "Shine server PDA not found"; + return false; + } + if (!serverState.isServer) { + errorOut = "Shine server PDA is not a server"; + return false; + } + if (serverState.serverAddress.isEmpty()) { + errorOut = "Shine server address is empty"; + return false; + } + + serverUrlOut = serverState.serverAddress; + return true; +} + +static String currentShineServerLoginSource() { + if (gCachedAccountPdaValid && gCachedAccountPdaLogin == normalizeLoginValue(gLoginValue) && !gCachedAccountPdaState.accessServers.empty()) { + String fromPda = normalizeLoginValue(gCachedAccountPdaState.accessServers.front()); + if (!fromPda.isEmpty()) { + return fromPda; + } + } + return normalizeLoginValue(gShineServerLogin); +} + +static bool ensureCurrentShineServerUrl(String &errorOut) { + errorOut = ""; + String login = currentShineServerLoginSource(); + if (login.isEmpty()) { + errorOut = "Shine server login is not set"; + return false; + } + if (gShineServerUrl.isEmpty() || gResolvedShineServerLogin != login) { + String resolvedUrl; + if (!resolveShineServerUrlFromLogin(login, resolvedUrl, errorOut)) { + return false; + } + gShineServerUrl = resolvedUrl; + gResolvedShineServerLogin = login; + } + return true; +} + static void shortVecEncode(size_t value, std::vector &out) { do { uint8_t byte = value & 0x7F; @@ -1753,15 +1943,19 @@ static std::vector serializeUnsignedRecordState(const ShinePdaUserState pushU32LE(out, state.recordNumber); pushFixed(out, state.prevRecordHash32, 32); pushStrU8(out, state.login); - out.push_back(state.isServer ? 7 : 6); + out.push_back(state.isServer ? 8 : 7); + + out.push_back(kBlockTypeRecoveryKey); + out.push_back(0); + pushFixed(out, state.recoveryKey32, 32); out.push_back(kBlockTypeRootKey); out.push_back(0); pushFixed(out, state.rootKey32, 32); - out.push_back(kBlockTypeDeviceKey); + out.push_back(kBlockTypeClientKey); out.push_back(0); - pushFixed(out, state.deviceKey32, 32); + pushFixed(out, state.clientKey32, 32); out.push_back(kBlockTypeBlockchainRegistry); out.push_back(0); @@ -1831,13 +2025,14 @@ static std::vector buildUpdateInstructionData(const ShinePdaUserState & out.reserve(640); out.push_back(4); pushStrU8(out, state.login); + pushFixed(out, state.recoveryKey32, 32); pushFixed(out, state.rootKey32, 32); pushU64LE(out, state.createdAtMs); pushU64LE(out, updatedAtMs); pushU32LE(out, nextVersion); pushFixed(out, prevHash32, 32); pushU64LE(out, 0); - pushFixed(out, state.deviceKey32, 32); + pushFixed(out, state.clientKey32, 32); pushFixed(out, state.blockchainKey32, 32); pushStrU8(out, state.blockchainName); pushU64LE(out, state.usedBytes); @@ -1880,9 +2075,10 @@ static std::vector buildUpdateInstructionData(const ShinePdaUserState & static std::vector buildUnsignedCreateRecord( const String &login, const String &blockchainName, - const String &serverAddress, + const std::vector &accessServers, + const uint8_t recoveryPub[32], const uint8_t rootPub[32], - const uint8_t devicePub[32], + const uint8_t clientPub[32], const uint8_t blockchainPub[32], const uint8_t lastBlockSignature[64], uint64_t paidLimitBytes, @@ -1901,13 +2097,17 @@ static std::vector buildUnsignedCreateRecord( pushStrU8(out, login); out.push_back(7); + out.push_back(kBlockTypeRecoveryKey); + out.push_back(0); + pushFixed(out, recoveryPub, 32); + out.push_back(kBlockTypeRootKey); out.push_back(0); pushFixed(out, rootPub, 32); - out.push_back(kBlockTypeDeviceKey); + out.push_back(kBlockTypeClientKey); out.push_back(0); - pushFixed(out, devicePub, 32); + pushFixed(out, clientPub, 32); out.push_back(kBlockTypeBlockchainRegistry); out.push_back(0); @@ -1922,17 +2122,12 @@ static std::vector buildUnsignedCreateRecord( pushFixed(out, lastBlockSignature, 64); out.push_back(0); - out.push_back(kBlockTypeServerProfile); - out.push_back(0); - out.push_back(1); - out.push_back(0); - out.push_back(0); - pushStrU8(out, serverAddress); - out.push_back(0); - out.push_back(kBlockTypeAccessServers); out.push_back(0); - out.push_back(0); + out.push_back((uint8_t)accessServers.size()); + for (const auto &value : accessServers) { + pushStrU8(out, value); + } out.push_back(kBlockTypeSessions); out.push_back(0); @@ -1952,9 +2147,10 @@ static std::vector buildUnsignedCreateRecord( static std::vector buildCreateInstructionData( const String &login, const String &blockchainName, - const String &serverAddress, + const std::vector &accessServers, + const uint8_t recoveryPub[32], const uint8_t rootPub[32], - const uint8_t devicePub[32], + const uint8_t clientPub[32], const uint8_t blockchainPub[32], const uint8_t lastBlockSignature[64], const uint8_t rootSignature[64], @@ -1963,10 +2159,11 @@ static std::vector buildCreateInstructionData( out.reserve(512); out.push_back(3); pushStrU8(out, login); + pushFixed(out, recoveryPub, 32); pushFixed(out, rootPub, 32); pushU64LE(out, createdAtMs); pushU64LE(out, 0); - pushFixed(out, devicePub, 32); + pushFixed(out, clientPub, 32); pushFixed(out, blockchainPub, 32); pushStrU8(out, blockchainName); pushU64LE(out, 0); @@ -1974,12 +2171,11 @@ static std::vector buildCreateInstructionData( out.insert(out.end(), 32, 0); pushFixed(out, lastBlockSignature, 64); out.push_back(0); - out.push_back(1); - out.push_back(0); - out.push_back(0); - pushStrU8(out, serverAddress); - out.push_back(0); out.push_back(0); + out.push_back((uint8_t)accessServers.size()); + for (const auto &value : accessServers) { + pushStrU8(out, value); + } out.push_back(1); out.push_back(0); out.push_back(0); @@ -2010,20 +2206,20 @@ static std::vector buildEd25519InstructionData(const uint8_t signature[ } static bool getLatestBlockhashBytes(uint8_t out[32], String &blockhashB58, String &messageOut) { - String payload; - if (!rpcCallSolana("getLatestBlockhash", "[{\"commitment\":\"confirmed\"}]", payload)) { - messageOut = "RPC did not return blockhash"; - return false; + for (int attempt = 0; attempt < 3; ++attempt) { + String payload; + if (rpcCallSolana("getLatestBlockhash", "[{\"commitment\":\"confirmed\"}]", payload) + && jsonStringField(payload, "blockhash", blockhashB58) + && !blockhashB58.isEmpty() + && base58ToFixed32(blockhashB58, out)) { + return true; + } + if (attempt < 2) { + delay(120); + } } - if (!jsonStringField(payload, "blockhash", blockhashB58) || blockhashB58.isEmpty()) { - messageOut = "Blockhash missing in response"; - return false; - } - if (!base58ToFixed32(blockhashB58, out)) { - messageOut = "Invalid blockhash"; - return false; - } - return true; + messageOut = "RPC did not return blockhash"; + return false; } static bool pdaAlreadyExists(const String &login, String &pdaAddress, String &messageOut) { @@ -2215,7 +2411,7 @@ static bool simulateTransactionForError(const String &txBase64, String &messageO static std::vector buildLegacyMessage( const uint8_t recentBlockhash[32], - const uint8_t devicePub[32], + const uint8_t clientPub[32], const uint8_t userPda[32], const uint8_t inflowVault[32], const uint8_t economyConfig[32], @@ -2234,7 +2430,7 @@ static std::vector buildLegacyMessage( base58ToFixed32(kShineLoginGuardProgramId, loginGuardProgram); std::vector> accountKeys; - accountKeys.emplace_back(devicePub, devicePub + 32); + accountKeys.emplace_back(clientPub, clientPub + 32); accountKeys.emplace_back(userPda, userPda + 32); accountKeys.emplace_back(inflowVault, inflowVault + 32); accountKeys.emplace_back(systemProgram, systemProgram + 32); @@ -2282,7 +2478,7 @@ static std::vector buildLegacyMessage( static std::vector buildUpdateLegacyMessage( const uint8_t recentBlockhash[32], - const uint8_t devicePub[32], + const uint8_t clientPub[32], const uint8_t userPda[32], const uint8_t inflowVault[32], const uint8_t economyConfig[32], @@ -2299,7 +2495,7 @@ static std::vector buildUpdateLegacyMessage( base58ToFixed32(kSysvarInstructionsId, sysvarInstructions); std::vector> accountKeys; - accountKeys.emplace_back(devicePub, devicePub + 32); + accountKeys.emplace_back(clientPub, clientPub + 32); accountKeys.emplace_back(userPda, userPda + 32); accountKeys.emplace_back(inflowVault, inflowVault + 32); accountKeys.emplace_back(systemProgram, systemProgram + 32); @@ -2608,11 +2804,15 @@ static bool registerHomeserverOnSolana(String &messageOut) { }; String cleanLogin = normalizeLoginValue(gLoginValue); + String accessServerLogin = normalizeLoginValue(gShineServerLogin); + if (!isValidShineServerLoginValue(accessServerLogin)) { + accessServerLogin = kDefaultShineServerLogin; + } diagDetails += String("trigger=") + gRegisterTriggerSource + "\n"; diagDetails += String("test_uptime_ms=") + String(millis()) + "\n"; diagDetails += String("login=") + cleanLogin + "\n"; diagDetails += String("rpc=") + gSolanaRpcUrl + "\n"; - diagDetails += String("shine_server=") + gShineServerUrl + "\n"; + diagDetails += String("shine_server_login=") + accessServerLogin + "\n"; diagDetails += String("homeserver=") + gHomeserverValue + "\n"; if (cleanLogin.isEmpty()) { @@ -2640,7 +2840,7 @@ static bool registerHomeserverOnSolana(String &messageOut) { gAccountPdaStatusMessage = "User is already registered"; gShowRegisterAccountButton = false; gAccountStatusMessage = "User is already registered"; - gShineStatusLine = String("SHiNE: ") + (gShineServerUrl.isEmpty() ? "not set" : gShineServerUrl) + " registered"; + gShineStatusLine = String("SHiNE: ") + (!gShineServerUrl.isEmpty() ? gShineServerUrl : accessServerLogin) + " registered"; refreshAccountPdaStatus(); diagDetails += String("user_pda=") + existingPda + "\n"; saveRegisterDiag("ok", "User is already registered", diagDetails); @@ -2705,20 +2905,25 @@ static bool registerHomeserverOnSolana(String &messageOut) { uint8_t rootSeed[32] = {}; uint8_t rootPub[32] = {}; uint8_t rootSec[64] = {}; + uint8_t recoverySeed[32] = {}; + uint8_t recoveryPub[32] = {}; + uint8_t recoverySec[64] = {}; uint8_t blockchainSeed[32] = {}; uint8_t blockchainPub[32] = {}; uint8_t blockchainSec[64] = {}; - uint8_t deviceSeed[32] = {}; - uint8_t devicePub[32] = {}; + uint8_t clientSeed[32] = {}; + uint8_t clientPub[32] = {}; uint8_t deviceSec[64] = {}; - if (!deriveSeedKeypairFromBase58(gRootPrivB58, rootSeed, rootPub, rootSec) || + if (!deriveSeedKeypairFromBase58(gRecoveryPrivB58, recoverySeed, recoveryPub, recoverySec) || + !deriveSeedKeypairFromBase58(gRootPrivB58, rootSeed, rootPub, rootSec) || !deriveSeedKeypairFromBase58(gBlockchainPrivB58, blockchainSeed, blockchainPub, blockchainSec) || - !deriveSeedKeypairFromBase58(gDevicePrivB58, deviceSeed, devicePub, deviceSec)) { + !deriveSeedKeypairFromBase58(gDevicePrivB58, clientSeed, clientPub, deviceSec)) { return failWithDiag("Failed to restore keys"); } + diagDetails += String("recovery_pub=") + bytesToBase58(recoveryPub, 32) + "\n"; diagDetails += String("root_pub=") + bytesToBase58(rootPub, 32) + "\n"; diagDetails += String("blockchain_pub=") + bytesToBase58(blockchainPub, 32) + "\n"; - diagDetails += String("device_pub=") + bytesToBase58(devicePub, 32) + "\n"; + diagDetails += String("device_pub=") + bytesToBase58(clientPub, 32) + "\n"; String blockchainName = cleanLogin + "-001"; diagDetails += String("blockchain_name=") + blockchainName + "\n"; @@ -2732,11 +2937,20 @@ static bool registerHomeserverOnSolana(String &messageOut) { diagDetails += String("last_block_hash=") + bytesToHexString(lastBlockHash, 32) + "\n"; diagDetails += String("last_block_signature_b64=") + bytesToBase64String(lastBlockSignature, 64) + "\n"; - uint64_t createdAtMs = shineNowMs(); + if (!ensureNtpTimeSynced(messageOut)) { + diagDetails += String("ntp_error=") + messageOut + "\n"; + return failWithDiag(messageOut); + } + uint64_t createdAtMs = 0; + if (!getSystemEpochMs(createdAtMs)) { + return failWithDiag("NTP time is not ready"); + } diagDetails += String("created_at_ms=") + String((unsigned long long)createdAtMs) + "\n"; + std::vector accessServers = {accessServerLogin}; std::vector unsignedRecord = buildUnsignedCreateRecord( - cleanLogin, blockchainName, gShineServerUrl, - rootPub, devicePub, blockchainPub, + cleanLogin, blockchainName, accessServers, + recoveryPub, + rootPub, clientPub, blockchainPub, lastBlockSignature, startBonusLimit, createdAtMs); uint8_t unsignedHash[32]; uint8_t rootSignature[64]; @@ -2749,8 +2963,9 @@ static bool registerHomeserverOnSolana(String &messageOut) { diagDetails += String("root_signature_b64=") + bytesToBase64String(rootSignature, 64) + "\n"; std::vector createData = buildCreateInstructionData( - cleanLogin, blockchainName, gShineServerUrl, - rootPub, devicePub, blockchainPub, + cleanLogin, blockchainName, accessServers, + recoveryPub, + rootPub, clientPub, blockchainPub, lastBlockSignature, rootSignature, createdAtMs); std::vector edRootData = buildEd25519InstructionData(rootSignature, rootPub, unsignedHash); std::vector edBchData = buildEd25519InstructionData(lastBlockSignature, blockchainPub, lastBlockHash); @@ -2765,7 +2980,7 @@ static bool registerHomeserverOnSolana(String &messageOut) { std::vector message = buildLegacyMessage( recentBlockhash, - devicePub, + clientPub, userPda, inflowVault, economyConfig, @@ -2819,7 +3034,7 @@ static bool registerHomeserverOnSolana(String &messageOut) { gAccountPdaStatus = ACCOUNT_PDA_OK; gAccountPdaStatusMessage = "User registered"; gShowRegisterAccountButton = false; - gShineStatusLine = String("SHiNE: ") + (gShineServerUrl.isEmpty() ? "not set" : gShineServerUrl) + " registered"; + gShineStatusLine = String("SHiNE: ") + (!gShineServerUrl.isEmpty() ? gShineServerUrl : accessServerLogin) + " registered"; saveAccountPrefs(); refreshAccountPdaStatus(); messageOut = "Solana registration confirmed"; @@ -2983,6 +3198,7 @@ static bool updateHomeserverSessionOnSolana(bool requireExisting, String &messag diagDetails += String("login=") + cleanLogin + "\n"; diagDetails += String("homeserver=") + gHomeserverValue + "\n"; diagDetails += String("rpc=") + gSolanaRpcUrl + "\n"; + Serial.println("HOMESERVER_UPDATE_BEGIN"); if (cleanLogin.isEmpty()) { return failWithDiag("Login is not set"); @@ -2998,32 +3214,67 @@ static bool updateHomeserverSessionOnSolana(bool requireExisting, String &messag return failWithDiag("Connect Wi-Fi first"); } diagDetails += String("wifi=connected:") + WiFi.localIP().toString() + "\n"; + Serial.println("HOMESERVER_UPDATE_WIFI_OK"); + if (gShineAuthenticated || gShineWs.connected) { + Serial.println("HOMESERVER_UPDATE_CLOSE_SHINE_SESSION"); + clearShineSessionState(false); + } ShinePdaUserState currentState; String stateError; - if (!readShineUserPda(cleanLogin, currentState, stateError)) { - diagDetails += String("read_pda_error=") + stateError + "\n"; - return failWithDiag(stateError.isEmpty() ? "Failed to read user PDA" : stateError); - } - if (!currentState.found) { - return failWithDiag("User PDA does not exist yet"); + bool usedCachedPda = false; + if (gCachedAccountPdaValid && gCachedAccountPdaLogin == cleanLogin && gCachedAccountPdaState.found) { + currentState = gCachedAccountPdaState; + usedCachedPda = true; + diagDetails += "read_pda_source=cache\n"; + } else { + Serial.println("HOMESERVER_UPDATE_READ_PDA_CALL"); + if (!readShineUserPda(cleanLogin, currentState, stateError)) { + diagDetails += String("read_pda_error=") + stateError + "\n"; + return failWithDiag(stateError.isEmpty() ? "Failed to read user PDA" : stateError); + } + if (!currentState.found) { + return failWithDiag("User PDA does not exist yet"); + } + diagDetails += "read_pda_source=rpc\n"; } + Serial.println(usedCachedPda ? "HOMESERVER_UPDATE_PDA_READ_CACHE" : "HOMESERVER_UPDATE_PDA_READ_RPC"); + Serial.println("HOMESERVER_UPDATE_PDA_SNAPSHOT_OK"); + uint8_t recoverySeed[32] = {}; + uint8_t recoveryPub[32] = {}; + uint8_t recoverySec[64] = {}; uint8_t rootSeed[32] = {}; uint8_t rootPub[32] = {}; uint8_t rootSec[64] = {}; - uint8_t deviceSeed[32] = {}; - uint8_t devicePub[32] = {}; + uint8_t clientSeed[32] = {}; + uint8_t clientPub[32] = {}; uint8_t deviceSec[64] = {}; uint8_t homeserverPub[32] = {}; - if (!deriveSeedKeypairFromBase58(gRootPrivB58, rootSeed, rootPub, rootSec) - || !deriveSeedKeypairFromBase58(gDevicePrivB58, deviceSeed, devicePub, deviceSec) + if (!deriveSeedKeypairFromBase58(gRecoveryPrivB58, recoverySeed, recoveryPub, recoverySec) + || !deriveSeedKeypairFromBase58(gRootPrivB58, rootSeed, rootPub, rootSec) + || !deriveSeedKeypairFromBase58(gDevicePrivB58, clientSeed, clientPub, deviceSec) || !base58ToFixed32(gHomeserverPubB58, homeserverPub)) { return failWithDiag("Failed to restore local keys"); } - if (memcmp(devicePub, currentState.deviceKey32, 32) != 0) { - return failWithDiag("Device key does not match PDA"); + if (memcmp(recoveryPub, currentState.recoveryKey32, 32) != 0) { + return failWithDiag("Recovery key does not match PDA"); } + if (memcmp(clientPub, currentState.clientKey32, 32) != 0) { + return failWithDiag("Client key does not match PDA"); + } + saveRegisterDiagCheckpoint("Local keys restored", diagDetails); + + if (!ensureNtpTimeSynced(messageOut)) { + diagDetails += String("ntp_error=") + messageOut + "\n"; + return failWithDiag(messageOut); + } + uint64_t updatedAtMs = 0; + if (!getSystemEpochMs(updatedAtMs)) { + return failWithDiag("NTP time is not ready"); + } + diagDetails += String("updated_at_ms=") + String((unsigned long long)updatedAtMs) + "\n"; + saveRegisterDiagCheckpoint("Homeserver update timestamp ready", diagDetails); uint8_t userPda[32] = {}; uint8_t economyConfig[32] = {}; @@ -3047,9 +3298,10 @@ static bool updateHomeserverSessionOnSolana(bool requireExisting, String &messag diagDetails += String("inflow_vault_pda=") + bytesToBase58(inflowVault, 32) + "\n"; ShinePdaUserState nextState = currentState; + memcpy(nextState.recoveryKey32, recoveryPub, 32); memcpy(nextState.rootKey32, rootPub, 32); - memcpy(nextState.deviceKey32, devicePub, 32); - nextState.updatedAtMs = shineNowMs(); + memcpy(nextState.clientKey32, clientPub, 32); + nextState.updatedAtMs = updatedAtMs; nextState.recordNumber = currentState.recordNumber + 1; if (nextState.sessionsMode == 0) { nextState.sessionsMode = 1; @@ -3075,6 +3327,7 @@ static bool updateHomeserverSessionOnSolana(bool requireExisting, String &messag memcpy(rec.sessionPubKey32, homeserverPub, 32); nextState.sessions.push_back(rec); } + saveRegisterDiagCheckpoint("Homeserver session merged", diagDetails); std::vector oldUnsigned = serializeUnsignedRecordState(currentState); uint8_t prevHash32[32] = {}; @@ -3091,6 +3344,7 @@ static bool updateHomeserverSessionOnSolana(bool requireExisting, String &messag } diagDetails += String("unsigned_record_len=") + String((unsigned long)newUnsigned.size()) + "\n"; diagDetails += String("unsigned_record_hash=") + bytesToHexString(unsignedHash, 32) + "\n"; + saveRegisterDiagCheckpoint("Unsigned update built", diagDetails); std::vector lastBlockState = buildLastBlockStateBytes( cleanLogin, @@ -3118,10 +3372,11 @@ static bool updateHomeserverSessionOnSolana(bool requireExisting, String &messag return failWithDiag(messageOut); } diagDetails += String("recent_blockhash=") + recentBlockhash58 + "\n"; + saveRegisterDiagCheckpoint("Recent blockhash loaded", diagDetails); std::vector message = buildUpdateLegacyMessage( recentBlockhash, - devicePub, + clientPub, userPda, inflowVault, economyConfig, @@ -3135,6 +3390,7 @@ static bool updateHomeserverSessionOnSolana(bool requireExisting, String &messag String txBase64 = encodeTransactionBase64(txSignature, message); String signatureB58 = bytesToBase58(txSignature, 64); diagDetails += String("tx_signature=") + signatureB58 + "\n"; + saveRegisterDiagCheckpoint("Signed update transaction", diagDetails); String payload; if (!rpcCallSolana("sendTransaction", "[\"" + txBase64 + "\",{\"encoding\":\"base64\",\"preflightCommitment\":\"confirmed\"}]", payload)) { @@ -3282,6 +3538,13 @@ static bool parseShineUserPdaBytes(const std::vector &bytes, ShinePdaUs errorOut = "Bad PDA block header"; return false; } + if (blockType == kBlockTypeRecoveryKey) { + if (!readBytes(outState.recoveryKey32, 32)) { + errorOut = "Bad recovery key block"; + return false; + } + continue; + } if (blockType == kBlockTypeRootKey) { if (!readBytes(outState.rootKey32, 32)) { errorOut = "Bad root key block"; @@ -3289,9 +3552,9 @@ static bool parseShineUserPdaBytes(const std::vector &bytes, ShinePdaUs } continue; } - if (blockType == kBlockTypeDeviceKey) { - if (!readBytes(outState.deviceKey32, 32)) { - errorOut = "Bad device key block"; + if (blockType == kBlockTypeClientKey) { + if (!readBytes(outState.clientKey32, 32)) { + errorOut = "Bad client key block"; return false; } continue; @@ -3457,6 +3720,7 @@ static bool readShineUserPda(const String &login, ShinePdaUserState &outState, S } String pdaB58 = base58From32(userPda); + Serial.println(String("READ_PDA_RPC_START: ") + pdaB58); int code = -1; String payload; String req = "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"getAccountInfo\",\"params\":[\"" + pdaB58 + "\",{\"encoding\":\"base64\",\"commitment\":\"confirmed\"}]}"; @@ -3464,6 +3728,7 @@ static bool readShineUserPda(const String &login, ShinePdaUserState &outState, S errorOut = "Solana RPC unavailable"; return false; } + Serial.println(String("READ_PDA_RPC_DONE_CODE: ") + String(code)); if (payload.indexOf("\"value\":null") >= 0) { outState.found = false; return true; @@ -3492,7 +3757,7 @@ static void refreshAccountPdaStatus() { if (gLoginValue.isEmpty() || !gSecretConfigured) { gAccountPdaStatus = ACCOUNT_PDA_UNKNOWN; gAccountPdaStatusMessage = "account not configured"; - gShineStatusLine = String("SHiNE: ") + (gShineServerUrl.isEmpty() ? "not set" : gShineServerUrl) + " account not configured"; + gShineStatusLine = String("SHiNE: ") + shineServerDisplayLabel() + " account not configured"; clearShineSessionState(false); return; } @@ -3502,7 +3767,7 @@ static void refreshAccountPdaStatus() { if (!readShineUserPda(gLoginValue, pdaState, error)) { gAccountPdaStatus = ACCOUNT_PDA_MISMATCH; gAccountPdaStatusMessage = error.isEmpty() ? "solana check failed" : error; - gShineStatusLine = String("SHiNE: ") + (gShineServerUrl.isEmpty() ? "not set" : gShineServerUrl) + " unavailable"; + gShineStatusLine = String("SHiNE: ") + shineServerDisplayLabel() + " unavailable"; clearShineSessionState(false); if (error == "Solana RPC unavailable") { gAccountCheckPending = true; @@ -3514,31 +3779,39 @@ static void refreshAccountPdaStatus() { gAccountPdaStatus = ACCOUNT_PDA_NOT_FOUND; gAccountPdaStatusMessage = "user not found"; gShowRegisterAccountButton = true; - gShineStatusLine = String("SHiNE: ") + (gShineServerUrl.isEmpty() ? "not set" : gShineServerUrl) + " account not configured"; + gShineStatusLine = String("SHiNE: ") + shineServerDisplayLabel() + " account not configured"; clearShineSessionState(false); return; } + gCachedAccountPdaState = pdaState; + gCachedAccountPdaLogin = normalizeLoginValue(gLoginValue); + gCachedAccountPdaValid = true; + + uint8_t recoveryPub[32] = {}; uint8_t rootPub[32] = {}; - uint8_t devicePub[32] = {}; + uint8_t clientPub[32] = {}; uint8_t blockchainPub[32] = {}; - if (!base58ToFixed32(gRootPubB58, rootPub) - || !base58ToFixed32(gDevicePubB58, devicePub) + if (!base58ToFixed32(gRecoveryPubB58, recoveryPub) + || !base58ToFixed32(gRootPubB58, rootPub) + || !base58ToFixed32(gDevicePubB58, clientPub) || !base58ToFixed32(gBlockchainPubB58, blockchainPub)) { gAccountPdaStatus = ACCOUNT_PDA_MISMATCH; gAccountPdaStatusMessage = "local keys invalid"; - gShineStatusLine = String("SHiNE: ") + (gShineServerUrl.isEmpty() ? "not set" : gShineServerUrl) + " account not configured"; + gShineStatusLine = String("SHiNE: ") + shineServerDisplayLabel() + " account not configured"; clearShineSessionState(false); return; } String mismatch; - if (memcmp(rootPub, pdaState.rootKey32, 32) != 0) { + if (memcmp(recoveryPub, pdaState.recoveryKey32, 32) != 0) { + mismatch = "recovery key mismatch"; + } else if (memcmp(rootPub, pdaState.rootKey32, 32) != 0) { mismatch = "root key mismatch"; } else if (memcmp(blockchainPub, pdaState.blockchainKey32, 32) != 0) { mismatch = "blockchain key mismatch"; - } else if (memcmp(devicePub, pdaState.deviceKey32, 32) != 0) { - mismatch = "device key mismatch"; + } else if (memcmp(clientPub, pdaState.clientKey32, 32) != 0) { + mismatch = "client key mismatch"; } else if (gHomeserverValue.isEmpty()) { mismatch = "homeserver not set"; } else { @@ -3578,7 +3851,7 @@ static void refreshAccountPdaStatus() { gHomeserverPdaActionReason = mismatch; gHomeserverPdaCanFix = true; } - gShineStatusLine = String("SHiNE: ") + (gShineServerUrl.isEmpty() ? "not set" : gShineServerUrl) + " account not configured"; + gShineStatusLine = String("SHiNE: ") + shineServerDisplayLabel() + " account not configured"; clearShineSessionState(false); return; } @@ -3963,20 +4236,20 @@ static bool buildPkcs8FromSeed32(const uint8_t seed32[32], String &pkcs8B64Out) static bool buildPairingSecretsPayload(const PairingRequestUiItem &item, String &payloadJsonOut, String &errorOut) { payloadJsonOut = ""; errorOut = ""; - uint8_t deviceSeed[32] = {}; - uint8_t devicePub[32] = {}; + uint8_t clientSeed[32] = {}; + uint8_t clientPub[32] = {}; uint8_t deviceSec[64] = {}; - if (!deriveSeedKeypairFromBase58(gDevicePrivB58, deviceSeed, devicePub, deviceSec)) { - errorOut = "Failed to derive device key"; + if (!deriveSeedKeypairFromBase58(gDevicePrivB58, clientSeed, clientPub, deviceSec)) { + errorOut = "Failed to derive client key"; return false; } String devicePkcs8; - if (!buildPkcs8FromSeed32(deviceSeed, devicePkcs8)) { - errorOut = "Failed to encode device key"; + if (!buildPkcs8FromSeed32(clientSeed, devicePkcs8)) { + errorOut = "Failed to encode client key"; return false; } payloadJsonOut = String("{\"v\":1,\"type\":\"shine-esp-pairing-transfer\",\"login\":\"") + jsonEscape(gLoginValue) - + "\",\"mode\":\"device-only\",\"keys\":{\"deviceKey\":\"" + jsonEscape(devicePkcs8) + + "\",\"mode\":\"device-only\",\"keys\":{\"clientKey\":\"" + jsonEscape(devicePkcs8) + "\",\"blockchainKey\":\"\",\"rootKey\":\"\"},\"payloadType\":1,\"createdAtMs\":" + String((unsigned long long)shineNowMs()) + "}"; return true; @@ -3985,11 +4258,11 @@ static bool buildPairingSecretsPayload(const PairingRequestUiItem &item, String static bool createDelegatedWalletSessionPayload(const PairingRequestUiItem &item, String &payloadJsonOut, String &errorOut) { payloadJsonOut = ""; errorOut = ""; - uint8_t deviceSeed[32] = {}; - uint8_t devicePub[32] = {}; + uint8_t clientSeed[32] = {}; + uint8_t clientPub[32] = {}; uint8_t deviceSec[64] = {}; - if (!deriveSeedKeypairFromBase58(gDevicePrivB58, deviceSeed, devicePub, deviceSec)) { - errorOut = "Failed to derive device key"; + if (!deriveSeedKeypairFromBase58(gDevicePrivB58, clientSeed, clientPub, deviceSec)) { + errorOut = "Failed to derive client key"; return false; } @@ -4040,7 +4313,7 @@ static bool createDelegatedWalletSessionPayload(const PairingRequestUiItem &item + "\",\"storagePwd\":\"" + jsonEscape(storagePwd) + "\",\"timeMs\":" + String((unsigned long long)timeMs) + ",\"authNonce\":\"" + jsonEscape(authNonce) - + "\",\"deviceKey\":\"" + jsonEscape(bytesToBase64String(devicePub, 32)) + + "\",\"clientKey\":\"" + jsonEscape(bytesToBase64String(clientPub, 32)) + "\",\"signatureB64\":\"" + jsonEscape(bytesToBase64String(signature, 64)) + "\",\"sessionType\":50" + ",\"clientPlatform\":\"" + jsonEscape(clientPlatform) @@ -4219,8 +4492,7 @@ static bool ensureShineSessionAuthenticated(String &errorOut) { diagDetails += String("uptime_ms=") + String(millis()) + "\n"; diagDetails += String("login=") + gLoginValue + "\n"; diagDetails += String("homeserver=") + gHomeserverValue + "\n"; - diagDetails += String("server_url=") + gShineServerUrl + "\n"; - diagDetails += String("ws_url=") + shineWsUrl() + "\n"; + diagDetails += String("server_login=") + currentShineServerLoginSource() + "\n"; diagDetails += String("pda_status=") + gAccountPdaStatusMessage + "\n"; if (WiFi.status() != WL_CONNECTED) { diagDetails += "wifi=disconnected\n"; @@ -4233,6 +4505,12 @@ static bool ensureShineSessionAuthenticated(String &errorOut) { if (gAccountPdaStatus != ACCOUNT_PDA_OK) { return failWithDiag("account not ready"); } + if (!ensureCurrentShineServerUrl(errorOut)) { + diagDetails += String("server_resolve_error=") + errorOut + "\n"; + return failWithDiag(errorOut); + } + diagDetails += String("server_url=") + gShineServerUrl + "\n"; + diagDetails += String("ws_url=") + shineWsUrl() + "\n"; String wsUrl = shineWsUrl(); if (wsUrl.isEmpty()) { @@ -4262,13 +4540,13 @@ static bool ensureShineSessionAuthenticated(String &errorOut) { diagDetails += String("server_time_offset_ms=") + String((long long)gShineServerTimeOffsetMs) + "\n"; } - uint8_t deviceSeed[32] = {}; - uint8_t devicePub[32] = {}; + uint8_t clientSeed[32] = {}; + uint8_t clientPub[32] = {}; uint8_t deviceSec[64] = {}; uint8_t subSeed[32] = {}; uint8_t subPub[32] = {}; uint8_t subSec[64] = {}; - if (!deriveSeedKeypairFromBase58(gDevicePrivB58, deviceSeed, devicePub, deviceSec) + if (!deriveSeedKeypairFromBase58(gDevicePrivB58, clientSeed, clientPub, deviceSec) || !deriveSeedKeypairFromBase58(gHomeserverPrivB58, subSeed, subPub, subSec)) { return failWithDiag("local key derive failed"); } @@ -4368,7 +4646,7 @@ static bool ensureShineSessionAuthenticated(String &errorOut) { + "\",\"storagePwd\":\"" + jsonEscape(gShineStoragePwd) + "\",\"timeMs\":" + String((unsigned long long)timeMs) + ",\"authNonce\":\"" + jsonEscape(authNonce) - + "\",\"deviceKey\":\"" + jsonEscape(bytesToBase64String(devicePub, 32)) + + "\",\"clientKey\":\"" + jsonEscape(bytesToBase64String(clientPub, 32)) + "\",\"signatureB64\":\"" + jsonEscape(bytesToBase64String(signature, 64)) + "\",\"sessionType\":" + String((unsigned int)kSessionTypeHomeserver) + ",\"clientPlatform\":\"" + jsonEscape(kSessionClientPlatformEsp32) @@ -4394,7 +4672,7 @@ static bool ensureShineSessionAuthenticated(String &errorOut) { } static void manageShineConnection() { - String serverLabel = gShineServerUrl.isEmpty() ? "not set" : gShineServerUrl; + String serverLabel = shineServerDisplayLabel(); if (gLoginValue.isEmpty() || !gSecretConfigured || gHomeserverValue.isEmpty()) { gShineStatusLine = String("SHiNE: ") + serverLabel + " account not configured"; clearShineSessionState(false); @@ -4424,6 +4702,13 @@ static void manageShineConnection() { } gLastShineAttemptMs = now; String error; + if (!ensureCurrentShineServerUrl(error)) { + gShineStatusLine = String("SHiNE: ") + serverLabel + " unavailable"; + clearShineSessionState(false); + gShineReconnectDelayMs = min(gShineReconnectDelayMs + SHINE_RECONNECT_MIN_MS, (unsigned long)SHINE_RECONNECT_MAX_MS); + return; + } + serverLabel = shineServerDisplayLabel(); if (ensureShineSessionAuthenticated(error)) { gShineStatusLine = String("SHiNE: ") + serverLabel + " connected"; gLastShinePingMs = now; @@ -4507,10 +4792,21 @@ static void loadPrefs() { upsertKnownWifi(gWifiSavedSsid, gWifiSavedPassword); } gSolanaRpcUrl = gPrefs.getString("solana_rpc", "https://api.devnet.solana.com"); - gShineServerUrl = gPrefs.getString("shine_server", "https://shineup.me"); + String storedShineServerLogin = normalizeLoginValue(gPrefs.getString("shine_server_login", "")); + if (!isValidShineServerLoginValue(storedShineServerLogin)) { + String legacyShineServer = normalizeLoginValue(gPrefs.getString("shine_server", "")); + if (isValidShineServerLoginValue(legacyShineServer)) { + storedShineServerLogin = legacyShineServer; + } else { + storedShineServerLogin = kDefaultShineServerLogin; + } + } + gShineServerLogin = storedShineServerLogin; + gShineServerUrl = ""; + gResolvedShineServerLogin = ""; gLoginValue = gPrefs.getString("login", ""); gHomeserverValue = gPrefs.getString("homeserver", "homeserver1"); - String walletTypeStored = gPrefs.getString("wallet_type", "dev.key"); + String walletTypeStored = gPrefs.getString("wallet_type", "client.key"); if (walletTypeStored == "root.key") { gSelectedWalletType = WALLET_SELECTION_ROOT; } else if (walletTypeStored == "custom") { @@ -4576,7 +4872,8 @@ static void saveWifiPrefs() { static void saveServerPrefs() { gPrefs.putString("solana_rpc", gSolanaRpcUrl); - gPrefs.putString("shine_server", gShineServerUrl); + gPrefs.putString("shine_server_login", gShineServerLogin); + gPrefs.remove("shine_server"); } static void saveAccountPrefs() { @@ -4730,6 +5027,10 @@ static void saveRegisterDiag(const String &status, const String &summary, const saveRegisterDiagDetailsToPrefs(details); } +static void saveRegisterDiagCheckpoint(const String &summary, const String &details) { + saveRegisterDiag("progress", summary, details); +} + static void clearRegisterDiag() { gLastRegisterDiagStatus = "none"; gLastRegisterDiagSummary = ""; @@ -5094,13 +5395,18 @@ static void applyEditorValue() { } if (gEditContext == EDIT_CONTEXT_SHINE_SERVER) { - gShineServerUrl = value; + gShineServerLogin = normalizeLoginValue(value); + if (!isValidShineServerLoginValue(gShineServerLogin)) { + gShineServerLogin = kDefaultShineServerLogin; + } + gShineServerUrl = ""; + gResolvedShineServerLogin = ""; saveServerPrefs(); - gServerStatusMessage = "Shine server saved"; + gServerStatusMessage = "SHiNE server login saved"; clearShineSessionState(false); gShineReconnectDelayMs = SHINE_RECONNECT_MIN_MS; gLastShineAttemptMs = 0; - gShineStatusLine = "SHiNE: reconnect pending"; + gShineStatusLine = String("SHiNE: ") + gShineServerLogin + " reconnect pending"; showScreen(SCREEN_SERVER); return; } @@ -5393,24 +5699,30 @@ static void actionButtonCb(lv_event_t *event) { showScreen(SCREEN_HOMESERVER_PDA_CONFIRM); break; case ACTION_HOMESERVER_PDA_ADD_EXECUTE: { + Serial.println("HOMESERVER_ADD_BUTTON_CLICK"); gRegisterTriggerSource = "manual-homeserver-add"; String updateMessage; + Serial.println("HOMESERVER_ADD_CALL_BEGIN"); if (!updateHomeserverSessionOnSolana(false, updateMessage)) { gHomeserverPdaResultSuccess = false; gHomeserverPdaResultMessage = updateMessage; gHomeserverPdaResultDetails = ""; } + Serial.println(String("HOMESERVER_ADD_CALL_END: ") + updateMessage); showScreen(SCREEN_HOMESERVER_PDA_RESULT); break; } case ACTION_HOMESERVER_PDA_FIX_EXECUTE: { + Serial.println("HOMESERVER_FIX_BUTTON_CLICK"); gRegisterTriggerSource = "manual-homeserver-fix"; String updateMessage; + Serial.println("HOMESERVER_FIX_CALL_BEGIN"); if (!updateHomeserverSessionOnSolana(true, updateMessage)) { gHomeserverPdaResultSuccess = false; gHomeserverPdaResultMessage = updateMessage; gHomeserverPdaResultDetails = ""; } + Serial.println(String("HOMESERVER_FIX_CALL_END: ") + updateMessage); showScreen(SCREEN_HOMESERVER_PDA_RESULT); break; } @@ -5454,9 +5766,9 @@ static void actionButtonCb(lv_event_t *event) { case ACTION_SERVER_EDIT_SHINE: openEditor(EDIT_CONTEXT_SHINE_SERVER, SCREEN_SERVER, - "EDIT SHINE HOST", + "EDIT SHINE SERVER LOGIN", "", - gShineServerUrl, + gShineServerLogin, false); break; case ACTION_ACCOUNT_EDIT_LOGIN: @@ -5786,7 +6098,7 @@ static void drawWalletSelectScreen() { makeTitle("SELECT WALLET", 22, &lv_font_montserrat_24); String currentLine = String("Current: ") + selectedWalletDisplayName(); makeBody(currentLine.c_str(), 88, 420); - String deviceLabel = String(gSelectedWalletType == WALLET_SELECTION_DEVICE ? "✓ DeviceKey" : "DeviceKey"); + String deviceLabel = String(gSelectedWalletType == WALLET_SELECTION_DEVICE ? "✓ ClientKey" : "ClientKey"); String rootLabel = String(gSelectedWalletType == WALLET_SELECTION_ROOT ? "✓ RootKey" : "RootKey"); String customBase = gCustomWalletName; customBase.trim(); @@ -6081,7 +6393,7 @@ static void drawPairingRequestDetailScreen() { String question = String("Connect session ") + pairingSessionNameLabel(item) + "?"; String explain = item.requesterSessionType == 50 ? "Wallet session. No keys will be transferred." - : "Client session. Only device key will be transferred. No additional keys will be sent."; + : "Client session. Only client key will be transferred. No additional keys will be sent."; String sessionNameText = String("Session: ") + pairingSessionNameLabel(item); String sessionKindText = String("Kind: ") + pairingSessionKindLabel(item.requesterSessionType); @@ -6201,8 +6513,12 @@ static void drawServerScreen() { showMessageAt(gServerStatusMessage, 56); showMessageAt(String("Solana: ") + gSolanaRpcUrl, 96); makeButton("SOLANA RPC", 22, 146, 436, 84, 0x355C7D, ACTION_SERVER_EDIT_SOLANA, &lv_font_montserrat_24); - showMessageAt(String("Shine: ") + gShineServerUrl, 248); - makeButton("SHINE SERVER", 22, 298, 436, 84, 0x355C7D, ACTION_SERVER_EDIT_SHINE, &lv_font_montserrat_24); + showMessageAt(String("SHiNE: ") + shineServerDisplayLabel(), 248); + if (gUserPdaAddress.isEmpty()) { + makeButton("SHiNE SERVER LOGIN", 22, 298, 436, 84, 0x355C7D, ACTION_SERVER_EDIT_SHINE, &lv_font_montserrat_22); + } else { + makeBody("SHiNE server login is read from PDA.", 312, 360); + } makeBody("Swipe right to return to Settings.", 396, 420); makeVersionTag(); } @@ -6243,14 +6559,14 @@ static void drawAccountHomeserverScreen() { static void drawAccountSecretScreen() { setRootStyle(); - makeTitle("SECRET", 18, &lv_font_montserrat_24); + makeTitle("MASTER SECRET", 18, &lv_font_montserrat_24); showMessageAt(gAccountStatusMessage, 56); - makeButton(gSecretConfigured ? "SHOW SECRET" : "SECRET NOT SET", + makeButton(gSecretConfigured ? "SHOW MASTER SECRET" : "MASTER SECRET NOT SET", 22, 118, 436, 84, gSecretConfigured ? 0x2A6F97 : 0x4A5560, gSecretConfigured ? ACTION_SECRET_SHOW : ACTION_NONE, &lv_font_montserrat_22); - makeButton("ENTER SECRET MANUALLY (NOT RECOMMENDED)", + makeButton("ENTER MASTER SECRET MANUALLY (NOT RECOMMENDED)", 22, 222, 436, 84, 0x355C7D, ACTION_SECRET_MANUAL, &lv_font_montserrat_18); - makeButton("GENERATE SECRET", + makeButton("GENERATE MASTER SECRET", 22, 326, 436, 84, 0x2A9D8F, ACTION_SECRET_GENERATE, &lv_font_montserrat_22); makeBody("Swipe right to return to Account.", 420, 420); makeVersionTag(); @@ -6258,7 +6574,7 @@ static void drawAccountSecretScreen() { static void drawSecretShowScreen() { setRootStyle(); - makeTitle("SECRET", 18, &lv_font_montserrat_24); + makeTitle("MASTER SECRET", 18, &lv_font_montserrat_24); if (gSecretConfigured) { lv_obj_t *panel = lv_obj_create(gRoot); lv_obj_set_size(panel, 440, 320); @@ -6268,25 +6584,25 @@ static void drawSecretShowScreen() { lv_obj_set_style_border_width(panel, 1, 0); lv_obj_set_style_border_color(panel, lv_color_hex(0x425466), 0); lv_obj_set_style_radius(panel, 14, 0); - lv_obj_set_style_pad_all(panel, 14, 0); + lv_obj_set_style_pad_top(panel, 14, 0); + lv_obj_set_style_pad_bottom(panel, 14, 0); + lv_obj_set_style_pad_left(panel, 30, 0); + lv_obj_set_style_pad_right(panel, 14, 0); lv_obj_set_style_pad_row(panel, 8, 0); lv_obj_set_scroll_dir(panel, LV_DIR_VER); lv_obj_set_scrollbar_mode(panel, LV_SCROLLBAR_MODE_ACTIVE); lv_obj_set_flex_flow(panel, LV_FLEX_FLOW_COLUMN); lv_obj_set_flex_align(panel, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START); + lv_obj_add_event_cb(panel, secretScrollEventCb, LV_EVENT_SCROLL_BEGIN, nullptr); + lv_obj_add_event_cb(panel, secretScrollEventCb, LV_EVENT_SCROLL_END, nullptr); + lv_obj_add_event_cb(panel, secretScrollEventCb, LV_EVENT_DELETE, nullptr); - auto addKeyBlock = [&](const String &title, const String &formula, const String &value) { + auto addKeyBlock = [&](const String &title, const String &value) { lv_obj_t *titleLabel = lv_label_create(panel); lv_label_set_text(titleLabel, title.c_str()); lv_obj_set_width(titleLabel, 400); lv_obj_set_style_text_font(titleLabel, &lv_font_montserrat_18, 0); - lv_obj_set_style_text_color(titleLabel, lv_color_hex(0xFFFFFF), 0); - - lv_obj_t *formulaLabel = lv_label_create(panel); - lv_label_set_text(formulaLabel, formula.c_str()); - lv_obj_set_width(formulaLabel, 400); - lv_obj_set_style_text_font(formulaLabel, &lv_font_montserrat_12, 0); - lv_obj_set_style_text_color(formulaLabel, lv_color_hex(0x8FA4B8), 0); + lv_obj_set_style_text_color(titleLabel, lv_color_hex(0xA7D8FF), 0); lv_obj_t *valueLabel = lv_label_create(panel); lv_label_set_text(valueLabel, value.c_str()); @@ -6296,15 +6612,17 @@ static void drawSecretShowScreen() { lv_obj_set_style_text_color(valueLabel, lv_color_hex(0xD9E1EA), 0); }; - addKeyBlock("Secret (base58)", "master secret", gSecretBase58); - addKeyBlock("Root key (base58)", "pub from sha256(base64(secret)|root.key)", gRootPubB58); - addKeyBlock("Root key priv (base58)", "sha256(base64(secret)|root.key)", gRootPrivB58); - addKeyBlock("Blockchain key (base58)", "pub from sha256(base64(secret)|bch.key)", gBlockchainPubB58); - addKeyBlock("Blockchain key priv (base58)", "sha256(base64(secret)|bch.key)", gBlockchainPrivB58); - addKeyBlock("Device key (base58)", "pub from sha256(base64(secret)|dev.key)", gDevicePubB58); - addKeyBlock("Device key priv (base58)", "sha256(base64(secret)|dev.key)", gDevicePrivB58); - addKeyBlock("Homeserver key (base58)", String("pub from sha256(base64(secret)|") + homeserverKeySuffix() + ")", gHomeserverPubB58); - addKeyBlock("Homeserver key priv (base58)", String("sha256(base64(secret)|") + homeserverKeySuffix() + ")", gHomeserverPrivB58); + addKeyBlock("Master Secret", gSecretBase58); + addKeyBlock("Recovery key", gRecoveryPubB58); + addKeyBlock("Recovery key priv", gRecoveryPrivB58); + addKeyBlock("Root key", gRootPubB58); + addKeyBlock("Root key priv", gRootPrivB58); + addKeyBlock("Blockchain key", gBlockchainPubB58); + addKeyBlock("Blockchain key priv", gBlockchainPrivB58); + addKeyBlock("Client key", gDevicePubB58); + addKeyBlock("Client key priv", gDevicePrivB58); + addKeyBlock("Homeserver key", gHomeserverPubB58); + addKeyBlock("Homeserver key priv", gHomeserverPrivB58); } else { showMessageAt("Secret not set", 96); } diff --git a/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/main-device/shine_homeserver_main/shine_secret_generation.cpp b/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/main-device/shine_homeserver_main/shine_secret_generation.cpp index 9e386fe..3193265 100644 --- a/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/main-device/shine_homeserver_main/shine_secret_generation.cpp +++ b/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/main-device/shine_homeserver_main/shine_secret_generation.cpp @@ -1,7 +1,6 @@ #include "shine_secret_generation.h" #include -#include #include #include #include @@ -80,6 +79,19 @@ static void setMessage(const char *message) { snprintf(gMessage, sizeof(gMessage), "%s", message ? message : ""); } +static void sha256calc(const uint8_t *in, size_t len, uint8_t *out32); + +static void finishSecretFromBytes(const uint8_t secret32[32], const char *message) { + memcpy(gSecret, secret32, 32); + shineSecretBase58Encode(gSecret, 32, gSecretB58, sizeof(gSecretB58)); + gDone = true; + gRunning = false; + gError = false; + gInitDone = false; + gDoneBlocks = TOTAL_FILLS; + setMessage(message ? message : "Secret generated"); +} + static void b2_compress(B2State *S, const uint8_t *blk) { uint64_t m[16], v[16]; for (int i = 0; i < 16; i++) m[i] = ((const uint64_t *)blk)[i]; @@ -449,7 +461,6 @@ bool shineSecretInitSd(String &error) { bool shineSecretStart(const char *normalizedLogin, const char *password, String &error) { error = ""; - if (!shineSecretInitSd(error)) return false; if (!normalizedLogin || !*normalizedLogin) { error = "login not set"; return false; @@ -463,6 +474,8 @@ bool shineSecretStart(const char *normalizedLogin, const char *password, String return false; } + if (!shineSecretInitSd(error)) return false; + if (gSdFile) gSdFile.close(); SD_MMC.remove(SD_MEM_FILE); gSdFile = SD_MMC.open(SD_MEM_FILE, "w+"); diff --git a/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/main-device/shine_homeserver_ui/shine_homeserver_ui.ino b/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/main-device/shine_homeserver_ui/shine_homeserver_ui.ino index e071a95..edf47cc 100644 --- a/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/main-device/shine_homeserver_ui/shine_homeserver_ui.ino +++ b/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/main-device/shine_homeserver_ui/shine_homeserver_ui.ino @@ -224,12 +224,14 @@ static int16_t gTouchLastY = 0; struct DerivedKeyState { bool ready; uint8_t masterSecret[32]; + uint8_t recoveryPub[32]; + uint8_t recoverySk[64]; uint8_t rootPub[32]; uint8_t rootSk[64]; uint8_t blockchainPub[32]; uint8_t blockchainSk[64]; - uint8_t devicePub[32]; - uint8_t deviceSk[64]; + uint8_t clientPub[32]; + uint8_t clientSk[64]; }; static DerivedKeyState gDerivedKeys = {}; @@ -237,7 +239,7 @@ static DerivedKeyState gDerivedKeys = {}; static const char *kSystemProgramId = "11111111111111111111111111111111"; static const char *kEd25519ProgramId = "Ed25519SigVerify111111111111111111111111111"; static const char *kSysvarInstructionsId = "Sysvar1nstructions1111111111111111111111111"; -static const char *kShineUsersProgramId = "FZS1YctoeEhCkZ5VTjsysUFAXR8CqxYztcLboXcg2Rpm"; +static const char *kShineUsersProgramId = "3bYrnXwLc56oVPUBAjY8zTMLwHCYq29b5rUMu3b64SQJ"; static const char *kShineLoginGuardProgramId = "3xkopA7cXagxzMFrKdv3NCBfV6BKiRJCk69kr27M2sRo"; static const char *kShinePaymentsProgramId = "c4yTa4JT9EtQDCBX9LmWFK6T2gp4JGsuymFbom2EudW"; static const char *kUsersSeedPrefix = "user_login="; @@ -246,7 +248,7 @@ static const char *kPaymentsInflowSeed = "shine_payments_inflow_vault"; static const char *kProgramDerivedAddressMarker = "ProgramDerivedAddress"; static const char *kLastBlockPrefix = "SHiNE_LAST_BLOCK"; static const uint8_t kBlockTypeRootKey = 1; -static const uint8_t kBlockTypeDeviceKey = 2; +static const uint8_t kBlockTypeClientKey = 2; static const uint8_t kBlockTypeBlockchainRegistry = 3; static const uint8_t kBlockTypeServerProfile = 30; static const uint8_t kBlockTypeAccessServers = 40; @@ -782,19 +784,20 @@ static void pushFixed(std::vector &out, const uint8_t *data, size_t len static bool deriveKeysFromMasterSecret(const uint8_t masterSecret[32]) { memset(&gDerivedKeys, 0, sizeof(gDerivedKeys)); memcpy(gDerivedKeys.masterSecret, masterSecret, 32); - String secretB64 = base64Encode(masterSecret, 32); - if (secretB64.length() == 0) { - return false; - } - const char *suffixes[3] = {"root.key", "bch.key", "dev.key"}; - uint8_t *pubs[3] = {gDerivedKeys.rootPub, gDerivedKeys.blockchainPub, gDerivedKeys.devicePub}; - uint8_t *sks[3] = {gDerivedKeys.rootSk, gDerivedKeys.blockchainSk, gDerivedKeys.deviceSk}; - for (int i = 0; i < 3; i++) { - String material = secretB64 + "|" + suffixes[i]; + const char *prefix = "SHiNE-key"; + const char *suffixes[4] = {"recovery.key", "root.key", "blockchain.key", "client.key"}; + uint8_t *pubs[4] = {gDerivedKeys.recoveryPub, gDerivedKeys.rootPub, gDerivedKeys.blockchainPub, gDerivedKeys.clientPub}; + uint8_t *sks[4] = {gDerivedKeys.recoverySk, gDerivedKeys.rootSk, gDerivedKeys.blockchainSk, gDerivedKeys.clientSk}; + for (int i = 0; i < 4; i++) { + std::vector material; + material.reserve(strlen(prefix) + 1 + 32 + 1 + strlen(suffixes[i])); + material.insert(material.end(), prefix, prefix + strlen(prefix)); + material.push_back(0); + material.insert(material.end(), masterSecret, masterSecret + 32); + material.push_back(0); + material.insert(material.end(), suffixes[i], suffixes[i] + strlen(suffixes[i])); uint8_t seed[32]; - if (!sha256String(material, seed)) { - return false; - } + sha256Raw(material.data(), material.size(), seed); if (crypto_sign_seed_keypair(pubs[i], sks[i], seed) != 0) { return false; } @@ -822,7 +825,7 @@ static bool restoreDerivedKeysFromSecret() { return false; } gData.secretReady = true; - gData.walletAddress = bytesToBase58(gDerivedKeys.devicePub, 32); + gData.walletAddress = bytesToBase58(gDerivedKeys.clientPub, 32); return true; } @@ -835,7 +838,7 @@ static bool deriveFreshSecretAndWallet() { return false; } gData.secret = bytesToBase58(secret, sizeof(secret)); - gData.walletAddress = bytesToBase58(gDerivedKeys.devicePub, 32); + gData.walletAddress = bytesToBase58(gDerivedKeys.clientPub, 32); gData.secretReady = true; return true; } @@ -889,7 +892,7 @@ static std::vector buildUnsignedCreateRecord( const String &blockchainName, const String &serverAddress, const uint8_t rootPub[32], - const uint8_t devicePub[32], + const uint8_t clientPub[32], const uint8_t blockchainPub[32], const uint8_t lastBlockSignature[64], uint64_t createdAtMs) { @@ -911,9 +914,9 @@ static std::vector buildUnsignedCreateRecord( out.push_back(0); pushFixed(out, rootPub, 32); - out.push_back(kBlockTypeDeviceKey); + out.push_back(kBlockTypeClientKey); out.push_back(0); - pushFixed(out, devicePub, 32); + pushFixed(out, clientPub, 32); out.push_back(kBlockTypeBlockchainRegistry); out.push_back(0); @@ -960,7 +963,7 @@ static std::vector buildCreateInstructionData( const String &blockchainName, const String &serverAddress, const uint8_t rootPub[32], - const uint8_t devicePub[32], + const uint8_t clientPub[32], const uint8_t blockchainPub[32], const uint8_t lastBlockSignature[64], const uint8_t rootSignature[64], @@ -972,7 +975,7 @@ static std::vector buildCreateInstructionData( pushFixed(out, rootPub, 32); pushU64LE(out, createdAtMs); pushU64LE(out, 0); - pushFixed(out, devicePub, 32); + pushFixed(out, clientPub, 32); pushFixed(out, blockchainPub, 32); pushStrU8(out, blockchainName); pushU64LE(out, 0); @@ -1079,7 +1082,7 @@ static bool pdaAlreadyExists(const String &login, String &pdaAddress, String &me static std::vector buildLegacyMessage( const uint8_t recentBlockhash[32], - const uint8_t devicePub[32], + const uint8_t clientPub[32], const uint8_t userPda[32], const uint8_t inflowVault[32], const uint8_t economyConfig[32], @@ -1098,7 +1101,7 @@ static std::vector buildLegacyMessage( base58ToFixed32(kShineLoginGuardProgramId, loginGuardProgram); std::vector> accountKeys; - accountKeys.emplace_back(devicePub, devicePub + 32); + accountKeys.emplace_back(clientPub, clientPub + 32); accountKeys.emplace_back(userPda, userPda + 32); accountKeys.emplace_back(inflowVault, inflowVault + 32); accountKeys.emplace_back(systemProgram, systemProgram + 32); @@ -1244,7 +1247,7 @@ static bool registerHomeserverOnSolana(String &messageOut) { uint64_t createdAtMs = (uint64_t)millis() + 1704067200000ULL; std::vector unsignedRecord = buildUnsignedCreateRecord( gData.login, blockchainName, gData.wsUrl, - gDerivedKeys.rootPub, gDerivedKeys.devicePub, gDerivedKeys.blockchainPub, + gDerivedKeys.rootPub, gDerivedKeys.clientPub, gDerivedKeys.blockchainPub, lastBlockSignature, createdAtMs); uint8_t unsignedHash[32]; uint8_t rootSignature[64]; @@ -1256,7 +1259,7 @@ static bool registerHomeserverOnSolana(String &messageOut) { std::vector createData = buildCreateInstructionData( gData.login, blockchainName, gData.wsUrl, - gDerivedKeys.rootPub, gDerivedKeys.devicePub, gDerivedKeys.blockchainPub, + gDerivedKeys.rootPub, gDerivedKeys.clientPub, gDerivedKeys.blockchainPub, lastBlockSignature, rootSignature, createdAtMs); std::vector edRootData = buildEd25519InstructionData(rootSignature, gDerivedKeys.rootPub, unsignedHash); std::vector edBchData = buildEd25519InstructionData(lastBlockSignature, gDerivedKeys.blockchainPub, lastBlockHash); @@ -1269,7 +1272,7 @@ static bool registerHomeserverOnSolana(String &messageOut) { std::vector message = buildLegacyMessage( recentBlockhash, - gDerivedKeys.devicePub, + gDerivedKeys.clientPub, userPda, inflowVault, economyConfig, @@ -1277,7 +1280,7 @@ static bool registerHomeserverOnSolana(String &messageOut) { edBchData, createData); uint8_t txSignature[64]; - if (!signMessageEd25519(message, gDerivedKeys.deviceSk, txSignature)) { + if (!signMessageEd25519(message, gDerivedKeys.clientSk, txSignature)) { messageOut = "Не удалось подписать Solana-транзакцию"; return false; } @@ -2107,7 +2110,7 @@ static void drawConfirmScreen() { String text = "Выполнить действие?"; if (gConfirmTarget == CONFIRM_REGISTER) { title = "Регистрация"; - text = "Отправить create_user_pda в Solana через device key этого устройства?"; + text = "Отправить create_user_pda в Solana через client key этого устройства?"; } else if (gConfirmTarget == CONFIRM_CLEAR_ACCOUNT) { title = "Очистка"; text = "Удалить секрет, кошелёк и статус регистрации?"; diff --git a/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/reference/shine_homeserver_ui_nav_minimal_spec.md b/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/reference/shine_homeserver_ui_nav_minimal_spec.md index 9ca9e84..a523ea6 100644 --- a/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/reference/shine_homeserver_ui_nav_minimal_spec.md +++ b/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/reference/shine_homeserver_ui_nav_minimal_spec.md @@ -70,11 +70,11 @@ Фоновая логика: - пока открыт `HOME`, экран сам обновляется примерно раз в секунду; - при наличии `login + secret + homeserver` и Wi-Fi устройство читает Solana `user_pda` напрямую через RPC; -- сравниваются `root key`, `blockchain key`, `device key` и `homeserver` session-запись типа `100`; +- сравниваются `root key`, `blockchain key`, `client key` и `homeserver` session-запись типа `100`; - для строки `SHiNE:` устройство держит отдельную WebSocket-сессию с сервером SHiNE: - авторизация через `AuthChallenge/CreateAuthSession` или `SessionChallenge/SessionLogin`; - session key = публичный `homeserver key`; - - подтверждение создания сессии подписывается `device key`; + - подтверждение создания сессии подписывается `client key`; - heartbeat выполняется `Ping` раз в минуту. ## SETTINGS_MENU @@ -150,12 +150,12 @@ - статусное сообщение; - текущий `Solana RPC` адрес; - кнопку `SOLANA RPC`; -- текущий `Shine server` адрес; -- кнопку `SHINE SERVER`. +- текущий `SHiNE server login` или уже резолвленный адрес; +- кнопку `SHiNE SERVER LOGIN`, если обычный `user PDA` ещё не зарегистрирован. Значения по умолчанию: - Solana RPC: `https://api.devnet.solana.com` -- Shine server: `https://shineup.me` +- SHiNE server login: `shineupme` Нажатие на любую из двух кнопок открывает `TEXT_EDIT_SCREEN`. @@ -214,8 +214,8 @@ - `Root key priv (base58)`; - `Blockchain key (base58)`; - `Blockchain key priv (base58)`; -- `Device key (base58)`; -- `Device key priv (base58)`; +- `Client key (base58)`; +- `Client key priv (base58)`; - `Homeserver key (base58)`; - `Homeserver key priv (base58)`; - для каждого поля показывается формула derivation; @@ -229,7 +229,7 @@ Используется для: - пароля Wi-Fi; - Solana RPC; -- Shine server. +- SHiNE server login. Показывает: - заголовок; @@ -291,7 +291,7 @@ Используется `Preferences` (NVS памяти ESP32): - `solana_rpc` -- `shine_server` +- `shine_server_login` ## Хранение аккаунта diff --git a/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/reference/shine_homeserver_ui_spec.md b/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/reference/shine_homeserver_ui_spec.md index eee3b39..5a310d9 100644 --- a/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/reference/shine_homeserver_ui_spec.md +++ b/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/reference/shine_homeserver_ui_spec.md @@ -65,12 +65,13 @@ - `user pda address`; - `registration signature`; - `balance`; -- `server api url`; -- `server rpc url`; -- `server ws url`; +- `server login` для первичной привязки; +- `resolved server api url` / `rpc url` / `ws url` после чтения PDA сервера; - флаги: `wifiReady`, `serversReady`, `secretReady`, `registered`, `online`. +Для первой регистрации обычного `user PDA` устройство берёт `createdAtMs` из NTP прямо перед отправкой транзакции в Solana. При последующих обновлениях `user PDA` устройство так же берёт актуальный `updatedAtMs` из NTP перед отправкой update-транзакции. Дальше в `user PDA` сохраняется `accessServers`, где по умолчанию лежит `shineupme`. + ## Правило серверной сессии SHiNE При подключении к серверу `SHiNE` устройство должно авторизовываться как homeserver-сеанс: @@ -86,7 +87,7 @@ Кнопка регистрации доступна только если одновременно выполнены условия: 1. настроен и подтверждён `Wi-Fi`; -2. заполнены и подтверждены серверные адреса; +2. задан и подтверждён `SHiNE server login`; 3. задан логин; 4. сгенерирован или введён секрет; 5. баланс кошелька не меньше `0.20 SOL`; @@ -178,7 +179,7 @@ - вместо старых двух кнопок `баланс` и `QR` показывается одна широкая кнопка; - текст кнопки: `Wallet: `; - доступные имена: - - `DeviceKey` + - `ClientKey` - `RootKey` - либо сохранённое имя `custom`-кошелька; - после старта устройства баланс активного выбранного кошелька пытается загрузиться автоматически, если уже есть секрет и `Wi-Fi`; @@ -429,7 +430,7 @@ - верхняя кнопка открывает экран выбора кошелька `WALLET_SELECT`; - `Показать баланс кошелька` читает реальный баланс именно активного выбранного кошелька из `Solana RPC`; - `Показать QR-код кошелька` открывает экран `WALLET_QR`; -- этот экран не меняет логику Solana-регистрации пользователя: on-chain регистрация и проверки `PDA` по-прежнему завязаны на `device key`. +- этот экран не меняет логику Solana-регистрации пользователя: on-chain регистрация и проверки `PDA` по-прежнему завязаны на `client key`. ## Экран WALLET_SELECT @@ -437,14 +438,14 @@ - строку `Current: `; - три кнопки выбора: - - `DeviceKey` + - `ClientKey` - `RootKey` - `Custom` или `Custom: <имя>`; - у текущего выбора видна галочка. Поведение: -- `DeviceKey` активирует кошелёк, выведенный из suffix `dev.key`; +- `ClientKey` активирует кошелёк, выведенный из suffix `client.key`; - `RootKey` активирует кошелёк, выведенный из suffix `root.key`; - `Custom` использует derivation: `sha256(base64(secret32) + "|wallet." + customName)`; @@ -533,7 +534,7 @@ - строку `Session: `; - строку `Kind: Client session` или `Kind: Wallet session`; - пояснение: - - для client session: `Only device key will be transferred. No additional keys will be sent.` + - для client session: `Only client key will be transferred. No additional keys will be sent.` - для wallet session: `No keys will be transferred.` Кнопки: @@ -544,7 +545,7 @@ Поведение: - `YES` подтверждает заявку: - - для client session устройство передаёт только `device key`; + - для client session устройство передаёт только `client key`; - для wallet session устройство выпускает отдельную `wallet-session` без передачи ключей; - `NO` отклоняет заявку; - после любого решения устройство возвращается в список `REQUESTS` и обновляет его; @@ -628,7 +629,7 @@ 2. открыть `Подключение -> Wi-Fi`; 3. ввести `SSID` и пароль, нажать `Проверить`; 4. открыть `Подключение -> Серверы`; -5. проверить или задать серверные адреса; +5. проверить или задать `SHiNE server login` (по умолчанию `shineupme`); 6. открыть `Аккаунт`; 7. ввести логин; 8. задать имя homeserver; @@ -637,14 +638,15 @@ 11. при необходимости пополнить баланс; 12. вернуться на `HOME`; 13. нажать `REGISTER ACCOUNT`; -14. на экране проверки ещё раз увидеть `login`, статус свободного `PDA`, баланс, `homeserver1` и при необходимости сообщение о неподключённом `Wi-Fi`; +14. на экране проверки ещё раз увидеть `login`, статус свободного `PDA`, баланс, `homeserver1`, серверный login и при необходимости сообщение о неподключённом `Wi-Fi`; 15. нажать `ЗАРЕГИСТРИРОВАТЬ В СИЯНИИ`; 16. после завершения увидеть либо экран успеха с `user_pda` и `tx signature`, либо подробную ошибку; 17. после успешной регистрации увидеть статус `Homeserver активен`. Примечание: -- устройство реально отправляет `create_user_pda` в `shine_users`, а после подтверждения сохраняет `PDA` и `tx signature`. +- устройство реально отправляет `create_user_pda` в `shine_users`, а после подтверждения сохраняет `PDA` и `tx signature`; +- при первой регистрации для обычного `user PDA` не заполняется `serverAddress`, а `accessServers` получает `shineupme` или другой выбранный `SHiNE server login`. ## Сценарий входящего запроса diff --git a/SHiNE-browser-plugin-wallet/README.md b/SHiNE-browser-plugin-wallet/README.md index eed308b..209dc4a 100644 --- a/SHiNE-browser-plugin-wallet/README.md +++ b/SHiNE-browser-plugin-wallet/README.md @@ -7,7 +7,7 @@ Chrome-compatible Manifest V3 plugin for SHiNE wallet-session login. - создать `wallet-session` через `StartTrustedDeviceLogin`; - показать код подключения; - дождаться подтверждения на доверенном устройстве; -- принять `session-only` payload без передачи `deviceKey/rootKey/blockchainKey`; +- принять `session-only` payload без передачи `clientKey/rootKey/blockchainKey`; - сохранить `sessionPriv/sessionKey/sessionId` в локальном хранилище plugin; - восстанавливать session через `SessionChallenge -> SessionLogin`; - держать wallet-state в `background service worker`, а side panel использовать как UI. diff --git a/SHiNE-browser-plugin-wallet/background.js b/SHiNE-browser-plugin-wallet/background.js index 18e6d16..e54cb75 100644 --- a/SHiNE-browser-plugin-wallet/background.js +++ b/SHiNE-browser-plugin-wallet/background.js @@ -30,6 +30,9 @@ const state = { devicesResolvedAtMs: 0, }, currentWallet: null, + pendingApprovals: [], + siteApprovalChain: Promise.resolve(), + sessionAttachInProgress: false, statusText: '', statusKind: 'info', }; @@ -69,6 +72,55 @@ function setStatus(message = '', kind = 'info') { state.statusKind = kind === 'error' ? 'error' : 'info'; } +function makePendingApprovalSnapshot(payload = {}) { + const pendingId = String(payload?.id || '').trim(); + const pendingApprovals = Array.isArray(state.pendingApprovals) ? state.pendingApprovals : []; + const queueIndex = pendingApprovals.findIndex((item) => String(item?.id || '').trim() === pendingId); + const queueLength = pendingApprovals.length; + return { + id: pendingId, + kind: String(payload?.kind || 'sign_transaction').trim() || 'sign_transaction', + origin: String(payload?.origin || '').trim(), + publicKeyBase58: String(payload?.publicKeyBase58 || '').trim(), + comment: String(payload?.comment || '').trim(), + createdAtMs: Number(payload?.createdAtMs || Date.now()), + status: String(payload?.status || 'queued').trim() || 'queued', + queuePosition: queueIndex >= 0 ? queueIndex + 1 : 1, + queueLength: queueLength || 1, + transactionSummary: payload?.transactionSummary && typeof payload.transactionSummary === 'object' + ? { ...payload.transactionSummary } + : null, + }; +} + +function getCurrentPendingApproval() { + return Array.isArray(state.pendingApprovals) && state.pendingApprovals.length + ? state.pendingApprovals[0] + : null; +} + +function removePendingApproval(pendingId, { rejectError = null } = {}) { + const pendingApprovals = Array.isArray(state.pendingApprovals) ? state.pendingApprovals : []; + const index = pendingApprovals.findIndex((item) => String(item?.id || '').trim() === String(pendingId || '').trim()); + if (index < 0) return; + const [pending] = pendingApprovals.splice(index, 1); + if (pending.timeoutId) { + clearTimeout(pending.timeoutId); + } + if (rejectError && pending.abortController) { + try { + pending.abortController.abort(rejectError); + } catch {} + } +} + +async function openSidePanelForSender(sender) { + if (!chrome.sidePanel?.open || !sender?.tab?.id) return; + try { + await chrome.sidePanel.open({ tabId: sender.tab.id }); + } catch {} +} + function stopPoll() { if (state.pollTimer) { clearTimeout(state.pollTimer); @@ -103,7 +155,10 @@ async function loadStateFromStorage() { login: String(settings?.login || '').trim(), connectedOrigins: Array.isArray(settings?.connectedOrigins) ? settings.connectedOrigins.map((item) => normalizeOrigin(item)).filter(Boolean) : [], }; - state.activeSession = await loadSessionMaterial(); + const storedSession = await loadSessionMaterial(); + if (storedSession || !state.sessionAttachInProgress) { + state.activeSession = storedSession; + } state.walletProfile = state.activeSession?.walletProfile || null; state.signing = { selectedDeviceName: String(state.activeSession?.selectedDeviceName || ''), @@ -317,18 +372,30 @@ async function attachApprovedSession(payload) { throw new Error('Получен неполный session-only payload'); } - await clearSessionMaterial(); - state.activeSession = sessionRecord; - await hydrateWalletProfile(login); - await saveActiveSessionRecord(); - await persistSettings({ - login: sessionRecord.login, - serverLogin: sessionRecord.serverLogin, - serverHttp: sessionRecord.serverHttp, - serverUrl: sessionRecord.serverUrl, - }); - state.connectionOnline = false; - state.currentWallet = null; + state.sessionAttachInProgress = true; + try { + state.activeSession = sessionRecord; + state.walletProfile = null; + state.currentWallet = null; + state.signing = { + ...state.signing, + selectedDeviceName: '', + devicesResolvedAtMs: 0, + }; + await saveActiveSessionRecord(); + await hydrateWalletProfile(login); + await saveActiveSessionRecord(); + await persistSettings({ + login: sessionRecord.login, + serverLogin: sessionRecord.serverLogin, + serverHttp: sessionRecord.serverHttp, + serverUrl: sessionRecord.serverUrl, + }); + state.connectionOnline = false; + state.currentWallet = null; + } finally { + state.sessionAttachInProgress = false; + } } async function pollPairingStatus() { @@ -506,7 +573,7 @@ async function resolveSelectedHomeserverSession() { return selectedDevice; } -async function callWalletRpc(requestData, timeoutMs = 8000) { +async function callWalletRpc(requestData, timeoutMs = 8000, abortSignal = null) { const selectedDevice = await resolveSelectedHomeserverSession(); const resumed = await resumeActiveSession({ keepConnected: true }); if (!resumed.ok) { @@ -523,22 +590,46 @@ async function callWalletRpc(requestData, timeoutMs = 8000) { try { const response = await new Promise((resolve, reject) => { - const timeoutId = setTimeout(() => { + let settled = false; + let timeoutId = 0; + let off = () => {}; + let removeAbortListener = () => {}; + const cleanup = () => { + if (settled) return; + settled = true; + if (timeoutId) clearTimeout(timeoutId); off(); + removeAbortListener(); + }; + timeoutId = setTimeout(() => { + cleanup(); reject(new Error('Таймаут ответа от ESP32.')); }, timeoutMs); - const off = ensureApi().onEvent('IncomingCallSignal', (evt) => { + off = ensureApi().onEvent('IncomingCallSignal', (evt) => { const eventPayload = evt?.payload || {}; if (String(eventPayload?.callId || '') !== callId) return; if (Number(eventPayload?.type || 0) !== WALLET_RPC_RESPONSE_TYPE) return; - clearTimeout(timeoutId); - off(); + cleanup(); try { resolve(JSON.parse(String(eventPayload?.data || '{}'))); } catch { reject(new Error('ESP32 вернул некорректный JSON.')); } }); + if (abortSignal) { + const onAbort = () => { + cleanup(); + reject(abortSignal.reason instanceof Error ? abortSignal.reason : new Error('Ожидание подписи отменено.')); + }; + if (abortSignal.aborted) { + onAbort(); + return; + } + abortSignal.addEventListener('abort', onAbort, { once: true }); + removeAbortListener = () => { + abortSignal.removeEventListener('abort', onAbort); + }; + } ensureApi().callSignalToSession({ toLogin: state.activeSession.login, targetSessionId: selectedDevice.activeSessionId, @@ -546,8 +637,7 @@ async function callWalletRpc(requestData, timeoutMs = 8000) { type: WALLET_RPC_REQUEST_TYPE, data: JSON.stringify(payload), }).catch((error) => { - clearTimeout(timeoutId); - off(); + cleanup(); reject(error); }); }); @@ -563,11 +653,13 @@ function verifyWalletAgainstPda(wallet) { const type = String(wallet?.type || '').trim(); const pub = String(wallet?.publicKeyBase58 || '').trim(); const rootKey = String(state.walletProfile?.publicKeys?.rootKeyBase58 || '').trim(); - const deviceKey = String(state.walletProfile?.publicKeys?.deviceKeyBase58 || '').trim(); - if (type === 'dev.key') { + const clientKey = String( + state.walletProfile?.publicKeys?.clientKeyBase58 || '', + ).trim(); + if (type === 'client.key') { return { - verified: !!deviceKey && deviceKey === pub, - verificationText: deviceKey === pub ? 'Совпадает с deviceKey из PDA.' : 'Не совпадает с deviceKey из PDA.', + verified: !!clientKey && clientKey === pub, + verificationText: clientKey === pub ? 'Совпадает с clientKey из PDA.' : 'Не совпадает с clientKey из PDA.', }; } if (type === 'root.key') { @@ -604,6 +696,60 @@ async function requestCurrentWallet() { return { ok: true, wallet: state.currentWallet }; } +async function cancelPendingSiteApproval() { + const pending = getCurrentPendingApproval(); + if (!pending) { + setStatus('Сейчас нет активного ожидания подписи.', 'info'); + return { ok: true }; + } + removePendingApproval(pending.id, { + rejectError: makeCodeError('User canceled request in extension.', 'USER_REJECTED'), + }); + setStatus('Ожидание подписи отменено в расширении.', 'info'); + return { ok: true }; +} + +async function markPendingSiteApprovalResolved(pendingId) { + removePendingApproval(pendingId); +} + +function enqueueSiteApproval(work) { + const run = state.siteApprovalChain.then(work, work); + state.siteApprovalChain = run.catch(() => {}); + return run; +} + +async function activatePendingApproval(pending, sender = null) { + const abortController = new AbortController(); + pending.status = 'active'; + pending.abortController = abortController; + pending.timeoutId = setTimeout(() => { + removePendingApproval(pending.id, { + rejectError: makeCodeError('Signing request timed out in extension.', 'USER_REJECTED'), + }); + setStatus('Ожидание подписи истекло в расширении.', 'error'); + }, 120000); + setStatus(`Сайт ${pending.origin} запросил подпись. Подтвердите или отмените на доверенном устройстве.`, 'info'); + await openSidePanelForSender(sender); + return pending; +} + +function beginSiteTransactionFlow(payload = {}) { + const pending = makePendingApprovalSnapshot({ + ...payload, + kind: 'sign_transaction', + id: `${Date.now()}-${Math.random().toString(16).slice(2, 8)}`, + createdAtMs: Date.now(), + status: 'queued', + }); + state.pendingApprovals.push({ + ...pending, + timeoutId: 0, + abortController: null, + }); + return pending; +} + async function siteConnect({ origin, onlyIfTrusted = false } = {}) { const normalizedOrigin = normalizeOrigin(origin); if (!normalizedOrigin) { @@ -634,7 +780,7 @@ async function siteDisconnect({ origin } = {}) { return { ok: true }; } -async function siteSignTransaction({ origin, publicKeyBase58, transactionBase64, comment } = {}) { +async function siteSignTransaction({ origin, publicKeyBase58, transactionBase64, comment, transactionSummary } = {}, sender = null) { const normalizedOrigin = normalizeOrigin(origin); if (!normalizedOrigin) { throw makeCodeError('Site origin is missing.', 'BAD_ORIGIN'); @@ -647,29 +793,44 @@ async function siteSignTransaction({ origin, publicKeyBase58, transactionBase64, if (!cleanPub || !cleanTx) { throw makeCodeError('Transaction payload is incomplete.', 'BAD_REQUEST'); } - const requestId = `${Date.now()}-${Math.random().toString(16).slice(2, 8)}`; - const signComment = String(comment || '').trim() || `Site ${normalizedOrigin} requested transaction signature`; - const { response } = await callWalletRpc({ - v: 1, - operation: 'sign_transaction', - requestId, + const pending = beginSiteTransactionFlow({ + origin: normalizedOrigin, publicKeyBase58: cleanPub, - transactionBase64: cleanTx, - comment: signComment, - }, 120000); - if (!response?.ok) { - const errorCode = String(response?.error || 'unknown_error').trim().toUpperCase(); - if (errorCode === 'REJECTED_BY_USER') { - throw makeCodeError('User rejected transaction signature on ESP32.', 'USER_REJECTED'); + comment: String(comment || '').trim(), + transactionSummary: transactionSummary || null, + }); + return enqueueSiteApproval(async () => { + await activatePendingApproval(getCurrentPendingApproval() || pending, sender); + const activePending = getCurrentPendingApproval() || pending; + const requestId = `${Date.now()}-${Math.random().toString(16).slice(2, 8)}`; + const signComment = String(comment || '').trim() || `Site ${normalizedOrigin} requested transaction signature`; + try { + const { response } = await callWalletRpc({ + v: 1, + operation: 'sign_transaction', + requestId, + publicKeyBase58: cleanPub, + transactionBase64: cleanTx, + comment: signComment, + }, 120000, activePending.abortController?.signal || null); + if (!response?.ok) { + const errorCode = String(response?.error || 'unknown_error').trim().toUpperCase(); + if (errorCode === 'REJECTED_BY_USER') { + throw makeCodeError('User rejected transaction signature on ESP32.', 'USER_REJECTED'); + } + throw makeCodeError(`ESP32 rejected transaction signature: ${String(response?.error || 'unknown_error')}`, errorCode || 'RPC_REJECTED'); + } + setStatus(`Подпись для ${normalizedOrigin} завершена.`, 'info'); + return { + ok: true, + publicKeyBase58: String(response?.publicKeyBase58 || cleanPub).trim(), + signedTransactionBase64: String(response?.signedTransactionBase64 || '').trim(), + signatureBase58: String(response?.signatureBase58 || '').trim(), + }; + } finally { + await markPendingSiteApprovalResolved(activePending.id); } - throw makeCodeError(`ESP32 rejected transaction signature: ${String(response?.error || 'unknown_error')}`, errorCode || 'RPC_REJECTED'); - } - return { - ok: true, - publicKeyBase58: String(response?.publicKeyBase58 || cleanPub).trim(), - signedTransactionBase64: String(response?.signedTransactionBase64 || '').trim(), - signatureBase58: String(response?.signatureBase58 || '').trim(), - }; + }); } function snapshot() { @@ -686,6 +847,7 @@ function snapshot() { connectionOnline: !!state.activeSession, walletProfile: state.walletProfile ? { ...state.walletProfile } : null, currentWallet: state.currentWallet ? { ...state.currentWallet } : null, + pendingApproval: getCurrentPendingApproval() ? makePendingApprovalSnapshot(getCurrentPendingApproval()) : null, signing: { ...state.signing }, status: { text: state.statusText, @@ -747,6 +909,11 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => { sendResponse({ ok: true, result, state: snapshot() }); return; } + if (type === 'wallet:cancelPendingSiteApproval') { + const result = await cancelPendingSiteApproval(); + sendResponse({ ok: true, result, state: snapshot() }); + return; + } if (type === 'wallet:siteConnect') { const result = await siteConnect(message?.payload || {}); sendResponse({ ok: true, result, state: snapshot() }); @@ -758,7 +925,7 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => { return; } if (type === 'wallet:siteSignTransaction') { - const result = await siteSignTransaction(message?.payload || {}); + const result = await siteSignTransaction(message?.payload || {}, _sender); sendResponse({ ok: true, result, state: snapshot() }); return; } diff --git a/SHiNE-browser-plugin-wallet/content-script.js b/SHiNE-browser-plugin-wallet/content-script.js index c2ca1f1..8fa7d79 100644 --- a/SHiNE-browser-plugin-wallet/content-script.js +++ b/SHiNE-browser-plugin-wallet/content-script.js @@ -1,5 +1,6 @@ const PAGE_REQUEST = 'shine-wallet-page-request'; const PAGE_RESPONSE = 'shine-wallet-page-response'; +const PAGE_MESSAGE_TARGET_ORIGIN = '*'; function injectProviderBridge() { const root = document.head || document.documentElement; @@ -20,14 +21,21 @@ function respondToPage(id, ok, result, error, code) { result: result || null, error: error ? String(error) : '', code: code ? String(code) : '', - }, window.location.origin); + }, PAGE_MESSAGE_TARGET_ORIGIN); } function sendRuntimeMessage(type, payload = {}) { return new Promise((resolve, reject) => { chrome.runtime.sendMessage({ type, payload }, (response) => { if (chrome.runtime.lastError) { - reject(new Error(chrome.runtime.lastError.message || 'Runtime message failed')); + const raw = String(chrome.runtime.lastError.message || 'Runtime message failed'); + if (/Extension context invalidated/i.test(raw)) { + const error = new Error('Расширение было перезагружено или отключено. Обновите страницу и откройте кошелёк заново.'); + error.code = 'EXTENSION_CONTEXT_INVALIDATED'; + reject(error); + return; + } + reject(new Error(raw)); return; } if (!response?.ok) { @@ -71,6 +79,7 @@ window.addEventListener('message', (event) => { publicKeyBase58: String(params?.publicKeyBase58 || '').trim(), transactionBase64: String(params?.transactionBase64 || '').trim(), comment: String(params?.comment || '').trim(), + transactionSummary: params?.transactionSummary || null, }); respondToPage(id, true, response.result || null); return; diff --git a/SHiNE-browser-plugin-wallet/js/lib/shine-server-resolver.js b/SHiNE-browser-plugin-wallet/js/lib/shine-server-resolver.js index e6fda2e..284363f 100644 --- a/SHiNE-browser-plugin-wallet/js/lib/shine-server-resolver.js +++ b/SHiNE-browser-plugin-wallet/js/lib/shine-server-resolver.js @@ -2,7 +2,7 @@ import { base64ToBytes } from './crypto-utils.js'; import { PublicKey } from './vendor/solana-publickey-bundle.js'; const SOLANA_ENDPOINT_DEFAULT = 'https://api.devnet.solana.com'; -const SHINE_USERS_PROGRAM_ID = 'FZS1YctoeEhCkZ5VTjsysUFAXR8CqxYztcLboXcg2Rpm'; +const SHINE_USERS_PROGRAM_ID = '3bYrnXwLc56oVPUBAjY8zTMLwHCYq29b5rUMu3b64SQJ'; const SHINE_USERS_USER_PDA_SEED_PREFIX = 'user_login='; const DEFAULT_SHINE_SERVER_LOGIN = 'shineupme'; const DEFAULT_SHINE_SERVER_ADDRESS = 'shineup.me'; @@ -69,8 +69,9 @@ function parseServerFieldsFromUserPda(dataBytes) { let isServer = false; let serverAddress = ''; let accessServers = []; + let recoveryKey32 = null; let rootKey32 = null; - let deviceKey32 = null; + let clientKey32 = null; let blockchainKey32 = null; let blockchainName = ''; let homeserverSessions = []; @@ -79,10 +80,11 @@ function parseServerFieldsFromUserPda(dataBytes) { const blockType = readU8(bytes, cursorRef); cursorRef.value += 1; // block_version - if (blockType === 1 || blockType === 2) { + if (blockType === 0 || blockType === 1 || blockType === 2) { const key32 = readBytes(bytes, cursorRef, 32); + if (blockType === 0) recoveryKey32 = key32; if (blockType === 1) rootKey32 = key32; - if (blockType === 2) deviceKey32 = key32; + if (blockType === 2) clientKey32 = key32; continue; } if (blockType === 3) { @@ -150,8 +152,9 @@ function parseServerFieldsFromUserPda(dataBytes) { serverAddress: normalizeHostLike(serverAddress), accessServers: accessServers.map((value) => normalizeServerLogin(value)).filter(Boolean), publicKeys: { + recoveryKeyBase58: recoveryKey32 ? new PublicKey(recoveryKey32).toBase58() : '', rootKeyBase58: rootKey32 ? new PublicKey(rootKey32).toBase58() : '', - deviceKeyBase58: deviceKey32 ? new PublicKey(deviceKey32).toBase58() : '', + clientKeyBase58: clientKey32 ? new PublicKey(clientKey32).toBase58() : '', blockchainKeyBase58: blockchainKey32 ? new PublicKey(blockchainKey32).toBase58() : '', blockchainName, }, diff --git a/SHiNE-browser-plugin-wallet/popup.css b/SHiNE-browser-plugin-wallet/popup.css index fe1e0f5..e1b677a 100644 --- a/SHiNE-browser-plugin-wallet/popup.css +++ b/SHiNE-browser-plugin-wallet/popup.css @@ -198,6 +198,12 @@ select { gap: 8px; } +.detail-list { + display: flex; + flex-direction: column; + gap: 8px; +} + .device-row { padding: 8px 10px; border: 1px solid #243446; @@ -205,6 +211,30 @@ select { background: #0d141d; } +.detail-row { + display: grid; + gap: 4px; + padding: 8px 10px; + border: 1px solid #243446; + border-radius: 8px; + background: #0d141d; +} + +.detail-label { + font-size: 12px; + color: #9aabbd; +} + +.detail-value { + color: #e8eef6; + word-break: break-word; +} + +.detail-value.mono { + font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; + color: #bed5f5; +} + .device-state { font-size: 12px; text-transform: lowercase; diff --git a/SHiNE-browser-plugin-wallet/popup.html b/SHiNE-browser-plugin-wallet/popup.html index 2e37ae4..41cf9e4 100644 --- a/SHiNE-browser-plugin-wallet/popup.html +++ b/SHiNE-browser-plugin-wallet/popup.html @@ -18,7 +18,6 @@

Сервер SHiNE: —

-

Адрес: —

Подключение
@@ -50,9 +49,6 @@ + +