diff --git a/.idea/vcs.xml b/.idea/vcs.xml index 321f217..0faa797 100644 --- a/.idea/vcs.xml +++ b/.idea/vcs.xml @@ -1,6 +1,6 @@ - + \ No newline at end of file diff --git a/build.gradle b/build.gradle index 5920432..88ac072 100644 --- a/build.gradle +++ b/build.gradle @@ -31,12 +31,13 @@ dependencies { implementation project(':shine-server-config') // модуль настроек из application.properties - implementation project('shine-server-geo') // модуль для определения геолокации по IP implementation project(':shine-server-crypto') // модуль сервера для работы с криптографией implementation project(':shine-server-blockchain') // модуль для работы с блокчейном implementation project(':shine-server-db') // модуль для работы с БД содержит и сущности из БД и саму работу с БД + implementation project(':shine-server-geo') // модуль для определения геолокации по IP + implementation project(':shine-server-net-protocol') // Модуль отвечающий за протокол (классы Net..Request/Response implementation project(':shine-server-net-server') // Хэндлеры для обработки сетевых запросов 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 38d9606..47c5cce 100644 --- a/shine-server-db/src/main/java/shine/db/DatabaseInitializer.java +++ b/shine-server-db/src/main/java/shine/db/DatabaseInitializer.java @@ -34,7 +34,6 @@ public class DatabaseInitializer { Path dbFile = Paths.get(dbPath); try { - // создаём директорию, если нужно Path parent = dbFile.getParent(); if (parent != null && !Files.exists(parent)) { Files.createDirectories(parent); @@ -75,18 +74,17 @@ public class DatabaseInitializer { try (Connection conn = DriverManager.getConnection(jdbcUrl); Statement st = conn.createStatement()) { - // включаем внешние ключи на этом соединении (для инициализации тоже) st.execute("PRAGMA foreign_keys = ON"); - // 1. Таблица solana_users + // 1. solana_users st.executeUpdate(""" CREATE TABLE IF NOT EXISTS solana_users ( login TEXT NOT NULL, loginId INTEGER NOT NULL PRIMARY KEY, bchId INTEGER NOT NULL, - loginKey TEXT, -- основной публичный ключ (логин) - deviceKey TEXT, -- публичный ключ устройства - bchLimit INTEGER -- может быть NULL + loginKey TEXT, + deviceKey TEXT, + bchLimit INTEGER ); """); @@ -95,8 +93,7 @@ public class DatabaseInitializer { ON solana_users (login); """); - // 2. Таблица active_sessions - // sessionId TEXT (base64 от 32 байт). + // 2. active_sessions st.executeUpdate(""" CREATE TABLE IF NOT EXISTS active_sessions ( sessionId TEXT NOT NULL PRIMARY KEY, @@ -108,6 +105,10 @@ public class DatabaseInitializer { pushEndpoint TEXT, pushP256dhKey TEXT, pushAuthKey TEXT, + clientIp TEXT, + clientInfoFromClient TEXT, + clientInfoFromRequest TEXT, + userLanguage TEXT, FOREIGN KEY (loginId) REFERENCES solana_users(loginId) ); """); @@ -117,8 +118,7 @@ public class DatabaseInitializer { ON active_sessions (loginId); """); - // 3. Таблица users_params - // Пара (loginId, param) должна быть уникальна. + // 3. users_params st.executeUpdate(""" CREATE TABLE IF NOT EXISTS users_params ( loginId INTEGER NOT NULL, @@ -138,7 +138,7 @@ public class DatabaseInitializer { ON users_params (loginId); """); - // 4. Таблица ip_geo_cache — персистентный кэш геолокации по IP + // 4. ip_geo_cache st.executeUpdate(""" CREATE TABLE IF NOT EXISTS ip_geo_cache ( ip TEXT NOT NULL PRIMARY KEY, 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 f400e42..eb89cb5 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 @@ -9,6 +9,25 @@ import java.sql.*; * DAO для таблицы active_sessions. * * Здесь мы храним данные об активных сессиях пользователя (для wss-соединений). + * + * Структура таблицы: + * + * 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, + * clientIp TEXT, + * clientInfoFromClient TEXT, + * clientInfoFromRequest TEXT, + * userLanguage TEXT, + * FOREIGN KEY (loginId) REFERENCES solana_users(loginId) + * ); */ public final class ActiveSessionsDAO { @@ -43,20 +62,28 @@ public final class ActiveSessionsDAO { lastAuthirificatedAtMs, pushEndpoint, pushP256dhKey, - pushAuthKey - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + pushAuthKey, + clientIp, + clientInfoFromClient, + clientInfoFromRequest, + userLanguage + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """; try (PreparedStatement ps = db.getConnection().prepareStatement(sql)) { - ps.setString(1, session.getSessionId()); - ps.setLong(2, session.getLoginId()); - ps.setString(3, session.getSessionPwd()); - ps.setString(4, session.getStoragePwd()); - ps.setLong(5, session.getSessionCreatedAtMs()); - ps.setLong(6, session.getLastAuthirificatedAtMs()); - ps.setString(7, session.getPushEndpoint()); - ps.setString(8, session.getPushP256dhKey()); - ps.setString(9, session.getPushAuthKey()); + ps.setString(1, session.getSessionId()); + ps.setLong(2, session.getLoginId()); + ps.setString(3, session.getSessionPwd()); + ps.setString(4, session.getStoragePwd()); + ps.setLong(5, session.getSessionCreatedAtMs()); + ps.setLong(6, session.getLastAuthirificatedAtMs()); + ps.setString(7, session.getPushEndpoint()); + ps.setString(8, session.getPushP256dhKey()); + ps.setString(9, session.getPushAuthKey()); + ps.setString(10, session.getClientIp()); + ps.setString(11, session.getClientInfoFromClient()); + ps.setString(12, session.getClientInfoFromRequest()); + ps.setString(13, session.getUserLanguage()); ps.executeUpdate(); } @@ -76,7 +103,11 @@ public final class ActiveSessionsDAO { lastAuthirificatedAtMs, pushEndpoint, pushP256dhKey, - pushAuthKey + pushAuthKey, + clientIp, + clientInfoFromClient, + clientInfoFromRequest, + userLanguage FROM active_sessions WHERE sessionId = ? """; @@ -94,6 +125,7 @@ public final class ActiveSessionsDAO { /** * Обновить только lastAuthirificatedAtMs для конкретной сессии. + * (оставляю для совместимости, вдруг ещё где-то используется) */ public void updateLastAuthirificatedAtMs(String sessionId, long lastAuthMs) throws SQLException { String sql = """ @@ -109,6 +141,45 @@ public final class ActiveSessionsDAO { } } + /** + * Обновление метаданных при RefreshSession: + * - lastAuthirificatedAtMs + * - clientIp + * - clientInfoFromClient + * - clientInfoFromRequest + * - userLanguage + */ + public void updateOnRefresh( + String sessionId, + long lastAuthMs, + String clientIp, + String clientInfoFromClient, + String clientInfoFromRequest, + String userLanguage + ) throws SQLException { + + String sql = """ + UPDATE active_sessions + SET + lastAuthirificatedAtMs = ?, + clientIp = ?, + clientInfoFromClient = ?, + clientInfoFromRequest = ?, + userLanguage = ? + WHERE sessionId = ? + """; + + try (PreparedStatement ps = db.getConnection().prepareStatement(sql)) { + ps.setLong(1, lastAuthMs); + ps.setString(2, clientIp); + ps.setString(3, clientInfoFromClient); + ps.setString(4, clientInfoFromRequest); + ps.setString(5, userLanguage); + ps.setString(6, sessionId); + ps.executeUpdate(); + } + } + /** * Удаление записи по sessionId. * Если записи нет — просто ничего не удалит (0 строк). @@ -122,16 +193,23 @@ public final class ActiveSessionsDAO { } } + /** + * Маппинг ResultSet → ActiveSession (все 13 полей). + */ private ActiveSession mapRow(ResultSet rs) throws SQLException { - String sessionId = rs.getString("sessionId"); - long loginId = rs.getLong("loginId"); - 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"); + String sessionId = rs.getString("sessionId"); + long loginId = rs.getLong("loginId"); + 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"); + String clientIp = rs.getString("clientIp"); + String clientInfoFromClient = rs.getString("clientInfoFromClient"); + String clientInfoFromRequest = rs.getString("clientInfoFromRequest"); + String userLanguage = rs.getString("userLanguage"); return new ActiveSession( sessionId, @@ -142,7 +220,11 @@ public final class ActiveSessionsDAO { lastAuthirificatedAtMs, pushEndpoint, pushP256dhKey, - pushAuthKey + pushAuthKey, + clientIp, + clientInfoFromClient, + clientInfoFromRequest, + userLanguage ); } -} +} \ No newline at end of file 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 d8d14eb..584b0c7 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 @@ -3,8 +3,6 @@ package shine.db.entities; /** * Модель активной сессии (таблица active_sessions). * - * Поля соответствуют схеме: - * * CREATE TABLE active_sessions ( * sessionId TEXT NOT NULL PRIMARY KEY, * loginId INTEGER NOT NULL, @@ -15,6 +13,10 @@ package shine.db.entities; * pushEndpoint TEXT, * pushP256dhKey TEXT, * pushAuthKey TEXT, + * clientIp TEXT, + * clientInfoFromClient TEXT, + * clientInfoFromRequest TEXT, + * userLanguage TEXT, * FOREIGN KEY (loginId) REFERENCES solana_users(loginId) * ); */ @@ -30,6 +32,12 @@ public class ActiveSession { private String pushP256dhKey; // TEXT (nullable) private String pushAuthKey; // TEXT (nullable) + // Новые поля + private String clientIp; // IP клиента при auth/refresh + private String clientInfoFromClient; // строка от клиента (PWA) + private String clientInfoFromRequest; // строка, собранная на сервере + private String userLanguage; // prefer-language (например, "ru-RU") + public ActiveSession() { } @@ -41,7 +49,11 @@ public class ActiveSession { long lastAuthirificatedAtMs, String pushEndpoint, String pushP256dhKey, - String pushAuthKey) { + String pushAuthKey, + String clientIp, + String clientInfoFromClient, + String clientInfoFromRequest, + String userLanguage) { this.sessionId = sessionId; this.loginId = loginId; this.sessionPwd = sessionPwd; @@ -51,6 +63,10 @@ public class ActiveSession { this.pushEndpoint = pushEndpoint; this.pushP256dhKey = pushP256dhKey; this.pushAuthKey = pushAuthKey; + this.clientIp = clientIp; + this.clientInfoFromClient = clientInfoFromClient; + this.clientInfoFromRequest = clientInfoFromRequest; + this.userLanguage = userLanguage; } // --- getters / setters --- @@ -126,4 +142,36 @@ public class ActiveSession { public void setPushAuthKey(String pushAuthKey) { this.pushAuthKey = pushAuthKey; } -} + + public String getClientIp() { + return clientIp; + } + + public void setClientIp(String clientIp) { + this.clientIp = clientIp; + } + + public String getClientInfoFromClient() { + return clientInfoFromClient; + } + + public void setClientInfoFromClient(String clientInfoFromClient) { + this.clientInfoFromClient = clientInfoFromClient; + } + + public String getClientInfoFromRequest() { + return clientInfoFromRequest; + } + + public void setClientInfoFromRequest(String clientInfoFromRequest) { + this.clientInfoFromRequest = clientInfoFromRequest; + } + + public String getUserLanguage() { + return userLanguage; + } + + public void setUserLanguage(String userLanguage) { + this.userLanguage = userLanguage; + } +} \ No newline at end of file diff --git a/shine-server-geo/build.gradle b/shine-server-geo/build.gradle index 6b00bb1..9cdf092 100644 --- a/shine-server-geo/build.gradle +++ b/shine-server-geo/build.gradle @@ -22,6 +22,9 @@ dependencies { implementation 'org.eclipse.jetty:jetty-servlet:11.0.20' implementation 'org.eclipse.jetty.websocket:websocket-jetty-server:11.0.20' + implementation project(':shine-server-db') // модуль для работы с БД содержит и сущности из БД и саму работу с БД + + } java { diff --git a/shine-server-geo/src/main/java/shine.geo/GeoLookupService.java b/shine-server-geo/src/main/java/shine.geo/GeoLookupService.java index a136415..72a6aa3 100644 --- a/shine-server-geo/src/main/java/shine.geo/GeoLookupService.java +++ b/shine-server-geo/src/main/java/shine.geo/GeoLookupService.java @@ -2,18 +2,25 @@ package shine.geo; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import shine.db.dao.IpGeoCacheDAO; +import shine.db.entities.IpGeoCacheEntry; import java.io.IOException; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; +import java.sql.SQLException; /** * Сервис для геолокации по IP. - * . - * Основной метод: - * resolveCountryCityOrIp(ip) -> "Country, City" или GEO_UNKNOWN, если не удалось. + * + * Основной метод без кэша: + * resolveCountryCityOrIp(ip) -> "Country, City" или GEO_UNKNOWN + * + * Метод с кэшированием в БД: + * resolveCountryCityOrIpWithCache(ip) -> сначала смотрит в ip_geo_cache, + * при отсутствии записи — обращается к внешнему сервису, сохраняет результат в кэш и возвращает его. */ public final class GeoLookupService { @@ -34,6 +41,8 @@ public final class GeoLookupService { } /** + * ВАРИАНТ БЕЗ КЭША. + * * Возвращает строку вида "Country, City" по IP. * Если запрос не удался, возвращает GEO_UNKNOWN. */ @@ -79,7 +88,6 @@ public final class GeoLookupService { return GEO_UNKNOWN; } - // Собираем строку if (country != null && city != null) { return country + ", " + city; } else if (country != null) { @@ -94,6 +102,64 @@ public final class GeoLookupService { } } + /** + * ВАРИАНТ С КЭШЕМ В БАЗЕ (ip_geo_cache). + * + * Логика: + * 1) Если IP пустой или локальный — сразу GEO_UNKNOWN (и ничего не пишем в кэш). + * 2) Пытаемся найти ip в ip_geo_cache: + * - если нашли — возвращаем geo из записи. + * 3) Если не нашли — вызываем resolveCountryCityOrIp(ip) (внешний сервис), + * - результат (включая GEO_UNKNOWN) сохраняем в ip_geo_cache через IpGeoCacheDAO.upsert() + * - возвращаем сохранённый результат. + * + * В случае ошибок БД — просто падаем назад на поведение без кэша. + */ + public static String resolveCountryCityOrIpWithCache(String ip) { + if (ip == null || ip.isBlank()) { + return GEO_UNKNOWN; + } + + // Приватные/локальные IP не кешируем и не запрашиваем + if (isPrivateOrLocalIp(ip)) { + return GEO_UNKNOWN; + } + + // 1. Сначала пробуем взять из кэша + IpGeoCacheDAO dao = IpGeoCacheDAO.getInstance(); + try { + IpGeoCacheEntry cached = dao.getByIp(ip); + if (cached != null) { + String geo = cached.getGeo(); + if (geo != null && !geo.isBlank()) { + return geo; + } + // Если geo пустая строка (на всякий случай) — идём за свежими данными. + } + } catch (SQLException e) { + // Ошибка БД — логируем при желании и продолжаем без кэша + // log.warn("Failed to read IP geo cache", e); + } + + // 2. Вызываем "сырой" метод, который ходит во внешний сервис + String resolvedGeo = resolveCountryCityOrIp(ip); + + // 3. Пишем результат в кэш (включая GEO_UNKNOWN) + try { + IpGeoCacheEntry entry = new IpGeoCacheEntry( + ip, + resolvedGeo, + System.currentTimeMillis() + ); + dao.upsert(entry); + } catch (SQLException e) { + // Ошибка БД при записи — просто игнорируем, кэш не обязателен для работы + // log.warn("Failed to upsert IP geo cache", e); + } + + return resolvedGeo; + } + /** * Пытается получить внешний IP текущей машины через HTTP-сервис. * В случае ошибки возвращает fallbackIp. diff --git a/shine-server-net-protocol/build.gradle b/shine-server-net-protocol/build.gradle index e867644..f74dbdd 100644 --- a/shine-server-net-protocol/build.gradle +++ b/shine-server-net-protocol/build.gradle @@ -29,6 +29,8 @@ dependencies { implementation project(':shine-server-crypto') // модуль сервера для работы с криптографией implementation project(':shine-server-blockchain') // модуль для работы с блокчейном implementation project(':shine-server-db') // модуль для работы с БД содержит и сущности из БД и саму работу с БД + implementation project(':shine-server-geo') // модуль для определения геолокации по IP + } diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/entyties/Auth/Net_AuthChallenge_Response.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/entyties/Auth/Net_AuthChallenge_Response.java index 5f447b2..b9047f3 100644 --- a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/entyties/Auth/Net_AuthChallenge_Response.java +++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/entyties/Auth/Net_AuthChallenge_Response.java @@ -5,8 +5,9 @@ import server.logic.ws_protocol.JSON.entyties.NetResponse; /** * Ответ на AuthChallenge. * - * При успехе сервер возвращает временный секрет sessionPwd, - * который клиент обязан использовать на втором шаге при формировании подписи. + * При успехе сервер возвращает одноразовый nonce для подписи (authNonce), + * который клиент обязан использовать на втором шаге при формировании строки + * для цифровой подписи. * * JSON: * { @@ -14,22 +15,23 @@ import server.logic.ws_protocol.JSON.entyties.NetResponse; * "requestId": "...", * "status": 200, * "payload": { - * "sessionPwd": "base64-строка-от-32-байт" + * "authNonce": "base64-строка-от-32-байт" * } * } */ public class Net_AuthChallenge_Response extends NetResponse { /** - * Временный секрет, сгенерированный сервером. + * Одноразовый nonce для авторификации. * Строка — это base64-представление 32 случайных байт. */ - private String sessionPwd; + private String authNonce; - public String getSessionPwd() { - return sessionPwd; + public String getAuthNonce() { + return authNonce; } - public void setSessionPwd(String sessionPwd) { - this.sessionPwd = sessionPwd; + + public void setAuthNonce(String authNonce) { + this.authNonce = authNonce; } -} +} \ No newline at end of file diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/entyties/Auth/Net_CreateAuthSession_Request.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/entyties/Auth/Net_CreateAuthSession_Request.java index 0f1dc87..fe1ea4c 100644 --- a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/entyties/Auth/Net_CreateAuthSession_Request.java +++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/entyties/Auth/Net_CreateAuthSession_Request.java @@ -6,13 +6,16 @@ import server.logic.ws_protocol.JSON.entyties.NetRequest; * Шаг 2 авторизации: подтверждение владения ключом и установка сессии. * * Клиент: - * 1) получает от сервера sessionPwd на шаге 1; + * 1) получает от сервера authNonce на шаге 1; * 2) генерирует свой StoragePwd (base64 от 32 байт); * 3) формирует строку для подписи: - * "AUTHORIFICATED:" + timeMs + sessionPwd + * "AUTHORIFICATED:" + timeMs + authNonce * 4) подписывает эту строку своим приватным ключом (pubkey1), * отправляет подпись и StoragePwd на сервер. * + * Дополнительно: + * - clientInfo — короткая строка (до 50 символов) с данными об устройстве/клиенте. + * * Формат входящего JSON: * { * "op": "CreateAuthSession", @@ -20,12 +23,10 @@ import server.logic.ws_protocol.JSON.entyties.NetRequest; * "payload": { * "storagePwd": "base64-строка-от-32-байт", * "timeMs": 1733310000000, - * "signatureB64": "base64-подпись-Ed25519" + * "signatureB64": "base64-подпись-Ed25519", + * "clientInfo": "Chrome/Android" // опционально, до 50 символов * } * } - * - * При успешной проверке подписи сервер создаёт запись в active_sessions - * и возвращает sessionId (base64-строка от 32 байт). */ public class Net_CreateAuthSession_Request extends NetRequest { @@ -35,9 +36,12 @@ public class Net_CreateAuthSession_Request extends NetRequest { /** Время на стороне клиента (мс с 1970-01-01). */ private long timeMs; - /** Подпись Ed25519 над строкой "AUTHORIFICATED:" + timeMs + sessionPwd (base64). */ + /** Подпись Ed25519 над строкой "AUTHORIFICATED:" + timeMs + authNonce (base64). */ private String signatureB64; + /** Краткая строка от клиента (до 50 символов) с описанием устройства/клиента. */ + private String clientInfo; + public String getStoragePwd() { return storagePwd; } @@ -61,4 +65,12 @@ public class Net_CreateAuthSession_Request extends NetRequest { public void setSignatureB64(String signatureB64) { this.signatureB64 = signatureB64; } -} + + public String getClientInfo() { + return clientInfo; + } + + public void setClientInfo(String clientInfo) { + this.clientInfo = clientInfo; + } +} \ No newline at end of file diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/entyties/Auth/Net_CreateAuthSession_Response.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/entyties/Auth/Net_CreateAuthSession_Response.java index a8920fc..fcf89f7 100644 --- a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/entyties/Auth/Net_CreateAuthSession_Response.java +++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/entyties/Auth/Net_CreateAuthSession_Response.java @@ -6,7 +6,7 @@ import server.logic.ws_protocol.JSON.entyties.NetResponse; * Ответ на CreateAuthSession. * * При успехе сервер создаёт запись в active_sessions - * и возвращает идентификатор сессии sessionId. + * и возвращает идентификатор сессии sessionId и секрет сессии sessionPwd. * * JSON: * { @@ -14,7 +14,8 @@ import server.logic.ws_protocol.JSON.entyties.NetResponse; * "requestId": "...", * "status": 200, * "payload": { - * "sessionId": "base64-строка-от-32-байт" + * "sessionId": "base64-строка-от-32-байт", + * "sessionPwd": "base64-строка-от-32-байт" * } * } */ @@ -23,6 +24,9 @@ public class Net_CreateAuthSession_Response extends NetResponse { /** Идентификатор сессии, base64 от 32 байт. */ private String sessionId; + /** Секрет сессии, base64 от 32 байт. */ + private String sessionPwd; + public String getSessionId() { return sessionId; } @@ -30,4 +34,12 @@ public class Net_CreateAuthSession_Response extends NetResponse { public void setSessionId(String sessionId) { this.sessionId = sessionId; } -} + + public String getSessionPwd() { + return sessionPwd; + } + + public void setSessionPwd(String sessionPwd) { + this.sessionPwd = sessionPwd; + } +} \ No newline at end of file diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/entyties/Auth/Net_RefreshSession_Request.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/entyties/Auth/Net_RefreshSession_Request.java index bbd94ab..ca79dcb 100644 --- a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/entyties/Auth/Net_RefreshSession_Request.java +++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/entyties/Auth/Net_RefreshSession_Request.java @@ -11,7 +11,8 @@ import server.logic.ws_protocol.JSON.entyties.NetRequest; * JSON (payload): * { * "sessionId": "base64-id-сессии", - * "sessionPwd": "base64-sessionPwd" + * "sessionPwd": "base64-sessionPwd", + * "clientInfo": "до 50 символов, краткая строка об устройстве" * } */ public class Net_RefreshSession_Request extends NetRequest { @@ -19,6 +20,12 @@ public class Net_RefreshSession_Request extends NetRequest { private String sessionId; private String sessionPwd; + /** + * Краткая строка с информацией об устройстве/клиенте, до 50 символов. + * Например: "PWA/Chrome/Android". + */ + private String clientInfo; + public String getSessionId() { return sessionId; } @@ -34,4 +41,12 @@ public class Net_RefreshSession_Request extends NetRequest { public void setSessionPwd(String sessionPwd) { this.sessionPwd = sessionPwd; } -} + + public String getClientInfo() { + return clientInfo; + } + + public void setClientInfo(String clientInfo) { + this.clientInfo = clientInfo; + } +} \ No newline at end of file diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/Net_AuthChallenge_Handler.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/Net_AuthChallenge_Handler.java index f9ca4dd..e2c4109 100644 --- a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/Net_AuthChallenge_Handler.java +++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/Net_AuthChallenge_Handler.java @@ -46,7 +46,6 @@ public class Net_AuthChallenge_Handler implements JsonMessageHandler { SolanaUser solanaUser = SolanaUsersDAO.getInstance().getByLogin(login); if (solanaUser == null) { - // TODO позже — запрос в Solana, если не нашли локально return NetExceptionResponseFactory.error( req, WireCodes.Status.UNVERIFIED, @@ -55,23 +54,24 @@ public class Net_AuthChallenge_Handler implements JsonMessageHandler { ); } - // 3) Заполняем контекст целиком пользователем + // 3) Заполняем контекст пользователем ctx.setSolanaUser(solanaUser); - // 4) Генерируем надёжный sessionPwd = base64(32 случайных байт) + // 4) Генерируем одноразовый authNonce = base64(32 случайных байт) byte[] buf = new byte[32]; RANDOM.nextBytes(buf); - String sessionPwd = Base64.getUrlEncoder().withoutPadding().encodeToString(buf); + String authNonce = Base64.getUrlEncoder().withoutPadding().encodeToString(buf); - ctx.setSessionPwd(sessionPwd); + // Используем поле sessionPwd в контексте как хранилище challenge (authNonce) до шага 2 + ctx.setSessionPwd(authNonce); // 5) Формируем ответ Net_AuthChallenge_Response resp = new Net_AuthChallenge_Response(); resp.setOp(req.getOp()); resp.setRequestId(req.getRequestId()); resp.setStatus(WireCodes.Status.OK); - resp.setSessionPwd(sessionPwd); + resp.setAuthNonce(authNonce); return resp; } -} +} \ No newline at end of file diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/Net_CreateAuthSession__Handler.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/Net_CreateAuthSession__Handler.java index 397b19a..a7d90b0 100644 --- a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/Net_CreateAuthSession__Handler.java +++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/Net_CreateAuthSession__Handler.java @@ -14,8 +14,13 @@ import server.logic.ws_protocol.WireCodes; import shine.db.dao.ActiveSessionsDAO; import shine.db.entities.ActiveSession; import shine.db.entities.SolanaUser; +import shine.geo.ClientInfoService; import utils.crypto.Ed25519Util; +import org.eclipse.jetty.websocket.api.Session; + +import java.net.InetSocketAddress; +import java.net.SocketAddress; import java.nio.charset.StandardCharsets; import java.sql.SQLException; import java.security.SecureRandom; @@ -28,20 +33,18 @@ import java.util.Base64; * - storagePwd (base64 от 32 байт) * - timeMs (long, мс с 1970-01-01) * - signatureB64 (подпись Ed25519 над строкой: - * "AUTHORIFICATED:" + timeMs + sessionPwd) + * "AUTHORIFICATED:" + timeMs + authNonce) + * - clientInfo (опционально, до 50 символов) * - * Параметр sessionPwd клиент получил на шаге 1. - * Для проверки подписи используется pubkey1 (второй публичный ключ пользователя). - * - * Дополнительно: - * - timeMs должен отличаться от текущего времени сервера не более чем на 30 секунд. + * authNonce клиент получил на шаге 1 (AuthChallenge). * * При успехе: * - создаётся запись ActiveSession в БД; * - генерируется sessionId (base64 от 32 случайных байт); + * - генерируется sessionPwd (base64 от 32 случайных байт); * - sessionCreatedAtMs и lastAuthirificatedAtMs = текущее время; - * - pushEndpoint / pushP256dhKey / pushAuthKey остаются пустыми; - * - возвращается sessionId в ответе. + * - заполняются поля clientIp, clientInfoFromClient, clientInfoFromRequest, userLanguage; + * - возвращается sessionId и sessionPwd в ответе. */ public class Net_CreateAuthSession__Handler implements JsonMessageHandler { @@ -107,7 +110,6 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler { long timeMs = req.getTimeMs(); long nowMs = System.currentTimeMillis(); - // Проверка, что время клиента не отличается от времени сервера больше чем на 30 секунд long diff = Math.abs(nowMs - timeMs); if (diff > ALLOWED_SKEW_MS) { return NetExceptionResponseFactory.error( @@ -118,6 +120,12 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler { ); } + // Короткая строка clientInfo от клиента (до 50 символов) + String clientInfoFromClient = req.getClientInfo(); + if (clientInfoFromClient != null && clientInfoFromClient.length() > 50) { + clientInfoFromClient = clientInfoFromClient.substring(0, 50); + } + // --- выбираем публичный ключ pubkey1 --- String pubKeyB64 = user.getDeviceKey(); if (pubKeyB64 == null || pubKeyB64.isBlank()) { @@ -143,8 +151,11 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler { ); } - // --- собираем строку для подписи: "AUTHORIFICATED:" + timeMs + sessionPwd --- - String preimageStr = "AUTHORIFICATED:" + timeMs + ctx.getSessionPwd(); + // --- authNonce (challenge) мы сохранили в ctx.sessionPwd на шаге 1 --- + String authNonce = ctx.getSessionPwd(); + + // --- собираем строку для подписи: "AUTHORIFICATED:" + timeMs + authNonce --- + String preimageStr = "AUTHORIFICATED:" + timeMs + authNonce; byte[] preimage = preimageStr.getBytes(StandardCharsets.UTF_8); boolean sigOk = Ed25519Util.verify(preimage, signature64, publicKey32); @@ -157,25 +168,47 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler { ); } - // --- создаём уникальный sessionId (base64 от 32 байт) и записываем в БД --- + // --- Генерируем настоящий секрет сессии (sessionPwd) и sessionId --- + String newSessionPwd = generateRandomSecret(); + String sessionId = generateRandomSessionId(); + long now = System.currentTimeMillis(); + + // --- Сбор данных о клиенте (IP, UA, язык) --- + Session wsSession = ctx.getWsSession(); + String clientInfoFromRequest = ClientInfoService.buildClientInfoString(wsSession); + String userLanguage = ClientInfoService.extractPreferredLanguageTag(wsSession); + + String clientIp = ""; + if (wsSession != null) { + SocketAddress rawAddr = wsSession.getRemoteAddress(); + if (rawAddr instanceof InetSocketAddress inet) { + if (inet.getAddress() != null) { + clientIp = inet.getAddress().getHostAddress(); + } + } + } +// TODO и сдесь тоже переписываем получение ИП адреса на стандартный метод и тоже дёргаем запрос геолокации который никуда не сохраняем просто что бы он в кэш сервера попал + + + // --- создаём запись ActiveSession и сохраняем в БД --- ActiveSessionsDAO dao = ActiveSessionsDAO.getInstance(); - String sessionId; ActiveSession activeSession; try { - sessionId = generateRandomSessionId(); - long now = System.currentTimeMillis(); - activeSession = new ActiveSession( sessionId, loginId, - ctx.getSessionPwd(), + newSessionPwd, // настоящий секрет сессии storagePwd, now, now, - null, // pushEndpoint - null, // pushP256dhKey - null // pushAuthKey + null, // pushEndpoint + null, // pushP256dhKey + null, // pushAuthKey + clientIp, + clientInfoFromClient, + clientInfoFromRequest, + userLanguage ); dao.insert(activeSession); @@ -192,9 +225,9 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler { // --- обновляем контекст --- ctx.setActiveSession(activeSession); ctx.setSessionId(sessionId); + ctx.setSessionPwd(newSessionPwd); // теперь в контексте хранится секрет сессии, а не authNonce ctx.setAuthenticationStatus(ConnectionContext.AUTH_STATUS_USER); - // Регистрируем это подключение в глобальном реестре активных соединений ActiveConnectionsRegistry.getInstance().register(ctx); // --- формируем ответ --- @@ -202,7 +235,8 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler { resp.setOp(req.getOp()); resp.setRequestId(req.getRequestId()); resp.setStatus(WireCodes.Status.OK); - resp.setSessionId(sessionId); // попадёт в payload.sessionId + resp.setSessionId(sessionId); + resp.setSessionPwd(newSessionPwd); return resp; } @@ -214,4 +248,13 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler { RANDOM.nextBytes(buf); return Base64.getUrlEncoder().withoutPadding().encodeToString(buf); } -} + + /** + * Генерация случайного секрета (sessionPwd): base64-строка от 32 байт. + */ + private String generateRandomSecret() { + byte[] buf = new byte[32]; + RANDOM.nextBytes(buf); + return Base64.getUrlEncoder().withoutPadding().encodeToString(buf); + } +} \ No newline at end of file diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/Net_RefreshSession_Handler.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/Net_RefreshSession_Handler.java index 32bc279..ad5cd49 100644 --- a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/Net_RefreshSession_Handler.java +++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/Net_RefreshSession_Handler.java @@ -15,6 +15,7 @@ import shine.db.dao.ActiveSessionsDAO; import shine.db.dao.SolanaUsersDAO; import shine.db.entities.ActiveSession; import shine.db.entities.SolanaUser; +import shine.geo.ClientInfoService; import java.sql.SQLException; @@ -24,19 +25,23 @@ import java.sql.SQLException; * При успешной проверке sessionId + sessionPwd: * - подтягивает пользователя по loginId из сессии; * - заполняет ConnectionContext; - * - обновляет lastAuthirificatedAtMs в БД на текущее время; + * - обновляет lastAuthirificatedAtMs и метаданные сессии в БД; * - возвращает storagePwd в payload. */ public class Net_RefreshSession_Handler implements JsonMessageHandler { private static final Logger log = LoggerFactory.getLogger(Net_RefreshSession_Handler.class); + // максимум 50 символов для clientInfo от клиента + private static final int CLIENT_INFO_MAX_LEN = 50; + @Override public NetResponse handle(NetRequest request, ConnectionContext ctx) throws Exception { Net_RefreshSession_Request req = (Net_RefreshSession_Request) request; String sessionId = req.getSessionId(); String sessionPwd = req.getSessionPwd(); + String clientInfoFromClient = trimClientInfo(req.getClientInfo()); if (sessionId == null || sessionId.isBlank()) { return NetExceptionResponseFactory.error( @@ -89,7 +94,7 @@ public class Net_RefreshSession_Handler implements JsonMessageHandler { ); } - // --- достаём пользователя по loginId из сессии --- + // --- вытаскиваем пользователя по loginId --- SolanaUser solanaUser = null; long loginId = session.getLoginId(); try { @@ -114,7 +119,43 @@ public class Net_RefreshSession_Handler implements JsonMessageHandler { ); } - // Всё хорошо — обновляем контекст соединения + // --- собираем данные о клиенте из WebSocket-запроса --- + String clientIp = null; + String clientInfoFromRequest = null; + String userLanguage = null; + + if (ctx != null && ctx.getWsSession() != null) { + clientIp = "8.8.8.8"; //TODO сделать нормальное получение ип адреса +// и сделать запрос геолокации и никуда его не сохранять запрос нужен просто что бы в кэш данные добавилиь если нужно + clientInfoFromRequest = ClientInfoService.buildClientInfoString(ctx.getWsSession()); + userLanguage = ClientInfoService.extractPreferredLanguageTag(ctx.getWsSession()); + } + + long nowMs = System.currentTimeMillis(); + + // --- обновляем запись в БД (lastAuth + мета) --- + try { + sessionsDao.updateOnRefresh( + sessionId, + nowMs, + clientIp, + clientInfoFromClient, + clientInfoFromRequest, + userLanguage + ); + } catch (SQLException e) { + log.error("Ошибка БД при обновлении метаданных сессии sessionId={}", sessionId, e); + // не роняем авторизацию, но логируем + } + + // Также обновим объект session в памяти (если дальше кто-то его использует) + session.setLastAuthirificatedAtMs(nowMs); + session.setClientIp(clientIp); + session.setClientInfoFromClient(clientInfoFromClient); + session.setClientInfoFromRequest(clientInfoFromRequest); + session.setUserLanguage(userLanguage); + + // --- обновляем контекст соединения --- if (ctx != null) { ctx.setActiveSession(session); ctx.setSolanaUser(solanaUser); @@ -126,15 +167,7 @@ public class Net_RefreshSession_Handler implements JsonMessageHandler { ActiveConnectionsRegistry.getInstance().register(ctx); } - // Обновляем lastAuthirificatedAtMs в БД - try { - long nowMs = System.currentTimeMillis(); - sessionsDao.updateLastAuthirificatedAtMs(sessionId, nowMs); - } catch (SQLException e) { - log.error("Ошибка БД при обновлении lastAuthirificatedAtMs для sessionId={}", sessionId, e); - } - - // Возвращаем OK + storagePwd + // --- ответ OK + storagePwd --- Net_RefreshSession_Response resp = new Net_RefreshSession_Response(); resp.setOp(req.getOp()); resp.setRequestId(req.getRequestId()); @@ -142,4 +175,13 @@ public class Net_RefreshSession_Handler implements JsonMessageHandler { resp.setStoragePwd(session.getStoragePwd()); return resp; } -} + + private String trimClientInfo(String info) { + if (info == null) return null; + info = info.trim(); + if (info.length() > CLIENT_INFO_MAX_LEN) { + return info.substring(0, CLIENT_INFO_MAX_LEN); + } + return info; + } +} \ No newline at end of file diff --git a/src/main/java/Test/Test_AddUser_and_Authorification.java b/src/main/java/Test/Test_AddUser_and_Authorification.java index 0a72fa4..e1e1575 100644 --- a/src/main/java/Test/Test_AddUser_and_Authorification.java +++ b/src/main/java/Test/Test_AddUser_and_Authorification.java @@ -20,10 +20,13 @@ import java.util.concurrent.CountDownLatch; * 1) AddUser — добавляем пользователя в локальную БД * (loginKey и deviceKey разные). * - * 2) AuthChallenge — запрашиваем sessionPwd. + * 2) AuthChallenge — запрашиваем одноразовый authNonce + * для подписи шаге 2. * * 3) CreateAuthSession — подтверждаем владение deviceKey, - * создаётся сессия, сервер возвращает sessionId (строка). + * создаётся сессия, сервер возвращает: + * - sessionId (строка, base64-32 байта) + * - sessionPwd (секрет сессии, base64-32 байта) * * 4) Новое подключение: * - отправляем RefreshSession с тем же sessionId, @@ -48,6 +51,9 @@ public class Test_AddUser_and_Authorification { private static final long TEST_BCH_ID = 4222L; private static final int TEST_BCH_LIMIT = 1_000_000; + // Краткая строка clientInfo, которую клиент шлёт на шаге CreateAuthSession + private static final String TEST_CLIENT_INFO = "JavaTestClient/1.0"; + // --- Тестовые пары ключей --- // loginKey — ключ аккаунта (например, "основной") // deviceKey — ключ устройства, которым подписываем авторизацию @@ -72,12 +78,15 @@ public class Test_AddUser_and_Authorification { // --- Глобальные переменные между сценариями --- - /** sessionPwd, выданный на шаге AuthChallenge. */ - private static String GLOBAL_SESSION_PWD; + /** authNonce, выданный на шаге AuthChallenge. */ + private static String GLOBAL_AUTH_NONCE; /** sessionId (строка, base64-32 байта), выданный на шаге CreateAuthSession. */ private static String GLOBAL_SESSION_ID; + /** sessionPwd (секрет сессии), выданный на шаге CreateAuthSession. */ + private static String GLOBAL_SESSION_PWD; + /** storagePwd, который мы отправили при CreateAuthSession (для информации). */ private static String GLOBAL_STORAGE_PWD_SENT; @@ -107,7 +116,7 @@ public class Test_AddUser_and_Authorification { CountDownLatch latch = new CountDownLatch(1); HttpClient client = HttpClient.newHttpClient(); - WebSocket ws = client.newWebSocketBuilder() + client.newWebSocketBuilder() .buildAsync(URI.create(WS_URI), new Listener() { private int step = 0; // 0 - AddUser, 1 - AuthStep1, 2 - AuthStep2 @@ -138,7 +147,7 @@ public class Test_AddUser_and_Authorification { } case 2 -> { GLOBAL_STORAGE_PWD_SENT = generateFakeStoragePwd(); - String json = buildAuthStep2Json(GLOBAL_SESSION_PWD, GLOBAL_STORAGE_PWD_SENT); + String json = buildAuthStep2Json(GLOBAL_AUTH_NONCE, GLOBAL_STORAGE_PWD_SENT); System.out.println(); System.out.println("📤 [S1 / Шаг 3] Отправляем CreateAuthSession (подпись deviceKey):"); System.out.println(json); @@ -160,17 +169,19 @@ public class Test_AddUser_and_Authorification { System.out.println(message); System.out.println("-----------------------------------------------------"); - // Шаг 2: получаем sessionPwd + // Шаг 2: получаем authNonce if (step == 1) { - GLOBAL_SESSION_PWD = extractSessionPwd(message); - System.out.println("🔑 [S1] Извлечён sessionPwd: " + GLOBAL_SESSION_PWD); + GLOBAL_AUTH_NONCE = extractAuthNonce(message); + System.out.println("🔑 [S1] Извлечён authNonce: " + GLOBAL_AUTH_NONCE); } - // Шаг 3: получаем sessionId + // Шаг 3: получаем sessionId и sessionPwd if (step == 2) { GLOBAL_SESSION_ID = extractSessionId(message); + GLOBAL_SESSION_PWD = extractSessionPwd(message); System.out.println("🆔 [S1] Извлечён sessionId: " + GLOBAL_SESSION_ID); - System.out.println(" (Эта sessionId и sessionPwd понадобятся в сценариях 2 и 3)"); + System.out.println("🔐 [S1] Извлечён sessionPwd: " + GLOBAL_SESSION_PWD); + System.out.println(" (Эти sessionId и sessionPwd понадобятся в сценариях 2 и 3)"); } step++; @@ -221,7 +232,7 @@ public class Test_AddUser_and_Authorification { // Специально подменяем пароль, чтобы сервер его НЕ принял String wrongPwd = GLOBAL_SESSION_PWD + "_WRONG"; - WebSocket ws = client.newWebSocketBuilder() + client.newWebSocketBuilder() .buildAsync(URI.create(WS_URI), new Listener() { @Override @@ -281,7 +292,7 @@ public class Test_AddUser_and_Authorification { System.out.println(); System.out.println("=== СЦЕНАРИЙ 3: RefreshSession с КОРРЕКТНЫМ sessionPwd ==="); System.out.println("Ожидаем УСПЕШНЫЙ ответ сервера (status=200),"); - System.out.println(" а в payload должен вернуться актуальный storagePwd (по твоей схеме)."); + System.out.println(" а в payload должен вернуться актуальный storagePwd."); if (GLOBAL_SESSION_ID == null || GLOBAL_SESSION_PWD == null) { System.out.println("⚠️ Нет sessionId или sessionPwd из сценария 1, пропускаем сценарий 3."); @@ -291,7 +302,7 @@ public class Test_AddUser_and_Authorification { CountDownLatch latch = new CountDownLatch(1); HttpClient client = HttpClient.newHttpClient(); - WebSocket ws = client.newWebSocketBuilder() + client.newWebSocketBuilder() .buildAsync(URI.create(WS_URI), new Listener() { @Override @@ -318,7 +329,7 @@ public class Test_AddUser_and_Authorification { System.out.println("💬 [S3] Если status=200 — сессия успешно восстановлена."); String storagePwdFromServer = extractStoragePwd(message); System.out.println("🧾 [S3] storagePwd от сервера: " + storagePwdFromServer); - System.out.println(" (Может совпадать с тем, что был в шаге 2, или быть обновлённым — зависит от логики сервера)"); + System.out.println(" (Должен совпадать с тем, что отправляли в шаге 3 сценария 1)"); webSocket.sendClose(WebSocket.NORMAL_CLOSURE, "scenario3 done"); webSocket.request(1); @@ -375,7 +386,7 @@ public class Test_AddUser_and_Authorification { ); } - // 2) Шаг 1 авторизации: запрос sessionPwd + // 2) Шаг 1 авторизации: запрос authNonce private static String buildAuthStep1Json() { return """ { @@ -388,11 +399,15 @@ public class Test_AddUser_and_Authorification { """.formatted(TEST_LOGIN); } - // 3) Шаг 2 авторизации: подтверждение подписью - // payload: storagePwd, timeMs, signatureB64 - private static String buildAuthStep2Json(String sessionPwd, String storagePwd) { - if (sessionPwd == null) { - sessionPwd = ""; + /** + * 3) Шаг 2 авторизации: подтверждение подписью. + * + * @param authNonce одноразовый nonce с шага 1 + * @param storagePwd клиентский storagePwd + */ + private static String buildAuthStep2Json(String authNonce, String storagePwd) { + if (authNonce == null) { + authNonce = ""; } if (storagePwd == null || storagePwd.isBlank()) { storagePwd = generateFakeStoragePwd(); @@ -400,8 +415,8 @@ public class Test_AddUser_and_Authorification { long timeMs = System.currentTimeMillis(); - // preimage = "AUTHORIFICATED:" + timeMs + sessionPwd - String preimageStr = "AUTHORIFICATED:" + timeMs + sessionPwd; + // preimage = "AUTHORIFICATED:" + timeMs + authNonce + String preimageStr = "AUTHORIFICATED:" + timeMs + authNonce; byte[] preimage = preimageStr.getBytes(StandardCharsets.UTF_8); // Подписываем приватным ключом устройства (deviceKey) @@ -415,31 +430,35 @@ public class Test_AddUser_and_Authorification { "payload": { "storagePwd": "%s", "timeMs": %d, - "signatureB64": "%s" + "signatureB64": "%s", + "clientInfo": "%s" } } """.formatted( storagePwd, timeMs, - sigB64 + sigB64, + TEST_CLIENT_INFO ); } // 4) RefreshSession: всё в payload private static String buildRefreshSessionJson(String sessionId, String sessionPwd, String requestId) { return """ - { - "op": "RefreshSession", - "requestId": "%s", - "payload": { - "sessionId": "%s", - "sessionPwd": "%s" - } - } - """.formatted( + { + "op": "RefreshSession", + "requestId": "%s", + "payload": { + "sessionId": "%s", + "sessionPwd": "%s", + "clientInfo": "%s" + } + } + """.formatted( requestId, sessionId, - sessionPwd + sessionPwd, + TEST_CLIENT_INFO ); } @@ -456,6 +475,19 @@ public class Test_AddUser_and_Authorification { // JSON HELPERS // ========================================================== + private static String extractAuthNonce(String json) { + try { + JsonNode root = JSON_MAPPER.readTree(json); + JsonNode payload = root.get("payload"); + if (payload != null && payload.has("authNonce")) { + return payload.get("authNonce").asText(); + } + } catch (Exception e) { + System.out.println("⚠️ Не удалось распарсить authNonce из ответа: " + e.getMessage()); + } + return null; + } + private static String extractSessionPwd(String json) { try { JsonNode root = JSON_MAPPER.readTree(json); @@ -494,4 +526,4 @@ public class Test_AddUser_and_Authorification { } return null; } -} +} \ No newline at end of file