SHA256
Перенести документацию в docs и добавить социальный граф
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
# ESP Pairing и режимы подключения
|
||||
|
||||
Этот документ фиксирует актуальные режимы входа/подключения в SHiNE. Он нужен как отдельная точка входа по сценариям подключения, чтобы не смешивать обычную авторизацию и серверный pairing через доверенное уже авторизованное устройство пользователя.
|
||||
|
||||
## 1. Текущие режимы
|
||||
|
||||
### 1. Создание новой сессии через `clientKey`
|
||||
|
||||
Поток:
|
||||
|
||||
`AuthChallenge -> CreateAuthSession`
|
||||
|
||||
Смысл:
|
||||
|
||||
- новое устройство уже владеет приватным `clientKey`;
|
||||
- сервер проверяет подпись `clientKey`;
|
||||
- создаётся обычная активная сессия пользователя;
|
||||
- этот поток остаётся без изменений.
|
||||
|
||||
### 2. Повторный вход в существующую сессию через `sessionKey`
|
||||
|
||||
Поток:
|
||||
|
||||
`SessionChallenge -> SessionLogin`
|
||||
|
||||
Смысл:
|
||||
|
||||
- устройство уже владеет приватным `sessionKey`;
|
||||
- сервер проверяет подпись `sessionKey`;
|
||||
- соединение снова входит в существующую сессию;
|
||||
- этот поток тоже остаётся без изменений.
|
||||
|
||||
## 2. Добавление сессии через доверенное устройство пользователя
|
||||
|
||||
Новый поток не заменяет обычный логин, а живёт рядом с ним.
|
||||
|
||||
Цель:
|
||||
|
||||
- новое устройство знает `login`, а `pairing password` используется только если он включён на доверённом устройстве;
|
||||
- сервер использует пароль только как фильтр от мусора;
|
||||
- реальное доверие даёт любая уже онлайн доверенная сессия пользователя;
|
||||
- сервер не выдаёт приватные ключи сам от себя.
|
||||
|
||||
Текущий поток:
|
||||
|
||||
1. Любая доверенная сессия пользователя создаёт на сервере pairing-настройку:
|
||||
`UpsertEspPairingSettings`
|
||||
2. Новое устройство создаёт pending-заявку:
|
||||
`StartEspPairing`
|
||||
3. Онлайн доверенная сессия видит список активных заявок:
|
||||
`ListEspPairingRequests`
|
||||
4. Доверенная сессия либо подтверждает заявку:
|
||||
`ApproveEspPairing`
|
||||
5. Либо отклоняет:
|
||||
`RejectEspPairing`
|
||||
6. Новое устройство читает результат:
|
||||
`GetEspPairingStatus`
|
||||
|
||||
## 3. Что именно делает сервер
|
||||
|
||||
- хранит включённость pairing и optional `passwordHash` в формате `sha256$<hex>`;
|
||||
- хранит pairing-заявки всех статусов, но в список активных для доверённого устройства отдаёт только pending `created`;
|
||||
- рассчитывает короткий код `shortCode` из `10` цифр;
|
||||
- рассчитывает длинный `fingerprintB58` из `SHA-256` заявки;
|
||||
- уведомляет онлайн доверенные сессии событием `IncomingEspPairingRequest`, если такие сессии подключены;
|
||||
- хранит переданный `encryptedPayload` как непрозрачную строку и не анализирует его содержимое.
|
||||
|
||||
## 4. Чего сервер в этом режиме не делает
|
||||
|
||||
- не передаёт приватный `clientKey`;
|
||||
- не расшифровывает `encryptedPayload`;
|
||||
- не проверяет криптографию содержимого payload;
|
||||
- не делает клиентский UI;
|
||||
- не навязывает конкретную схему `Ed25519 -> X25519` в коде сервера.
|
||||
|
||||
Это намеренно: сервер остаётся безопасным каркасом маршрутизации и состояния, а E2E-логика упаковки ключей живёт на клиентах и ESP-устройствах.
|
||||
|
||||
## 5. Роли и ограничения
|
||||
|
||||
- любая уже авторизованная доверенная сессия пользователя может вызывать:
|
||||
- `UpsertEspPairingSettings`
|
||||
- `ListEspPairingRequests`
|
||||
- `ApproveEspPairing`
|
||||
- `RejectEspPairing`
|
||||
- новое устройство может вызвать `StartEspPairing` и `GetEspPairingStatus` без уже существующей авторизованной сессии;
|
||||
- `payloadType` поддерживается в вариантах:
|
||||
- `1` — минимальный пакет
|
||||
- `2` — расширенный пакет
|
||||
- `3` — полный пакет
|
||||
|
||||
Сервер не интерпретирует эти три типа глубже, а только фиксирует их в состоянии заявки.
|
||||
|
||||
## 6. Статусы pairing-заявки
|
||||
|
||||
- `created` — заявка создана и ждёт решения доверенной сессии;
|
||||
- `approved` — доверенная сессия подтвердила и приложила `encryptedPayload`;
|
||||
- `rejected` — доверенная сессия отклонила заявку;
|
||||
- `expired` — TTL заявки истёк до подтверждения.
|
||||
|
||||
## 7. Практический смысл
|
||||
|
||||
Эта схема даёт нужное разделение доверия:
|
||||
|
||||
- пароль на сервере, если он включён, только отсеивает лишних;
|
||||
- онлайн доверенная сессия решает, добавлять ли новую сессию;
|
||||
- сервер остаётся маршрутизатором и хранилищем состояния, а не владельцем секретов.
|
||||
|
||||
Текущий формат pairing-пароля:
|
||||
|
||||
```text
|
||||
sha256$<hex( SHA-256("shine-pairing|" + lower(login.trim()) + "|" + password) )>
|
||||
```
|
||||
|
||||
## 8. Связанный документ по внешнему кошельку
|
||||
|
||||
Для отдельного RPC-взаимодействия между браузерным wallet-расширением и ESP32 см. документ:
|
||||
|
||||
- [Формат_взаимодействия_внешнего_кошелька_и_ESP32.md](/home/ai/work/SHiNE/SHiNE-server-sha256/Dev_Docs/Протоколы/Формат_взаимодействия_внешнего_кошелька_и_ESP32.md)
|
||||
@@ -0,0 +1,104 @@
|
||||
# SHiNE Arweave Wallet Derivation v1
|
||||
|
||||
Сокращение: **SAWD-v1**.
|
||||
|
||||
## Назначение
|
||||
Из 32-байтного `clientKey32` пользователя получить один и тот же нативный Arweave RSA-4096 JWK wallet и один и тот же Arweave address.
|
||||
|
||||
## Вход
|
||||
- `clientKey32`: ровно 32 байта.
|
||||
- Если исходный `client.key` хранится как Ed25519 PKCS8 base64, нужно извлечь последние 32 байта из PKCS8.
|
||||
- Если используется Solana keypair JSON на 64 байта, используются только `bytes[0..31]`.
|
||||
|
||||
## Выход
|
||||
```json
|
||||
{
|
||||
"derivation": "SAWD-v1",
|
||||
"jwk": {
|
||||
"kty": "RSA",
|
||||
"e": "AQAB",
|
||||
"n": "...",
|
||||
"d": "...",
|
||||
"p": "...",
|
||||
"q": "...",
|
||||
"dp": "...",
|
||||
"dq": "...",
|
||||
"qi": "..."
|
||||
},
|
||||
"owner": "...",
|
||||
"address": "..."
|
||||
}
|
||||
```
|
||||
|
||||
Где:
|
||||
- `owner = jwk.n`
|
||||
- `address = base64url_no_padding(SHA-256(unsigned_big_endian_bytes(n)))`
|
||||
|
||||
## Константы
|
||||
- `DERIVATION_NAME = "SAWD-v1"`
|
||||
- `MASTER_LABEL = "SHINE/ARWEAVE/RSA4096/SAWD-v1/MASTER"`
|
||||
- `STREAM_LABEL = "SHINE/ARWEAVE/RSA4096/SAWD-v1/STREAM"`
|
||||
- `MR_LABEL = "SHINE/ARWEAVE/RSA4096/SAWD-v1/MILLER-RABIN"`
|
||||
- `RSA_BITS = 4096`
|
||||
- `PRIME_BITS = 2048`
|
||||
- `PUBLIC_EXPONENT = 65537`
|
||||
- `MILLER_RABIN_ROUNDS = 64`
|
||||
- `SMALL_PRIME_LIMIT = 10000`
|
||||
|
||||
## Алгоритм
|
||||
1. Проверить `clientKey32.length == 32`.
|
||||
2. `masterSeed32 = HMAC-SHA256(key = UTF8(MASTER_LABEL), message = clientKey32)`.
|
||||
3. Реализовать `deriveBytes(label, length)`:
|
||||
- `output = empty`
|
||||
- `counter = 0`
|
||||
- while `output.length < length`:
|
||||
- `block = HMAC-SHA256(key = masterSeed32, message = UTF8(STREAM_LABEL) || UTF8("/") || UTF8(label) || UTF8("/") || uint64_be(counter))`
|
||||
- `output = output || block`
|
||||
- `counter++`
|
||||
- вернуть первые `length` байт.
|
||||
4. Для `p` и `q`:
|
||||
- `raw = deriveBytes(label + "/" + index, 256)`
|
||||
- `candidate = unsigned_big_endian_integer(raw)`
|
||||
- `candidate = candidate OR 2^2047`
|
||||
- `candidate = candidate OR 1`
|
||||
- Проверить:
|
||||
- `bitLength(candidate) == 2048`
|
||||
- `candidate odd`
|
||||
- не делится на малые простые `<= 10000`
|
||||
- `gcd(candidate - 1, 65537) == 1`
|
||||
- проходит Miller-Rabin `64 rounds`
|
||||
5. Базы Miller-Rabin детерминированные:
|
||||
- `baseBytes = HMAC-SHA256(key = masterSeed32, message = UTF8(MR_LABEL) || UTF8("/") || UTF8(label) || UTF8("/") || uint64_be(index) || UTF8("/") || uint32_be(round))`
|
||||
- `a = 2 + (unsigned_big_endian_integer(baseBytes) mod (candidate - 3))`
|
||||
6. `p = derivePrime("p")`, `q = derivePrime("q")`.
|
||||
7. Если `p == q`, продолжить поиск `q`.
|
||||
8. Если `p > q`, поменять местами. В SAWD-v1 всегда `p < q`.
|
||||
9. `n = p * q`
|
||||
10. `e = 65537`
|
||||
11. `lambda = lcm(p - 1, q - 1)`
|
||||
12. `d = modular_inverse(e, lambda)`
|
||||
13. `dp = d mod (p - 1)`
|
||||
14. `dq = d mod (q - 1)`
|
||||
15. `qi = modular_inverse(q, p)`
|
||||
16. Сформировать JWK:
|
||||
- `kty = "RSA"`
|
||||
- `e = "AQAB"`
|
||||
- `n,d,p,q,dp,dq,qi = base64url unsigned big-endian integer without padding`
|
||||
17. `owner = jwk.n`
|
||||
18. `address = base64url_no_padding(SHA-256(unsigned_big_endian_bytes(n)))`
|
||||
|
||||
## Запрещено
|
||||
- `crypto.generateKeyPair`
|
||||
- `WebCrypto generateKey`
|
||||
- `KeyPairGenerator`
|
||||
- `SecureRandom(seed)`
|
||||
- `Math.random`
|
||||
- системный `random`
|
||||
- ArDrive CLI
|
||||
- Turbo
|
||||
- внешний API для генерации ключа
|
||||
- сохранение приватного JWK
|
||||
|
||||
## Версионирование стандарта
|
||||
Если меняется любая константа или шаг алгоритма — это уже **SAWD-v2**.
|
||||
Пользователи, созданные на SAWD-v1, должны продолжать восстанавливаться через SAWD-v1.
|
||||
@@ -0,0 +1,151 @@
|
||||
# Преобразование Ed25519 в X25519
|
||||
|
||||
## Назначение
|
||||
|
||||
Этот документ фиксирует единое правило проекта SHiNE:
|
||||
|
||||
- публичная идентичность пользователя задаётся одним `clientKey` в формате `Ed25519`;
|
||||
- подпись пользовательских сообщений и команд выполняется ключом `Ed25519`;
|
||||
- шифрование личных сообщений использует ключ `X25519`, получаемый детерминированно из того же ключевого материала;
|
||||
- для преобразования используется только стандартное преобразование `Ed25519 <-> X25519`;
|
||||
- самодельные формулы, альтернативные хэши и нестандартные схемы преобразования в проекте не допускаются.
|
||||
|
||||
Цель документа: исключить путаницу между ключом подписи и ключом шифрования и зафиксировать один общий способ преобразования для клиента, сервера и будущих реализаций.
|
||||
|
||||
## Краткое правило
|
||||
|
||||
Если у пользователя есть:
|
||||
|
||||
- приватный ключ `Ed25519 private`;
|
||||
- публичный ключ `Ed25519 public` (`clientKey`);
|
||||
|
||||
то для шифрования личных сообщений из них получаются:
|
||||
|
||||
- `X25519 private` для расшифровки своих копий;
|
||||
- `X25519 public` для шифрования сообщений на этого пользователя.
|
||||
|
||||
При этом:
|
||||
|
||||
- в профиле пользователя по-прежнему публикуется только `clientKey`;
|
||||
- отдельный публичный `dmEncKey` не требуется;
|
||||
- владелец устройства локально может использовать один и тот же исходный секрет и для подписи, и для вывода ключа шифрования.
|
||||
|
||||
## Что считается стандартным преобразованием
|
||||
|
||||
Под "стандартным преобразованием" в проекте понимается совместимое с общепринятой практикой libsodium/NaCl преобразование:
|
||||
|
||||
- из `Ed25519 private` получается `X25519 private` через стандартную процедуру на основе SHA-512 и clamping;
|
||||
- из `Ed25519 public` получается `X25519 public` через стандартное преобразование точки Edwards в точку Montgomery.
|
||||
|
||||
Иными словами:
|
||||
|
||||
- приватный ключ шифрования должен получаться из приватного ключа подписи стандартным способом;
|
||||
- публичный ключ шифрования должен получаться из публичного `clientKey` стандартным способом;
|
||||
- обе стороны должны получать одинаковый `X25519 public` по одному и тому же `clientKey`.
|
||||
|
||||
## Преобразование приватного ключа
|
||||
|
||||
Исходные данные:
|
||||
|
||||
- `Ed25519 private` в SHiNE рассматривается как 32-байтный seed приватного ключа.
|
||||
|
||||
Стандартная процедура:
|
||||
|
||||
1. Вычислить `SHA-512(seed32)`.
|
||||
2. Взять первые 32 байта результата.
|
||||
3. Применить стандартный clamping для `X25519 private`.
|
||||
4. Полученный результат использовать как `X25519 private`.
|
||||
|
||||
Замечания:
|
||||
|
||||
- нельзя использовать для `X25519 private` просто исходный 32-байтный `Ed25519 seed` без преобразования;
|
||||
- нельзя заменять `SHA-512` на `SHA-256`, `HMAC`, `HKDF` или произвольную пользовательскую формулу;
|
||||
- clamping обязателен.
|
||||
|
||||
## Преобразование публичного ключа
|
||||
|
||||
Исходные данные:
|
||||
|
||||
- `clientKey` пользователя в формате `Ed25519 public` (32 байта).
|
||||
|
||||
Стандартная процедура:
|
||||
|
||||
1. Декодировать публичную точку Edwards из `Ed25519 public`.
|
||||
2. Выполнить стандартное преобразование Edwards -> Montgomery.
|
||||
3. Полученное значение использовать как `X25519 public`.
|
||||
|
||||
На уровне математики используется стандартное отображение:
|
||||
|
||||
- `u = (1 + y) / (1 - y)`
|
||||
|
||||
где `y` берётся из публичной точки Ed25519, а результат `u` кодируется как публичный ключ `X25519`.
|
||||
|
||||
Замечания:
|
||||
|
||||
- нельзя получать `X25519 public` простым хэшированием `clientKey`;
|
||||
- нельзя получать `X25519 public` "через приватный ключ на сервере";
|
||||
- любой клиент должен уметь вывести `X25519 public` другого пользователя, имея только его `clientKey`.
|
||||
|
||||
## Что хранится в проекте
|
||||
|
||||
На текущем архитектурном уровне:
|
||||
|
||||
- в публичном профиле пользователя хранится только `clientKey` (`Ed25519 public`);
|
||||
- приватная часть `clientKey` хранится только у владельца устройства;
|
||||
- `X25519 private` и `X25519 public` считаются производными величинами и могут вычисляться по мере необходимости;
|
||||
- отдельный `keyId` для DM пока не вводится.
|
||||
|
||||
Это означает:
|
||||
|
||||
- если в будущем пользователь сменит `clientKey`, старые DM должны будут быть перешифрованы;
|
||||
- массовая перешифровка истории рассматривается как отдельная функция более позднего этапа;
|
||||
- текущая архитектура должна оставлять возможность такой перешифровки, но не обязана реализовывать её прямо сейчас.
|
||||
|
||||
## Как это применяется в DM
|
||||
|
||||
Для личных сообщений SHiNE:
|
||||
|
||||
- входящая копия сообщения шифруется на `X25519 public`, полученный из `clientKey` получателя;
|
||||
- исходящая копия сообщения шифруется на `X25519 public`, полученный из `clientKey` отправителя;
|
||||
- расшифровать входящую копию может только владелец приватного ключа получателя;
|
||||
- расшифровать исходящую копию может только владелец приватного ключа отправителя.
|
||||
|
||||
Следствия:
|
||||
|
||||
- `encryptedBody` у входящей и исходящей копии одного логического сообщения обычно разный;
|
||||
- сервер не должен требовать побайтного совпадения ciphertext у пары;
|
||||
- сервер не должен уметь расшифровывать содержимое DM;
|
||||
- подпись и шифрование используют один корневой секрет пользователя, но разные стандартно выведенные представления ключа.
|
||||
|
||||
## Ограничения и компромиссы
|
||||
|
||||
Эта архитектура осознанно имеет следующие свойства:
|
||||
|
||||
- компрометация приватного `clientKey` одновременно даёт злоумышленнику возможность и подписывать от имени пользователя, и расшифровывать его личные сообщения;
|
||||
- отдельное разделение ключа подписи и ключа шифрования пока не используется;
|
||||
- простота пользовательской модели и минимизация числа публичных ключей считаются в проекте более важными, чем разделение этих ролей на текущем этапе.
|
||||
|
||||
Если позже проекту понадобится более строгая модель безопасности, допускается переход на отдельный публичный ключ шифрования, но только как отдельная версия протокола.
|
||||
|
||||
## Что запрещено
|
||||
|
||||
В проекте запрещено:
|
||||
|
||||
- придумывать собственное "детерминированное преобразование" `Ed25519 -> X25519`;
|
||||
- хэшировать `clientKey` произвольным образом и считать результат `X25519 public`;
|
||||
- использовать `Ed25519 public` напрямую как ключ ECDH;
|
||||
- использовать сырой `Ed25519 seed` как `X25519 private` без стандартной процедуры;
|
||||
- смешивать разные способы преобразования в разных клиентах.
|
||||
|
||||
## Требование к реализациям
|
||||
|
||||
Любая реализация SHiNE, которая:
|
||||
|
||||
- подписывает DM;
|
||||
- шифрует DM;
|
||||
- расшифровывает DM;
|
||||
- проверяет совместимость личных сообщений между устройствами;
|
||||
|
||||
обязана использовать именно этот подход.
|
||||
|
||||
Если библиотека уже предоставляет готовые совместимые функции преобразования `Ed25519 -> X25519`, нужно использовать их вместо самодельной реализации.
|
||||
@@ -0,0 +1,311 @@
|
||||
# Формат взаимодействия внешнего кошелька и ESP32
|
||||
|
||||
Этот документ фиксирует актуальный формат взаимодействия между внешним браузерным wallet-расширением SHiNE и устройством `ESP32-S3-Touch-AMOLED-2.16`.
|
||||
|
||||
Документ описывает:
|
||||
|
||||
- как расширение получает текущий активный публичный ключ кошелька с ESP32;
|
||||
- как расширение отправляет на ESP32 запрос подписи транзакции;
|
||||
- что именно считается активным кошельком на ESP32;
|
||||
- какие проверки и UI-реакции ожидаются в браузерном расширении и на устройстве;
|
||||
- какие ограничения действуют в текущей версии протокола.
|
||||
|
||||
## 1. Общая идея
|
||||
|
||||
Устройство ESP32 хранит `master secret` пользователя и локально умеет выводить несколько кошельков из одного секрета.
|
||||
|
||||
На устройстве в UI пользователь выбирает текущий активный кошелёк:
|
||||
|
||||
- `client.key`
|
||||
- `root.key`
|
||||
- `custom`
|
||||
|
||||
Для `custom` используется derivation:
|
||||
|
||||
```text
|
||||
sha256(base64(secret32) + "|wallet." + customName)
|
||||
```
|
||||
|
||||
Браузерное расширение не указывает ESP32, какой кошелёк нужно вернуть в первом запросе. Оно просто спрашивает:
|
||||
|
||||
```text
|
||||
какой кошелёк сейчас активен на устройстве
|
||||
```
|
||||
|
||||
ESP32 возвращает:
|
||||
|
||||
- тип текущего активного кошелька;
|
||||
- его публичный ключ `Base58`.
|
||||
|
||||
## 2. Транспорт и маршрут
|
||||
|
||||
Текущий формат использует уже существующую `wallet-session` браузерного расширения.
|
||||
|
||||
Схема маршрута:
|
||||
|
||||
`browser extension -> SHiNE server -> homeserver session on ESP32 -> SHiNE server -> browser extension`
|
||||
|
||||
В текущем формате:
|
||||
|
||||
- отдельная цифровая подпись payload не добавляется;
|
||||
- отдельное E2E-шифрование для wallet RPC не добавляется;
|
||||
- используется существующая авторизованная `wallet-session`, транспорт `WSS` и server-side маршрут через уже существующую операцию `CallSignalToSession`.
|
||||
|
||||
## 3. Запрос текущего публичного ключа кошелька
|
||||
|
||||
### 3.1. Назначение
|
||||
|
||||
Операция нужна, чтобы браузерное расширение могло узнать, какой кошелёк сейчас выбран на ESP32, и показать его пользователю перед дальнейшими действиями.
|
||||
|
||||
### 3.2. Формат запроса
|
||||
|
||||
```json
|
||||
{
|
||||
"v": 1,
|
||||
"operation": "get_wallet_public_key",
|
||||
"requestId": "1718998123456-482193",
|
||||
"timeMs": 1718998123456
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3. Поля запроса
|
||||
|
||||
- `v` — версия формата wallet RPC. Для текущего варианта: `1`.
|
||||
- `operation` — строка операции. Для текущего запроса: `get_wallet_public_key`.
|
||||
- `requestId` — идентификатор запроса, уникальный в пределах сеанса расширения. Рекомендуемый формат:
|
||||
`timeMs-random`.
|
||||
- `timeMs` — локальное время отправителя в миллисекундах.
|
||||
|
||||
### 3.4. Поведение ESP32
|
||||
|
||||
При получении такого запроса ESP32:
|
||||
|
||||
1. смотрит, какой кошелёк сейчас выбран в локальном UI;
|
||||
2. вычисляет или берёт уже подготовленный публичный ключ именно этого активного кошелька;
|
||||
3. возвращает тип кошелька и его `publicKeyBase58`.
|
||||
|
||||
Запрос не содержит:
|
||||
|
||||
- `walletSelector`;
|
||||
- `customName`;
|
||||
- `targetSessionName`.
|
||||
|
||||
Они намеренно не входят в текущий формат этого запроса.
|
||||
|
||||
## 4. Формат ответа
|
||||
|
||||
```json
|
||||
{
|
||||
"v": 1,
|
||||
"op": "get_wallet_public_key_result",
|
||||
"requestId": "1718998123456-482193",
|
||||
"ok": true,
|
||||
"wallet": {
|
||||
"type": "custom",
|
||||
"publicKeyBase58": "...."
|
||||
},
|
||||
"timeMs": 1718998123999
|
||||
}
|
||||
```
|
||||
|
||||
## 5. Поля ответа
|
||||
|
||||
- `v` — версия формата ответа. Сейчас `1`.
|
||||
- `op` — строка результата операции. Сейчас `get_wallet_public_key_result`.
|
||||
- `requestId` — должен совпадать с `requestId` исходного запроса.
|
||||
- `ok` — признак успешного результата.
|
||||
- `wallet.type` — тип активного кошелька:
|
||||
- `client.key`
|
||||
- `root.key`
|
||||
- `custom`
|
||||
- `wallet.publicKeyBase58` — публичный ключ активного кошелька в `Base58`.
|
||||
- `timeMs` — время формирования ответа на стороне ESP32 в миллисекундах.
|
||||
|
||||
## 6. Ошибки текущего формата
|
||||
|
||||
Минимальный формат ошибки допускается таким:
|
||||
|
||||
```json
|
||||
{
|
||||
"v": 1,
|
||||
"op": "get_wallet_public_key_result",
|
||||
"requestId": "1718998123456-482193",
|
||||
"ok": false,
|
||||
"error": "wallet_unavailable",
|
||||
"timeMs": 1718998123999
|
||||
}
|
||||
```
|
||||
|
||||
Рекомендуемые коды ошибок:
|
||||
|
||||
- `wallet_unavailable` — на устройстве нельзя получить текущий кошелёк;
|
||||
- `secret_not_configured` — на устройстве ещё нет корректно сохранённого секрета;
|
||||
- `wallet_type_unknown` — выбранный локальный тип кошелька не распознан;
|
||||
- `internal_error` — прочая локальная ошибка устройства.
|
||||
|
||||
## 7. Правила для браузерного расширения
|
||||
|
||||
После ответа `ok=true` расширение должно:
|
||||
|
||||
1. показать пользователю тип кошелька;
|
||||
2. показать полный `publicKeyBase58`;
|
||||
3. дать кнопку копирования ключа в буфер;
|
||||
4. сохранить этот ключ как текущий ключ устройства для следующей операции подписи.
|
||||
|
||||
### 7.1. Проверка через PDA Solana
|
||||
|
||||
Расширение уже знает публичные ключи пользователя из Solana PDA. Поэтому оно может дополнительно проверить ответ ESP32:
|
||||
|
||||
- если `wallet.type = client.key`, то `publicKeyBase58` должен совпасть с `clientKey`, прочитанным из PDA;
|
||||
- если `wallet.type = root.key`, то `publicKeyBase58` должен совпасть с `rootKey`, прочитанным из PDA;
|
||||
- если `wallet.type = custom`, такой проверки по PDA пока нет.
|
||||
|
||||
При несовпадении для `client.key` или `root.key` расширение должно показать пользователю предупреждение, что возвращённый ключ не совпал с ожидаемым ключом из PDA.
|
||||
|
||||
## 8. Ожидаемое поведение UI расширения
|
||||
|
||||
### 8.1. Общий вид popup
|
||||
|
||||
Popup браузерного расширения должен быть узким и вытянутым по вертикали.
|
||||
|
||||
### 8.2. Состояние без подключения
|
||||
|
||||
Если `wallet-session` ещё не подключена:
|
||||
|
||||
- показывается кнопка `Подключить`;
|
||||
- по нажатию открывается экран подключения, близкий по смыслу к сценарию `Войти через другое устройство`;
|
||||
- пользователь вводит логин устройства и получает код подключения.
|
||||
|
||||
### 8.3. Состояние после подключения
|
||||
|
||||
Если `wallet-session` уже подключена:
|
||||
|
||||
- показывается статус `Подключено`;
|
||||
- остаётся выбор homeserver;
|
||||
- появляется кнопка запроса текущего кошелька;
|
||||
- появляется кнопка `Отключить`.
|
||||
|
||||
### 8.4. Подключение кошелька с сайта
|
||||
|
||||
Когда сайт просит подключить кошелёк через расширение, расширение должно вести себя как обычный wallet extension:
|
||||
|
||||
1. показать пользователю подтверждение подключения;
|
||||
2. показать, какой именно кошелёк будет подключён;
|
||||
3. после подтверждения пользователя завершить подключение;
|
||||
4. если пользователь отказался, не подключать кошелёк к сайту.
|
||||
|
||||
## 9. Запрос подписи транзакции
|
||||
|
||||
### 9.1. Назначение
|
||||
|
||||
Операция нужна, чтобы браузерное расширение могло запросить у ESP32 подпись Solana-транзакции текущим активным кошельком.
|
||||
|
||||
Расширение передаёт:
|
||||
|
||||
- публичный ключ, которым ожидается подпись;
|
||||
- сериализованную транзакцию;
|
||||
- комментарий, который должен быть показан на экране ESP32.
|
||||
|
||||
ESP32:
|
||||
|
||||
1. показывает пользователю запрос подтверждения;
|
||||
2. показывает комментарий к подписи;
|
||||
3. после нажатия `APPROVE` или `REJECT` возвращает ответ в расширение.
|
||||
|
||||
### 9.2. Формат запроса
|
||||
|
||||
```json
|
||||
{
|
||||
"v": 1,
|
||||
"operation": "sign_transaction",
|
||||
"requestId": "1718998123456-482193",
|
||||
"timeMs": 1718998123456,
|
||||
"publicKeyBase58": "....",
|
||||
"transactionBase64": "....",
|
||||
"comment": "Site https://example.com requested transaction signature"
|
||||
}
|
||||
```
|
||||
|
||||
### 9.3. Поля запроса
|
||||
|
||||
- `v` — версия wallet RPC. Сейчас `1`.
|
||||
- `operation` — строка операции: `sign_transaction`.
|
||||
- `requestId` — идентификатор запроса.
|
||||
- `timeMs` — время отправки на стороне расширения.
|
||||
- `publicKeyBase58` — публичный ключ, от которого ожидается подпись.
|
||||
- `transactionBase64` — сериализованная Solana transaction в `base64`.
|
||||
- `comment` — короткое текстовое описание, которое ESP32 показывает пользователю при запросе подписи.
|
||||
|
||||
### 9.4. Поведение ESP32
|
||||
|
||||
При получении такого запроса ESP32:
|
||||
|
||||
1. сравнивает `publicKeyBase58` с публичным ключом текущего активного выбранного кошелька;
|
||||
2. если ключ не совпадает, сразу возвращает ошибку `wallet_mismatch`;
|
||||
3. если ключ совпадает, показывает отдельный экран подтверждения подписи;
|
||||
4. на экране показывает:
|
||||
- каким кошельком будет выполнена подпись;
|
||||
- комментарий `comment`;
|
||||
- кнопки `APPROVE` и `REJECT`;
|
||||
5. если пользователь подтверждает подпись, ESP32 подписывает транзакцию и возвращает результат;
|
||||
6. если пользователь отклоняет, ESP32 возвращает `rejected_by_user`.
|
||||
|
||||
## 10. Формат ответа на подпись
|
||||
|
||||
```json
|
||||
{
|
||||
"v": 1,
|
||||
"op": "sign_transaction_result",
|
||||
"requestId": "1718998123456-482193",
|
||||
"ok": true,
|
||||
"publicKeyBase58": "....",
|
||||
"signatureBase58": "....",
|
||||
"signedTransactionBase64": "....",
|
||||
"timeMs": 1718998123999
|
||||
}
|
||||
```
|
||||
|
||||
Если пользователь отклонил запрос:
|
||||
|
||||
```json
|
||||
{
|
||||
"v": 1,
|
||||
"op": "sign_transaction_result",
|
||||
"requestId": "1718998123456-482193",
|
||||
"ok": false,
|
||||
"error": "rejected_by_user",
|
||||
"timeMs": 1718998123999
|
||||
}
|
||||
```
|
||||
|
||||
Рекомендуемые ошибки для `sign_transaction`:
|
||||
|
||||
- `rejected_by_user`
|
||||
- `wallet_unavailable`
|
||||
- `wallet_mismatch`
|
||||
- `transaction_base64_invalid`
|
||||
- `transaction_sign_failed`
|
||||
- `bad_request`
|
||||
|
||||
## 11. Подключение кошелька с сайта
|
||||
|
||||
При вызове сайта `connect wallet` расширение должно вести себя как обычный wallet extension:
|
||||
|
||||
1. запросить подтверждение у пользователя в браузере;
|
||||
2. получить текущий публичный ключ с ESP32;
|
||||
3. вернуть сайту `publicKey` текущего активного кошелька.
|
||||
|
||||
Для `signTransaction` расширение:
|
||||
|
||||
1. получает транзакцию от сайта;
|
||||
2. пересылает её на ESP32 через `sign_transaction`;
|
||||
3. ждёт решение пользователя на устройстве;
|
||||
4. возвращает браузеру уже подписанную транзакцию.
|
||||
|
||||
## 12. Ограничения текущей версии
|
||||
|
||||
- запрос возвращает только текущий активный кошелёк, а не список всех кошельков;
|
||||
- выбор типа кошелька делается только на самом ESP32;
|
||||
- отдельная цифровая подпись ответа пока не используется;
|
||||
- отдельное E2E-шифрование wallet RPC пока не используется;
|
||||
- `custom`-кошельки пока не сверяются с PDA.
|
||||
Reference in New Issue
Block a user