SHA256
529 lines
24 KiB
Java
529 lines
24 KiB
Java
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) AuthChallenge — запрашиваем одноразовый authNonce
|
||
* для подписи шаге 2.
|
||
*
|
||
* 3) CreateAuthSession — подтверждаем владение deviceKey,
|
||
* создаётся сессия, сервер возвращает:
|
||
* - sessionId (строка, base64-32 байта)
|
||
* - sessionPwd (секрет сессии, base64-32 байта)
|
||
*
|
||
* 4) Новое подключение:
|
||
* - отправляем RefreshSession с тем же sessionId,
|
||
* но заведомо неверным sessionPwd
|
||
* (в консоль пишем: ожидаем ОТРИЦАТЕЛЬНЫЙ ответ).
|
||
*
|
||
* 5) Ещё одно новое подключение:
|
||
* - отправляем RefreshSession с 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 = "anya24";
|
||
private static final long TEST_LOGIN_ID = 1030120L;
|
||
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 — ключ устройства, которым подписываем авторизацию
|
||
|
||
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);
|
||
}
|
||
|
||
// --- Глобальные переменные между сценариями ---
|
||
|
||
/** 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;
|
||
|
||
public static void main(String[] args) throws Exception {
|
||
System.out.println("Подключаемся к " + WS_URI);
|
||
|
||
// Сценарий 1: регистрация + первичная авторизация
|
||
runScenario_AddUser_And_FirstAuth();
|
||
|
||
// Сценарий 2: новое подключение, RefreshSession с неверным sessionPwd
|
||
runScenario_RefreshSession_WrongPwd();
|
||
|
||
// Сценарий 3: новое подключение, RefreshSession с корректным sessionPwd
|
||
runScenario_RefreshSession_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 + AuthChallenge + CreateAuthSession ===");
|
||
|
||
CountDownLatch latch = new CountDownLatch(1);
|
||
HttpClient client = HttpClient.newHttpClient();
|
||
|
||
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] Отправляем AuthChallenge:");
|
||
System.out.println(json);
|
||
webSocket.sendText(json, true);
|
||
}
|
||
case 2 -> {
|
||
GLOBAL_STORAGE_PWD_SENT = generateFakeStoragePwd();
|
||
String json = buildAuthStep2Json(GLOBAL_AUTH_NONCE, GLOBAL_STORAGE_PWD_SENT);
|
||
System.out.println();
|
||
System.out.println("📤 [S1 / Шаг 3] Отправляем CreateAuthSession (подпись 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: получаем authNonce
|
||
if (step == 1) {
|
||
GLOBAL_AUTH_NONCE = extractAuthNonce(message);
|
||
System.out.println("🔑 [S1] Извлечён authNonce: " + GLOBAL_AUTH_NONCE);
|
||
}
|
||
|
||
// Шаг 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("🔐 [S1] Извлечён sessionPwd: " + GLOBAL_SESSION_PWD);
|
||
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: RefreshSession с неправильным паролем
|
||
// ==========================================================
|
||
|
||
private static void runScenario_RefreshSession_WrongPwd() throws Exception {
|
||
System.out.println();
|
||
System.out.println("=== СЦЕНАРИЙ 2: RefreshSession с НЕВЕРНЫМ 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";
|
||
|
||
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 = buildRefreshSessionJson(GLOBAL_SESSION_ID, wrongPwd, "test-refresh-wrong-1");
|
||
System.out.println();
|
||
System.out.println("📤 [S2] Отправляем RefreshSession с НЕВЕРНЫМ 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: RefreshSession с правильными данными
|
||
// ==========================================================
|
||
|
||
private static void runScenario_RefreshSession_CorrectPwd() throws Exception {
|
||
System.out.println();
|
||
System.out.println("=== СЦЕНАРИЙ 3: RefreshSession с КОРРЕКТНЫМ 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();
|
||
|
||
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 = buildRefreshSessionJson(GLOBAL_SESSION_ID, GLOBAL_SESSION_PWD, "test-refresh-ok-1");
|
||
System.out.println();
|
||
System.out.println("📤 [S3] Отправляем RefreshSession с КОРРЕКТНЫМ 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(" (Должен совпадать с тем, что отправляли в шаге 3 сценария 1)");
|
||
|
||
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 авторизации: запрос authNonce
|
||
private static String buildAuthStep1Json() {
|
||
return """
|
||
{
|
||
"op": "AuthChallenge",
|
||
"requestId": "test-auth-1",
|
||
"payload": {
|
||
"login": "%s"
|
||
}
|
||
}
|
||
""".formatted(TEST_LOGIN);
|
||
}
|
||
|
||
/**
|
||
* 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();
|
||
}
|
||
|
||
long timeMs = System.currentTimeMillis();
|
||
|
||
// preimage = "AUTHORIFICATED:" + timeMs + authNonce
|
||
String preimageStr = "AUTHORIFICATED:" + timeMs + authNonce;
|
||
byte[] preimage = preimageStr.getBytes(StandardCharsets.UTF_8);
|
||
|
||
// Подписываем приватным ключом устройства (deviceKey)
|
||
byte[] sig = Ed25519Util.sign(preimage, DEVICE_PRIV_KEY);
|
||
String sigB64 = Base64.getEncoder().encodeToString(sig);
|
||
|
||
return """
|
||
{
|
||
"op": "CreateAuthSession",
|
||
"requestId": "test-auth-2",
|
||
"payload": {
|
||
"storagePwd": "%s",
|
||
"timeMs": %d,
|
||
"signatureB64": "%s",
|
||
"clientInfo": "%s"
|
||
}
|
||
}
|
||
""".formatted(
|
||
storagePwd,
|
||
timeMs,
|
||
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",
|
||
"clientInfo": "%s"
|
||
}
|
||
}
|
||
""".formatted(
|
||
requestId,
|
||
sessionId,
|
||
sessionPwd,
|
||
TEST_CLIENT_INFO
|
||
);
|
||
}
|
||
|
||
// просто для теста: 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 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);
|
||
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;
|
||
}
|
||
} |