Перенести документацию в docs и добавить социальный граф

This commit is contained in:
AidarKC
2026-07-13 12:58:41 +04:00
parent c9cfb394d7
commit af12d7b954
91 changed files with 653 additions and 2 deletions
@@ -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.