From eaf1affb2784e0cb5a96bdbd5619ca8be8f95cb0b73778e9db49858f29768d74 Mon Sep 17 00:00:00 2001 From: AidarKC Date: Wed, 17 Dec 2025 13:06:08 +0300 Subject: [PATCH] =?UTF-8?q?17=2012=2025=20=D0=9F=D1=80=D0=BE=D0=BC=D0=B5?= =?UTF-8?q?=D0=B6=D1=83=D1=82=D0=BE=D1=87=D0=BD=D0=B0=D1=8F=20=D0=B2=D0=B5?= =?UTF-8?q?=D1=80=D1=81=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/blockchain/body/BodyRecord.java | 39 +++- ...dParser_new.java => BodyRecordParser.java} | 10 +- .../java/blockchain/body/BodyRecord_new.java | 38 --- .../main/java/blockchain/body/HeaderBody.java | 220 ++++++++---------- .../java/blockchain/body/HeaderBody_new.java | 155 ------------ .../main/java/blockchain/body/TextBody.java | 80 ++++--- .../java/blockchain/body/TextBody_new.java | 89 ------- .../java/utils/crypto/BchCryptoVerifier.java | 91 ++++++++ .../java/utils/crypto/CryptoSelfTest.java | 50 ++++ .../main/java/utils/crypto/Ed25519Util.java | 173 ++++++++++++++ .../java/utils/crypto/HashSHA256Util.java | 53 +++++ .../src/main/java/utils/crypto/README.md | 30 +++ .../java/shine/db/SqliteDbController.java | 2 - .../java/shine/db/dao/ActiveSessionsDAO.java | 14 +- .../java/shine/db/dao/SolanaUsersDAO.java | 16 +- .../main/java/shine/db/dao/UserParamsDAO.java | 14 +- ...veSession.java => ActiveSessionEntry.java} | 30 +-- .../{SolanaUser.java => SolanaUserEntry.java} | 16 +- .../{UserParam.java => UserParamEntry.java} | 18 +- .../ws_protocol/JSON/ConnectionContext.java | 32 +-- .../Blockchain/Net_AddBlock_new_Request.java | 38 +++ .../Blockchain/Net_AddBlock_new_Response.java | 29 +++ .../auth/Net_AuthChallenge_Handler.java | 8 +- .../auth/Net_CloseActiveSession_Handler.java | 8 +- .../auth/Net_CreateAuthSession__Handler.java | 16 +- .../auth/Net_ListSessions_Handler.java | 10 +- .../auth/Net_RefreshSession_Handler.java | 14 +- .../blockchain/Net_AddBlock_new_Handler.java | 190 +++++++++++++++ .../tempToTest/Net_AddUser_Handler.java | 4 +- src/main/java/server/ws/WsServer.java | 2 - 30 files changed, 926 insertions(+), 563 deletions(-) rename shine-server-blockchain/src/main/java/blockchain/body/{BodyRecordParser_new.java => BodyRecordParser.java} (78%) delete mode 100644 shine-server-blockchain/src/main/java/blockchain/body/BodyRecord_new.java delete mode 100644 shine-server-blockchain/src/main/java/blockchain/body/HeaderBody_new.java delete mode 100644 shine-server-blockchain/src/main/java/blockchain/body/TextBody_new.java create mode 100644 shine-server-crypto/src/main/java/utils/crypto/BchCryptoVerifier.java create mode 100644 shine-server-crypto/src/main/java/utils/crypto/CryptoSelfTest.java create mode 100644 shine-server-crypto/src/main/java/utils/crypto/Ed25519Util.java create mode 100644 shine-server-crypto/src/main/java/utils/crypto/HashSHA256Util.java create mode 100644 shine-server-crypto/src/main/java/utils/crypto/README.md rename shine-server-db/src/main/java/shine/db/entities/{ActiveSession.java => ActiveSessionEntry.java} (86%) rename shine-server-db/src/main/java/shine/db/entities/{SolanaUser.java => SolanaUserEntry.java} (86%) rename shine-server-db/src/main/java/shine/db/entities/{UserParam.java => UserParamEntry.java} (83%) create mode 100644 shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/entyties/Blockchain/Net_AddBlock_new_Request.java create mode 100644 shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/entyties/Blockchain/Net_AddBlock_new_Response.java create mode 100644 shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/blockchain/Net_AddBlock_new_Handler.java diff --git a/shine-server-blockchain/src/main/java/blockchain/body/BodyRecord.java b/shine-server-blockchain/src/main/java/blockchain/body/BodyRecord.java index ac8125f..d81c737 100644 --- a/shine-server-blockchain/src/main/java/blockchain/body/BodyRecord.java +++ b/shine-server-blockchain/src/main/java/blockchain/body/BodyRecord.java @@ -1,19 +1,38 @@ package blockchain.body; /** - * Общий интерфейс для всех тел (body) блоков. - *. - * Каждый тип тела реализует: - * - check() — проверку корректности данных - * - toBytes() — опциональную сериализацию обратно в байты + * BodyRecord_new — общий контракт для всех типов body (тела блока). + * + * Идея: + * - На каждый тип body (Header, Text, File, ...) — отдельный класс. + * - Десериализация из байтов делается КОНСТРУКТОРОМ: + * new XxxBody_new(byte[] bodyBytes) + * (конструктор обязан распарсить байты или кинуть IllegalArgumentException). + * + * - Валидация делается методом check(). + * check() должен: + * - вернуть this, если всё корректно + * - кинуть IllegalArgumentException, если данные некорректны + * + * - Сериализация обратно в байты делается методом toBytes(). + * + * - type() и version() — это идентификаторы формата body. + * Они должны быть константами для класса (например TYPE=1, VERSION=1). */ public interface BodyRecord { - /** Проверить корректность содержимого. */ + /** Код типа записи (совпадает с recordType в BchBlockEntry). */ + short type(); + + /** Версия формата записи (совпадает с recordTypeVersion в BchBlockEntry). */ + short version(); + + /** Проверить корректность содержимого и вернуть этот объект (или кинуть исключение). */ BodyRecord check(); - /** (опционально) Сериализация тела обратно в байты. */ - default byte[] toBytes() { - throw new UnsupportedOperationException("toBytes() не реализован"); - } + /** + * Сериализовать тело записи в байты (ровно то, что кладётся в block.body). + * Важно: НЕ включает общий заголовок блока (recordNumber/timestamp/type/version). + */ + byte[] toBytes(); } diff --git a/shine-server-blockchain/src/main/java/blockchain/body/BodyRecordParser_new.java b/shine-server-blockchain/src/main/java/blockchain/body/BodyRecordParser.java similarity index 78% rename from shine-server-blockchain/src/main/java/blockchain/body/BodyRecordParser_new.java rename to shine-server-blockchain/src/main/java/blockchain/body/BodyRecordParser.java index f5e847d..7708cfd 100644 --- a/shine-server-blockchain/src/main/java/blockchain/body/BodyRecordParser_new.java +++ b/shine-server-blockchain/src/main/java/blockchain/body/BodyRecordParser.java @@ -9,11 +9,11 @@ import java.nio.ByteOrder; * Правило совместимости (строгое): * - если (type, version) неизвестны → кидаем IllegalArgumentException */ -public final class BodyRecordParser_new { +public final class BodyRecordParser { - private BodyRecordParser_new() {} + private BodyRecordParser() {} - public static BodyRecord_new parse(byte[] bodyBytes) { + public static BodyRecord parse(byte[] bodyBytes) { if (bodyBytes == null) throw new IllegalArgumentException("bodyBytes == null"); if (bodyBytes.length < 4) throw new IllegalArgumentException("bodyBytes too short (<4)"); @@ -25,8 +25,8 @@ public final class BodyRecordParser_new { int key = ((type & 0xFFFF) << 16) | (ver & 0xFFFF); return switch (key) { - case 0x0000_0001 -> new HeaderBody_new(bodyBytes); // type=0, ver=1 - case 0x0001_0001 -> new TextBody_new(bodyBytes); // type=1, ver=1 + case 0x0000_0001 -> new HeaderBody(bodyBytes); // type=0, ver=1 + case 0x0001_0001 -> new TextBody(bodyBytes); // type=1, ver=1 default -> throw new IllegalArgumentException(String.format( "Unknown body type/version: type=%d ver=%d (key=0x%08X)", (type & 0xFFFF), (ver & 0xFFFF), key diff --git a/shine-server-blockchain/src/main/java/blockchain/body/BodyRecord_new.java b/shine-server-blockchain/src/main/java/blockchain/body/BodyRecord_new.java deleted file mode 100644 index 68f3978..0000000 --- a/shine-server-blockchain/src/main/java/blockchain/body/BodyRecord_new.java +++ /dev/null @@ -1,38 +0,0 @@ -package blockchain.body; - -/** - * BodyRecord_new — общий контракт для всех типов body (тела блока). - * - * Идея: - * - На каждый тип body (Header, Text, File, ...) — отдельный класс. - * - Десериализация из байтов делается КОНСТРУКТОРОМ: - * new XxxBody_new(byte[] bodyBytes) - * (конструктор обязан распарсить байты или кинуть IllegalArgumentException). - * - * - Валидация делается методом check(). - * check() должен: - * - вернуть this, если всё корректно - * - кинуть IllegalArgumentException, если данные некорректны - * - * - Сериализация обратно в байты делается методом toBytes(). - * - * - type() и version() — это идентификаторы формата body. - * Они должны быть константами для класса (например TYPE=1, VERSION=1). - */ -public interface BodyRecord_new { - - /** Код типа записи (совпадает с recordType в BchBlockEntry). */ - short type(); - - /** Версия формата записи (совпадает с recordTypeVersion в BchBlockEntry). */ - short version(); - - /** Проверить корректность содержимого и вернуть этот объект (или кинуть исключение). */ - BodyRecord_new check(); - - /** - * Сериализовать тело записи в байты (ровно то, что кладётся в block.body). - * Важно: НЕ включает общий заголовок блока (recordNumber/timestamp/type/version). - */ - byte[] toBytes(); -} diff --git a/shine-server-blockchain/src/main/java/blockchain/body/HeaderBody.java b/shine-server-blockchain/src/main/java/blockchain/body/HeaderBody.java index c6c51ba..b9a5647 100644 --- a/shine-server-blockchain/src/main/java/blockchain/body/HeaderBody.java +++ b/shine-server-blockchain/src/main/java/blockchain/body/HeaderBody.java @@ -7,128 +7,98 @@ import java.util.Arrays; import java.util.Objects; /** - * ============================================================================ - * HeaderBody — тело записи типа 0 (заглавие блокчейна) - * ============================================================================ - *. - * 🧩 Назначение: - * Первый блок каждой пользовательской цепочки (.bch) — это "заголовок". - * Он хранит базовую информацию о владельце, версии и публичном ключе. - *. - * Этот блок всегда имеет: - * • recordType = 0 - * • recordNumber = 0 - * • recordTypeVersion = 1 - *. - * ---------------------------------------------------------------------------- - * 🔹 Формат body (без общих 20 байт заголовка блока BchBlock) - *. - * | Смещение | Размер | Поле | Формат | Описание | - * |-----------|--------|--------------------|---------|-----------| - * | 0x00 | 8 | tag | ASCII | Статическая сигнатура "SHiNE001" | - * | 0x08 | 8 | blockchainId | long BE | Уникальный идентификатор цепочки | - * | 0x10 | 1 | userLoginLength=N | uint8 | Длина логина пользователя | - * | 0x11 | N | userLogin | UTF-8 | Логин пользователя | - * | 0x11+N | 4 | blockchainType | int BE | Зарезервировано (всегда 0) | - * | 0x15+N | 4 | blockchainNumber | int BE | Зарезервировано (всегда 0) | - * | 0x19+N | 2 | versionUserBch | short BE| Версия формата (всегда 1) | - * | 0x1B+N | 8 | prevUserBchId | long BE | Зарезервировано (всегда 0) | - * | 0x23+N | 32 | publicKey32 | raw | Публичный ключ (Ed25519, 32 байта) | - *. - * ---------------------------------------------------------------------------- - * 💡 Пример структуры в байтах: - *. - * 0000: 53 48 69 4E 45 30 30 31 "SHiNE001" - * 0008: 00 00 00 00 01 23 45 67 blockchainId - * 0010: 05 userLoginLength = 5 - * 0011: 41 69 64 61 72 userLogin = "Aidar" - * 0016: 00 00 00 00 blockchainType = 0 - * 001A: 00 00 00 00 blockchainNumber = 0 - * 001E: 00 01 versionUserBch = 1 - * 0020: 00 00 00 00 00 00 00 00 prevUserBchId = 0 - * 0028: [32 байта публичного ключа] - *. - * ---------------------------------------------------------------------------- - * 📘 Замечания: - * • Поля blockchainType, blockchainNumber, versionUserBch, prevUserBchId - * зарезервированы для будущего расширения формата. - * • На данный момент все они принимают фиксированные значения: - * blockchainType = 0 - * blockchainNumber = 0 - * versionUserBch = 1 - * prevUserBchId = 0 - *. - * ============================================================================ + * HeaderBody_new — type=0, version=1. + * + * Полный bodyBytes: + * [2] type=0 + * [2] version=1 + * [payload...] + * + * Payload (как у текущего HeaderBody): + * [8] tag ASCII "SHiNE001" + * [8] blockchainId (long BE) + * [1] loginLength=N (uint8) + * [N] userLogin UTF-8 + * [4] blockchainType (int BE) (резерв) + * [4] blockchainNumber (int BE) (резерв) + * [2] versionUserBch (short BE) (резерв) + * [8] prevUserBchId (long BE) (резерв) + * [32] publicKey32 (raw) */ public final class HeaderBody implements BodyRecord { public static final short TYPE = 0; + public static final short VER = 1; + public static final String TAG = "SHiNE001"; public static final int PUBKEY_LEN = 32; - public final String tag; // всегда "SHiNE001" + public final String tag; // "SHiNE001" public final long blockchainId; - public final String userLogin; // UTF-8 - public final int blockchainType; // пока 0 - public final int blockchainNumber; // пока 0 - public final short versionUserBch; // пока 1 - public final long prevUserBchId; // пока 0 - public final byte[] publicKey32; // 32 байта + public final String userLogin; + public final int blockchainType; + public final int blockchainNumber; + public final short versionUserBch; + public final long prevUserBchId; + public final byte[] publicKey32; - // ------------------------------------------------------------ - // Конструктор №1 — из массива байт (для парсинга существующего блока) - // ------------------------------------------------------------ - public HeaderBody(byte[] body) { - Objects.requireNonNull(body, "body == null"); - if (body.length < 8 + 8 + 1 + 2 + 4 + 4 + 8 + 32) - throw new IllegalArgumentException("HeaderBody слишком короткое"); + /** + * Десериализация из полного bodyBytes (ВКЛЮЧАЯ первые 4 байта type/version). + */ + public HeaderBody(byte[] bodyBytes) { + Objects.requireNonNull(bodyBytes, "bodyBytes == null"); + if (bodyBytes.length < 4) throw new IllegalArgumentException("HeaderBody_new too short"); - ByteBuffer buf = ByteBuffer.wrap(body).order(ByteOrder.BIG_ENDIAN); + ByteBuffer bb = ByteBuffer.wrap(bodyBytes).order(ByteOrder.BIG_ENDIAN); + short type = bb.getShort(); + short ver = bb.getShort(); + if (type != TYPE || ver != VER) + throw new IllegalArgumentException("Not HeaderBody_new: type=" + type + " ver=" + ver); + + // Теперь bb стоит на payload + if (bb.remaining() < 8 + 8 + 1 + 4 + 4 + 2 + 8 + 32) + throw new IllegalArgumentException("Header payload too short"); - // [8] тег byte[] tagBytes = new byte[8]; - buf.get(tagBytes); - String tag = new String(tagBytes, StandardCharsets.US_ASCII); - if (!TAG.equals(tag)) - throw new IllegalArgumentException("Неверный тег: " + tag); - this.tag = tag; + bb.get(tagBytes); + String t = new String(tagBytes, StandardCharsets.US_ASCII); + if (!TAG.equals(t)) throw new IllegalArgumentException("Bad tag: " + t); + this.tag = t; - // [8] blockchainId - this.blockchainId = buf.getLong(); + this.blockchainId = bb.getLong(); - // [1] длина логина - int loginLen = Byte.toUnsignedInt(buf.get()); - if (loginLen == 0 || buf.remaining() < loginLen + 4 + 4 + 2 + 8 + 32) - throw new IllegalArgumentException("Некорректная длина логина"); + int loginLen = Byte.toUnsignedInt(bb.get()); + if (loginLen <= 0 || bb.remaining() < loginLen + 4 + 4 + 2 + 8 + 32) + throw new IllegalArgumentException("Bad login length"); - // [N] логин byte[] loginBytes = new byte[loginLen]; - buf.get(loginBytes); + bb.get(loginBytes); this.userLogin = new String(loginBytes, StandardCharsets.UTF_8); - // Остальные поля - this.blockchainType = buf.getInt(); - this.blockchainNumber = buf.getInt(); - this.versionUserBch = buf.getShort(); - this.prevUserBchId = buf.getLong(); + this.blockchainType = bb.getInt(); + this.blockchainNumber = bb.getInt(); + this.versionUserBch = bb.getShort(); + this.prevUserBchId = bb.getLong(); this.publicKey32 = new byte[PUBKEY_LEN]; - buf.get(this.publicKey32); + bb.get(this.publicKey32); } - // ------------------------------------------------------------ - // Конструктор №2 — из параметров (для создания нового заголовка) - // ------------------------------------------------------------ - public HeaderBody(long blockchainId, String userLogin, - int blockchainType, int blockchainNumber, - short versionUserBch, long prevUserBchId, + /** + * Создание “вручную” (для генерации первого блока). + */ + public HeaderBody(long blockchainId, + String userLogin, + int blockchainType, + int blockchainNumber, + short versionUserBch, + long prevUserBchId, byte[] publicKey32) { Objects.requireNonNull(userLogin, "userLogin == null"); Objects.requireNonNull(publicKey32, "publicKey32 == null"); - if (publicKey32.length != PUBKEY_LEN) - throw new IllegalArgumentException("Публичный ключ должен состоять из 32 байт"); + throw new IllegalArgumentException("publicKey32 must be 32 bytes"); this.tag = TAG; this.blockchainId = blockchainId; @@ -140,17 +110,17 @@ public final class HeaderBody implements BodyRecord { this.publicKey32 = Arrays.copyOf(publicKey32, PUBKEY_LEN); } - // ------------------------------------------------------------ - // Проверка и сериализация - // ------------------------------------------------------------ + @Override public short type() { return TYPE; } + @Override public short version() { return VER; } + @Override public HeaderBody check() { if (userLogin == null || userLogin.isBlank()) - throw new IllegalArgumentException("Логин не может быть пустым"); + throw new IllegalArgumentException("Login is blank"); if (!userLogin.matches("^[A-Za-z0-9_]+$")) - throw new IllegalArgumentException("Логин может содержать только латиницу, цифры и _"); + throw new IllegalArgumentException("Login must match ^[A-Za-z0-9_]+$"); if (publicKey32 == null || publicKey32.length != PUBKEY_LEN) - throw new IllegalArgumentException("Публичный ключ должен быть 32 байта"); + throw new IllegalArgumentException("publicKey32 must be 32 bytes"); return this; } @@ -158,34 +128,28 @@ public final class HeaderBody implements BodyRecord { public byte[] toBytes() { byte[] loginUtf8 = userLogin.getBytes(StandardCharsets.UTF_8); if (loginUtf8.length > 255) - throw new IllegalArgumentException("Логин слишком длинный (>255 байт)"); + throw new IllegalArgumentException("Login too long (>255 bytes)"); - int cap = 8 + 8 + 1 + loginUtf8.length + 4 + 4 + 2 + 8 + 32; - ByteBuffer buf = ByteBuffer.allocate(cap).order(ByteOrder.BIG_ENDIAN); + int payloadCap = 8 + 8 + 1 + loginUtf8.length + 4 + 4 + 2 + 8 + 32; + int cap = 4 + payloadCap; - buf.put(TAG.getBytes(StandardCharsets.US_ASCII)); // [8] - buf.putLong(blockchainId); // [8] - buf.put((byte) loginUtf8.length); // [1] - buf.put(loginUtf8); // [N] - buf.putInt(blockchainType); // [4] - buf.putInt(blockchainNumber); // [4] - buf.putShort(versionUserBch); // [2] - buf.putLong(prevUserBchId); // [8] - buf.put(publicKey32); // [32] + ByteBuffer bb = ByteBuffer.allocate(cap).order(ByteOrder.BIG_ENDIAN); - return buf.array(); + // [type/version] + bb.putShort(TYPE); + bb.putShort(VER); + + // payload + bb.put(TAG.getBytes(StandardCharsets.US_ASCII)); // [8] + bb.putLong(blockchainId); // [8] + bb.put((byte) loginUtf8.length); // [1] + bb.put(loginUtf8); // [N] + bb.putInt(blockchainType); // [4] + bb.putInt(blockchainNumber); // [4] + bb.putShort(versionUserBch); // [2] + bb.putLong(prevUserBchId); // [8] + bb.put(publicKey32); // [32] + + return bb.array(); } - - @Override - public String toString() { - return "HeaderBody{" + - "id=" + blockchainId + - ", login='" + userLogin + '\'' + - ", type=" + blockchainType + - ", num=" + blockchainNumber + - ", ver=" + versionUserBch + - ", prev=" + prevUserBchId + - ", pubkey32=" + Arrays.toString(Arrays.copyOf(publicKey32, 4)) + "..." + - '}'; - } -} +} \ No newline at end of file diff --git a/shine-server-blockchain/src/main/java/blockchain/body/HeaderBody_new.java b/shine-server-blockchain/src/main/java/blockchain/body/HeaderBody_new.java deleted file mode 100644 index 18e99e0..0000000 --- a/shine-server-blockchain/src/main/java/blockchain/body/HeaderBody_new.java +++ /dev/null @@ -1,155 +0,0 @@ -package blockchain.body; - -import java.nio.ByteBuffer; -import java.nio.ByteOrder; -import java.nio.charset.StandardCharsets; -import java.util.Arrays; -import java.util.Objects; - -/** - * HeaderBody_new — type=0, version=1. - * - * Полный bodyBytes: - * [2] type=0 - * [2] version=1 - * [payload...] - * - * Payload (как у текущего HeaderBody): - * [8] tag ASCII "SHiNE001" - * [8] blockchainId (long BE) - * [1] loginLength=N (uint8) - * [N] userLogin UTF-8 - * [4] blockchainType (int BE) (резерв) - * [4] blockchainNumber (int BE) (резерв) - * [2] versionUserBch (short BE) (резерв) - * [8] prevUserBchId (long BE) (резерв) - * [32] publicKey32 (raw) - */ -public final class HeaderBody_new implements BodyRecord_new { - - public static final short TYPE = 0; - public static final short VER = 1; - - public static final String TAG = "SHiNE001"; - public static final int PUBKEY_LEN = 32; - - public final String tag; // "SHiNE001" - public final long blockchainId; - public final String userLogin; - public final int blockchainType; - public final int blockchainNumber; - public final short versionUserBch; - public final long prevUserBchId; - public final byte[] publicKey32; - - /** - * Десериализация из полного bodyBytes (ВКЛЮЧАЯ первые 4 байта type/version). - */ - public HeaderBody_new(byte[] bodyBytes) { - Objects.requireNonNull(bodyBytes, "bodyBytes == null"); - if (bodyBytes.length < 4) throw new IllegalArgumentException("HeaderBody_new too short"); - - ByteBuffer bb = ByteBuffer.wrap(bodyBytes).order(ByteOrder.BIG_ENDIAN); - short type = bb.getShort(); - short ver = bb.getShort(); - if (type != TYPE || ver != VER) - throw new IllegalArgumentException("Not HeaderBody_new: type=" + type + " ver=" + ver); - - // Теперь bb стоит на payload - if (bb.remaining() < 8 + 8 + 1 + 4 + 4 + 2 + 8 + 32) - throw new IllegalArgumentException("Header payload too short"); - - byte[] tagBytes = new byte[8]; - bb.get(tagBytes); - String t = new String(tagBytes, StandardCharsets.US_ASCII); - if (!TAG.equals(t)) throw new IllegalArgumentException("Bad tag: " + t); - this.tag = t; - - this.blockchainId = bb.getLong(); - - int loginLen = Byte.toUnsignedInt(bb.get()); - if (loginLen <= 0 || bb.remaining() < loginLen + 4 + 4 + 2 + 8 + 32) - throw new IllegalArgumentException("Bad login length"); - - byte[] loginBytes = new byte[loginLen]; - bb.get(loginBytes); - this.userLogin = new String(loginBytes, StandardCharsets.UTF_8); - - this.blockchainType = bb.getInt(); - this.blockchainNumber = bb.getInt(); - this.versionUserBch = bb.getShort(); - this.prevUserBchId = bb.getLong(); - - this.publicKey32 = new byte[PUBKEY_LEN]; - bb.get(this.publicKey32); - } - - /** - * Создание “вручную” (для генерации первого блока). - */ - public HeaderBody_new(long blockchainId, - String userLogin, - int blockchainType, - int blockchainNumber, - short versionUserBch, - long prevUserBchId, - byte[] publicKey32) { - - Objects.requireNonNull(userLogin, "userLogin == null"); - Objects.requireNonNull(publicKey32, "publicKey32 == null"); - if (publicKey32.length != PUBKEY_LEN) - throw new IllegalArgumentException("publicKey32 must be 32 bytes"); - - this.tag = TAG; - this.blockchainId = blockchainId; - this.userLogin = userLogin; - this.blockchainType = blockchainType; - this.blockchainNumber = blockchainNumber; - this.versionUserBch = versionUserBch; - this.prevUserBchId = prevUserBchId; - this.publicKey32 = Arrays.copyOf(publicKey32, PUBKEY_LEN); - } - - @Override public short type() { return TYPE; } - @Override public short version() { return VER; } - - @Override - public HeaderBody_new check() { - if (userLogin == null || userLogin.isBlank()) - throw new IllegalArgumentException("Login is blank"); - if (!userLogin.matches("^[A-Za-z0-9_]+$")) - throw new IllegalArgumentException("Login must match ^[A-Za-z0-9_]+$"); - if (publicKey32 == null || publicKey32.length != PUBKEY_LEN) - throw new IllegalArgumentException("publicKey32 must be 32 bytes"); - return this; - } - - @Override - public byte[] toBytes() { - byte[] loginUtf8 = userLogin.getBytes(StandardCharsets.UTF_8); - if (loginUtf8.length > 255) - throw new IllegalArgumentException("Login too long (>255 bytes)"); - - int payloadCap = 8 + 8 + 1 + loginUtf8.length + 4 + 4 + 2 + 8 + 32; - int cap = 4 + payloadCap; - - ByteBuffer bb = ByteBuffer.allocate(cap).order(ByteOrder.BIG_ENDIAN); - - // [type/version] - bb.putShort(TYPE); - bb.putShort(VER); - - // payload - bb.put(TAG.getBytes(StandardCharsets.US_ASCII)); // [8] - bb.putLong(blockchainId); // [8] - bb.put((byte) loginUtf8.length); // [1] - bb.put(loginUtf8); // [N] - bb.putInt(blockchainType); // [4] - bb.putInt(blockchainNumber); // [4] - bb.putShort(versionUserBch); // [2] - bb.putLong(prevUserBchId); // [8] - bb.put(publicKey32); // [32] - - return bb.array(); - } -} \ No newline at end of file diff --git a/shine-server-blockchain/src/main/java/blockchain/body/TextBody.java b/shine-server-blockchain/src/main/java/blockchain/body/TextBody.java index 2d06a45..5add828 100644 --- a/shine-server-blockchain/src/main/java/blockchain/body/TextBody.java +++ b/shine-server-blockchain/src/main/java/blockchain/body/TextBody.java @@ -1,77 +1,89 @@ package blockchain.body; import java.nio.ByteBuffer; +import java.nio.ByteOrder; import java.nio.charset.CharacterCodingException; import java.nio.charset.CodingErrorAction; import java.nio.charset.StandardCharsets; import java.util.Objects; /** - * TextBody — тело записи типа 1 (простое текстовое сообщение). - *. - * Формат body: - * [N] message (UTF-8) - *. - * Тело полностью состоит из UTF-8-строки без каких-либо метаданных. + * TextBody_new — type=1, version=1. + * + * Полный bodyBytes: + * [2] type=1 + * [2] version=1 + * [payload...] + * + * Payload: + * UTF-8 bytes (N>0) */ public final class TextBody implements BodyRecord { public static final short TYPE = 1; + public static final short VER = 1; public final String message; - // ------------------------------------------------------------ - // Конструктор №1 — из массива байт (для парсинга) - // ------------------------------------------------------------ - public TextBody(byte[] body) { - Objects.requireNonNull(body, "body == null"); - if (body.length == 0) - throw new IllegalArgumentException("Тело текстового сообщения пустое"); + /** Десериализация из полного bodyBytes (включая type/version). */ + public TextBody(byte[] bodyBytes) { + Objects.requireNonNull(bodyBytes, "bodyBytes == null"); + if (bodyBytes.length < 5) // минимум: 4 байта type/ver + 1 байт текста + throw new IllegalArgumentException("TextBody_new too short"); - // строгая проверка валидности UTF-8 + ByteBuffer bb = ByteBuffer.wrap(bodyBytes).order(ByteOrder.BIG_ENDIAN); + short type = bb.getShort(); + short ver = bb.getShort(); + if (type != TYPE || ver != VER) + throw new IllegalArgumentException("Not TextBody_new: type=" + type + " ver=" + ver); + + byte[] payload = new byte[bb.remaining()]; + bb.get(payload); + + // строгая проверка UTF-8 var decoder = StandardCharsets.UTF_8 .newDecoder() .onMalformedInput(CodingErrorAction.REPORT) .onUnmappableCharacter(CodingErrorAction.REPORT); try { - var chars = decoder.decode(ByteBuffer.wrap(body)); - this.message = chars.toString(); + this.message = decoder.decode(ByteBuffer.wrap(payload)).toString(); } catch (CharacterCodingException e) { - throw new IllegalArgumentException("Тело не является корректным UTF-8", e); + throw new IllegalArgumentException("Text payload is not valid UTF-8", e); } + + if (this.message.isBlank()) + throw new IllegalArgumentException("Text message is blank"); } - // ------------------------------------------------------------ - // Конструктор №2 — из строки (для создания нового сообщения) - // ------------------------------------------------------------ + /** Создание из строки. */ public TextBody(String message) { Objects.requireNonNull(message, "message == null"); if (message.isBlank()) - throw new IllegalArgumentException("Текст сообщения не может быть пустым"); + throw new IllegalArgumentException("message is blank"); this.message = message; } - // ------------------------------------------------------------ - // Проверка и сериализация - // ------------------------------------------------------------ + @Override public short type() { return TYPE; } + @Override public short version() { return VER; } + @Override public TextBody check() { if (message == null || message.isBlank()) - throw new IllegalArgumentException("Текст сообщения не может быть пустым"); + throw new IllegalArgumentException("Text message is blank"); return this; } @Override public byte[] toBytes() { - return message.getBytes(StandardCharsets.UTF_8); - } + byte[] msg = message.getBytes(StandardCharsets.UTF_8); + if (msg.length == 0) + throw new IllegalArgumentException("Text payload is empty"); - @Override - public String toString() { - return "TextBody{" + - "len=" + message.length() + - ", msg='" + (message.length() > 60 ? message.substring(0, 57) + "..." : message) + '\'' + - '}'; + ByteBuffer bb = ByteBuffer.allocate(4 + msg.length).order(ByteOrder.BIG_ENDIAN); + bb.putShort(TYPE); + bb.putShort(VER); + bb.put(msg); + return bb.array(); } -} +} \ No newline at end of file diff --git a/shine-server-blockchain/src/main/java/blockchain/body/TextBody_new.java b/shine-server-blockchain/src/main/java/blockchain/body/TextBody_new.java deleted file mode 100644 index 2524f9c..0000000 --- a/shine-server-blockchain/src/main/java/blockchain/body/TextBody_new.java +++ /dev/null @@ -1,89 +0,0 @@ -package blockchain.body; - -import java.nio.ByteBuffer; -import java.nio.ByteOrder; -import java.nio.charset.CharacterCodingException; -import java.nio.charset.CodingErrorAction; -import java.nio.charset.StandardCharsets; -import java.util.Objects; - -/** - * TextBody_new — type=1, version=1. - * - * Полный bodyBytes: - * [2] type=1 - * [2] version=1 - * [payload...] - * - * Payload: - * UTF-8 bytes (N>0) - */ -public final class TextBody_new implements BodyRecord_new { - - public static final short TYPE = 1; - public static final short VER = 1; - - public final String message; - - /** Десериализация из полного bodyBytes (включая type/version). */ - public TextBody_new(byte[] bodyBytes) { - Objects.requireNonNull(bodyBytes, "bodyBytes == null"); - if (bodyBytes.length < 5) // минимум: 4 байта type/ver + 1 байт текста - throw new IllegalArgumentException("TextBody_new too short"); - - ByteBuffer bb = ByteBuffer.wrap(bodyBytes).order(ByteOrder.BIG_ENDIAN); - short type = bb.getShort(); - short ver = bb.getShort(); - if (type != TYPE || ver != VER) - throw new IllegalArgumentException("Not TextBody_new: type=" + type + " ver=" + ver); - - byte[] payload = new byte[bb.remaining()]; - bb.get(payload); - - // строгая проверка UTF-8 - var decoder = StandardCharsets.UTF_8 - .newDecoder() - .onMalformedInput(CodingErrorAction.REPORT) - .onUnmappableCharacter(CodingErrorAction.REPORT); - - try { - this.message = decoder.decode(ByteBuffer.wrap(payload)).toString(); - } catch (CharacterCodingException e) { - throw new IllegalArgumentException("Text payload is not valid UTF-8", e); - } - - if (this.message.isBlank()) - throw new IllegalArgumentException("Text message is blank"); - } - - /** Создание из строки. */ - public TextBody_new(String message) { - Objects.requireNonNull(message, "message == null"); - if (message.isBlank()) - throw new IllegalArgumentException("message is blank"); - this.message = message; - } - - @Override public short type() { return TYPE; } - @Override public short version() { return VER; } - - @Override - public TextBody_new check() { - if (message == null || message.isBlank()) - throw new IllegalArgumentException("Text message is blank"); - return this; - } - - @Override - public byte[] toBytes() { - byte[] msg = message.getBytes(StandardCharsets.UTF_8); - if (msg.length == 0) - throw new IllegalArgumentException("Text payload is empty"); - - ByteBuffer bb = ByteBuffer.allocate(4 + msg.length).order(ByteOrder.BIG_ENDIAN); - bb.putShort(TYPE); - bb.putShort(VER); - bb.put(msg); - return bb.array(); - } -} \ No newline at end of file diff --git a/shine-server-crypto/src/main/java/utils/crypto/BchCryptoVerifier.java b/shine-server-crypto/src/main/java/utils/crypto/BchCryptoVerifier.java new file mode 100644 index 0000000..4906117 --- /dev/null +++ b/shine-server-crypto/src/main/java/utils/crypto/BchCryptoVerifier.java @@ -0,0 +1,91 @@ +package utils.crypto; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Objects; + +/** + * BchCryptoVerifier — проверка хэша и подписи Ed25519 для .bch сущностей. + *. + * Канонический пре-имидж: + * [N] userLogin UTF-8 (без длины! строго как байты строки) + * [8] blockchainId (big-endian long) + * [32] prevHash32 + * [*] rawBytes (без подписи и без хэша) + *. + * Проверяем: + * • hash32 == SHA-256(preimage) + * • signature64 валидна как Ed25519(preimage, publicKey32) + */ +public final class BchCryptoVerifier { + + private static final Logger log = LoggerFactory.getLogger(BchCryptoVerifier.class); + + private BchCryptoVerifier() {} + + public static boolean verifyAll(String userLogin, + long blockchainId, + byte[] prevHash32, + byte[] rawBytes, + byte[] signature64, + byte[] hash32, + byte[] publicKey32) { + try { + Objects.requireNonNull(userLogin, "userLogin"); + requireLen(prevHash32, 32, "prevHash32"); + requireLen(signature64, 64, "signature64"); + requireLen(hash32, 32, "hash32"); + requireLen(publicKey32, 32, "publicKey32"); + Objects.requireNonNull(rawBytes, "rawBytes"); + + byte[] preimage = buildPreimage(userLogin, blockchainId, prevHash32, rawBytes); + + // 1) Проверка хэша (BC) + byte[] calcHash = HashSHA256Util.sha256(preimage); + boolean hashOk = Arrays.equals(calcHash, hash32); + + // 2) Проверка подписи Ed25519 + boolean sigOk = Ed25519Util.verify(preimage, signature64, publicKey32); + + if (!hashOk) log.warn("Hash mismatch: hash32 != SHA-256(preimage)"); + if (!sigOk) log.warn("Signature mismatch: Ed25519 verify failed"); + return hashOk && sigOk; + } catch (IllegalArgumentException ex) { + log.error("verifyAll: bad arguments", ex); + return false; + } + } + + /** Собрать канонический пре-имидж без длины логина. */ + public static byte[] buildPreimage(String userLogin, + long blockchainId, + byte[] prevHash32, + byte[] rawBytes) { + Objects.requireNonNull(userLogin, "userLogin"); + Objects.requireNonNull(prevHash32, "prevHash32"); + Objects.requireNonNull(rawBytes, "rawBytes"); + + byte[] loginUtf8 = userLogin.getBytes(StandardCharsets.UTF_8); + requireLen(prevHash32, 32, "prevHash32"); + + int capacity = loginUtf8.length + 8 + 32 + rawBytes.length; + ByteBuffer buf = ByteBuffer.allocate(capacity).order(ByteOrder.BIG_ENDIAN); + buf.put(loginUtf8); + buf.putLong(blockchainId); + buf.put(prevHash32); + buf.put(rawBytes); + return buf.array(); + } + + private static void requireLen(byte[] arr, int len, String name) { + if (arr == null) throw new IllegalArgumentException(name + " is null"); + if (arr.length != len) { + throw new IllegalArgumentException(name + " length != " + len + " (got " + arr.length + ")"); + } + } +} diff --git a/shine-server-crypto/src/main/java/utils/crypto/CryptoSelfTest.java b/shine-server-crypto/src/main/java/utils/crypto/CryptoSelfTest.java new file mode 100644 index 0000000..1e41a05 --- /dev/null +++ b/shine-server-crypto/src/main/java/utils/crypto/CryptoSelfTest.java @@ -0,0 +1,50 @@ +package utils.crypto; + +import java.nio.charset.StandardCharsets; +import java.util.Arrays; + +public final class CryptoSelfTest { + + private CryptoSelfTest() {} + + /** + * Простой запуск: убедиться, что всё собрано и работает. + * Выводит ключи в Base64, знак/проверка подписи — OK/FAIL. + */ + public static void main(String[] args) { + System.out.println("=== Ed25519 self-check ==="); + + // 1) Генерация ключей + byte[] priv = Ed25519Util.generatePrivateKey(); + byte[] pub = Ed25519Util.derivePublicKey(priv); + + // 2) Конвертация в/из Base64 (чисто для демонстрации) + String privB64 = Ed25519Util.keyToBase64(priv); + String pubB64 = Ed25519Util.keyToBase64(pub); + System.out.println("Private (seed) Base64: " + privB64); + System.out.println("Public Base64 : " + pubB64); + + byte[] priv2 = Ed25519Util.keyFromBase64(privB64); + byte[] pub2 = Ed25519Util.keyFromBase64(pubB64); + if (!Arrays.equals(priv, priv2) || !Arrays.equals(pub, pub2)) { + throw new IllegalStateException("Base64 ⇆ bytes дала несовпадение (не должно случаться)."); + } + + // 3) Подпись и проверка + byte[] data = "Привет, мир Ed25519!".getBytes(StandardCharsets.UTF_8); + byte[] sig = Ed25519Util.sign(data, priv); + + boolean ok = Ed25519Util.verify(data, sig, pub); + System.out.println("Verify OK? " + ok); + + // 4) Негативный тест: портим данные + byte[] bad = "Привет, мир Ed25519?".getBytes(StandardCharsets.UTF_8); + boolean shouldFail = Ed25519Util.verify(bad, sig, pub); + System.out.println("Verify on changed data (should be false): " + shouldFail); + + if (!ok || shouldFail) { + throw new IllegalStateException("Self-test failed."); + } + System.out.println("Self-test passed ✅"); + } +} diff --git a/shine-server-crypto/src/main/java/utils/crypto/Ed25519Util.java b/shine-server-crypto/src/main/java/utils/crypto/Ed25519Util.java new file mode 100644 index 0000000..06fb388 --- /dev/null +++ b/shine-server-crypto/src/main/java/utils/crypto/Ed25519Util.java @@ -0,0 +1,173 @@ +package utils.crypto; + +import org.bouncycastle.crypto.params.Ed25519PrivateKeyParameters; +import org.bouncycastle.crypto.params.Ed25519PublicKeyParameters; +import org.bouncycastle.crypto.signers.Ed25519Signer; + +import java.nio.charset.StandardCharsets; +import java.security.SecureRandom; +import java.util.Base64; +import java.util.Objects; + +/** + * =============================================================== + * Ed25519Util — статическая утилита для работы с подписями Ed25519 + * на базе Bouncy Castle (bcprov). Совместимо с Java 17. + * --------------------------------------------------------------- + * Возможности: + * • generatePrivateKey() — приватный ключ 32 байта (seed) из SecureRandom. + * • generatePrivateKeyFromString(String) — приватный ключ 32 байта из строки через SHA-256. + * • derivePublicKey(byte[32]) — публичный ключ 32 байта из приватного. + * • sign(byte[], byte[32]) — подпись 64 байта. + * • verify(byte[], byte[64], byte[32]) — проверка подписи (true/false). + * • keyToBase64(byte[32]) / keyFromBase64(String) — Base64 ⇆ ключ (ровно 32 байта). + *. + * Форматы: + * • Приватный ключ — 32-байтный seed Ed25519. + * • Публичный ключ — 32-байтный public key. + * • Подпись — 64 байта. + *. + * Важно: + * • Здесь используется «классический» Ed25519 (подпись сырых данных). + * Если нужен режим Ed25519ph (prehash), делай отдельный класс. + *. + * Зависимость (Gradle/Groovy): + * implementation 'org.bouncycastle:bcprov-jdk18on:1.78.1' + * =============================================================== + */ +public final class Ed25519Util { + + /** Длина приватного ключа (seed) в байтах. */ + public static final int PRIVATE_KEY_LEN = 32; + /** Длина публичного ключа в байтах. */ + public static final int PUBLIC_KEY_LEN = 32; + /** Длина подписи в байтах. */ + public static final int SIGNATURE_LEN = 64; + + // Запрещаем инстанцирование: только статические методы + private Ed25519Util() {} + + // ===== Надёжный генератор случайных чисел (ленивая инициализация) ===== + private static final SecureRandom SECURE_RANDOM = createSecureRandom(); + + private static SecureRandom createSecureRandom() { + try { + return SecureRandom.getInstanceStrong(); + } catch (Exception ignore) { + return new SecureRandom(); + } + } + + // ===================================================================== + // API + // ===================================================================== + + /** + * Сгенерировать приватный ключ (seed) Ed25519: 32 случайных байта. + */ + public static byte[] generatePrivateKey() { + byte[] seed = new byte[PRIVATE_KEY_LEN]; + SECURE_RANDOM.nextBytes(seed); + return seed; + } + + /** + * Сгенерировать приватный ключ (seed, 32 байта) из произвольной строки: + * строка → UTF-8 → SHA-256 → 32 байта. + * + * @param anyString любая строка (не null) + * @return массив 32 байта (seed) + */ + public static byte[] generatePrivateKeyFromString(String anyString) { + Objects.requireNonNull(anyString, "Строка для генерации приватного ключа не должна быть null"); + byte[] input = anyString.getBytes(StandardCharsets.UTF_8); + return HashSHA256Util.sha256(input); // ровно 32 байта + } + + /** + * Получить публичный ключ (32 байта) из приватного (seed, 32 байта). + */ + public static byte[] derivePublicKey(byte[] privateKey32) { + requireLength(privateKey32, PRIVATE_KEY_LEN, "приватного ключа (seed)"); + Ed25519PrivateKeyParameters priv = new Ed25519PrivateKeyParameters(privateKey32, 0); + Ed25519PublicKeyParameters pub = priv.generatePublicKey(); + return pub.getEncoded(); // 32 байта + } + + /** + * Подписать сырые данные (без предварительного хеширования) приватным ключом Ed25519. + * + * @param data данные для подписи (не null) + * @param privateKey32 приватный ключ (seed) 32 байта + * @return подпись длиной 64 байта + */ + public static byte[] sign(byte[] data, byte[] privateKey32) { + Objects.requireNonNull(data, "Данные для подписи не должны быть null"); + requireLength(privateKey32, PRIVATE_KEY_LEN, "приватного ключа (seed)"); + + Ed25519PrivateKeyParameters priv = new Ed25519PrivateKeyParameters(privateKey32, 0); + Ed25519Signer signer = new Ed25519Signer(); + signer.init(true, priv); + signer.update(data, 0, data.length); + byte[] signature = signer.generateSignature(); + if (signature == null || signature.length != SIGNATURE_LEN) { + throw new IllegalStateException("Ожидалась подпись длиной 64 байта."); + } + return signature; + } + + /** + * Проверить подпись Ed25519. + * + * @param data исходные данные + * @param signature64 подпись 64 байта + * @param publicKey32 публичный ключ 32 байта + * @return true, если подпись корректна для этих данных и ключа + */ + public static boolean verify(byte[] data, byte[] signature64, byte[] publicKey32) { + Objects.requireNonNull(data, "Данные для проверки подписи не должны быть null"); + requireLength(signature64, SIGNATURE_LEN, "подписи Ed25519"); + requireLength(publicKey32, PUBLIC_KEY_LEN, "публичного ключа"); + + Ed25519PublicKeyParameters pub = new Ed25519PublicKeyParameters(publicKey32, 0); + Ed25519Signer verifier = new Ed25519Signer(); + verifier.init(false, pub); + verifier.update(data, 0, data.length); + return verifier.verifySignature(signature64); + } + + /** + * Преобразовать 32-байтный ключ (приватный seed или публичный key) в Base64-строку. + */ + public static String keyToBase64(byte[] key32) { + requireLength(key32, 32, "ключа (ожидалось 32 байта)"); + return Base64.getEncoder().encodeToString(key32); + } + + /** + * Из Base64-строки получить 32-байтный ключ. + * @throws IllegalArgumentException если после декодирования длина ≠ 32 + */ + public static byte[] keyFromBase64(String base64) { + Objects.requireNonNull(base64, "Base64-строка не должна быть null"); + byte[] raw = Base64.getDecoder().decode(base64); + requireLength(raw, 32, "ключа после декодирования Base64 (ожидалось 32 байта)"); + return raw; + } + + // ===================================================================== + // ВСПОМОГАТЕЛЬНЫЕ + // ===================================================================== + + private static void requireLength(byte[] data, int expectedLen, String what) { + if (data == null) { + throw new IllegalArgumentException("Массив " + what + " не должен быть null."); + } + if (data.length != expectedLen) { + throw new IllegalArgumentException( + "Некорректная длина " + what + ": " + data.length + " байт(а). Ожидалось: " + expectedLen + "." + ); + } + } + +} diff --git a/shine-server-crypto/src/main/java/utils/crypto/HashSHA256Util.java b/shine-server-crypto/src/main/java/utils/crypto/HashSHA256Util.java new file mode 100644 index 0000000..f6b2051 --- /dev/null +++ b/shine-server-crypto/src/main/java/utils/crypto/HashSHA256Util.java @@ -0,0 +1,53 @@ +package utils.crypto; + +import org.bouncycastle.crypto.digests.SHA256Digest; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.charset.StandardCharsets; + +public final class HashSHA256Util { + private HashSHA256Util() {} + + /** Посчитать SHA-256 от всего массива. */ + public static byte[] sha256(byte[] data) { + if (data == null) throw new IllegalArgumentException("data == null"); + SHA256Digest d = new SHA256Digest(); + d.update(data, 0, data.length); + byte[] out = new byte[32]; + d.doFinal(out, 0); + return out; + } + + /** Получить loginId из строки логина. + * Алгоритм: + * - login -> UTF-8 bytes + * - SHA-256 + * - берём последние 8 байт (справа) + * - интерпретируем как signed long (BigEndian) + */ + public static long loginToLoginId(String login) { + if (login == null || login.isBlank()) + throw new IllegalArgumentException("login is null or empty"); + + byte[] hash = sha256(login.getBytes(StandardCharsets.UTF_8)); + + // последние 8 байт SHA-256 + return ByteBuffer.wrap(hash, 24, 8) + .order(ByteOrder.BIG_ENDIAN) + .getLong(); + } + + /** Инкрементальный SHA-256 (если нужно будет кормить по кускам). */ + public static final class Sha256 { + private final SHA256Digest d = new SHA256Digest(); + public Sha256 update(byte[] part) { + if (part != null) d.update(part, 0, part.length); + return this; + } + public byte[] doFinal() { + byte[] out = new byte[32]; + d.doFinal(out, 0); + return out; + } + } +} \ No newline at end of file diff --git a/shine-server-crypto/src/main/java/utils/crypto/README.md b/shine-server-crypto/src/main/java/utils/crypto/README.md new file mode 100644 index 0000000..3e132be --- /dev/null +++ b/shine-server-crypto/src/main/java/utils/crypto/README.md @@ -0,0 +1,30 @@ +# utils.crypto + +Пакет отвечает за криптографию — подписи и хэши блоков. +Используется при создании и проверке целостности `.bch`-блоков. + +--- + +## Классы + +### **Ed25519Util** +Работает с подписями Ed25519. +Методы: +- `generatePrivateKey()` — создать приватный ключ (32 байта) +- `generatePrivateKeyFromString(String)` — детерминированный ключ из строки +- `derivePublicKey(byte[32])` — получить публичный ключ (32 байта) +- `sign(byte[], byte[32])` — подписать данные (64-байтная подпись) +- `verify(byte[], byte[64], byte[32])` — проверить подпись + +### **HashUtil** +Вычисляет SHA-256. +Методы: +- `sha256(byte[])` → `[32]` — вернуть хэш массива +- `Sha256` — вложенный класс для пошагового хэширования + +### **BchCryptoVerifier** +Проверяет подпись и хэш блока перед записью в блокчейн. +Методы: +- `verifyAll(userLogin, blockchainId, prevHash32, rawBytes, signature64, hash32, publicKey32)` + → `true/false` — корректна ли подпись и хэш +- `buildPreimage(...)` — собирает байты, которые подписываются: diff --git a/shine-server-db/src/main/java/shine/db/SqliteDbController.java b/shine-server-db/src/main/java/shine/db/SqliteDbController.java index 4667e8f..420cc29 100644 --- a/shine-server-db/src/main/java/shine/db/SqliteDbController.java +++ b/shine-server-db/src/main/java/shine/db/SqliteDbController.java @@ -1,7 +1,5 @@ package shine.db; -import shine.db.dao.ActiveSessionsDAO; -import shine.db.entities.ActiveSession; import utils.config.AppConfig; import java.nio.file.Files; 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 a904e50..44b3fe0 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 @@ -1,7 +1,7 @@ package shine.db.dao; import shine.db.SqliteDbController; -import shine.db.entities.ActiveSession; +import shine.db.entities.ActiveSessionEntry; import java.sql.*; import java.util.ArrayList; @@ -53,7 +53,7 @@ public final class ActiveSessionsDAO { /** * Вставка новой сессии. */ - public void insert(ActiveSession session) throws SQLException { + public void insert(ActiveSessionEntry session) throws SQLException { String sql = """ INSERT INTO active_sessions ( sessionId, @@ -94,7 +94,7 @@ public final class ActiveSessionsDAO { /** * Получить сессию по sessionId. */ - public ActiveSession getBySessionId(String sessionId) throws SQLException { + public ActiveSessionEntry getBySessionId(String sessionId) throws SQLException { String sql = """ SELECT sessionId, @@ -128,7 +128,7 @@ public final class ActiveSessionsDAO { /** * Получить список всех активных сессий пользователя по loginId. */ - public List getByLoginId(long loginId) throws SQLException { + public List getByLoginId(long loginId) throws SQLException { String sql = """ SELECT sessionId, @@ -148,7 +148,7 @@ public final class ActiveSessionsDAO { WHERE loginId = ? """; - List result = new ArrayList<>(); + List result = new ArrayList<>(); try (PreparedStatement ps = db.getConnection().prepareStatement(sql)) { ps.setLong(1, loginId); @@ -235,7 +235,7 @@ public final class ActiveSessionsDAO { /** * Маппинг ResultSet → ActiveSession (все 13 полей). */ - private ActiveSession mapRow(ResultSet rs) throws SQLException { + private ActiveSessionEntry mapRow(ResultSet rs) throws SQLException { String sessionId = rs.getString("sessionId"); long loginId = rs.getLong("loginId"); String sessionPwd = rs.getString("sessionPwd"); @@ -250,7 +250,7 @@ public final class ActiveSessionsDAO { String clientInfoFromRequest = rs.getString("clientInfoFromRequest"); String userLanguage = rs.getString("userLanguage"); - return new ActiveSession( + return new ActiveSessionEntry( sessionId, loginId, sessionPwd, diff --git a/shine-server-db/src/main/java/shine/db/dao/SolanaUsersDAO.java b/shine-server-db/src/main/java/shine/db/dao/SolanaUsersDAO.java index b4a8eb3..f5fda48 100644 --- a/shine-server-db/src/main/java/shine/db/dao/SolanaUsersDAO.java +++ b/shine-server-db/src/main/java/shine/db/dao/SolanaUsersDAO.java @@ -1,7 +1,7 @@ package shine.db.dao; import shine.db.SqliteDbController; -import shine.db.entities.SolanaUser; +import shine.db.entities.SolanaUserEntry; import java.sql.*; import java.util.ArrayList; @@ -36,7 +36,7 @@ public final class SolanaUsersDAO { return instance; } - public void insert(SolanaUser user) throws SQLException { + public void insert(SolanaUserEntry user) throws SQLException { String sql = """ INSERT INTO solana_users (login, loginId, bchId, loginKey, deviceKey, bchLimit) VALUES (?, ?, ?, ?, ?, ?) @@ -59,7 +59,7 @@ public final class SolanaUsersDAO { } } - public SolanaUser getByLoginId(long loginId) throws SQLException { + public SolanaUserEntry getByLoginId(long loginId) throws SQLException { String sql = """ SELECT login, loginId, bchId, loginKey, deviceKey, bchLimit FROM solana_users @@ -76,7 +76,7 @@ public final class SolanaUsersDAO { } } - public SolanaUser getByLogin(String login) throws SQLException { + public SolanaUserEntry getByLogin(String login) throws SQLException { String sql = """ SELECT login, loginId, bchId, loginKey, deviceKey, bchLimit FROM solana_users @@ -93,7 +93,7 @@ public final class SolanaUsersDAO { } } - public List searchByLoginPrefix(String prefix) throws SQLException { + public List searchByLoginPrefix(String prefix) throws SQLException { String sql = """ SELECT login, loginId, bchId, loginKey, deviceKey, bchLimit FROM solana_users @@ -102,7 +102,7 @@ public final class SolanaUsersDAO { LIMIT 5 """; - List result = new ArrayList<>(); + List result = new ArrayList<>(); try (PreparedStatement ps = db.getConnection().prepareStatement(sql)) { ps.setString(1, prefix.toLowerCase() + "%"); @@ -115,8 +115,8 @@ public final class SolanaUsersDAO { return result; } - private SolanaUser mapRow(ResultSet rs) throws SQLException { - return new SolanaUser( + private SolanaUserEntry mapRow(ResultSet rs) throws SQLException { + return new SolanaUserEntry( rs.getLong("loginId"), rs.getString("login"), rs.getLong("bchId"), diff --git a/shine-server-db/src/main/java/shine/db/dao/UserParamsDAO.java b/shine-server-db/src/main/java/shine/db/dao/UserParamsDAO.java index 3afdf32..03f7560 100644 --- a/shine-server-db/src/main/java/shine/db/dao/UserParamsDAO.java +++ b/shine-server-db/src/main/java/shine/db/dao/UserParamsDAO.java @@ -1,7 +1,7 @@ package shine.db.dao; import shine.db.SqliteDbController; -import shine.db.entities.UserParam; +import shine.db.entities.UserParamEntry; import java.sql.*; import java.util.ArrayList; @@ -33,7 +33,7 @@ public final class UserParamsDAO { * Если запись существует -> обновляем поля. * Если нет -> вставляем новую запись. */ - public void upsert(UserParam param) throws SQLException { + public void upsert(UserParamEntry param) throws SQLException { String sql = """ INSERT INTO users_params ( loginId, @@ -68,7 +68,7 @@ public final class UserParamsDAO { /** * Получить параметр по loginId + param. */ - public UserParam getByUserIdAndParam(long loginId, String paramName) throws SQLException { + public UserParamEntry getByUserIdAndParam(long loginId, String paramName) throws SQLException { String sql = """ SELECT loginId, @@ -95,7 +95,7 @@ public final class UserParamsDAO { /** * Получить все параметры пользователя. */ - public List getByUserId(long loginId) throws SQLException { + public List getByUserId(long loginId) throws SQLException { String sql = """ SELECT loginId, @@ -110,7 +110,7 @@ public final class UserParamsDAO { ORDER BY time_ms DESC """; - List result = new ArrayList<>(); + List result = new ArrayList<>(); try (PreparedStatement ps = db.getConnection().prepareStatement(sql)) { ps.setLong(1, loginId); @@ -122,8 +122,8 @@ public final class UserParamsDAO { return result; } - private UserParam mapRow(ResultSet rs) throws SQLException { - return new UserParam( + private UserParamEntry mapRow(ResultSet rs) throws SQLException { + return new UserParamEntry( rs.getLong("loginId"), rs.getString("param"), rs.getLong("bch_channel_id"), diff --git a/shine-server-db/src/main/java/shine/db/entities/ActiveSession.java b/shine-server-db/src/main/java/shine/db/entities/ActiveSessionEntry.java similarity index 86% rename from shine-server-db/src/main/java/shine/db/entities/ActiveSession.java rename to shine-server-db/src/main/java/shine/db/entities/ActiveSessionEntry.java index 584b0c7..c90bf5f 100644 --- a/shine-server-db/src/main/java/shine/db/entities/ActiveSession.java +++ b/shine-server-db/src/main/java/shine/db/entities/ActiveSessionEntry.java @@ -20,7 +20,7 @@ package shine.db.entities; * FOREIGN KEY (loginId) REFERENCES solana_users(loginId) * ); */ -public class ActiveSession { +public class ActiveSessionEntry { private String sessionId; // TEXT base64(32 bytes) private long loginId; // INTEGER @@ -38,22 +38,22 @@ public class ActiveSession { private String clientInfoFromRequest; // строка, собранная на сервере private String userLanguage; // prefer-language (например, "ru-RU") - public ActiveSession() { + public ActiveSessionEntry() { } - public ActiveSession(String sessionId, - long loginId, - String sessionPwd, - String storagePwd, - long sessionCreatedAtMs, - long lastAuthirificatedAtMs, - String pushEndpoint, - String pushP256dhKey, - String pushAuthKey, - String clientIp, - String clientInfoFromClient, - String clientInfoFromRequest, - String userLanguage) { + public ActiveSessionEntry(String sessionId, + long loginId, + String sessionPwd, + String storagePwd, + long sessionCreatedAtMs, + long lastAuthirificatedAtMs, + String pushEndpoint, + String pushP256dhKey, + String pushAuthKey, + String clientIp, + String clientInfoFromClient, + String clientInfoFromRequest, + String userLanguage) { this.sessionId = sessionId; this.loginId = loginId; this.sessionPwd = sessionPwd; diff --git a/shine-server-db/src/main/java/shine/db/entities/SolanaUser.java b/shine-server-db/src/main/java/shine/db/entities/SolanaUserEntry.java similarity index 86% rename from shine-server-db/src/main/java/shine/db/entities/SolanaUser.java rename to shine-server-db/src/main/java/shine/db/entities/SolanaUserEntry.java index 333a8d0..4796e0c 100644 --- a/shine-server-db/src/main/java/shine/db/entities/SolanaUser.java +++ b/shine-server-db/src/main/java/shine/db/entities/SolanaUserEntry.java @@ -10,7 +10,7 @@ package shine.db.entities; * - deviceKey — публичный ключ устройства (второй ключ); * - bchLimit — лимит по количеству блоков / размеру цепочки (может быть null). */ -public class SolanaUser { +public class SolanaUserEntry { private long loginId; private String login; @@ -19,15 +19,15 @@ public class SolanaUser { private String deviceKey; // раньше pubkey1 private Integer bchLimit; // может быть null - public SolanaUser() { + public SolanaUserEntry() { } - public SolanaUser(long loginId, - String login, - long bchId, - String loginKey, - String deviceKey, - Integer bchLimit) { + public SolanaUserEntry(long loginId, + String login, + long bchId, + String loginKey, + String deviceKey, + Integer bchLimit) { this.loginId = loginId; this.login = login; this.bchId = bchId; diff --git a/shine-server-db/src/main/java/shine/db/entities/UserParam.java b/shine-server-db/src/main/java/shine/db/entities/UserParamEntry.java similarity index 83% rename from shine-server-db/src/main/java/shine/db/entities/UserParam.java rename to shine-server-db/src/main/java/shine/db/entities/UserParamEntry.java index 0e6195a..0d6db78 100644 --- a/shine-server-db/src/main/java/shine/db/entities/UserParam.java +++ b/shine-server-db/src/main/java/shine/db/entities/UserParamEntry.java @@ -1,6 +1,6 @@ package shine.db.entities; -public class UserParam { +public class UserParamEntry { private long loginId; private String param; @@ -10,16 +10,16 @@ public class UserParam { private short pubkeyNum; private String signature; - public UserParam() { + public UserParamEntry() { } - public UserParam(long loginId, - String param, - long bchChannelId, - String value, - long timeMs, - short pubkeyNum, - String signature) { + public UserParamEntry(long loginId, + String param, + long bchChannelId, + String value, + long timeMs, + short pubkeyNum, + String signature) { this.loginId = loginId; this.param = param; this.bchChannelId = bchChannelId; diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/ConnectionContext.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/ConnectionContext.java index 2649e40..deef4a4 100644 --- a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/ConnectionContext.java +++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/ConnectionContext.java @@ -1,8 +1,8 @@ package server.logic.ws_protocol.JSON; import org.eclipse.jetty.websocket.api.Session; -import shine.db.entities.SolanaUser; -import shine.db.entities.ActiveSession; +import shine.db.entities.SolanaUserEntry; +import shine.db.entities.ActiveSessionEntry; /** * ConnectionContext — контекст состояния одного WebSocket-соединения. @@ -16,10 +16,10 @@ public class ConnectionContext { public static final int AUTH_STATUS_USER = 2; // авторизованный пользователь // Полный пользователь из БД (solana_users) - private SolanaUser solanaUser; + private SolanaUserEntry solanaUserEntry; // Активная сессия из БД (active_sessions) - private ActiveSession activeSession; + private ActiveSessionEntry activeSessionEntry; /** * Идентификатор сессии — base64-строка от 32 байт. @@ -61,30 +61,30 @@ public class ConnectionContext { // --- SolanaUser / ActiveSession --- - public SolanaUser getSolanaUser() { - return solanaUser; + public SolanaUserEntry getSolanaUser() { + return solanaUserEntry; } - public void setSolanaUser(SolanaUser solanaUser) { - this.solanaUser = solanaUser; + public void setSolanaUser(SolanaUserEntry solanaUserEntry) { + this.solanaUserEntry = solanaUserEntry; } - public ActiveSession getActiveSession() { - return activeSession; + public ActiveSessionEntry getActiveSession() { + return activeSessionEntry; } - public void setActiveSession(ActiveSession activeSession) { - this.activeSession = activeSession; + public void setActiveSession(ActiveSessionEntry activeSessionEntry) { + this.activeSessionEntry = activeSessionEntry; } // --- Удобные геттеры для логина --- public String getLogin() { - return solanaUser != null ? solanaUser.getLogin() : null; + return solanaUserEntry != null ? solanaUserEntry.getLogin() : null; } public Long getLoginId() { - return solanaUser != null ? solanaUser.getLoginId() : null; + return solanaUserEntry != null ? solanaUserEntry.getLoginId() : null; } // --- sessionId / sessionPwd --- @@ -134,8 +134,8 @@ public class ConnectionContext { } public void reset() { - solanaUser = null; - activeSession = null; + solanaUserEntry = null; + activeSessionEntry = null; sessionId = null; sessionPwd = null; diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/entyties/Blockchain/Net_AddBlock_new_Request.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/entyties/Blockchain/Net_AddBlock_new_Request.java new file mode 100644 index 0000000..c2ae59a --- /dev/null +++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/entyties/Blockchain/Net_AddBlock_new_Request.java @@ -0,0 +1,38 @@ +package server.logic.ws_protocol.JSON.entyties.Blockchain; + +import server.logic.ws_protocol.JSON.entyties.Net_Request; + +public class Net_AddBlock_new_Request extends Net_Request { + + private long blockchainId; + + private int globalNumber; + private String prevGlobalHash; // HEX(64) or "" + + private int lineNumber; // 0..7 + private int lineBlockNumber; + private String prevLineHash; // HEX(64) or "" + + private String blockBase64; // base64url of raw .bch bytes + + public long getBlockchainId() { return blockchainId; } + public void setBlockchainId(long blockchainId) { this.blockchainId = blockchainId; } + + public int getGlobalNumber() { return globalNumber; } + public void setGlobalNumber(int globalNumber) { this.globalNumber = globalNumber; } + + public String getPrevGlobalHash() { return prevGlobalHash; } + public void setPrevGlobalHash(String prevGlobalHash) { this.prevGlobalHash = prevGlobalHash; } + + public int getLineNumber() { return lineNumber; } + public void setLineNumber(int lineNumber) { this.lineNumber = lineNumber; } + + public int getLineBlockNumber() { return lineBlockNumber; } + public void setLineBlockNumber(int lineBlockNumber) { this.lineBlockNumber = lineBlockNumber; } + + public String getPrevLineHash() { return prevLineHash; } + public void setPrevLineHash(String prevLineHash) { this.prevLineHash = prevLineHash; } + + public String getBlockBase64() { return blockBase64; } + public void setBlockBase64(String blockBase64) { this.blockBase64 = blockBase64; } +} \ No newline at end of file diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/entyties/Blockchain/Net_AddBlock_new_Response.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/entyties/Blockchain/Net_AddBlock_new_Response.java new file mode 100644 index 0000000..b0a219e --- /dev/null +++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/entyties/Blockchain/Net_AddBlock_new_Response.java @@ -0,0 +1,29 @@ +package server.logic.ws_protocol.JSON.entyties.Blockchain; + +import server.logic.ws_protocol.JSON.entyties.Net_Response; + +public class Net_AddBlock_new_Response extends Net_Response { + + private int serverLastGlobalNumber; + private String serverLastGlobalHash; + + private int serverLastLineNumber; + private String serverLastLineHash; + + private String reasonCode; // "OUT_OF_SEQUENCE", "HASH_MISMATCH", ... + + public int getServerLastGlobalNumber() { return serverLastGlobalNumber; } + public void setServerLastGlobalNumber(int v) { this.serverLastGlobalNumber = v; } + + public String getServerLastGlobalHash() { return serverLastGlobalHash; } + public void setServerLastGlobalHash(String v) { this.serverLastGlobalHash = v; } + + public int getServerLastLineNumber() { return serverLastLineNumber; } + public void setServerLastLineNumber(int v) { this.serverLastLineNumber = v; } + + public String getServerLastLineHash() { return serverLastLineHash; } + public void setServerLastLineHash(String v) { this.serverLastLineHash = v; } + + public String getReasonCode() { return reasonCode; } + public void setReasonCode(String reasonCode) { this.reasonCode = reasonCode; } +} \ 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 1312f69..4c6077d 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 @@ -8,7 +8,7 @@ import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler; import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory; import server.logic.ws_protocol.WireCodes; import shine.db.dao.SolanaUsersDAO; -import shine.db.entities.SolanaUser; +import shine.db.entities.SolanaUserEntry; import java.security.SecureRandom; import java.util.Base64; @@ -49,9 +49,9 @@ public class Net_AuthChallenge_Handler implements JsonMessageHandler { } // 2) Ищем пользователя в локальной БД - SolanaUser solanaUser = SolanaUsersDAO.getInstance().getByLogin(login); + SolanaUserEntry solanaUserEntry = SolanaUsersDAO.getInstance().getByLogin(login); - if (solanaUser == null) { + if (solanaUserEntry == null) { return NetExceptionResponseFactory.error( req, WireCodes.Status.UNVERIFIED, @@ -61,7 +61,7 @@ public class Net_AuthChallenge_Handler implements JsonMessageHandler { } // 3) Заполняем контекст пользователем - ctx.setSolanaUser(solanaUser); + ctx.setSolanaUser(solanaUserEntry); // 3.1) Отмечаем, что по этому соединению начата авторификация ctx.setAuthenticationStatus(ConnectionContext.AUTH_STATUS_AUTH_IN_PROGRESS); diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/Net_CloseActiveSession_Handler.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/Net_CloseActiveSession_Handler.java index de1c55e..1b91849 100644 --- a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/Net_CloseActiveSession_Handler.java +++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/Net_CloseActiveSession_Handler.java @@ -13,8 +13,8 @@ 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.entities.ActiveSession; -import shine.db.entities.SolanaUser; +import shine.db.entities.ActiveSessionEntry; +import shine.db.entities.SolanaUserEntry; import java.sql.SQLException; @@ -62,7 +62,7 @@ public class Net_CloseActiveSession_Handler implements JsonMessageHandler { ); } - SolanaUser user = ctx.getSolanaUser(); + SolanaUserEntry user = ctx.getSolanaUser(); long currentLoginId = user.getLoginId(); int authStatus = ctx.getAuthenticationStatus(); @@ -158,7 +158,7 @@ public class Net_CloseActiveSession_Handler implements JsonMessageHandler { } ActiveSessionsDAO sessionsDao = ActiveSessionsDAO.getInstance(); - ActiveSession targetSession; + ActiveSessionEntry targetSession; try { targetSession = sessionsDao.getBySessionId(targetSessionId); } catch (SQLException e) { 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 00847ec..3125f61 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,8 @@ 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.entities.ActiveSession; -import shine.db.entities.SolanaUser; +import shine.db.entities.ActiveSessionEntry; +import shine.db.entities.SolanaUserEntry; import shine.geo.ClientInfoService; import shine.geo.GeoLookupService; import utils.crypto.Ed25519Util; @@ -72,7 +72,7 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler { * @throws IllegalArgumentException при некорректном base64 ключа/подписи */ public static boolean verifyAuthorificatedSignature( - SolanaUser user, + SolanaUserEntry user, String authNonce, long timeMs, String signatureB64 @@ -108,7 +108,7 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler { return err; } - SolanaUser user = ctx.getSolanaUser(); + SolanaUserEntry user = ctx.getSolanaUser(); Long loginId = user.getLoginId(); if (loginId == null) { Net_Response err = NetExceptionResponseFactory.error( @@ -237,10 +237,10 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler { // --- создаём запись ActiveSession и сохраняем в БД --- ActiveSessionsDAO dao = ActiveSessionsDAO.getInstance(); - ActiveSession activeSession; + ActiveSessionEntry activeSessionEntry; try { - activeSession = new ActiveSession( + activeSessionEntry = new ActiveSessionEntry( sessionId, loginId, newSessionPwd, // настоящий секрет сессии @@ -256,7 +256,7 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler { userLanguage ); - dao.insert(activeSession); + dao.insert(activeSessionEntry); } catch (SQLException e) { log.error("Ошибка БД при создании новой сессии для loginId={}", loginId, e); Net_Response err = NetExceptionResponseFactory.error( @@ -270,7 +270,7 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler { } // --- обновляем контекст --- - ctx.setActiveSession(activeSession); + ctx.setActiveSession(activeSessionEntry); ctx.setSessionId(sessionId); ctx.setSessionPwd(newSessionPwd); // теперь в контексте хранится секрет сессии ctx.setAuthNonce(null); // одноразовый nonce больше не нужен diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/Net_ListSessions_Handler.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/Net_ListSessions_Handler.java index 291ef93..99d1ce0 100644 --- a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/Net_ListSessions_Handler.java +++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/Net_ListSessions_Handler.java @@ -12,8 +12,8 @@ import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler; import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory; import server.logic.ws_protocol.WireCodes; import shine.db.dao.ActiveSessionsDAO; -import shine.db.entities.ActiveSession; -import shine.db.entities.SolanaUser; +import shine.db.entities.ActiveSessionEntry; +import shine.db.entities.SolanaUserEntry; import shine.geo.GeoLookupService; import java.sql.SQLException; @@ -50,7 +50,7 @@ public class Net_ListSessions_Handler implements JsonMessageHandler { ); } - SolanaUser user = ctx.getSolanaUser(); + SolanaUserEntry user = ctx.getSolanaUser(); long currentLoginId = user.getLoginId(); int authStatus = ctx.getAuthenticationStatus(); @@ -128,7 +128,7 @@ public class Net_ListSessions_Handler implements JsonMessageHandler { } // 3) Тянем все активные сессии пользователя из БД - List sessions; + List sessions; try { sessions = ActiveSessionsDAO.getInstance().getByLoginId(currentLoginId); } catch (SQLException e) { @@ -143,7 +143,7 @@ public class Net_ListSessions_Handler implements JsonMessageHandler { // 4) Собираем DTO с геолокацией List resultList = new ArrayList<>(); - for (ActiveSession s : sessions) { + for (ActiveSessionEntry s : sessions) { SessionInfo info = new SessionInfo(); info.setSessionId(s.getSessionId()); info.setClientInfoFromClient(s.getClientInfoFromClient()); 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 799fce8..53a8cdb 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 @@ -13,8 +13,8 @@ import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory; import server.logic.ws_protocol.WireCodes; import shine.db.dao.ActiveSessionsDAO; import shine.db.dao.SolanaUsersDAO; -import shine.db.entities.ActiveSession; -import shine.db.entities.SolanaUser; +import shine.db.entities.ActiveSessionEntry; +import shine.db.entities.SolanaUserEntry; import shine.geo.ClientInfoService; import shine.geo.GeoLookupService; @@ -63,7 +63,7 @@ public class Net_RefreshSession_Handler implements JsonMessageHandler { } ActiveSessionsDAO sessionsDao = ActiveSessionsDAO.getInstance(); - ActiveSession session; + ActiveSessionEntry session; try { session = sessionsDao.getBySessionId(sessionId); } catch (SQLException e) { @@ -96,11 +96,11 @@ public class Net_RefreshSession_Handler implements JsonMessageHandler { } // --- вытаскиваем пользователя по loginId --- - SolanaUser solanaUser = null; + SolanaUserEntry solanaUserEntry = null; long loginId = session.getLoginId(); try { SolanaUsersDAO usersDao = SolanaUsersDAO.getInstance(); - solanaUser = usersDao.getByLoginId(loginId); + solanaUserEntry = usersDao.getByLoginId(loginId); } catch (SQLException e) { log.error("Ошибка БД при поиске пользователя по loginId={} из сессии", loginId, e); return NetExceptionResponseFactory.error( @@ -111,7 +111,7 @@ public class Net_RefreshSession_Handler implements JsonMessageHandler { ); } - if (solanaUser == null) { + if (solanaUserEntry == null) { return NetExceptionResponseFactory.error( req, WireCodes.Status.UNVERIFIED, @@ -171,7 +171,7 @@ public class Net_RefreshSession_Handler implements JsonMessageHandler { // --- обновляем контекст соединения --- if (ctx != null) { ctx.setActiveSession(session); - ctx.setSolanaUser(solanaUser); + ctx.setSolanaUser(solanaUserEntry); ctx.setSessionId(sessionId); ctx.setSessionPwd(sessionPwd); ctx.setAuthenticationStatus(ConnectionContext.AUTH_STATUS_USER); diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/blockchain/Net_AddBlock_new_Handler.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/blockchain/Net_AddBlock_new_Handler.java new file mode 100644 index 0000000..41108c1 --- /dev/null +++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/blockchain/Net_AddBlock_new_Handler.java @@ -0,0 +1,190 @@ +package server.logic.ws_protocol.JSON.handlers.blockchain; + +import blockchain.BchBlockEntry; +import blockchain.BodyRecordParser; +import blockchain.body.BodyRecord; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import server.logic.ws_protocol.JSON.ConnectionContext; +import server.logic.ws_protocol.JSON.entyties.Net_Request; +import server.logic.ws_protocol.JSON.entyties.Net_Response; +import server.logic.ws_protocol.JSON.entyties.Blockchain.Net_AddBlock_new_Request; +import server.logic.ws_protocol.JSON.entyties.Blockchain.Net_AddBlock_new_Response; +import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler; +import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory; +import server.logic.ws_protocol.WireCodes; +import shine.db.dao.BlockchainStateDAO; +import shine.db.entities.BlockchainStateEntry; +import utils.crypto.BchCryptoVerifier; +import utils.files.FileStoreUtil; + +import java.util.Base64; + +public class Net_AddBlock_new_Handler implements JsonMessageHandler { + + private static final Logger log = LoggerFactory.getLogger(Net_AddBlock_new_Handler.class); + + @Override + public Net_Response handle(Net_Request baseReq, ConnectionContext ctx) throws Exception { + + Net_AddBlock_new_Request req = (Net_AddBlock_new_Request) baseReq; + + // 0) базовые проверки + if (req.getBlockchainId() <= 0) { + return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "BAD_BLOCKCHAIN_ID", "blockchainId <= 0"); + } + if (req.getGlobalNumber() < 0) { + return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "BAD_GLOBAL_NUMBER", "globalNumber < 0"); + } + if (req.getLineNumber() < 0 || req.getLineNumber() > 7) { + return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "BAD_LINE_NUMBER", "lineNumber must be 0..7"); + } + if (req.getLineBlockNumber() < 0) { + return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "BAD_LINE_BLOCK_NUMBER", "lineBlockNumber < 0"); + } + if (req.getBlockBase64() == null || req.getBlockBase64().isBlank()) { + return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "EMPTY_BLOCK", "blockBase64 is empty"); + } + + // 1) грузим состояние из БД + BlockchainStateDAO dao = BlockchainStateDAO.getInstance(); + BlockchainStateEntry state = dao.getByBlockchainId(req.getBlockchainId()); + if (state == null) { + // на MVP можно: запретить добавление, пока цепочка не создана отдельно + // либо разрешить только genesis/header — как ты делал раньше + return NetExceptionResponseFactory.error(req, WireCodes.Status.CHAIN_NOT_FOUND, "CHAIN_NOT_FOUND", "chain not found in DB"); + } + + // 2) быстрые проверки на “подходит ли блок” + int expectedGlobal = state.getLastGlobalNumber() + 1; + int expectedLine = state.getLastLineNumber(req.getLineNumber()) + 1; + + String dbPrevGlobalHash = nn(state.getLastGlobalHash()); + String dbPrevLineHash = nn(state.getLastLineHash(req.getLineNumber())); + + if (req.getGlobalNumber() != expectedGlobal) { + return outOfSeq(req, state, req.getLineNumber(), "OUT_OF_SEQUENCE_GLOBAL"); + } + if (!eqHash(req.getPrevGlobalHash(), dbPrevGlobalHash)) { + return outOfSeq(req, state, req.getLineNumber(), "GLOBAL_HASH_MISMATCH"); + } + if (req.getLineBlockNumber() != expectedLine) { + return outOfSeq(req, state, req.getLineNumber(), "OUT_OF_SEQUENCE_LINE"); + } + if (!eqHash(req.getPrevLineHash(), dbPrevLineHash)) { + return outOfSeq(req, state, req.getLineNumber(), "LINE_HASH_MISMATCH"); + } + + // 3) декодируем блок + byte[] fullBlockBytes; + try { + fullBlockBytes = Base64.getUrlDecoder().decode(req.getBlockBase64()); + } catch (IllegalArgumentException e) { + return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "BAD_BASE64", "blockBase64 decode failed"); + } + + // 4) парсим .bch + BchBlockEntry block; + try { + block = new BchBlockEntry(fullBlockBytes); + } catch (Exception e) { + return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "BAD_BLOCK_FORMAT", "cannot parse BchBlockEntry"); + } + + // 5) ПОЛНАЯ валидация: подпись/хэш/тело + // ⚠️ ниже я оставляю общий вызов verifyAll как у тебя раньше, + // но теперь prevHash берём из БД, а publicKey — из state (или из solana_users). + byte[] prevHashGlobal32 = hexToBytes32(dbPrevGlobalHash); + + boolean verified = BchCryptoVerifier.verifyAll( + state.getUserLogin(), + req.getBlockchainId(), + prevHashGlobal32, + block.rawBytes, + block.getSignature64(), + block.getHash32(), + Base64.getDecoder().decode(state.getPublicKeyBase64()) + ); + + if (!verified) { + return NetExceptionResponseFactory.error(req, WireCodes.Status.UNVERIFIED, "UNVERIFIED", "signature/hash verification failed"); + } + + // Проверка тела блока + BodyRecord body = BodyRecordParser.parse(block.recordType, block.recordTypeVersion, block.body).check(); + + // 6) TODO: извлечь lineNumber/lineBlockNumber/prevLineHash из body (если они реально в теле есть) + // и сверить с req + DB. Сейчас оставляю как “крючок”. + // BlockLineMeta meta = BlockLineMetaExtractor.extract(body); + // if (meta.lineNumber != req.getLineNumber()) ... + // if (meta.lineBlockNumber != req.getLineBlockNumber()) ... + // if (!eqHash(meta.prevLineHashHex, dbPrevLineHash)) ... + + // 7) запись в файл (фактическое хранение блоков) + FileStoreUtil.getInstance().addDataToBlockchain(req.getBlockchainId(), fullBlockBytes); + + // 8) TODO: обновление состояния в БД (вместо BchInfoManager) + // - state.sizeBytes += fullBlockBytes.length + // - state.lastGlobalNumber = req.globalNumber + // - state.lastGlobalHash = bytesToHex(block.getHash32()) + // - state.lineX_last_number/hash обновить по lineNumber + // - state.updatedAtMs = now + // dao.upsert(state); + + // 9) ответ OK + Net_AddBlock_new_Response resp = new Net_AddBlock_new_Response(); + resp.setOp(req.getOp()); + resp.setRequestId(req.getRequestId()); + resp.setStatus(WireCodes.Status.OK); + + // можно вернуть “новое” состояние, но на MVP вернём хотя бы серверные last’ы до апдейта/после апдейта + resp.setServerLastGlobalNumber(req.getGlobalNumber()); + resp.setServerLastGlobalHash(bytesToHex(block.getHash32())); + resp.setServerLastLineNumber(req.getLineBlockNumber()); + resp.setServerLastLineHash(resp.getServerLastGlobalHash()); + resp.setReasonCode(null); + + return resp; + } + + private static Net_AddBlock_new_Response outOfSeq(Net_AddBlock_new_Request req, BlockchainStateEntry state, int line, String reason) { + Net_AddBlock_new_Response resp = new Net_AddBlock_new_Response(); + resp.setOp(req.getOp()); + resp.setRequestId(req.getRequestId()); + resp.setStatus(WireCodes.Status.OUT_OF_SEQUENCE); // или свой статус + resp.setReasonCode(reason); + + resp.setServerLastGlobalNumber(state.getLastGlobalNumber()); + resp.setServerLastGlobalHash(nn(state.getLastGlobalHash())); + + resp.setServerLastLineNumber(state.getLastLineNumber(line)); + resp.setServerLastLineHash(nn(state.getLastLineHash(line))); + + return resp; + } + + private static boolean eqHash(String a, String b) { + return nn(a).equalsIgnoreCase(nn(b)); + } + + private static String nn(String s) { return s == null ? "" : s.trim(); } + + private static byte[] hexToBytes32(String hex) { + hex = nn(hex); + if (hex.isEmpty()) return new byte[32]; + int len = hex.length(); + byte[] out = new byte[len / 2]; + for (int i = 0; i < len; i += 2) out[i / 2] = (byte) Integer.parseInt(hex.substring(i, i + 2), 16); + if (out.length == 32) return out; + byte[] full = new byte[32]; + int copy = Math.min(out.length, 32); + System.arraycopy(out, out.length - copy, full, 32 - copy, copy); + return full; + } + + private static String bytesToHex(byte[] b) { + StringBuilder sb = new StringBuilder(b.length * 2); + for (byte x : b) sb.append(String.format("%02x", x)); + return sb.toString(); + } +} \ No newline at end of file diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/tempToTest/Net_AddUser_Handler.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/tempToTest/Net_AddUser_Handler.java index 9aeb84b..9effece 100644 --- a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/tempToTest/Net_AddUser_Handler.java +++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/tempToTest/Net_AddUser_Handler.java @@ -11,7 +11,7 @@ import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler; import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory; import server.logic.ws_protocol.WireCodes; import shine.db.dao.SolanaUsersDAO; -import shine.db.entities.SolanaUser; +import shine.db.entities.SolanaUserEntry; import java.sql.SQLException; @@ -61,7 +61,7 @@ public class Net_AddUser_Handler implements JsonMessageHandler { try { SolanaUsersDAO dao = SolanaUsersDAO.getInstance(); - SolanaUser user = new SolanaUser( + SolanaUserEntry user = new SolanaUserEntry( req.getLoginId(), req.getLogin(), req.getBchId(), diff --git a/src/main/java/server/ws/WsServer.java b/src/main/java/server/ws/WsServer.java index 3d03b9b..ded7929 100644 --- a/src/main/java/server/ws/WsServer.java +++ b/src/main/java/server/ws/WsServer.java @@ -5,8 +5,6 @@ import org.eclipse.jetty.servlet.ServletContextHandler; import org.eclipse.jetty.websocket.server.config.JettyWebSocketServletContainerInitializer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import shine.db.dao.SolanaUsersDAO; -import shine.db.entities.SolanaUser; import utils.config.AppConfig; import java.time.Duration;