373 lines
11 KiB
Dart
373 lines
11 KiB
Dart
import 'package:cryptography/cryptography.dart';
|
||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||
import 'dart:convert';
|
||
import 'dart:typed_data';
|
||
import 'package:chepuhagram/data/models/contact_model.dart';
|
||
|
||
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<(List<int>, String)?> encryptImage(
|
||
List<int> fileBytes,
|
||
SecretKey sharedKey,
|
||
) async {
|
||
try {
|
||
final SecretKey fileSecretKey = await aesGcm.newSecretKey();
|
||
final List<int> fileSecretKeyBytes = await fileSecretKey.extractBytes();
|
||
|
||
final SecretBox secretBox = await aesGcm.encrypt(
|
||
fileBytes,
|
||
secretKey: fileSecretKey,
|
||
);
|
||
final List<int> dataToUpload = secretBox.concatenation();
|
||
|
||
final encryptedKeyBox = await aesGcm.encrypt(
|
||
fileSecretKeyBytes,
|
||
secretKey: sharedKey,
|
||
);
|
||
|
||
final String encryptedKeyForServer = base64Encode(
|
||
encryptedKeyBox.concatenation(),
|
||
);
|
||
|
||
return (dataToUpload, encryptedKeyForServer);
|
||
} catch (e) {
|
||
print("Ошибка шифрования медиа: $e");
|
||
return null;
|
||
}
|
||
}
|
||
|
||
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?> decryptImage(
|
||
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<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');
|
||
}
|
||
}
|