Промежуточная версия
This commit is contained in:
AidarKC
2025-12-17 13:06:08 +03:00
parent ab44cc5282
commit eaf1affb27
30 changed files with 926 additions and 563 deletions
@@ -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 + ")");
}
}
}
@@ -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 ✅");
}
}
@@ -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 + "."
);
}
}
}
@@ -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;
}
}
}
@@ -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(...)` — собирает байты, которые подписываются: