27 03 25
Доделал сервер и документ для разработчиков по Авторификациии и серверам Всё работает
This commit is contained in:
parent
2f9cf2bff1
commit
51de9779e3
@ -84,6 +84,8 @@
|
||||
- `SessionLogin`
|
||||
- последующих перевходов в уже созданную сессию
|
||||
|
||||
В API клиент передаёт `sessionKey` целиком одной строкой, и сервер хранит `active_sessions.session_key` тоже целиком одной строкой.
|
||||
|
||||
### `storagePwd`
|
||||
|
||||
`storagePwd` тоже **генерируется и передаётся клиентом** при создании сессии.
|
||||
@ -204,6 +206,7 @@ rsa2048/MIIBIjANBgkqh...
|
||||
"storagePwd": "BASE64_OR_APP_SPECIFIC_SECRET",
|
||||
"timeMs": 1774600000123,
|
||||
"authNonce": "8f2f0f71-0b1c-4ab2-8f5d-0bc5d6f6aa11",
|
||||
"deviceKey": "BASE64_DEVICE_PUBLIC_KEY",
|
||||
"signatureB64": "BASE64_SIGNATURE_BY_DEVICE_KEY",
|
||||
"clientInfo": "Android 15; Pixel 9"
|
||||
}
|
||||
@ -230,6 +233,21 @@ AUTH_CREATE_SESSION:alice:ed25519/BASE64_PUBLIC_KEY:BASE64_OR_APP_SPECIFIC_SECRE
|
||||
- снижается риск подмены `session_key` между клиентом и сервером;
|
||||
- `storagePwd` становится частью подтверждённого набора параметров создания сессии.
|
||||
|
||||
### Дополнительная проверка `deviceKey`
|
||||
|
||||
Перед проверкой подписи сервер должен:
|
||||
|
||||
1. загрузить актуальный `device_key` пользователя;
|
||||
2. сравнить его со значением `payload.deviceKey`;
|
||||
3. только после совпадения ключей проверять подпись.
|
||||
|
||||
Если ключ не совпадает, сервер должен возвращать ошибку о том, что ключ не соответствует актуальной версии.
|
||||
|
||||
На будущее:
|
||||
|
||||
- для сценария обновления `device_key` желательно добавить дополнительную проверку актуального ключа через Solana;
|
||||
- если и после этого ключ не подтверждается, сервер всё равно должен возвращать ошибку о несовпадении актуального ключа.
|
||||
|
||||
### Успешный ответ
|
||||
|
||||
```json
|
||||
@ -289,6 +307,7 @@ AUTH_CREATE_SESSION:alice:ed25519/BASE64_PUBLIC_KEY:BASE64_OR_APP_SPECIFIC_SECRE
|
||||
"requestId": "slogin-001",
|
||||
"payload": {
|
||||
"sessionId": "sess_7c5e5c4b",
|
||||
"sessionKey": "ed25519/BASE64_PUBLIC_KEY",
|
||||
"timeMs": 1774600010456,
|
||||
"signatureB64": "BASE64_SIGNATURE_BY_SESSION_KEY",
|
||||
"clientInfo": "Android 15; Pixel 9"
|
||||
@ -308,6 +327,16 @@ SESSION_LOGIN:{sessionId}:{timeMs}:{nonce}
|
||||
SESSION_LOGIN:sess_7c5e5c4b:1774600010456:0e5bb0f4-c7d8-4efb-b44d-bf31a6126c66
|
||||
```
|
||||
|
||||
### Дополнительная проверка `sessionKey`
|
||||
|
||||
Перед проверкой подписи сервер должен:
|
||||
|
||||
1. загрузить `active_sessions.session_key` по `sessionId`;
|
||||
2. сравнить его со значением `payload.sessionKey`;
|
||||
3. только после совпадения ключей проверять подпись.
|
||||
|
||||
Если ключ не совпадает, сервер должен возвращать ошибку о том, что ключ не соответствует актуальной версии.
|
||||
|
||||
Успешный ответ:
|
||||
|
||||
```json
|
||||
@ -364,6 +393,8 @@ SESSION_LOGIN:sess_7c5e5c4b:1774600010456:0e5bb0f4-c7d8-4efb-b44d-bf31a6126c66
|
||||
|
||||
### `CloseActiveSession`
|
||||
|
||||
Доступно только после успешного `SessionLogin`.
|
||||
|
||||
Запрос:
|
||||
|
||||
```json
|
||||
@ -398,6 +429,8 @@ SESSION_LOGIN:sess_7c5e5c4b:1774600010456:0e5bb0f4-c7d8-4efb-b44d-bf31a6126c66
|
||||
- `400 BAD_REQUEST` — не хватает поля или неверный формат.
|
||||
- `401 UNAUTHORIZED` — challenge не был пройден или соединение не авторизовано.
|
||||
- `403 INVALID_SIGNATURE` — подпись не прошла проверку.
|
||||
- `403 DEVICE_KEY_NOT_ACTUAL` — присланный `deviceKey` не совпадает с актуальным ключом пользователя.
|
||||
- `403 SESSION_KEY_NOT_ACTUAL` — присланный `sessionKey` не совпадает с актуальным ключом сессии.
|
||||
- `404 SESSION_NOT_FOUND` — сессия не существует или уже закрыта.
|
||||
- `409 NONCE_ALREADY_USED` — challenge уже использован.
|
||||
- `410 CHALLENGE_EXPIRED` — nonce устарел.
|
||||
@ -422,32 +455,33 @@ SESSION_LOGIN:sess_7c5e5c4b:1774600010456:0e5bb0f4-c7d8-4efb-b44d-bf31a6126c66
|
||||
По текущему состоянию кода сервер уже использует схему:
|
||||
|
||||
- `AuthChallenge(login)`
|
||||
- `CreateAuthSession(storagePwd, sessionPubKeyB64, timeMs, signatureB64, clientInfo)`
|
||||
- `CreateAuthSession(login, sessionKey, storagePwd, timeMs, authNonce, deviceKey, signatureB64, clientInfo)`
|
||||
- `SessionChallenge(sessionId)`
|
||||
- `SessionLogin(sessionId, timeMs, signatureB64, clientInfo)`
|
||||
- `SessionLogin(sessionId, sessionKey, timeMs, signatureB64, clientInfo)`
|
||||
|
||||
Текущая строка подписи для `CreateAuthSession` в коде:
|
||||
|
||||
```text
|
||||
AUTH_CREATE_SESSION:{login}:{timeMs}:{authNonce}
|
||||
```
|
||||
|
||||
То есть **сейчас** `sessionPubKeyB64` и `storagePwd` ещё не входят в preimage подписи.
|
||||
|
||||
### Рекомендуемый путь миграции
|
||||
|
||||
1. Ввести новую версию контракта `CreateAuthSession`.
|
||||
2. Добавить поле `sessionKey` вместо `sessionPubKeyB64`.
|
||||
3. На сервере распознавать префикс алгоритма в `sessionKey`.
|
||||
4. Перейти на подпись строки:
|
||||
|
||||
```text
|
||||
AUTH_CREATE_SESSION:{login}:{sessionKey}:{storagePwd}:{timeMs}:{authNonce}
|
||||
```
|
||||
|
||||
5. На переходный период поддерживать оба варианта:
|
||||
- legacy: без `sessionKey` и `storagePwd` в подписи;
|
||||
- vNext: с полным набором полей.
|
||||
Перед проверкой подписи сервер также должен сверять:
|
||||
|
||||
- `payload.deviceKey` с актуальным `solana_users.device_key`;
|
||||
- `payload.sessionKey` с актуальным `active_sessions.session_key`.
|
||||
|
||||
### Рекомендуемый путь миграции
|
||||
|
||||
1. Ввести новую версию контракта `CreateAuthSession`.
|
||||
2. На сервере хранить `session_key` целиком одной строкой.
|
||||
3. На сервере распознавать префикс алгоритма в `sessionKey`.
|
||||
4. В `CreateAuthSession` передавать и сверять `deviceKey`.
|
||||
5. В `SessionLogin` передавать и сверять `sessionKey`.
|
||||
6. Использовать подпись строки:
|
||||
|
||||
```text
|
||||
AUTH_CREATE_SESSION:{login}:{sessionKey}:{storagePwd}:{timeMs}:{authNonce}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@ -467,5 +501,7 @@ AUTH_CREATE_SESSION:{login}:{sessionKey}:{storagePwd}:{timeMs}:{authNonce}
|
||||
- Да, клиент сам создаёт `session_key`.
|
||||
- Да, клиент сам передаёт `storagePwd`.
|
||||
- Для `session_key` имеет смысл ввести префикс алгоритма, например `ed25519/...`.
|
||||
- Для `CreateAuthSession` клиент должен дополнительно передавать `deviceKey`, а сервер должен сверять его с актуальным ключом пользователя.
|
||||
- Для `SessionLogin` клиент должен дополнительно передавать `sessionKey`, а сервер должен сверять его с актуальным ключом сессии.
|
||||
- Для `CreateAuthSession` рекомендуется подписывать не только `login`, `timeMs` и `authNonce`, но также `sessionKey` и `storagePwd`.
|
||||
- Для разработчиков клиентов лучше сразу документировать API через полные JSON-примеры запросов и ответов.
|
||||
|
||||
@ -20,7 +20,7 @@ import java.sql.Statement;
|
||||
*
|
||||
* v2 (sessions):
|
||||
* - active_sessions.session_pwd удалён
|
||||
* - active_sessions.session_key хранит публичный ключ сессии (sessionPubKeyB64)
|
||||
* - active_sessions.session_key хранит публичный ключ сессии целиком одной строкой
|
||||
*/
|
||||
public final class DatabaseInitializer {
|
||||
|
||||
|
||||
@ -8,7 +8,7 @@ public class ActiveSessionEntry {
|
||||
private String sessionId;
|
||||
private String login;
|
||||
|
||||
/** session_key: публичный ключ сессии (base64 от 32 байт). */
|
||||
/** session_key: публичный ключ сессии целиком одной строкой, например ed25519/BASE64_PUBLIC_KEY. */
|
||||
private String sessionKey;
|
||||
|
||||
private String storagePwd;
|
||||
|
||||
@ -10,10 +10,12 @@ import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
||||
import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler;
|
||||
import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_CreateAuthSession_Request;
|
||||
import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_CreateAuthSession_Response;
|
||||
import server.logic.ws_protocol.JSON.utils.AuthKeyUtils;
|
||||
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
||||
import server.logic.ws_protocol.WireCodes;
|
||||
import server.ws.WsConnectionUtils;
|
||||
import shine.db.dao.ActiveSessionsDAO;
|
||||
import shine.db.dao.SolanaUsersDAO;
|
||||
import shine.db.entities.ActiveSessionEntry;
|
||||
import shine.db.entities.SolanaUserEntry;
|
||||
import shine.geo.ClientInfoService;
|
||||
@ -31,12 +33,12 @@ import java.sql.SQLException;
|
||||
*
|
||||
* Логика авторизации (v2):
|
||||
* - Создание сессии: AuthChallenge(login) -> authNonce -> CreateAuthSession(...)
|
||||
* - Клиент генерирует sessionKey (Ed25519), хранит приватный ключ у себя,
|
||||
* отправляет на сервер ТОЛЬКО sessionPubKeyB64.
|
||||
* - Сервер сохраняет sessionPubKeyB64 в active_sessions.session_key.
|
||||
* - Клиент генерирует sessionKey, хранит приватный ключ у себя,
|
||||
* отправляет на сервер sessionKey целиком одной строкой.
|
||||
* - Сервер сохраняет sessionKey в active_sessions.session_key как есть.
|
||||
*
|
||||
* Подпись deviceKey (Ed25519) проверяется над строкой (UTF-8):
|
||||
* AUTH_CREATE_SESSION:{login}:{timeMs}:{authNonce}
|
||||
* AUTH_CREATE_SESSION:{login}:{sessionKey}:{storagePwd}:{timeMs}:{authNonce}
|
||||
*
|
||||
* На выходе:
|
||||
* - создаётся запись active_sessions
|
||||
@ -70,8 +72,54 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler {
|
||||
return err;
|
||||
}
|
||||
|
||||
SolanaUserEntry user = ctx.getSolanaUser();
|
||||
String login = user.getLogin();
|
||||
SolanaUserEntry userFromContext = ctx.getSolanaUser();
|
||||
String loginFromContext = userFromContext.getLogin();
|
||||
String login = req.getLogin();
|
||||
if (login == null || login.isBlank()) {
|
||||
Net_Response err = NetExceptionResponseFactory.error(
|
||||
req,
|
||||
WireCodes.Status.BAD_REQUEST,
|
||||
"EMPTY_LOGIN",
|
||||
"Пустой login"
|
||||
);
|
||||
WsConnectionUtils.closeConnection(ctx, 4001, "Auth failed: empty login");
|
||||
return err;
|
||||
}
|
||||
if (!login.equals(loginFromContext)) {
|
||||
Net_Response err = NetExceptionResponseFactory.error(
|
||||
req,
|
||||
WireCodes.Status.BAD_REQUEST,
|
||||
"LOGIN_MISMATCH",
|
||||
"login не соответствует контексту AuthChallenge"
|
||||
);
|
||||
WsConnectionUtils.closeConnection(ctx, 4001, "Auth failed: login mismatch");
|
||||
return err;
|
||||
}
|
||||
|
||||
SolanaUserEntry user;
|
||||
try {
|
||||
user = SolanaUsersDAO.getInstance().getByLogin(login);
|
||||
} catch (SQLException e) {
|
||||
Net_Response err = NetExceptionResponseFactory.error(
|
||||
req,
|
||||
WireCodes.Status.SERVER_DATA_ERROR,
|
||||
"DB_ERROR_USER_LOOKUP",
|
||||
"Ошибка БД при получении пользователя"
|
||||
);
|
||||
WsConnectionUtils.closeConnection(ctx, 4001, "Auth failed: db user lookup");
|
||||
return err;
|
||||
}
|
||||
if (user == null) {
|
||||
Net_Response err = NetExceptionResponseFactory.error(
|
||||
req,
|
||||
WireCodes.Status.UNVERIFIED,
|
||||
"USER_NOT_FOUND",
|
||||
"Пользователь не найден"
|
||||
);
|
||||
WsConnectionUtils.closeConnection(ctx, 4001, "Auth failed: user not found");
|
||||
return err;
|
||||
}
|
||||
|
||||
if (login == null || login.isBlank()) {
|
||||
Net_Response err = NetExceptionResponseFactory.error(
|
||||
req,
|
||||
@ -95,40 +143,38 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler {
|
||||
return err;
|
||||
}
|
||||
|
||||
String sessionPubKeyB64 = req.getSessionPubKeyB64();
|
||||
if (sessionPubKeyB64 == null || sessionPubKeyB64.isBlank()) {
|
||||
String sessionKey = req.getSessionKey();
|
||||
if (sessionKey == null || sessionKey.isBlank()) {
|
||||
Net_Response err = NetExceptionResponseFactory.error(
|
||||
req,
|
||||
WireCodes.Status.BAD_REQUEST,
|
||||
"EMPTY_SESSION_PUBKEY",
|
||||
"Пустой sessionPubKeyB64"
|
||||
"EMPTY_SESSION_KEY",
|
||||
"Пустой sessionKey"
|
||||
);
|
||||
WsConnectionUtils.closeConnection(ctx, 4001, "Auth failed: empty session pubkey");
|
||||
WsConnectionUtils.closeConnection(ctx, 4001, "Auth failed: empty session key");
|
||||
return err;
|
||||
}
|
||||
|
||||
// Проверим, что sessionPubKeyB64 декодируется в 32 байта
|
||||
byte[] sessionPubKey32;
|
||||
sessionKey = AuthKeyUtils.normalize(sessionKey, "sessionKey");
|
||||
try {
|
||||
sessionPubKey32 = Base64Ws.decode(sessionPubKeyB64);
|
||||
AuthKeyUtils.parseEd25519PublicKey(sessionKey, "sessionKey");
|
||||
} catch (UnsupportedOperationException e) {
|
||||
Net_Response err = NetExceptionResponseFactory.error(
|
||||
req,
|
||||
422,
|
||||
"UNSUPPORTED_KEY_ALGORITHM",
|
||||
"sessionKey prefix is not supported"
|
||||
);
|
||||
WsConnectionUtils.closeConnection(ctx, 4001, "Auth failed: unsupported session key algorithm");
|
||||
return err;
|
||||
} catch (IllegalArgumentException e) {
|
||||
Net_Response err = NetExceptionResponseFactory.error(
|
||||
req,
|
||||
WireCodes.Status.BAD_REQUEST,
|
||||
"BAD_BASE64",
|
||||
"Некорректный base64 в sessionPubKeyB64"
|
||||
"Некорректный формат sessionKey"
|
||||
);
|
||||
WsConnectionUtils.closeConnection(ctx, 4001, "Auth failed: bad session pubkey base64");
|
||||
return err;
|
||||
}
|
||||
if (sessionPubKey32.length != 32) {
|
||||
Net_Response err = NetExceptionResponseFactory.error(
|
||||
req,
|
||||
WireCodes.Status.BAD_REQUEST,
|
||||
"BAD_SESSION_PUBKEY_LEN",
|
||||
"sessionPubKey должен быть 32 байта"
|
||||
);
|
||||
WsConnectionUtils.closeConnection(ctx, 4001, "Auth failed: bad session pubkey length");
|
||||
WsConnectionUtils.closeConnection(ctx, 4001, "Auth failed: bad session key format");
|
||||
return err;
|
||||
}
|
||||
|
||||
@ -163,8 +209,8 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler {
|
||||
clientInfoFromClient = clientInfoFromClient.substring(0, 50);
|
||||
}
|
||||
|
||||
String devicePubKeyB64 = user.getDeviceKey();
|
||||
if (devicePubKeyB64 == null || devicePubKeyB64.isBlank()) {
|
||||
String deviceKeyFromDb = user.getDeviceKey();
|
||||
if (deviceKeyFromDb == null || deviceKeyFromDb.isBlank()) {
|
||||
Net_Response err = NetExceptionResponseFactory.error(
|
||||
req,
|
||||
WireCodes.Status.BAD_REQUEST,
|
||||
@ -176,16 +222,73 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler {
|
||||
}
|
||||
|
||||
String authNonce = ctx.getAuthNonce();
|
||||
String authNonceFromReq = req.getAuthNonce();
|
||||
if (authNonceFromReq == null || authNonceFromReq.isBlank()) {
|
||||
Net_Response err = NetExceptionResponseFactory.error(
|
||||
req,
|
||||
WireCodes.Status.BAD_REQUEST,
|
||||
"EMPTY_AUTH_NONCE",
|
||||
"Пустой authNonce"
|
||||
);
|
||||
WsConnectionUtils.closeConnection(ctx, 4001, "Auth failed: empty authNonce");
|
||||
return err;
|
||||
}
|
||||
if (!authNonce.equals(authNonceFromReq)) {
|
||||
Net_Response err = NetExceptionResponseFactory.error(
|
||||
req,
|
||||
WireCodes.Status.BAD_REQUEST,
|
||||
"AUTH_NONCE_MISMATCH",
|
||||
"authNonce не соответствует контексту AuthChallenge"
|
||||
);
|
||||
WsConnectionUtils.closeConnection(ctx, 4001, "Auth failed: authNonce mismatch");
|
||||
return err;
|
||||
}
|
||||
|
||||
String deviceKeyFromReq = req.getDeviceKey();
|
||||
if (deviceKeyFromReq == null || deviceKeyFromReq.isBlank()) {
|
||||
Net_Response err = NetExceptionResponseFactory.error(
|
||||
req,
|
||||
WireCodes.Status.BAD_REQUEST,
|
||||
"EMPTY_DEVICE_KEY",
|
||||
"Пустой deviceKey"
|
||||
);
|
||||
WsConnectionUtils.closeConnection(ctx, 4001, "Auth failed: empty deviceKey");
|
||||
return err;
|
||||
}
|
||||
deviceKeyFromReq = deviceKeyFromReq.trim();
|
||||
|
||||
// TODO: для ротации device_key стоит дополнительно сверять актуальное значение через Solana.
|
||||
if (!deviceKeyFromReq.equals(deviceKeyFromDb)) {
|
||||
Net_Response err = NetExceptionResponseFactory.error(
|
||||
req,
|
||||
WireCodes.Status.UNVERIFIED,
|
||||
"DEVICE_KEY_NOT_ACTUAL",
|
||||
"device_key не соответствует актуальной версии"
|
||||
);
|
||||
WsConnectionUtils.closeConnection(ctx, 4001, "Auth failed: device key mismatch");
|
||||
return err;
|
||||
}
|
||||
|
||||
boolean sigOk;
|
||||
try {
|
||||
sigOk = verifyCreateSessionSignature(
|
||||
user,
|
||||
login,
|
||||
sessionKey,
|
||||
storagePwd,
|
||||
authNonce,
|
||||
timeMs,
|
||||
deviceKeyFromDb,
|
||||
signatureB64
|
||||
);
|
||||
} catch (UnsupportedOperationException ex) {
|
||||
Net_Response err = NetExceptionResponseFactory.error(
|
||||
req,
|
||||
422,
|
||||
"UNSUPPORTED_KEY_ALGORITHM",
|
||||
"deviceKey algorithm is not supported"
|
||||
);
|
||||
WsConnectionUtils.closeConnection(ctx, 4001, "Auth failed: unsupported device key algorithm");
|
||||
return err;
|
||||
} catch (IllegalArgumentException ex) {
|
||||
Net_Response err = NetExceptionResponseFactory.error(
|
||||
req,
|
||||
@ -239,7 +342,7 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler {
|
||||
activeSessionEntry = new ActiveSessionEntry(
|
||||
sessionId,
|
||||
login,
|
||||
sessionPubKeyB64, // session_key (pubkey)
|
||||
sessionKey, // session_key (pubkey string as-is)
|
||||
storagePwd,
|
||||
now,
|
||||
now,
|
||||
@ -283,18 +386,24 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler {
|
||||
}
|
||||
|
||||
private static boolean verifyCreateSessionSignature(
|
||||
SolanaUserEntry user,
|
||||
String login,
|
||||
String sessionKey,
|
||||
String storagePwd,
|
||||
String authNonce,
|
||||
long timeMs,
|
||||
String deviceKey,
|
||||
String signatureB64
|
||||
) throws IllegalArgumentException {
|
||||
|
||||
// deviceKey (pub, 32)
|
||||
byte[] publicKey32 = Ed25519Util.keyFromBase64(user.getDeviceKey());
|
||||
byte[] signature64 = Base64Ws.decode(signatureB64);
|
||||
byte[] publicKey32 = AuthKeyUtils.parseEd25519PublicKey(deviceKey, "deviceKey");
|
||||
byte[] signature64 = Base64Ws.decodeLen(signatureB64, 64, "signatureB64");
|
||||
|
||||
String preimageStr = "AUTH_CREATE_SESSION:" + login + ":" + timeMs + ":" + authNonce;
|
||||
String preimageStr = "AUTH_CREATE_SESSION:"
|
||||
+ login + ":"
|
||||
+ sessionKey + ":"
|
||||
+ storagePwd + ":"
|
||||
+ timeMs + ":"
|
||||
+ authNonce;
|
||||
byte[] preimage = preimageStr.getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
return Ed25519Util.verify(preimage, signature64, publicKey32);
|
||||
|
||||
@ -10,6 +10,7 @@ import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
||||
import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler;
|
||||
import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_SessionLogin_Request;
|
||||
import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_SessionLogin_Response;
|
||||
import server.logic.ws_protocol.JSON.utils.AuthKeyUtils;
|
||||
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
||||
import server.logic.ws_protocol.WireCodes;
|
||||
import shine.db.dao.ActiveSessionsDAO;
|
||||
@ -30,7 +31,7 @@ import java.sql.SQLException;
|
||||
* - SessionChallenge(sessionId) выдаёт nonce (одноразовый, TTL).
|
||||
* - SessionLogin проверяет подпись sessionKey над строкой:
|
||||
* SESSION_LOGIN:{sessionId}:{timeMs}:{nonce}
|
||||
* - sessionPubKey берём из БД: active_sessions.session_key (base64 32 bytes).
|
||||
* - sessionKey берём из запроса и сверяем с active_sessions.session_key.
|
||||
*
|
||||
* При успехе:
|
||||
* - ctx становится AUTH_STATUS_USER
|
||||
@ -100,6 +101,17 @@ public class Net_SessionLogin_Handler implements JsonMessageHandler {
|
||||
);
|
||||
}
|
||||
|
||||
String sessionKeyFromReq = req.getSessionKey();
|
||||
if (sessionKeyFromReq == null || sessionKeyFromReq.isBlank()) {
|
||||
return NetExceptionResponseFactory.error(
|
||||
req,
|
||||
WireCodes.Status.BAD_REQUEST,
|
||||
"EMPTY_SESSION_KEY",
|
||||
"Пустой sessionKey"
|
||||
);
|
||||
}
|
||||
sessionKeyFromReq = AuthKeyUtils.normalize(sessionKeyFromReq, "sessionKey");
|
||||
|
||||
ActiveSessionEntry session;
|
||||
try {
|
||||
session = ActiveSessionsDAO.getInstance().getBySessionId(sessionId);
|
||||
@ -121,8 +133,8 @@ public class Net_SessionLogin_Handler implements JsonMessageHandler {
|
||||
);
|
||||
}
|
||||
|
||||
String sessionPubKeyB64 = session.getSessionKey(); // это pubKey (Base64(32))
|
||||
if (sessionPubKeyB64 == null || sessionPubKeyB64.isBlank()) {
|
||||
String sessionKeyFromDb = session.getSessionKey();
|
||||
if (sessionKeyFromDb == null || sessionKeyFromDb.isBlank()) {
|
||||
return NetExceptionResponseFactory.error(
|
||||
req,
|
||||
WireCodes.Status.SERVER_DATA_ERROR,
|
||||
@ -130,12 +142,29 @@ public class Net_SessionLogin_Handler implements JsonMessageHandler {
|
||||
"В сессии не задан session_key"
|
||||
);
|
||||
}
|
||||
sessionKeyFromDb = sessionKeyFromDb.trim();
|
||||
|
||||
if (!sessionKeyFromReq.equals(sessionKeyFromDb)) {
|
||||
return NetExceptionResponseFactory.error(
|
||||
req,
|
||||
WireCodes.Status.UNVERIFIED,
|
||||
"SESSION_KEY_NOT_ACTUAL",
|
||||
"session_key не соответствует актуальной версии"
|
||||
);
|
||||
}
|
||||
|
||||
String nonce = ctx.getSessionLoginNonce();
|
||||
|
||||
boolean sigOk;
|
||||
try {
|
||||
sigOk = verifySessionLoginSignature(sessionPubKeyB64, sessionId, timeMs, nonce, signatureB64);
|
||||
sigOk = verifySessionLoginSignature(sessionKeyFromReq, sessionId, timeMs, nonce, signatureB64);
|
||||
} catch (UnsupportedOperationException e) {
|
||||
return NetExceptionResponseFactory.error(
|
||||
req,
|
||||
422,
|
||||
"UNSUPPORTED_KEY_ALGORITHM",
|
||||
"sessionKey prefix is not supported"
|
||||
);
|
||||
} catch (IllegalArgumentException e) {
|
||||
return NetExceptionResponseFactory.error(
|
||||
req,
|
||||
@ -243,17 +272,15 @@ public class Net_SessionLogin_Handler implements JsonMessageHandler {
|
||||
}
|
||||
|
||||
private static boolean verifySessionLoginSignature(
|
||||
String sessionPubKeyB64,
|
||||
String sessionKey,
|
||||
String sessionId,
|
||||
long timeMs,
|
||||
String nonce,
|
||||
String signatureB64
|
||||
) throws IllegalArgumentException {
|
||||
|
||||
// pubKey: Base64(32). (Ed25519Util.keyFromBase64 должен использовать стандартный Base64)
|
||||
byte[] publicKey32 = Ed25519Util.keyFromBase64(sessionPubKeyB64);
|
||||
byte[] publicKey32 = AuthKeyUtils.parseEd25519PublicKey(sessionKey, "sessionKey");
|
||||
|
||||
// signature: Base64(64) через единую утилиту WS-протокола
|
||||
byte[] signature64 = Base64Ws.decodeLen(signatureB64, 64, "signatureB64");
|
||||
|
||||
String preimageStr = "SESSION_LOGIN:" + sessionId + ":" + timeMs + ":" + nonce;
|
||||
|
||||
@ -7,32 +7,48 @@ import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
||||
*
|
||||
* Шаги:
|
||||
* 1) AuthChallenge(login) -> authNonce
|
||||
* 2) CreateAuthSession(storagePwd, sessionPubKeyB64, timeMs, signatureB64, clientInfo)
|
||||
* 2) CreateAuthSession(login, sessionKey, storagePwd, timeMs, authNonce, deviceKey, signatureB64, clientInfo)
|
||||
*
|
||||
* Подпись deviceKey делается над строкой (UTF-8):
|
||||
* AUTH_CREATE_SESSION:{login}:{timeMs}:{authNonce}:{sessionPubKeyB64}:{storagePwd}
|
||||
* AUTH_CREATE_SESSION:{login}:{sessionKey}:{storagePwd}:{timeMs}:{authNonce}
|
||||
*
|
||||
* Важно:
|
||||
* - sessionKey генерируется на клиенте, на сервер отправляется ТОЛЬКО sessionPubKeyB64 (32 bytes base64).
|
||||
* - В БД active_sessions.session_key хранится sessionPubKeyB64.
|
||||
* - sessionKey генерируется на клиенте и передаётся на сервер целиком одной строкой.
|
||||
* - В БД active_sessions.session_key хранится sessionKey целиком одной строкой.
|
||||
*/
|
||||
public class Net_CreateAuthSession_Request extends Net_Request {
|
||||
|
||||
private String login;
|
||||
|
||||
/** Клиентский пароль для хранения данных (base64 от 32 байт). */
|
||||
private String storagePwd;
|
||||
|
||||
/** Публичный ключ сессии (sessionPubKey), base64 от 32 байт. */
|
||||
private String sessionPubKeyB64;
|
||||
/** Публичный ключ сессии в API-формате, например ed25519/BASE64_PUBLIC_KEY. */
|
||||
private String sessionKey;
|
||||
|
||||
/** Время на стороне клиента (мс с 1970-01-01). */
|
||||
private long timeMs;
|
||||
|
||||
/** Nonce из AuthChallenge. */
|
||||
private String authNonce;
|
||||
|
||||
/** Публичный ключ устройства пользователя. */
|
||||
private String deviceKey;
|
||||
|
||||
/** Подпись Ed25519(deviceKey) над строкой AUTH_CREATE_SESSION:... (base64). */
|
||||
private String signatureB64;
|
||||
|
||||
/** Краткая строка от клиента (до 50 символов) с описанием устройства/клиента. */
|
||||
private String clientInfo;
|
||||
|
||||
public String getLogin() {
|
||||
return login;
|
||||
}
|
||||
|
||||
public void setLogin(String login) {
|
||||
this.login = login;
|
||||
}
|
||||
|
||||
public String getStoragePwd() {
|
||||
return storagePwd;
|
||||
}
|
||||
@ -41,12 +57,12 @@ public class Net_CreateAuthSession_Request extends Net_Request {
|
||||
this.storagePwd = storagePwd;
|
||||
}
|
||||
|
||||
public String getSessionPubKeyB64() {
|
||||
return sessionPubKeyB64;
|
||||
public String getSessionKey() {
|
||||
return sessionKey;
|
||||
}
|
||||
|
||||
public void setSessionPubKeyB64(String sessionPubKeyB64) {
|
||||
this.sessionPubKeyB64 = sessionPubKeyB64;
|
||||
public void setSessionKey(String sessionKey) {
|
||||
this.sessionKey = sessionKey;
|
||||
}
|
||||
|
||||
public long getTimeMs() {
|
||||
@ -57,6 +73,22 @@ public class Net_CreateAuthSession_Request extends Net_Request {
|
||||
this.timeMs = timeMs;
|
||||
}
|
||||
|
||||
public String getAuthNonce() {
|
||||
return authNonce;
|
||||
}
|
||||
|
||||
public void setAuthNonce(String authNonce) {
|
||||
this.authNonce = authNonce;
|
||||
}
|
||||
|
||||
public String getDeviceKey() {
|
||||
return deviceKey;
|
||||
}
|
||||
|
||||
public void setDeviceKey(String deviceKey) {
|
||||
this.deviceKey = deviceKey;
|
||||
}
|
||||
|
||||
public String getSignatureB64() {
|
||||
return signatureB64;
|
||||
}
|
||||
|
||||
@ -4,7 +4,7 @@ import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
||||
|
||||
/**
|
||||
* Шаг 2 входа в существующую сессию (v2):
|
||||
* SessionLogin(sessionId, timeMs, signatureB64) -> storagePwd, AUTH_STATUS_USER
|
||||
* SessionLogin(sessionId, sessionKey, timeMs, signatureB64) -> storagePwd, AUTH_STATUS_USER
|
||||
*
|
||||
* Подпись делается sessionKey (приватный ключ на устройстве) над строкой (UTF-8):
|
||||
* SESSION_LOGIN:{sessionId}:{timeMs}:{nonce}
|
||||
@ -14,6 +14,7 @@ import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
||||
public class Net_SessionLogin_Request extends Net_Request {
|
||||
|
||||
private String sessionId;
|
||||
private String sessionKey;
|
||||
private long timeMs;
|
||||
private String signatureB64;
|
||||
|
||||
@ -28,6 +29,14 @@ public class Net_SessionLogin_Request extends Net_Request {
|
||||
this.sessionId = sessionId;
|
||||
}
|
||||
|
||||
public String getSessionKey() {
|
||||
return sessionKey;
|
||||
}
|
||||
|
||||
public void setSessionKey(String sessionKey) {
|
||||
this.sessionKey = sessionKey;
|
||||
}
|
||||
|
||||
public long getTimeMs() {
|
||||
return timeMs;
|
||||
}
|
||||
|
||||
@ -20,7 +20,7 @@ import static org.junit.jupiter.api.Assertions.*;
|
||||
* - и после завершения оставить в БД 3 активных сессии (S1,S2,S3)
|
||||
*
|
||||
* Протокол v2:
|
||||
* - создание сессии: AuthChallenge -> CreateAuthSession (deviceKey подпись, + sessionPubKey)
|
||||
* - создание сессии: AuthChallenge -> CreateAuthSession (deviceKey подпись, + deviceKey + sessionKey)
|
||||
* - вход в сессию: SessionChallenge(sessionId) -> nonce, затем SessionLogin(sessionId,time,signature(sessionKey))
|
||||
* - ListSessions и CloseActiveSession доступны только в AUTH_STATUS_USER (после SessionLogin)
|
||||
*/
|
||||
@ -109,16 +109,15 @@ public class IT_02_Sessions {
|
||||
String authNonce = JsonParsers.authNonce(nonceResp);
|
||||
assertNotNull(authNonce, "authNonce must not be null for " + label);
|
||||
|
||||
// для тестов: sessionKey = deviceKey (в реале будет отдельный keypair)
|
||||
String sessionPubKeyB64 = TestConfig.devicePublicKeyB64(login);
|
||||
String sessionKey = TestConfig.sessionKey(login);
|
||||
|
||||
// storagePwd на клиенте (сохраняем, чтобы потом проверить, что сервер вернул именно его)
|
||||
String storagePwd = TestConfig.fakeStoragePwd();
|
||||
|
||||
// шаг 2: CreateAuthSession (device подпись + sessionPubKey)
|
||||
// шаг 2: CreateAuthSession (device подпись + deviceKey + sessionKey)
|
||||
String createResp = ws.call(
|
||||
"CreateAuthSession(" + label + ")",
|
||||
JsonBuilders.createAuthSessionV2(login, authNonce, storagePwd, sessionPubKeyB64),
|
||||
JsonBuilders.createAuthSessionV2(login, authNonce, storagePwd, sessionKey),
|
||||
t
|
||||
);
|
||||
assertEquals(200, JsonParsers.status(createResp), "CreateAuthSession(" + label + ") must be 200");
|
||||
@ -128,10 +127,9 @@ public class IT_02_Sessions {
|
||||
|
||||
r.ok("Создана сессия " + label + ": sessionId=" + sid);
|
||||
|
||||
// для тестов используем devicePriv как sessionPriv
|
||||
byte[] sessionPrivKey = TestConfig.getDevicePrivatKey(login);
|
||||
byte[] sessionPrivKey = TestConfig.getSessionPrivatKey(login);
|
||||
|
||||
return new Session(sid, sessionPrivKey, storagePwd);
|
||||
return new Session(sid, sessionKey, sessionPrivKey, storagePwd);
|
||||
}
|
||||
}
|
||||
|
||||
@ -143,7 +141,7 @@ public class IT_02_Sessions {
|
||||
assertNotNull(nonce, "SessionChallenge nonce must not be null");
|
||||
|
||||
// шаг 2: SessionLogin(sessionId, timeMs, signature(sessionKey, SESSION_LOGIN:...))
|
||||
String loginResp = ws.call("SessionLogin " + label, JsonBuilders.sessionLogin(s.sessionId, nonce, s.sessionPrivKey), t);
|
||||
String loginResp = ws.call("SessionLogin " + label, JsonBuilders.sessionLogin(s.sessionId, s.sessionKey, nonce, s.sessionPrivKey), t);
|
||||
assertEquals(200, JsonParsers.status(loginResp), "SessionLogin must be 200");
|
||||
|
||||
String storagePwd = JsonParsers.storagePwd(loginResp);
|
||||
@ -153,5 +151,5 @@ public class IT_02_Sessions {
|
||||
r.ok(label + ": SessionLogin OK, storagePwd verified");
|
||||
}
|
||||
|
||||
private record Session(String sessionId, byte[] sessionPrivKey, String storagePwd) {}
|
||||
private record Session(String sessionId, String sessionKey, byte[] sessionPrivKey, String storagePwd) {}
|
||||
}
|
||||
@ -119,6 +119,7 @@ public final class TestConfig {
|
||||
|
||||
// NEW: session pub b64 helper
|
||||
public static String sessionPublicKeyB64(String login) { return Base64.getEncoder().encodeToString(getSessionPublicKey(login)); }
|
||||
public static String sessionKey(String login) { return "ed25519/" + sessionPublicKeyB64(login); }
|
||||
|
||||
// ============ backward-compatible helpers for "user1" ============
|
||||
public static String BCH_NAME() { return getBlockchainName(LOGIN()); }
|
||||
@ -143,6 +144,9 @@ public final class TestConfig {
|
||||
public static String SESSION_PUBKEY_B64() { return sessionPublicKeyB64(LOGIN()); }
|
||||
public static String SESSION2_PUBKEY_B64() { return sessionPublicKeyB64(LOGIN2()); }
|
||||
public static String SESSION3_PUBKEY_B64() { return sessionPublicKeyB64(LOGIN3()); }
|
||||
public static String SESSION_KEY() { return sessionKey(LOGIN()); }
|
||||
public static String SESSION2_KEY() { return sessionKey(LOGIN2()); }
|
||||
public static String SESSION3_KEY() { return sessionKey(LOGIN3()); }
|
||||
|
||||
// ============ misc ============
|
||||
public static String fakeStoragePwd() {
|
||||
|
||||
@ -104,20 +104,15 @@ public final class JsonBuilders {
|
||||
}
|
||||
|
||||
// ---------------- CreateAuthSession (v2) ----------------
|
||||
// v2: sessionKey генерируется/хранится на клиенте, на сервер отправляем sessionPubKeyB64 (base64).
|
||||
//
|
||||
// ВАЖНО (новое правило):
|
||||
// Подпись CreateAuthSession делается ТОЛЬКО deviceKey над строкой:
|
||||
// preimage = "AUTH_CREATE_SESSION:" + login + ":" + timeMs + ":" + authNonce
|
||||
//
|
||||
// storagePwd и sessionPubKeyB64 НЕ входят в preimage.
|
||||
// Подпись CreateAuthSession делается deviceKey над строкой:
|
||||
// preimage = "AUTH_CREATE_SESSION:" + login + ":" + sessionKey + ":" + storagePwd + ":" + timeMs + ":" + authNonce
|
||||
|
||||
public static String createAuthSessionV2(String login, String authNonce, String storagePwd, String sessionPubKeyB64) {
|
||||
public static String createAuthSessionV2(String login, String authNonce, String storagePwd, String sessionKey) {
|
||||
long timeMs = System.currentTimeMillis();
|
||||
|
||||
// подпись делаем devicePrivKey
|
||||
byte[] devicePriv = TestConfig.getDevicePrivatKey(login);
|
||||
String sigB64 = signAuthCreateSession(login, timeMs, authNonce, devicePriv);
|
||||
String deviceKey = TestConfig.devicePublicKeyB64(login);
|
||||
String sigB64 = signAuthCreateSession(login, sessionKey, storagePwd, timeMs, authNonce, devicePriv);
|
||||
|
||||
String requestId = TestIds.next("create");
|
||||
return """
|
||||
@ -125,18 +120,24 @@ public final class JsonBuilders {
|
||||
"op": "CreateAuthSession",
|
||||
"requestId": "%s",
|
||||
"payload": {
|
||||
"login": "%s",
|
||||
"storagePwd": "%s",
|
||||
"sessionPubKeyB64": "%s",
|
||||
"sessionKey": "%s",
|
||||
"timeMs": %d,
|
||||
"authNonce": "%s",
|
||||
"deviceKey": "%s",
|
||||
"signatureB64": "%s",
|
||||
"clientInfo": "%s"
|
||||
}
|
||||
}
|
||||
""".formatted(
|
||||
requestId,
|
||||
login,
|
||||
storagePwd,
|
||||
sessionPubKeyB64,
|
||||
sessionKey,
|
||||
timeMs,
|
||||
authNonce,
|
||||
deviceKey,
|
||||
sigB64,
|
||||
TestConfig.TEST_CLIENT_INFO
|
||||
);
|
||||
@ -161,7 +162,7 @@ public final class JsonBuilders {
|
||||
// Подпись SessionLogin по-прежнему делается sessionPrivKey:
|
||||
// preimage = "SESSION_LOGIN:" + sessionId + ":" + timeMs + ":" + nonce
|
||||
|
||||
public static String sessionLogin(String sessionId, String nonce, byte[] sessionPrivKey) {
|
||||
public static String sessionLogin(String sessionId, String sessionKey, String nonce, byte[] sessionPrivKey) {
|
||||
long timeMs = System.currentTimeMillis();
|
||||
String sigB64 = signSessionLogin(sessionId, timeMs, nonce, sessionPrivKey);
|
||||
|
||||
@ -172,12 +173,13 @@ public final class JsonBuilders {
|
||||
"requestId": "%s",
|
||||
"payload": {
|
||||
"sessionId": "%s",
|
||||
"sessionKey": "%s",
|
||||
"timeMs": %d,
|
||||
"signatureB64": "%s",
|
||||
"clientInfo": "%s"
|
||||
}
|
||||
}
|
||||
""".formatted(requestId, sessionId, timeMs, sigB64, TestConfig.TEST_CLIENT_INFO);
|
||||
""".formatted(requestId, sessionId, sessionKey, timeMs, sigB64, TestConfig.TEST_CLIENT_INFO);
|
||||
}
|
||||
|
||||
// ---------------- ListSessions ----------------
|
||||
@ -226,11 +228,11 @@ public final class JsonBuilders {
|
||||
|
||||
/**
|
||||
* Подпись CreateAuthSession(v2):
|
||||
* preimage = "AUTH_CREATE_SESSION:" + login + ":" + timeMs + ":" + authNonce
|
||||
* preimage = "AUTH_CREATE_SESSION:" + login + ":" + sessionKey + ":" + storagePwd + ":" + timeMs + ":" + authNonce
|
||||
* подписываем devicePrivKey.
|
||||
*/
|
||||
public static String signAuthCreateSession(String login, long timeMs, String authNonce, byte[] devicePrivKey) {
|
||||
String preimageStr = "AUTH_CREATE_SESSION:" + login + ":" + timeMs + ":" + authNonce;
|
||||
public static String signAuthCreateSession(String login, String sessionKey, String storagePwd, long timeMs, String authNonce, byte[] devicePrivKey) {
|
||||
String preimageStr = "AUTH_CREATE_SESSION:" + login + ":" + sessionKey + ":" + storagePwd + ":" + timeMs + ":" + authNonce;
|
||||
byte[] preimage = preimageStr.getBytes(StandardCharsets.UTF_8);
|
||||
byte[] sig = Ed25519Util.sign(preimage, devicePrivKey);
|
||||
return Base64.getEncoder().encodeToString(sig);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user