diff --git a/AGENTS.md b/AGENTS.md index c1bc31e..8bb574b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -24,12 +24,12 @@ - Подробные служебные правила Telegram-обработчика, его очередь, история, systemd-запуск и особенности ответов описывать в `SHiNE-agent-bot-coder/AGENT.md`. - Если в сообщениях пользователя встречается «агент MD» или похожая формулировка про файл инструкций Codex, считать, что имеется в виду автоматически читаемый `AGENTS.md`. -## ESP32 UI сабсервера +## ESP32 UI homeserver - Для UI-скетча устройства `ESP32-S3-Touch-AMOLED-2.16` документ-спецификация и Arduino-скетч должны всегда оставаться синхронными. - Актуальный документ по экранной логике, состояниям, кнопкам, полям, статусам и ограничениям UI считать источником истины для скетча. - При изменении документа обязательно в том же наборе изменений приводить в соответствие скетч. - При изменении скетча обязательно в том же наборе изменений обновлять документ, если поменялись экраны, тексты, переходы, статусы, кнопки, поля или поведение. -- Для нового ESP32 UI-прототипа сабсервера использовать русский язык как основной и отдельно следить, чтобы текст реально отображался на устройстве, а не только логически присутствовал в коде. +- Для нового ESP32 UI-прототипа homeserver использовать русский язык как основной и отдельно следить, чтобы текст реально отображался на устройстве, а не только логически присутствовал в коде. ## Solana-модуль - В проекте есть отдельный Solana/Anchor-модуль в папке `shine-solana/shine/`. diff --git a/DAO_запуск/README.md b/DAO_запуск/README.md index c739f28..ac7fd93 100644 --- a/DAO_запуск/README.md +++ b/DAO_запуск/README.md @@ -155,11 +155,11 @@ - это обязательный шаг перед переходом от "собрали" к "доверяем". -### 3. Устройство на ESP32 как сабсервер с ключами +### 3. Устройство на ESP32 как homeserver с ключами Что сделать: -- дописать прошивку, чтобы устройство могло выступать сабсервером с ключами; +- дописать прошивку, чтобы устройство могло выступать homeserver с ключами; - дать ему возможность регистрироваться и подключаться к серверу; - определить, какие операции устройство подписывает и где хранит ключевой материал. diff --git a/Dev_Docs/Future_Features/README.md b/Dev_Docs/Future_Features/README.md index e81b0e3..f841e89 100644 --- a/Dev_Docs/Future_Features/README.md +++ b/Dev_Docs/Future_Features/README.md @@ -37,7 +37,7 @@ - `medium/2026-05-25_1106_shine_balance_wallet.md` - кошелёк и пополнение баланса сияния через блокчейн. - `medium/2026-05-26_0029_esp32s3_file_storage.md` - ESP32S3 как личное файловое хранилище SHiNE для файлов переписок и вложений. - `medium/2026-06-03_подключение_других_устройств_через_qr.md` - довести подключение других устройств через QR: сейчас заготовка есть, но сценарий работает нестабильно и его нужно будет отдельно доделать. -- `medium/2026-06-02_сессионные_саб_серверы_в_pda.md` - несколько саб-серверов пользователя как типизированные сессии в PDA с версией записи. +- `medium/2026-06-02_сессионные_homeserver_в_pda.md` - несколько homeserver-ов пользователя как типизированные сессии в PDA с версией записи. ### DAO-запуск diff --git a/Dev_Docs/Future_Features/medium/2026-06-02_сессионные_саб_серверы_в_pda.md b/Dev_Docs/Future_Features/medium/2026-06-02_сессионные_homeserver_в_pda.md similarity index 81% rename from Dev_Docs/Future_Features/medium/2026-06-02_сессионные_саб_серверы_в_pda.md rename to Dev_Docs/Future_Features/medium/2026-06-02_сессионные_homeserver_в_pda.md index 46bd937..b8f92d3 100644 --- a/Dev_Docs/Future_Features/medium/2026-06-02_сессионные_саб_серверы_в_pda.md +++ b/Dev_Docs/Future_Features/medium/2026-06-02_сессионные_homeserver_в_pda.md @@ -1,4 +1,4 @@ -# Сессионные саб-серверы в PDA пользователя +# Сессионные homeserver-ы в PDA пользователя - Статус: `future` @@ -10,15 +10,15 @@ после завершения первого этапа по пользовательским сессиям - Основание: - Идея зафиксирована после обсуждения архитектуры пользовательских сессий и внутренних саб-серверов. Сейчас задача сознательно отложена: сначала нужно аккуратно ввести базовую модель сессий, а затем возвращаться к расширенной серверной роли. + Идея зафиксирована после обсуждения архитектуры пользовательских сессий и внутренних homeserver-ов. Сейчас задача сознательно отложена: сначала нужно аккуратно ввести базовую модель сессий, а затем возвращаться к расширенной серверной роли. ## Зачем нужна фича -У одного пользователя может быть несколько доверенных внутренних саб-серверов, и каждый из них должен жить как отдельная пользовательская сессия, а не как отдельная особая сущность вне общей модели. +У одного пользователя может быть несколько доверенных внутренних homeserver-ов, и каждый из них должен жить как отдельная пользовательская сессия, а не как отдельная особая сущность вне общей модели. Это нужно, чтобы: -- хранить несколько саб-серверов у одного пользователя одновременно; +- хранить несколько homeserver-ов у одного пользователя одновременно; - различать обычные клиентские сессии и серверные сессии по явному типу; - дать расширяемый формат записи с версией; - использовать единый подход для DM, звонков и внутренних команд между сессиями. @@ -35,18 +35,18 @@ Предварительные значения: - тип `1` - обычная пользовательская сессия; -- тип `100` - саб-сервер пользователя; +- тип `100` - homeserver пользователя; - версия `1` - первая рабочая версия формата записи сессии. На текущем этапе под это уже зарезервирован отдельный блок `SessionsBlock` с `block_type = 55`, а `TrustedStateBlock` остаётся на `50`. -Важно: саб-серверов у одного пользователя может быть несколько. +Важно: homeserver-ов у одного пользователя может быть несколько. ## Архитектурный принцип Внутренний протокол взаимодействия должен оставаться транспортным. -То есть SHiNE-сервер не должен разбирать прикладной смысл внутренней нагрузки саб-сервера, а должен: +То есть SHiNE-сервер не должен разбирать прикладной смысл внутренней нагрузки homeserver-а, а должен: - доставлять сообщения между сессиями; - доставлять сигналы звонков между сессиями; @@ -60,7 +60,7 @@ - Вызов звонка уже рассылается по нескольким активным сессиям пользователя. - Сигналы звонка уже адресуются конкретной сессии, а stop-сигналы дублируются на остальные сессии того же пользователя. -Иными словами, текущая серверная логика ближе к модели "сервер доставляет между сессиями", чем к модели "сервер понимает внутренний протокол саб-сервера". +Иными словами, текущая серверная логика ближе к модели "сервер доставляет между сессиями", чем к модели "сервер понимает внутренний протокол homeserver-а". ## Что нужно сделать при возврате к задаче @@ -77,7 +77,7 @@ - правила удаления и обновления записи; - правила ротации `sessionPubKey`. 6. Продумать, как UI и сервер будут отличать тип `1` и тип `100`. -7. Определить, какие внутренние сообщения саб-сервера останутся полностью прозрачными для SHiNE-сервера, а какие потребуют только технической маршрутизации. +7. Определить, какие внутренние сообщения homeserver-а останутся полностью прозрачными для SHiNE-сервера, а какие потребуют только технической маршрутизации. 8. Добавить API/операции чтения и обновления списка сессий, если для этого не хватит существующих механизмов. 9. После реализации обязательно обновить документацию. @@ -101,5 +101,5 @@ Продолжать после завершения первой части: 1. описать минимальный формат записи пользовательской сессии; -2. отдельно решить, живут ли саб-серверы в том же списке, что и обычные сессии; +2. отдельно решить, живут ли homeserver-ы в том же списке, что и обычные сессии; 3. затем уже проектировать операции регистрации, обновления и отключения таких сессий. diff --git a/Dev_Docs/Keys/DERIVATION.md b/Dev_Docs/Keys/DERIVATION.md new file mode 100644 index 0000000..3b66907 --- /dev/null +++ b/Dev_Docs/Keys/DERIVATION.md @@ -0,0 +1,154 @@ +# Деривация секрета и ключей SHiNE (формулы) + +> **Статус: ИСТОЧНИК ИСТИНЫ (single source of truth) по конкретной деривации.** +> Этот файл описывает, как из пароля получается секрет и как из секрета выводятся +> все ключи (root, blockchain, device/Solana, homeserver) — формулами, байт-в-байт. +> Если в коде меняется деривация (формула секрета, параметры Argon2id, соль, формула +> ключа, разделитель `|`, набор/имена суффиксов, формат homeserver-ключа, связь +> dev-ключ ↔ Solana-адрес) — **в том же изменении обязательно править этот документ**. +> Роли и назначение ключей описаны отдельно в `Dev_Docs/Keys/README.md` (архитектура). +> Здесь — только механика. Документ намеренно краткий. + +--- + +## 1. Секрет (masterSecret) + +`masterSecret` — 32 байта. Два источника: + +**А. Из пароля пользователя (основной путь, UI).** + +``` +login = trim(lowercase(login)) +salt = SHA-256("shine-auth-v2|login=" + login + "|suffix=master.secret")[0..16) // первые 16 байт +material = utf8(login + "\n" + password) +masterSecret(32) = Argon2id(material, salt, t=2, m=65536 KiB, p=1, dkLen=32) +``` + +- Параметры Argon2id фиксированы: `t=2`, `m=65536` (64 МиБ), `p=1`, `dkLen=32`. +- Логин входит и в соль, и в начало `material` (склейка через `\n`). +- Пустой пароль **запрещён**: легаси-fallback без Argon2 удалён, `deriveMasterSecretFromPassword` бросает ошибку на пустом пароле, а форма регистрации в UI блокирует пустой пароль (`register-view.js`). + +**Б. Случайный (прошивка ESP32, новый аккаунт без пароля).** + +``` +masterSecret(32) = 32 случайных байта (esp_random) // хранится на устройстве как base58 +``` + +Дальше деривация ключей одинакова независимо от источника секрета. + +--- + +## 2. Производные ключи + +Все ключи выводятся из `masterSecret` по **одной формуле**, отличается только суффикс: + +``` +material = base64_std(masterSecret) + "|" + <суффикс> +seed(32) = SHA-256(material) +(pub, priv) = Ed25519_keypair_from_seed(seed) +``` + +- `base64_std` — стандартный base64 (не url-safe). +- Разделитель — символ `|`. +- Суффиксы значимы байт-в-байт (регистр и точки важны). + +| Ключ | Суффикс | Назначение (кратко) | +|------|---------|---------------------| +| root | `root.key` | Личность. Подписывает unsigned-часть PDA-записи (`RootKeyBlock`). | +| blockchain | `bch.key` | Подписывает `LastBlockState` персонального блокчейна (`blockchain_public_key`). | +| device / **Solana** | `dev.key` | Ключ устройства = Solana-ключ. Fee payer и подпись Solana-транзакций; адрес кошелька = `base58(devicePub)`. См. §3. | +| homeserver | `homeserver.key:<имя>` | Ключ homeserver-устройства, по одному на каждый homeserver (различитель — имя). См. §4. | + +Полные роли каждого ключа — в `Dev_Docs/Keys/README.md`. + +--- + +## 3. Solana-ключ + +Отдельного «солана-ключа» нет. На Solana работают два ключа: + +- **`dev.key` (device) — пополняемый кошелёк и fee payer.** Solana-адрес = `base58(devicePub)`. + Этим ключом оплачиваются и подписываются `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»). + +Кратко про роли на Solana: `root.key` — это **главный (master) ключ**: им управляют PDA-записью +(`create/update`) и через это можно заменить все остальные ключи; `dev.key` — это **пополняемый +кошелёк и плательщик комиссий**. Полное описание ролей — `Dev_Docs/Keys/README.md`. + +--- + +## 4. Ключи homeserver + +У пользователя может быть несколько homeserver-ов. Каждый имеет **своё имя** и **свой приватный ключ**, +выведенный из секрета по той же формуле с именованным суффиксом: + +``` +suffix = "homeserver.key:" + <имя homeserver> // имя по умолчанию: "homeserver1" +material = base64_std(masterSecret) + "|" + suffix +seed(32) = SHA-256(material) +(pub, priv) = Ed25519_keypair_from_seed(seed) +``` + +Пример для двух homeserver-ов: + +``` +homeserver.key:home-a -> ключ A +homeserver.key:home-b -> ключ B +``` + +Публичный ключ homeserver-а публикуется в `SessionsBlock` пользовательской PDA как +`session_pub_key` с `session_type = 100`, имя — в `session_name` (формат PDA §13). + +> Это переименование прежней схемы `subserver.key:<имя>` → `homeserver.key:<имя>`. +> Термин «саб-сервер» по проекту заменяется на «homeserver». + +--- + +## 5. Где это в коде + +### Деривация секрета и ключей (UI, каноническая) +- `shine-UI/js/services/crypto-utils.js` + - секрет из пароля: `makeArgon2Salt`, `deriveMasterSecretArgon2id`, `deriveMasterSecretFromPassword` (~129–218); + - ключ из секрета: `deriveEd25519FromMasterSecret` (~220). +- `shine-UI/js/services/auth-service.js` — набор root/bch/dev из `masterSecret` (~732–758). +- `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`) удалён. + +### Деривация ключей (прошивка ESP32) +- `ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/test-device/shine_homeserver_ui/shine_homeserver_ui.ino` + - `deriveKeysFromMasterSecret` (~782), `restoreDerivedKeysFromSecret` (~806), `deriveFreshSecretAndWallet` (~829); + - регистрация/подпись Solana: `registerHomeserverOnSolana` (~1182), `signMessageEd25519` (~1147). +- `ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/test-device/test_sketches/lvgl_nav_minimal_test/lvgl_nav_minimal_test.ino` + - homeserver-ключ: `homeserverKeySuffix` (~690), `deriveKeyPairFromSecretSuffix` (~699), `refreshDerivedKeys` (~725); суффикс `homeserver.key:<имя>`. + +### Формат 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. + +### Сервер (тестовый seed) +- `SHiNE-server/src/test/java/test/it/cases/SeedDataPopulationHelper.java` `deriveKeysFromPassword` (~246) — + выводит ключи как `Ed25519(SHA-256(base64(SHA-256(password)) + suffix))`, **без** Argon2 и **без** разделителя `|`. + Это **не баг**, а точное повторение легаси-пути UI `derivePasswordSeed` (для пустого пароля), у которого тоже нет `|`. + С современным путём `masterSecret`-bundle (Argon2 + `base64(secret)|suffix`) он **не совпадает** by design. + Если потребуется, чтобы seed совпадал с реальными клиентами на Argon2 — нужно отдельно портировать + Argon2id+masterSecret в Java (на сервере Argon2 сейчас нет). Простое добавление `|` было бы **неверным**: + сломало бы совпадение с легаси-путём и всё равно не дало бы совпадения с Argon2-путём. + +--- + +## 6. Правило синхронизации (обязательно) + +1. Этот документ — источник истины по деривации секрета и ключей. +2. Любое изменение кода, затрагивающее формулу секрета, параметры Argon2id, соль, формулу ключа, + разделитель `|`, набор/имена суффиксов, формат homeserver-ключа или связь dev-ключ ↔ Solana-адрес — + **обязательно** отражать здесь в том же изменении. +3. Пункты, помеченные ⚠️, — это долг к устранению, а не норма. +4. Нельзя сознательно оставлять код и этот документ в рассинхроне без отдельной явной договорённости. diff --git a/Dev_Docs/Keys/README.md b/Dev_Docs/Keys/README.md index 0e12835..863a9da 100644 --- a/Dev_Docs/Keys/README.md +++ b/Dev_Docs/Keys/README.md @@ -8,7 +8,7 @@ В SHiNE у пользователя есть несколько уровней ключей: -- `root key` - главный корневой ключ пользователя, он же основной Solana-ключ. +- `root key` - главный (master) ключ пользователя: тот, кто им владеет, управляет пользовательской PDA в Solana и может заменить все остальные ключи. Это не пополняемый кошелёк (комиссии платит `device key`). - `blockchain key` - ключ записи в персональный SHiNE-блокчейн пользователя. - `device key` - общий ключ пользовательских устройств для повседневной работы, звонков, DM и мелких платежей. - `session key` - ключ конкретной сессии или конкретного устройства для авторизации на сервере. @@ -28,9 +28,9 @@ - управление остальными ключами; - подтверждение операций, которые должны иметь максимальный уровень доверия. -В текущей модели `root key` совпадает по смыслу с главным Solana-ключом пользователя. +`root key` — это **главный (master) ключ** в следующем смысле: зная `root key`, можно управлять пользовательской PDA-записью в Solana (`create_user_pda` / `update_user_pda`) и тем самым **заменить все остальные ключи** пользователя (device, blockchain, homeserver). Поэтому компрометация `root key` равносильна компрометации всей личности пользователя. -На `root key` могут храниться значимые средства, если пользователь сознательно выбирает такую модель. Для мелких текущих расходов предпочтительнее использовать `device key`. +Важно не путать авторитет и кошелёк: `root key` — это авторитет над PDA-записью, а **SOL-комиссии за create/update платит `device key`** (он же fee payer и адрес для пополнения). Подробнее о том, какой ключ за что отвечает на Solana, — в `Dev_Docs/Keys/DERIVATION.md`, §3. ## `blockchain key` @@ -158,6 +158,7 @@ Self-message - это сообщение пользователя самому ## Связанные документы +- `Dev_Docs/Keys/DERIVATION.md` - **источник истины по конкретной деривации** секрета и ключей (формулы Argon2id, `base64|suffix→SHA-256→Ed25519`, суффиксы `root.key`/`bch.key`/`dev.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 и движения средств. diff --git a/Dev_Docs/Pending_Features/2026-06-07_1650_esp32_subserver_ui_прототип.md b/Dev_Docs/Pending_Features/2026-06-07_1650_esp32_homeserver_ui_прототип.md similarity index 71% rename from Dev_Docs/Pending_Features/2026-06-07_1650_esp32_subserver_ui_прототип.md rename to Dev_Docs/Pending_Features/2026-06-07_1650_esp32_homeserver_ui_прототип.md index e3b8f5a..f3356da 100644 --- a/Dev_Docs/Pending_Features/2026-06-07_1650_esp32_subserver_ui_прототип.md +++ b/Dev_Docs/Pending_Features/2026-06-07_1650_esp32_homeserver_ui_прототип.md @@ -1,26 +1,26 @@ -# ESP32 UI-прототип сабсервера SHiNE +# ESP32 UI-прототип homeserver SHiNE - краткое описание фичи: - для `Waveshare ESP32-S3-Touch-AMOLED-2.16` добавлен новый интерактивный UI-скетч сабсервера `SHiNE` с хранением данных в `NVS`, настройками `Wi-Fi`, настройками серверов, кошельком, экраном `QR/URI`, живой Solana-регистрацией и экраном входящих запросов. Логика PIN в коде сохранена, но вход по PIN во временной сборке отключён, чтобы не блокировать проверку остальных экранов. В текущей версии `Wi-Fi` подключается реально, адреса `API/RPC/WS` проверяются реально, баланс кошелька читается из `Solana RPC`, а регистрация отправляет `create_user_pda` в `shine_users`. + для `Waveshare ESP32-S3-Touch-AMOLED-2.16` добавлен новый интерактивный UI-скетч homeserver `SHiNE` с хранением данных в `NVS`, настройками `Wi-Fi`, настройками серверов, кошельком, экраном `QR/URI`, живой Solana-регистрацией и экраном входящих запросов. Логика PIN в коде сохранена, но вход по PIN во временной сборке отключён, чтобы не блокировать проверку остальных экранов. В текущей версии `Wi-Fi` подключается реально, адреса `API/RPC/WS` проверяются реально, баланс кошелька читается из `Solana RPC`, а регистрация отправляет `create_user_pda` в `shine_users`. - что именно проверять: - 1. Прошить режим `subserver-ui` и дождаться открытия главного экрана без PIN. + 1. Прошить режим `homeserver-ui` и дождаться открытия главного экрана без PIN. 2. Проверить, что текст в заголовках, кнопках и статусах отображается читаемо; в текущей временной сборке допускается ASCII-транслитерация русского текста. 3. Открыть `Настройки` и убедиться, что показывается пометка о временно отключённом входе по PIN. 4. Открыть `Подключение -> Wi-Fi`, ввести `SSID` и пароль, нажать `Проверить`, дождаться реального подключения, затем перезагрузить устройство и проверить, что значения сохранились. 5. Открыть `Подключение -> Серверы`, проверить или изменить `API/RPC/WS`, нажать `Проверить` и убедиться, что показываются реальные статусы доступности, затем перезагрузить устройство и проверить сохранение значений. - 6. Открыть `Аккаунт`, ввести логин, имя сабсервера и нажать `Сгенерировать`; проверить, что появились секрет и адрес кошелька, а после перезагрузки они не исчезают. + 6. Открыть `Аккаунт`, ввести логин, имя homeserver и нажать `Сгенерировать`; проверить, что появились секрет и адрес кошелька, а после перезагрузки они не исчезают. 7. Открыть `Кошелёк`, нажать `Проверить` и убедиться, что баланс реально читается из `Solana RPC`; затем открыть `QR и URI` и проверить, что QR-код отрисовывается и сканируется как `solana:`-ссылка. 8. При необходимости отдельно проверить тестовые кнопки `+/- SOL`: они меняют локальный баланс для UX-сценариев, но после следующей реальной RPC-проверки баланс должен вернуться к сетевому значению. 9. Вернуться на главный экран и проверить, что до выполнения всех условий кнопка регистрации недоступна, а после выполнения становится доступной. - 10. Выполнить регистрацию и убедиться, что статус меняется на `Сабсервер активен`, онлайн-статус становится активным, а на экране появляются краткие отпечатки `PDA/TX`. + 10. Выполнить регистрацию и убедиться, что статус меняется на `Homeserver активен`, онлайн-статус становится активным, а на экране появляются краткие отпечатки `PDA/TX`. 11. После регистрации проверить через `Solana`/UI проекта, что `user_pda` для этого логина реально создана и соответствует `device`-адресу устройства. 12. Открыть `Запросы`, поочерёдно открыть оба демонстрационных запроса и проверить, что кнопки `Разрешить` и `Отклонить` меняют их статус. 13. При необходимости открыть `Настройки -> Сменить PIN` и убедиться, что новый PIN сохраняется, хотя вход по PIN временно не используется на старте. 14. Выполнить `Полный сброс` и убедиться, что все поля, секрет, баланс, онлайн и регистрация очищаются. - ожидаемый результат: - новый `ESP32`-скетч стабильно запускается, показывает читаемый интерфейс хотя бы в ASCII-транслитерации, сохраняет данные во внутренней памяти устройства, реально подключается к `Wi-Fi`, реально проверяет `API/RPC/WS`, реально читает баланс из `Solana RPC`, рисует рабочий `QR` для `solana:`-URI и позволяет вручную пройти полный сценарий on-chain регистрации сабсервера. + новый `ESP32`-скетч стабильно запускается, показывает читаемый интерфейс хотя бы в ASCII-транслитерации, сохраняет данные во внутренней памяти устройства, реально подключается к `Wi-Fi`, реально проверяет `API/RPC/WS`, реально читает баланс из `Solana RPC`, рисует рабочий `QR` для `solana:`-URI и позволяет вручную пройти полный сценарий on-chain регистрации homeserver. - статус: pending diff --git a/Dev_Docs/Pending_Features/2026-06-08_1150_esp32_auto_flash_script.md b/Dev_Docs/Pending_Features/2026-06-08_1150_esp32_auto_flash_script.md index 66e4021..68f9af6 100644 --- a/Dev_Docs/Pending_Features/2026-06-08_1150_esp32_auto_flash_script.md +++ b/Dev_Docs/Pending_Features/2026-06-08_1150_esp32_auto_flash_script.md @@ -1,13 +1,13 @@ -# ESP32 авто-прошивка shine_subserver_ui +# ESP32 авто-прошивка shine_homeserver_ui - краткое описание фичи: - добавлен исполняемый скрипт `flash_shine_subserver_ui.sh`, который автоматически ищет USB-порт `ESP32` и запускает заливку прошивки `shine_subserver_ui` без ручного указания `PORT`. + добавлен исполняемый скрипт `flash_shine_homeserver_ui.sh`, который автоматически ищет USB-порт `ESP32` и запускает заливку прошивки `shine_homeserver_ui` без ручного указания `PORT`. - что именно проверять: 1. Подключить плату `ESP32` по USB. 2. Перейти в папку `ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/test-device/`. - 3. Запустить `./flash_shine_subserver_ui.sh`. + 3. Запустить `./flash_shine_homeserver_ui.sh`. 4. Убедиться, что скрипт сам показывает найденный порт и успешно запускает compile/upload. - ожидаемый результат: - скрипт без ручного ввода порта находит `ESP32`, печатает найденный `/dev/ttyACM*` или `/dev/ttyUSB*` и заливает `shine_subserver_ui`. + скрипт без ручного ввода порта находит `ESP32`, печатает найденный `/dev/ttyACM*` или `/dev/ttyUSB*` и заливает `shine_homeserver_ui`. - статус: pending diff --git a/Dev_Docs/Pending_Features/2026-06-08_1245_esp32_pin_button_labels.md b/Dev_Docs/Pending_Features/2026-06-08_1245_esp32_pin_button_labels.md index f29c8d7..786a26e 100644 --- a/Dev_Docs/Pending_Features/2026-06-08_1245_esp32_pin_button_labels.md +++ b/Dev_Docs/Pending_Features/2026-06-08_1245_esp32_pin_button_labels.md @@ -1,7 +1,7 @@ # ESP32 PIN-клавиатура: подписи кнопок - краткое описание фичи: - в UI-скетче `shine_subserver_ui` изменена отрисовка подписей кнопок. Вместо малого шрифта теперь используется более стабильный шрифт с явным центрированием текста внутри кнопок, чтобы на экране ввода PIN и других экранах не пропадали цифры и надписи. + в UI-скетче `shine_homeserver_ui` изменена отрисовка подписей кнопок. Вместо малого шрифта теперь используется более стабильный шрифт с явным центрированием текста внутри кнопок, чтобы на экране ввода PIN и других экранах не пропадали цифры и надписи. - что именно проверять: 1. Включить устройство и дождаться экрана ввода PIN. 2. Убедиться, что на всех серых кнопках видны цифры `0-9`, `Отмена` и `OK`. diff --git a/Dev_Docs/Pending_Features/2026-06-08_1315_esp32_test_sketches_folder.md b/Dev_Docs/Pending_Features/2026-06-08_1315_esp32_test_sketches_folder.md index 222ebf9..80e7579 100644 --- a/Dev_Docs/Pending_Features/2026-06-08_1315_esp32_test_sketches_folder.md +++ b/Dev_Docs/Pending_Features/2026-06-08_1315_esp32_test_sketches_folder.md @@ -6,8 +6,8 @@ 1. Запустить `./burn.sh gfx-text-test` и убедиться, что прошивается тест текста из новой папки. 2. Запустить `./burn.sh gfx-layout-test` и проверить нижние ряды кнопок. 3. Запустить `./burn.sh lvgl-basic-test` и проверить, что `LVGL` показывает текст и кнопки. - 4. Убедиться, что новая папка не мешает сборке `subserver-ui`. + 4. Убедиться, что новая папка не мешает сборке `homeserver-ui`. - ожидаемый результат: - тестовые скетчи лежат отдельно от основного UI, шьются отдельными режимами и позволяют быстро проверять разные гипотезы по экрану без правок в `shine_subserver_ui`. + тестовые скетчи лежат отдельно от основного UI, шьются отдельными режимами и позволяют быстро проверять разные гипотезы по экрану без правок в `shine_homeserver_ui`. - статус: pending diff --git a/Dev_Docs/Pending_Features/2026-06-08_1940_esp32_nav_minimal_test.md b/Dev_Docs/Pending_Features/2026-06-08_1940_esp32_nav_minimal_test.md index 6984a83..577565a 100644 --- a/Dev_Docs/Pending_Features/2026-06-08_1940_esp32_nav_minimal_test.md +++ b/Dev_Docs/Pending_Features/2026-06-08_1940_esp32_nav_minimal_test.md @@ -1,9 +1,9 @@ # ESP32 nav minimal test -- Краткое описание: минимальный UI-прототип для сабсервера на базе `LVGL + subserver touch`, с Wi-Fi flow, серверными адресами и общим экраном редактирования текста. +- Краткое описание: минимальный UI-прототип для homeserver на базе `LVGL + subserver touch`, с Wi-Fi flow, серверными адресами и общим экраном редактирования текста. - Что проверять: - стартует экран `HOME`; - - на `HOME` видны реальное значение сабсервера или `subserver not set`, реальное значение логина или `login not set`, при отсутствии секрета строка `secret not set`, а также `STATUS`, верхний правый блок с процентом батареи, иконкой батареи и индикатором Wi-Fi, кнопка баланса, строка `SHiNE: ...`, кнопка `SETTINGS` уменьшенной ширины у правого края и нижняя подпись `SHiNE subserver (v.0.18)`; + - на `HOME` видны реальное значение homeserver или `homeserver not set`, реальное значение логина или `login not set`, при отсутствии секрета строка `secret not set`, а также `STATUS`, верхний правый блок с процентом батареи, иконкой батареи и индикатором Wi-Fi, кнопка баланса, строка `SHiNE: ...`, кнопка `SETTINGS` уменьшенной ширины у правого края и нижняя подпись `SHiNE homeserver (v.0.18)`; - справа от строки логина виден индикатор статуса Solana-аккаунта: - зелёный, если ключи совпали; - красный, если mismatch; @@ -19,9 +19,9 @@ - `unavailable` - пока открыт `HOME`, статус сам обновляется без перехода на другие экраны; - баланс обновляется кнопкой по нажатию; - - если логин зарегистрирован и секрет/сабсервер заданы, устройство: + - если логин зарегистрирован и секрет/homeserver заданы, устройство: - читает `user_pda` через Solana RPC; - - сверяет `root`, `blockchain`, `device` и `subserver` session type `100`; + - сверяет `root`, `blockchain`, `device` и `homeserver` session type `100`; - поднимает WebSocket-сессию с сервером SHiNE; - шлёт `Ping` раз в минуту; - кнопка `SETTINGS` открывает `SETTINGS_MENU`; @@ -54,7 +54,7 @@ - визуальный курсор в поле ввода не показывается; - новые символы всегда дописываются только в конец строки; - основные 3 ряда клавиш и нижний служебный ряд стали выше; - - внизу остаётся отдельная тёмная полоса с версией `SHiNE subserver (v.0.18)`, а рамка клавиатурного блока заканчивается выше неё; + - внизу остаётся отдельная тёмная полоса с версией `SHiNE homeserver (v.0.18)`, а рамка клавиатурного блока заканчивается выше неё; - одно непрерывное касание вызывает не более одного действия кнопки; - скольжение пальцем по клавиатуре не нажимает подряд несколько клавиш; - медленный свайп по экрану не должен превращаться в случайное нажатие кнопки; @@ -75,11 +75,11 @@ - нажатие `Account` открывает `ACCOUNT_SCREEN`; - `ACCOUNT_SCREEN` показывает 3 кнопки: - `Login ()` - - `Subserver ()` + - `Homeserver ()` - `Secret (<*****|not set>)` - `Login` открывает общий экран редактирования и сохраняется в NVS; - - `Subserver` открывает промежуточный экран с `USE SUBSERVER1` и `EDIT MANUALLY`; - - `USE SUBSERVER1` возвращает стандартное значение `subserver1`; + - `Homeserver` открывает промежуточный экран с `USE HOMESERVER1` и `EDIT MANUALLY`; + - `USE HOMESERVER1` возвращает стандартное значение `homeserver1`; - `EDIT MANUALLY` открывает общий экран редактирования и сохраняет значение в NVS; - `Secret` теперь открывает меню секрета с показом секрета, ручным вводом и генерацией; - в `SHOW SECRET` показывается прокручиваемый список всех ключей: @@ -90,15 +90,15 @@ - `Blockchain key priv (base58)` - `Device key (base58)` - `Device key priv (base58)` - - `Subserver key (base58)` - - `Subserver key priv (base58)` + - `Homeserver key (base58)` + - `Homeserver key priv (base58)` - значения ключей показываются полными строками увеличенным шрифтом; - при смене `login` сохранённый секрет сбрасывается в `not set`; - во время генерации секрета есть `CANCEL` и подтверждение остановки; - при отмене генерации старый секрет, если он был, не должен теряться; - свайп вправо из внутренних экранов возвращает в `SETTINGS_MENU`; - - свайп вправо из `ACCOUNT_SUBSERVER_SCREEN` и `ACCOUNT_SECRET_SCREEN` возвращает в `ACCOUNT_SCREEN`; + - свайп вправо из `ACCOUNT_HOMESERVER_SCREEN` и `ACCOUNT_SECRET_SCREEN` возвращает в `ACCOUNT_SCREEN`; - если во время реального свайпа палец проходит по кнопке, это не должно открывать кнопку как обычный `click`. - Ожидаемый результат: новый скетч даёт чистый навигационный каркас и уже умеет настраивать Wi-Fi и серверные адреса на самой ESP32. -- Дополнительно ожидается: `HOME` уже показывает реальный Solana/WS-статус сабсервера, а отсутствие пользователя в Solana заметно сразу без перехода в настройки. +- Дополнительно ожидается: `HOME` уже показывает реальный Solana/WS-статус homeserver, а отсутствие пользователя в Solana заметно сразу без перехода в настройки. - Статус: pending diff --git a/Dev_Docs/Solana/user_pda/README.md b/Dev_Docs/Solana/user_pda/README.md index e520ff9..1039e38 100644 --- a/Dev_Docs/Solana/user_pda/README.md +++ b/Dev_Docs/Solana/user_pda/README.md @@ -90,7 +90,7 @@ UserPdaRecordV1 | `3` | `BlockchainRegistryBlock` | Один или несколько блокчейнов пользователя. | | `30` | `ServerProfileBlock` | Серверные данные пользователя. | | `40` | `AccessServersBlock` | Серверы доступа/relay. | -| `50` | `SessionsBlock` | Опубликованные пользовательские сессии и саб-серверы. | +| `50` | `SessionsBlock` | Опубликованные пользовательские сессии и homeserver-ы. | | `70` | `TrustedStateBlock` | Счетчик trusted-связей. | | `255` | `ReservedBlock` | Зарезервировано, пока не используется. | @@ -309,7 +309,7 @@ SessionRecord | Значение | Смысл | |----------|-------| | `1` | Обычная пользовательская сессия. | -| `100` | Саб-сервер пользователя. | +| `100` | Homeserver пользователя. | Правила: 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 new file mode 100644 index 0000000..e5a93f6 --- /dev/null +++ b/Dev_Docs/audit/Solana-audit-3-by-Claude-12июня2026.md @@ -0,0 +1,134 @@ +# Аудит безопасности Solana-программ SHiNE — выпуск 3 (12.06.2026) + +Тематический аудит с фокусом на **полноту проверок входных аккаунтов** +(signer / owner / каноничный PDA-адрес / system-program / sysvar инструкций / +аккаунт оракула) — отвечает на вопрос «точно ли хватает всех проверок входных +аккаунтов». Код перечитан целиком после исправлений аудита №2 +(`Solana-audit-2-by-Claude-11июня2026.md`): + +- `shine_login_guard` (183 строки) — stateless-классификатор логинов, аккаунтами не пользуется; +- `shine_users` (1068 строк) — реестр пользователей, PDA-записи, ed25519-подписи, экономика лимитов; +- `shine_payments` (1398 строк) — очереди тикетов, выплаты из вольта, оракул Pyth. + +Это ручная (не-Anchor `#[derive(Accounts)]`) реализация на `solana_program`, поэтому +каждая проверка аккаунта выполняется явно в коде handler-а. Перебраны: подмена +аккаунтов/PDA, подмена владельца, bump-seed атаки, отсутствие signer/authority, +подмена system-program и sysvar, подмена аккаунта оракула, неинициализированные/ +повторно инициализируемые PDA, «лишние» аккаунты. + +## Итоговый вердикт + +**Проверок входных аккаунтов достаточно во всех трёх программах.** По каждому +handler присутствуют все требуемые классы проверок; грубых дыр (подмена PDA на +чужой аккаунт, отсутствие owner/signer-проверки, использование пользовательского +bump, подмена аккаунта оракула) не найдено. Все Critical/HIGH из аудитов №1 и №2 +закрыты и в этом проходе подтверждены в коде. Новых эксплуатируемых пробелов в +валидации аккаунтов нет; есть несколько LOW/INFO-замечаний «by design». + +## Статус прошлых находок (подтверждено в коде на 12.06.2026) + +- 🔴 Critical #1 (economy-config PDA, `shine_users`) — закрыто: `validate_users_economy_config_pda` (адрес + `owner == program_id`) вызывается и в create, и в update перед чтением. +- 🔴 Critical #2 (singleton-PDA, `shine_payments`) — закрыто: `validate_singleton_state_pda` (адрес + `owner == id()`) во всех инструкциях. +- 🟠 Medium (валидация Pyth) — закрыто: пин адреса `PYTH_SOL_USD_ACCOUNT`, `owner == pyth_receiver`, `PriceUpdateV2`, `feed_id`, возраст, доверительный интервал. +- 🟡 Low (griefing на предсказуемых адресах) — закрыто: `create_pda_account` создаёт «поверх предзаполненного» в обеих программах. +- 🔴 HIGH аудита №2 (`recipient_wallet == inflow_vault` замораживает выплаты) — закрыто: запрет `recipient == inflow_vault` в `buy_ticket_by_purchase_usd` (стр. 1026), `process_manager_add_ticket` (стр. 747), `process_change_ticket_recipient` (стр. 878) + защита по умолчанию `require!(vault.key != recipient.key)` в `transfer_from_vault` (стр. 1278). + +--- + +## Матрица проверок входных аккаунтов + +### shine_users + +| Инструкция | signer | owner PDA | адрес/seed PDA | system | sysvar / подпись | прочее | +|---|---|---|---|---|---|---| +| `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-поля сверены с прежней записью | + +### shine_payments + +| Инструкция | signer | owner / валидация PDA | адрес PDA | system | прочее | +|---|---|---|---|---|---| +| `init` | ✓ payer | все 4 PDA `is_uninitialized` | деривация + сверка | ✓ | `dao_wallet` из `settings`, нет лишних аккаунтов | +| `update_coef_limit` | ✓ + `signer == config.dao_wallet` | config/coef `owner == id()` | деривация + сверка | — | границы coef/limit/reward; нет лишних аккаунтов | +| `grant_manager_limits` | ✓ + `signer == config.dao_wallet` | config `owner == id()`; allowance create/read | allowance из `manager_wallet` | ✓ | `state.manager_wallet == args.manager_wallet` | +| `buy_ticket` / `_usd` / `_sol` | ✓ | config/coef/queues `owner == id()` | ticket деривация + сверка + `is_uninitialized` | ✓ | oracle (key+owner+возраст+confidence), `dao_wallet == config.dao_wallet`, `recipient != inflow_vault`, slippage | +| `manager_add_ticket` | ✓ | allowance/queues `owner == id()` | allowance из `signer`; ticket деривация + сверка + uninit | ✓ | `allowance.manager_wallet == signer`, `queue_id ∈ {1,2,3}`, `recipient != inflow_vault` | +| `step_payout` | ✓ | все singleton-PDA `owner == id()` | ticket деривация + сверка | — | `dao_wallet == config.dao_wallet`, `inflow == config.inflow_vault`, ticket `queue/index/!is_paid/recipient`, oracle | +| `change_ticket_recipient` | ✓ + `signer == ticket.recipient_wallet` | queues + ticket `owner == id()` (через `read_state`) | ticket деривация из своих `queue_id/index` + сверка | — | `!is_paid`, запрет менять «следующий к выплате», `recipient != inflow_vault` | + +### shine_login_guard + +Аккаунты не используются (`_accounts`); программа stateless, средствами не владеет. +Защита со стороны вызова реализована в `shine_users`: сверяется и адрес вызываемой +программы (`login_guard_program.key == SHINE_LOGIN_GUARD_PROGRAM_ID`), и `program_id` +в `get_return_data`. Подмена/подделка ответа исключены. Отдельных проверок входных +аккаунтов внутри программы не требуется. + +--- + +## 🟡 LOW / INFO — наблюдения без прямой эксплуатации + +### L1. Permissionless `init` в обеих программах +`shine_payments::init` и `shine_users::init_users_economy_config` может вызвать кто +угодно первым. Практического эксплойта нет: все значения (включая `dao_wallet` и +`DAO_AUTHORITY`) берутся из констант `settings`, а не из ввода, повторная +инициализация заблокирована проверками `is_uninitialized` / `data_is_empty`. Риск +низкий; при желании привязать init к ожидаемому деплой-кошельку. Совпадает с моделью +«первый init = деплой». + +### L2. В `shine_users` нет явной проверки «лишних аккаунтов» — ✅ ИСПРАВЛЕНО (12.06.2026) +`shine_payments` в каждом handler делает `require!(account_iter.next().is_none())`. +В `shine_users` такой проверки не было — лишние аккаунты в конце списка просто +игнорировались (читается строго нужное количество через `next_account_info`). Это +безвредно (на безопасность не влияло), но для симметрии и явности добавлено. +Класс: гигиена, не уязвимость. + +Закрыто: во все 4 инструкции `shine_users` (`init_users_economy_config`, +`update_users_economy_config`, `create_user_pda`, `update_user_pda`) после чтения +фиксированного набора аккаунтов добавлено `require!(it.next().is_none(), +ShineUsersError::InvalidInstruction)`. Документация — `doc/programs/shine_users.md` §3.4. + +### L3. Гонка за логином (first-come) в `shine_users` — known issue +Адрес `user_pda` детерминирован из логина; после закрытия griefing-подсева остаётся +обычное состязание за регистрацию (front-run в мемпуле). On-chain решается только +commit-reveal; для текущей модели — приемлемый риск, ранее зафиксирован в аудите №2 +(L2). К проверкам аккаунтов не относится. + +### L4. Экономическая устойчивость вольта (дизайн, не баг) +Деньги за покупку тикетов уходят на `dao_wallet`, а выплаты `step_payout` идут из +`inflow_vault`, наполняемого регистрационными комиссиями `shine_users` (коэффициент +по умолчанию `START_COEF_PPM = 5x`). При недостаточном притоке регистраций вольт +истощается и выплаты останавливаются (без потери средств). Это свойство +экономической модели «очередь/билеты», а не дефект валидации аккаунтов — отмечено +для полноты (ранее L4 в аудите №2). Мониторить баланс вольта vs обязательств. + +--- + +## ✅ Проверено и подтверждено как корректное (по входным аккаунтам) + +- **Подмена 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`). +- **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 по хэшу — указать на чужую инструкцию нельзя. +- **Алиасинг аккаунтов**: `recipient != inflow_vault` запрещён на входе во всех точках задания получателя + `vault.key != recipient.key` в `transfer_from_vault`. +- **`inflow_vault` в `shine_users`** сверяется с PDA, выведенным из `SHINE_PAYMENTS_PROGRAM_ID` и `SHINE_PAYMENTS_INFLOW_VAULT_SEED` — комиссия не может уйти на чужой адрес. +- **Реентранси** отсутствует: CPI только в System Program и в stateless `shine_login_guard` (с проверкой возвращённого `program_id`); обратных вызовов в наши программы нет. + +--- + +## Приоритет действий + +1. **LOW** — ✅ выполнено 12.06.2026: добавлено `require!(it.next().is_none(), …)` во + все инструкции `shine_users` для симметрии с `shine_payments` (L2). +2. **INFO** — зафиксировать в эксплуатационной документации known-issue гонки за + логином (L3) и экономику вольта (L4); рассмотреть привязку `init` к ожидаемому + деплой-кошельку (L1). + +Критичных и высоких находок по полноте проверок входных аккаунтов в этом проходе +нет. Единственная LOW-правка (L2) применена в рамках этого же изменения; код +`shine_users` собирается успешно (`cargo build -p shine_users`). diff --git a/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/reference/shine_subserver_ui_nav_minimal_spec.md b/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/reference/shine_homeserver_ui_nav_minimal_spec.md similarity index 93% rename from ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/reference/shine_subserver_ui_nav_minimal_spec.md rename to ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/reference/shine_homeserver_ui_nav_minimal_spec.md index 5261000..252e6d0 100644 --- a/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/reference/shine_subserver_ui_nav_minimal_spec.md +++ b/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/reference/shine_homeserver_ui_nav_minimal_spec.md @@ -1,4 +1,4 @@ -# SHiNE ESP32 Subserver UI Nav Minimal Spec +# SHiNE ESP32 Homeserver UI Nav Minimal Spec Минимальный навигационный прототип для `Waveshare ESP32-S3-Touch-AMOLED-2.16`. @@ -23,7 +23,7 @@ - `WIFI_SCREEN` - `SERVER_SCREEN` - `ACCOUNT_SCREEN` -- `ACCOUNT_SUBSERVER_SCREEN` +- `ACCOUNT_HOMESERVER_SCREEN` - `ACCOUNT_SECRET_SCREEN` - `SECRET_SHOW_SCREEN` - `SECRET_GENERATE_*` @@ -33,7 +33,7 @@ ## HOME Показывает: -- сверху слева значение сабсервера или `subserver not set`; +- сверху слева значение homeserver или `homeserver not set`; - ниже значение логина или `login not set`; - справа от строки логина индикатор статуса Solana-аккаунта: - зелёный — все ключи совпадают; @@ -51,7 +51,7 @@ - строка `SHiNE: connected/account not configured/unavailable`; - при отсутствии пользователя в Solana PDA слева снизу появляется кнопка `REGISTER ACCOUNT`; - снизу кнопку `SETTINGS`, уменьшенную примерно до половины ширины экрана и сдвинутую к правому краю. -- внизу на тёмной полосе подпись `SHiNE subserver (v.0.18)`. +- внизу на тёмной полосе подпись `SHiNE homeserver (v.0.18)`. Строка Wi-Fi на `HOME`: - `Wi-Fi (not configured) not configured` @@ -65,11 +65,11 @@ Фоновая логика: - пока открыт `HOME`, экран сам обновляется примерно раз в секунду; -- при наличии `login + secret + subserver` и Wi-Fi устройство читает Solana `user_pda` напрямую через RPC; -- сравниваются `root key`, `blockchain key`, `device key` и `subserver` session-запись типа `100`; +- при наличии `login + secret + homeserver` и Wi-Fi устройство читает Solana `user_pda` напрямую через RPC; +- сравниваются `root key`, `blockchain key`, `device key` и `homeserver` session-запись типа `100`; - для строки `SHiNE:` устройство держит отдельную WebSocket-сессию с сервером SHiNE: - авторизация через `AuthChallenge/CreateAuthSession` или `SessionChallenge/SessionLogin`; - - session key = публичный `subserver key`; + - session key = публичный `homeserver key`; - подтверждение создания сессии подписывается `device key`; - heartbeat выполняется `Ping` раз в минуту. @@ -164,26 +164,26 @@ - заголовок `ACCOUNT`; - статусное сообщение; - кнопку `Login ()`; -- кнопку `Subserver ()`; +- кнопку `Homeserver ()`; - кнопку `Secret (<*****|not set>)`. Переходы: - свайп вправо -> `SETTINGS_MENU` - `Login` -> `TEXT_EDIT_SCREEN` -- `Subserver` -> `ACCOUNT_SUBSERVER_SCREEN` +- `Homeserver` -> `ACCOUNT_HOMESERVER_SCREEN` - `Secret` -> `ACCOUNT_SECRET_SCREEN` -## ACCOUNT_SUBSERVER_SCREEN +## ACCOUNT_HOMESERVER_SCREEN Показывает: -- текущий `subserver`; -- рекомендацию оставить `subserver1`, если устройство одно; -- кнопку `USE SUBSERVER1`; +- текущий `homeserver`; +- рекомендацию оставить `homeserver1`, если устройство одно; +- кнопку `USE HOMESERVER1`; - кнопку `EDIT MANUALLY`; - кнопку `BACK`. Переходы: -- `USE SUBSERVER1` -> сохраняет `subserver1` и возвращает в `ACCOUNT_SCREEN` +- `USE HOMESERVER1` -> сохраняет `homeserver1` и возвращает в `ACCOUNT_SCREEN` - `EDIT MANUALLY` -> `TEXT_EDIT_SCREEN` - свайп вправо -> `ACCOUNT_SCREEN` @@ -212,8 +212,8 @@ - `Blockchain key priv (base58)`; - `Device key (base58)`; - `Device key priv (base58)`; -- `Subserver key (base58)`; -- `Subserver key priv (base58)`; +- `Homeserver key (base58)`; +- `Homeserver key priv (base58)`; - для каждого поля показывается формула derivation; - значения ключей показываются полными строками увеличенным шрифтом; - кнопку `BACK`. @@ -293,7 +293,7 @@ Используется `Preferences` (NVS памяти ESP32): - `login` -- `subserver` +- `homeserver` - `secret_set` ## Детали клавиатуры @@ -312,7 +312,7 @@ - `DEL` - `SAVE` - `CANCEL` -- ниже рамки клавиатурного блока остаётся отдельная тёмная полоса с версией `SHiNE subserver (v.0.18)`. +- ниже рамки клавиатурного блока остаётся отдельная тёмная полоса с версией `SHiNE homeserver (v.0.18)`. ## Жесты @@ -329,7 +329,7 @@ - `WIFI_SCREEN`: свайп вправо -> `SETTINGS_MENU` - `SERVER_SCREEN`: свайп вправо -> `SETTINGS_MENU` - `ACCOUNT_SCREEN`: свайп вправо -> `SETTINGS_MENU` -- `ACCOUNT_SUBSERVER_SCREEN`: свайп вправо -> `ACCOUNT_SCREEN` +- `ACCOUNT_HOMESERVER_SCREEN`: свайп вправо -> `ACCOUNT_SCREEN` - `ACCOUNT_SECRET_SCREEN`: свайп вправо -> `ACCOUNT_SCREEN` - `TEXT_EDIT_SCREEN`: свайп влево/вправо -> переключение страниц клавиатуры - переключение страниц клавиатуры срабатывает только если свайп начался в зоне самой клавиатуры, а не по всему экрану редактора diff --git a/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/reference/shine_subserver_ui_spec.md b/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/reference/shine_homeserver_ui_spec.md similarity index 96% rename from ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/reference/shine_subserver_ui_spec.md rename to ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/reference/shine_homeserver_ui_spec.md index 36f33de..2a97bc1 100644 --- a/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/reference/shine_subserver_ui_spec.md +++ b/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/reference/shine_homeserver_ui_spec.md @@ -1,8 +1,8 @@ -# SHiNE ESP32 Subserver UI Spec +# SHiNE ESP32 Homeserver UI Spec ## Назначение -Этот документ описывает актуальный UI-прототип сабсервера `SHiNE` для платы `Waveshare ESP32-S3-Touch-AMOLED-2.16`. +Этот документ описывает актуальный UI-прототип homeserver `SHiNE` для платы `Waveshare ESP32-S3-Touch-AMOLED-2.16`. Документ является источником истины для Arduino-скетча: @@ -37,13 +37,13 @@ ## Основная идея устройства -Устройство работает как отдельный сабсервер: +Устройство работает как отдельный homeserver: - хранит секрет на самом устройстве; -- позволяет ввести логин, секрет и имя сабсервера; +- позволяет ввести логин, секрет и имя homeserver; - показывает адрес кошелька устройства; - позволяет пополнить баланс перед регистрацией; -- после выполнения условий даёт зарегистрировать устройство как сабсервер; +- после выполнения условий даёт зарегистрировать устройство как homeserver; - после регистрации может принимать входящие запросы на вход и на подпись. `SD`-карта не нужна для постоянного хранения секрета в этом прототипе. @@ -57,7 +57,7 @@ - `Wi-Fi SSID`; - `Wi-Fi password`; - `login`; -- `session/subserver name`; +- `session/homeserver name`; - `master secret`; - `wallet address`; - `user pda address`; @@ -142,7 +142,7 @@ - крупный статус регистрации; - имя логина; -- имя сабсервера; +- имя homeserver; - короткий статус Wi-Fi; - короткий статус сервера; - короткий статус баланса. @@ -162,14 +162,14 @@ Если регистрация уже сделана: -- вместо призыва к регистрации показывается статус `Сабсервер активен`. +- вместо призыва к регистрации показывается статус `Homeserver активен`. ## Экран STATUS Показывает сводку: - логин; -- сабсервер; +- homeserver; - есть ли секрет; - зарегистрировано ли устройство; - подключён ли Wi-Fi; @@ -256,7 +256,7 @@ Показывает: - логин; -- имя сабсервера; +- имя homeserver; - статус секрета; - короткий отпечаток секрета; - статус регистрации; @@ -266,7 +266,7 @@ - `Изменить логин` - `Секрет` -- `Имя сабсервера` +- `Имя homeserver` - `Сгенерировать` - `Очистить` - `Назад` @@ -394,7 +394,7 @@ QR должен быть сканируемым, а не декоративны - `SSID` - `Пароль Wi-Fi` - `Логин` -- `Имя сабсервера` +- `Имя homeserver` - `API URL` - `RPC URL` - `WS URL` @@ -432,13 +432,13 @@ QR должен быть сканируемым, а не декоративны 5. проверить или задать серверные адреса; 6. открыть `Аккаунт`; 7. ввести логин; -8. задать имя сабсервера; +8. задать имя homeserver; 9. сгенерировать секрет; 10. открыть `Кошелёк`; 11. при необходимости пополнить баланс; 12. вернуться на `HOME`; 13. нажать `Зарегистрировать`; -14. после подтверждения увидеть статус `Сабсервер активен`. +14. после подтверждения увидеть статус `Homeserver активен`. Примечание: diff --git a/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/test-device/README.md b/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/test-device/README.md index 9ce964a..b2a3a28 100644 --- a/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/test-device/README.md +++ b/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/test-device/README.md @@ -14,7 +14,7 @@ - `hello` — базовый тест экрана (пример `01_HelloWorld`) - `simple` — простой кастомный тест: экран + touch + запись/проигрывание + наклон (IMU) - `argon2` — генерация masterSecret через Argon2id с SD-картой как памятью (тест скорости) -- `subserver-ui` — основной UI-прототип сабсервера SHiNE: NVS, PIN, Wi-Fi, серверы, кошелёк, QR, запросы +- `homeserver-ui` — основной UI-прототип homeserver SHiNE: NVS, PIN, Wi-Fi, серверы, кошелёк, QR, запросы - `text-test` — диагностический экран рендера текста: default font, U8g2 ASCII, U8g2 кириллица, кнопки с подписями - `gfx-text-test` — тот же тест рендера текста, но уже внутри новой папки `test_sketches/` - `gfx-layout-test` — тест геометрии и нижних рядов кнопок @@ -22,9 +22,9 @@ - `lvgl-interaction-test` — экран на `LVGL` с большим числом кнопок и сообщением о нажатой кнопке - `lvgl-touch-debug-test` — точечная диагностика touch: сырые координаты, маркер точки и большая тест-кнопка `LVGL` - `lvgl-official-based-test` — наш минимальный экран, но на максимально близкой к официальному `LVGL_Widgets` инициализации -- `lvgl-subserver-touch-test` — гибрид: `LVGL`-интерфейс, но display/touch init и raw touch-read взяты из `shine_subserver_ui`; подтверждено на устройстве, touch работает, зелёных линий по краям нет +- `lvgl-subserver-touch-test` — гибрид: `LVGL`-интерфейс, но display/touch init и raw touch-read взяты из `shine_homeserver_ui`; подтверждено на устройстве, touch работает, зелёных линий по краям нет - `lvgl-russian-font-test` — тест кастомного `LVGL`-шрифта с кириллицей: русские кнопки, длинные подписи и статусы -- `lvgl-nav-minimal-test` — новый минимальный UI-каркас сабсервера: `HOME`, `SETTINGS_MENU`, `Wi-Fi`, `Server`, `Account`, свайпы, крупные кнопки и реальная настройка Wi-Fi с сохранением в NVS +- `lvgl-nav-minimal-test` — новый минимальный UI-каркас homeserver: `HOME`, `SETTINGS_MENU`, `Wi-Fi`, `Server`, `Account`, свайпы, крупные кнопки и реальная настройка Wi-Fi с сохранением в NVS Запуск: @@ -32,7 +32,7 @@ - `./burn.sh audio` - `./burn.sh hello` - `./burn.sh simple` -- `./burn.sh subserver-ui` +- `./burn.sh homeserver-ui` - `./burn.sh text-test` - `./burn.sh gfx-text-test` - `./burn.sh gfx-layout-test` @@ -43,4 +43,4 @@ - `./burn.sh lvgl-subserver-touch-test` - `./burn.sh lvgl-russian-font-test` - `./burn.sh lvgl-nav-minimal-test` -- `./flash_shine_subserver_ui.sh` - автоматически находит USB-порт и заливает `shine_subserver_ui` +- `./flash_shine_homeserver_ui.sh` - автоматически находит USB-порт и заливает `shine_homeserver_ui` diff --git a/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/test-device/burn.sh b/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/test-device/burn.sh index 4783e44..763e059 100755 --- a/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/test-device/burn.sh +++ b/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/test-device/burn.sh @@ -34,7 +34,7 @@ case "${MODE}" in audio) SKETCH_DIR="${DEMO_BASE}/examples/07_ES8311" ;; simple) SKETCH_DIR="${ROOT_DIR}/simple_av_test" ;; argon2) SKETCH_DIR="${ROOT_DIR}/argon2_sd_test" ;; - subserver-ui) SKETCH_DIR="${ROOT_DIR}/shine_subserver_ui" ;; + homeserver-ui) SKETCH_DIR="${ROOT_DIR}/shine_homeserver_ui" ;; text-test) SKETCH_DIR="${ROOT_DIR}/text_render_test" ;; gfx-text-test) SKETCH_DIR="${ROOT_DIR}/test_sketches/gfx_text_render_test" ;; gfx-layout-test) SKETCH_DIR="${ROOT_DIR}/test_sketches/gfx_button_layout_test" ;; @@ -47,7 +47,7 @@ case "${MODE}" in lvgl-nav-minimal-test) SKETCH_DIR="${ROOT_DIR}/test_sketches/lvgl_nav_minimal_test" ;; *) echo "Unknown mode: ${MODE}" >&2 - echo "Use one of: hello, widgets, audio, simple, argon2, subserver-ui, text-test, gfx-text-test, gfx-layout-test, lvgl-basic-test, lvgl-interaction-test, lvgl-touch-debug-test, lvgl-official-based-test, lvgl-subserver-touch-test, lvgl-russian-font-test, lvgl-nav-minimal-test" >&2 + echo "Use one of: hello, widgets, audio, simple, argon2, homeserver-ui, text-test, gfx-text-test, gfx-layout-test, lvgl-basic-test, lvgl-interaction-test, lvgl-touch-debug-test, lvgl-official-based-test, lvgl-subserver-touch-test, lvgl-russian-font-test, lvgl-nav-minimal-test" >&2 exit 2 ;; esac diff --git a/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/test-device/flash_shine_subserver_ui.sh b/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/test-device/flash_shine_homeserver_ui.sh similarity index 92% rename from ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/test-device/flash_shine_subserver_ui.sh rename to ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/test-device/flash_shine_homeserver_ui.sh index 609b664..56ab206 100755 --- a/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/test-device/flash_shine_subserver_ui.sh +++ b/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/test-device/flash_shine_homeserver_ui.sh @@ -43,9 +43,9 @@ fi if [[ -z "${PORT}" ]]; then echo "Не удалось автоматически найти USB-порт ESP32." >&2 echo "Подключите плату и проверьте 'arduino-cli board list'." >&2 - echo "Либо укажите порт вручную: PORT=/dev/ttyACM0 ./flash_shine_subserver_ui.sh" >&2 + echo "Либо укажите порт вручную: PORT=/dev/ttyACM0 ./flash_shine_homeserver_ui.sh" >&2 exit 1 fi echo "== Найден порт: ${PORT}" -PORT="${PORT}" "${ROOT_DIR}/burn.sh" subserver-ui +PORT="${PORT}" "${ROOT_DIR}/burn.sh" homeserver-ui diff --git a/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/test-device/shine_subserver_ui/qrcode_bridge.c b/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/test-device/shine_homeserver_ui/qrcode_bridge.c similarity index 100% rename from ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/test-device/shine_subserver_ui/qrcode_bridge.c rename to ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/test-device/shine_homeserver_ui/qrcode_bridge.c diff --git a/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/test-device/shine_subserver_ui/shine_subserver_ui.ino b/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/test-device/shine_homeserver_ui/shine_homeserver_ui.ino similarity index 98% rename from ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/test-device/shine_subserver_ui/shine_subserver_ui.ino rename to ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/test-device/shine_homeserver_ui/shine_homeserver_ui.ino index be6eeb6..e071a95 100644 --- a/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/test-device/shine_subserver_ui/shine_subserver_ui.ino +++ b/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/test-device/shine_homeserver_ui/shine_homeserver_ui.ino @@ -97,7 +97,7 @@ enum ActionId { ACT_VERIFY_SERVERS, ACT_SET_TEST_SERVERS, ACT_EDIT_LOGIN, - ACT_EDIT_SUBSERVER, + ACT_EDIT_HOMESERVER, ACT_GENERATE_SECRET, ACT_CLEAR_ACCOUNT, ACT_SHOW_QR, @@ -137,7 +137,7 @@ enum EditTarget { EDIT_SSID, EDIT_WIFI_PASSWORD, EDIT_LOGIN, - EDIT_SUBSERVER, + EDIT_HOMESERVER, EDIT_API, EDIT_RPC, EDIT_WS, @@ -174,7 +174,7 @@ struct AppData { String wifiSsid; String wifiPassword; String login; - String subserverName; + String homeserverName; String secret; String walletAddress; String userPdaAddress; @@ -551,7 +551,7 @@ static bool canRegister() { static String registrationSummary() { if (gData.registered) { - return "Сабсервер активен"; + return "Homeserver активен"; } if (!gData.wifiReady) { return "Нужен Wi-Fi"; @@ -1179,7 +1179,7 @@ static bool awaitTransactionConfirmation(const String &signatureB58, String &mes return false; } -static bool registerSubserverOnSolana(String &messageOut) { +static bool registerHomeserverOnSolana(String &messageOut) { messageOut = ""; if (!gDerivedKeys.ready) { if (!restoreDerivedKeysFromSecret()) { @@ -1656,7 +1656,7 @@ static bool refreshWalletBalance(String &messageOut) { static void seedRequests() { gRequests[0].type = "Вход в сессию"; gRequests[0].actor = "Chrome / aidarkc"; - gRequests[0].details = "Клиент просит подключиться к сабсерверу и открыть сессию без ввода пароля."; + gRequests[0].details = "Клиент просит подключиться к homeserverу и открыть сессию без ввода пароля."; gRequests[0].status = "Ожидает"; gRequests[1].type = "Подпись сообщения"; @@ -1670,7 +1670,7 @@ static void loadDefaults() { gData.wifiSsid = ""; gData.wifiPassword = ""; gData.login = ""; - gData.subserverName = "subserver1"; + gData.homeserverName = "homeserver1"; gData.secret = ""; gData.walletAddress = ""; gData.userPdaAddress = ""; @@ -1692,7 +1692,7 @@ static void saveData() { gPrefs.putString("wifi_ssid", gData.wifiSsid); gPrefs.putString("wifi_pass", gData.wifiPassword); gPrefs.putString("login", gData.login); - gPrefs.putString("subserver", gData.subserverName); + gPrefs.putString("homeserver", gData.homeserverName); gPrefs.putString("secret", gData.secret); gPrefs.putString("wallet", gData.walletAddress); gPrefs.putString("user_pda", gData.userPdaAddress); @@ -1714,7 +1714,7 @@ static void loadData() { gData.wifiSsid = gPrefs.getString("wifi_ssid", gData.wifiSsid); gData.wifiPassword = gPrefs.getString("wifi_pass", gData.wifiPassword); gData.login = gPrefs.getString("login", gData.login); - gData.subserverName = gPrefs.getString("subserver", gData.subserverName); + gData.homeserverName = gPrefs.getString("homeserver", gData.homeserverName); gData.secret = gPrefs.getString("secret", gData.secret); gData.walletAddress = gPrefs.getString("wallet", gData.walletAddress); gData.userPdaAddress = gPrefs.getString("user_pda", gData.userPdaAddress); @@ -1758,8 +1758,8 @@ static void generateSecretAndWallet() { gData.registrationSignature = ""; gData.registered = false; gData.online = false; - if (gData.subserverName.length() == 0) { - gData.subserverName = "subserver1"; + if (gData.homeserverName.length() == 0) { + gData.homeserverName = "homeserver1"; } saveData(); } @@ -1815,7 +1815,7 @@ static String editTargetLabel() { case EDIT_SSID: return "SSID"; case EDIT_WIFI_PASSWORD: return "Пароль Wi-Fi"; case EDIT_LOGIN: return "Логин"; - case EDIT_SUBSERVER: return "Имя сабсервера"; + case EDIT_HOMESERVER: return "Имя homeserver"; case EDIT_API: return "API URL"; case EDIT_RPC: return "RPC URL"; case EDIT_WS: return "WS URL"; @@ -1846,7 +1846,7 @@ static void drawHomeScreen() { drawPanel(20, 92, 440, 98, C_PANEL, C_BORDER, 16); drawText(36, 122, registrationSummary(), canRegister() || gData.registered ? C_ACCENT : C_WARN, (const uint8_t *)FONT_HEAD); drawText(36, 152, "Логин: " + (gData.login.length() ? gData.login : "не задан"), C_TEXT, (const uint8_t *)FONT_BODY); - drawText(36, 174, "Сабсервер: " + gData.subserverName, C_MUTE, (const uint8_t *)FONT_BODY); + drawText(36, 174, "Homeserver: " + gData.homeserverName, C_MUTE, (const uint8_t *)FONT_BODY); drawPanel(20, 204, 210, 82, C_CARD, C_BORDER, 12); drawText(34, 232, "Wi-Fi", C_TEXT, (const uint8_t *)FONT_BODY); @@ -1871,7 +1871,7 @@ static void drawStatusScreen() { drawTopBar("Статус"); drawPanel(20, 92, 440, 286, C_PANEL, C_BORDER, 16); drawText(36, 122, "Логин: " + (gData.login.length() ? gData.login : "не задан"), C_TEXT); - drawText(36, 148, "Сабсервер: " + gData.subserverName, C_TEXT); + drawText(36, 148, "Homeserver: " + gData.homeserverName, C_TEXT); drawText(36, 174, "Секрет: " + boolText(gData.secretReady, "сохранён", "не задан"), gData.secretReady ? C_ACCENT : C_WARN); drawText(36, 200, "Отпечаток: " + (gData.secretReady ? shortenValue(gData.secret) : "-"), C_MUTE, (const uint8_t *)FONT_SMALL); drawText(36, 226, "Wi-Fi: " + boolText(gData.wifiReady, "готов", "не готов"), gData.wifiReady ? C_ACCENT : C_WARN); @@ -1947,13 +1947,13 @@ static void drawAccountScreen() { drawTopBar("Аккаунт"); drawPanel(20, 92, 440, 188, C_PANEL, C_BORDER, 16); drawText(36, 122, "Логин: " + (gData.login.length() ? gData.login : "не задан"), C_TEXT); - drawText(36, 152, "Сабсервер: " + gData.subserverName, C_TEXT); + drawText(36, 152, "Homeserver: " + gData.homeserverName, C_TEXT); drawText(36, 182, "Секрет: " + boolText(gData.secretReady, "сохранён", "не задан"), gData.secretReady ? C_ACCENT : C_WARN); drawText(36, 212, "Кошелёк: " + (gData.walletAddress.length() ? shortenValue(gData.walletAddress, 10, 8) : "не создан"), C_MUTE, (const uint8_t *)FONT_SMALL); drawText(36, 236, "Регистрация: " + boolText(gData.registered, "выполнена", "не выполнена"), gData.registered ? C_ACCENT : C_WARN); drawText(36, 260, "PDA: " + registrationDetailsShort(), C_MUTE, (const uint8_t *)FONT_SMALL); addButton(20, 300, 212, 48, ACT_EDIT_LOGIN, "Изменить логин"); - addButton(248, 300, 212, 48, ACT_EDIT_SUBSERVER, "Имя сабсервера"); + addButton(248, 300, 212, 48, ACT_EDIT_HOMESERVER, "Имя homeserver"); addButton(20, 360, 212, 48, ACT_GENERATE_SECRET, "Сгенерировать", true, C_OK); addButton(248, 360, 212, 48, ACT_CLEAR_ACCOUNT, "Очистить", true, C_BUTTON2); addButton(20, 420, 440, 36, ACT_BACK, "Назад"); @@ -2193,9 +2193,9 @@ static void applyEditValue() { gData.registrationSignature = ""; gNotice = "Логин сохранён"; break; - case EDIT_SUBSERVER: - gData.subserverName = value.length() ? value : "subserver1"; - gNotice = "Имя сабсервера сохранено"; + case EDIT_HOMESERVER: + gData.homeserverName = value.length() ? value : "homeserver1"; + gNotice = "Имя homeserver сохранено"; break; case EDIT_API: gData.apiUrl = value; @@ -2351,7 +2351,7 @@ static void handleAction(ActionId action) { } if (action == ACT_CONFIRM_YES) { if (gConfirmTarget == CONFIRM_REGISTER) { - registerSubserverOnSolana(gNotice); + registerHomeserverOnSolana(gNotice); } else if (gConfirmTarget == CONFIRM_CLEAR_ACCOUNT) { gData.secret = ""; gData.walletAddress = ""; @@ -2445,7 +2445,7 @@ static void handleAction(ActionId action) { gNeedRedraw = true; break; case ACT_EDIT_LOGIN: openEdit(EDIT_LOGIN, gData.login, false); break; - case ACT_EDIT_SUBSERVER: openEdit(EDIT_SUBSERVER, gData.subserverName, false); break; + case ACT_EDIT_HOMESERVER: openEdit(EDIT_HOMESERVER, gData.homeserverName, false); break; case ACT_GENERATE_SECRET: generateSecretAndWallet(); gNotice = gData.secretReady ? "Секрет сгенерирован, device-кошелёк выведен из него" : "Не удалось сгенерировать секрет"; diff --git a/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/test-device/test_sketches/README.md b/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/test-device/test_sketches/README.md index 19c379e..841dee6 100644 --- a/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/test-device/test_sketches/README.md +++ b/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/test-device/test_sketches/README.md @@ -2,7 +2,7 @@ Набор отдельных диагностических скетчей для `Waveshare ESP32-S3-Touch-AMOLED-2.16`. -Скетчи в этой папке нужны для быстрой проверки конкретных гипотез без влияния на основной `shine_subserver_ui`. +Скетчи в этой папке нужны для быстрой проверки конкретных гипотез без влияния на основной `shine_homeserver_ui`. ## Список @@ -12,7 +12,7 @@ - `lvgl_interaction_test/` - расширенный тест `LVGL` с 9 кнопками, touch-вводом и статусом нажатия - `lvgl_touch_debug_test/` - диагностика touch: сырые координаты, точка касания и одна большая кнопка `LVGL` - `lvgl_official_based_test/` - минимальный наш экран поверх максимально близкой к официальному `05_LVGL_Widgets` инициализации -- `lvgl_subserver_touch_test/` - гибридный тест: `LVGL`-экран с инициализацией дисплея и чтением touch из `shine_subserver_ui`; подтверждён на реальном устройстве +- `lvgl_subserver_touch_test/` - гибридный тест: `LVGL`-экран с инициализацией дисплея и чтением touch из `shine_homeserver_ui`; подтверждён на реальном устройстве - `lvgl_russian_font_test/` - тест кастомного кириллического `LVGL`-шрифта с русскими кнопками, длинными строками и рабочим touch - `lvgl_nav_minimal_test/` - новый минимальный навигационный каркас сабсервера на рабочем `LVGL + subserver touch`, расширенный настройкой Wi-Fi и сохранением в NVS diff --git a/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/test-device/test_sketches/lvgl_nav_minimal_test/lvgl_nav_minimal_test.ino b/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/test-device/test_sketches/lvgl_nav_minimal_test/lvgl_nav_minimal_test.ino index 1640db1..ae9bc23 100644 --- a/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/test-device/test_sketches/lvgl_nav_minimal_test/lvgl_nav_minimal_test.ino +++ b/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/test-device/test_sketches/lvgl_nav_minimal_test/lvgl_nav_minimal_test.ino @@ -48,7 +48,7 @@ #define TEXT_EDIT_PANEL_Y 112 #define TEXT_EDIT_PANEL_W 460 #define TEXT_EDIT_PANEL_H 330 -#define TEST_VERSION "SHiNE subserver (v.0.18)" +#define TEST_VERSION "SHiNE homeserver (v.0.18)" static const char *kShineUsersProgramId = "FZS1YctoeEhCkZ5VTjsysUFAXR8CqxYztcLboXcg2Rpm"; static const char *kShineUsersUserPdaSeedPrefix = "user_login="; @@ -60,7 +60,7 @@ static const uint8_t kBlockTypeServerProfile = 30; static const uint8_t kBlockTypeAccessServers = 40; static const uint8_t kBlockTypeSessions = 50; static const uint8_t kBlockTypeTrustedState = 70; -static const uint8_t kSessionTypeSubserver = 100; +static const uint8_t kSessionTypeHomeserver = 100; enum Screen { SCREEN_HOME, @@ -68,7 +68,7 @@ enum Screen { SCREEN_WIFI, SCREEN_SERVER, SCREEN_ACCOUNT, - SCREEN_ACCOUNT_SUBSERVER, + SCREEN_ACCOUNT_HOMESERVER, SCREEN_ACCOUNT_SECRET, SCREEN_SECRET_SHOW, SCREEN_SECRET_GENERATE_INFO, @@ -99,10 +99,10 @@ enum ActionId { ACTION_SERVER_EDIT_SOLANA, ACTION_SERVER_EDIT_SHINE, ACTION_ACCOUNT_EDIT_LOGIN, - ACTION_ACCOUNT_EDIT_SUBSERVER, + ACTION_ACCOUNT_EDIT_HOMESERVER, ACTION_ACCOUNT_EDIT_SECRET, - ACTION_ACCOUNT_SUBSERVER_USE_DEFAULT, - ACTION_ACCOUNT_SUBSERVER_EDIT_MANUAL, + ACTION_ACCOUNT_HOMESERVER_USE_DEFAULT, + ACTION_ACCOUNT_HOMESERVER_EDIT_MANUAL, ACTION_SECRET_SHOW, ACTION_SECRET_MANUAL, ACTION_SECRET_GENERATE, @@ -129,7 +129,7 @@ enum EditContext { EDIT_CONTEXT_SOLANA_RPC, EDIT_CONTEXT_SHINE_SERVER, EDIT_CONTEXT_LOGIN, - EDIT_CONTEXT_SUBSERVER, + EDIT_CONTEXT_HOMESERVER, EDIT_CONTEXT_SECRET_MANUAL, EDIT_CONTEXT_SECRET_GENERATE_PASSWORD, }; @@ -216,7 +216,7 @@ static String gSolanaRpcUrl = "https://api.devnet.solana.com"; static String gShineServerUrl = "https://shineup.me"; static String gServerStatusMessage = "Edit RPC or shine host"; static String gLoginValue; -static String gSubserverValue = "subserver1"; +static String gHomeserverValue = "homeserver1"; static bool gSecretConfigured = false; static String gSecretBase58; static uint8_t gSecretBytes[32] = {}; @@ -257,8 +257,8 @@ static String gBlockchainPubB58; static String gBlockchainPrivB58; static String gDevicePubB58; static String gDevicePrivB58; -static String gSubserverPubB58; -static String gSubserverPrivB58; +static String gHomeserverPubB58; +static String gHomeserverPrivB58; static EditContext gEditContext = EDIT_CONTEXT_NONE; static Screen gEditReturnScreen = SCREEN_HOME; @@ -305,7 +305,7 @@ static void restoreTextareaFromEditValue(); static void refreshDerivedKeys(); static void clearDerivedKeys(); static String loginDisplayValue(); -static String subserverDisplayValue(); +static String homeserverDisplayValue(); static String homeSecretStatus(); static String secretButtonValue(); static void clearSecretValue(); @@ -687,13 +687,13 @@ static bool parseUrlHostPortPath(const String &url, String &hostOut, uint16_t &p return hostOut.length() > 0 && portOut > 0; } -static String subserverKeySuffix() { - String name = gSubserverValue; +static String homeserverKeySuffix() { + String name = gHomeserverValue; name.trim(); if (name.isEmpty()) { - name = "subserver1"; + name = "homeserver1"; } - return String("subserver.key:") + name; + return String("homeserver.key:") + name; } static void deriveKeyPairFromSecretSuffix(const uint8_t *secret32, const String &suffix, String &pubB58, String &privB58) { @@ -718,8 +718,8 @@ static void clearDerivedKeys() { gBlockchainPrivB58 = ""; gDevicePubB58 = ""; gDevicePrivB58 = ""; - gSubserverPubB58 = ""; - gSubserverPrivB58 = ""; + gHomeserverPubB58 = ""; + gHomeserverPrivB58 = ""; } static void refreshDerivedKeys() { @@ -730,7 +730,7 @@ static void refreshDerivedKeys() { deriveKeyPairFromSecretSuffix(gSecretBytes, "root.key", gRootPubB58, gRootPrivB58); deriveKeyPairFromSecretSuffix(gSecretBytes, "bch.key", gBlockchainPubB58, gBlockchainPrivB58); deriveKeyPairFromSecretSuffix(gSecretBytes, "dev.key", gDevicePubB58, gDevicePrivB58); - deriveKeyPairFromSecretSuffix(gSecretBytes, subserverKeySuffix(), gSubserverPubB58, gSubserverPrivB58); + deriveKeyPairFromSecretSuffix(gSecretBytes, homeserverKeySuffix(), gHomeserverPubB58, gHomeserverPrivB58); } static bool deriveKeypairFromSeed32(const uint8_t seed32[32], uint8_t pub32[32], uint8_t sec64[64]) { @@ -1302,20 +1302,20 @@ static void refreshAccountPdaStatus() { mismatch = "blockchain key mismatch"; } else if (memcmp(devicePub, pdaState.deviceKey32, 32) != 0) { mismatch = "device key mismatch"; - } else if (gSubserverValue.isEmpty()) { - mismatch = "subserver not set"; + } else if (gHomeserverValue.isEmpty()) { + mismatch = "homeserver not set"; } else { bool foundSession = false; bool sessionMismatch = false; for (const auto &session : pdaState.sessions) { - if (session.sessionType == kSessionTypeSubserver && session.sessionName == gSubserverValue) { + if (session.sessionType == kSessionTypeHomeserver && session.sessionName == gHomeserverValue) { foundSession = true; - if (gSubserverPubB58.isEmpty()) { + if (gHomeserverPubB58.isEmpty()) { sessionMismatch = true; } else { - uint8_t subserverPub[32] = {}; - if (!base58ToFixed32(gSubserverPubB58, subserverPub) - || memcmp(subserverPub, session.sessionPubKey32, 32) != 0) { + uint8_t homeserverPub[32] = {}; + if (!base58ToFixed32(gHomeserverPubB58, homeserverPub) + || memcmp(homeserverPub, session.sessionPubKey32, 32) != 0) { sessionMismatch = true; } } @@ -1323,9 +1323,9 @@ static void refreshAccountPdaStatus() { } } if (!foundSession) { - mismatch = "subserver not in PDA"; + mismatch = "homeserver not in PDA"; } else if (sessionMismatch) { - mismatch = "subserver key mismatch"; + mismatch = "homeserver key mismatch"; } } @@ -1565,7 +1565,7 @@ static bool ensureShineSessionAuthenticated(String &errorOut) { errorOut = "Wi-Fi disconnected"; return false; } - if (gLoginValue.isEmpty() || !gSecretConfigured || gSubserverValue.isEmpty()) { + if (gLoginValue.isEmpty() || !gSecretConfigured || gHomeserverValue.isEmpty()) { errorOut = "account not configured"; return false; } @@ -1605,7 +1605,7 @@ static bool ensureShineSessionAuthenticated(String &errorOut) { uint8_t subPub[32] = {}; uint8_t subSec[64] = {}; if (!deriveSeedKeypairFromBase58(gDevicePrivB58, deviceSeed, devicePub, deviceSec) - || !deriveSeedKeypairFromBase58(gSubserverPrivB58, subSeed, subPub, subSec)) { + || !deriveSeedKeypairFromBase58(gHomeserverPrivB58, subSeed, subPub, subSec)) { errorOut = "local key derive failed"; return false; } @@ -1634,7 +1634,7 @@ static bool ensureShineSessionAuthenticated(String &errorOut) { + "\",\"sessionKey\":\"" + jsonEscape(sessionKey) + "\",\"timeMs\":" + String((unsigned long long)timeMs) + ",\"signatureB64\":\"" + jsonEscape(bytesToBase64String(signature, 64)) - + "\",\"clientInfo\":\"ESP32 subserver\"}"; + + "\",\"clientInfo\":\"ESP32 homeserver\"}"; String loginResp; if (shineWsRequest(gShineWs, "SessionLogin", loginReq, loginResp)) { if (jsonInt64Field(loginResp, "status", statusCode) && statusCode == 200) { @@ -1691,7 +1691,7 @@ static bool ensureShineSessionAuthenticated(String &errorOut) { + ",\"authNonce\":\"" + jsonEscape(authNonce) + "\",\"deviceKey\":\"" + jsonEscape(bytesToBase64String(devicePub, 32)) + "\",\"signatureB64\":\"" + jsonEscape(bytesToBase64String(signature, 64)) - + "\",\"clientInfo\":\"ESP32 subserver\"}"; + + "\",\"clientInfo\":\"ESP32 homeserver\"}"; String createResp; if (!shineWsRequest(gShineWs, "CreateAuthSession", createReq, createResp)) { errorOut = "CreateAuthSession failed"; @@ -1710,7 +1710,7 @@ static bool ensureShineSessionAuthenticated(String &errorOut) { static void manageShineConnection() { String serverLabel = gShineServerUrl.isEmpty() ? "not set" : gShineServerUrl; - if (gLoginValue.isEmpty() || !gSecretConfigured || gSubserverValue.isEmpty()) { + if (gLoginValue.isEmpty() || !gSecretConfigured || gHomeserverValue.isEmpty()) { gShineStatusLine = String("SHiNE: ") + serverLabel + " account not configured"; clearShineSessionState(false); return; @@ -1802,7 +1802,7 @@ static void loadPrefs() { gSolanaRpcUrl = gPrefs.getString("solana_rpc", "https://api.devnet.solana.com"); gShineServerUrl = gPrefs.getString("shine_server", "https://shineup.me"); gLoginValue = gPrefs.getString("login", ""); - gSubserverValue = gPrefs.getString("subserver", "subserver1"); + gHomeserverValue = gPrefs.getString("homeserver", "homeserver1"); gSecretConfigured = gPrefs.getBool("secret_set", false); gSecretBase58 = gPrefs.getString("secret_b58", ""); if (gSecretConfigured && gPrefs.getBytesLength("secret_bytes") == 32) { @@ -1850,7 +1850,7 @@ static void saveServerPrefs() { static void saveAccountPrefs() { gPrefs.putString("login", gLoginValue); - gPrefs.putString("subserver", gSubserverValue); + gPrefs.putString("homeserver", gHomeserverValue); gPrefs.putBool("secret_set", gSecretConfigured); gPrefs.putString("secret_b58", gSecretBase58); if (gSecretConfigured) { @@ -1925,8 +1925,8 @@ static String loginDisplayValue() { return gLoginValue.isEmpty() ? "login not set" : gLoginValue; } -static String subserverDisplayValue() { - return gSubserverValue.isEmpty() ? "subserver not set" : gSubserverValue; +static String homeserverDisplayValue() { + return gHomeserverValue.isEmpty() ? "homeserver not set" : gHomeserverValue; } static String homeSecretStatus() { @@ -2234,13 +2234,13 @@ static void applyEditorValue() { return; } - if (gEditContext == EDIT_CONTEXT_SUBSERVER) { + if (gEditContext == EDIT_CONTEXT_HOMESERVER) { value.trim(); - gSubserverValue = value; + gHomeserverValue = value; refreshDerivedKeys(); saveAccountPrefs(); markAccountStateDirty(); - gAccountStatusMessage = gSubserverValue.isEmpty() ? "Subserver cleared" : "Subserver saved"; + gAccountStatusMessage = gHomeserverValue.isEmpty() ? "Homeserver cleared" : "Homeserver saved"; showScreen(SCREEN_ACCOUNT); return; } @@ -2465,8 +2465,8 @@ static void actionButtonCb(lv_event_t *event) { gLoginValue, false); break; - case ACTION_ACCOUNT_EDIT_SUBSERVER: - showScreen(SCREEN_ACCOUNT_SUBSERVER); + case ACTION_ACCOUNT_EDIT_HOMESERVER: + showScreen(SCREEN_ACCOUNT_HOMESERVER); break; case ACTION_ACCOUNT_EDIT_SECRET: showScreen(SCREEN_ACCOUNT_SECRET); @@ -2506,20 +2506,20 @@ static void actionButtonCb(lv_event_t *event) { case ACTION_SECRET_GENERATE_CANCEL_NO: showScreen(SCREEN_SECRET_GENERATE_RUNNING); break; - case ACTION_ACCOUNT_SUBSERVER_USE_DEFAULT: - gSubserverValue = "subserver1"; + case ACTION_ACCOUNT_HOMESERVER_USE_DEFAULT: + gHomeserverValue = "homeserver1"; refreshDerivedKeys(); saveAccountPrefs(); markAccountStateDirty(); - gAccountStatusMessage = "Subserver set to subserver1"; + gAccountStatusMessage = "Homeserver set to homeserver1"; showScreen(SCREEN_ACCOUNT); break; - case ACTION_ACCOUNT_SUBSERVER_EDIT_MANUAL: - openEditor(EDIT_CONTEXT_SUBSERVER, + case ACTION_ACCOUNT_HOMESERVER_EDIT_MANUAL: + openEditor(EDIT_CONTEXT_HOMESERVER, SCREEN_ACCOUNT, - "EDIT SUBSERVER", + "EDIT HOMESERVER", "", - gSubserverValue, + gHomeserverValue, false); break; case ACTION_BACK_SECRET_MENU: @@ -2616,17 +2616,17 @@ static void makeVersionTag() { static void drawHome() { setRootStyle(); - lv_obj_t *subserver = lv_label_create(gRoot); - lv_label_set_text(subserver, subserverDisplayValue().c_str()); - lv_obj_set_style_text_font(subserver, &lv_font_montserrat_18, 0); - lv_obj_set_style_text_color(subserver, lv_color_hex(0xFFFFFF), 0); - lv_obj_align(subserver, LV_ALIGN_TOP_LEFT, 24, 18); + lv_obj_t *homeserver = lv_label_create(gRoot); + lv_label_set_text(homeserver, homeserverDisplayValue().c_str()); + lv_obj_set_style_text_font(homeserver, &lv_font_montserrat_18, 0); + lv_obj_set_style_text_color(homeserver, lv_color_hex(0xFFFFFF), 0); + lv_obj_align(homeserver, LV_ALIGN_TOP_LEFT, 24, 18); lv_obj_t *login = lv_label_create(gRoot); lv_label_set_text(login, loginDisplayValue().c_str()); lv_obj_set_style_text_font(login, &lv_font_montserrat_22, 0); lv_obj_set_style_text_color(login, lv_color_hex(0xD5DEE7), 0); - lv_obj_align_to(login, subserver, LV_ALIGN_OUT_BOTTOM_LEFT, 0, 6); + lv_obj_align_to(login, homeserver, LV_ALIGN_OUT_BOTTOM_LEFT, 0, 6); lv_obj_t *accountDot = lv_obj_create(gRoot); lv_obj_set_size(accountDot, 14, 14); @@ -2793,23 +2793,23 @@ static void drawAccountScreen() { showMessageAt(gAccountStatusMessage, 56); String loginButton = String("Login (") + (gLoginValue.isEmpty() ? "not set" : gLoginValue) + ")"; - String subserverButton = String("Subserver (") + (gSubserverValue.isEmpty() ? "not set" : gSubserverValue) + ")"; + String homeserverButton = String("Homeserver (") + (gHomeserverValue.isEmpty() ? "not set" : gHomeserverValue) + ")"; String secretButton = String("Secret (") + secretButtonValue() + ")"; makeButton(loginButton.c_str(), 22, 118, 436, 84, 0x355C7D, ACTION_ACCOUNT_EDIT_LOGIN, &lv_font_montserrat_20); - makeButton(subserverButton.c_str(), 22, 222, 436, 84, 0x355C7D, ACTION_ACCOUNT_EDIT_SUBSERVER, &lv_font_montserrat_20); + makeButton(homeserverButton.c_str(), 22, 222, 436, 84, 0x355C7D, ACTION_ACCOUNT_EDIT_HOMESERVER, &lv_font_montserrat_20); makeButton(secretButton.c_str(), 22, 326, 436, 84, 0x355C7D, ACTION_ACCOUNT_EDIT_SECRET, &lv_font_montserrat_20); makeBody("Swipe right to return to Settings.", 420, 420); makeVersionTag(); } -static void drawAccountSubserverScreen() { +static void drawAccountHomeserverScreen() { setRootStyle(); - makeTitle("SUBSERVER", 18, &lv_font_montserrat_24); - showMessageAt(String("Current: ") + subserverDisplayValue(), 56); - makeBody("If you only use one subserver, keep the default name subserver1.", 98, 420); - makeButton("USE SUBSERVER1", 22, 202, 436, 84, 0x2A9D8F, ACTION_ACCOUNT_SUBSERVER_USE_DEFAULT, &lv_font_montserrat_22); - makeButton("EDIT MANUALLY", 22, 306, 436, 84, 0x355C7D, ACTION_ACCOUNT_SUBSERVER_EDIT_MANUAL, &lv_font_montserrat_22); + makeTitle("HOMESERVER", 18, &lv_font_montserrat_24); + showMessageAt(String("Current: ") + homeserverDisplayValue(), 56); + makeBody("If you only use one homeserver, keep the default name homeserver1.", 98, 420); + makeButton("USE HOMESERVER1", 22, 202, 436, 84, 0x2A9D8F, ACTION_ACCOUNT_HOMESERVER_USE_DEFAULT, &lv_font_montserrat_22); + makeButton("EDIT MANUALLY", 22, 306, 436, 84, 0x355C7D, ACTION_ACCOUNT_HOMESERVER_EDIT_MANUAL, &lv_font_montserrat_22); makeButton("BACK", 140, 402, 200, 54, 0x5A6570, ACTION_BACK_ACCOUNT, &lv_font_montserrat_20); makeVersionTag(); } @@ -2876,8 +2876,8 @@ static void drawSecretShowScreen() { 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("Subserver key (base58)", String("pub from sha256(base64(secret)|") + subserverKeySuffix() + ")", gSubserverPubB58); - addKeyBlock("Subserver key priv (base58)", String("sha256(base64(secret)|") + subserverKeySuffix() + ")", gSubserverPrivB58); + addKeyBlock("Homeserver key (base58)", String("pub from sha256(base64(secret)|") + homeserverKeySuffix() + ")", gHomeserverPubB58); + addKeyBlock("Homeserver key priv (base58)", String("sha256(base64(secret)|") + homeserverKeySuffix() + ")", gHomeserverPrivB58); } else { showMessageAt("Secret not set", 96); } @@ -3084,8 +3084,8 @@ static void rebuildScreen() { case SCREEN_ACCOUNT: drawAccountScreen(); break; - case SCREEN_ACCOUNT_SUBSERVER: - drawAccountSubserverScreen(); + case SCREEN_ACCOUNT_HOMESERVER: + drawAccountHomeserverScreen(); break; case SCREEN_ACCOUNT_SECRET: drawAccountSecretScreen(); @@ -3219,7 +3219,7 @@ static void handleSwipe(SwipeDirection swipe) { case SCREEN_ACCOUNT: handleAccountSwipe(swipe); break; - case SCREEN_ACCOUNT_SUBSERVER: + case SCREEN_ACCOUNT_HOMESERVER: case SCREEN_ACCOUNT_SECRET: handleAccountSubscreenSwipe(swipe); break; diff --git a/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/test-device/test_sketches/lvgl_subserver_touch_test/lvgl_subserver_touch_test.ino b/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/test-device/test_sketches/lvgl_subserver_touch_test/lvgl_subserver_touch_test.ino index d76b1e9..8308cca 100644 --- a/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/test-device/test_sketches/lvgl_subserver_touch_test/lvgl_subserver_touch_test.ino +++ b/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/test-device/test_sketches/lvgl_subserver_touch_test/lvgl_subserver_touch_test.ino @@ -4,7 +4,7 @@ #include #include -// Подтверждено на устройстве: LVGL-рендер работает вместе с touch-путём из shine_subserver_ui. +// Подтверждено на устройстве: LVGL-рендер работает вместе с touch-путём из shine_homeserver_ui. #define PIN_LCD_CS 12 #define PIN_LCD_SCLK 38 @@ -146,7 +146,7 @@ static void createUi() { lv_obj_align(gVersionLabel, LV_ALIGN_TOP_MID, 0, 12); lv_obj_t *subtitle = lv_label_create(screen); - lv_label_set_text(subtitle, "Touch path comes from shine_subserver_ui. Tap buttons and watch status."); + lv_label_set_text(subtitle, "Touch path comes from shine_homeserver_ui. Tap buttons and watch status."); lv_obj_set_width(subtitle, 436); lv_label_set_long_mode(subtitle, LV_LABEL_LONG_WRAP); lv_obj_set_style_text_font(subtitle, &lv_font_montserrat_14, 0); diff --git a/VERSION.properties b/VERSION.properties index 38e42da..6fff200 100644 --- a/VERSION.properties +++ b/VERSION.properties @@ -1,2 +1,2 @@ -client.version=1.2.161 -server.version=1.2.150 +client.version=1.2.162 +server.version=1.2.151 diff --git a/shine-UI/js/pages/register-view.js b/shine-UI/js/pages/register-view.js index 4492b0a..3038f76 100644 --- a/shine-UI/js/pages/register-view.js +++ b/shine-UI/js/pages/register-view.js @@ -176,6 +176,11 @@ export function render({ navigate }) { const prevPassword = String(state.registrationDraft.password || ''); const nextLogin = String(loginInput.value.trim()); const nextPassword = String(passwordInput.value || ''); + if (nextPassword.length === 0) { + formError.textContent = 'Пустой пароль запрещён. Введите непустой пароль для регистрации.'; + formError.style.display = ''; + return; + } const credsChanged = prevLogin !== nextLogin || prevPassword !== nextPassword; state.registrationDraft.login = nextLogin; diff --git a/shine-UI/js/pages/topup-view.js b/shine-UI/js/pages/topup-view.js index 2c4a2eb..bfc87fd 100644 --- a/shine-UI/js/pages/topup-view.js +++ b/shine-UI/js/pages/topup-view.js @@ -1,7 +1,6 @@ import { renderHeader } from '../components/header.js'; import { state } from '../state.js'; import { - deriveWalletFromPassword, formatSol, getBalanceSol, getTopupSiteUrl, @@ -10,6 +9,21 @@ import { export const pageMeta = { id: 'topup-view', title: 'Пополнение счета', showAppChrome: false }; +// Канонический Solana-адрес пополнения = публичный device-ключ из сгенерированного набора ключей. +// Тот же путь, что в registration-payment-view (deriveUserWalletAddress); не выводим адрес +// напрямую из пароля, иначе он расходится с device-ключом регистрации. +async function deviceWalletAddressFromBundle() { + const keyBundle = state.registrationDraft.preGeneratedKeyBundle; + if (!keyBundle || !keyBundle.devicePair) { + throw new Error('Ключи ещё не сгенерированы. Вернитесь на экран регистрации.'); + } + const raw = atob(keyBundle.devicePair.publicKeyB64); + const bytes = new Uint8Array(raw.length); + for (let i = 0; i < raw.length; i += 1) bytes[i] = raw.charCodeAt(i); + const { PublicKey } = await import('https://esm.sh/@solana/web3.js@1.98.4'); + return new PublicKey(bytes).toBase58(); +} + export function render({ navigate }) { const screen = document.createElement('section'); screen.className = 'stack'; @@ -55,7 +69,7 @@ export function render({ navigate }) { Открыть сайт пополнения
-
Кошелёк для пополнения (wallet.key)
+
Кошелёк для пополнения (device key = Solana wallet)
`; card.children[3].append(walletRow); @@ -103,9 +117,9 @@ export function render({ navigate }) { (async () => { try { if (!walletValue.value) { - const wallet = await deriveWalletFromPassword(String(state.registrationDraft.password ?? '')); - state.registrationPayment.walletAddress = wallet.address; - walletValue.value = wallet.address; + const address = await deviceWalletAddressFromBundle(); + state.registrationPayment.walletAddress = address; + walletValue.value = address; } const topupSiteLink = card.querySelector('#topup-site-link'); if (topupSiteLink instanceof HTMLAnchorElement) { diff --git a/shine-UI/js/services/crypto-utils.js b/shine-UI/js/services/crypto-utils.js index f9b5ff5..d15f291 100644 --- a/shine-UI/js/services/crypto-utils.js +++ b/shine-UI/js/services/crypto-utils.js @@ -116,12 +116,6 @@ export async function sha256Text(text) { return sha256Bytes(utf8Bytes(text)); } -export async function derivePasswordSeed(password, suffix) { - const base = await sha256Text(password || ''); - const concat = `${bytesToBase64(base)}${suffix}`; - return sha256Text(concat); -} - function normalizeLoginForKdf(login) { return String(login || '').trim().toLowerCase(); } @@ -134,21 +128,6 @@ async function makeArgon2Salt(login, suffix) { return digest.slice(0, 16); } -async function derivePasswordSeedArgon2id({ login, password, suffix }) { - const normalizedLogin = normalizeLoginForKdf(login); - const normalizedPassword = String(password ?? ''); - const normalizedSuffix = String(suffix || '').trim(); - const salt = await makeArgon2Salt(normalizedLogin, normalizedSuffix); - const passBytes = utf8Bytes(`${normalizedLogin}\n${normalizedPassword}`); - const out = await argon2idAsync(passBytes, salt, { - t: 2, - m: 65536, - p: 1, - dkLen: 32, - }); - return new Uint8Array(out); -} - async function deriveMasterSecretArgon2id({ login, password, onProgress }) { const normalizedLogin = normalizeLoginForKdf(login); const normalizedPassword = String(password ?? ''); @@ -177,38 +156,13 @@ function ed25519Pkcs8FromSeed(seed32) { return out; } -export async function deriveEd25519FromPassword(password, suffix, options = {}) { - const normalizedPassword = String(password ?? ''); - const normalizedLogin = String(options?.login ?? ''); - const useLegacyEmptyPassword = normalizedPassword.length === 0; - const seed = useLegacyEmptyPassword - ? await derivePasswordSeed(normalizedPassword, suffix) - : await derivePasswordSeedArgon2id({ - login: normalizedLogin, - password: normalizedPassword, - suffix, - }); - const pkcs8 = ed25519Pkcs8FromSeed(seed); - const subtle = getSubtleApi(); - const privateKey = await subtle.importKey('pkcs8', pkcs8, { name: 'Ed25519' }, true, ['sign']); - const jwk = await subtle.exportKey('jwk', privateKey); - if (!jwk.x) throw new Error('Не удалось получить публичный ключ Ed25519'); - - return { - privateKey, - publicKeyB64: bytesToBase64(base64ToBytes(base64UrlToBase64(jwk.x))), - privatePkcs8B64: bytesToBase64(pkcs8), - }; -} - export async function deriveMasterSecretFromPassword(password, options = {}) { const normalizedPassword = String(password ?? ''); const normalizedLogin = String(options?.login ?? ''); const onProgress = typeof options?.onProgress === 'function' ? options.onProgress : undefined; if (normalizedPassword.length === 0) { - const legacy = await derivePasswordSeed(normalizedPassword, 'master.secret'); - if (onProgress) onProgress(1); - return legacy; + // Пустой пароль запрещён: упрощённый легаси-путь убран, регистрация/вход требуют непустой пароль. + throw new Error('Пустой пароль запрещён: регистрация и вход требуют непустой пароль.'); } return deriveMasterSecretArgon2id({ login: normalizedLogin, diff --git a/shine-UI/js/services/shine-user-pda-service.js b/shine-UI/js/services/shine-user-pda-service.js index d4491b8..4d41627 100644 --- a/shine-UI/js/services/shine-user-pda-service.js +++ b/shine-UI/js/services/shine-user-pda-service.js @@ -25,7 +25,7 @@ const BLOCK_TYPE_SESSIONS = 50; const BLOCK_TYPE_TRUSTED_STATE = 70; const SESSIONS_MODE_MIXED = 1; const SESSION_TYPE_USER = 1; -const SESSION_TYPE_SUBSERVER = 100; +const SESSION_TYPE_HOMESERVER = 100; let solanaLibPromise = null; function loadSolanaLib() { diff --git a/shine-UI/js/services/solana-wallet-service.js b/shine-UI/js/services/solana-wallet-service.js index 4c5a882..4582a18 100644 --- a/shine-UI/js/services/solana-wallet-service.js +++ b/shine-UI/js/services/solana-wallet-service.js @@ -1,4 +1,3 @@ -import { deriveEd25519FromPassword } from './crypto-utils.js'; import { extractDeviceKey32FromStoredValue } from './device-key-utils.js'; import { loadEncryptedUserSecrets } from './key-vault.js'; import { SOLANA_ENDPOINT_DEFAULT } from '../solana-programs.js'; @@ -68,17 +67,6 @@ async function keypairFromPkcs8(pkcs8B64) { return solana.Keypair.fromSeed(seed32); } -export async function deriveWalletFromPassword(password) { - const keyBundle = await deriveEd25519FromPassword(String(password ?? ''), 'dev.key'); - const keypair = await keypairFromPkcs8(keyBundle.privatePkcs8B64); - return { - address: keypair.publicKey.toBase58(), - keypair, - devicePublicKeyB64: keyBundle.publicKeyB64, - devicePrivatePkcs8B64: keyBundle.privatePkcs8B64, - }; -} - export async function createRandomSolanaWallet() { const solana = await loadSolanaLib(); const keypair = solana.Keypair.generate(); diff --git a/shine-solana/shine/doc/formats/shine-user-pda-format-v.1.0.md b/shine-solana/shine/doc/formats/shine-user-pda-format-v.1.0.md index e520ff9..1039e38 100644 --- a/shine-solana/shine/doc/formats/shine-user-pda-format-v.1.0.md +++ b/shine-solana/shine/doc/formats/shine-user-pda-format-v.1.0.md @@ -90,7 +90,7 @@ UserPdaRecordV1 | `3` | `BlockchainRegistryBlock` | Один или несколько блокчейнов пользователя. | | `30` | `ServerProfileBlock` | Серверные данные пользователя. | | `40` | `AccessServersBlock` | Серверы доступа/relay. | -| `50` | `SessionsBlock` | Опубликованные пользовательские сессии и саб-серверы. | +| `50` | `SessionsBlock` | Опубликованные пользовательские сессии и homeserver-ы. | | `70` | `TrustedStateBlock` | Счетчик trusted-связей. | | `255` | `ReservedBlock` | Зарезервировано, пока не используется. | @@ -309,7 +309,7 @@ SessionRecord | Значение | Смысл | |----------|-------| | `1` | Обычная пользовательская сессия. | -| `100` | Саб-сервер пользователя. | +| `100` | Homeserver пользователя. | Правила: diff --git a/shine-solana/shine/doc/programs/shine_users.md b/shine-solana/shine/doc/programs/shine_users.md index 46a6c3d..7f01b48 100644 --- a/shine-solana/shine/doc/programs/shine_users.md +++ b/shine-solana/shine/doc/programs/shine_users.md @@ -91,6 +91,18 @@ system-переводом. Если бы создание шло строго ч Проверки повторной инициализации (`owner == System Program` и пустые данные) остаются и не зависят от баланса аккаунта. +### 3.4. Строгий список аккаунтов (нет «лишних» аккаунтов) + +Все инструкции `shine_users` читают строго фиксированный набор аккаунтов и после этого +явно требуют, чтобы в переданном списке больше ничего не было +(`require!(it.next().is_none(), InvalidInstruction)`). Если вызывающий добавит лишние +аккаунты в хвост, инструкция завершится ошибкой `InvalidInstruction (1)`. + +Это не закрывает отдельной уязвимости (каждый используемый аккаунт и так строго +валидируется по signer/owner/адресу PDA), а является defense-in-depth и приводит поведение +к единому виду с `shine_payments`, где такая же проверка стоит во всех инструкциях. +Списки аккаунтов в разделах ниже надо считать исчерпывающими и точными по количеству. + ## 4. Состояния программы ### 4.1. `UsersEconomyConfigState` diff --git a/shine-solana/shine/programs/shine_users/src/lib.rs b/shine-solana/shine/programs/shine_users/src/lib.rs index b59709e..eeb869e 100644 --- a/shine-solana/shine/programs/shine_users/src/lib.rs +++ b/shine-solana/shine/programs/shine_users/src/lib.rs @@ -40,7 +40,7 @@ const BLOCKCHAIN_TYPE_MAIN_USER: u8 = 1; const SESSIONS_MODE_MIXED: u8 = 1; const SESSIONS_MODE_PDA_ONLY: u8 = 10; const SESSION_TYPE_USER: u8 = 1; -const SESSION_TYPE_SUBSERVER: u8 = 100; +const SESSION_TYPE_HOMESERVER: u8 = 100; const LAST_BLOCK_STATE_PREFIX: &[u8] = b"SHiNE_LAST_BLOCK"; const IX_INIT_USERS_ECONOMY_CONFIG: u8 = 1; @@ -375,6 +375,7 @@ fn process_init_users_economy_config(program_id: &Pubkey, accounts: &[AccountInf let signer = next_account_info(&mut it)?; let users_economy_config_pda = next_account_info(&mut it)?; let system_program_ai = next_account_info(&mut it)?; + require!(it.next().is_none(), ShineUsersError::InvalidInstruction); require!(signer.is_signer, ShineUsersError::InvalidSigner); require_keys_eq!(*system_program_ai.key, system_program::id(), ShineUsersError::InvalidSystemProgram); @@ -407,6 +408,7 @@ fn process_update_users_economy_config(program_id: &Pubkey, accounts: &[AccountI let mut it = accounts.iter(); let signer = next_account_info(&mut it)?; let users_economy_config_pda = next_account_info(&mut it)?; + require!(it.next().is_none(), ShineUsersError::InvalidInstruction); require!(signer.is_signer, ShineUsersError::InvalidSigner); let dao_authority = Pubkey::from_str(settings::DAO_AUTHORITY).map_err(|_| ProgramError::from(ShineUsersError::InvalidSigner))?; @@ -433,6 +435,7 @@ fn process_create_user_pda(program_id: &Pubkey, accounts: &[AccountInfo], args: let instructions_sysvar = next_account_info(&mut it)?; let users_economy_config_pda = next_account_info(&mut it)?; let login_guard_program = next_account_info(&mut it)?; + require!(it.next().is_none(), ShineUsersError::InvalidInstruction); require!(signer.is_signer, ShineUsersError::InvalidSigner); require_keys_eq!(*signer.key, args.fields.device_key, ShineUsersError::InvalidSigner); @@ -514,6 +517,7 @@ fn process_update_user_pda(program_id: &Pubkey, accounts: &[AccountInfo], args: let inflow_vault = next_account_info(&mut it)?; let instructions_sysvar = next_account_info(&mut it)?; let users_economy_config_pda = next_account_info(&mut it)?; + require!(it.next().is_none(), ShineUsersError::InvalidInstruction); require!(signer.is_signer, ShineUsersError::InvalidSigner); require_keys_eq!(*signer.key, args.fields.device_key, ShineUsersError::InvalidSigner); @@ -976,7 +980,7 @@ fn validate_sessions_fields(mode: u8, sessions: &[SessionRecord]) -> ProgramResu } fn validate_session_record(session: &SessionRecord) -> ProgramResult { - require!(session.session_type == SESSION_TYPE_USER || session.session_type == SESSION_TYPE_SUBSERVER, ShineUsersError::InvalidRecordData); + require!(session.session_type == SESSION_TYPE_USER || session.session_type == SESSION_TYPE_HOMESERVER, ShineUsersError::InvalidRecordData); require!(session.session_version == 1, ShineUsersError::InvalidRecordData); let bytes = session.session_name.as_bytes(); require!(!bytes.is_empty(), ShineUsersError::InvalidRecordData);