From 8c431c17428d93fca876556dcede229a00c6a09b Mon Sep 17 00:00:00 2001 From: fegor101 Date: Sun, 9 Aug 2026 23:47:39 +0300 Subject: [PATCH] ver 0.0.1 --- lib/potato_pass_core.dart | 7 +- lib/src/crypto/crypto_service.dart | 58 +++++ lib/src/crypto/salt_generator.dart | 20 ++ lib/src/models/pass_payload.dart | 79 ++++++ lib/src/serializer/binary_serializer.dart | 65 +++++ test/potato_pass_core_test.dart | 280 +++++++++++++++++++++- 6 files changed, 506 insertions(+), 3 deletions(-) create mode 100644 lib/src/crypto/crypto_service.dart create mode 100644 lib/src/crypto/salt_generator.dart create mode 100644 lib/src/models/pass_payload.dart create mode 100644 lib/src/serializer/binary_serializer.dart diff --git a/lib/potato_pass_core.dart b/lib/potato_pass_core.dart index bb163d7..e0fd53e 100644 --- a/lib/potato_pass_core.dart +++ b/lib/potato_pass_core.dart @@ -1,4 +1,7 @@ library; -// Экспортируем наш 6-битный кодировщик -export 'src/encoding/compact_text_encoder.dart'; \ No newline at end of file +export 'src/encoding/compact_text_encoder.dart'; //Кодировщик +export 'src/crypto/salt_generator.dart'; //Генератор солей +export 'src/crypto/crypto_service.dart'; //Создание и проверка подписей +export 'src/models/pass_payload.dart'; //Модель данных пропуска +export 'src/serializer/binary_serializer.dart'; //Упаковщик данных в модель и распаковщик данных из нее \ No newline at end of file diff --git a/lib/src/crypto/crypto_service.dart b/lib/src/crypto/crypto_service.dart new file mode 100644 index 0000000..aa22c7f --- /dev/null +++ b/lib/src/crypto/crypto_service.dart @@ -0,0 +1,58 @@ +import 'dart:typed_data'; +import 'package:cryptography/cryptography.dart'; + +/// Сервис асимметричной криптографии Ed25519 +class CryptoService { + static final Ed25519 _algorithm = Ed25519(); + + /// Генерация новой пары ключей + static Future generateKeyPair() async { + return await _algorithm.newKeyPair(); + } + + /// Получение сырых 32 байт публичного ключа из объекта SimpleKeyPair + static Future getPublicKeyBytes(SimpleKeyPair keyPair) async { + + final SimplePublicKey publicKey = await keyPair.extractPublicKey(); + return Uint8List.fromList(publicKey.bytes); + } + + /// Создание цифровой подписи (64 байта) + static Future sign({ + required List message, + required SimpleKeyPair keyPair, + }) async { + final Signature signature = await _algorithm.sign( + message, + keyPair: keyPair, + ); + return Uint8List.fromList(signature.bytes); + } + + /// Проверка цифровой подписи [signatureBytes] (64 байта) + /// для сообщения [message] с помощью публичного ключа [publicKeyBytes] (32 байта) + static Future verify({ + required List message, + required List signatureBytes, + required List publicKeyBytes, + }) async { + if (signatureBytes.length != 64 || publicKeyBytes.length != 32) { + return false; + } + + final SimplePublicKey publicKey = SimplePublicKey( + publicKeyBytes, + type: KeyPairType.ed25519, + ); + + final Signature signature = Signature( + signatureBytes, + publicKey: publicKey, + ); + + return await _algorithm.verify( + message, + signature: signature, + ); + } +} \ No newline at end of file diff --git a/lib/src/crypto/salt_generator.dart b/lib/src/crypto/salt_generator.dart new file mode 100644 index 0000000..9a7d49c --- /dev/null +++ b/lib/src/crypto/salt_generator.dart @@ -0,0 +1,20 @@ +import 'dart:math'; +import 'dart:typed_data'; + + +abstract class SaltGenerator { + // Стандартный размер соли: 16 байт (128 бит) + static const int defaultSize = 16; + + // Генерирует случайный массив байт указанной длины + static Uint8List generate([int length = defaultSize]) { + final Random secureRandom = Random.secure(); + final Uint8List salt = Uint8List(length); + + for (int i = 0; i < length; i++) { + salt[i] = secureRandom.nextInt(256); + } + + return salt; + } +} \ No newline at end of file diff --git a/lib/src/models/pass_payload.dart b/lib/src/models/pass_payload.dart new file mode 100644 index 0000000..ccd0364 --- /dev/null +++ b/lib/src/models/pass_payload.dart @@ -0,0 +1,79 @@ +import 'dart:typed_data'; +import 'package:potato_pass_core/src/encoding/compact_text_encoder.dart'; + +/// Модель данных пропуска PotatoPass +class PassPayload { + final String lastName; + final String firstName; + final String middleName; + final String position; + final String department; + + /// Битовая маска зон доступа + final Uint8List zoneMask; + + /// 32 байта публичного ключа клиента (он же уникальный ID пропуска) + final Uint8List clientPublicKey; + + /// 64 байта подписи Службы Безопасности (Security Department Signature) + final Uint8List securityDepartmentSignature; + + /// 64 байта подписи Соли + final Uint8List challengeSignature; + + PassPayload({ + required this.lastName, + required this.firstName, + required this.middleName, + required this.position, + required this.department, + required this.zoneMask, + required this.clientPublicKey, + required this.securityDepartmentSignature, + required this.challengeSignature, + }); + + /// Проверяет, разрешен ли проход в конкретную зону по её индексу + bool hasZoneAccess(int zoneIndex) { + final int byteIndex = zoneIndex ~/ 8; + final int bitIndex = zoneIndex % 8; + if (byteIndex >= zoneMask.length) return false; + return (zoneMask[byteIndex] & (1 << bitIndex)) != 0; + } + + /// Возвращает байты тела пропуска (Текст + Зоны + PubKey) + Uint8List getSignedBodyBytes() { + final List bytes = []; + + void writeField(String text) { + final Uint8List encoded = CompactTextEncoder.encode(text); + bytes.add(encoded.length); + bytes.addAll(encoded); + } + + writeField(lastName); + writeField(firstName); + writeField(middleName); + writeField(position); + writeField(department); + + bytes.add(zoneMask.length); + bytes.addAll(zoneMask); + bytes.addAll(clientPublicKey); + + return Uint8List.fromList(bytes); + } + + /// Вспомогательный статический метод для генерации битовой маски зон + static Uint8List createZoneMask(List activeZoneIndices, {int byteCount = 16}) { + final mask = Uint8List(byteCount); + for (final zone in activeZoneIndices) { + final int byteIndex = zone ~/ 8; + final int bitIndex = zone % 8; + if (byteIndex < byteCount) { + mask[byteIndex] |= (1 << bitIndex); + } + } + return mask; + } +} \ No newline at end of file diff --git a/lib/src/serializer/binary_serializer.dart b/lib/src/serializer/binary_serializer.dart new file mode 100644 index 0000000..06cd1cf --- /dev/null +++ b/lib/src/serializer/binary_serializer.dart @@ -0,0 +1,65 @@ +import 'dart:typed_data'; +import 'package:potato_pass_core/src/encoding/compact_text_encoder.dart'; +import 'package:potato_pass_core/src/models/pass_payload.dart'; + +/// Утилита для упаковки PassPayload в бинарный массив и обратно +abstract class BinarySerializer { + /// Упаковывает модель PassPayload в бинарный массив Uint8List + static Uint8List serialize(PassPayload pass) { + final List bytes = []; + + //Добавляем тело (Текст + Зоны + PubKey) + bytes.addAll(pass.getSignedBodyBytes()); + + //Добавляем 64 байта подписи Службы Безопасности + bytes.addAll(pass.securityDepartmentSignature); + + //Добавляем 64 байта подписи Соли + bytes.addAll(pass.challengeSignature); + + return Uint8List.fromList(bytes); + } + + /// Распаковывает байтовый массив обратно в объект PassPayload + static PassPayload deserialize(Uint8List bytes) { + int offset = 0; + + String readField() { + final int length = bytes[offset++]; + final Uint8List fieldBytes = Uint8List.sublistView(bytes, offset, offset + length); + offset += length; + return CompactTextEncoder.decode(fieldBytes); + } + + final String lastName = readField(); + final String firstName = readField(); + final String middleName = readField(); + final String position = readField(); + final String department = readField(); + + final int zoneMaskLength = bytes[offset++]; + final Uint8List zoneMask = Uint8List.sublistView(bytes, offset, offset + zoneMaskLength); + offset += zoneMaskLength; + + final Uint8List clientPublicKey = Uint8List.sublistView(bytes, offset, offset + 32); + offset += 32; + + final Uint8List securityDepartmentSignature = Uint8List.sublistView(bytes, offset, offset + 64); + offset += 64; + + final Uint8List challengeSignature = Uint8List.sublistView(bytes, offset, offset + 64); + offset += 64; + + return PassPayload( + lastName: lastName, + firstName: firstName, + middleName: middleName, + position: position, + department: department, + zoneMask: zoneMask, + clientPublicKey: clientPublicKey, + securityDepartmentSignature: securityDepartmentSignature, + challengeSignature: challengeSignature, + ); + } +} \ No newline at end of file diff --git a/test/potato_pass_core_test.dart b/test/potato_pass_core_test.dart index 551b6f7..1d2ad07 100644 --- a/test/potato_pass_core_test.dart +++ b/test/potato_pass_core_test.dart @@ -1,9 +1,10 @@ import 'dart:typed_data'; import 'package:potato_pass_core/potato_pass_core.dart'; +import 'package:cryptography/cryptography.dart'; import 'package:test/test.dart'; void main() { - group('CompactTextEncoder Tests', () { + group('6-и битный колдировщик', () { test('Кодирование и декодирование русской строки', () { const input = 'ИВАНОВ ИВАН ИВАНОВИЧ'; @@ -50,4 +51,281 @@ void main() { expect(decoded, equals('АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯABCDEFGHIJKLMNOPQRSTUVWXYZ .-,')); }); }); + + group('Тест полного цикла', () { + test('Нормальные данные', () async { + // 1. Создаем ключи СБ и Клиента + final SimpleKeyPair sbKeyPair = await CryptoService.generateKeyPair(); + final Uint8List sbPubKey = await CryptoService.getPublicKeyBytes(sbKeyPair); + + final SimpleKeyPair clientKeyPair = await CryptoService.generateKeyPair(); + final Uint8List clientPubKey = await CryptoService.getPublicKeyBytes(clientKeyPair); + + // 2. СБ создает маску зон доступа (разрешаем зоны 1, 5, 10) + final Uint8List zoneMask = PassPayload.createZoneMask([1, 5, 10]); + + // 3. Формируем тело пропуска для СБ + final passWithoutSignatures = PassPayload( + lastName: 'Иванов', + firstName: 'Иван', + middleName: 'Иванович', + position: 'Инженер', + department: 'Отдел ИТ', + zoneMask: zoneMask, + clientPublicKey: clientPubKey, + securityDepartmentSignature: Uint8List(64), + challengeSignature: Uint8List(64), + ); + + // 4. Служба Безопасности подписывает тело пропуска + final Uint8List sbSignedBodyBytes = passWithoutSignatures.getSignedBodyBytes(); + final Uint8List securityDepartmentSignature = await CryptoService.sign( + message: sbSignedBodyBytes, + keyPair: sbKeyPair, + ); + + // 5. Контролер/Сервер дает соль + final Uint8List challengeSalt = SaltGenerator.generate(16); + + // 6. Клиент подписывает (Соль + Подпись СБ) своим приватным ключом + final List challengeMessage = [...challengeSalt, ...securityDepartmentSignature]; + final Uint8List challengeSignature = await CryptoService.sign( + message: challengeMessage, + keyPair: clientKeyPair, + ); + + // 7. Собираем итоговый рабочий пропуск + final PassPayload fullPass = PassPayload( + lastName: 'Иванов', + firstName: 'Иван', + middleName: 'Иванович', + position: 'Инженер', + department: 'Отдел ИТ', + zoneMask: zoneMask, + clientPublicKey: clientPubKey, + securityDepartmentSignature: securityDepartmentSignature, + challengeSignature: challengeSignature, + ); + + // 8. СЕРИАЛИЗАЦИЯ В БИТЫ + final Uint8List binaryPayload = BinarySerializer.serialize(fullPass); + + // 9. ДЕСЕРИАЛИЗАЦИЯ ИЗ БИТОВ + final PassPayload restoredPass = BinarySerializer.deserialize(binaryPayload); + + // --- ПРОВЕРКА ДАННЫХ --- + expect(restoredPass.lastName, equals('ИВАНОВ')); + expect(restoredPass.firstName, equals('ИВАН')); + expect(restoredPass.position, equals('ИНЖЕНЕР')); + expect(restoredPass.department, equals('ОТДЕЛ ИТ')); + + // --- ПРОВЕРКА ЗОН ДОСТУПА --- + expect(restoredPass.hasZoneAccess(1), isTrue); + expect(restoredPass.hasZoneAccess(5), isTrue); + expect(restoredPass.hasZoneAccess(10), isTrue); + expect(restoredPass.hasZoneAccess(2), isFalse); + + // --- ПРОВЕРКА КРИПТОГРАФИИ (1): ВАЛИДНОСТЬ ПОДПИСИ СЛУЖБЫ БЕЗОПАСНОСТИ --- + final bool isSecuritySignatureValid = await CryptoService.verify( + message: restoredPass.getSignedBodyBytes(), + signatureBytes: restoredPass.securityDepartmentSignature, + publicKeyBytes: sbPubKey, + ); + expect(isSecuritySignatureValid, isTrue); + + // --- ПРОВЕРКА КРИПТОГРАФИИ (2): ВАЛИДНОСТЬ ПОДПИСИ СОЛИ КЛИЕНТОМ --- + final List restoredChallengeMessage = [...challengeSalt, ...restoredPass.securityDepartmentSignature]; + final bool isClientSignatureValid = await CryptoService.verify( + message: restoredChallengeMessage, + signatureBytes: restoredPass.challengeSignature, + publicKeyBytes: restoredPass.clientPublicKey, + ); + expect(isClientSignatureValid, isTrue); + }); + + test('Пустые строки', () async { + // 1. Создаем ключи СБ и Клиента + final SimpleKeyPair sbKeyPair = await CryptoService.generateKeyPair(); + final Uint8List sbPubKey = await CryptoService.getPublicKeyBytes(sbKeyPair); + + final SimpleKeyPair clientKeyPair = await CryptoService.generateKeyPair(); + final Uint8List clientPubKey = await CryptoService.getPublicKeyBytes(clientKeyPair); + + // 2. СБ создает маску зон доступа (разрешаем зоны 1, 5, 10) + final Uint8List zoneMask = PassPayload.createZoneMask([1, 5, 10]); + + // 3. Формируем тело пропуска для СБ + final passWithoutSignatures = PassPayload( + lastName: '', + firstName: '', + middleName: '', + position: '', + department: '', + zoneMask: zoneMask, + clientPublicKey: clientPubKey, + securityDepartmentSignature: Uint8List(64), + challengeSignature: Uint8List(64), + ); + + // 4. Служба Безопасности подписывает тело пропуска + final Uint8List sbSignedBodyBytes = passWithoutSignatures.getSignedBodyBytes(); + final Uint8List securityDepartmentSignature = await CryptoService.sign( + message: sbSignedBodyBytes, + keyPair: sbKeyPair, + ); + + // 5. Контролер/Сервер дает соль + final Uint8List challengeSalt = SaltGenerator.generate(16); + + // 6. Клиент подписывает (Соль + Подпись СБ) своим приватным ключом + final List challengeMessage = [...challengeSalt, ...securityDepartmentSignature]; + final Uint8List challengeSignature = await CryptoService.sign( + message: challengeMessage, + keyPair: clientKeyPair, + ); + + // 7. Собираем итоговый рабочий пропуск + final PassPayload fullPass = PassPayload( + lastName: '', + firstName: '', + middleName: '', + position: '', + department: '', + zoneMask: zoneMask, + clientPublicKey: clientPubKey, + securityDepartmentSignature: securityDepartmentSignature, + challengeSignature: challengeSignature, + ); + + // 8. СЕРИАЛИЗАЦИЯ В БИТЫ + final Uint8List binaryPayload = BinarySerializer.serialize(fullPass); + + // 9. ДЕСЕРИАЛИЗАЦИЯ ИЗ БИТОВ + final PassPayload restoredPass = BinarySerializer.deserialize(binaryPayload); + + // --- ПРОВЕРКА ДАННЫХ --- + expect(restoredPass.lastName, equals('')); + expect(restoredPass.firstName, equals('')); + expect(restoredPass.position, equals('')); + expect(restoredPass.department, equals('')); + + // --- ПРОВЕРКА ЗОН ДОСТУПА --- + expect(restoredPass.hasZoneAccess(1), isTrue); + expect(restoredPass.hasZoneAccess(5), isTrue); + expect(restoredPass.hasZoneAccess(10), isTrue); + expect(restoredPass.hasZoneAccess(2), isFalse); + + // --- ПРОВЕРКА КРИПТОГРАФИИ (1): ВАЛИДНОСТЬ ПОДПИСИ СЛУЖБЫ БЕЗОПАСНОСТИ --- + final bool isSecuritySignatureValid = await CryptoService.verify( + message: restoredPass.getSignedBodyBytes(), + signatureBytes: restoredPass.securityDepartmentSignature, + publicKeyBytes: sbPubKey, + ); + expect(isSecuritySignatureValid, isTrue); + + // --- ПРОВЕРКА КРИПТОГРАФИИ (2): ВАЛИДНОСТЬ ПОДПИСИ СОЛИ КЛИЕНТОМ --- + final List restoredChallengeMessage = [...challengeSalt, ...restoredPass.securityDepartmentSignature]; + final bool isClientSignatureValid = await CryptoService.verify( + message: restoredChallengeMessage, + signatureBytes: restoredPass.challengeSignature, + publicKeyBytes: restoredPass.clientPublicKey, + ); + expect(isClientSignatureValid, isTrue); + }); + + test('Пустые зоны', () async { + // 1. Создаем ключи СБ и Клиента + final SimpleKeyPair sbKeyPair = await CryptoService.generateKeyPair(); + final Uint8List sbPubKey = await CryptoService.getPublicKeyBytes(sbKeyPair); + + final SimpleKeyPair clientKeyPair = await CryptoService.generateKeyPair(); + final Uint8List clientPubKey = await CryptoService.getPublicKeyBytes(clientKeyPair); + + // 2. Заполняем пустые зоны + final Uint8List zoneMask = PassPayload.createZoneMask([]); + + // 3. Формируем тело пропуска для СБ + final passWithoutSignatures = PassPayload( + lastName: 'Иванов', + firstName: 'Иван', + middleName: 'Иванович', + position: 'Инженер', + department: 'Отдел ИТ', + zoneMask: zoneMask, + clientPublicKey: clientPubKey, + securityDepartmentSignature: Uint8List(64), + challengeSignature: Uint8List(64), + ); + + // 4. Служба Безопасности подписывает тело пропуска + final Uint8List sbSignedBodyBytes = passWithoutSignatures.getSignedBodyBytes(); + final Uint8List securityDepartmentSignature = await CryptoService.sign( + message: sbSignedBodyBytes, + keyPair: sbKeyPair, + ); + + // 5. Контролер/Сервер дает соль + final Uint8List challengeSalt = SaltGenerator.generate(16); + + // 6. Клиент подписывает (Соль + Подпись СБ) своим приватным ключом + final List challengeMessage = [...challengeSalt, ...securityDepartmentSignature]; + final Uint8List challengeSignature = await CryptoService.sign( + message: challengeMessage, + keyPair: clientKeyPair, + ); + + // 7. Собираем итоговый рабочий пропуск + final PassPayload fullPass = PassPayload( + lastName: 'Иванов', + firstName: 'Иван', + middleName: 'Иванович', + position: 'Инженер', + department: 'Отдел ИТ', + zoneMask: zoneMask, + clientPublicKey: clientPubKey, + securityDepartmentSignature: securityDepartmentSignature, + challengeSignature: challengeSignature, + ); + + // 8. СЕРИАЛИЗАЦИЯ В БИТЫ + final Uint8List binaryPayload = BinarySerializer.serialize(fullPass); + + // 9. ДЕСЕРИАЛИЗАЦИЯ ИЗ БИТОВ + final PassPayload restoredPass = BinarySerializer.deserialize(binaryPayload); + + // --- ПРОВЕРКА ДАННЫХ --- + expect(restoredPass.lastName, equals('ИВАНОВ')); + expect(restoredPass.firstName, equals('ИВАН')); + expect(restoredPass.position, equals('ИНЖЕНЕР')); + expect(restoredPass.department, equals('ОТДЕЛ ИТ')); + + // --- ПРОВЕРКА ЗОН ДОСТУПА --- + expect(restoredPass.hasZoneAccess(1), isFalse); + expect(restoredPass.hasZoneAccess(2), isFalse); + expect(restoredPass.hasZoneAccess(3), isFalse); + expect(restoredPass.hasZoneAccess(4), isFalse); + expect(restoredPass.hasZoneAccess(5), isFalse); + expect(restoredPass.hasZoneAccess(6), isFalse); + expect(restoredPass.hasZoneAccess(7), isFalse); + + // --- ПРОВЕРКА КРИПТОГРАФИИ (1): ВАЛИДНОСТЬ ПОДПИСИ СЛУЖБЫ БЕЗОПАСНОСТИ --- + final bool isSecuritySignatureValid = await CryptoService.verify( + message: restoredPass.getSignedBodyBytes(), + signatureBytes: restoredPass.securityDepartmentSignature, + publicKeyBytes: sbPubKey, + ); + expect(isSecuritySignatureValid, isTrue); + + // --- ПРОВЕРКА КРИПТОГРАФИИ (2): ВАЛИДНОСТЬ ПОДПИСИ СОЛИ КЛИЕНТОМ --- + final List restoredChallengeMessage = [...challengeSalt, ...restoredPass.securityDepartmentSignature]; + final bool isClientSignatureValid = await CryptoService.verify( + message: restoredChallengeMessage, + signatureBytes: restoredPass.challengeSignature, + publicKeyBytes: restoredPass.clientPublicKey, + ); + expect(isClientSignatureValid, isTrue); + }); + + }); + } \ No newline at end of file