186 lines
5.7 KiB
Dart
186 lines
5.7 KiB
Dart
import 'dart:async';
|
|
import 'dart:collection';
|
|
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:komet_crypto/komet_crypto.dart';
|
|
|
|
import '../../backend/modules/messages.dart';
|
|
import '../storage/app_database.dart';
|
|
import '../storage/chat_encryption_store.dart';
|
|
import 'chat_crypto_service.dart';
|
|
import 'e2ee_service.dart';
|
|
|
|
// #***! расшифровалось, ключ не тот, либо копии текста на этом устройстве нет
|
|
enum MessageDecryptionState { decrypted, wrongKey, unavailable }
|
|
|
|
@immutable
|
|
class MessageDecryption {
|
|
final String? plaintext;
|
|
final MessageDecryptionState state;
|
|
|
|
const MessageDecryption.decrypted(String this.plaintext)
|
|
: state = MessageDecryptionState.decrypted;
|
|
|
|
const MessageDecryption.wrongKey()
|
|
: plaintext = null,
|
|
state = MessageDecryptionState.wrongKey;
|
|
|
|
const MessageDecryption.unavailable()
|
|
: plaintext = null,
|
|
state = MessageDecryptionState.unavailable;
|
|
|
|
bool get isDecrypted => state == MessageDecryptionState.decrypted;
|
|
}
|
|
|
|
class MessageDecryptionCache {
|
|
MessageDecryptionCache._() {
|
|
ChatEncryptionStore.instance.revision.addListener(clear);
|
|
E2eeService.instance.revision.addListener(clear);
|
|
}
|
|
|
|
static final MessageDecryptionCache instance = MessageDecryptionCache._();
|
|
|
|
static const int _maxEntries = 1000;
|
|
|
|
final LinkedHashMap<String, ValueNotifier<MessageDecryption?>> _entries =
|
|
LinkedHashMap();
|
|
final Set<String> _inFlight = {};
|
|
|
|
ValueListenable<MessageDecryption?> listenableFor(String messageId) =>
|
|
_entryFor(messageId);
|
|
|
|
ValueNotifier<MessageDecryption?> _entryFor(String messageId) =>
|
|
_entries[messageId] ??= ValueNotifier<MessageDecryption?>(null);
|
|
|
|
void _evictStale(String keep) {
|
|
while (_entries.length > _maxEntries) {
|
|
final oldest = _entries.keys.first;
|
|
if (oldest == keep) break;
|
|
_entries.remove(oldest);
|
|
}
|
|
}
|
|
|
|
void seed(String messageId, String plaintext) {
|
|
_entryFor(messageId).value = MessageDecryption.decrypted(plaintext);
|
|
}
|
|
|
|
void adopt(String fromMessageId, String toMessageId) {
|
|
final value = _entries[fromMessageId]?.value;
|
|
if (value != null) _entryFor(toMessageId).value = value;
|
|
}
|
|
|
|
void request({
|
|
required int accountId,
|
|
required int chatId,
|
|
required String messageId,
|
|
required String cipherText,
|
|
}) {
|
|
if (cipherText.isEmpty) return;
|
|
final e2ee =
|
|
E2eeService.instance.available &&
|
|
KometCrypto.classifyText(cipherText) != TextClass.none;
|
|
if (!e2ee && !ChatCryptoService.instance.isEnabled(accountId, chatId)) {
|
|
return;
|
|
}
|
|
if (_entryFor(messageId).value != null) return;
|
|
if (!_inFlight.add(messageId)) return;
|
|
_evictStale(messageId);
|
|
unawaited(
|
|
e2ee
|
|
? _resolveSealed(accountId, chatId, messageId, cipherText)
|
|
: _resolve(accountId, chatId, messageId, cipherText),
|
|
);
|
|
}
|
|
|
|
// #***! сквозное: открытый текст лежит в базе под локальным ключом
|
|
Future<void> _resolveSealed(
|
|
int accountId,
|
|
int chatId,
|
|
String messageId,
|
|
String cipherText,
|
|
) async {
|
|
try {
|
|
final row = await AppDatabase.loadMessage(accountId, chatId, messageId);
|
|
if (row == null) return;
|
|
final flag = row['e2ee'] as int? ?? 0;
|
|
final sealed = row['text_sealed'];
|
|
if (flag == CachedMessage.e2eeText && sealed is Uint8List) {
|
|
final text = await E2eeService.instance.openText(
|
|
accountId,
|
|
chatId,
|
|
sealed,
|
|
);
|
|
_entryFor(messageId).value = text == null
|
|
? const MessageDecryption.unavailable()
|
|
: MessageDecryption.decrypted(text);
|
|
return;
|
|
}
|
|
if (flag == CachedMessage.e2eeFailed) {
|
|
_entryFor(messageId).value = const MessageDecryption.wrongKey();
|
|
return;
|
|
}
|
|
if (flag == CachedMessage.e2eeFile) {
|
|
_entryFor(messageId).value = const MessageDecryption.decrypted('');
|
|
return;
|
|
}
|
|
if (flag != CachedMessage.e2eeNone || !E2eeService.instance.available) {
|
|
return;
|
|
}
|
|
switch (KometCrypto.classifyText(cipherText)) {
|
|
case TextClass.session:
|
|
_entryFor(messageId).value = const MessageDecryption.unavailable();
|
|
case TextClass.offer:
|
|
case TextClass.answer:
|
|
_entryFor(messageId).value = MessageDecryption.decrypted(
|
|
cipherText.split('\n').first,
|
|
);
|
|
case TextClass.legacy:
|
|
if (ChatCryptoService.instance.isEnabled(accountId, chatId)) {
|
|
_inFlight.remove(messageId);
|
|
await _resolve(accountId, chatId, messageId, cipherText);
|
|
}
|
|
case TextClass.none:
|
|
break;
|
|
}
|
|
} finally {
|
|
_inFlight.remove(messageId);
|
|
}
|
|
}
|
|
|
|
Future<void> _resolve(
|
|
int accountId,
|
|
int chatId,
|
|
String messageId,
|
|
String cipherText,
|
|
) async {
|
|
try {
|
|
final crypto = ChatCryptoService.instance;
|
|
final result = await crypto.decrypt(accountId, chatId, cipherText);
|
|
if (result.isOk) {
|
|
_entryFor(messageId).value = MessageDecryption.decrypted(result.text!);
|
|
return;
|
|
}
|
|
switch (result.failure) {
|
|
case CryptoFailure.wrongKey:
|
|
_entryFor(messageId).value = const MessageDecryption.wrongKey();
|
|
case CryptoFailure.noKey:
|
|
if (crypto.looksEncrypted(cipherText)) {
|
|
_entryFor(messageId).value = const MessageDecryption.wrongKey();
|
|
}
|
|
case CryptoFailure.notEncrypted:
|
|
case CryptoFailure.malformed:
|
|
case CryptoFailure.unavailable:
|
|
case null:
|
|
break;
|
|
}
|
|
} finally {
|
|
_inFlight.remove(messageId);
|
|
}
|
|
}
|
|
|
|
void clear() {
|
|
_entries.clear();
|
|
_inFlight.clear();
|
|
}
|
|
}
|