From 888bb1595faf649b1c35991c9eb8780d360d0c79b9ee1691ef1acf3ebd337ffd Mon Sep 17 00:00:00 2001 From: AidarKC Date: Tue, 9 Dec 2025 20:04:18 +0300 Subject: [PATCH] 09 12 25 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Авторификация работает и тест авторификации проходит. (создание пользователя, два этапа создания сессии и рефреш сессии) --- .../java/shine/db/DatabaseInitializer.java | 41 +- .../java/shine/db/dao/ActiveSessionsDAO.java | 94 ++-- .../java/shine/db/dao/SolanaUsersDAO.java | 29 +- .../java/shine/db/entities/ActiveSession.java | 55 +- .../java/shine/db/entities/SolanaUser.java | 42 +- .../tempToTest/NetAddUserRequest.java | 45 +- .../tempToTest/NetAddUserResponse.java | 13 +- .../auth/NetAuthSessionNewStep2Handler.java | 2 +- .../tempToTest/NetAddUserHandler.java | 32 +- src/main/docs/Запуск на сервере.txt | 3 + .../java/Test/Test_AddUser_FirstAuth.java | 242 --------- .../Test_AddUser_and_Authorification.java | 497 ++++++++++++++++++ 12 files changed, 723 insertions(+), 372 deletions(-) delete mode 100644 src/main/java/Test/Test_AddUser_FirstAuth.java create mode 100644 src/main/java/Test/Test_AddUser_and_Authorification.java diff --git a/shine-server-db/src/main/java/shine/db/DatabaseInitializer.java b/shine-server-db/src/main/java/shine/db/DatabaseInitializer.java index 72a2ba1..bb19704 100644 --- a/shine-server-db/src/main/java/shine/db/DatabaseInitializer.java +++ b/shine-server-db/src/main/java/shine/db/DatabaseInitializer.java @@ -1,6 +1,5 @@ package shine.db; - import utils.config.AppConfig; import java.io.BufferedReader; @@ -12,6 +11,15 @@ import java.sql.DriverManager; import java.sql.SQLException; import java.sql.Statement; +/** + * DatabaseInitializer — создание новой SQLite-БД по схеме SHiNE. + * + * Читает путь к файлу БД из application.properties (db.path), + * при необходимости удаляет старый файл и создаёт таблицы: + * - solana_users + * - active_sessions + * - users_params + */ public class DatabaseInitializer { public static void createNewDB(String[] args) { @@ -75,9 +83,9 @@ public class DatabaseInitializer { login TEXT NOT NULL, loginId INTEGER NOT NULL PRIMARY KEY, bchId INTEGER NOT NULL, - pubkey0 TEXT, - pubkey1 TEXT, - bchLimit INTEGER -- может быть NULL + loginKey TEXT, -- основной публичный ключ (логин) + deviceKey TEXT, -- публичный ключ устройства + bchLimit INTEGER -- может быть NULL ); """); @@ -87,22 +95,29 @@ public class DatabaseInitializer { """); // 2. Таблица active_sessions + // sessionId теперь TEXT (base64 от 32 байт), а не INTEGER. st.executeUpdate(""" CREATE TABLE IF NOT EXISTS active_sessions ( - sessionId INTEGER NOT NULL PRIMARY KEY, - session_pwd TEXT NOT NULL, - loginId INTEGER NOT NULL, - time_ms INTEGER NOT NULL, - pubkey_num INTEGER NOT NULL, - push_endpoint TEXT, - push_p256dh_key TEXT, - push_auth_key TEXT, + sessionId TEXT NOT NULL PRIMARY KEY, + loginId INTEGER NOT NULL, + sessionPwd TEXT NOT NULL, + storagePwd TEXT NOT NULL, + sessionCreatedAtMs INTEGER NOT NULL, + lastAuthirificatedAtMs INTEGER NOT NULL, + pushEndpoint TEXT, + pushP256dhKey TEXT, + pushAuthKey TEXT, FOREIGN KEY (loginId) REFERENCES solana_users(loginId) ); """); + st.executeUpdate(""" + CREATE INDEX IF NOT EXISTS idx_active_sessions_loginId + ON active_sessions (loginId); + """); + // 3. Таблица users_params - // Важно: пара (loginId, param) должна быть уникальна + // Пара (loginId, param) должна быть уникальна. st.executeUpdate(""" CREATE TABLE IF NOT EXISTS users_params ( loginId INTEGER NOT NULL, diff --git a/shine-server-db/src/main/java/shine/db/dao/ActiveSessionsDAO.java b/shine-server-db/src/main/java/shine/db/dao/ActiveSessionsDAO.java index d34cd61..f400e42 100644 --- a/shine-server-db/src/main/java/shine/db/dao/ActiveSessionsDAO.java +++ b/shine-server-db/src/main/java/shine/db/dao/ActiveSessionsDAO.java @@ -5,7 +5,11 @@ import shine.db.entities.ActiveSession; import java.sql.*; -/** Здесь мы храним данные об активных сессиях пользователя (для wss соединений). */ +/** + * DAO для таблицы active_sessions. + * + * Здесь мы храним данные об активных сессиях пользователя (для wss-соединений). + */ public final class ActiveSessionsDAO { private static volatile ActiveSessionsDAO instance; @@ -25,18 +29,21 @@ public final class ActiveSessionsDAO { return instance; } + /** + * Вставка новой сессии. + */ public void insert(ActiveSession session) throws SQLException { String sql = """ INSERT INTO active_sessions ( sessionId, loginId, - session_pwd, - storage_pwd, - session_created_ms, - last_auth_ms, - push_endpoint, - push_p256dh_key, - push_auth_key + sessionPwd, + storagePwd, + sessionCreatedAtMs, + lastAuthirificatedAtMs, + pushEndpoint, + pushP256dhKey, + pushAuthKey ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) """; @@ -50,22 +57,26 @@ public final class ActiveSessionsDAO { ps.setString(7, session.getPushEndpoint()); ps.setString(8, session.getPushP256dhKey()); ps.setString(9, session.getPushAuthKey()); + ps.executeUpdate(); } } + /** + * Получить сессию по sessionId. + */ public ActiveSession getBySessionId(String sessionId) throws SQLException { String sql = """ SELECT sessionId, loginId, - session_pwd, - storage_pwd, - session_created_ms, - last_auth_ms, - push_endpoint, - push_p256dh_key, - push_auth_key + sessionPwd, + storagePwd, + sessionCreatedAtMs, + lastAuthirificatedAtMs, + pushEndpoint, + pushP256dhKey, + pushAuthKey FROM active_sessions WHERE sessionId = ? """; @@ -81,6 +92,23 @@ public final class ActiveSessionsDAO { } } + /** + * Обновить только lastAuthirificatedAtMs для конкретной сессии. + */ + public void updateLastAuthirificatedAtMs(String sessionId, long lastAuthMs) throws SQLException { + String sql = """ + UPDATE active_sessions + SET lastAuthirificatedAtMs = ? + WHERE sessionId = ? + """; + + try (PreparedStatement ps = db.getConnection().prepareStatement(sql)) { + ps.setLong(1, lastAuthMs); + ps.setString(2, sessionId); + ps.executeUpdate(); + } + } + /** * Удаление записи по sessionId. * Если записи нет — просто ничего не удалит (0 строк). @@ -94,42 +122,24 @@ public final class ActiveSessionsDAO { } } - /** - * Обновить поле last_auth_ms (lastAuthirificatedAtMs) для конкретной сессии. - * Остальные поля записи не меняются. - */ - public void updateLastAuthirificatedAtMs(String sessionId, long newTimeMs) throws SQLException { - String sql = """ - UPDATE active_sessions - SET last_auth_ms = ? - WHERE sessionId = ? - """; - - try (PreparedStatement ps = db.getConnection().prepareStatement(sql)) { - ps.setLong(1, newTimeMs); - ps.setString(2, sessionId); - ps.executeUpdate(); - } - } - private ActiveSession mapRow(ResultSet rs) throws SQLException { String sessionId = rs.getString("sessionId"); long loginId = rs.getLong("loginId"); - String sessionPwd = rs.getString("session_pwd"); - String storagePwd = rs.getString("storage_pwd"); - long sessionCreatedMs = rs.getLong("session_created_ms"); - long lastAuthMs = rs.getLong("last_auth_ms"); - String pushEndpoint = rs.getString("push_endpoint"); - String pushP256dhKey = rs.getString("push_p256dh_key"); - String pushAuthKey = rs.getString("push_auth_key"); + String sessionPwd = rs.getString("sessionPwd"); + String storagePwd = rs.getString("storagePwd"); + long sessionCreatedAtMs = rs.getLong("sessionCreatedAtMs"); + long lastAuthirificatedAtMs = rs.getLong("lastAuthirificatedAtMs"); + String pushEndpoint = rs.getString("pushEndpoint"); + String pushP256dhKey = rs.getString("pushP256dhKey"); + String pushAuthKey = rs.getString("pushAuthKey"); return new ActiveSession( sessionId, loginId, sessionPwd, storagePwd, - sessionCreatedMs, - lastAuthMs, + sessionCreatedAtMs, + lastAuthirificatedAtMs, pushEndpoint, pushP256dhKey, pushAuthKey diff --git a/shine-server-db/src/main/java/shine/db/dao/SolanaUsersDAO.java b/shine-server-db/src/main/java/shine/db/dao/SolanaUsersDAO.java index a379dd9..b4a8eb3 100644 --- a/shine-server-db/src/main/java/shine/db/dao/SolanaUsersDAO.java +++ b/shine-server-db/src/main/java/shine/db/dao/SolanaUsersDAO.java @@ -7,8 +7,17 @@ import java.sql.*; import java.util.ArrayList; import java.util.List; -/** Здесь храним данные об пользователях - локальная копия того что есть в солане */ - +/** + * SolanaUsersDAO — локальная таблица пользователей из Solana. + * + * Колонки: + * - login TEXT + * - loginId INTEGER (PK) + * - bchId INTEGER + * - loginKey TEXT + * - deviceKey TEXT + * - bchLimit INTEGER (может быть NULL) + */ public final class SolanaUsersDAO { private static volatile SolanaUsersDAO instance; @@ -29,7 +38,7 @@ public final class SolanaUsersDAO { public void insert(SolanaUser user) throws SQLException { String sql = """ - INSERT INTO solana_users (login, loginId, bchId, pubkey0, pubkey1, bchLimit) + INSERT INTO solana_users (login, loginId, bchId, loginKey, deviceKey, bchLimit) VALUES (?, ?, ?, ?, ?, ?) """; @@ -37,8 +46,8 @@ public final class SolanaUsersDAO { ps.setString(1, user.getLogin()); ps.setLong(2, user.getLoginId()); ps.setLong(3, user.getBchId()); - ps.setString(4, user.getPubkey0()); - ps.setString(5, user.getPubkey1()); + ps.setString(4, user.getLoginKey()); + ps.setString(5, user.getDeviceKey()); if (user.getBchLimit() != null) { ps.setInt(6, user.getBchLimit()); @@ -52,7 +61,7 @@ public final class SolanaUsersDAO { public SolanaUser getByLoginId(long loginId) throws SQLException { String sql = """ - SELECT login, loginId, bchId, pubkey0, pubkey1, bchLimit + SELECT login, loginId, bchId, loginKey, deviceKey, bchLimit FROM solana_users WHERE loginId = ? """; @@ -69,7 +78,7 @@ public final class SolanaUsersDAO { public SolanaUser getByLogin(String login) throws SQLException { String sql = """ - SELECT login, loginId, bchId, pubkey0, pubkey1, bchLimit + SELECT login, loginId, bchId, loginKey, deviceKey, bchLimit FROM solana_users WHERE LOWER(login) = LOWER(?) """; @@ -86,7 +95,7 @@ public final class SolanaUsersDAO { public List searchByLoginPrefix(String prefix) throws SQLException { String sql = """ - SELECT login, loginId, bchId, pubkey0, pubkey1, bchLimit + SELECT login, loginId, bchId, loginKey, deviceKey, bchLimit FROM solana_users WHERE LOWER(login) LIKE ? ORDER BY login @@ -111,8 +120,8 @@ public final class SolanaUsersDAO { rs.getLong("loginId"), rs.getString("login"), rs.getLong("bchId"), - rs.getString("pubkey0"), - rs.getString("pubkey1"), + rs.getString("loginKey"), + rs.getString("deviceKey"), rs.getObject("bchLimit") != null ? rs.getInt("bchLimit") : null ); } diff --git a/shine-server-db/src/main/java/shine/db/entities/ActiveSession.java b/shine-server-db/src/main/java/shine/db/entities/ActiveSession.java index ea7a0ba..d8d14eb 100644 --- a/shine-server-db/src/main/java/shine/db/entities/ActiveSession.java +++ b/shine-server-db/src/main/java/shine/db/entities/ActiveSession.java @@ -1,30 +1,34 @@ package shine.db.entities; /** - * ActiveSession — запись об активной сессии пользователя. + * Модель активной сессии (таблица active_sessions). * - * Поля: - * - sessionId – строка (base64 от 32 байт) - * - loginId – long - * - sessionPwd – строка (секрет шага 1) - * - storagePwd – строка (секрет клиента для хранения данных) - * - sessionCreatedAtMs – long (время создания) - * - lastAuthirificatedAtMs – long (последнее подтверждение/refresh) - * - pushEndpoint – строка (WebPush, пока null/пусто) - * - pushP256dhKey – строка (WebPush, пока null/пусто) - * - pushAuthKey – строка (WebPush, пока null/пусто) + * Поля соответствуют схеме: + * + * CREATE TABLE active_sessions ( + * sessionId TEXT NOT NULL PRIMARY KEY, + * loginId INTEGER NOT NULL, + * sessionPwd TEXT NOT NULL, + * storagePwd TEXT NOT NULL, + * sessionCreatedAtMs INTEGER NOT NULL, + * lastAuthirificatedAtMs INTEGER NOT NULL, + * pushEndpoint TEXT, + * pushP256dhKey TEXT, + * pushAuthKey TEXT, + * FOREIGN KEY (loginId) REFERENCES solana_users(loginId) + * ); */ public class ActiveSession { - private String sessionId; - private long loginId; - private String sessionPwd; - private String storagePwd; - private long sessionCreatedAtMs; - private long lastAuthirificatedAtMs; - private String pushEndpoint; - private String pushP256dhKey; - private String pushAuthKey; + private String sessionId; // TEXT base64(32 bytes) + private long loginId; // INTEGER + private String sessionPwd; // TEXT + private String storagePwd; // TEXT + private long sessionCreatedAtMs; // INTEGER + private long lastAuthirificatedAtMs; // INTEGER + private String pushEndpoint; // TEXT (nullable) + private String pushP256dhKey; // TEXT (nullable) + private String pushAuthKey; // TEXT (nullable) public ActiveSession() { } @@ -49,9 +53,12 @@ public class ActiveSession { this.pushAuthKey = pushAuthKey; } + // --- getters / setters --- + public String getSessionId() { return sessionId; } + public void setSessionId(String sessionId) { this.sessionId = sessionId; } @@ -59,6 +66,7 @@ public class ActiveSession { public long getLoginId() { return loginId; } + public void setLoginId(long loginId) { this.loginId = loginId; } @@ -66,6 +74,7 @@ public class ActiveSession { public String getSessionPwd() { return sessionPwd; } + public void setSessionPwd(String sessionPwd) { this.sessionPwd = sessionPwd; } @@ -73,6 +82,7 @@ public class ActiveSession { public String getStoragePwd() { return storagePwd; } + public void setStoragePwd(String storagePwd) { this.storagePwd = storagePwd; } @@ -80,6 +90,7 @@ public class ActiveSession { public long getSessionCreatedAtMs() { return sessionCreatedAtMs; } + public void setSessionCreatedAtMs(long sessionCreatedAtMs) { this.sessionCreatedAtMs = sessionCreatedAtMs; } @@ -87,6 +98,7 @@ public class ActiveSession { public long getLastAuthirificatedAtMs() { return lastAuthirificatedAtMs; } + public void setLastAuthirificatedAtMs(long lastAuthirificatedAtMs) { this.lastAuthirificatedAtMs = lastAuthirificatedAtMs; } @@ -94,6 +106,7 @@ public class ActiveSession { public String getPushEndpoint() { return pushEndpoint; } + public void setPushEndpoint(String pushEndpoint) { this.pushEndpoint = pushEndpoint; } @@ -101,6 +114,7 @@ public class ActiveSession { public String getPushP256dhKey() { return pushP256dhKey; } + public void setPushP256dhKey(String pushP256dhKey) { this.pushP256dhKey = pushP256dhKey; } @@ -108,6 +122,7 @@ public class ActiveSession { public String getPushAuthKey() { return pushAuthKey; } + public void setPushAuthKey(String pushAuthKey) { this.pushAuthKey = pushAuthKey; } diff --git a/shine-server-db/src/main/java/shine/db/entities/SolanaUser.java b/shine-server-db/src/main/java/shine/db/entities/SolanaUser.java index ab6ae99..333a8d0 100644 --- a/shine-server-db/src/main/java/shine/db/entities/SolanaUser.java +++ b/shine-server-db/src/main/java/shine/db/entities/SolanaUser.java @@ -1,13 +1,23 @@ package shine.db.entities; +/** + * Локальная копия пользователя из Solana. + * + * Храним: + * - login / loginId; + * - bchId — id персонального блокчейна; + * - loginKey — публичный ключ для логина / авторизации; + * - deviceKey — публичный ключ устройства (второй ключ); + * - bchLimit — лимит по количеству блоков / размеру цепочки (может быть null). + */ public class SolanaUser { private long loginId; private String login; private long bchId; - private String pubkey0; - private String pubkey1; - private Integer bchLimit; // может быть null + private String loginKey; // раньше pubkey0 + private String deviceKey; // раньше pubkey1 + private Integer bchLimit; // может быть null public SolanaUser() { } @@ -15,14 +25,14 @@ public class SolanaUser { public SolanaUser(long loginId, String login, long bchId, - String pubkey0, - String pubkey1, + String loginKey, + String deviceKey, Integer bchLimit) { this.loginId = loginId; this.login = login; this.bchId = bchId; - this.pubkey0 = pubkey0; - this.pubkey1 = pubkey1; + this.loginKey = loginKey; + this.deviceKey = deviceKey; this.bchLimit = bchLimit; } @@ -50,20 +60,22 @@ public class SolanaUser { this.bchId = bchId; } - public String getPubkey0() { - return pubkey0; + /** Публичный ключ логина (основной ключ пользователя). */ + public String getLoginKey() { + return loginKey; } - public void setPubkey0(String pubkey0) { - this.pubkey0 = pubkey0; + public void setLoginKey(String loginKey) { + this.loginKey = loginKey; } - public String getPubkey1() { - return pubkey1; + /** Публичный ключ устройства (device key). */ + public String getDeviceKey() { + return deviceKey; } - public void setPubkey1(String pubkey1) { - this.pubkey1 = pubkey1; + public void setDeviceKey(String deviceKey) { + this.deviceKey = deviceKey; } public Integer getBchLimit() { diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/entyties/tempToTest/NetAddUserRequest.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/entyties/tempToTest/NetAddUserRequest.java index b6d2305..c6526b9 100644 --- a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/entyties/tempToTest/NetAddUserRequest.java +++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/entyties/tempToTest/NetAddUserRequest.java @@ -3,27 +3,32 @@ package server.logic.ws_protocol.JSON.entyties.tempToTest; import server.logic.ws_protocol.JSON.entyties.NetRequest; /** - * Запрос AddUser. - *. - * Ожидаемый JSON: + * Запрос AddUser — временная/тестовая регистрация локального пользователя. + * + * Клиент отправляет: + * * { * "op": "AddUser", - * "requestId": "...", - * "login": "...", - * "loginId": 123, - * "bchId": 456, - * "pubkey0": "...", - * "pubkey1": "...", - * "bchLimit": 1000 + * "requestId": "test-add-1", + * "payload": { + * "login": "anya", + * "loginId": 100211, + * "bchId": 4222, + * "loginKey": "base64-ed25519-public-key-login", + * "deviceKey": "base64-ed25519-public-key-device", + * "bchLimit": 1000000 + * } * } + * + * Все поля лежат внутри payload. */ public class NetAddUserRequest extends NetRequest { private String login; private long loginId; private long bchId; - private String pubkey0; - private String pubkey1; + private String loginKey; + private String deviceKey; private Integer bchLimit; public String getLogin() { @@ -50,20 +55,20 @@ public class NetAddUserRequest extends NetRequest { this.bchId = bchId; } - public String getPubkey0() { - return pubkey0; + public String getLoginKey() { + return loginKey; } - public void setPubkey0(String pubkey0) { - this.pubkey0 = pubkey0; + public void setLoginKey(String loginKey) { + this.loginKey = loginKey; } - public String getPubkey1() { - return pubkey1; + public String getDeviceKey() { + return deviceKey; } - public void setPubkey1(String pubkey1) { - this.pubkey1 = pubkey1; + public void setDeviceKey(String deviceKey) { + this.deviceKey = deviceKey; } public Integer getBchLimit() { diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/entyties/tempToTest/NetAddUserResponse.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/entyties/tempToTest/NetAddUserResponse.java index 6d500c7..1dbc5ae 100644 --- a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/entyties/tempToTest/NetAddUserResponse.java +++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/entyties/tempToTest/NetAddUserResponse.java @@ -4,8 +4,17 @@ import server.logic.ws_protocol.JSON.entyties.NetResponse; /** * Успешный ответ на AddUser. - * Дополнительных полей нет — достаточно status=200. + * + * Сейчас дополнительных полей нет — достаточно status=200. + * + * Пример: + * { + * "op": "AddUser", + * "requestId": "test-add-1", + * "status": 200, + * "payload": { } + * } */ public class NetAddUserResponse extends NetResponse { - // Можно потом добавить какие-то данные, если понадобится. + // При необходимости сюда можно добавить, например, флаг created/updated и т.п. } diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/NetAuthSessionNewStep2Handler.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/NetAuthSessionNewStep2Handler.java index 44eddb0..e1e58f2 100644 --- a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/NetAuthSessionNewStep2Handler.java +++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/NetAuthSessionNewStep2Handler.java @@ -119,7 +119,7 @@ public class NetAuthSessionNewStep2Handler implements JsonMessageHandler { } // --- выбираем публичный ключ pubkey1 --- - String pubKeyB64 = user.getPubkey1(); + String pubKeyB64 = user.getDeviceKey(); if (pubKeyB64 == null || pubKeyB64.isBlank()) { return NetExceptionResponseFactory.error( req, diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/tempToTest/NetAddUserHandler.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/tempToTest/NetAddUserHandler.java index 90b1dc1..a0862d6 100644 --- a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/tempToTest/NetAddUserHandler.java +++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/tempToTest/NetAddUserHandler.java @@ -16,7 +16,25 @@ import shine.db.entities.SolanaUser; import java.sql.SQLException; /** - * Временный хэндлер AddUser (тестовая регистрация). + * Временный хэндлер AddUser (тестовая регистрация локального пользователя). + * + * Ожидаемый запрос (все поля в payload): + * { + * "op": "AddUser", + * "requestId": "...", + * "payload": { + * "login": "anya", + * "loginId": 100211, + * "bchId": 4222, + * "loginKey": "base64-pubkey-login", + * "deviceKey": "base64-pubkey-device", + * "bchLimit": 1000000 + * } + * } + * + * При успехе: + * - пользователь сохраняется в таблицу solana_users; + * - возвращается status=200 и пустой payload. */ public class NetAddUserHandler implements JsonMessageHandler { @@ -28,15 +46,15 @@ public class NetAddUserHandler implements JsonMessageHandler { // Одна общая проверка всех ключевых полей if (req.getLogin() == null || req.getLogin().isBlank() - || req.getPubkey0() == null || req.getPubkey0().isBlank() - || req.getPubkey1() == null || req.getPubkey1().isBlank() + || req.getLoginKey() == null || req.getLoginKey().isBlank() + || req.getDeviceKey() == null || req.getDeviceKey().isBlank() || req.getBchLimit() == null) { return NetExceptionResponseFactory.error( req, WireCodes.Status.BAD_REQUEST, "BAD_FIELDS", - "Некорректные или пустые поля: login, pubkey0, pubkey1, bchLimit" + "Некорректные или пустые поля: login, loginKey, deviceKey, bchLimit" ); } @@ -47,8 +65,8 @@ public class NetAddUserHandler implements JsonMessageHandler { req.getLoginId(), req.getLogin(), req.getBchId(), - req.getPubkey0(), - req.getPubkey1(), + req.getLoginKey(), + req.getDeviceKey(), req.getBchLimit() ); @@ -58,7 +76,7 @@ public class NetAddUserHandler implements JsonMessageHandler { resp.setOp(req.getOp()); resp.setRequestId(req.getRequestId()); resp.setStatus(WireCodes.Status.OK); - // payload сам станет {} через JsonInboundProcessor + // payload станет {} через JsonInboundProcessor log.info("✅ Пользователь добавлен: login={}, loginId={}", req.getLogin(), req.getLoginId()); return resp; diff --git a/src/main/docs/Запуск на сервере.txt b/src/main/docs/Запуск на сервере.txt index 6a4810b..b785aa6 100644 --- a/src/main/docs/Запуск на сервере.txt +++ b/src/main/docs/Запуск на сервере.txt @@ -8,3 +8,6 @@ user@p628065:~/docker/ws-server$ nohup java -jar ws-server.jar > server.log 2>&1 перестартовать кадди user@p628065:~/docker/ws-server$ docker restart caddy + +очистить того кто держит порт 7070 :) +kill -9 $(lsof -t -i:7070) diff --git a/src/main/java/Test/Test_AddUser_FirstAuth.java b/src/main/java/Test/Test_AddUser_FirstAuth.java deleted file mode 100644 index 6658291..0000000 --- a/src/main/java/Test/Test_AddUser_FirstAuth.java +++ /dev/null @@ -1,242 +0,0 @@ -package Test; - -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import utils.crypto.Ed25519Util; - -import java.net.URI; -import java.net.http.HttpClient; -import java.net.http.WebSocket; -import java.net.http.WebSocket.Listener; -import java.nio.charset.StandardCharsets; -import java.util.Base64; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionStage; -import java.util.concurrent.CountDownLatch; - -public class Test_AddUser_FirstAuth { - - // Адрес сервера - private static final String WS_URI = "ws://localhost:7070/ws"; - - private static final ObjectMapper JSON_MAPPER = new ObjectMapper(); - - // Тестовые данные пользователя - private static final String TEST_LOGIN = "anya2"; - private static final long TEST_LOGIN_ID = 100212L; - private static final long TEST_BCH_ID = 4222L; - private static final int TEST_BCH_LIMIT = 1_000_000; - - // Тестовая пара ключей Ed25519 (стабильная, чтобы поведение не прыгало) - private static final byte[] TEST_PRIV_KEY; - private static final String TEST_PUBKEY_B64; - - static { - // Можно сделать детерминированно от логина, чтобы всегда были одинаковые ключи - TEST_PRIV_KEY = Ed25519Util.generatePrivateKeyFromString("test-ed25519-" + TEST_LOGIN); - byte[] pub = Ed25519Util.derivePublicKey(TEST_PRIV_KEY); - TEST_PUBKEY_B64 = Ed25519Util.keyToBase64(pub); - } - - public static void main(String[] args) throws Exception { - System.out.println("Подключаемся к " + WS_URI); - - CountDownLatch latch = new CountDownLatch(1); - - HttpClient client = HttpClient.newHttpClient(); - - ClientListener listener = new ClientListener(latch); - - client.newWebSocketBuilder() - .buildAsync(URI.create(WS_URI), listener) - .join(); - - // Ждём, пока всё не завершится (успех/ошибка/закрытие) - latch.await(); - System.out.println("Тест завершён, выходим."); - } - - // --- вспомогательные билдера JSON-запросов --- - - // 1) Добавление пользователя - private static String buildAddUserJson() { - return """ - { - "op": "AddUser", - "requestId": "test-add-1", - "payload": { - "login": "%s", - "loginId": %d, - "bchId": %d, - "pubkey0": "%s", - "pubkey1": "%s", - "bchLimit": %d - } - } - """.formatted( - TEST_LOGIN, - TEST_LOGIN_ID, - TEST_BCH_ID, - TEST_PUBKEY_B64, // pubkey0 - TEST_PUBKEY_B64, // pubkey1 (для теста можно тот же) - TEST_BCH_LIMIT - ); - } - - // 2) Шаг 1 авторизации: запрос sessionPwd - private static String buildAuthStep1Json() { - return """ - { - "op": "AuthSessionNewStep1", - "requestId": "test-auth-1", - "payload": { - "login": "%s" - } - } - """.formatted(TEST_LOGIN); - } - - // 3) Шаг 2 авторизации: подтверждение подписью - private static String buildAuthStep2Json(String sessionPwd) { - if (sessionPwd == null) { - sessionPwd = ""; - } - - long timeMs = System.currentTimeMillis(); - - // preimage = loginId + timeMs + sessionPwd - String preimageStr = String.valueOf(TEST_LOGIN_ID) + timeMs + sessionPwd; - byte[] preimage = preimageStr.getBytes(StandardCharsets.UTF_8); - - // Подписываем приватным ключом - byte[] sig = Ed25519Util.sign(preimage, TEST_PRIV_KEY); - String sigB64 = Base64.getEncoder().encodeToString(sig); - - return """ - { - "op": "AuthSessionNewStep2", - "requestId": "test-auth-2", - "payload": { - "loginId": %d, - "sigNum": 0, - "timeMs": %d, - "signatureB64": "%s" - } - } - """.formatted( - TEST_LOGIN_ID, - timeMs, - sigB64 - ); - } - - // ================== LISTENER ================== - - // Внутренний Listener, который сам по шагам шлёт запросы и печатает ответы - private static class ClientListener implements Listener { - - private final CountDownLatch latch; - private int step = 0; // 0 - AddUser, 1 - AuthStep1, 2 - AuthStep2 - private String sessionPwdFromStep1; - - ClientListener(CountDownLatch latch) { - this.latch = latch; - } - - @Override - public void onOpen(WebSocket webSocket) { - System.out.println("✅ WebSocket подключен"); - - // Разрешаем приём первого сообщения - webSocket.request(1); - - sendNextRequest(webSocket); - Listener.super.onOpen(webSocket); - } - - // Отправка следующего запроса в зависимости от шага - private void sendNextRequest(WebSocket webSocket) { - switch (step) { - case 0 -> { - String json = buildAddUserJson(); - System.out.println(); - System.out.println("📤 [Шаг 1] Отправляем AddUser:"); - System.out.println(json); - webSocket.sendText(json, true); - } - case 1 -> { - String json = buildAuthStep1Json(); - System.out.println(); - System.out.println("📤 [Шаг 2] Отправляем AuthSessionNewStep1:"); - System.out.println(json); - webSocket.sendText(json, true); - } - case 2 -> { - String json = buildAuthStep2Json(sessionPwdFromStep1); - System.out.println(); - System.out.println("📤 [Шаг 3] Отправляем AuthSessionNewStep2 (подпись):"); - System.out.println(json); - webSocket.sendText(json, true); - } - default -> { - System.out.println("✅ Все шаги выполнены, закрываем соединение"); - webSocket.sendClose(WebSocket.NORMAL_CLOSURE, "all tests done"); - } - } - } - - @Override - public CompletionStage onText(WebSocket webSocket, - CharSequence data, - boolean last) { - String message = data.toString(); - System.out.println("📥 Ответ на шаг " + (step + 1) + ":"); - System.out.println(message); - System.out.println("-----------------------------------------------------"); - - // Если это ответ на шаг 2 (AuthSessionNewStep1) — достаем sessionPwd из payload - if (step == 1) { - sessionPwdFromStep1 = extractSessionPwd(message); - System.out.println("🔑 Извлечён sessionPwd: " + sessionPwdFromStep1); - } - - // Переходим к следующему шагу - step++; - sendNextRequest(webSocket); - - // Запрашиваем следующее входящее сообщение - webSocket.request(1); - - return CompletableFuture.completedFuture(null); - } - - @Override - public void onError(WebSocket webSocket, Throwable error) { - System.out.println("❌ Ошибка WebSocket-клиента: " + error.getMessage()); - error.printStackTrace(System.out); - latch.countDown(); - } - - @Override - public CompletionStage onClose(WebSocket webSocket, - int statusCode, - String reason) { - System.out.println("🔚 Соединение закрыто. Код=" + statusCode + ", причина=" + reason); - latch.countDown(); - return CompletableFuture.completedFuture(null); - } - - private String extractSessionPwd(String json) { - try { - JsonNode root = JSON_MAPPER.readTree(json); - JsonNode payload = root.get("payload"); - if (payload != null && payload.has("sessionPwd")) { - return payload.get("sessionPwd").asText(); - } - } catch (Exception e) { - System.out.println("⚠️ Не удалось распарсить sessionPwd из ответа: " + e.getMessage()); - } - return null; - } - } -} diff --git a/src/main/java/Test/Test_AddUser_and_Authorification.java b/src/main/java/Test/Test_AddUser_and_Authorification.java new file mode 100644 index 0000000..a1022e4 --- /dev/null +++ b/src/main/java/Test/Test_AddUser_and_Authorification.java @@ -0,0 +1,497 @@ +package Test; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import utils.crypto.Ed25519Util; + +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.WebSocket; +import java.net.http.WebSocket.Listener; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.CountDownLatch; + +/** + * Полный тестовый сценарий: + * + * 1) AddUser — добавляем пользователя в локальную БД + * (loginKey и deviceKey разные). + * + * 2) AuthSessionNewStep1 — запрашиваем sessionPwd. + * + * 3) AuthSessionNewStep2 — подтверждаем владение deviceKey, + * создаётся сессия, сервер возвращает sessionId (строка). + * + * 4) Новое подключение: + * - отправляем SessionRefresh с тем же sessionId, + * но заведомо неверным sessionPwd + * (в консоль пишем: ожидаем ОТРИЦАТЕЛЬНЫЙ ответ). + * + * 5) Ещё одно новое подключение: + * - отправляем SessionRefresh с sessionId + * и корректным sessionPwd + * (в консоль пишем: ожидаем УСПЕШНЫЙ ответ). + */ +public class Test_AddUser_and_Authorification { + + // Адрес сервера + private static final String WS_URI = "ws://localhost:7070/ws"; + + private static final ObjectMapper JSON_MAPPER = new ObjectMapper(); + + // Тестовые данные пользователя + private static final String TEST_LOGIN = "anya1"; + private static final long TEST_LOGIN_ID = 100310L; + private static final long TEST_BCH_ID = 4222L; + private static final int TEST_BCH_LIMIT = 1_000_000; + + // --- Тестовые пары ключей --- + // loginKey — ключ аккаунта (например, "основной") + // deviceKey — ключ устройства, которым подписываем авторизацию + + private static final byte[] LOGIN_PRIV_KEY; + private static final String LOGIN_PUBKEY_B64; + + private static final byte[] DEVICE_PRIV_KEY; + private static final String DEVICE_PUBKEY_B64; + + static { + // Детерминированное "семя" для логин-ключа + LOGIN_PRIV_KEY = Ed25519Util.generatePrivateKeyFromString("test-ed25519-login-11" + TEST_LOGIN); + byte[] loginPub = Ed25519Util.derivePublicKey(LOGIN_PRIV_KEY); + LOGIN_PUBKEY_B64 = Ed25519Util.keyToBase64(loginPub); + + // Детерминированное "семя" для девайс-ключа + DEVICE_PRIV_KEY = Ed25519Util.generatePrivateKeyFromString("test-ed25519-device-" + TEST_LOGIN); + byte[] devicePub = Ed25519Util.derivePublicKey(DEVICE_PRIV_KEY); + DEVICE_PUBKEY_B64 = Ed25519Util.keyToBase64(devicePub); + } + + // --- Глобальные переменные между сценариями --- + + /** sessionPwd, выданный на шаге AuthSessionNewStep1. */ + private static String GLOBAL_SESSION_PWD; + + /** sessionId (строка, base64-32 байта), выданный на шаге AuthSessionNewStep2. */ + private static String GLOBAL_SESSION_ID; + + /** storagePwd, который мы отправили при AuthSessionNewStep2 (для информации). */ + private static String GLOBAL_STORAGE_PWD_SENT; + + public static void main(String[] args) throws Exception { + System.out.println("Подключаемся к " + WS_URI); + + // Сценарий 1: регистрация + первичная авторизация + runScenario_AddUser_And_FirstAuth(); + + // Сценарий 2: новое подключение, SessionRefresh с неверным sessionPwd + runScenario_SessionRefresh_WrongPwd(); + + // Сценарий 3: новое подключение, SessionRefresh с корректным sessionPwd + runScenario_SessionRefresh_CorrectPwd(); + + System.out.println("Все тесты завершены, выходим."); + } + + // ========================================================== + // SCENARIO 1: AddUser + Auth + // ========================================================== + + private static void runScenario_AddUser_And_FirstAuth() throws Exception { + System.out.println(); + System.out.println("=== СЦЕНАРИЙ 1: AddUser + AuthSessionNewStep1 + AuthSessionNewStep2 ==="); + + CountDownLatch latch = new CountDownLatch(1); + HttpClient client = HttpClient.newHttpClient(); + + WebSocket ws = client.newWebSocketBuilder() + .buildAsync(URI.create(WS_URI), new Listener() { + + private int step = 0; // 0 - AddUser, 1 - AuthStep1, 2 - AuthStep2 + + @Override + public void onOpen(WebSocket webSocket) { + System.out.println("✅ [S1] WebSocket подключен"); + webSocket.request(1); + sendNextRequest(webSocket); + Listener.super.onOpen(webSocket); + } + + private void sendNextRequest(WebSocket webSocket) { + switch (step) { + case 0 -> { + String json = buildAddUserJson(); + System.out.println(); + System.out.println("📤 [S1 / Шаг 1] Отправляем AddUser:"); + System.out.println(json); + webSocket.sendText(json, true); + } + case 1 -> { + String json = buildAuthStep1Json(); + System.out.println(); + System.out.println("📤 [S1 / Шаг 2] Отправляем AuthSessionNewStep1:"); + System.out.println(json); + webSocket.sendText(json, true); + } + case 2 -> { + GLOBAL_STORAGE_PWD_SENT = generateFakeStoragePwd(); + String json = buildAuthStep2Json(GLOBAL_SESSION_PWD, GLOBAL_STORAGE_PWD_SENT); + System.out.println(); + System.out.println("📤 [S1 / Шаг 3] Отправляем AuthSessionNewStep2 (подпись deviceKey):"); + System.out.println(json); + webSocket.sendText(json, true); + } + default -> { + System.out.println("✅ [S1] Все шаги выполнены, закрываем соединение"); + webSocket.sendClose(WebSocket.NORMAL_CLOSURE, "scenario1 done"); + } + } + } + + @Override + public CompletionStage onText(WebSocket webSocket, + CharSequence data, + boolean last) { + String message = data.toString(); + System.out.println("📥 [S1] Ответ на шаг " + (step + 1) + ":"); + System.out.println(message); + System.out.println("-----------------------------------------------------"); + + // Шаг 2: получаем sessionPwd + if (step == 1) { + GLOBAL_SESSION_PWD = extractSessionPwd(message); + System.out.println("🔑 [S1] Извлечён sessionPwd: " + GLOBAL_SESSION_PWD); + } + + // Шаг 3: получаем sessionId + if (step == 2) { + GLOBAL_SESSION_ID = extractSessionId(message); + System.out.println("🆔 [S1] Извлечён sessionId: " + GLOBAL_SESSION_ID); + System.out.println(" (Эта sessionId и sessionPwd понадобятся в сценариях 2 и 3)"); + } + + step++; + sendNextRequest(webSocket); + webSocket.request(1); + + return CompletableFuture.completedFuture(null); + } + + @Override + public void onError(WebSocket webSocket, Throwable error) { + System.out.println("❌ [S1] Ошибка WebSocket-клиента: " + error.getMessage()); + error.printStackTrace(System.out); + latch.countDown(); + } + + @Override + public CompletionStage onClose(WebSocket webSocket, + int statusCode, + String reason) { + System.out.println("🔚 [S1] Соединение закрыто. Код=" + statusCode + ", причина=" + reason); + latch.countDown(); + return CompletableFuture.completedFuture(null); + } + }).join(); + + latch.await(); + System.out.println("=== СЦЕНАРИЙ 1 завершён ==="); + } + + // ========================================================== + // SCENARIO 2: SessionRefresh с неправильным паролем + // ========================================================== + + private static void runScenario_SessionRefresh_WrongPwd() throws Exception { + System.out.println(); + System.out.println("=== СЦЕНАРИЙ 2: SessionRefresh с НЕВЕРНЫМ sessionPwd ==="); + System.out.println("Ожидаем ОТРИЦАТЕЛЬНЫЙ ответ сервера (UNVERIFIED / SESSION_PWD_MISMATCH и т.п.)"); + + if (GLOBAL_SESSION_ID == null || GLOBAL_SESSION_PWD == null) { + System.out.println("⚠️ Нет sessionId или sessionPwd из сценария 1, пропускаем сценарий 2."); + return; + } + + CountDownLatch latch = new CountDownLatch(1); + HttpClient client = HttpClient.newHttpClient(); + + // Специально подменяем пароль, чтобы сервер его НЕ принял + String wrongPwd = GLOBAL_SESSION_PWD + "_WRONG"; + + WebSocket ws = client.newWebSocketBuilder() + .buildAsync(URI.create(WS_URI), new Listener() { + + @Override + public void onOpen(WebSocket webSocket) { + System.out.println("✅ [S2] WebSocket подключен"); + webSocket.request(1); + + String json = buildSessionRefreshJson(GLOBAL_SESSION_ID, wrongPwd, "test-refresh-wrong-1"); + System.out.println(); + System.out.println("📤 [S2] Отправляем SessionRefresh с НЕВЕРНЫМ sessionPwd:"); + System.out.println(json); + webSocket.sendText(json, true); + Listener.super.onOpen(webSocket); + } + + @Override + public CompletionStage onText(WebSocket webSocket, + CharSequence data, + boolean last) { + String message = data.toString(); + System.out.println("📥 [S2] Ответ сервера (ожидаем ошибку):"); + System.out.println(message); + System.out.println("-----------------------------------------------------"); + System.out.println("💬 [S2] Если в ответе status != 200 и/или код ошибки про неверный пароль — это ПРАВИЛЬНОЕ поведение."); + + webSocket.sendClose(WebSocket.NORMAL_CLOSURE, "scenario2 done"); + webSocket.request(1); + return CompletableFuture.completedFuture(null); + } + + @Override + public void onError(WebSocket webSocket, Throwable error) { + System.out.println("❌ [S2] Ошибка WebSocket-клиента: " + error.getMessage()); + error.printStackTrace(System.out); + latch.countDown(); + } + + @Override + public CompletionStage onClose(WebSocket webSocket, + int statusCode, + String reason) { + System.out.println("🔚 [S2] Соединение закрыто. Код=" + statusCode + ", причина=" + reason); + latch.countDown(); + return CompletableFuture.completedFuture(null); + } + }).join(); + + latch.await(); + System.out.println("=== СЦЕНАРИЙ 2 завершён ==="); + } + + // ========================================================== + // SCENARIO 3: SessionRefresh с правильными данными + // ========================================================== + + private static void runScenario_SessionRefresh_CorrectPwd() throws Exception { + System.out.println(); + System.out.println("=== СЦЕНАРИЙ 3: SessionRefresh с КОРРЕКТНЫМ sessionPwd ==="); + System.out.println("Ожидаем УСПЕШНЫЙ ответ сервера (status=200),"); + System.out.println(" а в payload должен вернуться актуальный storagePwd (по твоей схеме)."); + + if (GLOBAL_SESSION_ID == null || GLOBAL_SESSION_PWD == null) { + System.out.println("⚠️ Нет sessionId или sessionPwd из сценария 1, пропускаем сценарий 3."); + return; + } + + CountDownLatch latch = new CountDownLatch(1); + HttpClient client = HttpClient.newHttpClient(); + + WebSocket ws = client.newWebSocketBuilder() + .buildAsync(URI.create(WS_URI), new Listener() { + + @Override + public void onOpen(WebSocket webSocket) { + System.out.println("✅ [S3] WebSocket подключен"); + webSocket.request(1); + + String json = buildSessionRefreshJson(GLOBAL_SESSION_ID, GLOBAL_SESSION_PWD, "test-refresh-ok-1"); + System.out.println(); + System.out.println("📤 [S3] Отправляем SessionRefresh с КОРРЕКТНЫМ sessionPwd:"); + System.out.println(json); + webSocket.sendText(json, true); + Listener.super.onOpen(webSocket); + } + + @Override + public CompletionStage onText(WebSocket webSocket, + CharSequence data, + boolean last) { + String message = data.toString(); + System.out.println("📥 [S3] Ответ сервера (ожидаем успех):"); + System.out.println(message); + System.out.println("-----------------------------------------------------"); + System.out.println("💬 [S3] Если status=200 — сессия успешно восстановлена."); + String storagePwdFromServer = extractStoragePwd(message); + System.out.println("🧾 [S3] storagePwd от сервера: " + storagePwdFromServer); + System.out.println(" (Может совпадать с тем, что был в шаге 2, или быть обновлённым — зависит от логики сервера)"); + + webSocket.sendClose(WebSocket.NORMAL_CLOSURE, "scenario3 done"); + webSocket.request(1); + return CompletableFuture.completedFuture(null); + } + + @Override + public void onError(WebSocket webSocket, Throwable error) { + System.out.println("❌ [S3] Ошибка WebSocket-клиента: " + error.getMessage()); + error.printStackTrace(System.out); + latch.countDown(); + } + + @Override + public CompletionStage onClose(WebSocket webSocket, + int statusCode, + String reason) { + System.out.println("🔚 [S3] Соединение закрыто. Код=" + statusCode + ", причина=" + reason); + latch.countDown(); + return CompletableFuture.completedFuture(null); + } + }).join(); + + latch.await(); + System.out.println("=== СЦЕНАРИЙ 3 завершён ==="); + } + + // ========================================================== + // JSON BUILDERS + // ========================================================== + + // 1) AddUser с payload (loginKey != deviceKey) + private static String buildAddUserJson() { + return """ + { + "op": "AddUser", + "requestId": "test-add-1", + "payload": { + "login": "%s", + "loginId": %d, + "bchId": %d, + "loginKey": "%s", + "deviceKey": "%s", + "bchLimit": %d + } + } + """.formatted( + TEST_LOGIN, + TEST_LOGIN_ID, + TEST_BCH_ID, + LOGIN_PUBKEY_B64, // loginKey + DEVICE_PUBKEY_B64, // deviceKey + TEST_BCH_LIMIT + ); + } + + // 2) Шаг 1 авторизации: запрос sessionPwd + private static String buildAuthStep1Json() { + return """ + { + "op": "AuthSessionNewStep1", + "requestId": "test-auth-1", + "payload": { + "login": "%s" + } + } + """.formatted(TEST_LOGIN); + } + + // 3) Шаг 2 авторизации: подтверждение подписью + // payload: storagePwd, timeMs, signatureB64 + private static String buildAuthStep2Json(String sessionPwd, String storagePwd) { + if (sessionPwd == null) { + sessionPwd = ""; + } + if (storagePwd == null || storagePwd.isBlank()) { + storagePwd = generateFakeStoragePwd(); + } + + long timeMs = System.currentTimeMillis(); + + // preimage = "AUTHORIFICATED:" + timeMs + sessionPwd + String preimageStr = "AUTHORIFICATED:" + timeMs + sessionPwd; + byte[] preimage = preimageStr.getBytes(StandardCharsets.UTF_8); + + // Подписываем приватным ключом устройства (deviceKey) + byte[] sig = Ed25519Util.sign(preimage, DEVICE_PRIV_KEY); + String sigB64 = Base64.getEncoder().encodeToString(sig); + + return """ + { + "op": "AuthSessionNewStep2", + "requestId": "test-auth-2", + "payload": { + "storagePwd": "%s", + "timeMs": %d, + "signatureB64": "%s" + } + } + """.formatted( + storagePwd, + timeMs, + sigB64 + ); + } + + // 4) SessionRefresh: всё в payload + private static String buildSessionRefreshJson(String sessionId, String sessionPwd, String requestId) { + return """ + { + "op": "SessionRefresh", + "requestId": "%s", + "payload": { + "sessionId": "%s", + "sessionPwd": "%s" + } + } + """.formatted( + requestId, + sessionId, + sessionPwd + ); + } + + // просто для теста: base64 от 32 байт "storage" ключа + private static String generateFakeStoragePwd() { + byte[] data = new byte[32]; + for (int i = 0; i < data.length; i++) { + data[i] = (byte) (i + 1); + } + return Base64.getEncoder().encodeToString(data); + } + + // ========================================================== + // JSON HELPERS + // ========================================================== + + private static String extractSessionPwd(String json) { + try { + JsonNode root = JSON_MAPPER.readTree(json); + JsonNode payload = root.get("payload"); + if (payload != null && payload.has("sessionPwd")) { + return payload.get("sessionPwd").asText(); + } + } catch (Exception e) { + System.out.println("⚠️ Не удалось распарсить sessionPwd из ответа: " + e.getMessage()); + } + return null; + } + + private static String extractSessionId(String json) { + try { + JsonNode root = JSON_MAPPER.readTree(json); + JsonNode payload = root.get("payload"); + if (payload != null && payload.has("sessionId")) { + return payload.get("sessionId").asText(); + } + } catch (Exception e) { + System.out.println("⚠️ Не удалось распарсить sessionId из ответа: " + e.getMessage()); + } + return null; + } + + private static String extractStoragePwd(String json) { + try { + JsonNode root = JSON_MAPPER.readTree(json); + JsonNode payload = root.get("payload"); + if (payload != null && payload.has("storagePwd")) { + return payload.get("storagePwd").asText(); + } + } catch (Exception e) { + System.out.println("⚠️ Не удалось распарсить storagePwd из ответа: " + e.getMessage()); + } + return null; + } +}