Files
Qlyra/lib/core/crypto/message_decryption_cache.dart

112 lines
3.1 KiB
Dart

import 'dart:async';
import 'dart:collection';
import 'package:flutter/foundation.dart';
import '../storage/chat_encryption_store.dart';
import 'chat_crypto_service.dart';
enum MessageDecryptionState { decrypted, wrongKey }
@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;
bool get isDecrypted => state == MessageDecryptionState.decrypted;
}
class MessageDecryptionCache {
MessageDecryptionCache._() {
ChatEncryptionStore.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;
if (!ChatCryptoService.instance.isEnabled(accountId, chatId)) return;
if (_entryFor(messageId).value != null) return;
if (!_inFlight.add(messageId)) return;
_evictStale(messageId);
unawaited(_resolve(accountId, chatId, messageId, cipherText));
}
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 (await 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();
}
}