Авторификация работает и тест авторификации проходит.

(создание пользователя, два этапа создания сессии и рефреш сессии)
This commit is contained in:
AidarKC 2025-12-09 20:04:18 +03:00
parent 2ed4f6d666
commit 888bb1595f
12 changed files with 723 additions and 372 deletions

View File

@ -1,6 +1,5 @@
package shine.db; package shine.db;
import utils.config.AppConfig; import utils.config.AppConfig;
import java.io.BufferedReader; import java.io.BufferedReader;
@ -12,6 +11,15 @@ import java.sql.DriverManager;
import java.sql.SQLException; import java.sql.SQLException;
import java.sql.Statement; import java.sql.Statement;
/**
* DatabaseInitializer создание новой SQLite-БД по схеме SHiNE.
*
* Читает путь к файлу БД из application.properties (db.path),
* при необходимости удаляет старый файл и создаёт таблицы:
* - solana_users
* - active_sessions
* - users_params
*/
public class DatabaseInitializer { public class DatabaseInitializer {
public static void createNewDB(String[] args) { public static void createNewDB(String[] args) {
@ -75,9 +83,9 @@ public class DatabaseInitializer {
login TEXT NOT NULL, login TEXT NOT NULL,
loginId INTEGER NOT NULL PRIMARY KEY, loginId INTEGER NOT NULL PRIMARY KEY,
bchId INTEGER NOT NULL, bchId INTEGER NOT NULL,
pubkey0 TEXT, loginKey TEXT, -- основной публичный ключ (логин)
pubkey1 TEXT, deviceKey TEXT, -- публичный ключ устройства
bchLimit INTEGER -- может быть NULL bchLimit INTEGER -- может быть NULL
); );
"""); """);
@ -87,22 +95,29 @@ public class DatabaseInitializer {
"""); """);
// 2. Таблица active_sessions // 2. Таблица active_sessions
// sessionId теперь TEXT (base64 от 32 байт), а не INTEGER.
st.executeUpdate(""" st.executeUpdate("""
CREATE TABLE IF NOT EXISTS active_sessions ( CREATE TABLE IF NOT EXISTS active_sessions (
sessionId INTEGER NOT NULL PRIMARY KEY, sessionId TEXT NOT NULL PRIMARY KEY,
session_pwd TEXT NOT NULL, loginId INTEGER NOT NULL,
loginId INTEGER NOT NULL, sessionPwd TEXT NOT NULL,
time_ms INTEGER NOT NULL, storagePwd TEXT NOT NULL,
pubkey_num INTEGER NOT NULL, sessionCreatedAtMs INTEGER NOT NULL,
push_endpoint TEXT, lastAuthirificatedAtMs INTEGER NOT NULL,
push_p256dh_key TEXT, pushEndpoint TEXT,
push_auth_key TEXT, pushP256dhKey TEXT,
pushAuthKey TEXT,
FOREIGN KEY (loginId) REFERENCES solana_users(loginId) 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 // 3. Таблица users_params
// Важно: пара (loginId, param) должна быть уникальна // Пара (loginId, param) должна быть уникальна.
st.executeUpdate(""" st.executeUpdate("""
CREATE TABLE IF NOT EXISTS users_params ( CREATE TABLE IF NOT EXISTS users_params (
loginId INTEGER NOT NULL, loginId INTEGER NOT NULL,

View File

@ -5,7 +5,11 @@ import shine.db.entities.ActiveSession;
import java.sql.*; import java.sql.*;
/** Здесь мы храним данные об активных сессиях пользователя (для wss соединений). */ /**
* DAO для таблицы active_sessions.
*
* Здесь мы храним данные об активных сессиях пользователя (для wss-соединений).
*/
public final class ActiveSessionsDAO { public final class ActiveSessionsDAO {
private static volatile ActiveSessionsDAO instance; private static volatile ActiveSessionsDAO instance;
@ -25,18 +29,21 @@ public final class ActiveSessionsDAO {
return instance; return instance;
} }
/**
* Вставка новой сессии.
*/
public void insert(ActiveSession session) throws SQLException { public void insert(ActiveSession session) throws SQLException {
String sql = """ String sql = """
INSERT INTO active_sessions ( INSERT INTO active_sessions (
sessionId, sessionId,
loginId, loginId,
session_pwd, sessionPwd,
storage_pwd, storagePwd,
session_created_ms, sessionCreatedAtMs,
last_auth_ms, lastAuthirificatedAtMs,
push_endpoint, pushEndpoint,
push_p256dh_key, pushP256dhKey,
push_auth_key pushAuthKey
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
"""; """;
@ -50,22 +57,26 @@ public final class ActiveSessionsDAO {
ps.setString(7, session.getPushEndpoint()); ps.setString(7, session.getPushEndpoint());
ps.setString(8, session.getPushP256dhKey()); ps.setString(8, session.getPushP256dhKey());
ps.setString(9, session.getPushAuthKey()); ps.setString(9, session.getPushAuthKey());
ps.executeUpdate(); ps.executeUpdate();
} }
} }
/**
* Получить сессию по sessionId.
*/
public ActiveSession getBySessionId(String sessionId) throws SQLException { public ActiveSession getBySessionId(String sessionId) throws SQLException {
String sql = """ String sql = """
SELECT SELECT
sessionId, sessionId,
loginId, loginId,
session_pwd, sessionPwd,
storage_pwd, storagePwd,
session_created_ms, sessionCreatedAtMs,
last_auth_ms, lastAuthirificatedAtMs,
push_endpoint, pushEndpoint,
push_p256dh_key, pushP256dhKey,
push_auth_key pushAuthKey
FROM active_sessions FROM active_sessions
WHERE sessionId = ? 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. * Удаление записи по sessionId.
* Если записи нет просто ничего не удалит (0 строк). * Если записи нет просто ничего не удалит (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 { private ActiveSession mapRow(ResultSet rs) throws SQLException {
String sessionId = rs.getString("sessionId"); String sessionId = rs.getString("sessionId");
long loginId = rs.getLong("loginId"); long loginId = rs.getLong("loginId");
String sessionPwd = rs.getString("session_pwd"); String sessionPwd = rs.getString("sessionPwd");
String storagePwd = rs.getString("storage_pwd"); String storagePwd = rs.getString("storagePwd");
long sessionCreatedMs = rs.getLong("session_created_ms"); long sessionCreatedAtMs = rs.getLong("sessionCreatedAtMs");
long lastAuthMs = rs.getLong("last_auth_ms"); long lastAuthirificatedAtMs = rs.getLong("lastAuthirificatedAtMs");
String pushEndpoint = rs.getString("push_endpoint"); String pushEndpoint = rs.getString("pushEndpoint");
String pushP256dhKey = rs.getString("push_p256dh_key"); String pushP256dhKey = rs.getString("pushP256dhKey");
String pushAuthKey = rs.getString("push_auth_key"); String pushAuthKey = rs.getString("pushAuthKey");
return new ActiveSession( return new ActiveSession(
sessionId, sessionId,
loginId, loginId,
sessionPwd, sessionPwd,
storagePwd, storagePwd,
sessionCreatedMs, sessionCreatedAtMs,
lastAuthMs, lastAuthirificatedAtMs,
pushEndpoint, pushEndpoint,
pushP256dhKey, pushP256dhKey,
pushAuthKey pushAuthKey

View File

@ -7,8 +7,17 @@ import java.sql.*;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
/** Здесь храним данные об пользователях - локальная копия того что есть в солане */ /**
* SolanaUsersDAO локальная таблица пользователей из Solana.
*
* Колонки:
* - login TEXT
* - loginId INTEGER (PK)
* - bchId INTEGER
* - loginKey TEXT
* - deviceKey TEXT
* - bchLimit INTEGER (может быть NULL)
*/
public final class SolanaUsersDAO { public final class SolanaUsersDAO {
private static volatile SolanaUsersDAO instance; private static volatile SolanaUsersDAO instance;
@ -29,7 +38,7 @@ public final class SolanaUsersDAO {
public void insert(SolanaUser user) throws SQLException { public void insert(SolanaUser user) throws SQLException {
String sql = """ String sql = """
INSERT INTO solana_users (login, loginId, bchId, pubkey0, pubkey1, bchLimit) INSERT INTO solana_users (login, loginId, bchId, loginKey, deviceKey, bchLimit)
VALUES (?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?)
"""; """;
@ -37,8 +46,8 @@ public final class SolanaUsersDAO {
ps.setString(1, user.getLogin()); ps.setString(1, user.getLogin());
ps.setLong(2, user.getLoginId()); ps.setLong(2, user.getLoginId());
ps.setLong(3, user.getBchId()); ps.setLong(3, user.getBchId());
ps.setString(4, user.getPubkey0()); ps.setString(4, user.getLoginKey());
ps.setString(5, user.getPubkey1()); ps.setString(5, user.getDeviceKey());
if (user.getBchLimit() != null) { if (user.getBchLimit() != null) {
ps.setInt(6, user.getBchLimit()); ps.setInt(6, user.getBchLimit());
@ -52,7 +61,7 @@ public final class SolanaUsersDAO {
public SolanaUser getByLoginId(long loginId) throws SQLException { public SolanaUser getByLoginId(long loginId) throws SQLException {
String sql = """ String sql = """
SELECT login, loginId, bchId, pubkey0, pubkey1, bchLimit SELECT login, loginId, bchId, loginKey, deviceKey, bchLimit
FROM solana_users FROM solana_users
WHERE loginId = ? WHERE loginId = ?
"""; """;
@ -69,7 +78,7 @@ public final class SolanaUsersDAO {
public SolanaUser getByLogin(String login) throws SQLException { public SolanaUser getByLogin(String login) throws SQLException {
String sql = """ String sql = """
SELECT login, loginId, bchId, pubkey0, pubkey1, bchLimit SELECT login, loginId, bchId, loginKey, deviceKey, bchLimit
FROM solana_users FROM solana_users
WHERE LOWER(login) = LOWER(?) WHERE LOWER(login) = LOWER(?)
"""; """;
@ -86,7 +95,7 @@ public final class SolanaUsersDAO {
public List<SolanaUser> searchByLoginPrefix(String prefix) throws SQLException { public List<SolanaUser> searchByLoginPrefix(String prefix) throws SQLException {
String sql = """ String sql = """
SELECT login, loginId, bchId, pubkey0, pubkey1, bchLimit SELECT login, loginId, bchId, loginKey, deviceKey, bchLimit
FROM solana_users FROM solana_users
WHERE LOWER(login) LIKE ? WHERE LOWER(login) LIKE ?
ORDER BY login ORDER BY login
@ -111,8 +120,8 @@ public final class SolanaUsersDAO {
rs.getLong("loginId"), rs.getLong("loginId"),
rs.getString("login"), rs.getString("login"),
rs.getLong("bchId"), rs.getLong("bchId"),
rs.getString("pubkey0"), rs.getString("loginKey"),
rs.getString("pubkey1"), rs.getString("deviceKey"),
rs.getObject("bchLimit") != null ? rs.getInt("bchLimit") : null rs.getObject("bchLimit") != null ? rs.getInt("bchLimit") : null
); );
} }

View File

@ -1,30 +1,34 @@
package shine.db.entities; package shine.db.entities;
/** /**
* ActiveSession запись об активной сессии пользователя. * Модель активной сессии (таблица active_sessions).
* *
* Поля: * Поля соответствуют схеме:
* - sessionId строка (base64 от 32 байт) *
* - loginId long * CREATE TABLE active_sessions (
* - sessionPwd строка (секрет шага 1) * sessionId TEXT NOT NULL PRIMARY KEY,
* - storagePwd строка (секрет клиента для хранения данных) * loginId INTEGER NOT NULL,
* - sessionCreatedAtMs long (время создания) * sessionPwd TEXT NOT NULL,
* - lastAuthirificatedAtMs long (последнее подтверждение/refresh) * storagePwd TEXT NOT NULL,
* - pushEndpoint строка (WebPush, пока null/пусто) * sessionCreatedAtMs INTEGER NOT NULL,
* - pushP256dhKey строка (WebPush, пока null/пусто) * lastAuthirificatedAtMs INTEGER NOT NULL,
* - pushAuthKey строка (WebPush, пока null/пусто) * pushEndpoint TEXT,
* pushP256dhKey TEXT,
* pushAuthKey TEXT,
* FOREIGN KEY (loginId) REFERENCES solana_users(loginId)
* );
*/ */
public class ActiveSession { public class ActiveSession {
private String sessionId; private String sessionId; // TEXT base64(32 bytes)
private long loginId; private long loginId; // INTEGER
private String sessionPwd; private String sessionPwd; // TEXT
private String storagePwd; private String storagePwd; // TEXT
private long sessionCreatedAtMs; private long sessionCreatedAtMs; // INTEGER
private long lastAuthirificatedAtMs; private long lastAuthirificatedAtMs; // INTEGER
private String pushEndpoint; private String pushEndpoint; // TEXT (nullable)
private String pushP256dhKey; private String pushP256dhKey; // TEXT (nullable)
private String pushAuthKey; private String pushAuthKey; // TEXT (nullable)
public ActiveSession() { public ActiveSession() {
} }
@ -49,9 +53,12 @@ public class ActiveSession {
this.pushAuthKey = pushAuthKey; this.pushAuthKey = pushAuthKey;
} }
// --- getters / setters ---
public String getSessionId() { public String getSessionId() {
return sessionId; return sessionId;
} }
public void setSessionId(String sessionId) { public void setSessionId(String sessionId) {
this.sessionId = sessionId; this.sessionId = sessionId;
} }
@ -59,6 +66,7 @@ public class ActiveSession {
public long getLoginId() { public long getLoginId() {
return loginId; return loginId;
} }
public void setLoginId(long loginId) { public void setLoginId(long loginId) {
this.loginId = loginId; this.loginId = loginId;
} }
@ -66,6 +74,7 @@ public class ActiveSession {
public String getSessionPwd() { public String getSessionPwd() {
return sessionPwd; return sessionPwd;
} }
public void setSessionPwd(String sessionPwd) { public void setSessionPwd(String sessionPwd) {
this.sessionPwd = sessionPwd; this.sessionPwd = sessionPwd;
} }
@ -73,6 +82,7 @@ public class ActiveSession {
public String getStoragePwd() { public String getStoragePwd() {
return storagePwd; return storagePwd;
} }
public void setStoragePwd(String storagePwd) { public void setStoragePwd(String storagePwd) {
this.storagePwd = storagePwd; this.storagePwd = storagePwd;
} }
@ -80,6 +90,7 @@ public class ActiveSession {
public long getSessionCreatedAtMs() { public long getSessionCreatedAtMs() {
return sessionCreatedAtMs; return sessionCreatedAtMs;
} }
public void setSessionCreatedAtMs(long sessionCreatedAtMs) { public void setSessionCreatedAtMs(long sessionCreatedAtMs) {
this.sessionCreatedAtMs = sessionCreatedAtMs; this.sessionCreatedAtMs = sessionCreatedAtMs;
} }
@ -87,6 +98,7 @@ public class ActiveSession {
public long getLastAuthirificatedAtMs() { public long getLastAuthirificatedAtMs() {
return lastAuthirificatedAtMs; return lastAuthirificatedAtMs;
} }
public void setLastAuthirificatedAtMs(long lastAuthirificatedAtMs) { public void setLastAuthirificatedAtMs(long lastAuthirificatedAtMs) {
this.lastAuthirificatedAtMs = lastAuthirificatedAtMs; this.lastAuthirificatedAtMs = lastAuthirificatedAtMs;
} }
@ -94,6 +106,7 @@ public class ActiveSession {
public String getPushEndpoint() { public String getPushEndpoint() {
return pushEndpoint; return pushEndpoint;
} }
public void setPushEndpoint(String pushEndpoint) { public void setPushEndpoint(String pushEndpoint) {
this.pushEndpoint = pushEndpoint; this.pushEndpoint = pushEndpoint;
} }
@ -101,6 +114,7 @@ public class ActiveSession {
public String getPushP256dhKey() { public String getPushP256dhKey() {
return pushP256dhKey; return pushP256dhKey;
} }
public void setPushP256dhKey(String pushP256dhKey) { public void setPushP256dhKey(String pushP256dhKey) {
this.pushP256dhKey = pushP256dhKey; this.pushP256dhKey = pushP256dhKey;
} }
@ -108,6 +122,7 @@ public class ActiveSession {
public String getPushAuthKey() { public String getPushAuthKey() {
return pushAuthKey; return pushAuthKey;
} }
public void setPushAuthKey(String pushAuthKey) { public void setPushAuthKey(String pushAuthKey) {
this.pushAuthKey = pushAuthKey; this.pushAuthKey = pushAuthKey;
} }

View File

@ -1,13 +1,23 @@
package shine.db.entities; package shine.db.entities;
/**
* Локальная копия пользователя из Solana.
*
* Храним:
* - login / loginId;
* - bchId id персонального блокчейна;
* - loginKey публичный ключ для логина / авторизации;
* - deviceKey публичный ключ устройства (второй ключ);
* - bchLimit лимит по количеству блоков / размеру цепочки (может быть null).
*/
public class SolanaUser { public class SolanaUser {
private long loginId; private long loginId;
private String login; private String login;
private long bchId; private long bchId;
private String pubkey0; private String loginKey; // раньше pubkey0
private String pubkey1; private String deviceKey; // раньше pubkey1
private Integer bchLimit; // может быть null private Integer bchLimit; // может быть null
public SolanaUser() { public SolanaUser() {
} }
@ -15,14 +25,14 @@ public class SolanaUser {
public SolanaUser(long loginId, public SolanaUser(long loginId,
String login, String login,
long bchId, long bchId,
String pubkey0, String loginKey,
String pubkey1, String deviceKey,
Integer bchLimit) { Integer bchLimit) {
this.loginId = loginId; this.loginId = loginId;
this.login = login; this.login = login;
this.bchId = bchId; this.bchId = bchId;
this.pubkey0 = pubkey0; this.loginKey = loginKey;
this.pubkey1 = pubkey1; this.deviceKey = deviceKey;
this.bchLimit = bchLimit; this.bchLimit = bchLimit;
} }
@ -50,20 +60,22 @@ public class SolanaUser {
this.bchId = bchId; this.bchId = bchId;
} }
public String getPubkey0() { /** Публичный ключ логина (основной ключ пользователя). */
return pubkey0; public String getLoginKey() {
return loginKey;
} }
public void setPubkey0(String pubkey0) { public void setLoginKey(String loginKey) {
this.pubkey0 = pubkey0; this.loginKey = loginKey;
} }
public String getPubkey1() { /** Публичный ключ устройства (device key). */
return pubkey1; public String getDeviceKey() {
return deviceKey;
} }
public void setPubkey1(String pubkey1) { public void setDeviceKey(String deviceKey) {
this.pubkey1 = pubkey1; this.deviceKey = deviceKey;
} }
public Integer getBchLimit() { public Integer getBchLimit() {

View File

@ -3,27 +3,32 @@ package server.logic.ws_protocol.JSON.entyties.tempToTest;
import server.logic.ws_protocol.JSON.entyties.NetRequest; import server.logic.ws_protocol.JSON.entyties.NetRequest;
/** /**
* Запрос AddUser. * Запрос AddUser временная/тестовая регистрация локального пользователя.
*. *
* Ожидаемый JSON: * Клиент отправляет:
*
* { * {
* "op": "AddUser", * "op": "AddUser",
* "requestId": "...", * "requestId": "test-add-1",
* "login": "...", * "payload": {
* "loginId": 123, * "login": "anya",
* "bchId": 456, * "loginId": 100211,
* "pubkey0": "...", * "bchId": 4222,
* "pubkey1": "...", * "loginKey": "base64-ed25519-public-key-login",
* "bchLimit": 1000 * "deviceKey": "base64-ed25519-public-key-device",
* "bchLimit": 1000000
* }
* } * }
*
* Все поля лежат внутри payload.
*/ */
public class NetAddUserRequest extends NetRequest { public class NetAddUserRequest extends NetRequest {
private String login; private String login;
private long loginId; private long loginId;
private long bchId; private long bchId;
private String pubkey0; private String loginKey;
private String pubkey1; private String deviceKey;
private Integer bchLimit; private Integer bchLimit;
public String getLogin() { public String getLogin() {
@ -50,20 +55,20 @@ public class NetAddUserRequest extends NetRequest {
this.bchId = bchId; this.bchId = bchId;
} }
public String getPubkey0() { public String getLoginKey() {
return pubkey0; return loginKey;
} }
public void setPubkey0(String pubkey0) { public void setLoginKey(String loginKey) {
this.pubkey0 = pubkey0; this.loginKey = loginKey;
} }
public String getPubkey1() { public String getDeviceKey() {
return pubkey1; return deviceKey;
} }
public void setPubkey1(String pubkey1) { public void setDeviceKey(String deviceKey) {
this.pubkey1 = pubkey1; this.deviceKey = deviceKey;
} }
public Integer getBchLimit() { public Integer getBchLimit() {

View File

@ -4,8 +4,17 @@ import server.logic.ws_protocol.JSON.entyties.NetResponse;
/** /**
* Успешный ответ на AddUser. * Успешный ответ на AddUser.
* Дополнительных полей нет достаточно status=200. *
* Сейчас дополнительных полей нет достаточно status=200.
*
* Пример:
* {
* "op": "AddUser",
* "requestId": "test-add-1",
* "status": 200,
* "payload": { }
* }
*/ */
public class NetAddUserResponse extends NetResponse { public class NetAddUserResponse extends NetResponse {
// Можно потом добавить какие-то данные, если понадобится. // При необходимости сюда можно добавить, например, флаг created/updated и т.п.
} }

View File

@ -119,7 +119,7 @@ public class NetAuthSessionNewStep2Handler implements JsonMessageHandler {
} }
// --- выбираем публичный ключ pubkey1 --- // --- выбираем публичный ключ pubkey1 ---
String pubKeyB64 = user.getPubkey1(); String pubKeyB64 = user.getDeviceKey();
if (pubKeyB64 == null || pubKeyB64.isBlank()) { if (pubKeyB64 == null || pubKeyB64.isBlank()) {
return NetExceptionResponseFactory.error( return NetExceptionResponseFactory.error(
req, req,

View File

@ -16,7 +16,25 @@ import shine.db.entities.SolanaUser;
import java.sql.SQLException; 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 { public class NetAddUserHandler implements JsonMessageHandler {
@ -28,15 +46,15 @@ public class NetAddUserHandler implements JsonMessageHandler {
// Одна общая проверка всех ключевых полей // Одна общая проверка всех ключевых полей
if (req.getLogin() == null || req.getLogin().isBlank() if (req.getLogin() == null || req.getLogin().isBlank()
|| req.getPubkey0() == null || req.getPubkey0().isBlank() || req.getLoginKey() == null || req.getLoginKey().isBlank()
|| req.getPubkey1() == null || req.getPubkey1().isBlank() || req.getDeviceKey() == null || req.getDeviceKey().isBlank()
|| req.getBchLimit() == null) { || req.getBchLimit() == null) {
return NetExceptionResponseFactory.error( return NetExceptionResponseFactory.error(
req, req,
WireCodes.Status.BAD_REQUEST, WireCodes.Status.BAD_REQUEST,
"BAD_FIELDS", "BAD_FIELDS",
"Некорректные или пустые поля: login, pubkey0, pubkey1, bchLimit" "Некорректные или пустые поля: login, loginKey, deviceKey, bchLimit"
); );
} }
@ -47,8 +65,8 @@ public class NetAddUserHandler implements JsonMessageHandler {
req.getLoginId(), req.getLoginId(),
req.getLogin(), req.getLogin(),
req.getBchId(), req.getBchId(),
req.getPubkey0(), req.getLoginKey(),
req.getPubkey1(), req.getDeviceKey(),
req.getBchLimit() req.getBchLimit()
); );
@ -58,7 +76,7 @@ public class NetAddUserHandler implements JsonMessageHandler {
resp.setOp(req.getOp()); resp.setOp(req.getOp());
resp.setRequestId(req.getRequestId()); resp.setRequestId(req.getRequestId());
resp.setStatus(WireCodes.Status.OK); resp.setStatus(WireCodes.Status.OK);
// payload сам станет {} через JsonInboundProcessor // payload станет {} через JsonInboundProcessor
log.info("✅ Пользователь добавлен: login={}, loginId={}", req.getLogin(), req.getLoginId()); log.info("✅ Пользователь добавлен: login={}, loginId={}", req.getLogin(), req.getLoginId());
return resp; return resp;

View File

@ -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 user@p628065:~/docker/ws-server$ docker restart caddy
очистить того кто держит порт 7070 :)
kill -9 $(lsof -t -i:7070)

View File

@ -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;
}
}
}

View File

@ -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;
}
}