Chepuhagram/lib/domain/services/crypto_service.dart

555 lines
18 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'package:cryptography/cryptography.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'dart:convert';
import 'dart:typed_data';
import 'package:flutter/foundation.dart';
import 'package:chepuhagram/data/models/contact_model.dart';
import 'dart:async';
import 'package:pointycastle/export.dart' as pc;
import 'dart:io';
class CryptoService {
final _storage = const FlutterSecureStorage();
final algorithm = X25519();
final aesGcm = AesGcm.with256bits();
Future<Map<String, String>> initAccountSecurity(String masterPassword) async {
// Генерируем пару X25519 ключей
final keyPair = await algorithm.newKeyPair();
final publicKey = await keyPair.extractPublicKey();
final privateKeyBytes = await keyPair.extractPrivateKeyBytes();
// Сохраняем приватный ключ в Secure Storage
await _storage.write(
key: 'private_key',
value: base64Encode(privateKeyBytes),
);
// Шифруем приватный ключ с мастер-паролем (AES-GCM)
final masterKey = await _deriveKeyFromPassword(masterPassword);
final nonce = aesGcm.newNonce();
final encrypted = await aesGcm.encrypt(
privateKeyBytes,
secretKey: masterKey,
nonce: nonce,
);
// Комбинируем nonce и зашифрованные данные
final encryptedData = nonce + encrypted.mac.bytes + encrypted.cipherText;
final encryptedPrivateKey = base64Encode(encryptedData);
final publicKeyBase64 = base64Encode(publicKey.bytes);
return {
'public_key': publicKeyBase64,
'encrypted_private_key': encryptedPrivateKey,
};
}
Future<String> decryptPrivateKey(
String encryptedPrivateKey,
String masterPassword,
) async {
try {
final encryptedData = base64Decode(encryptedPrivateKey);
// Разделяем nonce и зашифрованные данные
final nonce = encryptedData.sublist(0, 12); // GCM nonce = 12 bytes
final macBytes = encryptedData.sublist(12, 28);
final cipherText = encryptedData.sublist(28);
final masterKey = await _deriveKeyFromPassword(masterPassword);
final decrypted = await aesGcm.decrypt(
SecretBox(cipherText, nonce: nonce, mac: Mac(macBytes)),
secretKey: masterKey,
);
return base64Encode(decrypted);
} catch (e) {
throw Exception('Неверный мастер-пароль или поврежденные данные');
}
}
Future<String> encryptPrivateKeyWithPassword(
String privateKeyBase64,
String masterPassword,
) async {
final privateKeyBytes = base64Decode(privateKeyBase64);
final masterKey = await _deriveKeyFromPassword(masterPassword);
final nonce = aesGcm.newNonce();
final encrypted = await aesGcm.encrypt(
privateKeyBytes,
secretKey: masterKey,
nonce: nonce,
);
final encryptedData = nonce + encrypted.mac.bytes + encrypted.cipherText;
return base64Encode(encryptedData);
}
Future<SecretKey> _deriveKeyFromPassword(String password) async {
final pbkdf2 = Pbkdf2(
macAlgorithm: Hmac.sha256(),
iterations: 10000,
bits: 256,
);
final salt = utf8.encode('chepuhagram_salt');
return await pbkdf2.deriveKeyFromPassword(password: password, nonce: salt);
}
Future<SecretKey> deriveSharedSecret(
String myPrivateKeyBase64,
String theirPublicKeyBase64,
) async {
final myKeyPair = await algorithm.newKeyPairFromSeed(
base64Decode(myPrivateKeyBase64),
);
final theirPublicKey = SimplePublicKey(
base64Decode(theirPublicKeyBase64),
type: KeyPairType.x25519,
);
return await algorithm.sharedSecretKey(
keyPair: myKeyPair,
remotePublicKey: theirPublicKey,
);
}
Future<String> encryptMessage(String text, SecretKey sharedKey) async {
final nonce = aesGcm.newNonce();
final encrypted = await aesGcm.encrypt(
utf8.encode(text),
secretKey: sharedKey,
nonce: nonce,
);
// Сохраняем Nonce + MAC + CipherText для передачи
return base64Encode(nonce + encrypted.mac.bytes + encrypted.cipherText);
}
static Future<String> decryptInIsolate(
String base64Data,
SecretKey sharedKey,
) async {
final data = base64Decode(base64Data);
final aesGcm = AesGcm.with256bits();
final nonce = data.sublist(0, 12);
final mac = data.sublist(12, 28);
final cipherText = data.sublist(28);
final decrypted = await aesGcm.decrypt(
SecretBox(cipherText, nonce: nonce, mac: Mac(mac)),
secretKey: sharedKey,
);
return utf8.decode(decrypted);
}
static Future<List<Contact>> bulkDecryptContacts(
Map<String, dynamic> data,
) async {
final List<Contact> contacts = data['contacts'];
final String privKey = data['privKey'];
final Map<int, SecretKey> cache = data['cache'];
final x25519 = X25519();
final aesGcm = AesGcm.with256bits();
final List<Contact> result = [];
// Вычисляем свою пару один раз
final myKeyPair = await x25519.newKeyPairFromSeed(base64Decode(privKey));
for (var contact in contacts) {
if (contact.lastMessage == null || contact.publicKey == null) {
result.add(contact);
continue;
}
try {
SecretKey sharedKey;
if (cache.containsKey(contact.id)) {
sharedKey = cache[contact.id]!;
} else {
final theirPubKey = SimplePublicKey(
base64Decode(contact.publicKey!),
type: KeyPairType.x25519,
);
sharedKey = await x25519.sharedSecretKey(
keyPair: myKeyPair,
remotePublicKey: theirPubKey,
);
}
// Дешифровка AES-GCM
final msgData = base64Decode(contact.lastMessage!);
final decrypted = await aesGcm.decrypt(
SecretBox(
msgData.sublist(28),
nonce: msgData.sublist(0, 12),
mac: Mac(msgData.sublist(12, 28)),
),
secretKey: sharedKey,
);
result.add(
contact.copyWith(
lastMessage: utf8.decode(decrypted),
isLastMsgDecrypted: true,
avatarFileId: contact.avatarFileId,
avatarUrl: contact.avatarUrl,
),
);
} catch (e) {
result.add(
contact.copyWith(
lastMessage: '[не удалось расшифровать: $e]',
isLastMsgDecrypted: true,
avatarFileId: contact.avatarFileId,
avatarUrl: contact.avatarUrl,
),
);
}
}
return result;
}
static Future<Map<int, List<int>>> computeSharedKeysTask(
Map<String, dynamic> params,
) async {
final Map<int, String> isolateKeysMap = params['keysMap'];
final String isolatePrivKey = params['privKey'];
final x25519 = X25519();
final Map<int, List<int>> result = {};
final myKeyPair = await x25519.newKeyPairFromSeed(
base64Decode(isolatePrivKey),
);
for (var entry in isolateKeysMap.entries) {
try {
final theirPubKey = SimplePublicKey(
base64Decode(entry.value),
type: KeyPairType.x25519,
);
final sharedKey = await x25519.sharedSecretKey(
keyPair: myKeyPair,
remotePublicKey: theirPubKey,
);
result[entry.key] = await sharedKey.extractBytes();
} catch (_) {
continue;
}
}
return result;
}
Future<(Stream<List<int>>, String)> encryptFileStream(
Stream<List<int>> fileStream,
SecretKey sharedKey, {
void Function(int processed, int total)? onProgress,
int? totalSize,
}) async {
// 1. Генерируем уникальный ключ для конкретного файла
final SecretKey fileKey = await aesGcm.newSecretKey();
final List<int> fileKeyBytes = await fileKey.extractBytes();
// 2. Шифруем этот ключ файла на общем ключе чата (sharedKey)
final keyNonce = aesGcm.newNonce();
final encryptedKeyBox = await aesGcm.encrypt(
fileKeyBytes,
secretKey: sharedKey,
nonce: keyNonce,
);
// Кодируем зашифрованный ключ в Base64 для сервера
final String encryptedKeyForServer = base64Encode(
encryptedKeyBox.concatenation(),
);
int processedBytes = 0;
final int total = totalSize ?? 0;
// 3. Создаем асинхронный генератор для поблочного шифрования самого файла
Stream<List<int>> processEncryption() async* {
final List<int> buffer = [];
const int chunkSize = 64 * 1024; // 64 KB
await for (final chunk in fileStream) {
buffer.addAll(chunk);
while (buffer.length >= chunkSize) {
final plainBlock = Uint8List.fromList(buffer.sublist(0, chunkSize));
buffer.removeRange(0, chunkSize);
final blockNonce = aesGcm.newNonce();
final secretBox = await aesGcm.encrypt(
plainBlock,
secretKey: fileKey,
nonce: blockNonce,
);
final payload = secretBox.concatenation();
final header = ByteData(4)..setUint32(0, payload.length);
yield header.buffer.asUint8List();
yield payload;
processedBytes += chunkSize;
if (onProgress != null) {
onProgress(processedBytes, total);
}
}
}
// Дозаписываем остаток файла (хвост), если он есть
if (buffer.isNotEmpty) {
final plainBlock = Uint8List.fromList(buffer);
final blockNonce = aesGcm.newNonce();
final secretBox = await aesGcm.encrypt(
plainBlock,
secretKey: fileKey,
nonce: blockNonce,
);
final payload = secretBox.concatenation();
final header = ByteData(4)..setUint32(0, payload.length);
yield header.buffer.asUint8List();
yield payload;
}
}
// Возвращаем кортеж (Record): очищенный зашифрованный поток данных и ключ для сервера
return (processEncryption(), encryptedKeyForServer);
}
Stream<List<int>> decryptFileStream(
Stream<List<int>> encryptedStream,
SecretKey sharedKey,
String encryptedFileKey, {
int? totalBytes,
void Function(int processed, int total)? onProgress,
}) async* {
try {
// 1. Дешифруем ключ файла с помощью общего ключа чата
final encryptedKeyBytes = base64Decode(encryptedFileKey);
final keySecretBox = SecretBox.fromConcatenation(
encryptedKeyBytes,
nonceLength: 12,
macLength: 16,
);
final fileKeyBytes = await aesGcm.decrypt(
keySecretBox,
secretKey: sharedKey,
);
final fileKey = SecretKey(fileKeyBytes);
final List<int> buffer = [];
int blocksDecrypted = 0;
int totalProcessedBytes = 0;
// 2. Потоковая дешифровка блоков файла
await for (final chunk in encryptedStream) {
buffer.addAll(chunk);
while (true) {
if (buffer.length < 4) break;
final headerBytes = Uint8List.fromList(buffer.sublist(0, 4));
final int payloadLength = ByteData.sublistView(
headerBytes,
).getUint32(0);
// Проверяем: если длина чанка подозрительно огромная (из-за неверного формата файла)
if (payloadLength > 500 * 1024 || payloadLength <= 0) {
print(
"ОШИБКА: Неверный заголовок длины чанка: $payloadLength байт. Возможно, файл зашифрован старым методом!",
);
throw Exception("Неверный формат зашифрованного блока");
}
if (buffer.length < 4 + payloadLength) break;
final encryptedBlockBytes = Uint8List.fromList(
buffer.sublist(4, 4 + payloadLength),
);
buffer.removeRange(0, 4 + payloadLength);
final blockSecretBox = SecretBox.fromConcatenation(
encryptedBlockBytes,
nonceLength: 12,
macLength: 16,
);
final decryptedBlock = await aesGcm.decrypt(
blockSecretBox,
secretKey: fileKey,
);
blocksDecrypted++;
if (blocksDecrypted % 10 == 0 || payloadLength < 64 * 1024) {
print(
"Дешифровано блоков: $blocksDecrypted. Текущий размер: ${decryptedBlock.length} байт. Всего обработано $totalProcessedBytes. Всего $totalBytes",
);
}
// Увеличиваем счетчик обработанных зашифрованных байт
totalProcessedBytes += 4 + payloadLength;
// Вызываем колбэк прогресса
if (onProgress != null) {
// Передаем, сколько байт обработано, и общий размер (если totalBytes null, передаем -1)
onProgress(totalProcessedBytes, totalBytes ?? -1);
}
yield decryptedBlock;
}
}
print(
"ПОТОК ДЕШИФРАЦИИ ЗАВЕРШЕН ПОЛНОСТЬЮ. Всего блоков: $blocksDecrypted",
);
} catch (e, stack) {
print("КРИТИЧЕСКАЯ ОШИБКА ВНУТРИ КРИПТОСТРИМА: $e");
print(stack);
rethrow;
}
}
Future<Uint8List?> decryptAesKey(
String encryptedKey,
SecretKey sharedKey,
) async {
try {
final keyBytes = base64Decode(encryptedKey);
final nonce = keyBytes.sublist(0, 12);
final cipherText = keyBytes.sublist(12, keyBytes.length - 16);
final mac = keyBytes.sublist(keyBytes.length - 16);
final decrypted = await aesGcm.decrypt(
SecretBox(cipherText, nonce: nonce, mac: Mac(mac)),
secretKey: sharedKey,
);
return Uint8List.fromList(decrypted);
} catch (e) {
print('Ошибка дешифровки AES ключа: $e');
return null;
}
}
Future<String?> encryptAesKey(List<int> keyBytes, SecretKey sharedKey) async {
try {
final encrypted = await aesGcm.encrypt(keyBytes, secretKey: sharedKey);
return base64Encode(encrypted.concatenation());
} catch (e) {
print('Ошибка шифрования AES ключа: $e');
return null;
}
}
Future<Uint8List?> decryptMedia(
List<int> encryptedData,
String encryptedKey,
SecretKey sharedKey,
) async {
try {
final keyBytes = base64Decode(encryptedKey);
final keyNonce = keyBytes.sublist(0, 12);
final keyCipher = keyBytes.sublist(12, keyBytes.length - 16);
final keyMac = keyBytes.sublist(keyBytes.length - 16);
final decryptedFileKey = await aesGcm.decrypt(
SecretBox(keyCipher, nonce: keyNonce, mac: Mac(keyMac)),
secretKey: sharedKey,
);
final fileSecretKey = SecretKey(decryptedFileKey);
final nonce = encryptedData.sublist(0, 12);
final cipherText = encryptedData.sublist(12, encryptedData.length - 16);
final mac = encryptedData.sublist(encryptedData.length - 16);
final decryptedBytes = await aesGcm.decrypt(
SecretBox(cipherText, nonce: nonce, mac: Mac(mac)),
secretKey: fileSecretKey,
);
return Uint8List.fromList(decryptedBytes);
} catch (e) {
print('Ошибка дешифровки медиа: $e');
return null;
}
}
Future<SecretKey?> _decryptFileKey(
String encryptedFileKey,
SecretKey sharedKey,
) async {
try {
final keyBytes = base64Decode(encryptedFileKey);
final nonce = keyBytes.sublist(0, 12);
final macBytes = keyBytes.sublist(keyBytes.length - 16);
final cipherText = keyBytes.sublist(12, keyBytes.length - 16);
final decrypted = await aesGcm.decrypt(
SecretBox(cipherText, nonce: nonce, mac: Mac(macBytes)),
secretKey: sharedKey,
);
return SecretKey(decrypted);
} catch (e) {
print('Error decrypting file key: $e');
return null;
}
}
Future<String> decryptMessage(String base64Data, SecretKey sharedKey) async {
final data = base64Decode(base64Data);
final nonce = data.sublist(0, 12);
final mac = data.sublist(12, 28);
final cipherText = data.sublist(28);
final decrypted = await aesGcm.decrypt(
SecretBox(cipherText, nonce: nonce, mac: Mac(mac)),
secretKey: sharedKey,
);
return utf8.decode(decrypted);
}
Future<String?> getPrivateKey() async {
return await _storage.read(key: 'private_key');
}
Future<bool> hasPrivateKey() async {
final key = await _storage.read(key: 'private_key');
return key != null;
}
Future<void> savePrivateKey(String privateKey) async {
await _storage.write(key: 'private_key', value: privateKey);
}
Future<void> deletePrivateKey() async {
await _storage.delete(key: 'private_key');
}
SecretKey? _currentSharedKey;
// Метод для установки ключа (вызывается при входе в чат)
void setCurrentSharedKey(SecretKey key) {
_currentSharedKey = key;
}
// Тот самый метод, который ищет ChatScreen
Future<SecretKey> getSharedKey(String? chatId) async {
if (_currentSharedKey == null) {
// Если ключа нет, его нужно либо вычислить заново,
// либо выбросить ошибку. Для теста можно вернуть ошибку:
throw Exception("Shared key not initialized for chat $chatId");
}
return _currentSharedKey!;
}
}