Миграция PDA на client.key

This commit is contained in:
AidarKC
2026-06-22 21:57:09 +04:00
parent ba348dafb3
commit 5c92b6a734
133 changed files with 941 additions and 30531 deletions
+2 -2
View File
@@ -35,7 +35,7 @@
"blockchainName": "anya-001",
"solanaKey": "BASE64_32_PUBLIC_KEY",
"blockchainKey": "BASE64_32_PUBLIC_KEY",
"deviceKey": "BASE64_32_PUBLIC_KEY",
"clientKey": "BASE64_32_PUBLIC_KEY",
"bchLimit": 1000000
}
}
@@ -99,7 +99,7 @@
"blockchainName": "anya-001",
"solanaKey": "BASE64_32_PUBLIC_KEY",
"blockchainKey": "BASE64_32_PUBLIC_KEY",
"deviceKey": "BASE64_32_PUBLIC_KEY",
"clientKey": "BASE64_32_PUBLIC_KEY",
"serverLastGlobalNumber": 128,
"serverLastGlobalHash": "4f...ab",
"serverBlockchainSizeBytes": 45212,
+13 -13
View File
@@ -11,7 +11,7 @@
Логика раздела такая:
- сначала клиент либо начинает создание новой сессии через `deviceKey`;
- сначала клиент либо начинает создание новой сессии через `clientKey`;
- либо начинает вход в уже созданную сессию через `sessionKey`;
- сервер на первом шаге выдаёт challenge/nonce;
- на втором шаге клиент присылает подписанный ответ;
@@ -55,7 +55,7 @@
2. Вход в существующую сессию:
`SessionChallenge` -> `SessionLogin`
`deviceKey` используется для создания новой сессии.
`clientKey` используется для создания новой сессии.
`sessionKey` используется для входа в уже созданную сессию.
@@ -119,7 +119,7 @@ ed25519/BASE64_PUBLIC_KEY
"storagePwd": "BASE64_OR_APP_SPECIFIC_SECRET",
"timeMs": 1774600000123,
"authNonce": "nonce",
"deviceKey": "BASE64_DEVICE_PUBLIC_KEY",
"clientKey": "BASE64_DEVICE_PUBLIC_KEY",
"signatureB64": "BASE64_SIGNATURE",
"sessionType": 1,
"clientPlatform": "Web",
@@ -138,15 +138,15 @@ AUTH_CREATE_SESSION:{login}:{sessionKey}:{storagePwd}:{timeMs}:{authNonce}
Перед проверкой подписи сервер должен:
1. взять актуальный `solana_users.device_key`;
2. сравнить его с `payload.deviceKey`;
1. взять актуальный `solana_users.client_key`;
2. сравнить его с `payload.clientKey`;
3. только потом проверять подпись.
Если ключ не совпадает, сервер возвращает ошибку `DEVICE_KEY_NOT_ACTUAL`.
Если `clientKey` не совпадает, сервер возвращает ошибку `DEVICE_KEY_NOT_ACTUAL`.
На будущее:
- для ротации `device_key` желательно добавить перепроверку через Solana.
- для ротации `client_key` желательно добавить перепроверку через Solana.
### Успешный ответ
@@ -172,15 +172,15 @@ AUTH_CREATE_SESSION:{login}:{sessionKey}:{storagePwd}:{timeMs}:{authNonce}
- `501 / NO_LOGIN` — у пользователя на сервере не заполнен `login`.
- `400 / EMPTY_STORAGE_PWD` — пустой `storagePwd`.
- `400 / EMPTY_SESSION_KEY` — пустой `sessionKey`.
- `422 / UNSUPPORTED_KEY_ALGORITHM` — префикс алгоритма в `sessionKey` или `deviceKey` не поддерживается текущим сервером.
- `400 / BAD_BASE64` — неверный Base64 в `sessionKey`, `deviceKey` или `signatureB64`.
- `422 / UNSUPPORTED_KEY_ALGORITHM` — префикс алгоритма в `sessionKey` или `clientKey` не поддерживается текущим сервером.
- `400 / BAD_BASE64` — неверный Base64 в `sessionKey`, `clientKey` или `signatureB64`.
- `400 / EMPTY_SIGNATURE` — пустая подпись.
- `400 / TIME_SKEW` — время клиента отличается от серверного больше допустимого окна.
- `400 / NO_DEVICE_KEY` — у пользователя в БД отсутствует `deviceKey`.
- `400 / NO_DEVICE_KEY` — у пользователя в БД отсутствует `clientKey`.
- `400 / EMPTY_AUTH_NONCE` — пустой `authNonce`.
- `400 / AUTH_NONCE_MISMATCH``authNonce` не соответствует значению из `AuthChallenge`.
- `400 / EMPTY_DEVICE_KEY` — в запросе не передан `deviceKey`.
- `422 / DEVICE_KEY_NOT_ACTUAL``deviceKey` не совпадает с актуальной версией на сервере.
- `400 / EMPTY_DEVICE_KEY` — в запросе не передан `clientKey`.
- `422 / DEVICE_KEY_NOT_ACTUAL``clientKey` не совпадает с актуальной версией на сервере.
- `422 / BAD_SIGNATURE` — подпись не прошла проверку.
- `460 / SESSION_TYPE_MISMATCH``sessionType` не совпадает с типом сессии, уже опубликованным для этого `sessionKey` в Solana PDA.
- `501 / SESSION_TYPE_PDA_CHECK_FAILED` — сервер не смог проверить `sessionType` по Solana PDA.
@@ -314,7 +314,7 @@ SESSION_LOGIN:{sessionId}:{timeMs}:{nonce}
В этом потоке:
- новое устройство не владеет `deviceKey` и не проходит обычный `CreateAuthSession`;
- новое устройство не владеет `clientKey` и не проходит обычный `CreateAuthSession`;
- пароль проверяется сервером только как фильтр;
- решение об одобрении принимает уже авторизованная доверенная сессия пользователя;
- сервер не расшифровывает `encryptedPayload` и не становится источником приватных ключей.
+4 -4
View File
@@ -21,7 +21,7 @@
"param": "display_name",
"time_ms": 1774700000123,
"value": "Alice",
"device_key": "BASE64_DEVICE_PUBLIC_KEY",
"client_key": "BASE64_DEVICE_PUBLIC_KEY",
"signature": "BASE64_SIGNATURE"
}
}
@@ -76,7 +76,7 @@
"param": "display_name",
"time_ms": 1774700000123,
"value": "Alice",
"device_key": "BASE64_DEVICE_PUBLIC_KEY",
"client_key": "BASE64_DEVICE_PUBLIC_KEY",
"signature": "BASE64_SIGNATURE"
}
}
@@ -116,7 +116,7 @@
"param": "display_name",
"time_ms": 1774700000123,
"value": "Alice",
"device_key": "BASE64_DEVICE_PUBLIC_KEY",
"client_key": "BASE64_DEVICE_PUBLIC_KEY",
"signature": "BASE64_SIGNATURE"
}
]
@@ -126,4 +126,4 @@
## Примечание
Имена JSON-полей `time_ms` и `device_key` сейчас соответствуют Java-модели ответа/запроса и должны передаваться именно в таком виде.
Имена JSON-полей `time_ms` и `client_key` сейчас соответствуют Java-модели ответа/запроса и должны передаваться именно в таком виде.
@@ -22,7 +22,7 @@ ESP32 становится аппаратным HSM (hardware security module):
### ESP32 (основная работа)
- [ ] Инициализация WiFi (SSID/пароль в NVS)
- [ ] WebSocket-клиент (`WebSocketsClient`) — постоянное соединение с сервером
- [ ] Авторизация на сервере: `AuthChallenge``CreateAuthSession` через `deviceKey` (уже есть в NVS), сохранить `sessionId` в NVS
- [ ] Авторизация на сервере: `AuthChallenge``CreateAuthSession` через `clientKey` (уже есть в NVS), сохранить `sessionId` в NVS
- [ ] Обработчик входящих WebSocket-событий: JSON-парсинг, диспетчер по типу
- [ ] Новые UI-экраны: «Разрешить сессию?» и «Подписать?» с кнопками Да/Нет
- [ ] Расширенное хранилище ключей в NVS (произвольные именованные ключи сверх базовых трёх)
+8 -8
View File
@@ -56,7 +56,7 @@ seed(32) = SHA-256(material)
|------|---------|---------------------|
| root | `root.key` | Личность. Подписывает unsigned-часть PDA-записи (`RootKeyBlock`). |
| blockchain | `bch.key` | Подписывает `LastBlockState` персонального блокчейна (`blockchain_public_key`). |
| device / **Solana** | `dev.key` | Ключ устройства = Solana-ключ. Fee payer и подпись Solana-транзакций; адрес кошелька = `base58(devicePub)`. См. §3. |
| device / **Solana** | `client.key` | Ключ устройства = Solana-ключ. Fee payer и подпись Solana-транзакций; адрес кошелька = `base58(clientPub)`. См. §3. |
| homeserver | `homeserver.key:<имя>` | Ключ homeserver-устройства, по одному на каждый homeserver (различитель — имя). См. §4. |
Полные роли каждого ключа — в `Dev_Docs/Keys/README.md`.
@@ -67,16 +67,16 @@ seed(32) = SHA-256(material)
Отдельного «солана-ключа» нет. На Solana работают два ключа:
- **`dev.key` (device) — пополняемый кошелёк и fee payer.** Solana-адрес = `base58(devicePub)`.
- **`client.key` (device) — пополняемый кошелёк и fee payer.** Solana-адрес = `base58(clientPub)`.
Этим ключом оплачиваются и подписываются `create_user_pda` / `update_user_pda`.
Пополнять SOL нужно именно на этот адрес.
- **`root.key` — авторитет записи**, подписывает unsigned-часть PDA через Ed25519-инструкцию, но **не** является fee payer.
Соответствует формату PDA `shine-solana/shine/doc/formats/shine-user-pda-format-v.1.0.md` §2.1
(«create/update оплачиваются с `device_key`», «root_key — не fee payer»).
(«create/update оплачиваются с `client_key`», «root_key — не fee payer»).
Кратко про роли на Solana: `root.key` — это **главный (master) ключ**: им управляют PDA-записью
(`create/update`) и через это можно заменить все остальные ключи; `dev.key` — это **пополняемый
(`create/update`) и через это можно заменить все остальные ключи; `client.key` — это **пополняемый
кошелёк и плательщик комиссий**. Полное описание ролей — `Dev_Docs/Keys/README.md`.
---
@@ -118,9 +118,9 @@ homeserver.key:home-b -> ключ B
- `shine-UI/server-ui/js/server-ui-shared.js` — те же root/bch/dev для серверного UI (~147160).
### Solana-ключ / адрес кошелька (UI)
- `shine-UI/js/pages/registration-payment-view.js``deriveUserWalletAddress`: адрес = `base58(devicePub)` (~113).
- `shine-UI/js/pages/topup-view.js``deviceWalletAddressFromBundle`: тот же канонический адрес из `preGeneratedKeyBundle.devicePair`.
Прежний расходящийся путь `deriveWalletFromPassword` (прямой Argon2 по `dev.key`, мимо `masterSecret`) удалён.
- `shine-UI/js/pages/registration-payment-view.js``deriveUserWalletAddress`: адрес = `base58(clientPub)` (~113).
- `shine-UI/js/pages/topup-view.js``clientWalletAddressFromBundle`: тот же канонический адрес из `preGeneratedKeyBundle.clientPair`.
Прежний расходящийся путь `deriveWalletFromPassword` (прямой Argon2 по `client.key`, мимо `masterSecret`) удалён.
### Деривация ключей (прошивка ESP32)
- `ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/main-device/shine_homeserver_main/shine_homeserver_main.ino`
@@ -131,7 +131,7 @@ homeserver.key:home-b -> ключ B
### Формат PDA (куда попадают ключи)
- `shine-solana/shine/doc/formats/shine-user-pda-format-v.1.0.md`
`RootKeyBlock` §6, `DeviceKeyBlock` §7, `blockchain_public_key` §9, `SessionsBlock`/`session_type=100` §13, оплата §2.1.
`RootKeyBlock` §6, `ClientKeyBlock` §7, `blockchain_public_key` §9, `SessionsBlock`/`session_type=100` §13, оплата §2.1.
### Сервер (тестовый seed)
- `SHiNE-server/src/test/java/test/it/cases/SeedDataPopulationHelper.java` `deriveKeysFromPassword` (~246) —
+14 -14
View File
@@ -8,9 +8,9 @@
В SHiNE у пользователя есть несколько уровней ключей:
- `root key` - главный (master) ключ пользователя: тот, кто им владеет, управляет пользовательской PDA в Solana и может заменить все остальные ключи. Это не пополняемый кошелёк (комиссии платит `device key`).
- `root key` - главный (master) ключ пользователя: тот, кто им владеет, управляет пользовательской PDA в Solana и может заменить все остальные ключи. Это не пополняемый кошелёк (комиссии платит `client key`).
- `blockchain key` - ключ записи в персональный SHiNE-блокчейн пользователя.
- `device key` - общий ключ пользовательских устройств для повседневной работы, звонков, DM и мелких платежей.
- `client key` - общий ключ пользовательских устройств для повседневной работы, звонков, DM и мелких платежей.
- `session key` - ключ конкретной сессии или конкретного устройства для авторизации на сервере.
Главная идея: самые важные ключи можно держать на доверенном серверном или аппаратном устройстве, а обычные клиентские устройства получают только ключи, нужные для текущей работы.
@@ -30,7 +30,7 @@
`root key` — это **главный (master) ключ** в следующем смысле: зная `root key`, можно управлять пользовательской PDA-записью в Solana (`create_user_pda` / `update_user_pda`) и тем самым **заменить все остальные ключи** пользователя (device, blockchain, homeserver). Поэтому компрометация `root key` равносильна компрометации всей личности пользователя.
Важно не путать авторитет и кошелёк: `root key` — это авторитет над PDA-записью, а **SOL-комиссии за create/update платит `device key`** (он же fee payer и адрес для пополнения). Подробнее о том, какой ключ за что отвечает на Solana, — в `Dev_Docs/Keys/DERIVATION.md`, §3.
Важно не путать авторитет и кошелёк: `root key` — это авторитет над PDA-записью, а **SOL-комиссии за create/update платит `client key`** (он же fee payer и адрес для пополнения). Подробнее о том, какой ключ за что отвечает на Solana, — в `Dev_Docs/Keys/DERIVATION.md`, §3.
## `blockchain key`
@@ -50,9 +50,9 @@
Рабочая логика по умолчанию должна использовать последнюю актуальную ветку. Старые ветки остаются читаемыми и показывают историю смены ключей.
## `device key`
## `client key`
`device key` - общий ключ, который знают доверенные устройства пользователя.
`client key` - общий ключ, который знают доверенные устройства пользователя.
Назначение:
@@ -63,11 +63,11 @@
- derivation Arweave-кошелька;
- оплата или подготовка добавления данных в Arweave-кошелек по отдельному протоколу.
Arweave-кошелёк должен выводиться из `device key` по протоколу:
Arweave-кошелёк должен выводиться из `client key` по протоколу:
- `Dev_Docs/Протоколы/SHINE_ARWEAVE_DERIVATION_V1.md`
Если пользователь теряет только `device key`, в худшем случае ломается повседневная переписка и доступ конкретных устройств к ежедневным операциям. `root key` и `blockchain key` при правильной архитектуре остаются отдельно защищёнными.
Если пользователь теряет только `client key`, в худшем случае ломается повседневная переписка и доступ конкретных устройств к ежедневным операциям. `root key` и `blockchain key` при правильной архитектуре остаются отдельно защищёнными.
## `session key`
@@ -83,7 +83,7 @@ Arweave-кошелёк должен выводиться из `device key` по
- авторизация сессии на сервере;
- привязка устройства к пользователю;
- подтверждение запросов от конкретной сессии;
- доступ к зашифрованному `device key` после успешной авторизации.
- доступ к зашифрованному `client key` после успешной авторизации.
Одна и та же сессия может быть пригодна для подключения к нескольким серверам пользователя, если архитектура конкретного пользователя это допускает.
@@ -108,14 +108,14 @@ Arweave-кошелёк должен выводиться из `device key` по
Обычное устройство обычно имеет:
- собственный `session key`;
- зашифрованный `device key`, который открывается после авторизации;
- зашифрованный `client key`, который открывается после авторизации;
- доступ к DM, звонкам и обычным пользовательским операциям.
Доверенное серверное или аппаратное устройство может иметь:
- `root key`;
- `blockchain key`;
- `device key`;
- `client key`;
- собственный `session key`.
Такая сессия может подписывать операции повышенной важности по запросам пользователя.
@@ -139,7 +139,7 @@ Self-message - это сообщение пользователя самому
Входящее сообщение может быть зашифровано:
- `device key`;
- `client key`;
- `session key`;
- отдельным ключом конкретного чата;
- другим ключом, который уже известен клиенту.
@@ -158,12 +158,12 @@ Self-message - это сообщение пользователя самому
## Связанные документы
- `Dev_Docs/Keys/DERIVATION.md` - **источник истины по конкретной деривации** секрета и ключей (формулы Argon2id, `base64|suffix→SHA-256→Ed25519`, суффиксы `root.key`/`bch.key`/`dev.key`/`homeserver.key:<имя>`, Solana-ключ, ссылки на код).
- `Dev_Docs/Keys/DERIVATION.md` - **источник истины по конкретной деривации** секрета и ключей (формулы Argon2id, `base64|suffix→SHA-256→Ed25519`, суффиксы `root.key`/`bch.key`/`client.key`/`homeserver.key:<имя>`, Solana-ключ, ссылки на код).
- `Dev_Docs/Personal_Messages/README.md` - текущая документация личных сообщений.
- `Dev_Docs/Blockchain/README.md` - точка входа по форматам SHiNE-блокчейна.
- `Dev_Docs/Solana_Architecture/README.md` - архитектура Solana-программ, PDA-счетов, DAO и движения средств.
- `Dev_Docs/Инициализация_Solana_регистрации/README.md` - деплой и первичная инициализация Solana-регистрации.
- `Dev_Docs/Протоколы/SHINE_ARWEAVE_DERIVATION_V1.md` - derivation Arweave-кошелька из `device key`.
- `Dev_Docs/Протоколы/SHINE_ARWEAVE_DERIVATION_V1.md` - derivation Arweave-кошелька из `client key`.
## Что нужно уточнить перед реализацией
@@ -172,5 +172,5 @@ Self-message - это сообщение пользователя самому
- какие операции требуют `root key`, а какие достаточно подписывать `blockchain key`;
- формат self-message-команд;
- порядок перебора ключей при расшифровке входящих сообщений;
- правила ротации `device key` и восстановления доступа после потери устройства;
- правила ротации `client key` и восстановления доступа после потери устройства;
- какие типы серверных и аппаратных сессий нужны в первой реализации.
@@ -11,7 +11,7 @@
- на ESP32 после успешной homeserver-авторизации в `SETTINGS` появляется пункт `Device requests` первым;
- `REFRESH` реально загружает активные заявки;
- на экране видно две плитки, список листается вертикально;
- client-session заявка после `YES` подключается с передачей только `device key`;
- client-session заявка после `YES` подключается с передачей только `client key`;
- wallet-session заявка после `YES` подключается без передачи ключей, через выпуск отдельной wallet-session;
- `NO` отклоняет заявку и она исчезает из списка активных.
@@ -2,20 +2,20 @@
- краткое описание:
- в ESP32 заменён домашний блок `баланс + QR` на единый вход в экран кошелька;
- добавлен выбор активного кошелька `DeviceKey / RootKey / Custom`;
- добавлен выбор активного кошелька `ClientKey / RootKey / Custom`;
- для browser extension добавлен первый RPC `get_wallet_public_key` через существующий `wallet-session` и `CallSignalToSession`;
- в popup расширения добавлен запрос текущего кошелька и копирование `publicKeyBase58`.
- что проверять:
- на ESP32 после ввода секрета на главном экране видна кнопка `Кошелёк: ...`;
- экран `WALLET` открывается и показывает текущий тип кошелька;
- экран `WALLET_SELECT` переключает `DeviceKey`, `RootKey` и `Custom`;
- экран `WALLET_SELECT` переключает `ClientKey`, `RootKey` и `Custom`;
- для `Custom` открывается ввод имени и после сохранения derivation работает;
- `Показать баланс кошелька` читает баланс именно активного кошелька;
- `Показать QR-код кошелька` показывает QR и адрес именно активного кошелька;
- browser extension после подключения wallet-session может запросить текущий кошелёк у ESP32;
- extension показывает тип кошелька, полный `publicKeyBase58`, результат проверки через PDA и копирует ключ в буфер;
- для `dev.key` и `root.key` проверка через PDA даёт ожидаемое совпадение.
- для `client.key` и `root.key` проверка через PDA даёт ожидаемое совпадение.
- ожидаемый результат:
- активный кошелёк на ESP32 реально влияет на баланс, QR и ответ `get_wallet_public_key`;
@@ -5,7 +5,7 @@
- Что проверять:
1. На ESP32 открыть `HOME`, `WALLET`, `SELECT WALLET`, `WALLET QR` и убедиться, что пользовательские строки на экране только на английском.
2. Проверить сценарий выбора `DeviceKey`, `RootKey`, `Custom` и чтение баланса/QR после переключения.
2. Проверить сценарий выбора `ClientKey`, `RootKey`, `Custom` и чтение баланса/QR после переключения.
3. В расширении открыть popup, запустить `Подключить` для логина, у которого ещё не настраивался trusted-device login.
4. Убедиться, что `StartTrustedDeviceLogin` больше не падает с `NullPointerException`.
5. После создания pairing-запроса проверить, что homeserver/UI может его увидеть и обработать как раньше.
@@ -12,7 +12,7 @@
6. Проверить оба варианта:
- `APPROVE` возвращает сайту подписанную транзакцию;
- `REJECT` возвращает отказ.
7. Проверить сценарии для `DeviceKey`, `RootKey`, `Custom`.
7. Проверить сценарии для `ClientKey`, `RootKey`, `Custom`.
- Ожидаемый результат:
- сайт может подключить кошелёк через provider расширения;
+88 -30
View File
@@ -12,8 +12,9 @@
- логин пользователя;
- неизменяемые параметры создания записи;
- публичный recovery-ключ пользователя;
- корневой публичный ключ пользователя;
- ключ устройства;
- клиентский публичный ключ пользователя;
- данные одного или нескольких пользовательских блокчейнов SHiNE;
- серверные данные пользователя, если пользователь выступает сервером;
- серверы доступа пользователя;
@@ -34,9 +35,9 @@
## 2.1. Кто оплачивает create/update PDA
- Инструкции `create_user_pda` и `update_user_pda` оплачиваются с `device_key`.
- Инструкции `create_user_pda` и `update_user_pda` оплачиваются с `client_key`.
- `root_key` используется для подписи unsigned части записи через Ed25519 instruction и не является fee payer.
- Для server PDA это правило то же самое: пополнять SOL нужно на адрес `device_key`.
- Для server PDA это правило то же самое: пополнять SOL нужно на адрес `client_key`.
## 3. Общие правила кодирования
@@ -85,8 +86,9 @@ UserPdaRecordV1
| block_type | Блок | Назначение |
|------------|------|------------|
| `0` | `RecoveryKeyBlock` | Ключ восстановления пользователя. |
| `1` | `RootKeyBlock` | Корневой ключ пользователя. |
| `2` | `DeviceKeyBlock` | Ключ устройства пользователя. |
| `2` | `ClientKeyBlock` | Клиентский ключ пользователя. |
| `3` | `BlockchainRegistryBlock` | Один или несколько блокчейнов пользователя. |
| `30` | `ServerProfileBlock` | Серверные данные пользователя. |
| `40` | `AccessServersBlock` | Серверы доступа/relay. |
@@ -97,13 +99,31 @@ UserPdaRecordV1
Правила:
- неизвестный `block_type` в `format_major = 1` считается ошибкой;
- обязательные блоки: `RootKeyBlock`, `DeviceKeyBlock`, `BlockchainRegistryBlock`;
- обязательные блоки: `RecoveryKeyBlock`, `RootKeyBlock`, `ClientKeyBlock`, `BlockchainRegistryBlock`;
- необязательные блоки: `ServerProfileBlock`, `AccessServersBlock`, `SessionsBlock`, `TrustedStateBlock`;
- каждый обязательный блок должен встречаться ровно один раз;
- порядок блоков в записи фиксируется для простоты проверки:
`RootKey`, `DeviceKey`, `BlockchainRegistry`, `ServerProfile`, `AccessServers`, `Sessions`, `TrustedState`.
`RecoveryKey`, `RootKey`, `ClientKey`, `BlockchainRegistry`, `ServerProfile`, `AccessServers`, `Sessions`, `TrustedState`.
## 6. RootKeyBlock
## 6. RecoveryKeyBlock
Recovery-ключ нужен для будущих сценариев восстановления и ротации остальных ключей. В текущей версии он только публикуется в записи и не меняется через обычный `update_user_pda`.
```text
RecoveryKeyBlock
- block_type: u8 = 0
- block_version: u8 = 0
- recovery_key: [u8; 32]
```
Правила:
- при создании задается публичный recovery-ключ пользователя;
- при обновлении `recovery_key` должен совпадать с предыдущей записью;
- приватный `recovery.key` в PDA не хранится;
- отдельная ротация recovery-ключа будет отдельным форматом/сценарием в будущем.
## 7. RootKeyBlock
Смена `root_key` пока не проектируется и не реализуется. Блок фиксирует только стадию `0`.
@@ -120,24 +140,24 @@ RootKeyBlock
- при обновлении `root_key` должен совпадать с предыдущей записью;
- ротация root-key будет отдельным форматом/сценарием в будущем.
## 7. DeviceKeyBlock
## 8. ClientKeyBlock
Смена `device_key` пока также не проектируется как отдельная ротация. В версии `0` хранится один ключ устройства.
Смена `client_key` пока также не проектируется как отдельная ротация. В версии `0` хранится один клиентский ключ пользователя.
```text
DeviceKeyBlock
ClientKeyBlock
- block_type: u8 = 2
- block_version: u8 = 0
- device_key: [u8; 32]
- client_key: [u8; 32]
```
Правила:
- при создании задается текущий публичный ключ устройства;
- при обновлении ключ устройства может быть обновлен только если это отдельно разрешено бизнес-логикой инструкции;
- история устройств и несколько устройств в этом формате не хранятся.
- при создании задается текущий клиентский публичный ключ пользователя;
- при обновлении `client_key` должен совпадать с предыдущей записью;
- история устройств и несколько клиентских ключей в этом формате не хранятся.
## 8. BlockchainRegistryBlock
## 9. BlockchainRegistryBlock
Блок хранит данные пользовательских блокчейнов SHiNE. Сейчас используется один блокчейн, но структура сразу сделана как список.
@@ -155,7 +175,7 @@ BlockchainRegistryBlock
- в будущем можно увеличить количество записей без изменения смысла `BlockchainRecord`;
- каждый `BlockchainRecord` описывает один пользовательский SHiNE-блокчейн.
## 9. BlockchainRecord
## 10. BlockchainRecord
```text
BlockchainRecord
@@ -191,7 +211,7 @@ BlockchainRecord
Arweave `tx_id` - обычное поле внутри записи конкретного блокчейна. Solana-программа не проверяет, что такой Arweave transaction действительно существует и содержит корректные данные; это ответственность клиента/сервера/пользователя.
## 10. Правила обновления BlockchainRecord
## 11. Правила обновления BlockchainRecord
При обновлении записи:
@@ -229,7 +249,7 @@ last_block_signature = Ed25519(blockchain_public_key, message)
Причина проверки подписи `LastBlockState`: `root_key` управляет Solana-записью пользователя, а `blockchain_public_key` подтверждает состояние конкретного пользовательского блокчейна. Подписывается не голый хэш, а связка логина, имени блокчейна, номера последнего блока, хэша последнего блока и занятого размера.
## 11. ServerProfileBlock
## 12. ServerProfileBlock
Блок присутствует, если пользователь выступает сервером.
@@ -255,7 +275,7 @@ ServerProfileBlock
- `server_address` - строковый адрес сервера в соответствии с `address_format_type`;
- `sync_servers` - логины SHiNE-пользователей, зарегистрированных как серверы, с которыми этот сервер синхронизирует блокчейн и личные сообщения. Solana-программа не обязана проверять, что эти логины действительно зарегистрированы как серверы.
## 12. AccessServersBlock
## 13. AccessServersBlock
Блок хранит серверы доступа/relay для пользователя.
@@ -274,7 +294,7 @@ AccessServersBlock
- `access_servers` - логины пользователей системы, используемых как серверы доступа/relay. Solana-программа не обязана проверять, что эти логины действительно зарегистрированы как серверы;
- точная семантика выбора сервера доступа определяется клиентской/серверной логикой SHiNE.
## 13. SessionsBlock
## 14. SessionsBlock
Блок хранит опубликованные пользовательские сессии. На текущем этапе регистрация пользователя не добавляет туда записи автоматически, поэтому стандартный create/update продолжает работать с пустым списком.
@@ -309,6 +329,7 @@ SessionRecord
| Значение | Смысл |
|----------|-------|
| `1` | Обычная пользовательская сессия. |
| `50` | Кошелёк пользователя. |
| `100` | Homeserver пользователя. |
Правила:
@@ -320,7 +341,7 @@ SessionRecord
- внутри одного блока должны быть уникальны и `session_name`, и `session_pub_key`;
- на текущем этапе UI и регистрация не обязаны добавлять туда записи автоматически.
## 14. TrustedStateBlock
## 15. TrustedStateBlock
Пока trusted-логика не реализована полностью, поэтому блок хранит только счетчик.
@@ -333,7 +354,7 @@ TrustedStateBlock
Пока блок с доверенными лицами не реализуется, потому что полный формат trusted-логики еще не составлен. В будущем trusted-связи, очереди, таймеры и подтверждения должны быть вынесены в отдельный формат.
## 15. Подпись user_pda
## 16. Подпись user_pda
Подписывается не вся PDA целиком, а unsigned-часть записи:
@@ -354,7 +375,7 @@ Solana-программа проверяет подпись через встр
Смену формата подписи сейчас не трогаем.
## 16. Регистрация пользователя
## 17. Регистрация пользователя
При регистрации:
@@ -372,12 +393,12 @@ Solana-программа проверяет подпись через встр
- если покупается дополнительный лимит, пользователь платит комиссию за этот лимит;
- вся unsigned-часть записи подписана `root_key`.
## 17. Обновление пользователя
## 18. Обновление пользователя
При обновлении:
- PDA должна существовать;
- `login`, `created_at_ms`, `root_key` не меняются;
- `login`, `created_at_ms`, `recovery_key`, `root_key`, `client_key` не меняются;
- `record_number = previous_record_number + 1`;
- `prev_record_hash` равен хэшу unsigned-части предыдущей записи;
- `updated_at_ms` обновляется;
@@ -387,7 +408,7 @@ Solana-программа проверяет подпись через встр
- при увеличении оплаченного лимита пользователь доплачивает комиссию;
- Arweave `tx_id` может быть пустым или обновленным, но его содержимое Solana не валидирует.
## 18. Отличия от старого линейного формата
## 19. Отличия от старого линейного формата
Старый формат после `login` хранил поля линейно:
@@ -395,8 +416,8 @@ Solana-программа проверяет подпись через встр
- `root_key`;
- `blockchain_key_status`;
- `blockchain_key`;
- `device_key_status`;
- `device_key`;
- `client_key_status`;
- `client_key`;
- `chain_number`;
- `balance`;
- серверные поля;
@@ -407,17 +428,54 @@ Solana-программа проверяет подпись через встр
Новый целевой формат сохраняет первые 9 фиксированных полей как заголовок, но дальше переходит на типизированные блоки:
- recovery-ключ становится отдельным обязательным блоком;
- ключи становятся отдельными блоками;
- данные блокчейна становятся расширенным блоком со своим публичным ключом, лимитом, занятым размером, вершиной цепочки и Arweave `tx_id`;
- серверные данные и access-серверы отделяются от данных блокчейна;
- расширение формата делается добавлением новых версий блоков или новых `block_type`, а не вставкой полей в середину линейной записи.
## 18. Что пока не входит в формат
## 20. Деривация ключей из master secret
Сама Solana-программа не вычисляет ключи из секрета и не хранит приватные ключи. Но текущая согласованная клиентская схема деривации для публичной версии формата фиксируется здесь как reference для UI/ESP32/внешних клиентов.
Базовая формула:
```text
seed = SHA-256("SHiNE-key" || 0x00 || master_secret32 || 0x00 || suffix_utf8)
```
Где:
- `master_secret32` — 32-байтовый master secret пользователя;
- `suffix_utf8` — строка назначения ключа.
Согласованные suffix:
```text
"recovery.key"
"root.key"
"blockchain.key"
"client.key"
```
Соответствие:
```text
recovery.seed = SHA-256("SHiNE-key" || 0x00 || master_secret32 || 0x00 || "recovery.key")
root.seed = SHA-256("SHiNE-key" || 0x00 || master_secret32 || 0x00 || "root.key")
blockchain.seed = SHA-256("SHiNE-key" || 0x00 || master_secret32 || 0x00 || "blockchain.key")
client.seed = SHA-256("SHiNE-key" || 0x00 || master_secret32 || 0x00 || "client.key")
```
Далее каждая строка `seed` интерпретируется off-chain как `seed32` для отдельной пары Ed25519.
## 21. Что пока не входит в формат
Пока не проектируем:
- ротацию `recovery_key`;
- ротацию `root_key`;
- сложную ротацию `device_key`;
- сложную ротацию `client_key`;
- ротацию `blockchain_public_key`;
- проверку содержимого Arweave transaction;
- хранение полной истории пользовательского блокчейна внутри Solana;
@@ -19,7 +19,7 @@
5. `DAO_TREASURY_WALLET` / `dao_wallet` — казна DAO.
6. `manager_wallet` — кошелек менеджера, которому DAO выдает лимиты на создание тикетов.
7. `user root_key` — корневой ключ пользователя для подписи пользовательской записи.
8. `user device_key` — ключ устройства пользователя.
8. `user client_key` — ключ устройства пользователя.
9. `server_key` — ключ сервера пользователя, если пользователь является сервером.
Текущие адреса из `programs/common/src/deploy_config.rs`:
@@ -62,7 +62,7 @@
`UserMutableFields`:
- `device_key: Pubkey`
- `client_key: Pubkey`
- `blockchain_public_key: Pubkey`
- `blockchain_name: String`
- `used_bytes: u64`
@@ -32,7 +32,7 @@
<rect class="box actor" x="52" y="150" width="210" height="78"/>
<text class="txt" x="72" y="181">Пользователь</text>
<text class="small" x="72" y="206">signer, root_key, device_key</text>
<text class="small" x="72" y="206">signer, root_key, client_key</text>
<rect class="box actor" x="52" y="310" width="210" height="78"/>
<text class="txt" x="72" y="341">Покупатель тикета</text>

Before

Width:  |  Height:  |  Size: 7.1 KiB

After

Width:  |  Height:  |  Size: 7.1 KiB

@@ -43,8 +43,8 @@ bump, подмена аккаунта оракула) не найдено. Вс
|---|---|---|---|---|---|---|
| `init_users_economy_config` | ✓ | `owner == system` + `data_is_empty` (анти-reinit) | деривация + сверка | ✓ | — | значения из `settings`, не из ввода |
| `update_users_economy_config` | ✓ + `signer == DAO_AUTHORITY` | `owner == program_id` | деривация + сверка | — | — | `lamports_per_limit_step > 0` |
| `create_user_pda` | ✓ + `signer == device_key` | user_pda `owner == system` + empty; econ_config `owner == program_id` | user_pda, econ_config, inflow_vault, login_guard — все сверены | ✓ | ed25519 (record sig idx 2, last_block idx 1) | `inflow_vault` сверен с PDA `shine_payments`; login_guard сверен дважды |
| `update_user_pda` | ✓ + `signer == device_key` | user_pda `owner == program_id`; econ_config `owner == program_id` | деривация + сверка | ✓ | ed25519 + `version == old+1` + `prev_hash == hash(old)` | immutable-поля сверены с прежней записью |
| `create_user_pda` | ✓ + `signer == client_key` | user_pda `owner == system` + empty; econ_config `owner == program_id` | user_pda, econ_config, inflow_vault, login_guard — все сверены | ✓ | ed25519 (record sig idx 2, last_block idx 1) | `inflow_vault` сверен с PDA `shine_payments`; login_guard сверен дважды |
| `update_user_pda` | ✓ + `signer == client_key` | user_pda `owner == program_id`; econ_config `owner == program_id` | деривация + сверка | ✓ | ed25519 + `version == old+1` + `prev_hash == hash(old)` | immutable-поля сверены с прежней записью |
### shine_payments
@@ -111,7 +111,7 @@ commit-reveal; для текущей модели — приемлемый ри
- **Подмена PDA** невозможна нигде: всюду пара «деривация `find_program_address` + сверка полного адреса». Пользовательский bump не принимается, `create_program_address` с внешним bump не используется — bump-seed атаки исключены.
- **Проверка владельца** при каждом чтении PDA: `read_state` и `validate_singleton_state_pda` (`shine_payments`) требуют `owner == id()`; `validate_users_economy_config_pda` и проверка `user_pda.owner == program_id` (`shine_users`) — перед десериализацией данных.
- **Создаваемые PDA**: проверка `is_uninitialized` / `owner == system && data_is_empty` исключает повторную инициализацию и перезапись чужого аккаунта.
- **signer / authority**: все handler начинают с обязательного `is_signer`; привилегированные операции дополнительно сверяют ключ с авторитетом (`config.dao_wallet`, `DAO_AUTHORITY`, `allowance.manager_wallet`, `ticket.recipient_wallet`, `device_key`).
- **signer / authority**: все handler начинают с обязательного `is_signer`; привилегированные операции дополнительно сверяют ключ с авторитетом (`config.dao_wallet`, `DAO_AUTHORITY`, `allowance.manager_wallet`, `ticket.recipient_wallet`, `client_key`).
- **system-program** сверяется с `system_program::ID` там, где идёт создание аккаунта/перевод; **sysvar инструкций** сверяется с `sysvar::instructions::id()` перед ed25519-интроспекцией.
- **Аккаунт оракула**: пин адреса `PYTH_SOL_USD_ACCOUNT` + `owner == pyth_receiver` + `feed_id` + возраст (120 с) + доверительный интервал (10%).
- **Ed25519 в `shine_users`**: относительные индексы −1/−2, `num_signatures == 1`, все три `ix_index == u16::MAX` (offset-данные внутри самой ed25519-инструкции), сверка `program_id == ed25519_program` и pubkey/signature/message по хэшу — указать на чужую инструкцию нельзя.
@@ -84,15 +84,15 @@ anchor deploy -p shine_users
## Кто оплачивает create/update user_pda
- И обычная регистрация `create_user_pda`, и последующее `update_user_pda` оплачиваются с `deviceKey`.
- И обычная регистрация `create_user_pda`, и последующее `update_user_pda` оплачиваются с `clientKey`.
- В UI это означает, что Solana fee payer всегда берётся из `device`-ключа пользователя/сервера.
- `rootKey` нужен для подписи unsigned PDA-записи, но не оплачивает транзакцию.
- Для server UI это особенно важно: перед `create` и `update` нужно пополнять именно Solana-адрес `deviceKey`.
- Для server UI это особенно важно: перед `create` и `update` нужно пополнять именно Solana-адрес `clientKey`.
## Важно
- `init_users_economy_config` выполняется один раз на программу.
Если PDA уже создан, повторный вызов вернёт ошибку "already initialized" (это нормальное поведение).
- Серверные приватные ключи для Solana не используются как отдельный backend-wallet: в UI/server UI транзакцию оплачивает именно `deviceKey`, а содержимое записи подписывает `rootKey`.
- Серверные приватные ключи для Solana не используются как отдельный backend-wallet: в UI/server UI транзакцию оплачивает именно `clientKey`, а содержимое записи подписывает `rootKey`.
- `shine_users` внутри `create_user_pda` требует корректный адрес `shine_login_guard` для CPI-классификации логина.
Несовпадение адреса приведёт к ошибке регистрации.
@@ -4,7 +4,7 @@
## 1. Текущие режимы
### 1. Создание новой сессии через `deviceKey`
### 1. Создание новой сессии через `clientKey`
Поток:
@@ -12,8 +12,8 @@
Смысл:
- новое устройство уже владеет приватным `deviceKey`;
- сервер проверяет подпись `deviceKey`;
- новое устройство уже владеет приватным `clientKey`;
- сервер проверяет подпись `clientKey`;
- создаётся обычная активная сессия пользователя;
- этот поток остаётся без изменений.
@@ -67,7 +67,7 @@
## 4. Чего сервер в этой версии не делает
- не передаёт приватный `deviceKey`;
- не передаёт приватный `clientKey`;
- не расшифровывает `encryptedPayload`;
- не проверяет криптографию содержимого payload;
- не делает клиентский UI;
@@ -3,11 +3,11 @@
Сокращение: **SAWD-v1**.
## Назначение
Из 32-байтного `deviceKey32` пользователя получить один и тот же нативный Arweave RSA-4096 JWK wallet и один и тот же Arweave address.
Из 32-байтного `clientKey32` пользователя получить один и тот же нативный Arweave RSA-4096 JWK wallet и один и тот же Arweave address.
## Вход
- `deviceKey32`: ровно 32 байта.
- Если исходный `device.key` хранится как Ed25519 PKCS8 base64, нужно извлечь последние 32 байта из PKCS8.
- `clientKey32`: ровно 32 байта.
- Если исходный `client.key` хранится как Ed25519 PKCS8 base64, нужно извлечь последние 32 байта из PKCS8.
- Если используется Solana keypair JSON на 64 байта, используются только `bytes[0..31]`.
## Выход
@@ -46,8 +46,8 @@
- `SMALL_PRIME_LIMIT = 10000`
## Алгоритм
1. Проверить `deviceKey32.length == 32`.
2. `masterSeed32 = HMAC-SHA256(key = UTF8(MASTER_LABEL), message = deviceKey32)`.
1. Проверить `clientKey32.length == 32`.
2. `masterSeed32 = HMAC-SHA256(key = UTF8(MASTER_LABEL), message = clientKey32)`.
3. Реализовать `deriveBytes(label, length)`:
- `output = empty`
- `counter = 0`
@@ -16,7 +16,7 @@
На устройстве в UI пользователь выбирает текущий активный кошелёк:
- `dev.key`
- `client.key`
- `root.key`
- `custom`
@@ -115,7 +115,7 @@ ESP32 возвращает:
- `requestId` — должен совпадать с `requestId` исходного запроса.
- `ok` — признак успешного результата.
- `wallet.type` — тип активного кошелька:
- `dev.key`
- `client.key`
- `root.key`
- `custom`
- `wallet.publicKeyBase58` — публичный ключ активного кошелька в `Base58`.
@@ -156,11 +156,11 @@ ESP32 возвращает:
Расширение уже знает публичные ключи пользователя из Solana PDA. Поэтому оно может дополнительно проверить ответ ESP32:
- если `wallet.type = dev.key`, то `publicKeyBase58` должен совпасть с `deviceKey`, прочитанным из PDA;
- если `wallet.type = client.key`, то `publicKeyBase58` должен совпасть с `clientKey`, прочитанным из PDA;
- если `wallet.type = root.key`, то `publicKeyBase58` должен совпасть с `rootKey`, прочитанным из PDA;
- если `wallet.type = custom`, такой проверки по PDA в первой версии нет.
При несовпадении для `dev.key` или `root.key` расширение должно показать пользователю предупреждение, что возвращённый ключ не совпал с ожидаемым ключом из PDA.
При несовпадении для `client.key` или `root.key` расширение должно показать пользователю предупреждение, что возвращённый ключ не совпал с ожидаемым ключом из PDA.
## 8. Ожидаемое поведение UI расширения