Миграция PDA на client.key
This commit is contained in:
parent
ba348dafb3
commit
5c92b6a734
@ -134,7 +134,7 @@
|
|||||||
|
|
||||||
Что сделать:
|
Что сделать:
|
||||||
|
|
||||||
- продумать и реализовать смену `root key`, `device key`, `blockchain key`;
|
- продумать и реализовать смену `root key`, `client key`, `blockchain key`;
|
||||||
- описать ограничения, кто и в каком сценарии может менять каждый тип ключа;
|
- описать ограничения, кто и в каком сценарии может менять каждый тип ключа;
|
||||||
- продумать, как не потерять доступ и как обновлять доверие к новым ключам.
|
- продумать, как не потерять доступ и как обновлять доверие к новым ключам.
|
||||||
|
|
||||||
|
|||||||
@ -8,7 +8,7 @@ shine.db.SqliteDbController — один вход в БД: читает db.path,
|
|||||||
shine.db.DatabaseInitializer — разовая сборка схемы (таблицы + индексы).
|
shine.db.DatabaseInitializer — разовая сборка схемы (таблицы + индексы).
|
||||||
|
|
||||||
|
|
||||||
shine.db.entities.* — POJO-модели строк таблиц (без логики, только поля/геттеры/сеттеры + иногда удобные методы вроде getDeviceKeyByte()).
|
shine.db.entities.* — POJO-модели строк таблиц (без логики, только поля/геттеры/сеттеры + иногда удобные методы вроде getClientKeyByte()).
|
||||||
shine.db.dao.* — DAO по таблицам: ActiveSessionsDAO, SolanaUsersDAO, UserParamsDAO, IpGeoCacheDAO, BlockchainStateDAO, BlocksDAO; плюс “сервисные” DAO:
|
shine.db.dao.* — DAO по таблицам: ActiveSessionsDAO, SolanaUsersDAO, UserParamsDAO, IpGeoCacheDAO, BlockchainStateDAO, BlocksDAO; плюс “сервисные” DAO:
|
||||||
|
|
||||||
UserCreateDAO — атомарная регистрация пользователя в транзакции (BEGIN IMMEDIATE + rollback/commit).
|
UserCreateDAO — атомарная регистрация пользователя в транзакции (BEGIN IMMEDIATE + rollback/commit).
|
||||||
|
|||||||
@ -38,7 +38,7 @@ message_stats ⭐
|
|||||||
|
|
||||||
solana_users
|
solana_users
|
||||||
login — TEXT PK — уникальный логин пользователя
|
login — TEXT PK — уникальный логин пользователя
|
||||||
device_key — TEXT NOT NULL — публичный ключ устройства (Base64(32) / HEX(64))
|
client_key — TEXT NOT NULL — публичный ключ устройства (Base64(32) / HEX(64))
|
||||||
solana_key — TEXT NULL — публичный ключ Solana-аккаунта
|
solana_key — TEXT NULL — публичный ключ Solana-аккаунта
|
||||||
|
|
||||||
active_sessions
|
active_sessions
|
||||||
@ -61,7 +61,7 @@ login — TEXT NOT NULL, FK → solana_users(login)
|
|||||||
param — TEXT NOT NULL
|
param — TEXT NOT NULL
|
||||||
time_ms — INTEGER NOT NULL
|
time_ms — INTEGER NOT NULL
|
||||||
value — TEXT NOT NULL
|
value — TEXT NOT NULL
|
||||||
device_key — TEXT NULL
|
client_key — TEXT NULL
|
||||||
signature — TEXT NULL
|
signature — TEXT NULL
|
||||||
|
|
||||||
Ограничение:
|
Ограничение:
|
||||||
|
|||||||
@ -35,7 +35,7 @@
|
|||||||
"blockchainName": "anya-001",
|
"blockchainName": "anya-001",
|
||||||
"solanaKey": "BASE64_32_PUBLIC_KEY",
|
"solanaKey": "BASE64_32_PUBLIC_KEY",
|
||||||
"blockchainKey": "BASE64_32_PUBLIC_KEY",
|
"blockchainKey": "BASE64_32_PUBLIC_KEY",
|
||||||
"deviceKey": "BASE64_32_PUBLIC_KEY",
|
"clientKey": "BASE64_32_PUBLIC_KEY",
|
||||||
"bchLimit": 1000000
|
"bchLimit": 1000000
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -99,7 +99,7 @@
|
|||||||
"blockchainName": "anya-001",
|
"blockchainName": "anya-001",
|
||||||
"solanaKey": "BASE64_32_PUBLIC_KEY",
|
"solanaKey": "BASE64_32_PUBLIC_KEY",
|
||||||
"blockchainKey": "BASE64_32_PUBLIC_KEY",
|
"blockchainKey": "BASE64_32_PUBLIC_KEY",
|
||||||
"deviceKey": "BASE64_32_PUBLIC_KEY",
|
"clientKey": "BASE64_32_PUBLIC_KEY",
|
||||||
"serverLastGlobalNumber": 128,
|
"serverLastGlobalNumber": 128,
|
||||||
"serverLastGlobalHash": "4f...ab",
|
"serverLastGlobalHash": "4f...ab",
|
||||||
"serverBlockchainSizeBytes": 45212,
|
"serverBlockchainSizeBytes": 45212,
|
||||||
|
|||||||
@ -11,7 +11,7 @@
|
|||||||
|
|
||||||
Логика раздела такая:
|
Логика раздела такая:
|
||||||
|
|
||||||
- сначала клиент либо начинает создание новой сессии через `deviceKey`;
|
- сначала клиент либо начинает создание новой сессии через `clientKey`;
|
||||||
- либо начинает вход в уже созданную сессию через `sessionKey`;
|
- либо начинает вход в уже созданную сессию через `sessionKey`;
|
||||||
- сервер на первом шаге выдаёт challenge/nonce;
|
- сервер на первом шаге выдаёт challenge/nonce;
|
||||||
- на втором шаге клиент присылает подписанный ответ;
|
- на втором шаге клиент присылает подписанный ответ;
|
||||||
@ -55,7 +55,7 @@
|
|||||||
2. Вход в существующую сессию:
|
2. Вход в существующую сессию:
|
||||||
`SessionChallenge` -> `SessionLogin`
|
`SessionChallenge` -> `SessionLogin`
|
||||||
|
|
||||||
`deviceKey` используется для создания новой сессии.
|
`clientKey` используется для создания новой сессии.
|
||||||
|
|
||||||
`sessionKey` используется для входа в уже созданную сессию.
|
`sessionKey` используется для входа в уже созданную сессию.
|
||||||
|
|
||||||
@ -119,7 +119,7 @@ ed25519/BASE64_PUBLIC_KEY
|
|||||||
"storagePwd": "BASE64_OR_APP_SPECIFIC_SECRET",
|
"storagePwd": "BASE64_OR_APP_SPECIFIC_SECRET",
|
||||||
"timeMs": 1774600000123,
|
"timeMs": 1774600000123,
|
||||||
"authNonce": "nonce",
|
"authNonce": "nonce",
|
||||||
"deviceKey": "BASE64_DEVICE_PUBLIC_KEY",
|
"clientKey": "BASE64_DEVICE_PUBLIC_KEY",
|
||||||
"signatureB64": "BASE64_SIGNATURE",
|
"signatureB64": "BASE64_SIGNATURE",
|
||||||
"sessionType": 1,
|
"sessionType": 1,
|
||||||
"clientPlatform": "Web",
|
"clientPlatform": "Web",
|
||||||
@ -138,15 +138,15 @@ AUTH_CREATE_SESSION:{login}:{sessionKey}:{storagePwd}:{timeMs}:{authNonce}
|
|||||||
|
|
||||||
Перед проверкой подписи сервер должен:
|
Перед проверкой подписи сервер должен:
|
||||||
|
|
||||||
1. взять актуальный `solana_users.device_key`;
|
1. взять актуальный `solana_users.client_key`;
|
||||||
2. сравнить его с `payload.deviceKey`;
|
2. сравнить его с `payload.clientKey`;
|
||||||
3. только потом проверять подпись.
|
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`.
|
- `501 / NO_LOGIN` — у пользователя на сервере не заполнен `login`.
|
||||||
- `400 / EMPTY_STORAGE_PWD` — пустой `storagePwd`.
|
- `400 / EMPTY_STORAGE_PWD` — пустой `storagePwd`.
|
||||||
- `400 / EMPTY_SESSION_KEY` — пустой `sessionKey`.
|
- `400 / EMPTY_SESSION_KEY` — пустой `sessionKey`.
|
||||||
- `422 / UNSUPPORTED_KEY_ALGORITHM` — префикс алгоритма в `sessionKey` или `deviceKey` не поддерживается текущим сервером.
|
- `422 / UNSUPPORTED_KEY_ALGORITHM` — префикс алгоритма в `sessionKey` или `clientKey` не поддерживается текущим сервером.
|
||||||
- `400 / BAD_BASE64` — неверный Base64 в `sessionKey`, `deviceKey` или `signatureB64`.
|
- `400 / BAD_BASE64` — неверный Base64 в `sessionKey`, `clientKey` или `signatureB64`.
|
||||||
- `400 / EMPTY_SIGNATURE` — пустая подпись.
|
- `400 / EMPTY_SIGNATURE` — пустая подпись.
|
||||||
- `400 / TIME_SKEW` — время клиента отличается от серверного больше допустимого окна.
|
- `400 / TIME_SKEW` — время клиента отличается от серверного больше допустимого окна.
|
||||||
- `400 / NO_DEVICE_KEY` — у пользователя в БД отсутствует `deviceKey`.
|
- `400 / NO_DEVICE_KEY` — у пользователя в БД отсутствует `clientKey`.
|
||||||
- `400 / EMPTY_AUTH_NONCE` — пустой `authNonce`.
|
- `400 / EMPTY_AUTH_NONCE` — пустой `authNonce`.
|
||||||
- `400 / AUTH_NONCE_MISMATCH` — `authNonce` не соответствует значению из `AuthChallenge`.
|
- `400 / AUTH_NONCE_MISMATCH` — `authNonce` не соответствует значению из `AuthChallenge`.
|
||||||
- `400 / EMPTY_DEVICE_KEY` — в запросе не передан `deviceKey`.
|
- `400 / EMPTY_DEVICE_KEY` — в запросе не передан `clientKey`.
|
||||||
- `422 / DEVICE_KEY_NOT_ACTUAL` — `deviceKey` не совпадает с актуальной версией на сервере.
|
- `422 / DEVICE_KEY_NOT_ACTUAL` — `clientKey` не совпадает с актуальной версией на сервере.
|
||||||
- `422 / BAD_SIGNATURE` — подпись не прошла проверку.
|
- `422 / BAD_SIGNATURE` — подпись не прошла проверку.
|
||||||
- `460 / SESSION_TYPE_MISMATCH` — `sessionType` не совпадает с типом сессии, уже опубликованным для этого `sessionKey` в Solana PDA.
|
- `460 / SESSION_TYPE_MISMATCH` — `sessionType` не совпадает с типом сессии, уже опубликованным для этого `sessionKey` в Solana PDA.
|
||||||
- `501 / SESSION_TYPE_PDA_CHECK_FAILED` — сервер не смог проверить `sessionType` по 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` и не становится источником приватных ключей.
|
- сервер не расшифровывает `encryptedPayload` и не становится источником приватных ключей.
|
||||||
|
|||||||
@ -21,7 +21,7 @@
|
|||||||
"param": "display_name",
|
"param": "display_name",
|
||||||
"time_ms": 1774700000123,
|
"time_ms": 1774700000123,
|
||||||
"value": "Alice",
|
"value": "Alice",
|
||||||
"device_key": "BASE64_DEVICE_PUBLIC_KEY",
|
"client_key": "BASE64_DEVICE_PUBLIC_KEY",
|
||||||
"signature": "BASE64_SIGNATURE"
|
"signature": "BASE64_SIGNATURE"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -76,7 +76,7 @@
|
|||||||
"param": "display_name",
|
"param": "display_name",
|
||||||
"time_ms": 1774700000123,
|
"time_ms": 1774700000123,
|
||||||
"value": "Alice",
|
"value": "Alice",
|
||||||
"device_key": "BASE64_DEVICE_PUBLIC_KEY",
|
"client_key": "BASE64_DEVICE_PUBLIC_KEY",
|
||||||
"signature": "BASE64_SIGNATURE"
|
"signature": "BASE64_SIGNATURE"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -116,7 +116,7 @@
|
|||||||
"param": "display_name",
|
"param": "display_name",
|
||||||
"time_ms": 1774700000123,
|
"time_ms": 1774700000123,
|
||||||
"value": "Alice",
|
"value": "Alice",
|
||||||
"device_key": "BASE64_DEVICE_PUBLIC_KEY",
|
"client_key": "BASE64_DEVICE_PUBLIC_KEY",
|
||||||
"signature": "BASE64_SIGNATURE"
|
"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 (основная работа)
|
### ESP32 (основная работа)
|
||||||
- [ ] Инициализация WiFi (SSID/пароль в NVS)
|
- [ ] Инициализация WiFi (SSID/пароль в NVS)
|
||||||
- [ ] WebSocket-клиент (`WebSocketsClient`) — постоянное соединение с сервером
|
- [ ] WebSocket-клиент (`WebSocketsClient`) — постоянное соединение с сервером
|
||||||
- [ ] Авторизация на сервере: `AuthChallenge` → `CreateAuthSession` через `deviceKey` (уже есть в NVS), сохранить `sessionId` в NVS
|
- [ ] Авторизация на сервере: `AuthChallenge` → `CreateAuthSession` через `clientKey` (уже есть в NVS), сохранить `sessionId` в NVS
|
||||||
- [ ] Обработчик входящих WebSocket-событий: JSON-парсинг, диспетчер по типу
|
- [ ] Обработчик входящих WebSocket-событий: JSON-парсинг, диспетчер по типу
|
||||||
- [ ] Новые UI-экраны: «Разрешить сессию?» и «Подписать?» с кнопками Да/Нет
|
- [ ] Новые UI-экраны: «Разрешить сессию?» и «Подписать?» с кнопками Да/Нет
|
||||||
- [ ] Расширенное хранилище ключей в NVS (произвольные именованные ключи сверх базовых трёх)
|
- [ ] Расширенное хранилище ключей в NVS (произвольные именованные ключи сверх базовых трёх)
|
||||||
|
|||||||
@ -56,7 +56,7 @@ seed(32) = SHA-256(material)
|
|||||||
|------|---------|---------------------|
|
|------|---------|---------------------|
|
||||||
| root | `root.key` | Личность. Подписывает unsigned-часть PDA-записи (`RootKeyBlock`). |
|
| root | `root.key` | Личность. Подписывает unsigned-часть PDA-записи (`RootKeyBlock`). |
|
||||||
| blockchain | `bch.key` | Подписывает `LastBlockState` персонального блокчейна (`blockchain_public_key`). |
|
| 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. |
|
| homeserver | `homeserver.key:<имя>` | Ключ homeserver-устройства, по одному на каждый homeserver (различитель — имя). См. §4. |
|
||||||
|
|
||||||
Полные роли каждого ключа — в `Dev_Docs/Keys/README.md`.
|
Полные роли каждого ключа — в `Dev_Docs/Keys/README.md`.
|
||||||
@ -67,16 +67,16 @@ seed(32) = SHA-256(material)
|
|||||||
|
|
||||||
Отдельного «солана-ключа» нет. На Solana работают два ключа:
|
Отдельного «солана-ключа» нет. На Solana работают два ключа:
|
||||||
|
|
||||||
- **`dev.key` (device) — пополняемый кошелёк и fee payer.** Solana-адрес = `base58(devicePub)`.
|
- **`client.key` (device) — пополняемый кошелёк и fee payer.** Solana-адрес = `base58(clientPub)`.
|
||||||
Этим ключом оплачиваются и подписываются `create_user_pda` / `update_user_pda`.
|
Этим ключом оплачиваются и подписываются `create_user_pda` / `update_user_pda`.
|
||||||
Пополнять SOL нужно именно на этот адрес.
|
Пополнять SOL нужно именно на этот адрес.
|
||||||
- **`root.key` — авторитет записи**, подписывает unsigned-часть PDA через Ed25519-инструкцию, но **не** является fee payer.
|
- **`root.key` — авторитет записи**, подписывает unsigned-часть PDA через Ed25519-инструкцию, но **не** является fee payer.
|
||||||
|
|
||||||
Соответствует формату PDA `shine-solana/shine/doc/formats/shine-user-pda-format-v.1.0.md` §2.1
|
Соответствует формату 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-записью
|
Кратко про роли на Solana: `root.key` — это **главный (master) ключ**: им управляют PDA-записью
|
||||||
(`create/update`) и через это можно заменить все остальные ключи; `dev.key` — это **пополняемый
|
(`create/update`) и через это можно заменить все остальные ключи; `client.key` — это **пополняемый
|
||||||
кошелёк и плательщик комиссий**. Полное описание ролей — `Dev_Docs/Keys/README.md`.
|
кошелёк и плательщик комиссий**. Полное описание ролей — `Dev_Docs/Keys/README.md`.
|
||||||
|
|
||||||
---
|
---
|
||||||
@ -118,9 +118,9 @@ homeserver.key:home-b -> ключ B
|
|||||||
- `shine-UI/server-ui/js/server-ui-shared.js` — те же root/bch/dev для серверного UI (~147–160).
|
- `shine-UI/server-ui/js/server-ui-shared.js` — те же root/bch/dev для серверного UI (~147–160).
|
||||||
|
|
||||||
### Solana-ключ / адрес кошелька (UI)
|
### Solana-ключ / адрес кошелька (UI)
|
||||||
- `shine-UI/js/pages/registration-payment-view.js` — `deriveUserWalletAddress`: адрес = `base58(devicePub)` (~113).
|
- `shine-UI/js/pages/registration-payment-view.js` — `deriveUserWalletAddress`: адрес = `base58(clientPub)` (~113).
|
||||||
- `shine-UI/js/pages/topup-view.js` — `deviceWalletAddressFromBundle`: тот же канонический адрес из `preGeneratedKeyBundle.devicePair`.
|
- `shine-UI/js/pages/topup-view.js` — `clientWalletAddressFromBundle`: тот же канонический адрес из `preGeneratedKeyBundle.clientPair`.
|
||||||
Прежний расходящийся путь `deriveWalletFromPassword` (прямой Argon2 по `dev.key`, мимо `masterSecret`) удалён.
|
Прежний расходящийся путь `deriveWalletFromPassword` (прямой Argon2 по `client.key`, мимо `masterSecret`) удалён.
|
||||||
|
|
||||||
### Деривация ключей (прошивка ESP32)
|
### Деривация ключей (прошивка ESP32)
|
||||||
- `ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/main-device/shine_homeserver_main/shine_homeserver_main.ino`
|
- `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 (куда попадают ключи)
|
### Формат PDA (куда попадают ключи)
|
||||||
- `shine-solana/shine/doc/formats/shine-user-pda-format-v.1.0.md`
|
- `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)
|
### Сервер (тестовый seed)
|
||||||
- `SHiNE-server/src/test/java/test/it/cases/SeedDataPopulationHelper.java` `deriveKeysFromPassword` (~246) —
|
- `SHiNE-server/src/test/java/test/it/cases/SeedDataPopulationHelper.java` `deriveKeysFromPassword` (~246) —
|
||||||
|
|||||||
@ -8,9 +8,9 @@
|
|||||||
|
|
||||||
В SHiNE у пользователя есть несколько уровней ключей:
|
В SHiNE у пользователя есть несколько уровней ключей:
|
||||||
|
|
||||||
- `root key` - главный (master) ключ пользователя: тот, кто им владеет, управляет пользовательской PDA в Solana и может заменить все остальные ключи. Это не пополняемый кошелёк (комиссии платит `device key`).
|
- `root key` - главный (master) ключ пользователя: тот, кто им владеет, управляет пользовательской PDA в Solana и может заменить все остальные ключи. Это не пополняемый кошелёк (комиссии платит `client key`).
|
||||||
- `blockchain key` - ключ записи в персональный SHiNE-блокчейн пользователя.
|
- `blockchain key` - ключ записи в персональный SHiNE-блокчейн пользователя.
|
||||||
- `device key` - общий ключ пользовательских устройств для повседневной работы, звонков, DM и мелких платежей.
|
- `client key` - общий ключ пользовательских устройств для повседневной работы, звонков, DM и мелких платежей.
|
||||||
- `session key` - ключ конкретной сессии или конкретного устройства для авторизации на сервере.
|
- `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` — это **главный (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`
|
## `blockchain key`
|
||||||
|
|
||||||
@ -50,9 +50,9 @@
|
|||||||
|
|
||||||
Рабочая логика по умолчанию должна использовать последнюю актуальную ветку. Старые ветки остаются читаемыми и показывают историю смены ключей.
|
Рабочая логика по умолчанию должна использовать последнюю актуальную ветку. Старые ветки остаются читаемыми и показывают историю смены ключей.
|
||||||
|
|
||||||
## `device key`
|
## `client key`
|
||||||
|
|
||||||
`device key` - общий ключ, который знают доверенные устройства пользователя.
|
`client key` - общий ключ, который знают доверенные устройства пользователя.
|
||||||
|
|
||||||
Назначение:
|
Назначение:
|
||||||
|
|
||||||
@ -63,11 +63,11 @@
|
|||||||
- derivation Arweave-кошелька;
|
- derivation Arweave-кошелька;
|
||||||
- оплата или подготовка добавления данных в Arweave-кошелек по отдельному протоколу.
|
- оплата или подготовка добавления данных в Arweave-кошелек по отдельному протоколу.
|
||||||
|
|
||||||
Arweave-кошелёк должен выводиться из `device key` по протоколу:
|
Arweave-кошелёк должен выводиться из `client key` по протоколу:
|
||||||
|
|
||||||
- `Dev_Docs/Протоколы/SHINE_ARWEAVE_DERIVATION_V1.md`
|
- `Dev_Docs/Протоколы/SHINE_ARWEAVE_DERIVATION_V1.md`
|
||||||
|
|
||||||
Если пользователь теряет только `device key`, в худшем случае ломается повседневная переписка и доступ конкретных устройств к ежедневным операциям. `root key` и `blockchain key` при правильной архитектуре остаются отдельно защищёнными.
|
Если пользователь теряет только `client key`, в худшем случае ломается повседневная переписка и доступ конкретных устройств к ежедневным операциям. `root key` и `blockchain key` при правильной архитектуре остаются отдельно защищёнными.
|
||||||
|
|
||||||
## `session key`
|
## `session key`
|
||||||
|
|
||||||
@ -83,7 +83,7 @@ Arweave-кошелёк должен выводиться из `device key` по
|
|||||||
- авторизация сессии на сервере;
|
- авторизация сессии на сервере;
|
||||||
- привязка устройства к пользователю;
|
- привязка устройства к пользователю;
|
||||||
- подтверждение запросов от конкретной сессии;
|
- подтверждение запросов от конкретной сессии;
|
||||||
- доступ к зашифрованному `device key` после успешной авторизации.
|
- доступ к зашифрованному `client key` после успешной авторизации.
|
||||||
|
|
||||||
Одна и та же сессия может быть пригодна для подключения к нескольким серверам пользователя, если архитектура конкретного пользователя это допускает.
|
Одна и та же сессия может быть пригодна для подключения к нескольким серверам пользователя, если архитектура конкретного пользователя это допускает.
|
||||||
|
|
||||||
@ -108,14 +108,14 @@ Arweave-кошелёк должен выводиться из `device key` по
|
|||||||
Обычное устройство обычно имеет:
|
Обычное устройство обычно имеет:
|
||||||
|
|
||||||
- собственный `session key`;
|
- собственный `session key`;
|
||||||
- зашифрованный `device key`, который открывается после авторизации;
|
- зашифрованный `client key`, который открывается после авторизации;
|
||||||
- доступ к DM, звонкам и обычным пользовательским операциям.
|
- доступ к DM, звонкам и обычным пользовательским операциям.
|
||||||
|
|
||||||
Доверенное серверное или аппаратное устройство может иметь:
|
Доверенное серверное или аппаратное устройство может иметь:
|
||||||
|
|
||||||
- `root key`;
|
- `root key`;
|
||||||
- `blockchain key`;
|
- `blockchain key`;
|
||||||
- `device key`;
|
- `client key`;
|
||||||
- собственный `session key`.
|
- собственный `session key`.
|
||||||
|
|
||||||
Такая сессия может подписывать операции повышенной важности по запросам пользователя.
|
Такая сессия может подписывать операции повышенной важности по запросам пользователя.
|
||||||
@ -139,7 +139,7 @@ Self-message - это сообщение пользователя самому
|
|||||||
|
|
||||||
Входящее сообщение может быть зашифровано:
|
Входящее сообщение может быть зашифровано:
|
||||||
|
|
||||||
- `device key`;
|
- `client key`;
|
||||||
- `session 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/Personal_Messages/README.md` - текущая документация личных сообщений.
|
||||||
- `Dev_Docs/Blockchain/README.md` - точка входа по форматам SHiNE-блокчейна.
|
- `Dev_Docs/Blockchain/README.md` - точка входа по форматам SHiNE-блокчейна.
|
||||||
- `Dev_Docs/Solana_Architecture/README.md` - архитектура Solana-программ, PDA-счетов, DAO и движения средств.
|
- `Dev_Docs/Solana_Architecture/README.md` - архитектура Solana-программ, PDA-счетов, DAO и движения средств.
|
||||||
- `Dev_Docs/Инициализация_Solana_регистрации/README.md` - деплой и первичная инициализация Solana-регистрации.
|
- `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`;
|
- какие операции требуют `root key`, а какие достаточно подписывать `blockchain key`;
|
||||||
- формат self-message-команд;
|
- формат self-message-команд;
|
||||||
- порядок перебора ключей при расшифровке входящих сообщений;
|
- порядок перебора ключей при расшифровке входящих сообщений;
|
||||||
- правила ротации `device key` и восстановления доступа после потери устройства;
|
- правила ротации `client key` и восстановления доступа после потери устройства;
|
||||||
- какие типы серверных и аппаратных сессий нужны в первой реализации.
|
- какие типы серверных и аппаратных сессий нужны в первой реализации.
|
||||||
|
|||||||
@ -11,7 +11,7 @@
|
|||||||
- на ESP32 после успешной homeserver-авторизации в `SETTINGS` появляется пункт `Device requests` первым;
|
- на ESP32 после успешной homeserver-авторизации в `SETTINGS` появляется пункт `Device requests` первым;
|
||||||
- `REFRESH` реально загружает активные заявки;
|
- `REFRESH` реально загружает активные заявки;
|
||||||
- на экране видно две плитки, список листается вертикально;
|
- на экране видно две плитки, список листается вертикально;
|
||||||
- client-session заявка после `YES` подключается с передачей только `device key`;
|
- client-session заявка после `YES` подключается с передачей только `client key`;
|
||||||
- wallet-session заявка после `YES` подключается без передачи ключей, через выпуск отдельной wallet-session;
|
- wallet-session заявка после `YES` подключается без передачи ключей, через выпуск отдельной wallet-session;
|
||||||
- `NO` отклоняет заявку и она исчезает из списка активных.
|
- `NO` отклоняет заявку и она исчезает из списка активных.
|
||||||
|
|
||||||
|
|||||||
@ -2,20 +2,20 @@
|
|||||||
|
|
||||||
- краткое описание:
|
- краткое описание:
|
||||||
- в ESP32 заменён домашний блок `баланс + QR` на единый вход в экран кошелька;
|
- в ESP32 заменён домашний блок `баланс + QR` на единый вход в экран кошелька;
|
||||||
- добавлен выбор активного кошелька `DeviceKey / RootKey / Custom`;
|
- добавлен выбор активного кошелька `ClientKey / RootKey / Custom`;
|
||||||
- для browser extension добавлен первый RPC `get_wallet_public_key` через существующий `wallet-session` и `CallSignalToSession`;
|
- для browser extension добавлен первый RPC `get_wallet_public_key` через существующий `wallet-session` и `CallSignalToSession`;
|
||||||
- в popup расширения добавлен запрос текущего кошелька и копирование `publicKeyBase58`.
|
- в popup расширения добавлен запрос текущего кошелька и копирование `publicKeyBase58`.
|
||||||
|
|
||||||
- что проверять:
|
- что проверять:
|
||||||
- на ESP32 после ввода секрета на главном экране видна кнопка `Кошелёк: ...`;
|
- на ESP32 после ввода секрета на главном экране видна кнопка `Кошелёк: ...`;
|
||||||
- экран `WALLET` открывается и показывает текущий тип кошелька;
|
- экран `WALLET` открывается и показывает текущий тип кошелька;
|
||||||
- экран `WALLET_SELECT` переключает `DeviceKey`, `RootKey` и `Custom`;
|
- экран `WALLET_SELECT` переключает `ClientKey`, `RootKey` и `Custom`;
|
||||||
- для `Custom` открывается ввод имени и после сохранения derivation работает;
|
- для `Custom` открывается ввод имени и после сохранения derivation работает;
|
||||||
- `Показать баланс кошелька` читает баланс именно активного кошелька;
|
- `Показать баланс кошелька` читает баланс именно активного кошелька;
|
||||||
- `Показать QR-код кошелька` показывает QR и адрес именно активного кошелька;
|
- `Показать QR-код кошелька` показывает QR и адрес именно активного кошелька;
|
||||||
- browser extension после подключения wallet-session может запросить текущий кошелёк у ESP32;
|
- browser extension после подключения wallet-session может запросить текущий кошелёк у ESP32;
|
||||||
- extension показывает тип кошелька, полный `publicKeyBase58`, результат проверки через PDA и копирует ключ в буфер;
|
- extension показывает тип кошелька, полный `publicKeyBase58`, результат проверки через PDA и копирует ключ в буфер;
|
||||||
- для `dev.key` и `root.key` проверка через PDA даёт ожидаемое совпадение.
|
- для `client.key` и `root.key` проверка через PDA даёт ожидаемое совпадение.
|
||||||
|
|
||||||
- ожидаемый результат:
|
- ожидаемый результат:
|
||||||
- активный кошелёк на ESP32 реально влияет на баланс, QR и ответ `get_wallet_public_key`;
|
- активный кошелёк на ESP32 реально влияет на баланс, QR и ответ `get_wallet_public_key`;
|
||||||
|
|||||||
@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
- Что проверять:
|
- Что проверять:
|
||||||
1. На ESP32 открыть `HOME`, `WALLET`, `SELECT WALLET`, `WALLET QR` и убедиться, что пользовательские строки на экране только на английском.
|
1. На ESP32 открыть `HOME`, `WALLET`, `SELECT WALLET`, `WALLET QR` и убедиться, что пользовательские строки на экране только на английском.
|
||||||
2. Проверить сценарий выбора `DeviceKey`, `RootKey`, `Custom` и чтение баланса/QR после переключения.
|
2. Проверить сценарий выбора `ClientKey`, `RootKey`, `Custom` и чтение баланса/QR после переключения.
|
||||||
3. В расширении открыть popup, запустить `Подключить` для логина, у которого ещё не настраивался trusted-device login.
|
3. В расширении открыть popup, запустить `Подключить` для логина, у которого ещё не настраивался trusted-device login.
|
||||||
4. Убедиться, что `StartTrustedDeviceLogin` больше не падает с `NullPointerException`.
|
4. Убедиться, что `StartTrustedDeviceLogin` больше не падает с `NullPointerException`.
|
||||||
5. После создания pairing-запроса проверить, что homeserver/UI может его увидеть и обработать как раньше.
|
5. После создания pairing-запроса проверить, что homeserver/UI может его увидеть и обработать как раньше.
|
||||||
|
|||||||
@ -12,7 +12,7 @@
|
|||||||
6. Проверить оба варианта:
|
6. Проверить оба варианта:
|
||||||
- `APPROVE` возвращает сайту подписанную транзакцию;
|
- `APPROVE` возвращает сайту подписанную транзакцию;
|
||||||
- `REJECT` возвращает отказ.
|
- `REJECT` возвращает отказ.
|
||||||
7. Проверить сценарии для `DeviceKey`, `RootKey`, `Custom`.
|
7. Проверить сценарии для `ClientKey`, `RootKey`, `Custom`.
|
||||||
|
|
||||||
- Ожидаемый результат:
|
- Ожидаемый результат:
|
||||||
- сайт может подключить кошелёк через provider расширения;
|
- сайт может подключить кошелёк через provider расширения;
|
||||||
|
|||||||
@ -12,8 +12,9 @@
|
|||||||
|
|
||||||
- логин пользователя;
|
- логин пользователя;
|
||||||
- неизменяемые параметры создания записи;
|
- неизменяемые параметры создания записи;
|
||||||
|
- публичный recovery-ключ пользователя;
|
||||||
- корневой публичный ключ пользователя;
|
- корневой публичный ключ пользователя;
|
||||||
- ключ устройства;
|
- клиентский публичный ключ пользователя;
|
||||||
- данные одного или нескольких пользовательских блокчейнов SHiNE;
|
- данные одного или нескольких пользовательских блокчейнов SHiNE;
|
||||||
- серверные данные пользователя, если пользователь выступает сервером;
|
- серверные данные пользователя, если пользователь выступает сервером;
|
||||||
- серверы доступа пользователя;
|
- серверы доступа пользователя;
|
||||||
@ -34,9 +35,9 @@
|
|||||||
|
|
||||||
## 2.1. Кто оплачивает create/update PDA
|
## 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.
|
- `root_key` используется для подписи unsigned части записи через Ed25519 instruction и не является fee payer.
|
||||||
- Для server PDA это правило то же самое: пополнять SOL нужно на адрес `device_key`.
|
- Для server PDA это правило то же самое: пополнять SOL нужно на адрес `client_key`.
|
||||||
|
|
||||||
## 3. Общие правила кодирования
|
## 3. Общие правила кодирования
|
||||||
|
|
||||||
@ -85,8 +86,9 @@ UserPdaRecordV1
|
|||||||
|
|
||||||
| block_type | Блок | Назначение |
|
| block_type | Блок | Назначение |
|
||||||
|------------|------|------------|
|
|------------|------|------------|
|
||||||
|
| `0` | `RecoveryKeyBlock` | Ключ восстановления пользователя. |
|
||||||
| `1` | `RootKeyBlock` | Корневой ключ пользователя. |
|
| `1` | `RootKeyBlock` | Корневой ключ пользователя. |
|
||||||
| `2` | `DeviceKeyBlock` | Ключ устройства пользователя. |
|
| `2` | `ClientKeyBlock` | Клиентский ключ пользователя. |
|
||||||
| `3` | `BlockchainRegistryBlock` | Один или несколько блокчейнов пользователя. |
|
| `3` | `BlockchainRegistryBlock` | Один или несколько блокчейнов пользователя. |
|
||||||
| `30` | `ServerProfileBlock` | Серверные данные пользователя. |
|
| `30` | `ServerProfileBlock` | Серверные данные пользователя. |
|
||||||
| `40` | `AccessServersBlock` | Серверы доступа/relay. |
|
| `40` | `AccessServersBlock` | Серверы доступа/relay. |
|
||||||
@ -97,13 +99,31 @@ UserPdaRecordV1
|
|||||||
Правила:
|
Правила:
|
||||||
|
|
||||||
- неизвестный `block_type` в `format_major = 1` считается ошибкой;
|
- неизвестный `block_type` в `format_major = 1` считается ошибкой;
|
||||||
- обязательные блоки: `RootKeyBlock`, `DeviceKeyBlock`, `BlockchainRegistryBlock`;
|
- обязательные блоки: `RecoveryKeyBlock`, `RootKeyBlock`, `ClientKeyBlock`, `BlockchainRegistryBlock`;
|
||||||
- необязательные блоки: `ServerProfileBlock`, `AccessServersBlock`, `SessionsBlock`, `TrustedStateBlock`;
|
- необязательные блоки: `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`.
|
Смена `root_key` пока не проектируется и не реализуется. Блок фиксирует только стадию `0`.
|
||||||
|
|
||||||
@ -120,24 +140,24 @@ RootKeyBlock
|
|||||||
- при обновлении `root_key` должен совпадать с предыдущей записью;
|
- при обновлении `root_key` должен совпадать с предыдущей записью;
|
||||||
- ротация root-key будет отдельным форматом/сценарием в будущем.
|
- ротация root-key будет отдельным форматом/сценарием в будущем.
|
||||||
|
|
||||||
## 7. DeviceKeyBlock
|
## 8. ClientKeyBlock
|
||||||
|
|
||||||
Смена `device_key` пока также не проектируется как отдельная ротация. В версии `0` хранится один ключ устройства.
|
Смена `client_key` пока также не проектируется как отдельная ротация. В версии `0` хранится один клиентский ключ пользователя.
|
||||||
|
|
||||||
```text
|
```text
|
||||||
DeviceKeyBlock
|
ClientKeyBlock
|
||||||
- block_type: u8 = 2
|
- block_type: u8 = 2
|
||||||
- block_version: u8 = 0
|
- block_version: u8 = 0
|
||||||
- device_key: [u8; 32]
|
- client_key: [u8; 32]
|
||||||
```
|
```
|
||||||
|
|
||||||
Правила:
|
Правила:
|
||||||
|
|
||||||
- при создании задается текущий публичный ключ устройства;
|
- при создании задается текущий клиентский публичный ключ пользователя;
|
||||||
- при обновлении ключ устройства может быть обновлен только если это отдельно разрешено бизнес-логикой инструкции;
|
- при обновлении `client_key` должен совпадать с предыдущей записью;
|
||||||
- история устройств и несколько устройств в этом формате не хранятся.
|
- история устройств и несколько клиентских ключей в этом формате не хранятся.
|
||||||
|
|
||||||
## 8. BlockchainRegistryBlock
|
## 9. BlockchainRegistryBlock
|
||||||
|
|
||||||
Блок хранит данные пользовательских блокчейнов SHiNE. Сейчас используется один блокчейн, но структура сразу сделана как список.
|
Блок хранит данные пользовательских блокчейнов SHiNE. Сейчас используется один блокчейн, но структура сразу сделана как список.
|
||||||
|
|
||||||
@ -155,7 +175,7 @@ BlockchainRegistryBlock
|
|||||||
- в будущем можно увеличить количество записей без изменения смысла `BlockchainRecord`;
|
- в будущем можно увеличить количество записей без изменения смысла `BlockchainRecord`;
|
||||||
- каждый `BlockchainRecord` описывает один пользовательский SHiNE-блокчейн.
|
- каждый `BlockchainRecord` описывает один пользовательский SHiNE-блокчейн.
|
||||||
|
|
||||||
## 9. BlockchainRecord
|
## 10. BlockchainRecord
|
||||||
|
|
||||||
```text
|
```text
|
||||||
BlockchainRecord
|
BlockchainRecord
|
||||||
@ -191,7 +211,7 @@ BlockchainRecord
|
|||||||
|
|
||||||
Arweave `tx_id` - обычное поле внутри записи конкретного блокчейна. Solana-программа не проверяет, что такой Arweave transaction действительно существует и содержит корректные данные; это ответственность клиента/сервера/пользователя.
|
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` подтверждает состояние конкретного пользовательского блокчейна. Подписывается не голый хэш, а связка логина, имени блокчейна, номера последнего блока, хэша последнего блока и занятого размера.
|
Причина проверки подписи `LastBlockState`: `root_key` управляет Solana-записью пользователя, а `blockchain_public_key` подтверждает состояние конкретного пользовательского блокчейна. Подписывается не голый хэш, а связка логина, имени блокчейна, номера последнего блока, хэша последнего блока и занятого размера.
|
||||||
|
|
||||||
## 11. ServerProfileBlock
|
## 12. ServerProfileBlock
|
||||||
|
|
||||||
Блок присутствует, если пользователь выступает сервером.
|
Блок присутствует, если пользователь выступает сервером.
|
||||||
|
|
||||||
@ -255,7 +275,7 @@ ServerProfileBlock
|
|||||||
- `server_address` - строковый адрес сервера в соответствии с `address_format_type`;
|
- `server_address` - строковый адрес сервера в соответствии с `address_format_type`;
|
||||||
- `sync_servers` - логины SHiNE-пользователей, зарегистрированных как серверы, с которыми этот сервер синхронизирует блокчейн и личные сообщения. Solana-программа не обязана проверять, что эти логины действительно зарегистрированы как серверы.
|
- `sync_servers` - логины SHiNE-пользователей, зарегистрированных как серверы, с которыми этот сервер синхронизирует блокчейн и личные сообщения. Solana-программа не обязана проверять, что эти логины действительно зарегистрированы как серверы.
|
||||||
|
|
||||||
## 12. AccessServersBlock
|
## 13. AccessServersBlock
|
||||||
|
|
||||||
Блок хранит серверы доступа/relay для пользователя.
|
Блок хранит серверы доступа/relay для пользователя.
|
||||||
|
|
||||||
@ -274,7 +294,7 @@ AccessServersBlock
|
|||||||
- `access_servers` - логины пользователей системы, используемых как серверы доступа/relay. Solana-программа не обязана проверять, что эти логины действительно зарегистрированы как серверы;
|
- `access_servers` - логины пользователей системы, используемых как серверы доступа/relay. Solana-программа не обязана проверять, что эти логины действительно зарегистрированы как серверы;
|
||||||
- точная семантика выбора сервера доступа определяется клиентской/серверной логикой SHiNE.
|
- точная семантика выбора сервера доступа определяется клиентской/серверной логикой SHiNE.
|
||||||
|
|
||||||
## 13. SessionsBlock
|
## 14. SessionsBlock
|
||||||
|
|
||||||
Блок хранит опубликованные пользовательские сессии. На текущем этапе регистрация пользователя не добавляет туда записи автоматически, поэтому стандартный create/update продолжает работать с пустым списком.
|
Блок хранит опубликованные пользовательские сессии. На текущем этапе регистрация пользователя не добавляет туда записи автоматически, поэтому стандартный create/update продолжает работать с пустым списком.
|
||||||
|
|
||||||
@ -309,6 +329,7 @@ SessionRecord
|
|||||||
| Значение | Смысл |
|
| Значение | Смысл |
|
||||||
|----------|-------|
|
|----------|-------|
|
||||||
| `1` | Обычная пользовательская сессия. |
|
| `1` | Обычная пользовательская сессия. |
|
||||||
|
| `50` | Кошелёк пользователя. |
|
||||||
| `100` | Homeserver пользователя. |
|
| `100` | Homeserver пользователя. |
|
||||||
|
|
||||||
Правила:
|
Правила:
|
||||||
@ -320,7 +341,7 @@ SessionRecord
|
|||||||
- внутри одного блока должны быть уникальны и `session_name`, и `session_pub_key`;
|
- внутри одного блока должны быть уникальны и `session_name`, и `session_pub_key`;
|
||||||
- на текущем этапе UI и регистрация не обязаны добавлять туда записи автоматически.
|
- на текущем этапе UI и регистрация не обязаны добавлять туда записи автоматически.
|
||||||
|
|
||||||
## 14. TrustedStateBlock
|
## 15. TrustedStateBlock
|
||||||
|
|
||||||
Пока trusted-логика не реализована полностью, поэтому блок хранит только счетчик.
|
Пока trusted-логика не реализована полностью, поэтому блок хранит только счетчик.
|
||||||
|
|
||||||
@ -333,7 +354,7 @@ TrustedStateBlock
|
|||||||
|
|
||||||
Пока блок с доверенными лицами не реализуется, потому что полный формат trusted-логики еще не составлен. В будущем trusted-связи, очереди, таймеры и подтверждения должны быть вынесены в отдельный формат.
|
Пока блок с доверенными лицами не реализуется, потому что полный формат trusted-логики еще не составлен. В будущем trusted-связи, очереди, таймеры и подтверждения должны быть вынесены в отдельный формат.
|
||||||
|
|
||||||
## 15. Подпись user_pda
|
## 16. Подпись user_pda
|
||||||
|
|
||||||
Подписывается не вся PDA целиком, а unsigned-часть записи:
|
Подписывается не вся PDA целиком, а unsigned-часть записи:
|
||||||
|
|
||||||
@ -354,7 +375,7 @@ Solana-программа проверяет подпись через встр
|
|||||||
|
|
||||||
Смену формата подписи сейчас не трогаем.
|
Смену формата подписи сейчас не трогаем.
|
||||||
|
|
||||||
## 16. Регистрация пользователя
|
## 17. Регистрация пользователя
|
||||||
|
|
||||||
При регистрации:
|
При регистрации:
|
||||||
|
|
||||||
@ -372,12 +393,12 @@ Solana-программа проверяет подпись через встр
|
|||||||
- если покупается дополнительный лимит, пользователь платит комиссию за этот лимит;
|
- если покупается дополнительный лимит, пользователь платит комиссию за этот лимит;
|
||||||
- вся unsigned-часть записи подписана `root_key`.
|
- вся unsigned-часть записи подписана `root_key`.
|
||||||
|
|
||||||
## 17. Обновление пользователя
|
## 18. Обновление пользователя
|
||||||
|
|
||||||
При обновлении:
|
При обновлении:
|
||||||
|
|
||||||
- PDA должна существовать;
|
- PDA должна существовать;
|
||||||
- `login`, `created_at_ms`, `root_key` не меняются;
|
- `login`, `created_at_ms`, `recovery_key`, `root_key`, `client_key` не меняются;
|
||||||
- `record_number = previous_record_number + 1`;
|
- `record_number = previous_record_number + 1`;
|
||||||
- `prev_record_hash` равен хэшу unsigned-части предыдущей записи;
|
- `prev_record_hash` равен хэшу unsigned-части предыдущей записи;
|
||||||
- `updated_at_ms` обновляется;
|
- `updated_at_ms` обновляется;
|
||||||
@ -387,7 +408,7 @@ Solana-программа проверяет подпись через встр
|
|||||||
- при увеличении оплаченного лимита пользователь доплачивает комиссию;
|
- при увеличении оплаченного лимита пользователь доплачивает комиссию;
|
||||||
- Arweave `tx_id` может быть пустым или обновленным, но его содержимое Solana не валидирует.
|
- Arweave `tx_id` может быть пустым или обновленным, но его содержимое Solana не валидирует.
|
||||||
|
|
||||||
## 18. Отличия от старого линейного формата
|
## 19. Отличия от старого линейного формата
|
||||||
|
|
||||||
Старый формат после `login` хранил поля линейно:
|
Старый формат после `login` хранил поля линейно:
|
||||||
|
|
||||||
@ -395,8 +416,8 @@ Solana-программа проверяет подпись через встр
|
|||||||
- `root_key`;
|
- `root_key`;
|
||||||
- `blockchain_key_status`;
|
- `blockchain_key_status`;
|
||||||
- `blockchain_key`;
|
- `blockchain_key`;
|
||||||
- `device_key_status`;
|
- `client_key_status`;
|
||||||
- `device_key`;
|
- `client_key`;
|
||||||
- `chain_number`;
|
- `chain_number`;
|
||||||
- `balance`;
|
- `balance`;
|
||||||
- серверные поля;
|
- серверные поля;
|
||||||
@ -407,17 +428,54 @@ Solana-программа проверяет подпись через встр
|
|||||||
|
|
||||||
Новый целевой формат сохраняет первые 9 фиксированных полей как заголовок, но дальше переходит на типизированные блоки:
|
Новый целевой формат сохраняет первые 9 фиксированных полей как заголовок, но дальше переходит на типизированные блоки:
|
||||||
|
|
||||||
|
- recovery-ключ становится отдельным обязательным блоком;
|
||||||
- ключи становятся отдельными блоками;
|
- ключи становятся отдельными блоками;
|
||||||
- данные блокчейна становятся расширенным блоком со своим публичным ключом, лимитом, занятым размером, вершиной цепочки и Arweave `tx_id`;
|
- данные блокчейна становятся расширенным блоком со своим публичным ключом, лимитом, занятым размером, вершиной цепочки и Arweave `tx_id`;
|
||||||
- серверные данные и access-серверы отделяются от данных блокчейна;
|
- серверные данные и access-серверы отделяются от данных блокчейна;
|
||||||
- расширение формата делается добавлением новых версий блоков или новых `block_type`, а не вставкой полей в середину линейной записи.
|
- расширение формата делается добавлением новых версий блоков или новых `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`;
|
- ротацию `root_key`;
|
||||||
- сложную ротацию `device_key`;
|
- сложную ротацию `client_key`;
|
||||||
- ротацию `blockchain_public_key`;
|
- ротацию `blockchain_public_key`;
|
||||||
- проверку содержимого Arweave transaction;
|
- проверку содержимого Arweave transaction;
|
||||||
- хранение полной истории пользовательского блокчейна внутри Solana;
|
- хранение полной истории пользовательского блокчейна внутри Solana;
|
||||||
|
|||||||
@ -19,7 +19,7 @@
|
|||||||
5. `DAO_TREASURY_WALLET` / `dao_wallet` — казна DAO.
|
5. `DAO_TREASURY_WALLET` / `dao_wallet` — казна DAO.
|
||||||
6. `manager_wallet` — кошелек менеджера, которому DAO выдает лимиты на создание тикетов.
|
6. `manager_wallet` — кошелек менеджера, которому DAO выдает лимиты на создание тикетов.
|
||||||
7. `user root_key` — корневой ключ пользователя для подписи пользовательской записи.
|
7. `user root_key` — корневой ключ пользователя для подписи пользовательской записи.
|
||||||
8. `user device_key` — ключ устройства пользователя.
|
8. `user client_key` — ключ устройства пользователя.
|
||||||
9. `server_key` — ключ сервера пользователя, если пользователь является сервером.
|
9. `server_key` — ключ сервера пользователя, если пользователь является сервером.
|
||||||
|
|
||||||
Текущие адреса из `programs/common/src/deploy_config.rs`:
|
Текущие адреса из `programs/common/src/deploy_config.rs`:
|
||||||
|
|||||||
@ -62,7 +62,7 @@
|
|||||||
|
|
||||||
`UserMutableFields`:
|
`UserMutableFields`:
|
||||||
|
|
||||||
- `device_key: Pubkey`
|
- `client_key: Pubkey`
|
||||||
- `blockchain_public_key: Pubkey`
|
- `blockchain_public_key: Pubkey`
|
||||||
- `blockchain_name: String`
|
- `blockchain_name: String`
|
||||||
- `used_bytes: u64`
|
- `used_bytes: u64`
|
||||||
|
|||||||
@ -32,7 +32,7 @@
|
|||||||
|
|
||||||
<rect class="box actor" x="52" y="150" width="210" height="78"/>
|
<rect class="box actor" x="52" y="150" width="210" height="78"/>
|
||||||
<text class="txt" x="72" y="181">Пользователь</text>
|
<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"/>
|
<rect class="box actor" x="52" y="310" width="210" height="78"/>
|
||||||
<text class="txt" x="72" y="341">Покупатель тикета</text>
|
<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`, не из ввода |
|
| `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` |
|
| `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 сверен дважды |
|
| `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 == device_key` | user_pda `owner == program_id`; econ_config `owner == program_id` | деривация + сверка | ✓ | ed25519 + `version == old+1` + `prev_hash == hash(old)` | immutable-поля сверены с прежней записью |
|
| `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
|
### shine_payments
|
||||||
|
|
||||||
@ -111,7 +111,7 @@ commit-reveal; для текущей модели — приемлемый ри
|
|||||||
- **Подмена PDA** невозможна нигде: всюду пара «деривация `find_program_address` + сверка полного адреса». Пользовательский bump не принимается, `create_program_address` с внешним bump не используется — bump-seed атаки исключены.
|
- **Подмена 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: `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` исключает повторную инициализацию и перезапись чужого аккаунта.
|
- **Создаваемые 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-интроспекцией.
|
- **system-program** сверяется с `system_program::ID` там, где идёт создание аккаунта/перевод; **sysvar инструкций** сверяется с `sysvar::instructions::id()` перед ed25519-интроспекцией.
|
||||||
- **Аккаунт оракула**: пин адреса `PYTH_SOL_USD_ACCOUNT` + `owner == pyth_receiver` + `feed_id` + возраст (120 с) + доверительный интервал (10%).
|
- **Аккаунт оракула**: пин адреса `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 по хэшу — указать на чужую инструкцию нельзя.
|
- **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/update user_pda
|
||||||
|
|
||||||
- И обычная регистрация `create_user_pda`, и последующее `update_user_pda` оплачиваются с `deviceKey`.
|
- И обычная регистрация `create_user_pda`, и последующее `update_user_pda` оплачиваются с `clientKey`.
|
||||||
- В UI это означает, что Solana fee payer всегда берётся из `device`-ключа пользователя/сервера.
|
- В UI это означает, что Solana fee payer всегда берётся из `device`-ключа пользователя/сервера.
|
||||||
- `rootKey` нужен для подписи unsigned PDA-записи, но не оплачивает транзакцию.
|
- `rootKey` нужен для подписи unsigned PDA-записи, но не оплачивает транзакцию.
|
||||||
- Для server UI это особенно важно: перед `create` и `update` нужно пополнять именно Solana-адрес `deviceKey`.
|
- Для server UI это особенно важно: перед `create` и `update` нужно пополнять именно Solana-адрес `clientKey`.
|
||||||
|
|
||||||
## Важно
|
## Важно
|
||||||
|
|
||||||
- `init_users_economy_config` выполняется один раз на программу.
|
- `init_users_economy_config` выполняется один раз на программу.
|
||||||
Если PDA уже создан, повторный вызов вернёт ошибку "already initialized" (это нормальное поведение).
|
Если 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-классификации логина.
|
- `shine_users` внутри `create_user_pda` требует корректный адрес `shine_login_guard` для CPI-классификации логина.
|
||||||
Несовпадение адреса приведёт к ошибке регистрации.
|
Несовпадение адреса приведёт к ошибке регистрации.
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
## 1. Текущие режимы
|
## 1. Текущие режимы
|
||||||
|
|
||||||
### 1. Создание новой сессии через `deviceKey`
|
### 1. Создание новой сессии через `clientKey`
|
||||||
|
|
||||||
Поток:
|
Поток:
|
||||||
|
|
||||||
@ -12,8 +12,8 @@
|
|||||||
|
|
||||||
Смысл:
|
Смысл:
|
||||||
|
|
||||||
- новое устройство уже владеет приватным `deviceKey`;
|
- новое устройство уже владеет приватным `clientKey`;
|
||||||
- сервер проверяет подпись `deviceKey`;
|
- сервер проверяет подпись `clientKey`;
|
||||||
- создаётся обычная активная сессия пользователя;
|
- создаётся обычная активная сессия пользователя;
|
||||||
- этот поток остаётся без изменений.
|
- этот поток остаётся без изменений.
|
||||||
|
|
||||||
@ -67,7 +67,7 @@
|
|||||||
|
|
||||||
## 4. Чего сервер в этой версии не делает
|
## 4. Чего сервер в этой версии не делает
|
||||||
|
|
||||||
- не передаёт приватный `deviceKey`;
|
- не передаёт приватный `clientKey`;
|
||||||
- не расшифровывает `encryptedPayload`;
|
- не расшифровывает `encryptedPayload`;
|
||||||
- не проверяет криптографию содержимого payload;
|
- не проверяет криптографию содержимого payload;
|
||||||
- не делает клиентский UI;
|
- не делает клиентский UI;
|
||||||
|
|||||||
@ -3,11 +3,11 @@
|
|||||||
Сокращение: **SAWD-v1**.
|
Сокращение: **SAWD-v1**.
|
||||||
|
|
||||||
## Назначение
|
## Назначение
|
||||||
Из 32-байтного `deviceKey32` пользователя получить один и тот же нативный Arweave RSA-4096 JWK wallet и один и тот же Arweave address.
|
Из 32-байтного `clientKey32` пользователя получить один и тот же нативный Arweave RSA-4096 JWK wallet и один и тот же Arweave address.
|
||||||
|
|
||||||
## Вход
|
## Вход
|
||||||
- `deviceKey32`: ровно 32 байта.
|
- `clientKey32`: ровно 32 байта.
|
||||||
- Если исходный `device.key` хранится как Ed25519 PKCS8 base64, нужно извлечь последние 32 байта из PKCS8.
|
- Если исходный `client.key` хранится как Ed25519 PKCS8 base64, нужно извлечь последние 32 байта из PKCS8.
|
||||||
- Если используется Solana keypair JSON на 64 байта, используются только `bytes[0..31]`.
|
- Если используется Solana keypair JSON на 64 байта, используются только `bytes[0..31]`.
|
||||||
|
|
||||||
## Выход
|
## Выход
|
||||||
@ -46,8 +46,8 @@
|
|||||||
- `SMALL_PRIME_LIMIT = 10000`
|
- `SMALL_PRIME_LIMIT = 10000`
|
||||||
|
|
||||||
## Алгоритм
|
## Алгоритм
|
||||||
1. Проверить `deviceKey32.length == 32`.
|
1. Проверить `clientKey32.length == 32`.
|
||||||
2. `masterSeed32 = HMAC-SHA256(key = UTF8(MASTER_LABEL), message = deviceKey32)`.
|
2. `masterSeed32 = HMAC-SHA256(key = UTF8(MASTER_LABEL), message = clientKey32)`.
|
||||||
3. Реализовать `deriveBytes(label, length)`:
|
3. Реализовать `deriveBytes(label, length)`:
|
||||||
- `output = empty`
|
- `output = empty`
|
||||||
- `counter = 0`
|
- `counter = 0`
|
||||||
|
|||||||
@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
На устройстве в UI пользователь выбирает текущий активный кошелёк:
|
На устройстве в UI пользователь выбирает текущий активный кошелёк:
|
||||||
|
|
||||||
- `dev.key`
|
- `client.key`
|
||||||
- `root.key`
|
- `root.key`
|
||||||
- `custom`
|
- `custom`
|
||||||
|
|
||||||
@ -115,7 +115,7 @@ ESP32 возвращает:
|
|||||||
- `requestId` — должен совпадать с `requestId` исходного запроса.
|
- `requestId` — должен совпадать с `requestId` исходного запроса.
|
||||||
- `ok` — признак успешного результата.
|
- `ok` — признак успешного результата.
|
||||||
- `wallet.type` — тип активного кошелька:
|
- `wallet.type` — тип активного кошелька:
|
||||||
- `dev.key`
|
- `client.key`
|
||||||
- `root.key`
|
- `root.key`
|
||||||
- `custom`
|
- `custom`
|
||||||
- `wallet.publicKeyBase58` — публичный ключ активного кошелька в `Base58`.
|
- `wallet.publicKeyBase58` — публичный ключ активного кошелька в `Base58`.
|
||||||
@ -156,11 +156,11 @@ ESP32 возвращает:
|
|||||||
|
|
||||||
Расширение уже знает публичные ключи пользователя из Solana PDA. Поэтому оно может дополнительно проверить ответ 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 = root.key`, то `publicKeyBase58` должен совпасть с `rootKey`, прочитанным из PDA;
|
||||||
- если `wallet.type = custom`, такой проверки по PDA в первой версии нет.
|
- если `wallet.type = custom`, такой проверки по PDA в первой версии нет.
|
||||||
|
|
||||||
При несовпадении для `dev.key` или `root.key` расширение должно показать пользователю предупреждение, что возвращённый ключ не совпал с ожидаемым ключом из PDA.
|
При несовпадении для `client.key` или `root.key` расширение должно показать пользователю предупреждение, что возвращённый ключ не совпал с ожидаемым ключом из PDA.
|
||||||
|
|
||||||
## 8. Ожидаемое поведение UI расширения
|
## 8. Ожидаемое поведение UI расширения
|
||||||
|
|
||||||
|
|||||||
@ -9,3 +9,9 @@
|
|||||||
## Синхронизация со спецификацией
|
## Синхронизация со спецификацией
|
||||||
|
|
||||||
- При изменении экранов, кнопок, переходов, статусов или текстов обязательно обновлять соответствующую спецификацию в `ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/reference/`.
|
- При изменении экранов, кнопок, переходов, статусов или текстов обязательно обновлять соответствующую спецификацию в `ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/reference/`.
|
||||||
|
|
||||||
|
## Сборка ESP32
|
||||||
|
|
||||||
|
- Основной способ проверки и прошивки скетчей для `ESP32-S3-Touch-AMOLED-2.16` - `main-device/burn.sh`.
|
||||||
|
- Не собирать эти скетчи напрямую через `arduino-cli compile` без `burn.sh`, потому что скрипт добавляет нужные локальные библиотеки и конфиги из `official-demo/examples/Arduino-v3.3.5/libraries`.
|
||||||
|
- Если сборка падает по `lv_conf.h` или `TouchDrvCSTXXX.hpp`, сначала проверять именно `burn.sh` и его `--library` пути, а не считать, что файл пропал из репозитория.
|
||||||
|
|||||||
@ -11,7 +11,7 @@
|
|||||||
* legacy(empty password):
|
* legacy(empty password):
|
||||||
* secret = SHA256(base64(SHA256(password)) + "master.secret")
|
* secret = SHA256(base64(SHA256(password)) + "master.secret")
|
||||||
* keyPair_i = Ed25519(SHA256(base64(secret) + "|" + suffix_i))
|
* keyPair_i = Ed25519(SHA256(base64(secret) + "|" + suffix_i))
|
||||||
* suffixes = ["root.key", "bch.key", "dev.key"]
|
* suffixes = ["root.key", "blockchain.key", "client.key"]
|
||||||
*
|
*
|
||||||
* Плата: Waveshare ESP32-S3-Touch-AMOLED-2.16
|
* Плата: Waveshare ESP32-S3-Touch-AMOLED-2.16
|
||||||
* SD : SDMMC 1-bit CLK=GPIO2, CMD=GPIO1, D0=GPIO3
|
* SD : SDMMC 1-bit CLK=GPIO2, CMD=GPIO1, D0=GPIO3
|
||||||
@ -116,8 +116,8 @@ static bool gKbNums = false;
|
|||||||
// ═══════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════
|
||||||
struct KeyPair { uint8_t pub[32]; uint8_t priv[32]; };
|
struct KeyPair { uint8_t pub[32]; uint8_t priv[32]; };
|
||||||
static KeyPair gKeys[3];
|
static KeyPair gKeys[3];
|
||||||
static const char * KEY_SUFFIXES[3] = {"root.key", "bch.key", "dev.key"};
|
static const char * KEY_SUFFIXES[3] = {"root.key", "blockchain.key", "client.key"};
|
||||||
static const char * KEY_LABELS[3] = {"root.key", "bch.key", "dev.key"};
|
static const char * KEY_LABELS[3] = {"root.key", "blockchain.key", "client.key"};
|
||||||
static uint32_t gElapsedSec = 0;
|
static uint32_t gElapsedSec = 0;
|
||||||
|
|
||||||
// Base58 представления (43-44 символа для 32 байт + \0)
|
// Base58 представления (43-44 символа для 32 байт + \0)
|
||||||
|
|||||||
@ -81,7 +81,7 @@ static const char *kPaymentsInflowSeed = "shine_payments_inflow_vault";
|
|||||||
static const char *kLastBlockPrefix = "SHiNE_LAST_BLOCK";
|
static const char *kLastBlockPrefix = "SHiNE_LAST_BLOCK";
|
||||||
static const char *kProgramDerivedAddressMarker = "ProgramDerivedAddress";
|
static const char *kProgramDerivedAddressMarker = "ProgramDerivedAddress";
|
||||||
static const uint8_t kBlockTypeRootKey = 1;
|
static const uint8_t kBlockTypeRootKey = 1;
|
||||||
static const uint8_t kBlockTypeDeviceKey = 2;
|
static const uint8_t kBlockTypeClientKey = 2;
|
||||||
static const uint8_t kBlockTypeBlockchainRegistry = 3;
|
static const uint8_t kBlockTypeBlockchainRegistry = 3;
|
||||||
static const uint8_t kBlockTypeServerProfile = 30;
|
static const uint8_t kBlockTypeServerProfile = 30;
|
||||||
static const uint8_t kBlockTypeAccessServers = 40;
|
static const uint8_t kBlockTypeAccessServers = 40;
|
||||||
@ -230,7 +230,7 @@ struct ShinePdaUserState {
|
|||||||
uint8_t sessionsMode = 1;
|
uint8_t sessionsMode = 1;
|
||||||
uint8_t trustedCount = 0;
|
uint8_t trustedCount = 0;
|
||||||
uint8_t rootKey32[32] = {};
|
uint8_t rootKey32[32] = {};
|
||||||
uint8_t deviceKey32[32] = {};
|
uint8_t clientKey32[32] = {};
|
||||||
uint8_t blockchainKey32[32] = {};
|
uint8_t blockchainKey32[32] = {};
|
||||||
String blockchainName;
|
String blockchainName;
|
||||||
uint64_t paidLimitBytes = 0;
|
uint64_t paidLimitBytes = 0;
|
||||||
@ -542,7 +542,7 @@ static std::vector<uint8_t> buildUnsignedCreateRecord(
|
|||||||
const String &blockchainName,
|
const String &blockchainName,
|
||||||
const String &serverAddress,
|
const String &serverAddress,
|
||||||
const uint8_t rootPub[32],
|
const uint8_t rootPub[32],
|
||||||
const uint8_t devicePub[32],
|
const uint8_t clientPub[32],
|
||||||
const uint8_t blockchainPub[32],
|
const uint8_t blockchainPub[32],
|
||||||
const uint8_t lastBlockSignature[64],
|
const uint8_t lastBlockSignature[64],
|
||||||
uint64_t paidLimitBytes,
|
uint64_t paidLimitBytes,
|
||||||
@ -552,7 +552,7 @@ static std::vector<uint8_t> buildCreateInstructionData(
|
|||||||
const String &blockchainName,
|
const String &blockchainName,
|
||||||
const String &serverAddress,
|
const String &serverAddress,
|
||||||
const uint8_t rootPub[32],
|
const uint8_t rootPub[32],
|
||||||
const uint8_t devicePub[32],
|
const uint8_t clientPub[32],
|
||||||
const uint8_t blockchainPub[32],
|
const uint8_t blockchainPub[32],
|
||||||
const uint8_t lastBlockSignature[64],
|
const uint8_t lastBlockSignature[64],
|
||||||
const uint8_t rootSignature[64],
|
const uint8_t rootSignature[64],
|
||||||
@ -566,7 +566,7 @@ static std::vector<uint8_t> buildUpdateInstructionData(const ShinePdaUserState &
|
|||||||
const uint8_t rootSignature64[64]);
|
const uint8_t rootSignature64[64]);
|
||||||
static std::vector<uint8_t> buildLegacyMessage(
|
static std::vector<uint8_t> buildLegacyMessage(
|
||||||
const uint8_t recentBlockhash[32],
|
const uint8_t recentBlockhash[32],
|
||||||
const uint8_t devicePub[32],
|
const uint8_t clientPub[32],
|
||||||
const uint8_t userPda[32],
|
const uint8_t userPda[32],
|
||||||
const uint8_t inflowVault[32],
|
const uint8_t inflowVault[32],
|
||||||
const uint8_t economyConfig[32],
|
const uint8_t economyConfig[32],
|
||||||
@ -575,7 +575,7 @@ static std::vector<uint8_t> buildLegacyMessage(
|
|||||||
const std::vector<uint8_t> &createData);
|
const std::vector<uint8_t> &createData);
|
||||||
static std::vector<uint8_t> buildUpdateLegacyMessage(
|
static std::vector<uint8_t> buildUpdateLegacyMessage(
|
||||||
const uint8_t recentBlockhash[32],
|
const uint8_t recentBlockhash[32],
|
||||||
const uint8_t devicePub[32],
|
const uint8_t clientPub[32],
|
||||||
const uint8_t userPda[32],
|
const uint8_t userPda[32],
|
||||||
const uint8_t inflowVault[32],
|
const uint8_t inflowVault[32],
|
||||||
const uint8_t economyConfig[32],
|
const uint8_t economyConfig[32],
|
||||||
@ -1234,8 +1234,8 @@ static void refreshDerivedKeys() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
deriveKeyPairFromSecretSuffix(gSecretBytes, "root.key", gRootPubB58, gRootPrivB58);
|
deriveKeyPairFromSecretSuffix(gSecretBytes, "root.key", gRootPubB58, gRootPrivB58);
|
||||||
deriveKeyPairFromSecretSuffix(gSecretBytes, "bch.key", gBlockchainPubB58, gBlockchainPrivB58);
|
deriveKeyPairFromSecretSuffix(gSecretBytes, "blockchain.key", gBlockchainPubB58, gBlockchainPrivB58);
|
||||||
deriveKeyPairFromSecretSuffix(gSecretBytes, "dev.key", gDevicePubB58, gDevicePrivB58);
|
deriveKeyPairFromSecretSuffix(gSecretBytes, "client.key", gDevicePubB58, gDevicePrivB58);
|
||||||
deriveKeyPairFromSecretSuffix(gSecretBytes, homeserverKeySuffix(), gHomeserverPubB58, gHomeserverPrivB58);
|
deriveKeyPairFromSecretSuffix(gSecretBytes, homeserverKeySuffix(), gHomeserverPubB58, gHomeserverPrivB58);
|
||||||
String customName = gCustomWalletName;
|
String customName = gCustomWalletName;
|
||||||
customName.trim();
|
customName.trim();
|
||||||
@ -1255,7 +1255,7 @@ static String selectedWalletDisplayName() {
|
|||||||
}
|
}
|
||||||
case WALLET_SELECTION_DEVICE:
|
case WALLET_SELECTION_DEVICE:
|
||||||
default:
|
default:
|
||||||
return "DeviceKey";
|
return "ClientKey";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1267,7 +1267,7 @@ static String selectedWalletTypeCode() {
|
|||||||
return "custom";
|
return "custom";
|
||||||
case WALLET_SELECTION_DEVICE:
|
case WALLET_SELECTION_DEVICE:
|
||||||
default:
|
default:
|
||||||
return "dev.key";
|
return "client.key";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1282,7 +1282,7 @@ static String selectedWalletDerivationSuffix() {
|
|||||||
}
|
}
|
||||||
case WALLET_SELECTION_DEVICE:
|
case WALLET_SELECTION_DEVICE:
|
||||||
default:
|
default:
|
||||||
return "dev.key";
|
return "client.key";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1759,9 +1759,9 @@ static std::vector<uint8_t> serializeUnsignedRecordState(const ShinePdaUserState
|
|||||||
out.push_back(0);
|
out.push_back(0);
|
||||||
pushFixed(out, state.rootKey32, 32);
|
pushFixed(out, state.rootKey32, 32);
|
||||||
|
|
||||||
out.push_back(kBlockTypeDeviceKey);
|
out.push_back(kBlockTypeClientKey);
|
||||||
out.push_back(0);
|
out.push_back(0);
|
||||||
pushFixed(out, state.deviceKey32, 32);
|
pushFixed(out, state.clientKey32, 32);
|
||||||
|
|
||||||
out.push_back(kBlockTypeBlockchainRegistry);
|
out.push_back(kBlockTypeBlockchainRegistry);
|
||||||
out.push_back(0);
|
out.push_back(0);
|
||||||
@ -1837,7 +1837,7 @@ static std::vector<uint8_t> buildUpdateInstructionData(const ShinePdaUserState &
|
|||||||
pushU32LE(out, nextVersion);
|
pushU32LE(out, nextVersion);
|
||||||
pushFixed(out, prevHash32, 32);
|
pushFixed(out, prevHash32, 32);
|
||||||
pushU64LE(out, 0);
|
pushU64LE(out, 0);
|
||||||
pushFixed(out, state.deviceKey32, 32);
|
pushFixed(out, state.clientKey32, 32);
|
||||||
pushFixed(out, state.blockchainKey32, 32);
|
pushFixed(out, state.blockchainKey32, 32);
|
||||||
pushStrU8(out, state.blockchainName);
|
pushStrU8(out, state.blockchainName);
|
||||||
pushU64LE(out, state.usedBytes);
|
pushU64LE(out, state.usedBytes);
|
||||||
@ -1882,7 +1882,7 @@ static std::vector<uint8_t> buildUnsignedCreateRecord(
|
|||||||
const String &blockchainName,
|
const String &blockchainName,
|
||||||
const String &serverAddress,
|
const String &serverAddress,
|
||||||
const uint8_t rootPub[32],
|
const uint8_t rootPub[32],
|
||||||
const uint8_t devicePub[32],
|
const uint8_t clientPub[32],
|
||||||
const uint8_t blockchainPub[32],
|
const uint8_t blockchainPub[32],
|
||||||
const uint8_t lastBlockSignature[64],
|
const uint8_t lastBlockSignature[64],
|
||||||
uint64_t paidLimitBytes,
|
uint64_t paidLimitBytes,
|
||||||
@ -1905,9 +1905,9 @@ static std::vector<uint8_t> buildUnsignedCreateRecord(
|
|||||||
out.push_back(0);
|
out.push_back(0);
|
||||||
pushFixed(out, rootPub, 32);
|
pushFixed(out, rootPub, 32);
|
||||||
|
|
||||||
out.push_back(kBlockTypeDeviceKey);
|
out.push_back(kBlockTypeClientKey);
|
||||||
out.push_back(0);
|
out.push_back(0);
|
||||||
pushFixed(out, devicePub, 32);
|
pushFixed(out, clientPub, 32);
|
||||||
|
|
||||||
out.push_back(kBlockTypeBlockchainRegistry);
|
out.push_back(kBlockTypeBlockchainRegistry);
|
||||||
out.push_back(0);
|
out.push_back(0);
|
||||||
@ -1954,7 +1954,7 @@ static std::vector<uint8_t> buildCreateInstructionData(
|
|||||||
const String &blockchainName,
|
const String &blockchainName,
|
||||||
const String &serverAddress,
|
const String &serverAddress,
|
||||||
const uint8_t rootPub[32],
|
const uint8_t rootPub[32],
|
||||||
const uint8_t devicePub[32],
|
const uint8_t clientPub[32],
|
||||||
const uint8_t blockchainPub[32],
|
const uint8_t blockchainPub[32],
|
||||||
const uint8_t lastBlockSignature[64],
|
const uint8_t lastBlockSignature[64],
|
||||||
const uint8_t rootSignature[64],
|
const uint8_t rootSignature[64],
|
||||||
@ -1966,7 +1966,7 @@ static std::vector<uint8_t> buildCreateInstructionData(
|
|||||||
pushFixed(out, rootPub, 32);
|
pushFixed(out, rootPub, 32);
|
||||||
pushU64LE(out, createdAtMs);
|
pushU64LE(out, createdAtMs);
|
||||||
pushU64LE(out, 0);
|
pushU64LE(out, 0);
|
||||||
pushFixed(out, devicePub, 32);
|
pushFixed(out, clientPub, 32);
|
||||||
pushFixed(out, blockchainPub, 32);
|
pushFixed(out, blockchainPub, 32);
|
||||||
pushStrU8(out, blockchainName);
|
pushStrU8(out, blockchainName);
|
||||||
pushU64LE(out, 0);
|
pushU64LE(out, 0);
|
||||||
@ -2215,7 +2215,7 @@ static bool simulateTransactionForError(const String &txBase64, String &messageO
|
|||||||
|
|
||||||
static std::vector<uint8_t> buildLegacyMessage(
|
static std::vector<uint8_t> buildLegacyMessage(
|
||||||
const uint8_t recentBlockhash[32],
|
const uint8_t recentBlockhash[32],
|
||||||
const uint8_t devicePub[32],
|
const uint8_t clientPub[32],
|
||||||
const uint8_t userPda[32],
|
const uint8_t userPda[32],
|
||||||
const uint8_t inflowVault[32],
|
const uint8_t inflowVault[32],
|
||||||
const uint8_t economyConfig[32],
|
const uint8_t economyConfig[32],
|
||||||
@ -2234,7 +2234,7 @@ static std::vector<uint8_t> buildLegacyMessage(
|
|||||||
base58ToFixed32(kShineLoginGuardProgramId, loginGuardProgram);
|
base58ToFixed32(kShineLoginGuardProgramId, loginGuardProgram);
|
||||||
|
|
||||||
std::vector<std::vector<uint8_t>> accountKeys;
|
std::vector<std::vector<uint8_t>> accountKeys;
|
||||||
accountKeys.emplace_back(devicePub, devicePub + 32);
|
accountKeys.emplace_back(clientPub, clientPub + 32);
|
||||||
accountKeys.emplace_back(userPda, userPda + 32);
|
accountKeys.emplace_back(userPda, userPda + 32);
|
||||||
accountKeys.emplace_back(inflowVault, inflowVault + 32);
|
accountKeys.emplace_back(inflowVault, inflowVault + 32);
|
||||||
accountKeys.emplace_back(systemProgram, systemProgram + 32);
|
accountKeys.emplace_back(systemProgram, systemProgram + 32);
|
||||||
@ -2282,7 +2282,7 @@ static std::vector<uint8_t> buildLegacyMessage(
|
|||||||
|
|
||||||
static std::vector<uint8_t> buildUpdateLegacyMessage(
|
static std::vector<uint8_t> buildUpdateLegacyMessage(
|
||||||
const uint8_t recentBlockhash[32],
|
const uint8_t recentBlockhash[32],
|
||||||
const uint8_t devicePub[32],
|
const uint8_t clientPub[32],
|
||||||
const uint8_t userPda[32],
|
const uint8_t userPda[32],
|
||||||
const uint8_t inflowVault[32],
|
const uint8_t inflowVault[32],
|
||||||
const uint8_t economyConfig[32],
|
const uint8_t economyConfig[32],
|
||||||
@ -2299,7 +2299,7 @@ static std::vector<uint8_t> buildUpdateLegacyMessage(
|
|||||||
base58ToFixed32(kSysvarInstructionsId, sysvarInstructions);
|
base58ToFixed32(kSysvarInstructionsId, sysvarInstructions);
|
||||||
|
|
||||||
std::vector<std::vector<uint8_t>> accountKeys;
|
std::vector<std::vector<uint8_t>> accountKeys;
|
||||||
accountKeys.emplace_back(devicePub, devicePub + 32);
|
accountKeys.emplace_back(clientPub, clientPub + 32);
|
||||||
accountKeys.emplace_back(userPda, userPda + 32);
|
accountKeys.emplace_back(userPda, userPda + 32);
|
||||||
accountKeys.emplace_back(inflowVault, inflowVault + 32);
|
accountKeys.emplace_back(inflowVault, inflowVault + 32);
|
||||||
accountKeys.emplace_back(systemProgram, systemProgram + 32);
|
accountKeys.emplace_back(systemProgram, systemProgram + 32);
|
||||||
@ -2708,17 +2708,17 @@ static bool registerHomeserverOnSolana(String &messageOut) {
|
|||||||
uint8_t blockchainSeed[32] = {};
|
uint8_t blockchainSeed[32] = {};
|
||||||
uint8_t blockchainPub[32] = {};
|
uint8_t blockchainPub[32] = {};
|
||||||
uint8_t blockchainSec[64] = {};
|
uint8_t blockchainSec[64] = {};
|
||||||
uint8_t deviceSeed[32] = {};
|
uint8_t clientSeed[32] = {};
|
||||||
uint8_t devicePub[32] = {};
|
uint8_t clientPub[32] = {};
|
||||||
uint8_t deviceSec[64] = {};
|
uint8_t deviceSec[64] = {};
|
||||||
if (!deriveSeedKeypairFromBase58(gRootPrivB58, rootSeed, rootPub, rootSec) ||
|
if (!deriveSeedKeypairFromBase58(gRootPrivB58, rootSeed, rootPub, rootSec) ||
|
||||||
!deriveSeedKeypairFromBase58(gBlockchainPrivB58, blockchainSeed, blockchainPub, blockchainSec) ||
|
!deriveSeedKeypairFromBase58(gBlockchainPrivB58, blockchainSeed, blockchainPub, blockchainSec) ||
|
||||||
!deriveSeedKeypairFromBase58(gDevicePrivB58, deviceSeed, devicePub, deviceSec)) {
|
!deriveSeedKeypairFromBase58(gDevicePrivB58, clientSeed, clientPub, deviceSec)) {
|
||||||
return failWithDiag("Failed to restore keys");
|
return failWithDiag("Failed to restore keys");
|
||||||
}
|
}
|
||||||
diagDetails += String("root_pub=") + bytesToBase58(rootPub, 32) + "\n";
|
diagDetails += String("root_pub=") + bytesToBase58(rootPub, 32) + "\n";
|
||||||
diagDetails += String("blockchain_pub=") + bytesToBase58(blockchainPub, 32) + "\n";
|
diagDetails += String("blockchain_pub=") + bytesToBase58(blockchainPub, 32) + "\n";
|
||||||
diagDetails += String("device_pub=") + bytesToBase58(devicePub, 32) + "\n";
|
diagDetails += String("device_pub=") + bytesToBase58(clientPub, 32) + "\n";
|
||||||
|
|
||||||
String blockchainName = cleanLogin + "-001";
|
String blockchainName = cleanLogin + "-001";
|
||||||
diagDetails += String("blockchain_name=") + blockchainName + "\n";
|
diagDetails += String("blockchain_name=") + blockchainName + "\n";
|
||||||
@ -2736,7 +2736,7 @@ static bool registerHomeserverOnSolana(String &messageOut) {
|
|||||||
diagDetails += String("created_at_ms=") + String((unsigned long long)createdAtMs) + "\n";
|
diagDetails += String("created_at_ms=") + String((unsigned long long)createdAtMs) + "\n";
|
||||||
std::vector<uint8_t> unsignedRecord = buildUnsignedCreateRecord(
|
std::vector<uint8_t> unsignedRecord = buildUnsignedCreateRecord(
|
||||||
cleanLogin, blockchainName, gShineServerUrl,
|
cleanLogin, blockchainName, gShineServerUrl,
|
||||||
rootPub, devicePub, blockchainPub,
|
rootPub, clientPub, blockchainPub,
|
||||||
lastBlockSignature, startBonusLimit, createdAtMs);
|
lastBlockSignature, startBonusLimit, createdAtMs);
|
||||||
uint8_t unsignedHash[32];
|
uint8_t unsignedHash[32];
|
||||||
uint8_t rootSignature[64];
|
uint8_t rootSignature[64];
|
||||||
@ -2750,7 +2750,7 @@ static bool registerHomeserverOnSolana(String &messageOut) {
|
|||||||
|
|
||||||
std::vector<uint8_t> createData = buildCreateInstructionData(
|
std::vector<uint8_t> createData = buildCreateInstructionData(
|
||||||
cleanLogin, blockchainName, gShineServerUrl,
|
cleanLogin, blockchainName, gShineServerUrl,
|
||||||
rootPub, devicePub, blockchainPub,
|
rootPub, clientPub, blockchainPub,
|
||||||
lastBlockSignature, rootSignature, createdAtMs);
|
lastBlockSignature, rootSignature, createdAtMs);
|
||||||
std::vector<uint8_t> edRootData = buildEd25519InstructionData(rootSignature, rootPub, unsignedHash);
|
std::vector<uint8_t> edRootData = buildEd25519InstructionData(rootSignature, rootPub, unsignedHash);
|
||||||
std::vector<uint8_t> edBchData = buildEd25519InstructionData(lastBlockSignature, blockchainPub, lastBlockHash);
|
std::vector<uint8_t> edBchData = buildEd25519InstructionData(lastBlockSignature, blockchainPub, lastBlockHash);
|
||||||
@ -2765,7 +2765,7 @@ static bool registerHomeserverOnSolana(String &messageOut) {
|
|||||||
|
|
||||||
std::vector<uint8_t> message = buildLegacyMessage(
|
std::vector<uint8_t> message = buildLegacyMessage(
|
||||||
recentBlockhash,
|
recentBlockhash,
|
||||||
devicePub,
|
clientPub,
|
||||||
userPda,
|
userPda,
|
||||||
inflowVault,
|
inflowVault,
|
||||||
economyConfig,
|
economyConfig,
|
||||||
@ -3012,17 +3012,17 @@ static bool updateHomeserverSessionOnSolana(bool requireExisting, String &messag
|
|||||||
uint8_t rootSeed[32] = {};
|
uint8_t rootSeed[32] = {};
|
||||||
uint8_t rootPub[32] = {};
|
uint8_t rootPub[32] = {};
|
||||||
uint8_t rootSec[64] = {};
|
uint8_t rootSec[64] = {};
|
||||||
uint8_t deviceSeed[32] = {};
|
uint8_t clientSeed[32] = {};
|
||||||
uint8_t devicePub[32] = {};
|
uint8_t clientPub[32] = {};
|
||||||
uint8_t deviceSec[64] = {};
|
uint8_t deviceSec[64] = {};
|
||||||
uint8_t homeserverPub[32] = {};
|
uint8_t homeserverPub[32] = {};
|
||||||
if (!deriveSeedKeypairFromBase58(gRootPrivB58, rootSeed, rootPub, rootSec)
|
if (!deriveSeedKeypairFromBase58(gRootPrivB58, rootSeed, rootPub, rootSec)
|
||||||
|| !deriveSeedKeypairFromBase58(gDevicePrivB58, deviceSeed, devicePub, deviceSec)
|
|| !deriveSeedKeypairFromBase58(gDevicePrivB58, clientSeed, clientPub, deviceSec)
|
||||||
|| !base58ToFixed32(gHomeserverPubB58, homeserverPub)) {
|
|| !base58ToFixed32(gHomeserverPubB58, homeserverPub)) {
|
||||||
return failWithDiag("Failed to restore local keys");
|
return failWithDiag("Failed to restore local keys");
|
||||||
}
|
}
|
||||||
if (memcmp(devicePub, currentState.deviceKey32, 32) != 0) {
|
if (memcmp(clientPub, currentState.clientKey32, 32) != 0) {
|
||||||
return failWithDiag("Device key does not match PDA");
|
return failWithDiag("Client key does not match PDA");
|
||||||
}
|
}
|
||||||
|
|
||||||
uint8_t userPda[32] = {};
|
uint8_t userPda[32] = {};
|
||||||
@ -3048,7 +3048,7 @@ static bool updateHomeserverSessionOnSolana(bool requireExisting, String &messag
|
|||||||
|
|
||||||
ShinePdaUserState nextState = currentState;
|
ShinePdaUserState nextState = currentState;
|
||||||
memcpy(nextState.rootKey32, rootPub, 32);
|
memcpy(nextState.rootKey32, rootPub, 32);
|
||||||
memcpy(nextState.deviceKey32, devicePub, 32);
|
memcpy(nextState.clientKey32, clientPub, 32);
|
||||||
nextState.updatedAtMs = shineNowMs();
|
nextState.updatedAtMs = shineNowMs();
|
||||||
nextState.recordNumber = currentState.recordNumber + 1;
|
nextState.recordNumber = currentState.recordNumber + 1;
|
||||||
if (nextState.sessionsMode == 0) {
|
if (nextState.sessionsMode == 0) {
|
||||||
@ -3121,7 +3121,7 @@ static bool updateHomeserverSessionOnSolana(bool requireExisting, String &messag
|
|||||||
|
|
||||||
std::vector<uint8_t> message = buildUpdateLegacyMessage(
|
std::vector<uint8_t> message = buildUpdateLegacyMessage(
|
||||||
recentBlockhash,
|
recentBlockhash,
|
||||||
devicePub,
|
clientPub,
|
||||||
userPda,
|
userPda,
|
||||||
inflowVault,
|
inflowVault,
|
||||||
economyConfig,
|
economyConfig,
|
||||||
@ -3289,9 +3289,9 @@ static bool parseShineUserPdaBytes(const std::vector<uint8_t> &bytes, ShinePdaUs
|
|||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (blockType == kBlockTypeDeviceKey) {
|
if (blockType == kBlockTypeClientKey) {
|
||||||
if (!readBytes(outState.deviceKey32, 32)) {
|
if (!readBytes(outState.clientKey32, 32)) {
|
||||||
errorOut = "Bad device key block";
|
errorOut = "Bad client key block";
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
@ -3520,10 +3520,10 @@ static void refreshAccountPdaStatus() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
uint8_t rootPub[32] = {};
|
uint8_t rootPub[32] = {};
|
||||||
uint8_t devicePub[32] = {};
|
uint8_t clientPub[32] = {};
|
||||||
uint8_t blockchainPub[32] = {};
|
uint8_t blockchainPub[32] = {};
|
||||||
if (!base58ToFixed32(gRootPubB58, rootPub)
|
if (!base58ToFixed32(gRootPubB58, rootPub)
|
||||||
|| !base58ToFixed32(gDevicePubB58, devicePub)
|
|| !base58ToFixed32(gDevicePubB58, clientPub)
|
||||||
|| !base58ToFixed32(gBlockchainPubB58, blockchainPub)) {
|
|| !base58ToFixed32(gBlockchainPubB58, blockchainPub)) {
|
||||||
gAccountPdaStatus = ACCOUNT_PDA_MISMATCH;
|
gAccountPdaStatus = ACCOUNT_PDA_MISMATCH;
|
||||||
gAccountPdaStatusMessage = "local keys invalid";
|
gAccountPdaStatusMessage = "local keys invalid";
|
||||||
@ -3537,8 +3537,8 @@ static void refreshAccountPdaStatus() {
|
|||||||
mismatch = "root key mismatch";
|
mismatch = "root key mismatch";
|
||||||
} else if (memcmp(blockchainPub, pdaState.blockchainKey32, 32) != 0) {
|
} else if (memcmp(blockchainPub, pdaState.blockchainKey32, 32) != 0) {
|
||||||
mismatch = "blockchain key mismatch";
|
mismatch = "blockchain key mismatch";
|
||||||
} else if (memcmp(devicePub, pdaState.deviceKey32, 32) != 0) {
|
} else if (memcmp(clientPub, pdaState.clientKey32, 32) != 0) {
|
||||||
mismatch = "device key mismatch";
|
mismatch = "client key mismatch";
|
||||||
} else if (gHomeserverValue.isEmpty()) {
|
} else if (gHomeserverValue.isEmpty()) {
|
||||||
mismatch = "homeserver not set";
|
mismatch = "homeserver not set";
|
||||||
} else {
|
} else {
|
||||||
@ -3963,20 +3963,20 @@ static bool buildPkcs8FromSeed32(const uint8_t seed32[32], String &pkcs8B64Out)
|
|||||||
static bool buildPairingSecretsPayload(const PairingRequestUiItem &item, String &payloadJsonOut, String &errorOut) {
|
static bool buildPairingSecretsPayload(const PairingRequestUiItem &item, String &payloadJsonOut, String &errorOut) {
|
||||||
payloadJsonOut = "";
|
payloadJsonOut = "";
|
||||||
errorOut = "";
|
errorOut = "";
|
||||||
uint8_t deviceSeed[32] = {};
|
uint8_t clientSeed[32] = {};
|
||||||
uint8_t devicePub[32] = {};
|
uint8_t clientPub[32] = {};
|
||||||
uint8_t deviceSec[64] = {};
|
uint8_t deviceSec[64] = {};
|
||||||
if (!deriveSeedKeypairFromBase58(gDevicePrivB58, deviceSeed, devicePub, deviceSec)) {
|
if (!deriveSeedKeypairFromBase58(gDevicePrivB58, clientSeed, clientPub, deviceSec)) {
|
||||||
errorOut = "Failed to derive device key";
|
errorOut = "Failed to derive client key";
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
String devicePkcs8;
|
String devicePkcs8;
|
||||||
if (!buildPkcs8FromSeed32(deviceSeed, devicePkcs8)) {
|
if (!buildPkcs8FromSeed32(clientSeed, devicePkcs8)) {
|
||||||
errorOut = "Failed to encode device key";
|
errorOut = "Failed to encode client key";
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
payloadJsonOut = String("{\"v\":1,\"type\":\"shine-esp-pairing-transfer\",\"login\":\"") + jsonEscape(gLoginValue)
|
payloadJsonOut = String("{\"v\":1,\"type\":\"shine-esp-pairing-transfer\",\"login\":\"") + jsonEscape(gLoginValue)
|
||||||
+ "\",\"mode\":\"device-only\",\"keys\":{\"deviceKey\":\"" + jsonEscape(devicePkcs8)
|
+ "\",\"mode\":\"device-only\",\"keys\":{\"clientKey\":\"" + jsonEscape(devicePkcs8)
|
||||||
+ "\",\"blockchainKey\":\"\",\"rootKey\":\"\"},\"payloadType\":1,\"createdAtMs\":"
|
+ "\",\"blockchainKey\":\"\",\"rootKey\":\"\"},\"payloadType\":1,\"createdAtMs\":"
|
||||||
+ String((unsigned long long)shineNowMs()) + "}";
|
+ String((unsigned long long)shineNowMs()) + "}";
|
||||||
return true;
|
return true;
|
||||||
@ -3985,11 +3985,11 @@ static bool buildPairingSecretsPayload(const PairingRequestUiItem &item, String
|
|||||||
static bool createDelegatedWalletSessionPayload(const PairingRequestUiItem &item, String &payloadJsonOut, String &errorOut) {
|
static bool createDelegatedWalletSessionPayload(const PairingRequestUiItem &item, String &payloadJsonOut, String &errorOut) {
|
||||||
payloadJsonOut = "";
|
payloadJsonOut = "";
|
||||||
errorOut = "";
|
errorOut = "";
|
||||||
uint8_t deviceSeed[32] = {};
|
uint8_t clientSeed[32] = {};
|
||||||
uint8_t devicePub[32] = {};
|
uint8_t clientPub[32] = {};
|
||||||
uint8_t deviceSec[64] = {};
|
uint8_t deviceSec[64] = {};
|
||||||
if (!deriveSeedKeypairFromBase58(gDevicePrivB58, deviceSeed, devicePub, deviceSec)) {
|
if (!deriveSeedKeypairFromBase58(gDevicePrivB58, clientSeed, clientPub, deviceSec)) {
|
||||||
errorOut = "Failed to derive device key";
|
errorOut = "Failed to derive client key";
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -4040,7 +4040,7 @@ static bool createDelegatedWalletSessionPayload(const PairingRequestUiItem &item
|
|||||||
+ "\",\"storagePwd\":\"" + jsonEscape(storagePwd)
|
+ "\",\"storagePwd\":\"" + jsonEscape(storagePwd)
|
||||||
+ "\",\"timeMs\":" + String((unsigned long long)timeMs)
|
+ "\",\"timeMs\":" + String((unsigned long long)timeMs)
|
||||||
+ ",\"authNonce\":\"" + jsonEscape(authNonce)
|
+ ",\"authNonce\":\"" + jsonEscape(authNonce)
|
||||||
+ "\",\"deviceKey\":\"" + jsonEscape(bytesToBase64String(devicePub, 32))
|
+ "\",\"clientKey\":\"" + jsonEscape(bytesToBase64String(clientPub, 32))
|
||||||
+ "\",\"signatureB64\":\"" + jsonEscape(bytesToBase64String(signature, 64))
|
+ "\",\"signatureB64\":\"" + jsonEscape(bytesToBase64String(signature, 64))
|
||||||
+ "\",\"sessionType\":50"
|
+ "\",\"sessionType\":50"
|
||||||
+ ",\"clientPlatform\":\"" + jsonEscape(clientPlatform)
|
+ ",\"clientPlatform\":\"" + jsonEscape(clientPlatform)
|
||||||
@ -4262,13 +4262,13 @@ static bool ensureShineSessionAuthenticated(String &errorOut) {
|
|||||||
diagDetails += String("server_time_offset_ms=") + String((long long)gShineServerTimeOffsetMs) + "\n";
|
diagDetails += String("server_time_offset_ms=") + String((long long)gShineServerTimeOffsetMs) + "\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
uint8_t deviceSeed[32] = {};
|
uint8_t clientSeed[32] = {};
|
||||||
uint8_t devicePub[32] = {};
|
uint8_t clientPub[32] = {};
|
||||||
uint8_t deviceSec[64] = {};
|
uint8_t deviceSec[64] = {};
|
||||||
uint8_t subSeed[32] = {};
|
uint8_t subSeed[32] = {};
|
||||||
uint8_t subPub[32] = {};
|
uint8_t subPub[32] = {};
|
||||||
uint8_t subSec[64] = {};
|
uint8_t subSec[64] = {};
|
||||||
if (!deriveSeedKeypairFromBase58(gDevicePrivB58, deviceSeed, devicePub, deviceSec)
|
if (!deriveSeedKeypairFromBase58(gDevicePrivB58, clientSeed, clientPub, deviceSec)
|
||||||
|| !deriveSeedKeypairFromBase58(gHomeserverPrivB58, subSeed, subPub, subSec)) {
|
|| !deriveSeedKeypairFromBase58(gHomeserverPrivB58, subSeed, subPub, subSec)) {
|
||||||
return failWithDiag("local key derive failed");
|
return failWithDiag("local key derive failed");
|
||||||
}
|
}
|
||||||
@ -4368,7 +4368,7 @@ static bool ensureShineSessionAuthenticated(String &errorOut) {
|
|||||||
+ "\",\"storagePwd\":\"" + jsonEscape(gShineStoragePwd)
|
+ "\",\"storagePwd\":\"" + jsonEscape(gShineStoragePwd)
|
||||||
+ "\",\"timeMs\":" + String((unsigned long long)timeMs)
|
+ "\",\"timeMs\":" + String((unsigned long long)timeMs)
|
||||||
+ ",\"authNonce\":\"" + jsonEscape(authNonce)
|
+ ",\"authNonce\":\"" + jsonEscape(authNonce)
|
||||||
+ "\",\"deviceKey\":\"" + jsonEscape(bytesToBase64String(devicePub, 32))
|
+ "\",\"clientKey\":\"" + jsonEscape(bytesToBase64String(clientPub, 32))
|
||||||
+ "\",\"signatureB64\":\"" + jsonEscape(bytesToBase64String(signature, 64))
|
+ "\",\"signatureB64\":\"" + jsonEscape(bytesToBase64String(signature, 64))
|
||||||
+ "\",\"sessionType\":" + String((unsigned int)kSessionTypeHomeserver)
|
+ "\",\"sessionType\":" + String((unsigned int)kSessionTypeHomeserver)
|
||||||
+ ",\"clientPlatform\":\"" + jsonEscape(kSessionClientPlatformEsp32)
|
+ ",\"clientPlatform\":\"" + jsonEscape(kSessionClientPlatformEsp32)
|
||||||
@ -4510,7 +4510,7 @@ static void loadPrefs() {
|
|||||||
gShineServerUrl = gPrefs.getString("shine_server", "https://shineup.me");
|
gShineServerUrl = gPrefs.getString("shine_server", "https://shineup.me");
|
||||||
gLoginValue = gPrefs.getString("login", "");
|
gLoginValue = gPrefs.getString("login", "");
|
||||||
gHomeserverValue = gPrefs.getString("homeserver", "homeserver1");
|
gHomeserverValue = gPrefs.getString("homeserver", "homeserver1");
|
||||||
String walletTypeStored = gPrefs.getString("wallet_type", "dev.key");
|
String walletTypeStored = gPrefs.getString("wallet_type", "client.key");
|
||||||
if (walletTypeStored == "root.key") {
|
if (walletTypeStored == "root.key") {
|
||||||
gSelectedWalletType = WALLET_SELECTION_ROOT;
|
gSelectedWalletType = WALLET_SELECTION_ROOT;
|
||||||
} else if (walletTypeStored == "custom") {
|
} else if (walletTypeStored == "custom") {
|
||||||
@ -5786,7 +5786,7 @@ static void drawWalletSelectScreen() {
|
|||||||
makeTitle("SELECT WALLET", 22, &lv_font_montserrat_24);
|
makeTitle("SELECT WALLET", 22, &lv_font_montserrat_24);
|
||||||
String currentLine = String("Current: ") + selectedWalletDisplayName();
|
String currentLine = String("Current: ") + selectedWalletDisplayName();
|
||||||
makeBody(currentLine.c_str(), 88, 420);
|
makeBody(currentLine.c_str(), 88, 420);
|
||||||
String deviceLabel = String(gSelectedWalletType == WALLET_SELECTION_DEVICE ? "✓ DeviceKey" : "DeviceKey");
|
String deviceLabel = String(gSelectedWalletType == WALLET_SELECTION_DEVICE ? "✓ ClientKey" : "ClientKey");
|
||||||
String rootLabel = String(gSelectedWalletType == WALLET_SELECTION_ROOT ? "✓ RootKey" : "RootKey");
|
String rootLabel = String(gSelectedWalletType == WALLET_SELECTION_ROOT ? "✓ RootKey" : "RootKey");
|
||||||
String customBase = gCustomWalletName;
|
String customBase = gCustomWalletName;
|
||||||
customBase.trim();
|
customBase.trim();
|
||||||
@ -6081,7 +6081,7 @@ static void drawPairingRequestDetailScreen() {
|
|||||||
String question = String("Connect session ") + pairingSessionNameLabel(item) + "?";
|
String question = String("Connect session ") + pairingSessionNameLabel(item) + "?";
|
||||||
String explain = item.requesterSessionType == 50
|
String explain = item.requesterSessionType == 50
|
||||||
? "Wallet session. No keys will be transferred."
|
? "Wallet session. No keys will be transferred."
|
||||||
: "Client session. Only device key will be transferred. No additional keys will be sent.";
|
: "Client session. Only client key will be transferred. No additional keys will be sent.";
|
||||||
String sessionNameText = String("Session: ") + pairingSessionNameLabel(item);
|
String sessionNameText = String("Session: ") + pairingSessionNameLabel(item);
|
||||||
String sessionKindText = String("Kind: ") + pairingSessionKindLabel(item.requesterSessionType);
|
String sessionKindText = String("Kind: ") + pairingSessionKindLabel(item.requesterSessionType);
|
||||||
|
|
||||||
@ -6301,8 +6301,8 @@ static void drawSecretShowScreen() {
|
|||||||
addKeyBlock("Root key priv (base58)", "sha256(base64(secret)|root.key)", gRootPrivB58);
|
addKeyBlock("Root key priv (base58)", "sha256(base64(secret)|root.key)", gRootPrivB58);
|
||||||
addKeyBlock("Blockchain key (base58)", "pub from sha256(base64(secret)|bch.key)", gBlockchainPubB58);
|
addKeyBlock("Blockchain key (base58)", "pub from sha256(base64(secret)|bch.key)", gBlockchainPubB58);
|
||||||
addKeyBlock("Blockchain key priv (base58)", "sha256(base64(secret)|bch.key)", gBlockchainPrivB58);
|
addKeyBlock("Blockchain key priv (base58)", "sha256(base64(secret)|bch.key)", gBlockchainPrivB58);
|
||||||
addKeyBlock("Device key (base58)", "pub from sha256(base64(secret)|dev.key)", gDevicePubB58);
|
addKeyBlock("Client key (base58)", "pub from sha256(base64(secret)|client.key)", gDevicePubB58);
|
||||||
addKeyBlock("Device key priv (base58)", "sha256(base64(secret)|dev.key)", gDevicePrivB58);
|
addKeyBlock("Client key priv (base58)", "sha256(base64(secret)|client.key)", gDevicePrivB58);
|
||||||
addKeyBlock("Homeserver key (base58)", String("pub from sha256(base64(secret)|") + homeserverKeySuffix() + ")", gHomeserverPubB58);
|
addKeyBlock("Homeserver key (base58)", String("pub from sha256(base64(secret)|") + homeserverKeySuffix() + ")", gHomeserverPubB58);
|
||||||
addKeyBlock("Homeserver key priv (base58)", String("sha256(base64(secret)|") + homeserverKeySuffix() + ")", gHomeserverPrivB58);
|
addKeyBlock("Homeserver key priv (base58)", String("sha256(base64(secret)|") + homeserverKeySuffix() + ")", gHomeserverPrivB58);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -228,7 +228,7 @@ struct DerivedKeyState {
|
|||||||
uint8_t rootSk[64];
|
uint8_t rootSk[64];
|
||||||
uint8_t blockchainPub[32];
|
uint8_t blockchainPub[32];
|
||||||
uint8_t blockchainSk[64];
|
uint8_t blockchainSk[64];
|
||||||
uint8_t devicePub[32];
|
uint8_t clientPub[32];
|
||||||
uint8_t deviceSk[64];
|
uint8_t deviceSk[64];
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -246,7 +246,7 @@ static const char *kPaymentsInflowSeed = "shine_payments_inflow_vault";
|
|||||||
static const char *kProgramDerivedAddressMarker = "ProgramDerivedAddress";
|
static const char *kProgramDerivedAddressMarker = "ProgramDerivedAddress";
|
||||||
static const char *kLastBlockPrefix = "SHiNE_LAST_BLOCK";
|
static const char *kLastBlockPrefix = "SHiNE_LAST_BLOCK";
|
||||||
static const uint8_t kBlockTypeRootKey = 1;
|
static const uint8_t kBlockTypeRootKey = 1;
|
||||||
static const uint8_t kBlockTypeDeviceKey = 2;
|
static const uint8_t kBlockTypeClientKey = 2;
|
||||||
static const uint8_t kBlockTypeBlockchainRegistry = 3;
|
static const uint8_t kBlockTypeBlockchainRegistry = 3;
|
||||||
static const uint8_t kBlockTypeServerProfile = 30;
|
static const uint8_t kBlockTypeServerProfile = 30;
|
||||||
static const uint8_t kBlockTypeAccessServers = 40;
|
static const uint8_t kBlockTypeAccessServers = 40;
|
||||||
@ -786,8 +786,8 @@ static bool deriveKeysFromMasterSecret(const uint8_t masterSecret[32]) {
|
|||||||
if (secretB64.length() == 0) {
|
if (secretB64.length() == 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const char *suffixes[3] = {"root.key", "bch.key", "dev.key"};
|
const char *suffixes[3] = {"root.key", "blockchain.key", "client.key"};
|
||||||
uint8_t *pubs[3] = {gDerivedKeys.rootPub, gDerivedKeys.blockchainPub, gDerivedKeys.devicePub};
|
uint8_t *pubs[3] = {gDerivedKeys.rootPub, gDerivedKeys.blockchainPub, gDerivedKeys.clientPub};
|
||||||
uint8_t *sks[3] = {gDerivedKeys.rootSk, gDerivedKeys.blockchainSk, gDerivedKeys.deviceSk};
|
uint8_t *sks[3] = {gDerivedKeys.rootSk, gDerivedKeys.blockchainSk, gDerivedKeys.deviceSk};
|
||||||
for (int i = 0; i < 3; i++) {
|
for (int i = 0; i < 3; i++) {
|
||||||
String material = secretB64 + "|" + suffixes[i];
|
String material = secretB64 + "|" + suffixes[i];
|
||||||
@ -822,7 +822,7 @@ static bool restoreDerivedKeysFromSecret() {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
gData.secretReady = true;
|
gData.secretReady = true;
|
||||||
gData.walletAddress = bytesToBase58(gDerivedKeys.devicePub, 32);
|
gData.walletAddress = bytesToBase58(gDerivedKeys.clientPub, 32);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -835,7 +835,7 @@ static bool deriveFreshSecretAndWallet() {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
gData.secret = bytesToBase58(secret, sizeof(secret));
|
gData.secret = bytesToBase58(secret, sizeof(secret));
|
||||||
gData.walletAddress = bytesToBase58(gDerivedKeys.devicePub, 32);
|
gData.walletAddress = bytesToBase58(gDerivedKeys.clientPub, 32);
|
||||||
gData.secretReady = true;
|
gData.secretReady = true;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -889,7 +889,7 @@ static std::vector<uint8_t> buildUnsignedCreateRecord(
|
|||||||
const String &blockchainName,
|
const String &blockchainName,
|
||||||
const String &serverAddress,
|
const String &serverAddress,
|
||||||
const uint8_t rootPub[32],
|
const uint8_t rootPub[32],
|
||||||
const uint8_t devicePub[32],
|
const uint8_t clientPub[32],
|
||||||
const uint8_t blockchainPub[32],
|
const uint8_t blockchainPub[32],
|
||||||
const uint8_t lastBlockSignature[64],
|
const uint8_t lastBlockSignature[64],
|
||||||
uint64_t createdAtMs) {
|
uint64_t createdAtMs) {
|
||||||
@ -911,9 +911,9 @@ static std::vector<uint8_t> buildUnsignedCreateRecord(
|
|||||||
out.push_back(0);
|
out.push_back(0);
|
||||||
pushFixed(out, rootPub, 32);
|
pushFixed(out, rootPub, 32);
|
||||||
|
|
||||||
out.push_back(kBlockTypeDeviceKey);
|
out.push_back(kBlockTypeClientKey);
|
||||||
out.push_back(0);
|
out.push_back(0);
|
||||||
pushFixed(out, devicePub, 32);
|
pushFixed(out, clientPub, 32);
|
||||||
|
|
||||||
out.push_back(kBlockTypeBlockchainRegistry);
|
out.push_back(kBlockTypeBlockchainRegistry);
|
||||||
out.push_back(0);
|
out.push_back(0);
|
||||||
@ -960,7 +960,7 @@ static std::vector<uint8_t> buildCreateInstructionData(
|
|||||||
const String &blockchainName,
|
const String &blockchainName,
|
||||||
const String &serverAddress,
|
const String &serverAddress,
|
||||||
const uint8_t rootPub[32],
|
const uint8_t rootPub[32],
|
||||||
const uint8_t devicePub[32],
|
const uint8_t clientPub[32],
|
||||||
const uint8_t blockchainPub[32],
|
const uint8_t blockchainPub[32],
|
||||||
const uint8_t lastBlockSignature[64],
|
const uint8_t lastBlockSignature[64],
|
||||||
const uint8_t rootSignature[64],
|
const uint8_t rootSignature[64],
|
||||||
@ -972,7 +972,7 @@ static std::vector<uint8_t> buildCreateInstructionData(
|
|||||||
pushFixed(out, rootPub, 32);
|
pushFixed(out, rootPub, 32);
|
||||||
pushU64LE(out, createdAtMs);
|
pushU64LE(out, createdAtMs);
|
||||||
pushU64LE(out, 0);
|
pushU64LE(out, 0);
|
||||||
pushFixed(out, devicePub, 32);
|
pushFixed(out, clientPub, 32);
|
||||||
pushFixed(out, blockchainPub, 32);
|
pushFixed(out, blockchainPub, 32);
|
||||||
pushStrU8(out, blockchainName);
|
pushStrU8(out, blockchainName);
|
||||||
pushU64LE(out, 0);
|
pushU64LE(out, 0);
|
||||||
@ -1079,7 +1079,7 @@ static bool pdaAlreadyExists(const String &login, String &pdaAddress, String &me
|
|||||||
|
|
||||||
static std::vector<uint8_t> buildLegacyMessage(
|
static std::vector<uint8_t> buildLegacyMessage(
|
||||||
const uint8_t recentBlockhash[32],
|
const uint8_t recentBlockhash[32],
|
||||||
const uint8_t devicePub[32],
|
const uint8_t clientPub[32],
|
||||||
const uint8_t userPda[32],
|
const uint8_t userPda[32],
|
||||||
const uint8_t inflowVault[32],
|
const uint8_t inflowVault[32],
|
||||||
const uint8_t economyConfig[32],
|
const uint8_t economyConfig[32],
|
||||||
@ -1098,7 +1098,7 @@ static std::vector<uint8_t> buildLegacyMessage(
|
|||||||
base58ToFixed32(kShineLoginGuardProgramId, loginGuardProgram);
|
base58ToFixed32(kShineLoginGuardProgramId, loginGuardProgram);
|
||||||
|
|
||||||
std::vector<std::vector<uint8_t>> accountKeys;
|
std::vector<std::vector<uint8_t>> accountKeys;
|
||||||
accountKeys.emplace_back(devicePub, devicePub + 32);
|
accountKeys.emplace_back(clientPub, clientPub + 32);
|
||||||
accountKeys.emplace_back(userPda, userPda + 32);
|
accountKeys.emplace_back(userPda, userPda + 32);
|
||||||
accountKeys.emplace_back(inflowVault, inflowVault + 32);
|
accountKeys.emplace_back(inflowVault, inflowVault + 32);
|
||||||
accountKeys.emplace_back(systemProgram, systemProgram + 32);
|
accountKeys.emplace_back(systemProgram, systemProgram + 32);
|
||||||
@ -1244,7 +1244,7 @@ static bool registerHomeserverOnSolana(String &messageOut) {
|
|||||||
uint64_t createdAtMs = (uint64_t)millis() + 1704067200000ULL;
|
uint64_t createdAtMs = (uint64_t)millis() + 1704067200000ULL;
|
||||||
std::vector<uint8_t> unsignedRecord = buildUnsignedCreateRecord(
|
std::vector<uint8_t> unsignedRecord = buildUnsignedCreateRecord(
|
||||||
gData.login, blockchainName, gData.wsUrl,
|
gData.login, blockchainName, gData.wsUrl,
|
||||||
gDerivedKeys.rootPub, gDerivedKeys.devicePub, gDerivedKeys.blockchainPub,
|
gDerivedKeys.rootPub, gDerivedKeys.clientPub, gDerivedKeys.blockchainPub,
|
||||||
lastBlockSignature, createdAtMs);
|
lastBlockSignature, createdAtMs);
|
||||||
uint8_t unsignedHash[32];
|
uint8_t unsignedHash[32];
|
||||||
uint8_t rootSignature[64];
|
uint8_t rootSignature[64];
|
||||||
@ -1256,7 +1256,7 @@ static bool registerHomeserverOnSolana(String &messageOut) {
|
|||||||
|
|
||||||
std::vector<uint8_t> createData = buildCreateInstructionData(
|
std::vector<uint8_t> createData = buildCreateInstructionData(
|
||||||
gData.login, blockchainName, gData.wsUrl,
|
gData.login, blockchainName, gData.wsUrl,
|
||||||
gDerivedKeys.rootPub, gDerivedKeys.devicePub, gDerivedKeys.blockchainPub,
|
gDerivedKeys.rootPub, gDerivedKeys.clientPub, gDerivedKeys.blockchainPub,
|
||||||
lastBlockSignature, rootSignature, createdAtMs);
|
lastBlockSignature, rootSignature, createdAtMs);
|
||||||
std::vector<uint8_t> edRootData = buildEd25519InstructionData(rootSignature, gDerivedKeys.rootPub, unsignedHash);
|
std::vector<uint8_t> edRootData = buildEd25519InstructionData(rootSignature, gDerivedKeys.rootPub, unsignedHash);
|
||||||
std::vector<uint8_t> edBchData = buildEd25519InstructionData(lastBlockSignature, gDerivedKeys.blockchainPub, lastBlockHash);
|
std::vector<uint8_t> edBchData = buildEd25519InstructionData(lastBlockSignature, gDerivedKeys.blockchainPub, lastBlockHash);
|
||||||
@ -1269,7 +1269,7 @@ static bool registerHomeserverOnSolana(String &messageOut) {
|
|||||||
|
|
||||||
std::vector<uint8_t> message = buildLegacyMessage(
|
std::vector<uint8_t> message = buildLegacyMessage(
|
||||||
recentBlockhash,
|
recentBlockhash,
|
||||||
gDerivedKeys.devicePub,
|
gDerivedKeys.clientPub,
|
||||||
userPda,
|
userPda,
|
||||||
inflowVault,
|
inflowVault,
|
||||||
economyConfig,
|
economyConfig,
|
||||||
@ -2107,7 +2107,7 @@ static void drawConfirmScreen() {
|
|||||||
String text = "Выполнить действие?";
|
String text = "Выполнить действие?";
|
||||||
if (gConfirmTarget == CONFIRM_REGISTER) {
|
if (gConfirmTarget == CONFIRM_REGISTER) {
|
||||||
title = "Регистрация";
|
title = "Регистрация";
|
||||||
text = "Отправить create_user_pda в Solana через device key этого устройства?";
|
text = "Отправить create_user_pda в Solana через client key этого устройства?";
|
||||||
} else if (gConfirmTarget == CONFIRM_CLEAR_ACCOUNT) {
|
} else if (gConfirmTarget == CONFIRM_CLEAR_ACCOUNT) {
|
||||||
title = "Очистка";
|
title = "Очистка";
|
||||||
text = "Удалить секрет, кошелёк и статус регистрации?";
|
text = "Удалить секрет, кошелёк и статус регистрации?";
|
||||||
|
|||||||
@ -70,11 +70,11 @@
|
|||||||
Фоновая логика:
|
Фоновая логика:
|
||||||
- пока открыт `HOME`, экран сам обновляется примерно раз в секунду;
|
- пока открыт `HOME`, экран сам обновляется примерно раз в секунду;
|
||||||
- при наличии `login + secret + homeserver` и Wi-Fi устройство читает Solana `user_pda` напрямую через RPC;
|
- при наличии `login + secret + homeserver` и Wi-Fi устройство читает Solana `user_pda` напрямую через RPC;
|
||||||
- сравниваются `root key`, `blockchain key`, `device key` и `homeserver` session-запись типа `100`;
|
- сравниваются `root key`, `blockchain key`, `client key` и `homeserver` session-запись типа `100`;
|
||||||
- для строки `SHiNE:` устройство держит отдельную WebSocket-сессию с сервером SHiNE:
|
- для строки `SHiNE:` устройство держит отдельную WebSocket-сессию с сервером SHiNE:
|
||||||
- авторизация через `AuthChallenge/CreateAuthSession` или `SessionChallenge/SessionLogin`;
|
- авторизация через `AuthChallenge/CreateAuthSession` или `SessionChallenge/SessionLogin`;
|
||||||
- session key = публичный `homeserver key`;
|
- session key = публичный `homeserver key`;
|
||||||
- подтверждение создания сессии подписывается `device key`;
|
- подтверждение создания сессии подписывается `client key`;
|
||||||
- heartbeat выполняется `Ping` раз в минуту.
|
- heartbeat выполняется `Ping` раз в минуту.
|
||||||
|
|
||||||
## SETTINGS_MENU
|
## SETTINGS_MENU
|
||||||
@ -214,8 +214,8 @@
|
|||||||
- `Root key priv (base58)`;
|
- `Root key priv (base58)`;
|
||||||
- `Blockchain key (base58)`;
|
- `Blockchain key (base58)`;
|
||||||
- `Blockchain key priv (base58)`;
|
- `Blockchain key priv (base58)`;
|
||||||
- `Device key (base58)`;
|
- `Client key (base58)`;
|
||||||
- `Device key priv (base58)`;
|
- `Client key priv (base58)`;
|
||||||
- `Homeserver key (base58)`;
|
- `Homeserver key (base58)`;
|
||||||
- `Homeserver key priv (base58)`;
|
- `Homeserver key priv (base58)`;
|
||||||
- для каждого поля показывается формула derivation;
|
- для каждого поля показывается формула derivation;
|
||||||
|
|||||||
@ -178,7 +178,7 @@
|
|||||||
- вместо старых двух кнопок `баланс` и `QR` показывается одна широкая кнопка;
|
- вместо старых двух кнопок `баланс` и `QR` показывается одна широкая кнопка;
|
||||||
- текст кнопки: `Wallet: <selected wallet name>`;
|
- текст кнопки: `Wallet: <selected wallet name>`;
|
||||||
- доступные имена:
|
- доступные имена:
|
||||||
- `DeviceKey`
|
- `ClientKey`
|
||||||
- `RootKey`
|
- `RootKey`
|
||||||
- либо сохранённое имя `custom`-кошелька;
|
- либо сохранённое имя `custom`-кошелька;
|
||||||
- после старта устройства баланс активного выбранного кошелька пытается загрузиться автоматически, если уже есть секрет и `Wi-Fi`;
|
- после старта устройства баланс активного выбранного кошелька пытается загрузиться автоматически, если уже есть секрет и `Wi-Fi`;
|
||||||
@ -429,7 +429,7 @@
|
|||||||
- верхняя кнопка открывает экран выбора кошелька `WALLET_SELECT`;
|
- верхняя кнопка открывает экран выбора кошелька `WALLET_SELECT`;
|
||||||
- `Показать баланс кошелька` читает реальный баланс именно активного выбранного кошелька из `Solana RPC`;
|
- `Показать баланс кошелька` читает реальный баланс именно активного выбранного кошелька из `Solana RPC`;
|
||||||
- `Показать QR-код кошелька` открывает экран `WALLET_QR`;
|
- `Показать QR-код кошелька` открывает экран `WALLET_QR`;
|
||||||
- этот экран не меняет логику Solana-регистрации пользователя: on-chain регистрация и проверки `PDA` по-прежнему завязаны на `device key`.
|
- этот экран не меняет логику Solana-регистрации пользователя: on-chain регистрация и проверки `PDA` по-прежнему завязаны на `client key`.
|
||||||
|
|
||||||
## Экран WALLET_SELECT
|
## Экран WALLET_SELECT
|
||||||
|
|
||||||
@ -437,14 +437,14 @@
|
|||||||
|
|
||||||
- строку `Current: <selected wallet name>`;
|
- строку `Current: <selected wallet name>`;
|
||||||
- три кнопки выбора:
|
- три кнопки выбора:
|
||||||
- `DeviceKey`
|
- `ClientKey`
|
||||||
- `RootKey`
|
- `RootKey`
|
||||||
- `Custom` или `Custom: <имя>`;
|
- `Custom` или `Custom: <имя>`;
|
||||||
- у текущего выбора видна галочка.
|
- у текущего выбора видна галочка.
|
||||||
|
|
||||||
Поведение:
|
Поведение:
|
||||||
|
|
||||||
- `DeviceKey` активирует кошелёк, выведенный из suffix `dev.key`;
|
- `ClientKey` активирует кошелёк, выведенный из suffix `client.key`;
|
||||||
- `RootKey` активирует кошелёк, выведенный из suffix `root.key`;
|
- `RootKey` активирует кошелёк, выведенный из suffix `root.key`;
|
||||||
- `Custom` использует derivation:
|
- `Custom` использует derivation:
|
||||||
`sha256(base64(secret32) + "|wallet." + customName)`;
|
`sha256(base64(secret32) + "|wallet." + customName)`;
|
||||||
@ -533,7 +533,7 @@
|
|||||||
- строку `Session: <platform/name>`;
|
- строку `Session: <platform/name>`;
|
||||||
- строку `Kind: Client session` или `Kind: Wallet session`;
|
- строку `Kind: Client session` или `Kind: Wallet session`;
|
||||||
- пояснение:
|
- пояснение:
|
||||||
- для client session: `Only device key will be transferred. No additional keys will be sent.`
|
- для client session: `Only client key will be transferred. No additional keys will be sent.`
|
||||||
- для wallet session: `No keys will be transferred.`
|
- для wallet session: `No keys will be transferred.`
|
||||||
|
|
||||||
Кнопки:
|
Кнопки:
|
||||||
@ -544,7 +544,7 @@
|
|||||||
Поведение:
|
Поведение:
|
||||||
|
|
||||||
- `YES` подтверждает заявку:
|
- `YES` подтверждает заявку:
|
||||||
- для client session устройство передаёт только `device key`;
|
- для client session устройство передаёт только `client key`;
|
||||||
- для wallet session устройство выпускает отдельную `wallet-session` без передачи ключей;
|
- для wallet session устройство выпускает отдельную `wallet-session` без передачи ключей;
|
||||||
- `NO` отклоняет заявку;
|
- `NO` отклоняет заявку;
|
||||||
- после любого решения устройство возвращается в список `REQUESTS` и обновляет его;
|
- после любого решения устройство возвращается в список `REQUESTS` и обновляет его;
|
||||||
|
|||||||
@ -7,7 +7,7 @@ Chrome-compatible Manifest V3 plugin for SHiNE wallet-session login.
|
|||||||
- создать `wallet-session` через `StartTrustedDeviceLogin`;
|
- создать `wallet-session` через `StartTrustedDeviceLogin`;
|
||||||
- показать код подключения;
|
- показать код подключения;
|
||||||
- дождаться подтверждения на доверенном устройстве;
|
- дождаться подтверждения на доверенном устройстве;
|
||||||
- принять `session-only` payload без передачи `deviceKey/rootKey/blockchainKey`;
|
- принять `session-only` payload без передачи `clientKey/rootKey/blockchainKey`;
|
||||||
- сохранить `sessionPriv/sessionKey/sessionId` в локальном хранилище plugin;
|
- сохранить `sessionPriv/sessionKey/sessionId` в локальном хранилище plugin;
|
||||||
- восстанавливать session через `SessionChallenge -> SessionLogin`;
|
- восстанавливать session через `SessionChallenge -> SessionLogin`;
|
||||||
- держать wallet-state в `background service worker`, а side panel использовать как UI.
|
- держать wallet-state в `background service worker`, а side panel использовать как UI.
|
||||||
|
|||||||
@ -563,11 +563,13 @@ function verifyWalletAgainstPda(wallet) {
|
|||||||
const type = String(wallet?.type || '').trim();
|
const type = String(wallet?.type || '').trim();
|
||||||
const pub = String(wallet?.publicKeyBase58 || '').trim();
|
const pub = String(wallet?.publicKeyBase58 || '').trim();
|
||||||
const rootKey = String(state.walletProfile?.publicKeys?.rootKeyBase58 || '').trim();
|
const rootKey = String(state.walletProfile?.publicKeys?.rootKeyBase58 || '').trim();
|
||||||
const deviceKey = String(state.walletProfile?.publicKeys?.deviceKeyBase58 || '').trim();
|
const clientKey = String(
|
||||||
if (type === 'dev.key') {
|
state.walletProfile?.publicKeys?.clientKeyBase58 || '',
|
||||||
|
).trim();
|
||||||
|
if (type === 'client.key') {
|
||||||
return {
|
return {
|
||||||
verified: !!deviceKey && deviceKey === pub,
|
verified: !!clientKey && clientKey === pub,
|
||||||
verificationText: deviceKey === pub ? 'Совпадает с deviceKey из PDA.' : 'Не совпадает с deviceKey из PDA.',
|
verificationText: clientKey === pub ? 'Совпадает с clientKey из PDA.' : 'Не совпадает с clientKey из PDA.',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (type === 'root.key') {
|
if (type === 'root.key') {
|
||||||
|
|||||||
@ -69,8 +69,9 @@ function parseServerFieldsFromUserPda(dataBytes) {
|
|||||||
let isServer = false;
|
let isServer = false;
|
||||||
let serverAddress = '';
|
let serverAddress = '';
|
||||||
let accessServers = [];
|
let accessServers = [];
|
||||||
|
let recoveryKey32 = null;
|
||||||
let rootKey32 = null;
|
let rootKey32 = null;
|
||||||
let deviceKey32 = null;
|
let clientKey32 = null;
|
||||||
let blockchainKey32 = null;
|
let blockchainKey32 = null;
|
||||||
let blockchainName = '';
|
let blockchainName = '';
|
||||||
let homeserverSessions = [];
|
let homeserverSessions = [];
|
||||||
@ -79,10 +80,11 @@ function parseServerFieldsFromUserPda(dataBytes) {
|
|||||||
const blockType = readU8(bytes, cursorRef);
|
const blockType = readU8(bytes, cursorRef);
|
||||||
cursorRef.value += 1; // block_version
|
cursorRef.value += 1; // block_version
|
||||||
|
|
||||||
if (blockType === 1 || blockType === 2) {
|
if (blockType === 0 || blockType === 1 || blockType === 2) {
|
||||||
const key32 = readBytes(bytes, cursorRef, 32);
|
const key32 = readBytes(bytes, cursorRef, 32);
|
||||||
|
if (blockType === 0) recoveryKey32 = key32;
|
||||||
if (blockType === 1) rootKey32 = key32;
|
if (blockType === 1) rootKey32 = key32;
|
||||||
if (blockType === 2) deviceKey32 = key32;
|
if (blockType === 2) clientKey32 = key32;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (blockType === 3) {
|
if (blockType === 3) {
|
||||||
@ -150,8 +152,9 @@ function parseServerFieldsFromUserPda(dataBytes) {
|
|||||||
serverAddress: normalizeHostLike(serverAddress),
|
serverAddress: normalizeHostLike(serverAddress),
|
||||||
accessServers: accessServers.map((value) => normalizeServerLogin(value)).filter(Boolean),
|
accessServers: accessServers.map((value) => normalizeServerLogin(value)).filter(Boolean),
|
||||||
publicKeys: {
|
publicKeys: {
|
||||||
|
recoveryKeyBase58: recoveryKey32 ? new PublicKey(recoveryKey32).toBase58() : '',
|
||||||
rootKeyBase58: rootKey32 ? new PublicKey(rootKey32).toBase58() : '',
|
rootKeyBase58: rootKey32 ? new PublicKey(rootKey32).toBase58() : '',
|
||||||
deviceKeyBase58: deviceKey32 ? new PublicKey(deviceKey32).toBase58() : '',
|
clientKeyBase58: clientKey32 ? new PublicKey(clientKey32).toBase58() : '',
|
||||||
blockchainKeyBase58: blockchainKey32 ? new PublicKey(blockchainKey32).toBase58() : '',
|
blockchainKeyBase58: blockchainKey32 ? new PublicKey(blockchainKey32).toBase58() : '',
|
||||||
blockchainName,
|
blockchainName,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -52,7 +52,7 @@
|
|||||||
<div class="summary-row"><span>Логин</span><strong id="session-login">—</strong></div>
|
<div class="summary-row"><span>Логин</span><strong id="session-login">—</strong></div>
|
||||||
<div class="summary-row"><span>Session</span><code id="session-id">—</code></div>
|
<div class="summary-row"><span>Session</span><code id="session-id">—</code></div>
|
||||||
<div class="summary-row"><span>Тип</span><strong id="session-type">wallet</strong></div>
|
<div class="summary-row"><span>Тип</span><strong id="session-type">wallet</strong></div>
|
||||||
<div class="summary-row"><span>deviceKey</span><code id="device-key-short">—</code></div>
|
<div class="summary-row"><span>clientKey</span><code id="client-key-short">—</code></div>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button id="resume-btn" class="btn secondary" type="button">Проверить session</button>
|
<button id="resume-btn" class="btn secondary" type="button">Проверить session</button>
|
||||||
<button id="disconnect-btn" class="btn danger" type="button">Отключить</button>
|
<button id="disconnect-btn" class="btn danger" type="button">Отключить</button>
|
||||||
|
|||||||
@ -19,7 +19,7 @@ const els = {
|
|||||||
sessionLogin: document.querySelector('#session-login'),
|
sessionLogin: document.querySelector('#session-login'),
|
||||||
sessionId: document.querySelector('#session-id'),
|
sessionId: document.querySelector('#session-id'),
|
||||||
sessionType: document.querySelector('#session-type'),
|
sessionType: document.querySelector('#session-type'),
|
||||||
deviceKeyShort: document.querySelector('#device-key-short'),
|
clientKeyShort: document.querySelector('#client-key-short'),
|
||||||
resumeBtn: document.querySelector('#resume-btn'),
|
resumeBtn: document.querySelector('#resume-btn'),
|
||||||
refreshDevicesBtn: document.querySelector('#refresh-devices-btn'),
|
refreshDevicesBtn: document.querySelector('#refresh-devices-btn'),
|
||||||
disconnectBtn: document.querySelector('#disconnect-btn'),
|
disconnectBtn: document.querySelector('#disconnect-btn'),
|
||||||
@ -134,7 +134,7 @@ function applyState(nextState) {
|
|||||||
els.sessionLogin.textContent = session.login || '—';
|
els.sessionLogin.textContent = session.login || '—';
|
||||||
els.sessionId.textContent = session.sessionId || '—';
|
els.sessionId.textContent = session.sessionId || '—';
|
||||||
els.sessionType.textContent = String(session.sessionType || 50) === '50' ? 'wallet' : String(session.sessionType || '—');
|
els.sessionType.textContent = String(session.sessionType || 50) === '50' ? 'wallet' : String(session.sessionType || '—');
|
||||||
els.deviceKeyShort.textContent = shortKey(walletProfile?.publicKeys?.deviceKeyBase58 || '');
|
els.clientKeyShort.textContent = shortKey(walletProfile?.publicKeys?.clientKeyBase58 || '');
|
||||||
}
|
}
|
||||||
|
|
||||||
const homeservers = Array.isArray(walletProfile?.homeserverSessions) ? walletProfile.homeserverSessions : [];
|
const homeservers = Array.isArray(walletProfile?.homeserverSessions) ? walletProfile.homeserverSessions : [];
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -1,20 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
OUTFILE="all_files.txt"
|
|
||||||
|
|
||||||
# очищаем или создаём файл
|
|
||||||
: > "$OUTFILE"
|
|
||||||
|
|
||||||
# собрать только *.java файлы и вывести их содержимое в файл
|
|
||||||
find . -type f -name "*.java" | sort | while read -r f; do
|
|
||||||
cat "$f" >> "$OUTFILE"
|
|
||||||
echo >> "$OUTFILE" # пустая строка-разделитель
|
|
||||||
done
|
|
||||||
|
|
||||||
# скопировать весь файл в буфер обмена (Wayland)
|
|
||||||
wl-copy < "$OUTFILE"
|
|
||||||
|
|
||||||
echo "Готово!"
|
|
||||||
echo "Все .java файлы собраны в $OUTFILE"
|
|
||||||
echo "Содержимое скопировано в буфер обмена (Wayland)"
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
OUTFILE="all_files.txt"
|
|
||||||
|
|
||||||
# очищаем или создаём файл
|
|
||||||
: > "$OUTFILE"
|
|
||||||
|
|
||||||
# собрать только *.java файлы и вывести их содержимое в файл
|
|
||||||
find . -type f -name "*.java" | sort | while read -r f; do
|
|
||||||
cat "$f" >> "$OUTFILE"
|
|
||||||
echo >> "$OUTFILE" # пустая строка-разделитель
|
|
||||||
done
|
|
||||||
|
|
||||||
# скопировать весь файл в буфер обмена (Wayland)
|
|
||||||
wl-copy < "$OUTFILE"
|
|
||||||
|
|
||||||
echo "Готово!"
|
|
||||||
echo "Все .java файлы собраны в $OUTFILE"
|
|
||||||
echo "Содержимое скопировано в буфер обмена (Wayland)"
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
OUTFILE="all_files.txt"
|
|
||||||
|
|
||||||
# очищаем или создаём файл
|
|
||||||
: > "$OUTFILE"
|
|
||||||
|
|
||||||
# собрать только *.java файлы и вывести их содержимое в файл
|
|
||||||
find . -type f -name "*.java" | sort | while read -r f; do
|
|
||||||
cat "$f" >> "$OUTFILE"
|
|
||||||
echo >> "$OUTFILE" # пустая строка-разделитель
|
|
||||||
done
|
|
||||||
|
|
||||||
# скопировать весь файл в буфер обмена (Wayland)
|
|
||||||
wl-copy < "$OUTFILE"
|
|
||||||
|
|
||||||
echo "Готово!"
|
|
||||||
echo "Все .java файлы собраны в $OUTFILE"
|
|
||||||
echo "Содержимое скопировано в буфер обмена (Wayland)"
|
|
||||||
File diff suppressed because it is too large
Load Diff
@ -1,20 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
OUTFILE="all_files.txt"
|
|
||||||
|
|
||||||
# очищаем или создаём файл
|
|
||||||
: > "$OUTFILE"
|
|
||||||
|
|
||||||
# собрать только *.java файлы и вывести их содержимое в файл
|
|
||||||
find . -type f -name "*.java" | sort | while read -r f; do
|
|
||||||
cat "$f" >> "$OUTFILE"
|
|
||||||
echo >> "$OUTFILE" # пустая строка-разделитель
|
|
||||||
done
|
|
||||||
|
|
||||||
# скопировать весь файл в буфер обмена (Wayland)
|
|
||||||
wl-copy < "$OUTFILE"
|
|
||||||
|
|
||||||
echo "Готово!"
|
|
||||||
echo "Все .java файлы собраны в $OUTFILE"
|
|
||||||
echo "Содержимое скопировано в буфер обмена (Wayland)"
|
|
||||||
@ -140,7 +140,7 @@ public final class DatabaseInitializer {
|
|||||||
// 1. solana_users
|
// 1. solana_users
|
||||||
// ВАЖНО:
|
// ВАЖНО:
|
||||||
// - Все требуемые поля теперь лежат в solana_users:
|
// - Все требуемые поля теперь лежат в solana_users:
|
||||||
// login, blockchain_name, solana_key, blockchain_key, device_key
|
// login, blockchain_name, solana_key, blockchain_key, client_key
|
||||||
// - Поиск по login в DAO сделан case-insensitive.
|
// - Поиск по login в DAO сделан case-insensitive.
|
||||||
// - Для защиты от дублей "Anya" и "anya" добавляем COLLATE NOCASE на PRIMARY KEY.
|
// - Для защиты от дублей "Anya" и "anya" добавляем COLLATE NOCASE на PRIMARY KEY.
|
||||||
st.executeUpdate("""
|
st.executeUpdate("""
|
||||||
@ -149,7 +149,7 @@ public final class DatabaseInitializer {
|
|||||||
blockchain_name TEXT NOT NULL,
|
blockchain_name TEXT NOT NULL,
|
||||||
solana_key TEXT NOT NULL,
|
solana_key TEXT NOT NULL,
|
||||||
blockchain_key TEXT NOT NULL,
|
blockchain_key TEXT NOT NULL,
|
||||||
device_key TEXT NOT NULL
|
client_key TEXT NOT NULL
|
||||||
);
|
);
|
||||||
""");
|
""");
|
||||||
|
|
||||||
@ -238,7 +238,7 @@ public final class DatabaseInitializer {
|
|||||||
param TEXT NOT NULL,
|
param TEXT NOT NULL,
|
||||||
time_ms INTEGER NOT NULL,
|
time_ms INTEGER NOT NULL,
|
||||||
value TEXT NOT NULL,
|
value TEXT NOT NULL,
|
||||||
device_key TEXT,
|
client_key TEXT,
|
||||||
signature TEXT,
|
signature TEXT,
|
||||||
FOREIGN KEY (login) REFERENCES solana_users(login),
|
FOREIGN KEY (login) REFERENCES solana_users(login),
|
||||||
UNIQUE (login, param)
|
UNIQUE (login, param)
|
||||||
|
|||||||
@ -17,7 +17,7 @@ import java.util.List;
|
|||||||
* - blockchain_name TEXT NOT NULL
|
* - blockchain_name TEXT NOT NULL
|
||||||
* - solana_key TEXT NOT NULL
|
* - solana_key TEXT NOT NULL
|
||||||
* - blockchain_key TEXT NOT NULL
|
* - blockchain_key TEXT NOT NULL
|
||||||
* - device_key TEXT NOT NULL
|
* - client_key TEXT NOT NULL
|
||||||
*
|
*
|
||||||
* Правило работы с соединениями:
|
* Правило работы с соединениями:
|
||||||
* - методы с Connection НЕ закрывают соединение
|
* - методы с Connection НЕ закрывают соединение
|
||||||
@ -45,7 +45,7 @@ public final class SolanaUsersDAO {
|
|||||||
public void insert(Connection c, SolanaUserEntry user) throws SQLException {
|
public void insert(Connection c, SolanaUserEntry user) throws SQLException {
|
||||||
String sql = """
|
String sql = """
|
||||||
INSERT INTO solana_users (
|
INSERT INTO solana_users (
|
||||||
login, blockchain_name, solana_key, blockchain_key, device_key
|
login, blockchain_name, solana_key, blockchain_key, client_key
|
||||||
) VALUES (?, ?, ?, ?, ?)
|
) VALUES (?, ?, ?, ?, ?)
|
||||||
""";
|
""";
|
||||||
|
|
||||||
@ -54,7 +54,7 @@ public final class SolanaUsersDAO {
|
|||||||
ps.setString(2, user.getBlockchainName());
|
ps.setString(2, user.getBlockchainName());
|
||||||
ps.setString(3, user.getSolanaKey());
|
ps.setString(3, user.getSolanaKey());
|
||||||
ps.setString(4, user.getBlockchainKey());
|
ps.setString(4, user.getBlockchainKey());
|
||||||
ps.setString(5, user.getDeviceKey());
|
ps.setString(5, user.getClientKey());
|
||||||
ps.executeUpdate();
|
ps.executeUpdate();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -126,7 +126,7 @@ public final class SolanaUsersDAO {
|
|||||||
blockchain_name,
|
blockchain_name,
|
||||||
solana_key,
|
solana_key,
|
||||||
blockchain_key,
|
blockchain_key,
|
||||||
device_key
|
client_key
|
||||||
FROM solana_users
|
FROM solana_users
|
||||||
WHERE LOWER(login) = LOWER(?)
|
WHERE LOWER(login) = LOWER(?)
|
||||||
""";
|
""";
|
||||||
@ -155,7 +155,7 @@ public final class SolanaUsersDAO {
|
|||||||
blockchain_name,
|
blockchain_name,
|
||||||
solana_key,
|
solana_key,
|
||||||
blockchain_key,
|
blockchain_key,
|
||||||
device_key
|
client_key
|
||||||
FROM solana_users
|
FROM solana_users
|
||||||
WHERE blockchain_name = ?
|
WHERE blockchain_name = ?
|
||||||
""";
|
""";
|
||||||
@ -184,7 +184,7 @@ public final class SolanaUsersDAO {
|
|||||||
blockchain_name,
|
blockchain_name,
|
||||||
solana_key,
|
solana_key,
|
||||||
blockchain_key,
|
blockchain_key,
|
||||||
device_key
|
client_key
|
||||||
FROM solana_users
|
FROM solana_users
|
||||||
WHERE LOWER(login) LIKE ?
|
WHERE LOWER(login) LIKE ?
|
||||||
ORDER BY login
|
ORDER BY login
|
||||||
@ -219,7 +219,7 @@ public final class SolanaUsersDAO {
|
|||||||
e.setBlockchainName(rs.getString("blockchain_name"));
|
e.setBlockchainName(rs.getString("blockchain_name"));
|
||||||
e.setSolanaKey(rs.getString("solana_key"));
|
e.setSolanaKey(rs.getString("solana_key"));
|
||||||
e.setBlockchainKey(rs.getString("blockchain_key"));
|
e.setBlockchainKey(rs.getString("blockchain_key"));
|
||||||
e.setDeviceKey(rs.getString("device_key"));
|
e.setClientKey(rs.getString("client_key"));
|
||||||
|
|
||||||
return e;
|
return e;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import java.sql.*;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* UserCreateDAO — атомарное добавление пользователя:
|
* UserCreateDAO — атомарное добавление пользователя:
|
||||||
* - solana_users (login, blockchain_name, solana_key, blockchain_key, device_key)
|
* - solana_users (login, blockchain_name, solana_key, blockchain_key, client_key)
|
||||||
* - blockchain_state (blockchain_name, login, blockchain_key, size_limit, ... last_block_number=-1 ...)
|
* - blockchain_state (blockchain_name, login, blockchain_key, size_limit, ... last_block_number=-1 ...)
|
||||||
*
|
*
|
||||||
* ВАЖНО:
|
* ВАЖНО:
|
||||||
@ -39,7 +39,7 @@ public final class UserCreateDAO {
|
|||||||
String blockchainName,
|
String blockchainName,
|
||||||
String solanaKey,
|
String solanaKey,
|
||||||
String blockchainKey,
|
String blockchainKey,
|
||||||
String deviceKey,
|
String clientKey,
|
||||||
long sizeLimit,
|
long sizeLimit,
|
||||||
long nowMs
|
long nowMs
|
||||||
) throws SQLException {
|
) throws SQLException {
|
||||||
@ -55,7 +55,7 @@ public final class UserCreateDAO {
|
|||||||
u.setBlockchainName(blockchainName);
|
u.setBlockchainName(blockchainName);
|
||||||
u.setSolanaKey(solanaKey);
|
u.setSolanaKey(solanaKey);
|
||||||
u.setBlockchainKey(blockchainKey);
|
u.setBlockchainKey(blockchainKey);
|
||||||
u.setDeviceKey(deviceKey);
|
u.setClientKey(clientKey);
|
||||||
|
|
||||||
usersDao.insert(c, u); // если login занят (NOCASE) или blockchainName (unique) -> constraint
|
usersDao.insert(c, u); // если login занят (NOCASE) или blockchainName (unique) -> constraint
|
||||||
|
|
||||||
|
|||||||
@ -43,14 +43,14 @@ public final class UserParamsDAO {
|
|||||||
param,
|
param,
|
||||||
time_ms,
|
time_ms,
|
||||||
value,
|
value,
|
||||||
device_key,
|
client_key,
|
||||||
signature
|
signature
|
||||||
) VALUES (?, ?, ?, ?, ?, ?)
|
) VALUES (?, ?, ?, ?, ?, ?)
|
||||||
ON CONFLICT(login, param)
|
ON CONFLICT(login, param)
|
||||||
DO UPDATE SET
|
DO UPDATE SET
|
||||||
time_ms = excluded.time_ms,
|
time_ms = excluded.time_ms,
|
||||||
value = excluded.value,
|
value = excluded.value,
|
||||||
device_key = excluded.device_key,
|
client_key = excluded.client_key,
|
||||||
signature = excluded.signature
|
signature = excluded.signature
|
||||||
WHERE users_params.time_ms < excluded.time_ms
|
WHERE users_params.time_ms < excluded.time_ms
|
||||||
""";
|
""";
|
||||||
@ -61,7 +61,7 @@ public final class UserParamsDAO {
|
|||||||
ps.setLong(3, e.getTimeMs());
|
ps.setLong(3, e.getTimeMs());
|
||||||
ps.setString(4, e.getValue());
|
ps.setString(4, e.getValue());
|
||||||
|
|
||||||
if (e.getDeviceKey() != null) ps.setString(5, e.getDeviceKey());
|
if (e.getClientKey() != null) ps.setString(5, e.getClientKey());
|
||||||
else ps.setNull(5, Types.VARCHAR);
|
else ps.setNull(5, Types.VARCHAR);
|
||||||
|
|
||||||
if (e.getSignature() != null) ps.setString(6, e.getSignature());
|
if (e.getSignature() != null) ps.setString(6, e.getSignature());
|
||||||
@ -86,7 +86,7 @@ public final class UserParamsDAO {
|
|||||||
param,
|
param,
|
||||||
time_ms,
|
time_ms,
|
||||||
value,
|
value,
|
||||||
device_key,
|
client_key,
|
||||||
signature
|
signature
|
||||||
FROM users_params
|
FROM users_params
|
||||||
WHERE login = ? COLLATE NOCASE AND param = ?
|
WHERE login = ? COLLATE NOCASE AND param = ?
|
||||||
@ -117,7 +117,7 @@ public final class UserParamsDAO {
|
|||||||
param,
|
param,
|
||||||
time_ms,
|
time_ms,
|
||||||
value,
|
value,
|
||||||
device_key,
|
client_key,
|
||||||
signature
|
signature
|
||||||
FROM users_params
|
FROM users_params
|
||||||
WHERE login = ? COLLATE NOCASE
|
WHERE login = ? COLLATE NOCASE
|
||||||
@ -149,9 +149,9 @@ public final class UserParamsDAO {
|
|||||||
e.setTimeMs(rs.getLong("time_ms"));
|
e.setTimeMs(rs.getLong("time_ms"));
|
||||||
e.setValue(rs.getString("value"));
|
e.setValue(rs.getString("value"));
|
||||||
|
|
||||||
String dk = rs.getString("device_key");
|
String dk = rs.getString("client_key");
|
||||||
if (rs.wasNull()) dk = null;
|
if (rs.wasNull()) dk = null;
|
||||||
e.setDeviceKey(dk);
|
e.setClientKey(dk);
|
||||||
|
|
||||||
String sig = rs.getString("signature");
|
String sig = rs.getString("signature");
|
||||||
if (rs.wasNull()) sig = null;
|
if (rs.wasNull()) sig = null;
|
||||||
|
|||||||
@ -12,7 +12,7 @@ import java.util.Base64;
|
|||||||
* - blockchain_name — TEXT NOT NULL
|
* - blockchain_name — TEXT NOT NULL
|
||||||
* - solana_key — TEXT NOT NULL
|
* - solana_key — TEXT NOT NULL
|
||||||
* - blockchain_key — TEXT NOT NULL
|
* - blockchain_key — TEXT NOT NULL
|
||||||
* - device_key — TEXT NOT NULL
|
* - client_key — TEXT NOT NULL
|
||||||
*/
|
*/
|
||||||
public class SolanaUserEntry {
|
public class SolanaUserEntry {
|
||||||
|
|
||||||
@ -27,7 +27,7 @@ public class SolanaUserEntry {
|
|||||||
private String blockchainKey;
|
private String blockchainKey;
|
||||||
|
|
||||||
/** Ключ устройства (публичный ключ устройства) */
|
/** Ключ устройства (публичный ключ устройства) */
|
||||||
private String deviceKey;
|
private String clientKey;
|
||||||
|
|
||||||
public SolanaUserEntry() {}
|
public SolanaUserEntry() {}
|
||||||
|
|
||||||
@ -35,12 +35,12 @@ public class SolanaUserEntry {
|
|||||||
String blockchainName,
|
String blockchainName,
|
||||||
String solanaKey,
|
String solanaKey,
|
||||||
String blockchainKey,
|
String blockchainKey,
|
||||||
String deviceKey) {
|
String clientKey) {
|
||||||
this.login = login;
|
this.login = login;
|
||||||
this.blockchainName = blockchainName;
|
this.blockchainName = blockchainName;
|
||||||
this.solanaKey = solanaKey;
|
this.solanaKey = solanaKey;
|
||||||
this.blockchainKey = blockchainKey;
|
this.blockchainKey = blockchainKey;
|
||||||
this.deviceKey = deviceKey;
|
this.clientKey = clientKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getLogin() { return login; }
|
public String getLogin() { return login; }
|
||||||
@ -55,13 +55,13 @@ public class SolanaUserEntry {
|
|||||||
public String getBlockchainKey() { return blockchainKey; }
|
public String getBlockchainKey() { return blockchainKey; }
|
||||||
public void setBlockchainKey(String blockchainKey) { this.blockchainKey = blockchainKey; }
|
public void setBlockchainKey(String blockchainKey) { this.blockchainKey = blockchainKey; }
|
||||||
|
|
||||||
public String getDeviceKey() { return deviceKey; }
|
public String getClientKey() { return clientKey; }
|
||||||
public void setDeviceKey(String deviceKey) { this.deviceKey = deviceKey; }
|
public void setClientKey(String clientKey) { this.clientKey = clientKey; }
|
||||||
|
|
||||||
// оставляю этот метод как утилиту (иногда удобно), но он работает только для deviceKey:
|
// оставляю этот метод как утилиту (иногда удобно), но он работает только для clientKey:
|
||||||
public byte[] getDeviceKeyByte() {
|
public byte[] getClientKeyByte() {
|
||||||
if (deviceKey == null) return null;
|
if (clientKey == null) return null;
|
||||||
String s = deviceKey.trim();
|
String s = clientKey.trim();
|
||||||
if (s.isEmpty()) return null;
|
if (s.isEmpty()) return null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@ -8,7 +8,7 @@ package shine.db.entities;
|
|||||||
* - param TEXT NOT NULL
|
* - param TEXT NOT NULL
|
||||||
* - time_ms INTEGER NOT NULL
|
* - time_ms INTEGER NOT NULL
|
||||||
* - value TEXT NOT NULL
|
* - value TEXT NOT NULL
|
||||||
* - device_key TEXT NULL
|
* - client_key TEXT NULL
|
||||||
* - signature TEXT NULL
|
* - signature TEXT NULL
|
||||||
*/
|
*/
|
||||||
public class UserParamEntry {
|
public class UserParamEntry {
|
||||||
@ -18,17 +18,17 @@ public class UserParamEntry {
|
|||||||
private long timeMs;
|
private long timeMs;
|
||||||
private String value;
|
private String value;
|
||||||
|
|
||||||
private String deviceKey;
|
private String clientKey;
|
||||||
private String signature;
|
private String signature;
|
||||||
|
|
||||||
public UserParamEntry() {}
|
public UserParamEntry() {}
|
||||||
|
|
||||||
public UserParamEntry(String login, String param, long timeMs, String value, String deviceKey, String signature) {
|
public UserParamEntry(String login, String param, long timeMs, String value, String clientKey, String signature) {
|
||||||
this.login = login;
|
this.login = login;
|
||||||
this.param = param;
|
this.param = param;
|
||||||
this.timeMs = timeMs;
|
this.timeMs = timeMs;
|
||||||
this.value = value;
|
this.value = value;
|
||||||
this.deviceKey = deviceKey;
|
this.clientKey = clientKey;
|
||||||
this.signature = signature;
|
this.signature = signature;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -44,8 +44,8 @@ public class UserParamEntry {
|
|||||||
public String getValue() { return value; }
|
public String getValue() { return value; }
|
||||||
public void setValue(String value) { this.value = value; }
|
public void setValue(String value) { this.value = value; }
|
||||||
|
|
||||||
public String getDeviceKey() { return deviceKey; }
|
public String getClientKey() { return clientKey; }
|
||||||
public void setDeviceKey(String deviceKey) { this.deviceKey = deviceKey; }
|
public void setClientKey(String clientKey) { this.clientKey = clientKey; }
|
||||||
|
|
||||||
public String getSignature() { return signature; }
|
public String getSignature() { return signature; }
|
||||||
public void setSignature(String signature) { this.signature = signature; }
|
public void setSignature(String signature) { this.signature = signature; }
|
||||||
|
|||||||
@ -1,20 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
OUTFILE="all_files.txt"
|
|
||||||
|
|
||||||
# очищаем или создаём файл
|
|
||||||
: > "$OUTFILE"
|
|
||||||
|
|
||||||
# собрать только *.java файлы и вывести их содержимое в файл
|
|
||||||
find . -type f -name "*.java" | sort | while read -r f; do
|
|
||||||
cat "$f" >> "$OUTFILE"
|
|
||||||
echo >> "$OUTFILE" # пустая строка-разделитель
|
|
||||||
done
|
|
||||||
|
|
||||||
# скопировать весь файл в буфер обмена (Wayland)
|
|
||||||
wl-copy < "$OUTFILE"
|
|
||||||
|
|
||||||
echo "Готово!"
|
|
||||||
echo "Все .java файлы собраны в $OUTFILE"
|
|
||||||
echo "Содержимое скопировано в буфер обмена (Wayland)"
|
|
||||||
File diff suppressed because it is too large
Load Diff
@ -1,20 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
OUTFILE="all_files.txt"
|
|
||||||
|
|
||||||
# очищаем или создаём файл
|
|
||||||
: > "$OUTFILE"
|
|
||||||
|
|
||||||
# собрать только *.java файлы и вывести их содержимое в файл
|
|
||||||
find . -type f -name "*.java" | sort | while read -r f; do
|
|
||||||
cat "$f" >> "$OUTFILE"
|
|
||||||
echo >> "$OUTFILE" # пустая строка-разделитель
|
|
||||||
done
|
|
||||||
|
|
||||||
# скопировать весь файл в буфер обмена (Wayland)
|
|
||||||
wl-copy < "$OUTFILE"
|
|
||||||
|
|
||||||
echo "Готово!"
|
|
||||||
echo "Все .java файлы собраны в $OUTFILE"
|
|
||||||
echo "Содержимое скопировано в буфер обмена (Wayland)"
|
|
||||||
File diff suppressed because it is too large
Load Diff
@ -1,20 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
OUTFILE="all_files.txt"
|
|
||||||
|
|
||||||
# очищаем или создаём файл
|
|
||||||
: > "$OUTFILE"
|
|
||||||
|
|
||||||
# собрать только *.java файлы и вывести их содержимое в файл
|
|
||||||
find . -type f -name "*.java" | sort | while read -r f; do
|
|
||||||
cat "$f" >> "$OUTFILE"
|
|
||||||
echo >> "$OUTFILE" # пустая строка-разделитель
|
|
||||||
done
|
|
||||||
|
|
||||||
# скопировать весь файл в буфер обмена (Wayland)
|
|
||||||
wl-copy < "$OUTFILE"
|
|
||||||
|
|
||||||
echo "Готово!"
|
|
||||||
echo "Все .java файлы собраны в $OUTFILE"
|
|
||||||
echo "Содержимое скопировано в буфер обмена (Wayland)"
|
|
||||||
@ -10,7 +10,7 @@ import shine.db.entities.ActiveSessionEntry;
|
|||||||
*
|
*
|
||||||
* Важно (v2):
|
* Важно (v2):
|
||||||
* - Авторизация всегда 2 шага:
|
* - Авторизация всегда 2 шага:
|
||||||
* A) Создание новой сессии через deviceKey:
|
* A) Создание новой сессии через clientKey:
|
||||||
* AuthChallenge(login) -> ctx.authNonce
|
* AuthChallenge(login) -> ctx.authNonce
|
||||||
* CreateAuthSession(...) -> ctx.AUTH_STATUS_USER + ctx.activeSession
|
* CreateAuthSession(...) -> ctx.AUTH_STATUS_USER + ctx.activeSession
|
||||||
*
|
*
|
||||||
@ -39,7 +39,7 @@ public class ConnectionContext {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Одноразовый nonce, выданный на шаге 1 (AuthChallenge),
|
* Одноразовый nonce, выданный на шаге 1 (AuthChallenge),
|
||||||
* используется на шаге CreateAuthSession для проверки подписи deviceKey.
|
* используется на шаге CreateAuthSession для проверки подписи clientKey.
|
||||||
*/
|
*/
|
||||||
private String authNonce;
|
private String authNonce;
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -1,20 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
OUTFILE="all_files.txt"
|
|
||||||
|
|
||||||
# очищаем или создаём файл
|
|
||||||
: > "$OUTFILE"
|
|
||||||
|
|
||||||
# собрать только *.java файлы и вывести их содержимое в файл
|
|
||||||
find . -type f -name "*.java" | sort | while read -r f; do
|
|
||||||
cat "$f" >> "$OUTFILE"
|
|
||||||
echo >> "$OUTFILE" # пустая строка-разделитель
|
|
||||||
done
|
|
||||||
|
|
||||||
# скопировать весь файл в буфер обмена (Wayland)
|
|
||||||
wl-copy < "$OUTFILE"
|
|
||||||
|
|
||||||
echo "Готово!"
|
|
||||||
echo "Все .java файлы собраны в $OUTFILE"
|
|
||||||
echo "Содержимое скопировано в буфер обмена (Wayland)"
|
|
||||||
@ -1,140 +0,0 @@
|
|||||||
package server.logic.ws_protocol.JSON.entyties;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Базовый класс для всех событий (event).
|
|
||||||
* Общие поля: op и payload.
|
|
||||||
*.
|
|
||||||
* Формат JSON (event):
|
|
||||||
* {
|
|
||||||
* "op": "...",
|
|
||||||
* "payload": { ... }
|
|
||||||
* }
|
|
||||||
*/
|
|
||||||
public abstract class Net_Event {
|
|
||||||
|
|
||||||
/** Имя операции / события (op). */
|
|
||||||
private String op;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Произвольные данные.
|
|
||||||
* В JSON это поле "payload".
|
|
||||||
*/
|
|
||||||
private Object payload;
|
|
||||||
|
|
||||||
// --- getters / setters ---
|
|
||||||
|
|
||||||
public String getOp() {
|
|
||||||
return op;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setOp(String op) {
|
|
||||||
this.op = op;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Object getPayload() {
|
|
||||||
return payload;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setPayload(Object payload) {
|
|
||||||
this.payload = payload;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
package server.logic.ws_protocol.JSON.entyties;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Ответ с ошибкой (любой отказ).
|
|
||||||
*.
|
|
||||||
* В payload будет:
|
|
||||||
* {
|
|
||||||
* "code": "...",
|
|
||||||
* "message": "..."
|
|
||||||
* }
|
|
||||||
*/
|
|
||||||
public class Net_Exception_Response extends Net_Response {
|
|
||||||
|
|
||||||
private String code;
|
|
||||||
private String message;
|
|
||||||
|
|
||||||
public String getCode() {
|
|
||||||
return code;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setCode(String code) {
|
|
||||||
this.code = code;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getMessage() {
|
|
||||||
return message;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setMessage(String message) {
|
|
||||||
this.message = message;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
package server.logic.ws_protocol.JSON.entyties;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Базовый класс для всех запросов (client → server).
|
|
||||||
*.
|
|
||||||
* Наследуется от NetEvent и добавляет requestId.
|
|
||||||
*.
|
|
||||||
* Формат JSON (request):
|
|
||||||
* {
|
|
||||||
* "op": "...",
|
|
||||||
* "requestId": "...",
|
|
||||||
* "payload": { ... }
|
|
||||||
* }
|
|
||||||
*/
|
|
||||||
public abstract class Net_Request extends Net_Event {
|
|
||||||
|
|
||||||
/** Идентификатор запроса, чтобы связать запрос и ответ. */
|
|
||||||
private String requestId;
|
|
||||||
|
|
||||||
// --- getters / setters ---
|
|
||||||
|
|
||||||
public String getRequestId() {
|
|
||||||
return requestId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setRequestId(String requestId) {
|
|
||||||
this.requestId = requestId;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
package server.logic.ws_protocol.JSON.entyties;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Базовый класс для всех ответов (server → client).
|
|
||||||
*.
|
|
||||||
* Наследуется от NetRequest и добавляет status.
|
|
||||||
*.
|
|
||||||
* Формат JSON (response):
|
|
||||||
* {
|
|
||||||
* "op": "...",
|
|
||||||
* "requestId": "...",
|
|
||||||
* "status": 200,
|
|
||||||
* "payload": { ... } // и для успеха, и для ошибки
|
|
||||||
* }
|
|
||||||
*/
|
|
||||||
public abstract class Net_Response extends Net_Request {
|
|
||||||
|
|
||||||
/** Статус результата (200 — успех, любое другое значение — ошибка). */
|
|
||||||
private int status;
|
|
||||||
|
|
||||||
// --- getters / setters ---
|
|
||||||
|
|
||||||
public int getStatus() {
|
|
||||||
return status;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setStatus(int status) {
|
|
||||||
this.status = status;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isOk() {
|
|
||||||
return status == 200;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
OUTFILE="all_files.txt"
|
|
||||||
|
|
||||||
# очищаем или создаём файл
|
|
||||||
: > "$OUTFILE"
|
|
||||||
|
|
||||||
# собрать только *.java файлы и вывести их содержимое в файл
|
|
||||||
find . -type f -name "*.java" | sort | while read -r f; do
|
|
||||||
cat "$f" >> "$OUTFILE"
|
|
||||||
echo >> "$OUTFILE" # пустая строка-разделитель
|
|
||||||
done
|
|
||||||
|
|
||||||
# скопировать весь файл в буфер обмена (Wayland)
|
|
||||||
wl-copy < "$OUTFILE"
|
|
||||||
|
|
||||||
echo "Готово!"
|
|
||||||
echo "Все .java файлы собраны в $OUTFILE"
|
|
||||||
echo "Содержимое скопировано в буфер обмена (Wayland)"
|
|
||||||
File diff suppressed because it is too large
Load Diff
@ -20,9 +20,9 @@ import java.security.SecureRandom;
|
|||||||
* AuthChallenge (v2) — шаг 1 создания новой сессии.
|
* AuthChallenge (v2) — шаг 1 создания новой сессии.
|
||||||
*
|
*
|
||||||
* Логика авторизации (v2):
|
* Логика авторизации (v2):
|
||||||
* - Создание новой сессии возможно ТОЛЬКО через deviceKey пользователя.
|
* - Создание новой сессии возможно ТОЛЬКО через clientKey пользователя.
|
||||||
* - Этот handler выдаёт одноразовый authNonce, который клиент использует во втором шаге:
|
* - Этот handler выдаёт одноразовый authNonce, который клиент использует во втором шаге:
|
||||||
* CreateAuthSession(..., signature(deviceKey, AUTH_CREATE_SESSION:...))
|
* CreateAuthSession(..., signature(clientKey, AUTH_CREATE_SESSION:...))
|
||||||
*
|
*
|
||||||
* Что делает:
|
* Что делает:
|
||||||
* 1) Проверяет login.
|
* 1) Проверяет login.
|
||||||
|
|||||||
@ -30,7 +30,7 @@ import java.security.SecureRandom;
|
|||||||
import java.sql.SQLException;
|
import java.sql.SQLException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* CreateAuthSession (v2) — шаг 2 создания новой сессии (ТОЛЬКО deviceKey).
|
* CreateAuthSession (v2) — шаг 2 создания новой сессии (ТОЛЬКО clientKey).
|
||||||
*
|
*
|
||||||
* Логика авторизации (v2):
|
* Логика авторизации (v2):
|
||||||
* - Создание сессии: AuthChallenge(login) -> authNonce -> CreateAuthSession(...)
|
* - Создание сессии: AuthChallenge(login) -> authNonce -> CreateAuthSession(...)
|
||||||
@ -38,7 +38,7 @@ import java.sql.SQLException;
|
|||||||
* отправляет на сервер sessionKey целиком одной строкой.
|
* отправляет на сервер sessionKey целиком одной строкой.
|
||||||
* - Сервер сохраняет sessionKey в active_sessions.session_key как есть.
|
* - Сервер сохраняет sessionKey в active_sessions.session_key как есть.
|
||||||
*
|
*
|
||||||
* Подпись deviceKey (Ed25519) проверяется над строкой (UTF-8):
|
* Подпись clientKey (Ed25519) проверяется над строкой (UTF-8):
|
||||||
* AUTH_CREATE_SESSION:{login}:{sessionKey}:{storagePwd}:{timeMs}:{authNonce}
|
* AUTH_CREATE_SESSION:{login}:{sessionKey}:{storagePwd}:{timeMs}:{authNonce}
|
||||||
*
|
*
|
||||||
* На выходе:
|
* На выходе:
|
||||||
@ -226,15 +226,15 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler {
|
|||||||
}
|
}
|
||||||
String clientPlatform = AuthSessionTypeSupport.normalizeClientPlatform(req.getClientPlatform());
|
String clientPlatform = AuthSessionTypeSupport.normalizeClientPlatform(req.getClientPlatform());
|
||||||
|
|
||||||
String deviceKeyFromDb = user.getDeviceKey();
|
String clientKeyFromDb = user.getClientKey();
|
||||||
if (deviceKeyFromDb == null || deviceKeyFromDb.isBlank()) {
|
if (clientKeyFromDb == null || clientKeyFromDb.isBlank()) {
|
||||||
Net_Response err = NetExceptionResponseFactory.error(
|
Net_Response err = NetExceptionResponseFactory.error(
|
||||||
req,
|
req,
|
||||||
WireCodes.Status.BAD_REQUEST,
|
WireCodes.Status.BAD_REQUEST,
|
||||||
"NO_DEVICE_KEY",
|
"NO_DEVICE_KEY",
|
||||||
"Отсутствует deviceKey у пользователя"
|
"Отсутствует clientKey у пользователя"
|
||||||
);
|
);
|
||||||
closeConnectionAfterErrorResponse(ctx, 4001, "Auth failed: no deviceKey");
|
closeConnectionAfterErrorResponse(ctx, 4001, "Auth failed: no clientKey");
|
||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -261,28 +261,28 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler {
|
|||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
|
|
||||||
String deviceKeyFromReq = req.getDeviceKey();
|
String clientKeyFromReq = req.getClientKey();
|
||||||
if (deviceKeyFromReq == null || deviceKeyFromReq.isBlank()) {
|
if (clientKeyFromReq == null || clientKeyFromReq.isBlank()) {
|
||||||
Net_Response err = NetExceptionResponseFactory.error(
|
Net_Response err = NetExceptionResponseFactory.error(
|
||||||
req,
|
req,
|
||||||
WireCodes.Status.BAD_REQUEST,
|
WireCodes.Status.BAD_REQUEST,
|
||||||
"EMPTY_DEVICE_KEY",
|
"EMPTY_DEVICE_KEY",
|
||||||
"Пустой deviceKey"
|
"Пустой clientKey"
|
||||||
);
|
);
|
||||||
closeConnectionAfterErrorResponse(ctx, 4001, "Auth failed: empty deviceKey");
|
closeConnectionAfterErrorResponse(ctx, 4001, "Auth failed: empty clientKey");
|
||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
deviceKeyFromReq = deviceKeyFromReq.trim();
|
clientKeyFromReq = clientKeyFromReq.trim();
|
||||||
|
|
||||||
// TODO: для ротации device_key стоит дополнительно сверять актуальное значение через Solana.
|
// TODO: для ротации client_key стоит дополнительно сверять актуальное значение через Solana.
|
||||||
if (!deviceKeyFromReq.equals(deviceKeyFromDb)) {
|
if (!clientKeyFromReq.equals(clientKeyFromDb)) {
|
||||||
Net_Response err = NetExceptionResponseFactory.error(
|
Net_Response err = NetExceptionResponseFactory.error(
|
||||||
req,
|
req,
|
||||||
WireCodes.Status.UNVERIFIED,
|
WireCodes.Status.UNVERIFIED,
|
||||||
"DEVICE_KEY_NOT_ACTUAL",
|
"DEVICE_KEY_NOT_ACTUAL",
|
||||||
"device_key не соответствует актуальной версии"
|
"client_key не соответствует актуальной версии"
|
||||||
);
|
);
|
||||||
closeConnectionAfterErrorResponse(ctx, 4001, "Auth failed: device key mismatch");
|
closeConnectionAfterErrorResponse(ctx, 4001, "Auth failed: client key mismatch");
|
||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -294,7 +294,7 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler {
|
|||||||
storagePwd,
|
storagePwd,
|
||||||
authNonce,
|
authNonce,
|
||||||
timeMs,
|
timeMs,
|
||||||
deviceKeyFromDb,
|
clientKeyFromDb,
|
||||||
signatureB64
|
signatureB64
|
||||||
);
|
);
|
||||||
} catch (UnsupportedOperationException ex) {
|
} catch (UnsupportedOperationException ex) {
|
||||||
@ -302,9 +302,9 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler {
|
|||||||
req,
|
req,
|
||||||
422,
|
422,
|
||||||
"UNSUPPORTED_KEY_ALGORITHM",
|
"UNSUPPORTED_KEY_ALGORITHM",
|
||||||
"deviceKey algorithm is not supported"
|
"clientKey algorithm is not supported"
|
||||||
);
|
);
|
||||||
closeConnectionAfterErrorResponse(ctx, 4001, "Auth failed: unsupported device key algorithm");
|
closeConnectionAfterErrorResponse(ctx, 4001, "Auth failed: unsupported client key algorithm");
|
||||||
return err;
|
return err;
|
||||||
} catch (IllegalArgumentException ex) {
|
} catch (IllegalArgumentException ex) {
|
||||||
Net_Response err = NetExceptionResponseFactory.error(
|
Net_Response err = NetExceptionResponseFactory.error(
|
||||||
@ -440,11 +440,11 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler {
|
|||||||
String storagePwd,
|
String storagePwd,
|
||||||
String authNonce,
|
String authNonce,
|
||||||
long timeMs,
|
long timeMs,
|
||||||
String deviceKey,
|
String clientKey,
|
||||||
String signatureB64
|
String signatureB64
|
||||||
) throws IllegalArgumentException {
|
) throws IllegalArgumentException {
|
||||||
|
|
||||||
byte[] publicKey32 = AuthKeyUtils.parseEd25519PublicKey(deviceKey, "deviceKey");
|
byte[] publicKey32 = AuthKeyUtils.parseEd25519PublicKey(clientKey, "clientKey");
|
||||||
byte[] signature64 = Base64Ws.decodeLen(signatureB64, 64, "signatureB64");
|
byte[] signature64 = Base64Ws.decodeLen(signatureB64, 64, "signatureB64");
|
||||||
|
|
||||||
String preimageStr = "AUTH_CREATE_SESSION:"
|
String preimageStr = "AUTH_CREATE_SESSION:"
|
||||||
|
|||||||
@ -48,9 +48,9 @@ public final class SolanaUserPdaImportService {
|
|||||||
boolean inserted = UserCreateDAO.getInstance().insertUserWithBlockchain(
|
boolean inserted = UserCreateDAO.getInstance().insertUserWithBlockchain(
|
||||||
parsed.login,
|
parsed.login,
|
||||||
parsed.blockchainName,
|
parsed.blockchainName,
|
||||||
parsed.deviceKeyB64, // в текущей модели solanaKey = deviceKey
|
parsed.clientKeyB64, // в текущей модели solanaKey = clientKey
|
||||||
parsed.blockchainKeyB64,
|
parsed.blockchainKeyB64,
|
||||||
parsed.deviceKeyB64,
|
parsed.clientKeyB64,
|
||||||
sizeLimit,
|
sizeLimit,
|
||||||
now
|
now
|
||||||
);
|
);
|
||||||
@ -158,7 +158,7 @@ public final class SolanaUserPdaImportService {
|
|||||||
int blocksCount = u8(raw, c++);
|
int blocksCount = u8(raw, c++);
|
||||||
String blockchainName = null;
|
String blockchainName = null;
|
||||||
byte[] blockchainKey32 = null;
|
byte[] blockchainKey32 = null;
|
||||||
byte[] deviceKey32 = null;
|
byte[] clientKey32 = null;
|
||||||
long paidLimitBytes = 0L;
|
long paidLimitBytes = 0L;
|
||||||
List<ParsedSessionRecord> sessions = new ArrayList<>();
|
List<ParsedSessionRecord> sessions = new ArrayList<>();
|
||||||
|
|
||||||
@ -170,7 +170,7 @@ public final class SolanaUserPdaImportService {
|
|||||||
if (blockType == 1) {
|
if (blockType == 1) {
|
||||||
c += 32;
|
c += 32;
|
||||||
} else if (blockType == 2) {
|
} else if (blockType == 2) {
|
||||||
deviceKey32 = slice(raw, c, 32);
|
clientKey32 = slice(raw, c, 32);
|
||||||
c += 32;
|
c += 32;
|
||||||
} else if (blockType == 3) {
|
} else if (blockType == 3) {
|
||||||
int count = u8(raw, c++);
|
int count = u8(raw, c++);
|
||||||
@ -245,12 +245,12 @@ public final class SolanaUserPdaImportService {
|
|||||||
if (c > recordLen) return null;
|
if (c > recordLen) return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (blockchainName == null || blockchainKey32 == null || deviceKey32 == null) return null;
|
if (blockchainName == null || blockchainKey32 == null || clientKey32 == null) return null;
|
||||||
return new ParsedSolanaUser(
|
return new ParsedSolanaUser(
|
||||||
login,
|
login,
|
||||||
blockchainName,
|
blockchainName,
|
||||||
Base64.getEncoder().encodeToString(blockchainKey32),
|
Base64.getEncoder().encodeToString(blockchainKey32),
|
||||||
Base64.getEncoder().encodeToString(deviceKey32),
|
Base64.getEncoder().encodeToString(clientKey32),
|
||||||
paidLimitBytes,
|
paidLimitBytes,
|
||||||
sessions
|
sessions
|
||||||
);
|
);
|
||||||
@ -318,7 +318,7 @@ public final class SolanaUserPdaImportService {
|
|||||||
String login,
|
String login,
|
||||||
String blockchainName,
|
String blockchainName,
|
||||||
String blockchainKeyB64,
|
String blockchainKeyB64,
|
||||||
String deviceKeyB64,
|
String clientKeyB64,
|
||||||
long paidLimitBytes,
|
long paidLimitBytes,
|
||||||
List<ParsedSessionRecord> sessions
|
List<ParsedSessionRecord> sessions
|
||||||
) {}
|
) {}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -1,20 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
OUTFILE="all_files.txt"
|
|
||||||
|
|
||||||
# очищаем или создаём файл
|
|
||||||
: > "$OUTFILE"
|
|
||||||
|
|
||||||
# собрать только *.java файлы и вывести их содержимое в файл
|
|
||||||
find . -type f -name "*.java" | sort | while read -r f; do
|
|
||||||
cat "$f" >> "$OUTFILE"
|
|
||||||
echo >> "$OUTFILE" # пустая строка-разделитель
|
|
||||||
done
|
|
||||||
|
|
||||||
# скопировать весь файл в буфер обмена (Wayland)
|
|
||||||
wl-copy < "$OUTFILE"
|
|
||||||
|
|
||||||
echo "Готово!"
|
|
||||||
echo "Все .java файлы собраны в $OUTFILE"
|
|
||||||
echo "Содержимое скопировано в буфер обмена (Wayland)"
|
|
||||||
@ -35,7 +35,7 @@
|
|||||||
|
|
||||||
1. Добавление пользователя (AddUser)
|
1. Добавление пользователя (AddUser)
|
||||||
|
|
||||||
Назначение: создать локальную запись пользователя с двумя ключами — solanaKey и deviceKey.
|
Назначение: создать локальную запись пользователя с двумя ключами — solanaKey и clientKey.
|
||||||
|
|
||||||
📤 Запрос клиента
|
📤 Запрос клиента
|
||||||
{
|
{
|
||||||
@ -46,7 +46,7 @@
|
|||||||
"loginId": 100212,
|
"loginId": 100212,
|
||||||
"bchId": 4222,
|
"bchId": 4222,
|
||||||
"solanaKey": "BASE64_LOGIN_KEY",
|
"solanaKey": "BASE64_LOGIN_KEY",
|
||||||
"deviceKey": "BASE64_DEVICE_KEY",
|
"clientKey": "BASE64_DEVICE_KEY",
|
||||||
"bchLimit": 1000000
|
"bchLimit": 1000000
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -62,7 +62,7 @@ login TEXT NOT NULL,
|
|||||||
loginId INTEGER PRIMARY KEY,
|
loginId INTEGER PRIMARY KEY,
|
||||||
bchId INTEGER NOT NULL,
|
bchId INTEGER NOT NULL,
|
||||||
solanaKey TEXT,
|
solanaKey TEXT,
|
||||||
deviceKey TEXT,
|
clientKey TEXT,
|
||||||
bchLimit INTEGER
|
bchLimit INTEGER
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -118,7 +118,7 @@ timeMs — timestamp клиента (UTC).
|
|||||||
|
|
||||||
sessionPwd — строка с шага 1.
|
sessionPwd — строка с шага 1.
|
||||||
|
|
||||||
signatureB64 — Ed25519‐подпись preimage приватным ключом deviceKey.
|
signatureB64 — Ed25519‐подпись preimage приватным ключом clientKey.
|
||||||
|
|
||||||
📤 Запрос клиента
|
📤 Запрос клиента
|
||||||
{
|
{
|
||||||
@ -141,7 +141,7 @@ signatureB64 — Ed25519‐подпись preimage приватным ключо
|
|||||||
|
|
||||||
Восстанавливает preimage.
|
Восстанавливает preimage.
|
||||||
|
|
||||||
Находит deviceKey пользователя.
|
Находит clientKey пользователя.
|
||||||
|
|
||||||
Проверяет Ed25519-подпись.
|
Проверяет Ed25519-подпись.
|
||||||
|
|
||||||
|
|||||||
@ -3,13 +3,13 @@ package server.logic.ws_protocol.JSON.handlers.auth.entyties;
|
|||||||
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Шаг 2 (v2): создание новой сессии ТОЛЬКО через deviceKey.
|
* Шаг 2 (v2): создание новой сессии ТОЛЬКО через clientKey.
|
||||||
*
|
*
|
||||||
* Шаги:
|
* Шаги:
|
||||||
* 1) AuthChallenge(login) -> authNonce
|
* 1) AuthChallenge(login) -> authNonce
|
||||||
* 2) CreateAuthSession(login, sessionKey, storagePwd, timeMs, authNonce, deviceKey, signatureB64, clientInfo)
|
* 2) CreateAuthSession(login, sessionKey, storagePwd, timeMs, authNonce, clientKey, signatureB64, clientInfo)
|
||||||
*
|
*
|
||||||
* Подпись deviceKey делается над строкой (UTF-8):
|
* Подпись clientKey делается над строкой (UTF-8):
|
||||||
* AUTH_CREATE_SESSION:{login}:{sessionKey}:{storagePwd}:{timeMs}:{authNonce}
|
* AUTH_CREATE_SESSION:{login}:{sessionKey}:{storagePwd}:{timeMs}:{authNonce}
|
||||||
*
|
*
|
||||||
* Важно:
|
* Важно:
|
||||||
@ -33,9 +33,9 @@ public class Net_CreateAuthSession_Request extends Net_Request {
|
|||||||
private String authNonce;
|
private String authNonce;
|
||||||
|
|
||||||
/** Публичный ключ устройства пользователя. */
|
/** Публичный ключ устройства пользователя. */
|
||||||
private String deviceKey;
|
private String clientKey;
|
||||||
|
|
||||||
/** Подпись Ed25519(deviceKey) над строкой AUTH_CREATE_SESSION:... (base64). */
|
/** Подпись Ed25519(clientKey) над строкой AUTH_CREATE_SESSION:... (base64). */
|
||||||
private String signatureB64;
|
private String signatureB64;
|
||||||
|
|
||||||
/** Краткая строка от клиента (до 50 символов) с описанием устройства/клиента. */
|
/** Краткая строка от клиента (до 50 символов) с описанием устройства/клиента. */
|
||||||
@ -87,12 +87,12 @@ public class Net_CreateAuthSession_Request extends Net_Request {
|
|||||||
this.authNonce = authNonce;
|
this.authNonce = authNonce;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getDeviceKey() {
|
public String getClientKey() {
|
||||||
return deviceKey;
|
return clientKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setDeviceKey(String deviceKey) {
|
public void setClientKey(String clientKey) {
|
||||||
this.deviceKey = deviceKey;
|
this.clientKey = clientKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getSignatureB64() {
|
public String getSignatureB64() {
|
||||||
|
|||||||
@ -1,20 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
OUTFILE="all_files.txt"
|
|
||||||
|
|
||||||
# очищаем или создаём файл
|
|
||||||
: > "$OUTFILE"
|
|
||||||
|
|
||||||
# собрать только *.java файлы и вывести их содержимое в файл
|
|
||||||
find . -type f -name "*.java" | sort | while read -r f; do
|
|
||||||
cat "$f" >> "$OUTFILE"
|
|
||||||
echo >> "$OUTFILE" # пустая строка-разделитель
|
|
||||||
done
|
|
||||||
|
|
||||||
# скопировать весь файл в буфер обмена (Wayland)
|
|
||||||
wl-copy < "$OUTFILE"
|
|
||||||
|
|
||||||
echo "Готово!"
|
|
||||||
echo "Все .java файлы собраны в $OUTFILE"
|
|
||||||
echo "Содержимое скопировано в буфер обмена (Wayland)"
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
OUTFILE="all_files.txt"
|
|
||||||
|
|
||||||
# очищаем или создаём файл
|
|
||||||
: > "$OUTFILE"
|
|
||||||
|
|
||||||
# собрать только *.java файлы и вывести их содержимое в файл
|
|
||||||
find . -type f -name "*.java" | sort | while read -r f; do
|
|
||||||
cat "$f" >> "$OUTFILE"
|
|
||||||
echo >> "$OUTFILE" # пустая строка-разделитель
|
|
||||||
done
|
|
||||||
|
|
||||||
# скопировать весь файл в буфер обмена (Wayland)
|
|
||||||
wl-copy < "$OUTFILE"
|
|
||||||
|
|
||||||
echo "Готово!"
|
|
||||||
echo "Все .java файлы собраны в $OUTFILE"
|
|
||||||
echo "Содержимое скопировано в буфер обмена (Wayland)"
|
|
||||||
@ -1,180 +0,0 @@
|
|||||||
package server.logic.ws_protocol.JSON.handlers.connections.entyties;
|
|
||||||
|
|
||||||
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Запрос GetFriendsLists — получить два списка "друзей" по connections_state.
|
|
||||||
*
|
|
||||||
* {
|
|
||||||
* "op": "GetFriendsLists",
|
|
||||||
* "requestId": "req-100",
|
|
||||||
* "payload": {
|
|
||||||
* "login": "anya"
|
|
||||||
* }
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
* Возвращает:
|
|
||||||
* - out_friends: кому login поставил FRIEND
|
|
||||||
* - in_friends: кто поставил FRIEND этому login
|
|
||||||
*
|
|
||||||
* ПРО ДОСТУП (на будущее):
|
|
||||||
* Сейчас (MVP) без ограничений. Позже можно ограничить видимость связей.
|
|
||||||
*/
|
|
||||||
public class Net_GetFriendsLists_Request extends Net_Request {
|
|
||||||
|
|
||||||
private String login;
|
|
||||||
|
|
||||||
public String getLogin() { return login; }
|
|
||||||
public void setLogin(String login) { this.login = login; }
|
|
||||||
}
|
|
||||||
package server.logic.ws_protocol.JSON.handlers.connections.entyties;
|
|
||||||
|
|
||||||
import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Ответ GetFriendsLists.
|
|
||||||
*
|
|
||||||
* {
|
|
||||||
* "op": "GetFriendsLists",
|
|
||||||
* "requestId": "req-100",
|
|
||||||
* "status": 200,
|
|
||||||
* "payload": {
|
|
||||||
* "login": "Anya", // канонический регистр из БД
|
|
||||||
* "out_friends": ["Bob", "Kate"], // кому login поставил FRIEND
|
|
||||||
* "in_friends": ["Alex", "Kate"] // кто поставил FRIEND login
|
|
||||||
* }
|
|
||||||
* }
|
|
||||||
*/
|
|
||||||
public class Net_GetFriendsLists_Response extends Net_Response {
|
|
||||||
|
|
||||||
private String login;
|
|
||||||
|
|
||||||
private List<String> out_friends = new ArrayList<>();
|
|
||||||
private List<String> in_friends = new ArrayList<>();
|
|
||||||
|
|
||||||
public String getLogin() { return login; }
|
|
||||||
public void setLogin(String login) { this.login = login; }
|
|
||||||
|
|
||||||
public List<String> getOut_friends() { return out_friends; }
|
|
||||||
public void setOut_friends(List<String> out_friends) { this.out_friends = out_friends; }
|
|
||||||
|
|
||||||
public List<String> getIn_friends() { return in_friends; }
|
|
||||||
public void setIn_friends(List<String> in_friends) { this.in_friends = in_friends; }
|
|
||||||
}
|
|
||||||
package server.logic.ws_protocol.JSON.handlers.connections;
|
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import server.logic.ws_protocol.JSON.ConnectionContext;
|
|
||||||
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
|
||||||
import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
|
||||||
import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler;
|
|
||||||
import server.logic.ws_protocol.JSON.handlers.connections.entyties.Net_GetFriendsLists_Request;
|
|
||||||
import server.logic.ws_protocol.JSON.handlers.connections.entyties.Net_GetFriendsLists_Response;
|
|
||||||
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
|
||||||
import server.logic.ws_protocol.WireCodes;
|
|
||||||
import shine.db.MsgSubType;
|
|
||||||
import shine.db.SqliteDbController;
|
|
||||||
import shine.db.dao.ConnectionsStateDAO;
|
|
||||||
|
|
||||||
import java.sql.Connection;
|
|
||||||
import java.sql.PreparedStatement;
|
|
||||||
import java.sql.ResultSet;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* GetFriendsLists — получить 2 списка:
|
|
||||||
* - out_friends: кому login поставил FRIEND
|
|
||||||
* - in_friends: кто поставил FRIEND этому login
|
|
||||||
*
|
|
||||||
* ВАЖНО:
|
|
||||||
* - login в запросе может быть любым регистром
|
|
||||||
* - в ответе возвращаем канонический регистр (как в solana_users.login)
|
|
||||||
*
|
|
||||||
* ПРИМЕЧАНИЕ:
|
|
||||||
* Таблица пользователей тут названа "solana_users". Если у тебя иначе — поменяй SQL.
|
|
||||||
*/
|
|
||||||
public class Net_GetFriendsLists_Handler implements JsonMessageHandler {
|
|
||||||
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(Net_GetFriendsLists_Handler.class);
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) {
|
|
||||||
Net_GetFriendsLists_Request req = (Net_GetFriendsLists_Request) baseRequest;
|
|
||||||
|
|
||||||
if (req.getLogin() == null || req.getLogin().isBlank()) {
|
|
||||||
return NetExceptionResponseFactory.error(
|
|
||||||
req,
|
|
||||||
WireCodes.Status.BAD_REQUEST,
|
|
||||||
"BAD_FIELDS",
|
|
||||||
"Некорректные поля: login"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
final String loginAnyCase = req.getLogin().trim();
|
|
||||||
|
|
||||||
try {
|
|
||||||
SqliteDbController db = SqliteDbController.getInstance();
|
|
||||||
ConnectionsStateDAO dao = ConnectionsStateDAO.getInstance();
|
|
||||||
|
|
||||||
try (Connection c = db.getConnection()) {
|
|
||||||
|
|
||||||
// 1) Канонизируем login через solana_users (NOCASE)
|
|
||||||
String canonicalLogin = findCanonicalLogin(c, loginAnyCase);
|
|
||||||
if (canonicalLogin == null) {
|
|
||||||
return NetExceptionResponseFactory.error(
|
|
||||||
req,
|
|
||||||
404,
|
|
||||||
"USER_NOT_FOUND",
|
|
||||||
"Пользователь не найден"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
int relType = (int) MsgSubType.CONNECTION_FRIEND;
|
|
||||||
|
|
||||||
// 2) Два списка (логины канонические)
|
|
||||||
List<String> outFriends = dao.listOutgoingByRelTypeCanonical(c, canonicalLogin, relType);
|
|
||||||
List<String> inFriends = dao.listIncomingByRelTypeCanonical(c, canonicalLogin, relType);
|
|
||||||
|
|
||||||
Net_GetFriendsLists_Response resp = new Net_GetFriendsLists_Response();
|
|
||||||
resp.setOp(req.getOp());
|
|
||||||
resp.setRequestId(req.getRequestId());
|
|
||||||
resp.setStatus(WireCodes.Status.OK);
|
|
||||||
|
|
||||||
resp.setLogin(canonicalLogin);
|
|
||||||
resp.setOut_friends(outFriends);
|
|
||||||
resp.setIn_friends(inFriends);
|
|
||||||
|
|
||||||
return resp;
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("❌ Internal error GetFriendsLists", e);
|
|
||||||
return NetExceptionResponseFactory.error(
|
|
||||||
req,
|
|
||||||
WireCodes.Status.INTERNAL_ERROR,
|
|
||||||
"INTERNAL_ERROR",
|
|
||||||
"Внутренняя ошибка сервера"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private String findCanonicalLogin(Connection c, String loginAnyCase) throws Exception {
|
|
||||||
String sql = """
|
|
||||||
SELECT login
|
|
||||||
FROM solana_users
|
|
||||||
WHERE login = ? COLLATE NOCASE
|
|
||||||
LIMIT 1
|
|
||||||
""";
|
|
||||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
|
||||||
ps.setString(1, loginAnyCase);
|
|
||||||
try (ResultSet rs = ps.executeQuery()) {
|
|
||||||
if (!rs.next()) return null;
|
|
||||||
return rs.getString("login");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
OUTFILE="all_files.txt"
|
|
||||||
|
|
||||||
# очищаем или создаём файл
|
|
||||||
: > "$OUTFILE"
|
|
||||||
|
|
||||||
# собрать только *.java файлы и вывести их содержимое в файл
|
|
||||||
find . -type f -name "*.java" | sort | while read -r f; do
|
|
||||||
cat "$f" >> "$OUTFILE"
|
|
||||||
echo >> "$OUTFILE" # пустая строка-разделитель
|
|
||||||
done
|
|
||||||
|
|
||||||
# скопировать весь файл в буфер обмена (Wayland)
|
|
||||||
wl-copy < "$OUTFILE"
|
|
||||||
|
|
||||||
echo "Готово!"
|
|
||||||
echo "Все .java файлы собраны в $OUTFILE"
|
|
||||||
echo "Содержимое скопировано в буфер обмена (Wayland)"
|
|
||||||
@ -62,7 +62,7 @@ public class Net_GetUser_Handler implements JsonMessageHandler {
|
|||||||
resp.setBlockchainName(u.getBlockchainName());
|
resp.setBlockchainName(u.getBlockchainName());
|
||||||
resp.setSolanaKey(u.getSolanaKey());
|
resp.setSolanaKey(u.getSolanaKey());
|
||||||
resp.setBlockchainKey(u.getBlockchainKey());
|
resp.setBlockchainKey(u.getBlockchainKey());
|
||||||
resp.setDeviceKey(u.getDeviceKey());
|
resp.setClientKey(u.getClientKey());
|
||||||
|
|
||||||
// Возвращаем актуальный курсор блокчейна и, если запись состояния потеряна,
|
// Возвращаем актуальный курсор блокчейна и, если запись состояния потеряна,
|
||||||
// автоматически восстанавливаем её для существующего пользователя.
|
// автоматически восстанавливаем её для существующего пользователя.
|
||||||
|
|||||||
@ -1,240 +0,0 @@
|
|||||||
package server.logic.ws_protocol.JSON.handlers.tempToTest.entyties;
|
|
||||||
|
|
||||||
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Запрос AddUser — временная/тестовая регистрация локального пользователя.
|
|
||||||
*
|
|
||||||
* Клиент отправляет:
|
|
||||||
*
|
|
||||||
* {
|
|
||||||
* "op": "AddUser",
|
|
||||||
* "requestId": "test-add-1",
|
|
||||||
* "payload": {
|
|
||||||
* "login": "anya",
|
|
||||||
* "blockchainName": "anya-001",
|
|
||||||
* "solanaKey": "base64-ed25519-public-key-login",
|
|
||||||
* "blockchainKey": "base64-ed25519-public-key-blockchain",
|
|
||||||
* "deviceKey": "base64-ed25519-public-key-device",
|
|
||||||
* "bchLimit": 1000000
|
|
||||||
* }
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
* Все поля лежат внутри payload.
|
|
||||||
*/
|
|
||||||
public class Net_AddUser_Request extends Net_Request {
|
|
||||||
|
|
||||||
private String login;
|
|
||||||
private String blockchainName;
|
|
||||||
|
|
||||||
/** Ключ пользователя Solana (публичный ключ логина) */
|
|
||||||
private String solanaKey;
|
|
||||||
|
|
||||||
/** Ключ блокчейна (публичный ключ блокчейна) */
|
|
||||||
private String blockchainKey;
|
|
||||||
|
|
||||||
/** Ключ устройства (публичный ключ устройства) */
|
|
||||||
private String deviceKey;
|
|
||||||
|
|
||||||
private Integer bchLimit;
|
|
||||||
|
|
||||||
public String getLogin() { return login; }
|
|
||||||
public void setLogin(String login) { this.login = login; }
|
|
||||||
|
|
||||||
public String getBlockchainName() { return blockchainName; }
|
|
||||||
public void setBlockchainName(String blockchainName) { this.blockchainName = blockchainName; }
|
|
||||||
|
|
||||||
public String getSolanaKey() { return solanaKey; }
|
|
||||||
public void setSolanaKey(String solanaKey) { this.solanaKey = solanaKey; }
|
|
||||||
|
|
||||||
public String getBlockchainKey() { return blockchainKey; }
|
|
||||||
public void setBlockchainKey(String blockchainKey) { this.blockchainKey = blockchainKey; }
|
|
||||||
|
|
||||||
public String getDeviceKey() { return deviceKey; }
|
|
||||||
public void setDeviceKey(String deviceKey) { this.deviceKey = deviceKey; }
|
|
||||||
|
|
||||||
public Integer getBchLimit() { return bchLimit; }
|
|
||||||
public void setBchLimit(Integer bchLimit) { this.bchLimit = bchLimit; }
|
|
||||||
}
|
|
||||||
package server.logic.ws_protocol.JSON.handlers.tempToTest.entyties;
|
|
||||||
|
|
||||||
import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Успешный ответ на AddUser.
|
|
||||||
*
|
|
||||||
* Сейчас дополнительных полей нет — достаточно status=200.
|
|
||||||
*
|
|
||||||
* Пример:
|
|
||||||
* {
|
|
||||||
* "op": "AddUser",
|
|
||||||
* "requestId": "test-add-1",
|
|
||||||
* "status": 200,
|
|
||||||
* "payload": { }
|
|
||||||
* }
|
|
||||||
*/
|
|
||||||
public class Net_AddUser_Response extends Net_Response {
|
|
||||||
// При необходимости сюда можно добавить, например, флаг created/updated и т.п.
|
|
||||||
}
|
|
||||||
package server.logic.ws_protocol.JSON.handlers.tempToTest;
|
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import server.logic.ws_protocol.JSON.ConnectionContext;
|
|
||||||
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
|
||||||
import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
|
||||||
import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler;
|
|
||||||
import server.logic.ws_protocol.JSON.handlers.tempToTest.entyties.Net_AddUser_Request;
|
|
||||||
import server.logic.ws_protocol.JSON.handlers.tempToTest.entyties.Net_AddUser_Response;
|
|
||||||
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
|
||||||
import server.logic.ws_protocol.WireCodes;
|
|
||||||
import shine.db.SqliteDbController;
|
|
||||||
import shine.db.dao.BlockchainStateDAO;
|
|
||||||
import shine.db.dao.SolanaUsersDAO;
|
|
||||||
import shine.db.entities.BlockchainStateEntry;
|
|
||||||
import shine.db.entities.SolanaUserEntry;
|
|
||||||
import utils.blockchain.BlockchainNameUtil;
|
|
||||||
|
|
||||||
import java.sql.Connection;
|
|
||||||
import java.sql.SQLException;
|
|
||||||
import java.util.Base64;
|
|
||||||
|
|
||||||
public class Net_AddUser_Handler implements JsonMessageHandler {
|
|
||||||
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(Net_AddUser_Handler.class);
|
|
||||||
|
|
||||||
/** TEST ONLY */
|
|
||||||
private static final int TEST_BCH_LIMIT = 1_000_000;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) {
|
|
||||||
Net_AddUser_Request req = (Net_AddUser_Request) baseRequest;
|
|
||||||
|
|
||||||
if (req.getLogin() == null || req.getLogin().isBlank()
|
|
||||||
|| req.getBlockchainName() == null || req.getBlockchainName().isBlank()
|
|
||||||
|| req.getSolanaKey() == null || req.getSolanaKey().isBlank()
|
|
||||||
|| req.getBlockchainKey() == null || req.getBlockchainKey().isBlank()
|
|
||||||
|| req.getDeviceKey() == null || req.getDeviceKey().isBlank()) {
|
|
||||||
|
|
||||||
return NetExceptionResponseFactory.error(
|
|
||||||
req,
|
|
||||||
WireCodes.Status.BAD_REQUEST,
|
|
||||||
"BAD_FIELDS",
|
|
||||||
"Некорректные поля: login/blockchainName/solanaKey/blockchainKey/deviceKey"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// blockchainName должен быть вида: <login>-NNN
|
|
||||||
if (!BlockchainNameUtil.isBlockchainNameMatchesLogin(req.getBlockchainName(), req.getLogin())) {
|
|
||||||
return NetExceptionResponseFactory.error(
|
|
||||||
req,
|
|
||||||
WireCodes.Status.BAD_REQUEST,
|
|
||||||
"BAD_BLOCKCHAIN_NAME",
|
|
||||||
"blockchainName должен быть вида <login>-NNN (пример: anya-001)"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
int limit = (req.getBchLimit() == null || req.getBchLimit() <= 0)
|
|
||||||
? TEST_BCH_LIMIT
|
|
||||||
: req.getBchLimit();
|
|
||||||
|
|
||||||
try {
|
|
||||||
byte[] blockchainKey32 = Base64.getDecoder().decode(req.getBlockchainKey());
|
|
||||||
if (blockchainKey32.length != 32) {
|
|
||||||
return NetExceptionResponseFactory.error(
|
|
||||||
req,
|
|
||||||
WireCodes.Status.BAD_REQUEST,
|
|
||||||
"BAD_BLOCKCHAIN_KEY",
|
|
||||||
"blockchainKey должен быть Base64(32 bytes)"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
SolanaUsersDAO usersDAO = SolanaUsersDAO.getInstance();
|
|
||||||
BlockchainStateDAO stateDAO = BlockchainStateDAO.getInstance();
|
|
||||||
|
|
||||||
SqliteDbController db = SqliteDbController.getInstance();
|
|
||||||
|
|
||||||
try (Connection c = db.getConnection()) {
|
|
||||||
c.setAutoCommit(false);
|
|
||||||
|
|
||||||
// 1. Проверяем, что пользователя нет
|
|
||||||
if (usersDAO.getByLogin(req.getLogin()) != null) {
|
|
||||||
return NetExceptionResponseFactory.error(
|
|
||||||
req,
|
|
||||||
409,
|
|
||||||
"USER_ALREADY_EXISTS",
|
|
||||||
"Пользователь с таким login уже существует"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Проверяем, что blockchain_state ещё нет
|
|
||||||
if (stateDAO.getByBlockchainName(req.getBlockchainName()) != null) {
|
|
||||||
return NetExceptionResponseFactory.error(
|
|
||||||
req,
|
|
||||||
409,
|
|
||||||
"BLOCKCHAIN_ALREADY_EXISTS",
|
|
||||||
"blockchain_state уже существует"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Создаём пользователя (solanaKey + deviceKey)
|
|
||||||
SolanaUserEntry user = new SolanaUserEntry(
|
|
||||||
req.getLogin(),
|
|
||||||
req.getSolanaKey(),
|
|
||||||
req.getDeviceKey()
|
|
||||||
);
|
|
||||||
|
|
||||||
usersDAO.insert(c, user);
|
|
||||||
|
|
||||||
// 4. Создаём INITIAL blockchain_state (blockchainKey)
|
|
||||||
BlockchainStateEntry st = new BlockchainStateEntry();
|
|
||||||
st.setBlockchainName(req.getBlockchainName());
|
|
||||||
st.setLogin(req.getLogin());
|
|
||||||
st.setBlockchainKey(req.getBlockchainKey()); // Base64(32)
|
|
||||||
st.setLastBlockNumber(-1);
|
|
||||||
st.setLastBlockHash(new byte[32]);
|
|
||||||
st.setFileSizeBytes(0);
|
|
||||||
st.setSizeLimit(limit);
|
|
||||||
st.setUpdatedAtMs(System.currentTimeMillis());
|
|
||||||
|
|
||||||
stateDAO.upsert(c, st);
|
|
||||||
|
|
||||||
c.commit();
|
|
||||||
}
|
|
||||||
|
|
||||||
Net_AddUser_Response resp = new Net_AddUser_Response();
|
|
||||||
resp.setOp(req.getOp());
|
|
||||||
resp.setRequestId(req.getRequestId());
|
|
||||||
resp.setStatus(WireCodes.Status.OK);
|
|
||||||
|
|
||||||
log.info("✅ AddUser ok: login={}, blockchainName={}, limit={}",
|
|
||||||
req.getLogin(), req.getBlockchainName(), limit);
|
|
||||||
|
|
||||||
return resp;
|
|
||||||
|
|
||||||
} catch (IllegalArgumentException e) {
|
|
||||||
return NetExceptionResponseFactory.error(
|
|
||||||
req,
|
|
||||||
WireCodes.Status.BAD_REQUEST,
|
|
||||||
"BAD_KEY_FORMAT",
|
|
||||||
e.getMessage()
|
|
||||||
);
|
|
||||||
} catch (SQLException e) {
|
|
||||||
log.error("❌ DB error AddUser", e);
|
|
||||||
return NetExceptionResponseFactory.error(
|
|
||||||
req,
|
|
||||||
WireCodes.Status.SERVER_DATA_ERROR,
|
|
||||||
"DB_ERROR",
|
|
||||||
"Ошибка БД"
|
|
||||||
);
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("❌ Internal error AddUser", e);
|
|
||||||
return NetExceptionResponseFactory.error(
|
|
||||||
req,
|
|
||||||
WireCodes.Status.INTERNAL_ERROR,
|
|
||||||
"INTERNAL_ERROR",
|
|
||||||
"Внутренняя ошибка сервера"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
OUTFILE="all_files.txt"
|
|
||||||
|
|
||||||
# очищаем или создаём файл
|
|
||||||
: > "$OUTFILE"
|
|
||||||
|
|
||||||
# собрать только *.java файлы и вывести их содержимое в файл
|
|
||||||
find . -type f -name "*.java" | sort | while read -r f; do
|
|
||||||
cat "$f" >> "$OUTFILE"
|
|
||||||
echo >> "$OUTFILE" # пустая строка-разделитель
|
|
||||||
done
|
|
||||||
|
|
||||||
# скопировать весь файл в буфер обмена (Wayland)
|
|
||||||
wl-copy < "$OUTFILE"
|
|
||||||
|
|
||||||
echo "Готово!"
|
|
||||||
echo "Все .java файлы собраны в $OUTFILE"
|
|
||||||
echo "Содержимое скопировано в буфер обмена (Wayland)"
|
|
||||||
@ -15,7 +15,7 @@ import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
|||||||
* "blockchainName": "anya-001",
|
* "blockchainName": "anya-001",
|
||||||
* "solanaKey": "base64-ed25519-public-key-login",
|
* "solanaKey": "base64-ed25519-public-key-login",
|
||||||
* "blockchainKey": "base64-ed25519-public-key-blockchain",
|
* "blockchainKey": "base64-ed25519-public-key-blockchain",
|
||||||
* "deviceKey": "base64-ed25519-public-key-device",
|
* "clientKey": "base64-ed25519-public-key-device",
|
||||||
* "bchLimit": 1000000
|
* "bchLimit": 1000000
|
||||||
* }
|
* }
|
||||||
* }
|
* }
|
||||||
@ -34,7 +34,7 @@ public class Net_AddUser_Request extends Net_Request {
|
|||||||
private String blockchainKey;
|
private String blockchainKey;
|
||||||
|
|
||||||
/** Ключ устройства (публичный ключ устройства) */
|
/** Ключ устройства (публичный ключ устройства) */
|
||||||
private String deviceKey;
|
private String clientKey;
|
||||||
|
|
||||||
private Integer bchLimit;
|
private Integer bchLimit;
|
||||||
|
|
||||||
@ -50,8 +50,8 @@ public class Net_AddUser_Request extends Net_Request {
|
|||||||
public String getBlockchainKey() { return blockchainKey; }
|
public String getBlockchainKey() { return blockchainKey; }
|
||||||
public void setBlockchainKey(String blockchainKey) { this.blockchainKey = blockchainKey; }
|
public void setBlockchainKey(String blockchainKey) { this.blockchainKey = blockchainKey; }
|
||||||
|
|
||||||
public String getDeviceKey() { return deviceKey; }
|
public String getClientKey() { return clientKey; }
|
||||||
public void setDeviceKey(String deviceKey) { this.deviceKey = deviceKey; }
|
public void setClientKey(String clientKey) { this.clientKey = clientKey; }
|
||||||
|
|
||||||
public Integer getBchLimit() { return bchLimit; }
|
public Integer getBchLimit() { return bchLimit; }
|
||||||
public void setBchLimit(Integer bchLimit) { this.bchLimit = bchLimit; }
|
public void setBchLimit(Integer bchLimit) { this.bchLimit = bchLimit; }
|
||||||
|
|||||||
@ -26,7 +26,7 @@ import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
|||||||
* "blockchainName": "anya-001",
|
* "blockchainName": "anya-001",
|
||||||
* "solanaKey": "...",
|
* "solanaKey": "...",
|
||||||
* "blockchainKey": "...",
|
* "blockchainKey": "...",
|
||||||
* "deviceKey": "..."
|
* "clientKey": "..."
|
||||||
* }
|
* }
|
||||||
* }
|
* }
|
||||||
*/
|
*/
|
||||||
@ -38,7 +38,7 @@ public class Net_GetUser_Response extends Net_Response {
|
|||||||
private String blockchainName;
|
private String blockchainName;
|
||||||
private String solanaKey;
|
private String solanaKey;
|
||||||
private String blockchainKey;
|
private String blockchainKey;
|
||||||
private String deviceKey;
|
private String clientKey;
|
||||||
private Integer serverLastGlobalNumber;
|
private Integer serverLastGlobalNumber;
|
||||||
private String serverLastGlobalHash;
|
private String serverLastGlobalHash;
|
||||||
private Long serverBlockchainSizeBytes;
|
private Long serverBlockchainSizeBytes;
|
||||||
@ -59,8 +59,8 @@ public class Net_GetUser_Response extends Net_Response {
|
|||||||
public String getBlockchainKey() { return blockchainKey; }
|
public String getBlockchainKey() { return blockchainKey; }
|
||||||
public void setBlockchainKey(String blockchainKey) { this.blockchainKey = blockchainKey; }
|
public void setBlockchainKey(String blockchainKey) { this.blockchainKey = blockchainKey; }
|
||||||
|
|
||||||
public String getDeviceKey() { return deviceKey; }
|
public String getClientKey() { return clientKey; }
|
||||||
public void setDeviceKey(String deviceKey) { this.deviceKey = deviceKey; }
|
public void setClientKey(String clientKey) { this.clientKey = clientKey; }
|
||||||
|
|
||||||
public Integer getServerLastGlobalNumber() { return serverLastGlobalNumber; }
|
public Integer getServerLastGlobalNumber() { return serverLastGlobalNumber; }
|
||||||
public void setServerLastGlobalNumber(Integer serverLastGlobalNumber) { this.serverLastGlobalNumber = serverLastGlobalNumber; }
|
public void setServerLastGlobalNumber(Integer serverLastGlobalNumber) { this.serverLastGlobalNumber = serverLastGlobalNumber; }
|
||||||
|
|||||||
@ -71,7 +71,7 @@ public class Net_GetUserParam_Handler implements JsonMessageHandler {
|
|||||||
resp.setParam(e.getParam());
|
resp.setParam(e.getParam());
|
||||||
resp.setTime_ms(e.getTimeMs());
|
resp.setTime_ms(e.getTimeMs());
|
||||||
resp.setValue(e.getValue());
|
resp.setValue(e.getValue());
|
||||||
resp.setDevice_key(e.getDeviceKey());
|
resp.setClient_key(e.getClientKey());
|
||||||
resp.setSignature(e.getSignature());
|
resp.setSignature(e.getSignature());
|
||||||
|
|
||||||
return resp;
|
return resp;
|
||||||
|
|||||||
@ -73,7 +73,7 @@ public class Net_ListUserParams_Handler implements JsonMessageHandler {
|
|||||||
it.setParam(e.getParam());
|
it.setParam(e.getParam());
|
||||||
it.setTime_ms(e.getTimeMs());
|
it.setTime_ms(e.getTimeMs());
|
||||||
it.setValue(e.getValue());
|
it.setValue(e.getValue());
|
||||||
it.setDevice_key(e.getDeviceKey());
|
it.setClient_key(e.getClientKey());
|
||||||
it.setSignature(e.getSignature());
|
it.setSignature(e.getSignature());
|
||||||
items.add(it);
|
items.add(it);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -28,8 +28,8 @@ import java.sql.SQLException;
|
|||||||
*
|
*
|
||||||
* Делает (MVP, без "сессий"):
|
* Делает (MVP, без "сессий"):
|
||||||
* 1) Проверка входных полей.
|
* 1) Проверка входных полей.
|
||||||
* 2) Проверка подписи Ed25519 по device_key.
|
* 2) Проверка подписи Ed25519 по client_key.
|
||||||
* 3) Проверка, что пользователь существует и что device_key принадлежит этому login.
|
* 3) Проверка, что пользователь существует и что client_key принадлежит этому login.
|
||||||
* 4) Атомарная запись в БД "только если time_ms новее" (UPSERT + WHERE).
|
* 4) Атомарная запись в БД "только если time_ms новее" (UPSERT + WHERE).
|
||||||
*
|
*
|
||||||
* ВАЖНО:
|
* ВАЖНО:
|
||||||
@ -50,14 +50,14 @@ public class Net_UpsertUserParam_Handler implements JsonMessageHandler {
|
|||||||
|| req.getParam() == null || req.getParam().isBlank()
|
|| req.getParam() == null || req.getParam().isBlank()
|
||||||
|| req.getTime_ms() == null || req.getTime_ms() <= 0
|
|| req.getTime_ms() == null || req.getTime_ms() <= 0
|
||||||
|| req.getValue() == null
|
|| req.getValue() == null
|
||||||
|| req.getDevice_key() == null || req.getDevice_key().isBlank()
|
|| req.getClient_key() == null || req.getClient_key().isBlank()
|
||||||
|| req.getSignature() == null || req.getSignature().isBlank()) {
|
|| req.getSignature() == null || req.getSignature().isBlank()) {
|
||||||
|
|
||||||
return NetExceptionResponseFactory.error(
|
return NetExceptionResponseFactory.error(
|
||||||
req,
|
req,
|
||||||
WireCodes.Status.BAD_REQUEST,
|
WireCodes.Status.BAD_REQUEST,
|
||||||
"BAD_FIELDS",
|
"BAD_FIELDS",
|
||||||
"Некорректные поля: login/param/time_ms/value/device_key/signature"
|
"Некорректные поля: login/param/time_ms/value/client_key/signature"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -65,7 +65,7 @@ public class Net_UpsertUserParam_Handler implements JsonMessageHandler {
|
|||||||
final String param = req.getParam().trim();
|
final String param = req.getParam().trim();
|
||||||
final long timeMs = req.getTime_ms();
|
final long timeMs = req.getTime_ms();
|
||||||
final String value = req.getValue();
|
final String value = req.getValue();
|
||||||
final String deviceKeyB64 = req.getDevice_key().trim();
|
final String clientKeyB64 = req.getClient_key().trim();
|
||||||
final String signatureB64 = req.getSignature().trim();
|
final String signatureB64 = req.getSignature().trim();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -73,14 +73,14 @@ public class Net_UpsertUserParam_Handler implements JsonMessageHandler {
|
|||||||
byte[] pubKey32;
|
byte[] pubKey32;
|
||||||
byte[] sig64;
|
byte[] sig64;
|
||||||
try {
|
try {
|
||||||
pubKey32 = Base64Ws.decodeLen(deviceKeyB64, 32, "device_key");
|
pubKey32 = Base64Ws.decodeLen(clientKeyB64, 32, "client_key");
|
||||||
sig64 = Base64Ws.decodeLen(signatureB64, 64, "signature");
|
sig64 = Base64Ws.decodeLen(signatureB64, 64, "signature");
|
||||||
} catch (IllegalArgumentException e) {
|
} catch (IllegalArgumentException e) {
|
||||||
return NetExceptionResponseFactory.error(
|
return NetExceptionResponseFactory.error(
|
||||||
req,
|
req,
|
||||||
WireCodes.Status.BAD_REQUEST,
|
WireCodes.Status.BAD_REQUEST,
|
||||||
"BAD_BASE64",
|
"BAD_BASE64",
|
||||||
"device_key/signature должны быть Base64"
|
"client_key/signature должны быть Base64"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -120,23 +120,23 @@ public class Net_UpsertUserParam_Handler implements JsonMessageHandler {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2) device key must match the user's stored deviceKey
|
// 2) client key must match the user's stored clientKey
|
||||||
String userDeviceKey = user.getDeviceKey();
|
String userClientKey = user.getClientKey();
|
||||||
if (userDeviceKey == null || userDeviceKey.isBlank()) {
|
if (userClientKey == null || userClientKey.isBlank()) {
|
||||||
return NetExceptionResponseFactory.error(
|
return NetExceptionResponseFactory.error(
|
||||||
req,
|
req,
|
||||||
WireCodes.Status.SERVER_DATA_ERROR,
|
WireCodes.Status.SERVER_DATA_ERROR,
|
||||||
"USER_DEVICE_KEY_EMPTY",
|
"USER_DEVICE_KEY_EMPTY",
|
||||||
"У пользователя не задан deviceKey в БД"
|
"У пользователя не задан clientKey в БД"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!userDeviceKey.trim().equals(deviceKeyB64)) {
|
if (!userClientKey.trim().equals(clientKeyB64)) {
|
||||||
return NetExceptionResponseFactory.error(
|
return NetExceptionResponseFactory.error(
|
||||||
req,
|
req,
|
||||||
403,
|
403,
|
||||||
"DEVICE_KEY_MISMATCH",
|
"DEVICE_KEY_MISMATCH",
|
||||||
"device_key не соответствует пользователю"
|
"client_key не соответствует пользователю"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -146,7 +146,7 @@ public class Net_UpsertUserParam_Handler implements JsonMessageHandler {
|
|||||||
param,
|
param,
|
||||||
timeMs,
|
timeMs,
|
||||||
value,
|
value,
|
||||||
deviceKeyB64,
|
clientKeyB64,
|
||||||
signatureB64
|
signatureB64
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -1,640 +0,0 @@
|
|||||||
package server.logic.ws_protocol.JSON.handlers.userParams.entyties;
|
|
||||||
|
|
||||||
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Запрос GetUserParam — получить один параметр пользователя.
|
|
||||||
*
|
|
||||||
* {
|
|
||||||
* "op": "GetUserParam",
|
|
||||||
* "requestId": "req-1",
|
|
||||||
* "payload": {
|
|
||||||
* "login": "anya",
|
|
||||||
* "param": "feed:lastSeenGlobal"
|
|
||||||
* }
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
* ПРО ДОСТУП (на будущее):
|
|
||||||
* ---------------------------------------------------------------------------------
|
|
||||||
* Сейчас (MVP) этот запрос не ограничивает просмотр параметров, т.к. проект в тестовом режиме.
|
|
||||||
* Позже, вероятно, потребуется ограничить: кто и какие параметры может читать (сессия/права).
|
|
||||||
* Но для MVP эти проверки не нужны.
|
|
||||||
* ---------------------------------------------------------------------------------
|
|
||||||
*/
|
|
||||||
public class Net_GetUserParam_Request extends Net_Request {
|
|
||||||
|
|
||||||
private String login;
|
|
||||||
private String param;
|
|
||||||
|
|
||||||
public String getLogin() { return login; }
|
|
||||||
public void setLogin(String login) { this.login = login; }
|
|
||||||
|
|
||||||
public String getParam() { return param; }
|
|
||||||
public void setParam(String param) { this.param = param; }
|
|
||||||
}
|
|
||||||
package server.logic.ws_protocol.JSON.handlers.userParams.entyties;
|
|
||||||
|
|
||||||
import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Ответ GetUserParam.
|
|
||||||
*
|
|
||||||
* Если найден:
|
|
||||||
* {
|
|
||||||
* "op": "GetUserParam",
|
|
||||||
* "requestId": "req-1",
|
|
||||||
* "status": 200,
|
|
||||||
* "payload": {
|
|
||||||
* "login": "anya",
|
|
||||||
* "param": "feed:lastSeenGlobal",
|
|
||||||
* "time_ms": 1736000000123,
|
|
||||||
* "value": "105",
|
|
||||||
* "device_key": "base64-32",
|
|
||||||
* "signature": "base64-64"
|
|
||||||
* }
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
* Если не найден:
|
|
||||||
* status=404, payload пустой.
|
|
||||||
*/
|
|
||||||
public class Net_GetUserParam_Response extends Net_Response {
|
|
||||||
|
|
||||||
private String login;
|
|
||||||
private String param;
|
|
||||||
private Long time_ms;
|
|
||||||
private String value;
|
|
||||||
private String device_key;
|
|
||||||
private String signature;
|
|
||||||
|
|
||||||
public String getLogin() { return login; }
|
|
||||||
public void setLogin(String login) { this.login = login; }
|
|
||||||
|
|
||||||
public String getParam() { return param; }
|
|
||||||
public void setParam(String param) { this.param = param; }
|
|
||||||
|
|
||||||
public Long getTime_ms() { return time_ms; }
|
|
||||||
public void setTime_ms(Long time_ms) { this.time_ms = time_ms; }
|
|
||||||
|
|
||||||
public String getValue() { return value; }
|
|
||||||
public void setValue(String value) { this.value = value; }
|
|
||||||
|
|
||||||
public String getDevice_key() { return device_key; }
|
|
||||||
public void setDevice_key(String device_key) { this.device_key = device_key; }
|
|
||||||
|
|
||||||
public String getSignature() { return signature; }
|
|
||||||
public void setSignature(String signature) { this.signature = signature; }
|
|
||||||
}
|
|
||||||
package server.logic.ws_protocol.JSON.handlers.userParams.entyties;
|
|
||||||
|
|
||||||
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Запрос ListUserParams — получить все сохранённые параметры пользователя.
|
|
||||||
*
|
|
||||||
* {
|
|
||||||
* "op": "ListUserParams",
|
|
||||||
* "requestId": "req-2",
|
|
||||||
* "payload": {
|
|
||||||
* "login": "anya"
|
|
||||||
* }
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
* ПРО ДОСТУП (на будущее):
|
|
||||||
* ---------------------------------------------------------------------------------
|
|
||||||
* Сейчас (MVP) запрос не ограничивает просмотр параметров.
|
|
||||||
* В будущем, вероятно, потребуется проверка сессии/прав: кто может читать параметры.
|
|
||||||
* Для MVP эти проверки не нужны.
|
|
||||||
* ---------------------------------------------------------------------------------
|
|
||||||
*/
|
|
||||||
public class Net_ListUserParams_Request extends Net_Request {
|
|
||||||
|
|
||||||
private String login;
|
|
||||||
|
|
||||||
public String getLogin() { return login; }
|
|
||||||
public void setLogin(String login) { this.login = login; }
|
|
||||||
}
|
|
||||||
package server.logic.ws_protocol.JSON.handlers.userParams.entyties;
|
|
||||||
|
|
||||||
import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Ответ ListUserParams — список всех параметров пользователя.
|
|
||||||
*
|
|
||||||
* {
|
|
||||||
* "op": "ListUserParams",
|
|
||||||
* "requestId": "req-2",
|
|
||||||
* "status": 200,
|
|
||||||
* "payload": {
|
|
||||||
* "login": "anya",
|
|
||||||
* "params": [
|
|
||||||
* {
|
|
||||||
* "login": "anya",
|
|
||||||
* "param": "feed:lastSeenGlobal",
|
|
||||||
* "time_ms": 1736000000123,
|
|
||||||
* "value": "105",
|
|
||||||
* "device_key": "base64-32",
|
|
||||||
* "signature": "base64-64"
|
|
||||||
* },
|
|
||||||
* ...
|
|
||||||
* ]
|
|
||||||
* }
|
|
||||||
* }
|
|
||||||
*/
|
|
||||||
public class Net_ListUserParams_Response extends Net_Response {
|
|
||||||
|
|
||||||
private String login;
|
|
||||||
private List<Item> params = new ArrayList<>();
|
|
||||||
|
|
||||||
public String getLogin() { return login; }
|
|
||||||
public void setLogin(String login) { this.login = login; }
|
|
||||||
|
|
||||||
public List<Item> getParams() { return params; }
|
|
||||||
public void setParams(List<Item> params) { this.params = params; }
|
|
||||||
|
|
||||||
public static class Item {
|
|
||||||
private String login;
|
|
||||||
private String param;
|
|
||||||
private Long time_ms;
|
|
||||||
private String value;
|
|
||||||
private String device_key;
|
|
||||||
private String signature;
|
|
||||||
|
|
||||||
public String getLogin() { return login; }
|
|
||||||
public void setLogin(String login) { this.login = login; }
|
|
||||||
|
|
||||||
public String getParam() { return param; }
|
|
||||||
public void setParam(String param) { this.param = param; }
|
|
||||||
|
|
||||||
public Long getTime_ms() { return time_ms; }
|
|
||||||
public void setTime_ms(Long time_ms) { this.time_ms = time_ms; }
|
|
||||||
|
|
||||||
public String getValue() { return value; }
|
|
||||||
public void setValue(String value) { this.value = value; }
|
|
||||||
|
|
||||||
public String getDevice_key() { return device_key; }
|
|
||||||
public void setDevice_key(String device_key) { this.device_key = device_key; }
|
|
||||||
|
|
||||||
public String getSignature() { return signature; }
|
|
||||||
public void setSignature(String signature) { this.signature = signature; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
package server.logic.ws_protocol.JSON.handlers.userParams.entyties;
|
|
||||||
|
|
||||||
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Запрос UpsertUserParam — добавить/обновить сохранённый параметр пользователя.
|
|
||||||
*
|
|
||||||
* Клиент отправляет:
|
|
||||||
*
|
|
||||||
* {
|
|
||||||
* "op": "UpsertUserParam",
|
|
||||||
* "requestId": "req-123",
|
|
||||||
* "payload": {
|
|
||||||
* "login": "anya",
|
|
||||||
* "param": "feed:lastSeenGlobal",
|
|
||||||
* "time_ms": 1736000000123,
|
|
||||||
* "value": "105",
|
|
||||||
* "device_key": "base64-ed25519-public-key-32",
|
|
||||||
* "signature": "base64-ed25519-signature-64"
|
|
||||||
* }
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
* Подпись считается от UTF-8 строки:
|
|
||||||
* USER_PARAMETER_PREFIX + login + param + time_ms + value
|
|
||||||
*/
|
|
||||||
public class Net_UpsertUserParam_Request extends Net_Request {
|
|
||||||
|
|
||||||
private String login;
|
|
||||||
private String param;
|
|
||||||
private Long time_ms;
|
|
||||||
private String value;
|
|
||||||
|
|
||||||
private String device_key;
|
|
||||||
private String signature;
|
|
||||||
|
|
||||||
public String getLogin() { return login; }
|
|
||||||
public void setLogin(String login) { this.login = login; }
|
|
||||||
|
|
||||||
public String getParam() { return param; }
|
|
||||||
public void setParam(String param) { this.param = param; }
|
|
||||||
|
|
||||||
public Long getTime_ms() { return time_ms; }
|
|
||||||
public void setTime_ms(Long time_ms) { this.time_ms = time_ms; }
|
|
||||||
|
|
||||||
public String getValue() { return value; }
|
|
||||||
public void setValue(String value) { this.value = value; }
|
|
||||||
|
|
||||||
public String getDevice_key() { return device_key; }
|
|
||||||
public void setDevice_key(String device_key) { this.device_key = device_key; }
|
|
||||||
|
|
||||||
public String getSignature() { return signature; }
|
|
||||||
public void setSignature(String signature) { this.signature = signature; }
|
|
||||||
}
|
|
||||||
package server.logic.ws_protocol.JSON.handlers.userParams.entyties;
|
|
||||||
|
|
||||||
import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Ответ на UpsertUserParam.
|
|
||||||
*
|
|
||||||
* Успех:
|
|
||||||
* {
|
|
||||||
* "op": "UpsertUserParam",
|
|
||||||
* "requestId": "req-123",
|
|
||||||
* "status": 200,
|
|
||||||
* "payload": { }
|
|
||||||
* }
|
|
||||||
*/
|
|
||||||
public class Net_UpsertUserParam_Response extends Net_Response {
|
|
||||||
// MVP: без payload. При желании позже можно добавить created/updated.
|
|
||||||
}
|
|
||||||
package server.logic.ws_protocol.JSON.handlers.userParams;
|
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import server.logic.ws_protocol.JSON.ConnectionContext;
|
|
||||||
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
|
||||||
import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
|
||||||
import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler;
|
|
||||||
import server.logic.ws_protocol.JSON.handlers.userParams.entyties.Net_GetUserParam_Request;
|
|
||||||
import server.logic.ws_protocol.JSON.handlers.userParams.entyties.Net_GetUserParam_Response;
|
|
||||||
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
|
||||||
import server.logic.ws_protocol.WireCodes;
|
|
||||||
import shine.db.SqliteDbController;
|
|
||||||
import shine.db.dao.UserParamsDAO;
|
|
||||||
import shine.db.entities.UserParamEntry;
|
|
||||||
|
|
||||||
import java.sql.Connection;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* GetUserParam — получить один параметр пользователя.
|
|
||||||
*
|
|
||||||
* ПРО ДОСТУП (на будущее):
|
|
||||||
* ---------------------------------------------------------------------------------
|
|
||||||
* Сейчас (MVP) запрос не ограничивает просмотр параметров.
|
|
||||||
* В будущем, вероятно, потребуется проверка сессии/прав: кто может читать параметры.
|
|
||||||
* Для MVP эти проверки не нужны.
|
|
||||||
* ---------------------------------------------------------------------------------
|
|
||||||
*/
|
|
||||||
public class Net_GetUserParam_Handler implements JsonMessageHandler {
|
|
||||||
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(Net_GetUserParam_Handler.class);
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) {
|
|
||||||
Net_GetUserParam_Request req = (Net_GetUserParam_Request) baseRequest;
|
|
||||||
|
|
||||||
if (req.getLogin() == null || req.getLogin().isBlank()
|
|
||||||
|| req.getParam() == null || req.getParam().isBlank()) {
|
|
||||||
return NetExceptionResponseFactory.error(
|
|
||||||
req,
|
|
||||||
WireCodes.Status.BAD_REQUEST,
|
|
||||||
"BAD_FIELDS",
|
|
||||||
"Некорректные поля: login/param"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
String login = req.getLogin().trim();
|
|
||||||
String param = req.getParam().trim();
|
|
||||||
|
|
||||||
try {
|
|
||||||
SqliteDbController db = SqliteDbController.getInstance();
|
|
||||||
UserParamsDAO dao = UserParamsDAO.getInstance();
|
|
||||||
|
|
||||||
try (Connection c = db.getConnection()) {
|
|
||||||
UserParamEntry e = dao.getByLoginAndParam(c, login, param);
|
|
||||||
|
|
||||||
if (e == null) {
|
|
||||||
Net_GetUserParam_Response resp = new Net_GetUserParam_Response();
|
|
||||||
resp.setOp(req.getOp());
|
|
||||||
resp.setRequestId(req.getRequestId());
|
|
||||||
resp.setStatus(404);
|
|
||||||
return resp;
|
|
||||||
}
|
|
||||||
|
|
||||||
Net_GetUserParam_Response resp = new Net_GetUserParam_Response();
|
|
||||||
resp.setOp(req.getOp());
|
|
||||||
resp.setRequestId(req.getRequestId());
|
|
||||||
resp.setStatus(WireCodes.Status.OK);
|
|
||||||
|
|
||||||
resp.setLogin(e.getLogin());
|
|
||||||
resp.setParam(e.getParam());
|
|
||||||
resp.setTime_ms(e.getTimeMs());
|
|
||||||
resp.setValue(e.getValue());
|
|
||||||
resp.setDevice_key(e.getDeviceKey());
|
|
||||||
resp.setSignature(e.getSignature());
|
|
||||||
|
|
||||||
return resp;
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("❌ Internal error GetUserParam", e);
|
|
||||||
return NetExceptionResponseFactory.error(
|
|
||||||
req,
|
|
||||||
WireCodes.Status.INTERNAL_ERROR,
|
|
||||||
"INTERNAL_ERROR",
|
|
||||||
"Внутренняя ошибка сервера"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
package server.logic.ws_protocol.JSON.handlers.userParams;
|
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import server.logic.ws_protocol.JSON.ConnectionContext;
|
|
||||||
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
|
||||||
import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
|
||||||
import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler;
|
|
||||||
import server.logic.ws_protocol.JSON.handlers.userParams.entyties.Net_ListUserParams_Request;
|
|
||||||
import server.logic.ws_protocol.JSON.handlers.userParams.entyties.Net_ListUserParams_Response;
|
|
||||||
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
|
||||||
import server.logic.ws_protocol.WireCodes;
|
|
||||||
import shine.db.SqliteDbController;
|
|
||||||
import shine.db.dao.UserParamsDAO;
|
|
||||||
import shine.db.entities.UserParamEntry;
|
|
||||||
|
|
||||||
import java.sql.Connection;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ListUserParams — получить все параметры пользователя.
|
|
||||||
*
|
|
||||||
* ПРО ДОСТУП (на будущее):
|
|
||||||
* ---------------------------------------------------------------------------------
|
|
||||||
* Сейчас (MVP) запрос не ограничивает просмотр параметров.
|
|
||||||
* В будущем, вероятно, потребуется проверка сессии/прав: кто может читать параметры.
|
|
||||||
* Для MVP эти проверки не нужны.
|
|
||||||
* ---------------------------------------------------------------------------------
|
|
||||||
*/
|
|
||||||
public class Net_ListUserParams_Handler implements JsonMessageHandler {
|
|
||||||
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(Net_ListUserParams_Handler.class);
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) {
|
|
||||||
Net_ListUserParams_Request req = (Net_ListUserParams_Request) baseRequest;
|
|
||||||
|
|
||||||
if (req.getLogin() == null || req.getLogin().isBlank()) {
|
|
||||||
return NetExceptionResponseFactory.error(
|
|
||||||
req,
|
|
||||||
WireCodes.Status.BAD_REQUEST,
|
|
||||||
"BAD_FIELDS",
|
|
||||||
"Некорректные поля: login"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
String login = req.getLogin().trim();
|
|
||||||
|
|
||||||
try {
|
|
||||||
SqliteDbController db = SqliteDbController.getInstance();
|
|
||||||
UserParamsDAO dao = UserParamsDAO.getInstance();
|
|
||||||
|
|
||||||
List<UserParamEntry> entries;
|
|
||||||
try (Connection c = db.getConnection()) {
|
|
||||||
entries = dao.getByLogin(c, login);
|
|
||||||
}
|
|
||||||
|
|
||||||
Net_ListUserParams_Response resp = new Net_ListUserParams_Response();
|
|
||||||
resp.setOp(req.getOp());
|
|
||||||
resp.setRequestId(req.getRequestId());
|
|
||||||
resp.setStatus(WireCodes.Status.OK);
|
|
||||||
|
|
||||||
resp.setLogin(login);
|
|
||||||
|
|
||||||
List<Net_ListUserParams_Response.Item> items = new ArrayList<>();
|
|
||||||
for (UserParamEntry e : entries) {
|
|
||||||
Net_ListUserParams_Response.Item it = new Net_ListUserParams_Response.Item();
|
|
||||||
it.setLogin(e.getLogin());
|
|
||||||
it.setParam(e.getParam());
|
|
||||||
it.setTime_ms(e.getTimeMs());
|
|
||||||
it.setValue(e.getValue());
|
|
||||||
it.setDevice_key(e.getDeviceKey());
|
|
||||||
it.setSignature(e.getSignature());
|
|
||||||
items.add(it);
|
|
||||||
}
|
|
||||||
resp.setParams(items);
|
|
||||||
|
|
||||||
return resp;
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("❌ Internal error ListUserParams", e);
|
|
||||||
return NetExceptionResponseFactory.error(
|
|
||||||
req,
|
|
||||||
WireCodes.Status.INTERNAL_ERROR,
|
|
||||||
"INTERNAL_ERROR",
|
|
||||||
"Внутренняя ошибка сервера"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
package server.logic.ws_protocol.JSON.handlers.userParams;
|
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import server.logic.ws_protocol.JSON.ConnectionContext;
|
|
||||||
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
|
||||||
import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
|
||||||
import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler;
|
|
||||||
import server.logic.ws_protocol.JSON.handlers.userParams.entyties.Net_UpsertUserParam_Request;
|
|
||||||
import server.logic.ws_protocol.JSON.handlers.userParams.entyties.Net_UpsertUserParam_Response;
|
|
||||||
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
|
||||||
import server.logic.ws_protocol.WireCodes;
|
|
||||||
import shine.db.SqliteDbController;
|
|
||||||
import shine.db.dao.SolanaUsersDAO;
|
|
||||||
import shine.db.dao.UserParamsDAO;
|
|
||||||
import shine.db.entities.SolanaUserEntry;
|
|
||||||
import shine.db.entities.UserParamEntry;
|
|
||||||
import utils.config.ShineSignatureConstants;
|
|
||||||
import utils.crypto.Ed25519Util;
|
|
||||||
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.sql.Connection;
|
|
||||||
import java.sql.SQLException;
|
|
||||||
import java.util.Base64;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Net_UpsertUserParam_Handler
|
|
||||||
*
|
|
||||||
* Делает (MVP, без "сессий"):
|
|
||||||
* 1) Проверка входных полей.
|
|
||||||
* 2) Проверка подписи Ed25519 по device_key.
|
|
||||||
* 3) Проверка, что пользователь существует и что device_key принадлежит этому login.
|
|
||||||
* 4) Атомарная запись в БД "только если time_ms новее" (UPSERT + WHERE).
|
|
||||||
*
|
|
||||||
* ВАЖНО:
|
|
||||||
* - НИКАКИХ ручных транзакций / BEGIN здесь нет.
|
|
||||||
* - autoCommit=true, каждый statement завершённый сам по себе.
|
|
||||||
* - Гонки не страшны: если за время проверок кто-то записал более новый time_ms,
|
|
||||||
* наш финальный UPSERT просто вернёт 0 обновлённых строк.
|
|
||||||
*/
|
|
||||||
public class Net_UpsertUserParam_Handler implements JsonMessageHandler {
|
|
||||||
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(Net_UpsertUserParam_Handler.class);
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) {
|
|
||||||
Net_UpsertUserParam_Request req = (Net_UpsertUserParam_Request) baseRequest;
|
|
||||||
|
|
||||||
if (req.getLogin() == null || req.getLogin().isBlank()
|
|
||||||
|| req.getParam() == null || req.getParam().isBlank()
|
|
||||||
|| req.getTime_ms() == null || req.getTime_ms() <= 0
|
|
||||||
|| req.getValue() == null
|
|
||||||
|| req.getDevice_key() == null || req.getDevice_key().isBlank()
|
|
||||||
|| req.getSignature() == null || req.getSignature().isBlank()) {
|
|
||||||
|
|
||||||
return NetExceptionResponseFactory.error(
|
|
||||||
req,
|
|
||||||
WireCodes.Status.BAD_REQUEST,
|
|
||||||
"BAD_FIELDS",
|
|
||||||
"Некорректные поля: login/param/time_ms/value/device_key/signature"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
final String login = req.getLogin().trim();
|
|
||||||
final String param = req.getParam().trim();
|
|
||||||
final long timeMs = req.getTime_ms();
|
|
||||||
final String value = req.getValue();
|
|
||||||
final String deviceKeyB64 = req.getDevice_key().trim();
|
|
||||||
final String signatureB64 = req.getSignature().trim();
|
|
||||||
|
|
||||||
try {
|
|
||||||
// ---------------- Base64 decode ----------------
|
|
||||||
byte[] pubKey32;
|
|
||||||
byte[] sig64;
|
|
||||||
try {
|
|
||||||
pubKey32 = Base64.getDecoder().decode(deviceKeyB64);
|
|
||||||
sig64 = Base64.getDecoder().decode(signatureB64);
|
|
||||||
} catch (IllegalArgumentException e) {
|
|
||||||
return NetExceptionResponseFactory.error(
|
|
||||||
req,
|
|
||||||
WireCodes.Status.BAD_REQUEST,
|
|
||||||
"BAD_BASE64",
|
|
||||||
"device_key/signature должны быть Base64"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pubKey32.length != 32) {
|
|
||||||
return NetExceptionResponseFactory.error(
|
|
||||||
req,
|
|
||||||
WireCodes.Status.BAD_REQUEST,
|
|
||||||
"BAD_DEVICE_KEY",
|
|
||||||
"device_key должен быть Base64(32 bytes)"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (sig64.length != 64) {
|
|
||||||
return NetExceptionResponseFactory.error(
|
|
||||||
req,
|
|
||||||
WireCodes.Status.BAD_REQUEST,
|
|
||||||
"BAD_SIGNATURE",
|
|
||||||
"signature должна быть Base64(64 bytes)"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------- Signature verify ----------------
|
|
||||||
String signText = ShineSignatureConstants.USER_PARAMETER_PREFIX
|
|
||||||
+ login
|
|
||||||
+ param
|
|
||||||
+ timeMs
|
|
||||||
+ value;
|
|
||||||
|
|
||||||
byte[] signBytes = signText.getBytes(StandardCharsets.UTF_8);
|
|
||||||
|
|
||||||
boolean sigOk = Ed25519Util.verify(signBytes, sig64, pubKey32);
|
|
||||||
if (!sigOk) {
|
|
||||||
return NetExceptionResponseFactory.error(
|
|
||||||
req,
|
|
||||||
403,
|
|
||||||
"SIGNATURE_INVALID",
|
|
||||||
"Подпись не прошла проверку"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------- DB checks + upsert ----------------
|
|
||||||
SqliteDbController db = SqliteDbController.getInstance();
|
|
||||||
SolanaUsersDAO usersDAO = SolanaUsersDAO.getInstance();
|
|
||||||
UserParamsDAO paramsDAO = UserParamsDAO.getInstance();
|
|
||||||
|
|
||||||
try (Connection c = db.getConnection()) {
|
|
||||||
// 1) user exists
|
|
||||||
SolanaUserEntry user = usersDAO.getByLogin(c, login);
|
|
||||||
if (user == null) {
|
|
||||||
return NetExceptionResponseFactory.error(
|
|
||||||
req,
|
|
||||||
404,
|
|
||||||
"USER_NOT_FOUND",
|
|
||||||
"Пользователь не найден"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2) device key must match the user's stored deviceKey
|
|
||||||
String userDeviceKey = user.getDeviceKey();
|
|
||||||
if (userDeviceKey == null || userDeviceKey.isBlank()) {
|
|
||||||
return NetExceptionResponseFactory.error(
|
|
||||||
req,
|
|
||||||
WireCodes.Status.SERVER_DATA_ERROR,
|
|
||||||
"USER_DEVICE_KEY_EMPTY",
|
|
||||||
"У пользователя не задан deviceKey в БД"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!userDeviceKey.trim().equals(deviceKeyB64)) {
|
|
||||||
return NetExceptionResponseFactory.error(
|
|
||||||
req,
|
|
||||||
403,
|
|
||||||
"DEVICE_KEY_MISMATCH",
|
|
||||||
"device_key не соответствует пользователю"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3) atomic upsert-if-newer
|
|
||||||
UserParamEntry e = new UserParamEntry(
|
|
||||||
login,
|
|
||||||
param,
|
|
||||||
timeMs,
|
|
||||||
value,
|
|
||||||
deviceKeyB64,
|
|
||||||
signatureB64
|
|
||||||
);
|
|
||||||
|
|
||||||
int changed = paramsDAO.upsertIfNewer(c, e);
|
|
||||||
|
|
||||||
Net_UpsertUserParam_Response resp = new Net_UpsertUserParam_Response();
|
|
||||||
resp.setOp(req.getOp());
|
|
||||||
resp.setRequestId(req.getRequestId());
|
|
||||||
resp.setStatus(WireCodes.Status.OK);
|
|
||||||
|
|
||||||
if (changed == 1) {
|
|
||||||
log.info("✅ UpsertUserParam applied: login={}, param={}, time_ms={}", login, param, timeMs);
|
|
||||||
} else {
|
|
||||||
// 0 строк — значит в БД уже есть time_ms >= incoming
|
|
||||||
log.info("ℹ️ UpsertUserParam ignored (not newer): login={}, param={}, time_ms={}", login, param, timeMs);
|
|
||||||
}
|
|
||||||
|
|
||||||
return resp;
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (SQLException e) {
|
|
||||||
log.error("❌ DB error UpsertUserParam", e);
|
|
||||||
return NetExceptionResponseFactory.error(
|
|
||||||
req,
|
|
||||||
WireCodes.Status.SERVER_DATA_ERROR,
|
|
||||||
"DB_ERROR",
|
|
||||||
"Ошибка БД"
|
|
||||||
);
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("❌ Internal error UpsertUserParam", e);
|
|
||||||
return NetExceptionResponseFactory.error(
|
|
||||||
req,
|
|
||||||
WireCodes.Status.INTERNAL_ERROR,
|
|
||||||
"INTERNAL_ERROR",
|
|
||||||
"Внутренняя ошибка сервера"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
OUTFILE="all_files.txt"
|
|
||||||
|
|
||||||
# очищаем или создаём файл
|
|
||||||
: > "$OUTFILE"
|
|
||||||
|
|
||||||
# собрать только *.java файлы и вывести их содержимое в файл
|
|
||||||
find . -type f -name "*.java" | sort | while read -r f; do
|
|
||||||
cat "$f" >> "$OUTFILE"
|
|
||||||
echo >> "$OUTFILE" # пустая строка-разделитель
|
|
||||||
done
|
|
||||||
|
|
||||||
# скопировать весь файл в буфер обмена (Wayland)
|
|
||||||
wl-copy < "$OUTFILE"
|
|
||||||
|
|
||||||
echo "Готово!"
|
|
||||||
echo "Все .java файлы собраны в $OUTFILE"
|
|
||||||
echo "Содержимое скопировано в буфер обмена (Wayland)"
|
|
||||||
@ -15,7 +15,7 @@ import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
|||||||
* "param": "feed:lastSeenGlobal",
|
* "param": "feed:lastSeenGlobal",
|
||||||
* "time_ms": 1736000000123,
|
* "time_ms": 1736000000123,
|
||||||
* "value": "105",
|
* "value": "105",
|
||||||
* "device_key": "base64-32",
|
* "client_key": "base64-32",
|
||||||
* "signature": "base64-64"
|
* "signature": "base64-64"
|
||||||
* }
|
* }
|
||||||
* }
|
* }
|
||||||
@ -29,7 +29,7 @@ public class Net_GetUserParam_Response extends Net_Response {
|
|||||||
private String param;
|
private String param;
|
||||||
private Long time_ms;
|
private Long time_ms;
|
||||||
private String value;
|
private String value;
|
||||||
private String device_key;
|
private String client_key;
|
||||||
private String signature;
|
private String signature;
|
||||||
|
|
||||||
public String getLogin() { return login; }
|
public String getLogin() { return login; }
|
||||||
@ -44,8 +44,8 @@ public class Net_GetUserParam_Response extends Net_Response {
|
|||||||
public String getValue() { return value; }
|
public String getValue() { return value; }
|
||||||
public void setValue(String value) { this.value = value; }
|
public void setValue(String value) { this.value = value; }
|
||||||
|
|
||||||
public String getDevice_key() { return device_key; }
|
public String getClient_key() { return client_key; }
|
||||||
public void setDevice_key(String device_key) { this.device_key = device_key; }
|
public void setClient_key(String client_key) { this.client_key = client_key; }
|
||||||
|
|
||||||
public String getSignature() { return signature; }
|
public String getSignature() { return signature; }
|
||||||
public void setSignature(String signature) { this.signature = signature; }
|
public void setSignature(String signature) { this.signature = signature; }
|
||||||
|
|||||||
@ -20,7 +20,7 @@ import java.util.List;
|
|||||||
* "param": "feed:lastSeenGlobal",
|
* "param": "feed:lastSeenGlobal",
|
||||||
* "time_ms": 1736000000123,
|
* "time_ms": 1736000000123,
|
||||||
* "value": "105",
|
* "value": "105",
|
||||||
* "device_key": "base64-32",
|
* "client_key": "base64-32",
|
||||||
* "signature": "base64-64"
|
* "signature": "base64-64"
|
||||||
* },
|
* },
|
||||||
* ...
|
* ...
|
||||||
@ -44,7 +44,7 @@ public class Net_ListUserParams_Response extends Net_Response {
|
|||||||
private String param;
|
private String param;
|
||||||
private Long time_ms;
|
private Long time_ms;
|
||||||
private String value;
|
private String value;
|
||||||
private String device_key;
|
private String client_key;
|
||||||
private String signature;
|
private String signature;
|
||||||
|
|
||||||
public String getLogin() { return login; }
|
public String getLogin() { return login; }
|
||||||
@ -59,8 +59,8 @@ public class Net_ListUserParams_Response extends Net_Response {
|
|||||||
public String getValue() { return value; }
|
public String getValue() { return value; }
|
||||||
public void setValue(String value) { this.value = value; }
|
public void setValue(String value) { this.value = value; }
|
||||||
|
|
||||||
public String getDevice_key() { return device_key; }
|
public String getClient_key() { return client_key; }
|
||||||
public void setDevice_key(String device_key) { this.device_key = device_key; }
|
public void setClient_key(String client_key) { this.client_key = client_key; }
|
||||||
|
|
||||||
public String getSignature() { return signature; }
|
public String getSignature() { return signature; }
|
||||||
public void setSignature(String signature) { this.signature = signature; }
|
public void setSignature(String signature) { this.signature = signature; }
|
||||||
|
|||||||
@ -15,7 +15,7 @@ import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
|||||||
* "param": "feed:lastSeenGlobal",
|
* "param": "feed:lastSeenGlobal",
|
||||||
* "time_ms": 1736000000123,
|
* "time_ms": 1736000000123,
|
||||||
* "value": "105",
|
* "value": "105",
|
||||||
* "device_key": "base64-ed25519-public-key-32",
|
* "client_key": "base64-ed25519-public-key-32",
|
||||||
* "signature": "base64-ed25519-signature-64"
|
* "signature": "base64-ed25519-signature-64"
|
||||||
* }
|
* }
|
||||||
* }
|
* }
|
||||||
@ -30,7 +30,7 @@ public class Net_UpsertUserParam_Request extends Net_Request {
|
|||||||
private Long time_ms;
|
private Long time_ms;
|
||||||
private String value;
|
private String value;
|
||||||
|
|
||||||
private String device_key;
|
private String client_key;
|
||||||
private String signature;
|
private String signature;
|
||||||
|
|
||||||
public String getLogin() { return login; }
|
public String getLogin() { return login; }
|
||||||
@ -45,8 +45,8 @@ public class Net_UpsertUserParam_Request extends Net_Request {
|
|||||||
public String getValue() { return value; }
|
public String getValue() { return value; }
|
||||||
public void setValue(String value) { this.value = value; }
|
public void setValue(String value) { this.value = value; }
|
||||||
|
|
||||||
public String getDevice_key() { return device_key; }
|
public String getClient_key() { return client_key; }
|
||||||
public void setDevice_key(String device_key) { this.device_key = device_key; }
|
public void setClient_key(String client_key) { this.client_key = client_key; }
|
||||||
|
|
||||||
public String getSignature() { return signature; }
|
public String getSignature() { return signature; }
|
||||||
public void setSignature(String signature) { this.signature = signature; }
|
public void setSignature(String signature) { this.signature = signature; }
|
||||||
|
|||||||
@ -62,9 +62,9 @@ public class Net_SendDirectMessage_Handler implements JsonMessageHandler {
|
|||||||
|
|
||||||
byte[] publicKey32;
|
byte[] publicKey32;
|
||||||
try {
|
try {
|
||||||
publicKey32 = Ed25519Util.keyFromBase64(fromUser.getDeviceKey());
|
publicKey32 = Ed25519Util.keyFromBase64(fromUser.getClientKey());
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
return NetExceptionResponseFactory.error(req, WireCodes.Status.UNVERIFIED, "BAD_DEVICE_KEY", "Некорректный deviceKey отправителя");
|
return NetExceptionResponseFactory.error(req, WireCodes.Status.UNVERIFIED, "BAD_DEVICE_KEY", "Некорректный clientKey отправителя");
|
||||||
}
|
}
|
||||||
if (!Ed25519Util.verify(packet.signedBody, packet.signature64, publicKey32)) {
|
if (!Ed25519Util.verify(packet.signedBody, packet.signature64, publicKey32)) {
|
||||||
return NetExceptionResponseFactory.error(req, WireCodes.Status.UNVERIFIED, "BAD_SIGNATURE", "Подпись не прошла проверку");
|
return NetExceptionResponseFactory.error(req, WireCodes.Status.UNVERIFIED, "BAD_SIGNATURE", "Подпись не прошла проверку");
|
||||||
|
|||||||
@ -44,7 +44,7 @@ final class SignedMessagesCore {
|
|||||||
if (from == null || to == null) {
|
if (from == null || to == null) {
|
||||||
throw new IllegalArgumentException("USER_NOT_FOUND");
|
throw new IllegalArgumentException("USER_NOT_FOUND");
|
||||||
}
|
}
|
||||||
byte[] pubKey32 = Ed25519Util.keyFromBase64(from.getDeviceKey());
|
byte[] pubKey32 = Ed25519Util.keyFromBase64(from.getClientKey());
|
||||||
if (!Ed25519Util.verify(block.signedBody, block.signature64, pubKey32)) {
|
if (!Ed25519Util.verify(block.signedBody, block.signature64, pubKey32)) {
|
||||||
throw new IllegalArgumentException("BAD_SIGNATURE");
|
throw new IllegalArgumentException("BAD_SIGNATURE");
|
||||||
}
|
}
|
||||||
|
|||||||
@ -31,7 +31,7 @@
|
|||||||
// }
|
// }
|
||||||
//
|
//
|
||||||
// /**
|
// /**
|
||||||
// * Проверка подписи CreateAuthSession(v2) по deviceKey пользователя.
|
// * Проверка подписи CreateAuthSession(v2) по clientKey пользователя.
|
||||||
// * Подпись проверяется над preimageCreateAuthSession(...).
|
// * Подпись проверяется над preimageCreateAuthSession(...).
|
||||||
// */
|
// */
|
||||||
// public static boolean verifyCreateAuthSessionSignature(
|
// public static boolean verifyCreateAuthSessionSignature(
|
||||||
@ -42,8 +42,8 @@
|
|||||||
// String signatureB64
|
// String signatureB64
|
||||||
// ) throws IllegalArgumentException {
|
// ) throws IllegalArgumentException {
|
||||||
//
|
//
|
||||||
// // user.getDeviceKey() — base64 публичного ключа (32 байта)
|
// // user.getClientKey() — base64 публичного ключа (32 байта)
|
||||||
// byte[] publicKey32 = decodeBase64Any(user.getDeviceKey());
|
// byte[] publicKey32 = decodeBase64Any(user.getClientKey());
|
||||||
// byte[] signature64 = decodeBase64Any(signatureB64);
|
// byte[] signature64 = decodeBase64Any(signatureB64);
|
||||||
//
|
//
|
||||||
// byte[] preimage = preimageCreateAuthSession(login, timeMs, authNonce);
|
// byte[] preimage = preimageCreateAuthSession(login, timeMs, authNonce);
|
||||||
|
|||||||
@ -1,552 +0,0 @@
|
|||||||
package server.logic;
|
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import server.logic.ws_protocol.binary.handlers.*;
|
|
||||||
import server.logic.ws_protocol.WireCodes;
|
|
||||||
|
|
||||||
import java.nio.ByteBuffer;
|
|
||||||
import java.nio.ByteOrder;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Обработчик входящих сообщение на сервер.
|
|
||||||
* По коду сообщения (первые 4 байта сообщения) находи нужный хэндлер и передаёт в него сообщение
|
|
||||||
* Получает и возвращает ответ от хэндлера
|
|
||||||
*/
|
|
||||||
public final class InboundMessageProcessor {
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(InboundMessageProcessor.class);
|
|
||||||
|
|
||||||
private static final Map<Integer, MessageHandler> HANDLERS = Map.of(
|
|
||||||
// WireCodes.Op.PING, new PingHandler()
|
|
||||||
// WireCodes.Op.ADD_BLOCK, new AddBlockHandler(),
|
|
||||||
// WireCodes.Op.GET_BLOCKCHAIN,new GetBlockchainHandler()
|
|
||||||
// WireCodes.Op.SEARCH_USERS, new SearchUsersHandler(),
|
|
||||||
// WireCodes.Op.GET_LAST_BLOCK_INFO,new GetLastBlockInfoHandler()
|
|
||||||
|
|
||||||
);
|
|
||||||
|
|
||||||
private InboundMessageProcessor() {}
|
|
||||||
|
|
||||||
public static byte[] process(byte[] msg) {
|
|
||||||
if (msg == null || msg.length < 4)
|
|
||||||
return intTo4Bytes(WireCodes.Status.BAD_REQUEST);
|
|
||||||
|
|
||||||
int op = first4ToInt(msg);
|
|
||||||
MessageHandler h = HANDLERS.get(op);
|
|
||||||
if (h == null) {
|
|
||||||
log.warn("Неизвестная операция: {}", op);
|
|
||||||
return intTo4Bytes(WireCodes.Status.BAD_REQUEST);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
return h.handle(msg);
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("Ошибка при обработке операции {}", op, e);
|
|
||||||
return intTo4Bytes(WireCodes.Status.INTERNAL_ERROR);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int first4ToInt(byte[] msg) {
|
|
||||||
return ByteBuffer.wrap(msg, 0, 4)
|
|
||||||
.order(ByteOrder.BIG_ENDIAN)
|
|
||||||
.getInt();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static byte[] intTo4Bytes(int code) {
|
|
||||||
return ByteBuffer.allocate(4)
|
|
||||||
.order(ByteOrder.BIG_ENDIAN)
|
|
||||||
.putInt(code)
|
|
||||||
.array();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
package server.logic.ws_protocol.binary.handlers;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Общий интерфейс для всех обработчиков входящих сообщений.
|
|
||||||
*/
|
|
||||||
public interface MessageHandler {
|
|
||||||
/**
|
|
||||||
* Обработать входящее сообщение и вернуть бинарный ответ.
|
|
||||||
*/
|
|
||||||
byte[] handle(byte[] msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
package server.ws;
|
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import shine.db.dao.BlockchainStateDAO;
|
|
||||||
import shine.db.entities.BlockchainStateEntry;
|
|
||||||
import utils.files.FileStoreUtil;
|
|
||||||
import shine.log.BlockchainAdminNotifier;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.nio.file.*;
|
|
||||||
import java.sql.SQLException;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ===============================================================
|
|
||||||
* BlockchainTmpRecoveryOnStartup — восстановление консистентности
|
|
||||||
* blockchain файлов при старте сервера.
|
|
||||||
*
|
|
||||||
* Сценарий проблемы:
|
|
||||||
* - при добавлении блока сначала пишется <name>.tmp_bch
|
|
||||||
* - потом коммитится БД (state.fileSizeBytes)
|
|
||||||
* - потом tmp переименовывается поверх <name>.bch (атомарно, если возможно)
|
|
||||||
*
|
|
||||||
* Если сервер упал в середине, может остаться tmp:
|
|
||||||
* - tmp есть, а основной .bch остался старым
|
|
||||||
* - tmp есть, а основной .bch уже удалили/заменить не успели
|
|
||||||
* - tmp есть, а БД успела/не успела обновиться
|
|
||||||
*
|
|
||||||
* Этот класс при старте:
|
|
||||||
* - ищет все *.tmp_bch в data/
|
|
||||||
* - сравнивает размеры:
|
|
||||||
* - tmp
|
|
||||||
* - main (если есть)
|
|
||||||
* - state.fileSizeBytes (если есть)
|
|
||||||
*
|
|
||||||
* Правила:
|
|
||||||
*
|
|
||||||
* A) state есть:
|
|
||||||
* - если stateSize == mainSize => tmp удаляем
|
|
||||||
* - если stateSize == tmpSize => tmp ставим на место main (atomicReplaceBlockchainFile)
|
|
||||||
* - иначе => КРИТИЧЕСКАЯ ОШИБКА: сервер останавливаем + уведомление администратору
|
|
||||||
*
|
|
||||||
* B) state НЕТ:
|
|
||||||
* - если main НЕТ и tmp ЕСТЬ => tmp удаляем (мусор после падения/неуспешной транзакции)
|
|
||||||
* - если main ЕСТЬ и tmp ЕСТЬ => КРИТИЧЕСКАЯ ОШИБКА: уведомление администратору + стоп сервера
|
|
||||||
*
|
|
||||||
* Логирование:
|
|
||||||
* - обо всех восстановленных/удалённых tmp пишем в лог
|
|
||||||
* - если tmp-файлов нет — тоже пишем в лог
|
|
||||||
* ===============================================================
|
|
||||||
*/
|
|
||||||
public final class BlockchainTmpRecoveryOnStartup {
|
|
||||||
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(BlockchainTmpRecoveryOnStartup.class);
|
|
||||||
|
|
||||||
private BlockchainTmpRecoveryOnStartup() {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Запуск восстановления.
|
|
||||||
* Если обнаружена ситуация, когда размеры не совпали и сервер сам не может чинить — бросаем исключение.
|
|
||||||
*/
|
|
||||||
public static void runRecoveryOrThrow() {
|
|
||||||
FileStoreUtil fs = FileStoreUtil.getInstance();
|
|
||||||
BlockchainStateDAO stateDAO = BlockchainStateDAO.getInstance();
|
|
||||||
|
|
||||||
Path dataDir = Paths.get(FileStoreUtil.DATA_DIR_NAME);
|
|
||||||
ensureDirExists(dataDir);
|
|
||||||
|
|
||||||
List<Path> tmpFiles = listTmpFiles(dataDir);
|
|
||||||
|
|
||||||
if (tmpFiles.isEmpty()) {
|
|
||||||
log.info("🟢 BlockchainTmpRecovery: временных *.tmp_bch файлов не найдено — восстановление не требуется.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
log.warn("🟡 BlockchainTmpRecovery: найдено временных файлов: {}", tmpFiles.size());
|
|
||||||
|
|
||||||
for (Path tmpPath : tmpFiles) {
|
|
||||||
String fileName = tmpPath.getFileName().toString();
|
|
||||||
String blockchainName = extractBlockchainNameFromTmp(fileName);
|
|
||||||
|
|
||||||
if (blockchainName == null || blockchainName.isBlank()) {
|
|
||||||
// странное имя — не трогаем автоматически, но это уже повод дернуть админа
|
|
||||||
BlockchainAdminNotifier.critical(
|
|
||||||
"НАЙДЕН TMP-ФАЙЛ С НЕОЖИДАННЫМ ИМЕНЕМ: " + fileName + " (не могу определить blockchainName).",
|
|
||||||
null
|
|
||||||
);
|
|
||||||
throw new IllegalStateException("Bad tmp file name: " + fileName);
|
|
||||||
}
|
|
||||||
|
|
||||||
Path mainPath = dataDir.resolve(fs.buildBlockchainFileName(blockchainName));
|
|
||||||
|
|
||||||
long tmpSize = safeSize(tmpPath);
|
|
||||||
boolean mainExists = Files.exists(mainPath);
|
|
||||||
long mainSize = mainExists ? safeSize(mainPath) : -1L;
|
|
||||||
|
|
||||||
BlockchainStateEntry st = null;
|
|
||||||
try {
|
|
||||||
st = stateDAO.getByBlockchainName(blockchainName);
|
|
||||||
} catch (SQLException e) {
|
|
||||||
BlockchainAdminNotifier.critical(
|
|
||||||
"ОШИБКА БД ПРИ ВОССТАНОВЛЕНИИ TMP: blockchainName=" + blockchainName + " (сервер остановлен).",
|
|
||||||
e
|
|
||||||
);
|
|
||||||
throw new IllegalStateException("DB error during tmp recovery for " + blockchainName, e);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// CASE B) state НЕТ
|
|
||||||
// ============================================================
|
|
||||||
if (st == null) {
|
|
||||||
|
|
||||||
if (!mainExists) {
|
|
||||||
// НЕТ state, НЕТ main, есть tmp => удаляем tmp
|
|
||||||
log.warn("🟠 BlockchainTmpRecovery: state отсутствует и main отсутствует, но tmp найден => удаляем tmp. blockchainName={}, tmpSize={}",
|
|
||||||
blockchainName, tmpSize);
|
|
||||||
safeDelete(tmpPath);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// НЕТ state, но main есть и tmp есть => это уже подозрительно
|
|
||||||
BlockchainAdminNotifier.critical(
|
|
||||||
"НЕСОГЛАСОВАННОСТЬ: ЕСТЬ main И tmp, НО НЕТ state В БД. " +
|
|
||||||
"blockchainName=" + blockchainName +
|
|
||||||
", mainSize=" + mainSize +
|
|
||||||
", tmpSize=" + tmpSize +
|
|
||||||
". СЕРВЕР ОСТАНОВЛЕН. " +
|
|
||||||
"ПОДОЗРЕНИЕ: файлы могли быть изменены вне сервера.",
|
|
||||||
null
|
|
||||||
);
|
|
||||||
throw new IllegalStateException("State missing but both main and tmp exist for " + blockchainName);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// CASE A) state ЕСТЬ
|
|
||||||
// ============================================================
|
|
||||||
long stateSize = st.getFileSizeBytes();
|
|
||||||
|
|
||||||
// 1) stateSize == mainSize => tmp мусор
|
|
||||||
if (mainExists && mainSize == stateSize) {
|
|
||||||
log.info("🟢 BlockchainTmpRecovery: stateSize совпадает с main => tmp удаляем. blockchainName={}, stateSize={}, mainSize={}, tmpSize={}",
|
|
||||||
blockchainName, stateSize, mainSize, tmpSize);
|
|
||||||
safeDelete(tmpPath);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2) stateSize == tmpSize => tmp это актуальная версия, ставим на место main
|
|
||||||
if (tmpSize == stateSize) {
|
|
||||||
log.warn("🟡 BlockchainTmpRecovery: stateSize совпадает с tmp => восстанавливаем main из tmp. blockchainName={}, stateSize={}, mainSize={}, tmpSize={}",
|
|
||||||
blockchainName, stateSize, mainSize, tmpSize);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// метод уже есть и делает move tmp->main с попыткой ATOMIC_MOVE
|
|
||||||
fs.atomicReplaceBlockchainFile(blockchainName);
|
|
||||||
|
|
||||||
// после move tmp должен исчезнуть сам (перемещён)
|
|
||||||
log.info("✅ BlockchainTmpRecovery: восстановление выполнено. blockchainName={}, newMainSize={}",
|
|
||||||
blockchainName, safeSize(mainPath));
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
BlockchainAdminNotifier.critical(
|
|
||||||
"НЕ УДАЛОСЬ ВОССТАНОВИТЬ main ИЗ tmp (move failed). " +
|
|
||||||
"blockchainName=" + blockchainName +
|
|
||||||
", stateSize=" + stateSize +
|
|
||||||
", mainSize=" + mainSize +
|
|
||||||
", tmpSize=" + tmpSize +
|
|
||||||
". СЕРВЕР ОСТАНОВЛЕН.",
|
|
||||||
e
|
|
||||||
);
|
|
||||||
throw new IllegalStateException("Cannot replace main from tmp for " + blockchainName, e);
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3) НИЧЕГО НЕ СОВПАЛО => критическая ситуация
|
|
||||||
BlockchainAdminNotifier.critical(
|
|
||||||
"ФАТАЛЬНАЯ НЕСОГЛАСОВАННОСТЬ BLOCKCHAIN ФАЙЛОВ. " +
|
|
||||||
"blockchainName=" + blockchainName +
|
|
||||||
", stateSize=" + stateSize +
|
|
||||||
", mainExists=" + mainExists +
|
|
||||||
", mainSize=" + mainSize +
|
|
||||||
", tmpSize=" + tmpSize +
|
|
||||||
". СЕРВЕР ОСТАНОВЛЕН. " +
|
|
||||||
"ТУТ НУЖНО УВЕДОМЛЕНИЕ АДМИНИСТРАТОРУ: возможно файлы изменены вручную/другой программой.",
|
|
||||||
null
|
|
||||||
);
|
|
||||||
throw new IllegalStateException("Blockchain files mismatch for " + blockchainName);
|
|
||||||
}
|
|
||||||
|
|
||||||
log.info("✅ BlockchainTmpRecovery: обработка tmp-файлов завершена.");
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ===================================================================== */
|
|
||||||
/* =============================== Helpers ============================== */
|
|
||||||
/* ===================================================================== */
|
|
||||||
|
|
||||||
private static void ensureDirExists(Path dir) {
|
|
||||||
try {
|
|
||||||
if (!Files.exists(dir)) {
|
|
||||||
Files.createDirectories(dir);
|
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new IllegalStateException("Cannot create data dir: " + dir, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static List<Path> listTmpFiles(Path dataDir) {
|
|
||||||
List<Path> out = new ArrayList<>();
|
|
||||||
try (DirectoryStream<Path> ds = Files.newDirectoryStream(dataDir, "*" + FileStoreUtil.BLOCKCHAIN_TMP_EXTENSION)) {
|
|
||||||
for (Path p : ds) {
|
|
||||||
if (Files.isRegularFile(p)) out.add(p);
|
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new IllegalStateException("Cannot list tmp files in: " + dataDir, e);
|
|
||||||
}
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Из "anya0001.tmp_bch" -> "anya0001"
|
|
||||||
*/
|
|
||||||
private static String extractBlockchainNameFromTmp(String tmpFileName) {
|
|
||||||
if (tmpFileName == null) return null;
|
|
||||||
if (!tmpFileName.endsWith(FileStoreUtil.BLOCKCHAIN_TMP_EXTENSION)) return null;
|
|
||||||
|
|
||||||
String base = tmpFileName.substring(0, tmpFileName.length() - FileStoreUtil.BLOCKCHAIN_TMP_EXTENSION.length());
|
|
||||||
|
|
||||||
// базовая защита: не допускаем слэши/.. даже если кто-то подложил файл
|
|
||||||
if (base.isBlank()) return null;
|
|
||||||
if (base.contains("/") || base.contains("\\") || base.contains("..")) return null;
|
|
||||||
|
|
||||||
return base;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static long safeSize(Path p) {
|
|
||||||
try {
|
|
||||||
return Files.size(p);
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new IllegalStateException("Cannot read file size: " + p, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void safeDelete(Path p) {
|
|
||||||
try {
|
|
||||||
Files.deleteIfExists(p);
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new IllegalStateException("Cannot delete file: " + p, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
package server.ws;
|
|
||||||
|
|
||||||
import org.eclipse.jetty.websocket.api.Session;
|
|
||||||
import org.eclipse.jetty.websocket.api.WriteCallback;
|
|
||||||
import org.eclipse.jetty.websocket.api.annotations.*;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import server.logic.InboundMessageProcessor;
|
|
||||||
import server.logic.ws_protocol.JSON.ActiveConnectionsRegistry;
|
|
||||||
import server.logic.ws_protocol.JSON.ConnectionContext;
|
|
||||||
import server.logic.ws_protocol.JSON.JsonInboundProcessor;
|
|
||||||
|
|
||||||
import java.nio.ByteBuffer;
|
|
||||||
import java.util.concurrent.CompletableFuture;
|
|
||||||
|
|
||||||
@WebSocket
|
|
||||||
public class BlockchainWsEndpoint {
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(BlockchainWsEndpoint.class);
|
|
||||||
|
|
||||||
private Session session;
|
|
||||||
|
|
||||||
/** Контекст для текущего WebSocket-соединения. */
|
|
||||||
private final ConnectionContext connectionContext = new ConnectionContext();
|
|
||||||
|
|
||||||
@OnWebSocketConnect
|
|
||||||
public void onConnect(Session session) {
|
|
||||||
this.session = session;
|
|
||||||
// Привязываем WebSocket-сессию к ConnectionContext
|
|
||||||
connectionContext.setWsSession(session);
|
|
||||||
log.info("WS connected: {}", session.getRemoteAddress());
|
|
||||||
}
|
|
||||||
|
|
||||||
@OnWebSocketMessage
|
|
||||||
public void onBinary(byte[] payload, int offset, int length) {
|
|
||||||
byte[] msg = new byte[length];
|
|
||||||
System.arraycopy(payload, offset, msg, 0, length);
|
|
||||||
|
|
||||||
// Асинхронно обрабатываем входящее бинарное сообщение
|
|
||||||
CompletableFuture
|
|
||||||
.supplyAsync(() -> InboundMessageProcessor.process(msg))
|
|
||||||
.thenAccept(resp -> {
|
|
||||||
if (resp != null && session != null && session.isOpen()) {
|
|
||||||
session.getRemote().sendBytes(ByteBuffer.wrap(resp), new WriteCallback() {
|
|
||||||
@Override
|
|
||||||
public void writeFailed(Throwable x) {
|
|
||||||
log.warn("Failed to send response", x);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void writeSuccess() {
|
|
||||||
log.debug("Response sent successfully");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.exceptionally(ex -> {
|
|
||||||
log.error("Processing failed", ex);
|
|
||||||
trySendCode(500);
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void trySendCode(int code) {
|
|
||||||
if (session != null && session.isOpen()) {
|
|
||||||
byte[] resp = InboundMessageProcessor.intTo4Bytes(code);
|
|
||||||
session.getRemote().sendBytes(ByteBuffer.wrap(resp), new WriteCallback() {
|
|
||||||
@Override
|
|
||||||
public void writeFailed(Throwable x) {
|
|
||||||
log.warn("Failed to send error code", x);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void writeSuccess() {
|
|
||||||
log.debug("Error code {} sent", code);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@OnWebSocketClose
|
|
||||||
public void onClose(int statusCode, String reason) {
|
|
||||||
log.info("WS closed: {} {}", statusCode, reason);
|
|
||||||
// Удаляем это подключение из реестра активных соединений
|
|
||||||
ActiveConnectionsRegistry.getInstance().remove(connectionContext);
|
|
||||||
// На всякий случай очищаем контекст
|
|
||||||
connectionContext.reset();
|
|
||||||
}
|
|
||||||
|
|
||||||
@OnWebSocketError
|
|
||||||
public void onError(Throwable cause) {
|
|
||||||
log.error("WS error", cause);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Обработка текстовых JSON-запросов
|
|
||||||
@OnWebSocketMessage
|
|
||||||
public void onText(String message) {
|
|
||||||
log.info("📥 Получено TEXT-сообщение от клиента: {}", message);
|
|
||||||
|
|
||||||
CompletableFuture
|
|
||||||
.supplyAsync(() -> JsonInboundProcessor.processJson(message, connectionContext))
|
|
||||||
.thenAccept(respJson -> {
|
|
||||||
if (respJson != null && session != null && session.isOpen()) {
|
|
||||||
|
|
||||||
log.info("📤 Отправляем ответ клиенту: {}", respJson);
|
|
||||||
|
|
||||||
session.getRemote().sendString(respJson, new WriteCallback() {
|
|
||||||
@Override
|
|
||||||
public void writeFailed(Throwable x) {
|
|
||||||
log.warn("⚠️ Не удалось отправить JSON-ответ клиенту: {}", x.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void writeSuccess() {
|
|
||||||
log.debug("✔ JSON-ответ успешно отправлен");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.exceptionally(ex -> {
|
|
||||||
log.error("❌ Ошибка при обработке JSON-сообщения", ex);
|
|
||||||
trySendJsonError();
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void trySendJsonError() {
|
|
||||||
if (session != null && session.isOpen()) {
|
|
||||||
String resp = "{\"op\":null,\"requestId\":null,\"status\":500,"
|
|
||||||
+ "\"payload\":{\"code\":\"INTERNAL_ERROR\",\"message\":\"Ошибка сервера\"}}";
|
|
||||||
|
|
||||||
log.info("📤 Отправляем клиенту ошибку JSON: {}", resp);
|
|
||||||
|
|
||||||
session.getRemote().sendString(resp, new WriteCallback() {
|
|
||||||
@Override
|
|
||||||
public void writeFailed(Throwable x) {
|
|
||||||
log.warn("⚠️ Не удалось отправить JSON-ответ клиенту: {}", x.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void writeSuccess() {
|
|
||||||
log.debug("✔ JSON-ошибка успешно отправлена");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
package server.ws;
|
|
||||||
|
|
||||||
import org.eclipse.jetty.server.Server;
|
|
||||||
import org.eclipse.jetty.servlet.ServletContextHandler;
|
|
||||||
import org.eclipse.jetty.websocket.server.config.JettyWebSocketServletContainerInitializer;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import utils.config.AppConfig;
|
|
||||||
|
|
||||||
import java.time.Duration;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* WsServer — поднимает Jetty WS на /ws.
|
|
||||||
*
|
|
||||||
* ВАЖНО:
|
|
||||||
* - перед стартом сервера выполняем recovery tmp-блокчейнов.
|
|
||||||
* - если обнаружена несогласованность, которую сервер сам чинить не может —
|
|
||||||
* recovery бросает исключение и сервер не стартует.
|
|
||||||
*/
|
|
||||||
public final class WsServer {
|
|
||||||
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(WsServer.class);
|
|
||||||
|
|
||||||
public static void main(String[] args) throws Exception {
|
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// 0) Восстановление консистентности blockchain файлов
|
|
||||||
// ============================================================
|
|
||||||
try {
|
|
||||||
BlockchainTmpRecoveryOnStartup.runRecoveryOrThrow();
|
|
||||||
} catch (Exception e) {
|
|
||||||
// Уже должно быть “большое” уведомление через BlockchainAdminNotifier,
|
|
||||||
// но на всякий случай логируем ещё раз.
|
|
||||||
log.error("❌ Сервер НЕ будет запущен: критическая ошибка восстановления blockchain tmp-файлов.", e);
|
|
||||||
throw e; // останавливаем запуск
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// 1) Настройки порта
|
|
||||||
// ============================================================
|
|
||||||
AppConfig config = AppConfig.getInstance();
|
|
||||||
int port = 7070;
|
|
||||||
try {
|
|
||||||
String portStr = config.getParam("server.port");
|
|
||||||
if (portStr != null && !portStr.isBlank()) {
|
|
||||||
port = Integer.parseInt(portStr.trim());
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.info("Не удалось прочитать параметр server.port, используем порт по умолчанию {}", port);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// 2) Запуск Jetty WS
|
|
||||||
// ============================================================
|
|
||||||
Server server = new Server(port);
|
|
||||||
|
|
||||||
ServletContextHandler context = new ServletContextHandler();
|
|
||||||
context.setContextPath("/");
|
|
||||||
server.setHandler(context);
|
|
||||||
|
|
||||||
// Инициализация контейнера WebSocket
|
|
||||||
JettyWebSocketServletContainerInitializer.configure(context, (servletContext, wsContainer) -> {
|
|
||||||
// Таймаут простоя соединения (Jetty 11 синтаксис)
|
|
||||||
wsContainer.setIdleTimeout(Duration.ofMinutes(5));
|
|
||||||
|
|
||||||
// Маппинг эндпоинта
|
|
||||||
wsContainer.addMapping("/ws", (req, resp) -> new BlockchainWsEndpoint());
|
|
||||||
});
|
|
||||||
|
|
||||||
server.start();
|
|
||||||
log.info("✅ WS сервер запущен на ws://localhost:{}/ws", port);
|
|
||||||
server.join();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
OUTFILE="all_files.txt"
|
|
||||||
|
|
||||||
# очищаем или создаём файл
|
|
||||||
: > "$OUTFILE"
|
|
||||||
|
|
||||||
# собрать только *.java файлы и вывести их содержимое в файл
|
|
||||||
find . -type f -name "*.java" | sort | while read -r f; do
|
|
||||||
cat "$f" >> "$OUTFILE"
|
|
||||||
echo >> "$OUTFILE" # пустая строка-разделитель
|
|
||||||
done
|
|
||||||
|
|
||||||
# скопировать весь файл в буфер обмена (Wayland)
|
|
||||||
wl-copy < "$OUTFILE"
|
|
||||||
|
|
||||||
echo "Готово!"
|
|
||||||
echo "Все .java файлы собраны в $OUTFILE"
|
|
||||||
echo "Содержимое скопировано в буфер обмена (Wayland)"
|
|
||||||
@ -1,38 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
OUTFILE="all_files.txt"
|
|
||||||
SKIPFILE="skip.txt"
|
|
||||||
|
|
||||||
# очищаем или создаём файл
|
|
||||||
: > "$OUTFILE"
|
|
||||||
|
|
||||||
# читаем список исключённых имён (без расширения) в массив
|
|
||||||
if [[ -f "$SKIPFILE" ]]; then
|
|
||||||
mapfile -t SKIP_LIST < "$SKIPFILE"
|
|
||||||
else
|
|
||||||
SKIP_LIST=()
|
|
||||||
fi
|
|
||||||
|
|
||||||
find . -type f -name "*.java" | sort | while read -r f; do
|
|
||||||
fname=$(basename "$f" .java) # имя файла без расширения
|
|
||||||
|
|
||||||
# проверяем, есть ли имя в списке исключений
|
|
||||||
skip=false
|
|
||||||
for skipf in "${SKIP_LIST[@]}"; do
|
|
||||||
if [[ "$fname" == "$skipf" ]]; then
|
|
||||||
skip=true
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
if [[ "$skip" == true ]]; then
|
|
||||||
echo "Пропускаем $f"
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
|
|
||||||
cat "$f" >> "$OUTFILE"
|
|
||||||
echo >> "$OUTFILE" # пустая строка-разделитель
|
|
||||||
done
|
|
||||||
|
|
||||||
echo "Готово! Все .java файлы собраны в $OUTFILE (кроме исключённых из $SKIPFILE)"
|
|
||||||
@ -1,76 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
# OUTFILE:
|
|
||||||
# - если пустая строка ("") -> в файл НЕ пишем, только в буфер
|
|
||||||
# - если не пустая -> пишем в файл + (если есть wl-copy) копируем в буфер
|
|
||||||
OUTFILE="all_files.txt"
|
|
||||||
# OUTFILE=""
|
|
||||||
|
|
||||||
# === НАСТРОЙКА: перечисляй тут пути (каталоги и/или конкретные файлы) ===
|
|
||||||
# - Если путь указывает на ФАЙЛ: берём его ВСЕГДА, даже если это не .java
|
|
||||||
# - Если путь указывает на КАТАЛОГ: рекурсивно берём только *.java внутри
|
|
||||||
# - Пустые строки игнорируются
|
|
||||||
TARGETS=(
|
|
||||||
#"./src/main/java"
|
|
||||||
# "./server"
|
|
||||||
# /home/ai/work/SHiNE/SHiNE-server/shine-server-blockchain
|
|
||||||
"/home/ai/work/SHiNE/SHiNE-server/shine-server-blockchain"
|
|
||||||
"/home/ai/work/SHiNE/SHiNE-server/shine-server-db"
|
|
||||||
)
|
|
||||||
|
|
||||||
RED=$'\033[0;31m'
|
|
||||||
RESET=$'\033[0m'
|
|
||||||
|
|
||||||
warn_red() {
|
|
||||||
echo "${RED}WARN:${RESET} $*" >&2
|
|
||||||
}
|
|
||||||
|
|
||||||
# временные файлы
|
|
||||||
TMP_LIST="$(mktemp)"
|
|
||||||
TMP_OUT="$(mktemp)"
|
|
||||||
trap 'rm -f "$TMP_LIST" "$TMP_OUT"' EXIT
|
|
||||||
|
|
||||||
# собрать пути
|
|
||||||
for path in "${TARGETS[@]}"; do
|
|
||||||
path="$(printf '%s' "$path" | sed -e 's/^[[:space:]]\+//' -e 's/[[:space:]]\+$//')"
|
|
||||||
[[ -z "$path" ]] && continue
|
|
||||||
|
|
||||||
if [[ -f "$path" ]]; then
|
|
||||||
printf '%s\n' "$path" >> "$TMP_LIST"
|
|
||||||
elif [[ -d "$path" ]]; then
|
|
||||||
find "$path" -type f -name "*.java" >> "$TMP_LIST"
|
|
||||||
else
|
|
||||||
warn_red "Не найдено (пропускаю): $path"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
# склеиваем в TMP_OUT
|
|
||||||
sort -u "$TMP_LIST" | while IFS= read -r f; do
|
|
||||||
if [[ ! -f "$f" ]]; then
|
|
||||||
warn_red "Файл исчез (пропускаю): $f"
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
cat "$f" >> "$TMP_OUT"
|
|
||||||
echo >> "$TMP_OUT"
|
|
||||||
done
|
|
||||||
|
|
||||||
# если OUTFILE не пуст — пишем файл
|
|
||||||
if [[ -n "${OUTFILE:-}" ]]; then
|
|
||||||
: > "$OUTFILE"
|
|
||||||
cat "$TMP_OUT" > "$OUTFILE"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# копирование в буфер (Wayland), если доступно
|
|
||||||
if command -v wl-copy >/dev/null 2>&1; then
|
|
||||||
wl-copy < "$TMP_OUT"
|
|
||||||
else
|
|
||||||
warn_red "wl-copy не найден — в буфер не скопировано."
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Готово!"
|
|
||||||
if [[ -n "${OUTFILE:-}" ]]; then
|
|
||||||
echo "Все файлы собраны в $OUTFILE"
|
|
||||||
else
|
|
||||||
echo "OUTFILE пуст — в файл не писали, только буфер (если wl-copy доступен)"
|
|
||||||
fi
|
|
||||||
@ -1,39 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
OUTFILE="all_files.txt"
|
|
||||||
|
|
||||||
# === Список файлов (ТОЛЬКО имена без расширений) ===
|
|
||||||
# пример: Main значит Main.java, Utils значит Utils.java
|
|
||||||
NAMES=(
|
|
||||||
"IT_04_UserParams_NoAuth"
|
|
||||||
"AddBlockSender"
|
|
||||||
"ChainState"
|
|
||||||
"JsonBuilders"
|
|
||||||
)
|
|
||||||
|
|
||||||
# очищаем или создаём файл
|
|
||||||
: > "$OUTFILE"
|
|
||||||
|
|
||||||
# Быстрый фильтр: сделаем хеш-таблицу из имён (ассоц. массив)
|
|
||||||
declare -A WANT=()
|
|
||||||
for name in "${NAMES[@]}"; do
|
|
||||||
WANT["$name"]=1
|
|
||||||
done
|
|
||||||
|
|
||||||
# собрать только нужные *.java по базовому имени
|
|
||||||
find . -type f -name "*.java" | sort | while read -r f; do
|
|
||||||
base="$(basename "$f" .java)"
|
|
||||||
if [[ -n "${WANT[$base]+x}" ]]; then
|
|
||||||
cat "$f" >> "$OUTFILE"
|
|
||||||
echo >> "$OUTFILE" # пустая строка-разделитель
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
# скопировать весь файл в буфер обмена (Wayland)
|
|
||||||
wl-copy < "$OUTFILE"
|
|
||||||
|
|
||||||
echo "Готово!"
|
|
||||||
echo "Выбрано имён: ${#NAMES[@]}"
|
|
||||||
echo "Все нужные .java файлы собраны в $OUTFILE"
|
|
||||||
echo "Содержимое скопировано в буфер обмена (Wayland)"
|
|
||||||
File diff suppressed because it is too large
Load Diff
@ -1,20 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
OUTFILE="all_files.txt"
|
|
||||||
|
|
||||||
# очищаем или создаём файл
|
|
||||||
: > "$OUTFILE"
|
|
||||||
|
|
||||||
# собрать только *.java файлы и вывести их содержимое в файл
|
|
||||||
find . -type f -name "*.java" | sort | while read -r f; do
|
|
||||||
cat "$f" >> "$OUTFILE"
|
|
||||||
echo >> "$OUTFILE" # пустая строка-разделитель
|
|
||||||
done
|
|
||||||
|
|
||||||
# скопировать весь файл в буфер обмена (Wayland)
|
|
||||||
wl-copy < "$OUTFILE"
|
|
||||||
|
|
||||||
echo "Готово!"
|
|
||||||
echo "Все .java файлы собраны в $OUTFILE"
|
|
||||||
echo "Содержимое скопировано в буфер обмена (Wayland)"
|
|
||||||
@ -160,9 +160,9 @@ public class IT_01_AddUser {
|
|||||||
String blockchainName = JsonParsers.userBlockchainName(resp);
|
String blockchainName = JsonParsers.userBlockchainName(resp);
|
||||||
String solanaKey = JsonParsers.userSolanaKey(resp);
|
String solanaKey = JsonParsers.userSolanaKey(resp);
|
||||||
String blockchainKey = JsonParsers.userBlockchainKey(resp);
|
String blockchainKey = JsonParsers.userBlockchainKey(resp);
|
||||||
String deviceKey = JsonParsers.userDeviceKey(resp);
|
String clientKey = JsonParsers.userClientKey(resp);
|
||||||
|
|
||||||
if (isBlank(login) || isBlank(blockchainName) || isBlank(solanaKey) || isBlank(blockchainKey) || isBlank(deviceKey)) {
|
if (isBlank(login) || isBlank(blockchainName) || isBlank(solanaKey) || isBlank(blockchainKey) || isBlank(clientKey)) {
|
||||||
r.fail("GetUser: exists=true, но поля пустые/неполные, resp=" + resp);
|
r.fail("GetUser: exists=true, но поля пустые/неполные, resp=" + resp);
|
||||||
fail("GetUser returned incomplete user data");
|
fail("GetUser returned incomplete user data");
|
||||||
}
|
}
|
||||||
@ -187,7 +187,7 @@ public class IT_01_AddUser {
|
|||||||
// ключи должны совпадать с теми, что AddUser использует при регистрации
|
// ключи должны совпадать с теми, что AddUser использует при регистрации
|
||||||
String expSol = TestConfig.solanaPublicKeyB64(canonical);
|
String expSol = TestConfig.solanaPublicKeyB64(canonical);
|
||||||
String expBchKey = TestConfig.blockchainPublicKeyB64(canonical);
|
String expBchKey = TestConfig.blockchainPublicKeyB64(canonical);
|
||||||
String expDev = TestConfig.devicePublicKeyB64(canonical);
|
String expDev = TestConfig.clientPublicKeyB64(canonical);
|
||||||
|
|
||||||
if (!solanaKey.equals(expSol)) {
|
if (!solanaKey.equals(expSol)) {
|
||||||
r.fail("GetUser: solanaKey mismatch, resp=" + resp);
|
r.fail("GetUser: solanaKey mismatch, resp=" + resp);
|
||||||
@ -197,9 +197,9 @@ public class IT_01_AddUser {
|
|||||||
r.fail("GetUser: blockchainKey mismatch, resp=" + resp);
|
r.fail("GetUser: blockchainKey mismatch, resp=" + resp);
|
||||||
fail("GetUser blockchainKey mismatch");
|
fail("GetUser blockchainKey mismatch");
|
||||||
}
|
}
|
||||||
if (!deviceKey.equals(expDev)) {
|
if (!clientKey.equals(expDev)) {
|
||||||
r.fail("GetUser: deviceKey mismatch, resp=" + resp);
|
r.fail("GetUser: clientKey mismatch, resp=" + resp);
|
||||||
fail("GetUser deviceKey mismatch");
|
fail("GetUser clientKey mismatch");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -270,7 +270,7 @@ public class IT_01_AddUser {
|
|||||||
"blockchainName": "%s",
|
"blockchainName": "%s",
|
||||||
"solanaKey": "%s",
|
"solanaKey": "%s",
|
||||||
"blockchainKey": "%s",
|
"blockchainKey": "%s",
|
||||||
"deviceKey": "%s",
|
"clientKey": "%s",
|
||||||
"bchLimit": %d
|
"bchLimit": %d
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,7 +23,7 @@ import static org.junit.jupiter.api.Assertions.*;
|
|||||||
* - и после завершения оставить в БД 3 активных сессии (S1,S2,S3)
|
* - и после завершения оставить в БД 3 активных сессии (S1,S2,S3)
|
||||||
*
|
*
|
||||||
* Протокол v2:
|
* Протокол v2:
|
||||||
* - создание сессии: AuthChallenge -> CreateAuthSession (deviceKey подпись, + deviceKey + sessionKey)
|
* - создание сессии: AuthChallenge -> CreateAuthSession (clientKey подпись, + clientKey + sessionKey)
|
||||||
* - вход в сессию: SessionChallenge(sessionId) -> nonce, затем SessionLogin(sessionId,time,signature(sessionKey))
|
* - вход в сессию: SessionChallenge(sessionId) -> nonce, затем SessionLogin(sessionId,time,signature(sessionKey))
|
||||||
* - ListSessions и CloseActiveSession доступны только в AUTH_STATUS_USER (после SessionLogin)
|
* - ListSessions и CloseActiveSession доступны только в AUTH_STATUS_USER (после SessionLogin)
|
||||||
*/
|
*/
|
||||||
@ -122,7 +122,7 @@ public class IT_02_Sessions {
|
|||||||
// storagePwd на клиенте (сохраняем, чтобы потом проверить, что сервер вернул именно его)
|
// storagePwd на клиенте (сохраняем, чтобы потом проверить, что сервер вернул именно его)
|
||||||
String storagePwd = TestConfig.fakeStoragePwd();
|
String storagePwd = TestConfig.fakeStoragePwd();
|
||||||
|
|
||||||
// шаг 2: CreateAuthSession (device подпись + deviceKey + sessionKey)
|
// шаг 2: CreateAuthSession (device подпись + clientKey + sessionKey)
|
||||||
String createResp = ws.call(
|
String createResp = ws.call(
|
||||||
"CreateAuthSession(" + label + ")",
|
"CreateAuthSession(" + label + ")",
|
||||||
JsonBuilders.createAuthSessionV2(login, authNonce, storagePwd, sessionMaterial.sessionKey()),
|
JsonBuilders.createAuthSessionV2(login, authNonce, storagePwd, sessionMaterial.sessionKey()),
|
||||||
@ -194,7 +194,7 @@ public class IT_02_Sessions {
|
|||||||
"storagePwd": "%s",
|
"storagePwd": "%s",
|
||||||
"timeMs": %d,
|
"timeMs": %d,
|
||||||
"authNonce": "%s",
|
"authNonce": "%s",
|
||||||
"deviceKey": "%s",
|
"clientKey": "%s",
|
||||||
"signatureB64": "%s",
|
"signatureB64": "%s",
|
||||||
"clientInfo": "%s"
|
"clientInfo": "%s"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -40,15 +40,15 @@ public class IT_04_UserParams_NoAuth {
|
|||||||
Duration timeout = Duration.ofSeconds(5);
|
Duration timeout = Duration.ofSeconds(5);
|
||||||
|
|
||||||
final String login = TestConfig.LOGIN();
|
final String login = TestConfig.LOGIN();
|
||||||
final String deviceKeyB64 = TestConfig.devicePublicKeyB64(login);
|
final String clientKeyB64 = TestConfig.clientPublicKeyB64(login);
|
||||||
final byte[] devicePrivKey = TestConfig.getDevicePrivatKey(login);
|
final byte[] clientPrivKey = TestConfig.getDevicePrivatKey(login);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 1) сохранить param1
|
// 1) сохранить param1
|
||||||
final String p1 = "profile:name";
|
final String p1 = "profile:name";
|
||||||
final String v1 = "Anna";
|
final String v1 = "Anna";
|
||||||
final long t1 = System.currentTimeMillis();
|
final long t1 = System.currentTimeMillis();
|
||||||
upsertUserParam_OK(r, login, p1, t1, v1, deviceKeyB64, devicePrivKey, timeout);
|
upsertUserParam_OK(r, login, p1, t1, v1, clientKeyB64, clientPrivKey, timeout);
|
||||||
|
|
||||||
// 2) получить param1 и проверить
|
// 2) получить param1 и проверить
|
||||||
NetParam got1 = getUserParam_200(r, login, p1, timeout);
|
NetParam got1 = getUserParam_200(r, login, p1, timeout);
|
||||||
@ -56,7 +56,7 @@ public class IT_04_UserParams_NoAuth {
|
|||||||
assertEquals(p1, got1.param);
|
assertEquals(p1, got1.param);
|
||||||
assertEquals(t1, got1.timeMs);
|
assertEquals(t1, got1.timeMs);
|
||||||
assertEquals(v1, got1.value);
|
assertEquals(v1, got1.value);
|
||||||
assertEquals(deviceKeyB64, got1.deviceKeyB64);
|
assertEquals(clientKeyB64, got1.clientKeyB64);
|
||||||
assertNotNull(got1.signatureB64);
|
assertNotNull(got1.signatureB64);
|
||||||
assertFalse(got1.signatureB64.isBlank());
|
assertFalse(got1.signatureB64.isBlank());
|
||||||
r.ok("GetUserParam(param1) OK");
|
r.ok("GetUserParam(param1) OK");
|
||||||
@ -65,12 +65,12 @@ public class IT_04_UserParams_NoAuth {
|
|||||||
final String p2 = "profile:city";
|
final String p2 = "profile:city";
|
||||||
final String v2 = "Amsterdam";
|
final String v2 = "Amsterdam";
|
||||||
final long t2 = t1 + 10;
|
final long t2 = t1 + 10;
|
||||||
upsertUserParam_OK(r, login, p2, t2, v2, deviceKeyB64, devicePrivKey, timeout);
|
upsertUserParam_OK(r, login, p2, t2, v2, clientKeyB64, clientPrivKey, timeout);
|
||||||
|
|
||||||
// 4) обновить param1
|
// 4) обновить param1
|
||||||
final String v1b = "Anna Updated";
|
final String v1b = "Anna Updated";
|
||||||
final long t1b = t2 + 10;
|
final long t1b = t2 + 10;
|
||||||
upsertUserParam_OK(r, login, p1, t1b, v1b, deviceKeyB64, devicePrivKey, timeout);
|
upsertUserParam_OK(r, login, p1, t1b, v1b, clientKeyB64, clientPrivKey, timeout);
|
||||||
|
|
||||||
NetParam got1b = getUserParam_200(r, login, p1, timeout);
|
NetParam got1b = getUserParam_200(r, login, p1, timeout);
|
||||||
assertEquals(t1b, got1b.timeMs);
|
assertEquals(t1b, got1b.timeMs);
|
||||||
@ -92,8 +92,8 @@ public class IT_04_UserParams_NoAuth {
|
|||||||
assertEquals(t2, lp2.timeMs);
|
assertEquals(t2, lp2.timeMs);
|
||||||
assertEquals(v2, lp2.value);
|
assertEquals(v2, lp2.value);
|
||||||
|
|
||||||
assertEquals(deviceKeyB64, lp1.deviceKeyB64);
|
assertEquals(clientKeyB64, lp1.clientKeyB64);
|
||||||
assertEquals(deviceKeyB64, lp2.deviceKeyB64);
|
assertEquals(clientKeyB64, lp2.clientKeyB64);
|
||||||
assertNotNull(lp1.signatureB64);
|
assertNotNull(lp1.signatureB64);
|
||||||
assertNotNull(lp2.signatureB64);
|
assertNotNull(lp2.signatureB64);
|
||||||
|
|
||||||
@ -110,8 +110,8 @@ public class IT_04_UserParams_NoAuth {
|
|||||||
// WS helpers: Upsert/Get/List
|
// WS helpers: Upsert/Get/List
|
||||||
// =================================================================================
|
// =================================================================================
|
||||||
|
|
||||||
private static void upsertUserParam_OK(TestResult r, String login, String param, long timeMs, String value, String deviceKeyB64, byte[] devicePrivKey, Duration timeout) {
|
private static void upsertUserParam_OK(TestResult r, String login, String param, long timeMs, String value, String clientKeyB64, byte[] clientPrivKey, Duration timeout) {
|
||||||
String signatureB64 = signUserParam(devicePrivKey, login, param, timeMs, value);
|
String signatureB64 = signUserParam(clientPrivKey, login, param, timeMs, value);
|
||||||
|
|
||||||
String reqJson = """
|
String reqJson = """
|
||||||
{
|
{
|
||||||
@ -122,11 +122,11 @@ public class IT_04_UserParams_NoAuth {
|
|||||||
"param": "%s",
|
"param": "%s",
|
||||||
"time_ms": %d,
|
"time_ms": %d,
|
||||||
"value": "%s",
|
"value": "%s",
|
||||||
"device_key": "%s",
|
"client_key": "%s",
|
||||||
"signature": "%s"
|
"signature": "%s"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
""".formatted(TestIds.next("upsert"), login, param, timeMs, jsonEscape(value), deviceKeyB64, signatureB64);
|
""".formatted(TestIds.next("upsert"), login, param, timeMs, jsonEscape(value), clientKeyB64, signatureB64);
|
||||||
|
|
||||||
try (WsSession ws = WsSession.open()) {
|
try (WsSession ws = WsSession.open()) {
|
||||||
String resp = ws.call("UpsertUserParam(" + param + ")", reqJson, timeout);
|
String resp = ws.call("UpsertUserParam(" + param + ")", reqJson, timeout);
|
||||||
@ -187,7 +187,7 @@ public class IT_04_UserParams_NoAuth {
|
|||||||
p.param = text(payload, "param");
|
p.param = text(payload, "param");
|
||||||
p.timeMs = longVal(payload, "time_ms");
|
p.timeMs = longVal(payload, "time_ms");
|
||||||
p.value = text(payload, "value");
|
p.value = text(payload, "value");
|
||||||
p.deviceKeyB64 = text(payload, "device_key");
|
p.clientKeyB64 = text(payload, "client_key");
|
||||||
p.signatureB64 = text(payload, "signature");
|
p.signatureB64 = text(payload, "signature");
|
||||||
return p;
|
return p;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
@ -214,7 +214,7 @@ public class IT_04_UserParams_NoAuth {
|
|||||||
p.param = text(it, "param");
|
p.param = text(it, "param");
|
||||||
p.timeMs = longVal(it, "time_ms");
|
p.timeMs = longVal(it, "time_ms");
|
||||||
p.value = text(it, "value");
|
p.value = text(it, "value");
|
||||||
p.deviceKeyB64 = text(it, "device_key");
|
p.clientKeyB64 = text(it, "client_key");
|
||||||
p.signatureB64 = text(it, "signature");
|
p.signatureB64 = text(it, "signature");
|
||||||
out.items = out.itemsAppend(p);
|
out.items = out.itemsAppend(p);
|
||||||
}
|
}
|
||||||
@ -239,10 +239,10 @@ public class IT_04_UserParams_NoAuth {
|
|||||||
// Signature + JSON helpers
|
// Signature + JSON helpers
|
||||||
// =================================================================================
|
// =================================================================================
|
||||||
|
|
||||||
private static String signUserParam(byte[] devicePrivKey, String login, String param, long timeMs, String value) {
|
private static String signUserParam(byte[] clientPrivKey, String login, String param, long timeMs, String value) {
|
||||||
String signText = ShineSignatureConstants.USER_PARAMETER_PREFIX + login + param + timeMs + value;
|
String signText = ShineSignatureConstants.USER_PARAMETER_PREFIX + login + param + timeMs + value;
|
||||||
byte[] signBytes = signText.getBytes(StandardCharsets.UTF_8);
|
byte[] signBytes = signText.getBytes(StandardCharsets.UTF_8);
|
||||||
byte[] sig64 = Ed25519Util.sign(signBytes, devicePrivKey);
|
byte[] sig64 = Ed25519Util.sign(signBytes, clientPrivKey);
|
||||||
return Base64.getEncoder().encodeToString(sig64);
|
return Base64.getEncoder().encodeToString(sig64);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -260,7 +260,7 @@ public class IT_04_UserParams_NoAuth {
|
|||||||
String param;
|
String param;
|
||||||
long timeMs;
|
long timeMs;
|
||||||
String value;
|
String value;
|
||||||
String deviceKeyB64;
|
String clientKeyB64;
|
||||||
String signatureB64;
|
String signatureB64;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -216,7 +216,7 @@ public class IT_07_EspPairing {
|
|||||||
entry.setBlockchainName(TestConfig.getBlockchainName(LOGIN));
|
entry.setBlockchainName(TestConfig.getBlockchainName(LOGIN));
|
||||||
entry.setSolanaKey(TestConfig.solanaPublicKeyB64(LOGIN));
|
entry.setSolanaKey(TestConfig.solanaPublicKeyB64(LOGIN));
|
||||||
entry.setBlockchainKey(TestConfig.blockchainPublicKeyB64(LOGIN));
|
entry.setBlockchainKey(TestConfig.blockchainPublicKeyB64(LOGIN));
|
||||||
entry.setDeviceKey(TestConfig.devicePublicKeyB64(LOGIN));
|
entry.setClientKey(TestConfig.clientPublicKeyB64(LOGIN));
|
||||||
SolanaUsersDAO.getInstance().insert(entry);
|
SolanaUsersDAO.getInstance().insert(entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -170,7 +170,7 @@ public final class SeedDataPopulationHelper {
|
|||||||
"blockchainName": "%s",
|
"blockchainName": "%s",
|
||||||
"solanaKey": "%s",
|
"solanaKey": "%s",
|
||||||
"blockchainKey": "%s",
|
"blockchainKey": "%s",
|
||||||
"deviceKey": "%s",
|
"clientKey": "%s",
|
||||||
"bchLimit": 50000000
|
"bchLimit": 50000000
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -180,7 +180,7 @@ public final class SeedDataPopulationHelper {
|
|||||||
bch(login),
|
bch(login),
|
||||||
keys.solanaPublicB64,
|
keys.solanaPublicB64,
|
||||||
keys.blockchainPublicB64,
|
keys.blockchainPublicB64,
|
||||||
keys.devicePublicB64
|
keys.clientPublicB64
|
||||||
);
|
);
|
||||||
|
|
||||||
String resp = ws.call("AddUser#" + login, req, timeout);
|
String resp = ws.call("AddUser#" + login, req, timeout);
|
||||||
@ -244,12 +244,11 @@ public final class SeedDataPopulationHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static UserKeys deriveKeysFromPassword(String password) {
|
private static UserKeys deriveKeysFromPassword(String password) {
|
||||||
byte[] base = HashSHA256Util.sha256(password.getBytes(StandardCharsets.UTF_8));
|
byte[] masterSecret = HashSHA256Util.sha256(password.getBytes(StandardCharsets.UTF_8));
|
||||||
String baseB64 = Base64.getEncoder().encodeToString(base);
|
|
||||||
|
|
||||||
byte[] rootPriv = HashSHA256Util.sha256((baseB64 + "root.key").getBytes(StandardCharsets.UTF_8));
|
byte[] rootPriv = deriveSeed(masterSecret, "root.key");
|
||||||
byte[] bchPriv = HashSHA256Util.sha256((baseB64 + "bch.key").getBytes(StandardCharsets.UTF_8));
|
byte[] bchPriv = deriveSeed(masterSecret, "blockchain.key");
|
||||||
byte[] devPriv = HashSHA256Util.sha256((baseB64 + "dev.key").getBytes(StandardCharsets.UTF_8));
|
byte[] devPriv = deriveSeed(masterSecret, "client.key");
|
||||||
|
|
||||||
String rootPubB64 = Base64.getEncoder().encodeToString(Ed25519Util.derivePublicKey(rootPriv));
|
String rootPubB64 = Base64.getEncoder().encodeToString(Ed25519Util.derivePublicKey(rootPriv));
|
||||||
String bchPubB64 = Base64.getEncoder().encodeToString(Ed25519Util.derivePublicKey(bchPriv));
|
String bchPubB64 = Base64.getEncoder().encodeToString(Ed25519Util.derivePublicKey(bchPriv));
|
||||||
@ -258,6 +257,21 @@ public final class SeedDataPopulationHelper {
|
|||||||
return new UserKeys(rootPubB64, bchPubB64, devPubB64, bchPriv);
|
return new UserKeys(rootPubB64, bchPubB64, devPubB64, bchPriv);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static byte[] deriveSeed(byte[] masterSecret, String suffix) {
|
||||||
|
byte[] prefix = "SHiNE-key".getBytes(StandardCharsets.UTF_8);
|
||||||
|
byte[] suffixBytes = suffix.getBytes(StandardCharsets.UTF_8);
|
||||||
|
byte[] material = new byte[prefix.length + 1 + masterSecret.length + 1 + suffixBytes.length];
|
||||||
|
int offset = 0;
|
||||||
|
System.arraycopy(prefix, 0, material, offset, prefix.length);
|
||||||
|
offset += prefix.length;
|
||||||
|
material[offset++] = 0;
|
||||||
|
System.arraycopy(masterSecret, 0, material, offset, masterSecret.length);
|
||||||
|
offset += masterSecret.length;
|
||||||
|
material[offset++] = 0;
|
||||||
|
System.arraycopy(suffixBytes, 0, material, offset, suffixBytes.length);
|
||||||
|
return HashSHA256Util.sha256(material);
|
||||||
|
}
|
||||||
|
|
||||||
public static final class UserSpec {
|
public static final class UserSpec {
|
||||||
public final String login;
|
public final String login;
|
||||||
public final String firstName;
|
public final String firstName;
|
||||||
@ -292,16 +306,16 @@ public final class SeedDataPopulationHelper {
|
|||||||
private static final class UserKeys {
|
private static final class UserKeys {
|
||||||
final String solanaPublicB64;
|
final String solanaPublicB64;
|
||||||
final String blockchainPublicB64;
|
final String blockchainPublicB64;
|
||||||
final String devicePublicB64;
|
final String clientPublicB64;
|
||||||
final byte[] blockchainPrivate32;
|
final byte[] blockchainPrivate32;
|
||||||
|
|
||||||
private UserKeys(String solanaPublicB64,
|
private UserKeys(String solanaPublicB64,
|
||||||
String blockchainPublicB64,
|
String blockchainPublicB64,
|
||||||
String devicePublicB64,
|
String clientPublicB64,
|
||||||
byte[] blockchainPrivate32) {
|
byte[] blockchainPrivate32) {
|
||||||
this.solanaPublicB64 = solanaPublicB64;
|
this.solanaPublicB64 = solanaPublicB64;
|
||||||
this.blockchainPublicB64 = blockchainPublicB64;
|
this.blockchainPublicB64 = blockchainPublicB64;
|
||||||
this.devicePublicB64 = devicePublicB64;
|
this.clientPublicB64 = clientPublicB64;
|
||||||
this.blockchainPrivate32 = blockchainPrivate32;
|
this.blockchainPrivate32 = blockchainPrivate32;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -55,8 +55,8 @@ public final class TestConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ============ key maps ============
|
// ============ key maps ============
|
||||||
private static final Map<String, byte[]> devicePriv = new ConcurrentHashMap<>();
|
private static final Map<String, byte[]> clientPriv = new ConcurrentHashMap<>();
|
||||||
private static final Map<String, byte[]> devicePub = new ConcurrentHashMap<>();
|
private static final Map<String, byte[]> clientPub = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
private static final Map<String, byte[]> solanaPriv = new ConcurrentHashMap<>();
|
private static final Map<String, byte[]> solanaPriv = new ConcurrentHashMap<>();
|
||||||
private static final Map<String, byte[]> solanaPub = new ConcurrentHashMap<>();
|
private static final Map<String, byte[]> solanaPub = new ConcurrentHashMap<>();
|
||||||
@ -80,8 +80,8 @@ public final class TestConfig {
|
|||||||
byte[] pub = Ed25519Util.derivePublicKey(priv);
|
byte[] pub = Ed25519Util.derivePublicKey(priv);
|
||||||
|
|
||||||
// пока одинаковые
|
// пока одинаковые
|
||||||
devicePriv.put(login, priv);
|
clientPriv.put(login, priv);
|
||||||
devicePub.put(login, pub);
|
clientPub.put(login, pub);
|
||||||
|
|
||||||
solanaPriv.put(login, priv);
|
solanaPriv.put(login, priv);
|
||||||
solanaPub.put(login, pub);
|
solanaPub.put(login, pub);
|
||||||
@ -99,8 +99,8 @@ public final class TestConfig {
|
|||||||
|
|
||||||
// ============ requested getters (with your names) ============
|
// ============ requested getters (with your names) ============
|
||||||
|
|
||||||
public static byte[] getDevicePrivatKey(String login) { return cloneOrThrow(devicePriv.get(login), "devicePriv", login); }
|
public static byte[] getDevicePrivatKey(String login) { return cloneOrThrow(clientPriv.get(login), "clientPriv", login); }
|
||||||
public static byte[] getDevicePublicKey(String login) { return cloneOrThrow(devicePub.get(login), "devicePub", login); }
|
public static byte[] getDevicePublicKey(String login) { return cloneOrThrow(clientPub.get(login), "clientPub", login); }
|
||||||
|
|
||||||
public static byte[] getSolanaPrivatKey(String login) { return cloneOrThrow(solanaPriv.get(login), "solanaPriv", login); }
|
public static byte[] getSolanaPrivatKey(String login) { return cloneOrThrow(solanaPriv.get(login), "solanaPriv", login); }
|
||||||
public static byte[] getSolanaPublicKey(String login) { return cloneOrThrow(solanaPub.get(login), "solanaPub", login); }
|
public static byte[] getSolanaPublicKey(String login) { return cloneOrThrow(solanaPub.get(login), "solanaPub", login); }
|
||||||
@ -113,7 +113,7 @@ public final class TestConfig {
|
|||||||
public static byte[] getSessionPublicKey(String login) { return cloneOrThrow(sessionPub.get(login), "sessionPub", login); }
|
public static byte[] getSessionPublicKey(String login) { return cloneOrThrow(sessionPub.get(login), "sessionPub", login); }
|
||||||
|
|
||||||
// ============ base64 helpers ============
|
// ============ base64 helpers ============
|
||||||
public static String devicePublicKeyB64(String login) { return Base64.getEncoder().encodeToString(getDevicePublicKey(login)); }
|
public static String clientPublicKeyB64(String login) { return Base64.getEncoder().encodeToString(getDevicePublicKey(login)); }
|
||||||
public static String solanaPublicKeyB64(String login) { return Base64.getEncoder().encodeToString(getSolanaPublicKey(login)); }
|
public static String solanaPublicKeyB64(String login) { return Base64.getEncoder().encodeToString(getSolanaPublicKey(login)); }
|
||||||
public static String blockchainPublicKeyB64(String login) { return Base64.getEncoder().encodeToString(getBlockchainPublicKey(login)); }
|
public static String blockchainPublicKeyB64(String login) { return Base64.getEncoder().encodeToString(getBlockchainPublicKey(login)); }
|
||||||
|
|
||||||
@ -136,9 +136,9 @@ public final class TestConfig {
|
|||||||
public static String BLOCKCHAIN2_PUBKEY_B64() { return blockchainPublicKeyB64(LOGIN2()); }
|
public static String BLOCKCHAIN2_PUBKEY_B64() { return blockchainPublicKeyB64(LOGIN2()); }
|
||||||
public static String BLOCKCHAIN3_PUBKEY_B64() { return blockchainPublicKeyB64(LOGIN3()); }
|
public static String BLOCKCHAIN3_PUBKEY_B64() { return blockchainPublicKeyB64(LOGIN3()); }
|
||||||
|
|
||||||
public static String DEVICE_PUBKEY_B64() { return devicePublicKeyB64(LOGIN()); }
|
public static String DEVICE_PUBKEY_B64() { return clientPublicKeyB64(LOGIN()); }
|
||||||
public static String DEVICE2_PUBKEY_B64() { return devicePublicKeyB64(LOGIN2()); }
|
public static String DEVICE2_PUBKEY_B64() { return clientPublicKeyB64(LOGIN2()); }
|
||||||
public static String DEVICE3_PUBKEY_B64() { return devicePublicKeyB64(LOGIN3()); }
|
public static String DEVICE3_PUBKEY_B64() { return clientPublicKeyB64(LOGIN3()); }
|
||||||
|
|
||||||
// NEW: session pub b64 compat
|
// NEW: session pub b64 compat
|
||||||
public static String SESSION_PUBKEY_B64() { return sessionPublicKeyB64(LOGIN()); }
|
public static String SESSION_PUBKEY_B64() { return sessionPublicKeyB64(LOGIN()); }
|
||||||
|
|||||||
@ -19,7 +19,7 @@ public final class JsonBuilders {
|
|||||||
|
|
||||||
String solanaKeyB64 = TestConfig.solanaPublicKeyB64(login);
|
String solanaKeyB64 = TestConfig.solanaPublicKeyB64(login);
|
||||||
String blockchainKeyB64 = TestConfig.blockchainPublicKeyB64(login);
|
String blockchainKeyB64 = TestConfig.blockchainPublicKeyB64(login);
|
||||||
String deviceKeyB64 = TestConfig.devicePublicKeyB64(login);
|
String clientKeyB64 = TestConfig.clientPublicKeyB64(login);
|
||||||
|
|
||||||
return """
|
return """
|
||||||
{
|
{
|
||||||
@ -30,7 +30,7 @@ public final class JsonBuilders {
|
|||||||
"blockchainName": "%s",
|
"blockchainName": "%s",
|
||||||
"solanaKey": "%s",
|
"solanaKey": "%s",
|
||||||
"blockchainKey": "%s",
|
"blockchainKey": "%s",
|
||||||
"deviceKey": "%s",
|
"clientKey": "%s",
|
||||||
"bchLimit": %d
|
"bchLimit": %d
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -40,7 +40,7 @@ public final class JsonBuilders {
|
|||||||
blockchainName,
|
blockchainName,
|
||||||
solanaKeyB64,
|
solanaKeyB64,
|
||||||
blockchainKeyB64,
|
blockchainKeyB64,
|
||||||
deviceKeyB64,
|
clientKeyB64,
|
||||||
TestConfig.TEST_BCH_LIMIT
|
TestConfig.TEST_BCH_LIMIT
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -133,7 +133,7 @@ public final class JsonBuilders {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ---------------- CreateAuthSession (v2) ----------------
|
// ---------------- CreateAuthSession (v2) ----------------
|
||||||
// Подпись CreateAuthSession делается deviceKey над строкой:
|
// Подпись CreateAuthSession делается clientKey над строкой:
|
||||||
// preimage = "AUTH_CREATE_SESSION:" + login + ":" + sessionKey + ":" + storagePwd + ":" + timeMs + ":" + authNonce
|
// preimage = "AUTH_CREATE_SESSION:" + login + ":" + sessionKey + ":" + storagePwd + ":" + timeMs + ":" + authNonce
|
||||||
|
|
||||||
public static String createAuthSessionV2(String login, String authNonce, String storagePwd, String sessionKey) {
|
public static String createAuthSessionV2(String login, String authNonce, String storagePwd, String sessionKey) {
|
||||||
@ -143,9 +143,9 @@ public final class JsonBuilders {
|
|||||||
public static String createAuthSessionV2(String login, String authNonce, String storagePwd, String sessionKey, int sessionType, String clientPlatform) {
|
public static String createAuthSessionV2(String login, String authNonce, String storagePwd, String sessionKey, int sessionType, String clientPlatform) {
|
||||||
long timeMs = System.currentTimeMillis();
|
long timeMs = System.currentTimeMillis();
|
||||||
|
|
||||||
byte[] devicePriv = TestConfig.getDevicePrivatKey(login);
|
byte[] clientPriv = TestConfig.getDevicePrivatKey(login);
|
||||||
String deviceKey = TestConfig.devicePublicKeyB64(login);
|
String clientKey = TestConfig.clientPublicKeyB64(login);
|
||||||
String sigB64 = signAuthCreateSession(login, sessionKey, storagePwd, timeMs, authNonce, devicePriv);
|
String sigB64 = signAuthCreateSession(login, sessionKey, storagePwd, timeMs, authNonce, clientPriv);
|
||||||
|
|
||||||
String requestId = TestIds.next("create");
|
String requestId = TestIds.next("create");
|
||||||
return """
|
return """
|
||||||
@ -158,7 +158,7 @@ public final class JsonBuilders {
|
|||||||
"sessionKey": "%s",
|
"sessionKey": "%s",
|
||||||
"timeMs": %d,
|
"timeMs": %d,
|
||||||
"authNonce": "%s",
|
"authNonce": "%s",
|
||||||
"deviceKey": "%s",
|
"clientKey": "%s",
|
||||||
"signatureB64": "%s",
|
"signatureB64": "%s",
|
||||||
"sessionType": %d,
|
"sessionType": %d,
|
||||||
"clientPlatform": "%s",
|
"clientPlatform": "%s",
|
||||||
@ -172,7 +172,7 @@ public final class JsonBuilders {
|
|||||||
sessionKey,
|
sessionKey,
|
||||||
timeMs,
|
timeMs,
|
||||||
authNonce,
|
authNonce,
|
||||||
deviceKey,
|
clientKey,
|
||||||
sigB64,
|
sigB64,
|
||||||
sessionType,
|
sessionType,
|
||||||
clientPlatform == null ? "" : clientPlatform,
|
clientPlatform == null ? "" : clientPlatform,
|
||||||
@ -431,12 +431,12 @@ public final class JsonBuilders {
|
|||||||
/**
|
/**
|
||||||
* Подпись CreateAuthSession(v2):
|
* Подпись CreateAuthSession(v2):
|
||||||
* preimage = "AUTH_CREATE_SESSION:" + login + ":" + sessionKey + ":" + storagePwd + ":" + timeMs + ":" + authNonce
|
* preimage = "AUTH_CREATE_SESSION:" + login + ":" + sessionKey + ":" + storagePwd + ":" + timeMs + ":" + authNonce
|
||||||
* подписываем devicePrivKey.
|
* подписываем clientPrivKey.
|
||||||
*/
|
*/
|
||||||
public static String signAuthCreateSession(String login, String sessionKey, String storagePwd, long timeMs, String authNonce, byte[] devicePrivKey) {
|
public static String signAuthCreateSession(String login, String sessionKey, String storagePwd, long timeMs, String authNonce, byte[] clientPrivKey) {
|
||||||
String preimageStr = "AUTH_CREATE_SESSION:" + login + ":" + sessionKey + ":" + storagePwd + ":" + timeMs + ":" + authNonce;
|
String preimageStr = "AUTH_CREATE_SESSION:" + login + ":" + sessionKey + ":" + storagePwd + ":" + timeMs + ":" + authNonce;
|
||||||
byte[] preimage = preimageStr.getBytes(StandardCharsets.UTF_8);
|
byte[] preimage = preimageStr.getBytes(StandardCharsets.UTF_8);
|
||||||
byte[] sig = Ed25519Util.sign(preimage, devicePrivKey);
|
byte[] sig = Ed25519Util.sign(preimage, clientPrivKey);
|
||||||
return Base64.getEncoder().encodeToString(sig);
|
return Base64.getEncoder().encodeToString(sig);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -229,8 +229,8 @@ public final class JsonParsers {
|
|||||||
return getPayloadText(json, "blockchainKey");
|
return getPayloadText(json, "blockchainKey");
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String userDeviceKey(String json) {
|
public static String userClientKey(String json) {
|
||||||
return getPayloadText(json, "deviceKey");
|
return getPayloadText(json, "clientKey");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------- SearchUsers helpers ----------------
|
// ---------------- SearchUsers helpers ----------------
|
||||||
|
|||||||
@ -9,7 +9,7 @@
|
|||||||
Добавить полностью рабочий сценарий доставки личных сообщений с приоритетом:
|
Добавить полностью рабочий сценарий доставки личных сообщений с приоритетом:
|
||||||
1) онлайн-доставка в активную WebSocket-сессию;
|
1) онлайн-доставка в активную WebSocket-сессию;
|
||||||
2) если не подтверждено — Web Push;
|
2) если не подтверждено — Web Push;
|
||||||
3) поддержать отдельный API отправки без авторизации, где доступ проверяется цифровой подписью Ed25519 по `deviceKey` отправителя.
|
3) поддержать отдельный API отправки без авторизации, где доступ проверяется цифровой подписью Ed25519 по `clientKey` отправителя.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -48,7 +48,7 @@
|
|||||||
Операция (через WS JSON обертку) условно `SendSignedDirectMessage`:
|
Операция (через WS JSON обертку) условно `SendSignedDirectMessage`:
|
||||||
- принимает пакет (base64 binary blob);
|
- принимает пакет (base64 binary blob);
|
||||||
- парсит и валидирует формат;
|
- парсит и валидирует формат;
|
||||||
- достает `fromLogin`, поднимает `deviceKey` пользователя;
|
- достает `fromLogin`, поднимает `clientKey` пользователя;
|
||||||
- проверяет подпись Ed25519;
|
- проверяет подпись Ed25519;
|
||||||
- проверяет анти-replay (time window + nonce);
|
- проверяет анти-replay (time window + nonce);
|
||||||
- отправляет сообщение по правилам маршрутизации;
|
- отправляет сообщение по правилам маршрутизации;
|
||||||
@ -135,7 +135,7 @@
|
|||||||
### Что уже внедрено в коде
|
### Что уже внедрено в коде
|
||||||
- `SendDirectMessage` переведён на signed-binary payload (`blobB64`) без обязательной авторизации WS-сессии.
|
- `SendDirectMessage` переведён на signed-binary payload (`blobB64`) без обязательной авторизации WS-сессии.
|
||||||
- Внедрён бинарный парсер пакета формата `SHiNE_msg + version(1) + ... + signature64`.
|
- Внедрён бинарный парсер пакета формата `SHiNE_msg + version(1) + ... + signature64`.
|
||||||
- Проверка подписи Ed25519 делается по `deviceKey` отправителя через `shine-server-crypto` (`Ed25519Util`).
|
- Проверка подписи Ed25519 делается по `clientKey` отправителя через `shine-server-crypto` (`Ed25519Util`).
|
||||||
- Добавлен anti-replay guard `(from_login, time_ms, nonce)` с TTL 15 минут.
|
- Добавлен anti-replay guard `(from_login, time_ms, nonce)` с TTL 15 минут.
|
||||||
- Добавлено историческое хранилище `signed_direct_messages_history` с сырым пакетом `raw_packet`.
|
- Добавлено историческое хранилище `signed_direct_messages_history` с сырым пакетом `raw_packet`.
|
||||||
- Логика доставки: сначала WS+ACK, затем fallback на Web Push (по подписке конкретной session).
|
- Логика доставки: сначала WS+ACK, затем fallback на Web Push (по подписке конкретной session).
|
||||||
|
|||||||
@ -1,2 +1,2 @@
|
|||||||
client.version=1.2.232
|
client.version=1.2.233
|
||||||
server.version=1.2.218
|
server.version=1.2.219
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user