ver 0.0.1

This commit is contained in:
2026-08-09 23:47:39 +03:00
parent 411b00c3ee
commit 8c431c1742
6 changed files with 506 additions and 3 deletions

View File

@@ -1,4 +1,7 @@
library; library;
// Экспортируем наш 6-битный кодировщик export 'src/encoding/compact_text_encoder.dart'; //Кодировщик
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'; //Упаковщик данных в модель и распаковщик данных из нее

View File

@@ -0,0 +1,58 @@
import 'dart:typed_data';
import 'package:cryptography/cryptography.dart';
/// Сервис асимметричной криптографии Ed25519
class CryptoService {
static final Ed25519 _algorithm = Ed25519();
/// Генерация новой пары ключей
static Future<SimpleKeyPair> generateKeyPair() async {
return await _algorithm.newKeyPair();
}
/// Получение сырых 32 байт публичного ключа из объекта SimpleKeyPair
static Future<Uint8List> getPublicKeyBytes(SimpleKeyPair keyPair) async {
final SimplePublicKey publicKey = await keyPair.extractPublicKey();
return Uint8List.fromList(publicKey.bytes);
}
/// Создание цифровой подписи (64 байта)
static Future<Uint8List> sign({
required List<int> 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<bool> verify({
required List<int> message,
required List<int> signatureBytes,
required List<int> 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,
);
}
}

View File

@@ -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;
}
}

View File

@@ -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<int> 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<int> 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;
}
}

View File

@@ -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<int> 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,
);
}
}

View File

@@ -1,9 +1,10 @@
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:potato_pass_core/potato_pass_core.dart'; import 'package:potato_pass_core/potato_pass_core.dart';
import 'package:cryptography/cryptography.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
void main() { void main() {
group('CompactTextEncoder Tests', () { group('6-и битный колдировщик', () {
test('Кодирование и декодирование русской строки', () { test('Кодирование и декодирование русской строки', () {
const input = 'ИВАНОВ ИВАН ИВАНОВИЧ'; const input = 'ИВАНОВ ИВАН ИВАНОВИЧ';
@@ -50,4 +51,281 @@ void main() {
expect(decoded, equals('АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯABCDEFGHIJKLMNOPQRSTUVWXYZ .-,')); 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<int> 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<int> 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<int> 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<int> 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<int> 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<int> restoredChallengeMessage = [...challengeSalt, ...restoredPass.securityDepartmentSignature];
final bool isClientSignatureValid = await CryptoService.verify(
message: restoredChallengeMessage,
signatureBytes: restoredPass.challengeSignature,
publicKeyBytes: restoredPass.clientPublicKey,
);
expect(isClientSignatureValid, isTrue);
});
});
} }