use mutex over all signal operations

This commit is contained in:
otsmr 2026-04-22 20:56:28 +02:00
parent 0c8bd0a7b4
commit dde339d1b3
7 changed files with 160 additions and 147 deletions

View file

@ -35,6 +35,7 @@ import 'package:twonly/src/services/group.services.dart';
import 'package:twonly/src/services/notifications/fcm.notifications.dart';
import 'package:twonly/src/services/notifications/pushkeys.notifications.dart';
import 'package:twonly/src/services/signal/identity.signal.dart';
import 'package:twonly/src/services/signal/protocol_state.signal.dart';
import 'package:twonly/src/services/signal/utils.signal.dart';
import 'package:twonly/src/services/subscription.service.dart';
import 'package:twonly/src/services/user.service.dart';
@ -121,6 +122,7 @@ class ApiService {
unawaited(syncFlameCounters());
unawaited(setupNotificationWithUsers());
unawaited(signalHandleNewServerConnection());
resetResyncedUsers();
unawaited(fetchGroupStatesForUnjoinedGroups());
unawaited(fetchMissingGroupPublicKey());
unawaited(checkForDeletedUsernames());

View file

@ -26,6 +26,8 @@ Future<void> handleErrorMessage(
requested: Value(true),
),
);
case EncryptedContent_ErrorMessages_Type.SESSION_OUT_OF_SYNC:
break; // The other user initiated a new signal session, so ignore the error in this case, as the new session works...
// ignore: no_default_cases
default:
break;

View file

@ -74,7 +74,6 @@ Future<void> handleServerMessage(server.ServerToClient msg) async {
}
DateTime lastPushKeyRequest = clock.now().subtract(const Duration(hours: 1));
bool alreadyPerformedAnResync = false;
Mutex protectReceiptCheck = Mutex();

View file

@ -3,21 +3,18 @@ import 'dart:typed_data';
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
// ignore: implementation_imports
import 'package:libsignal_protocol_dart/src/invalid_message_exception.dart';
import 'package:mutex/mutex.dart';
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart';
import 'package:twonly/src/services/api/messages.api.dart';
import 'package:twonly/src/services/signal/protocol_state.signal.dart';
import 'package:twonly/src/services/signal/session.signal.dart';
import 'package:twonly/src/services/signal/utils.signal.dart';
import 'package:twonly/src/utils/log.dart';
/// This caused some troubles, so protection the encryption...
final lockingSignalEncryption = Mutex();
Future<CiphertextMessage?> signalEncryptMessage(
int target,
Uint8List plaintextContent,
) async {
return lockingSignalEncryption.protect<CiphertextMessage?>(() async {
return lockingSignalProtocol.protect<CiphertextMessage?>(() async {
try {
final signalStore = (await getSignalStore())!;
final address = getSignalAddress(target);
@ -30,14 +27,13 @@ Future<CiphertextMessage?> signalEncryptMessage(
});
}
bool alreadyPerformedAnResync = false;
Future<(EncryptedContent?, PlaintextContent_DecryptionErrorMessage_Type?)>
signalDecryptMessage(
int fromUserId,
Uint8List encryptedContentRaw,
int type,
) async {
return lockingSignalProtocol.protect(() async {
try {
final session = SessionCipher.fromStore(
(await getSignalStore())!,
@ -65,11 +61,11 @@ signalDecryptMessage(
Log.warn(e);
return (null, PlaintextContent_DecryptionErrorMessage_Type.PREKEY_UNKNOWN);
} on InvalidMessageException catch (e) {
if (!alreadyPerformedAnResync) {
if (!resyncedUsers.contains(fromUserId)) {
if (await handleSessionResync(fromUserId)) {
// This flag prevents from resyncing the session the client received multiple new
// messages from the server he could not decrypt
alreadyPerformedAnResync = true;
resyncedUsers.add(fromUserId);
// This message contains a new PreKeyBundle establishing a new signal session
await sendCipherText(
@ -88,4 +84,5 @@ signalDecryptMessage(
Log.error(e);
return (null, PlaintextContent_DecryptionErrorMessage_Type.UNKNOWN);
}
});
}

View file

@ -9,6 +9,7 @@ import 'package:twonly/src/constants/secure_storage.keys.dart';
import 'package:twonly/src/database/signal/signal_protocol_store.dart';
import 'package:twonly/src/model/json/signal_identity.model.dart';
import 'package:twonly/src/services/signal/consts.signal.dart';
import 'package:twonly/src/services/signal/protocol_state.signal.dart';
import 'package:twonly/src/services/signal/utils.signal.dart';
import 'package:twonly/src/services/user.service.dart';
import 'package:twonly/src/utils/log.dart';
@ -57,6 +58,7 @@ Future<void> signalHandleNewServerConnection() async {
}
Future<List<PreKeyRecord>> signalGetPreKeys() async {
return lockingSignalProtocol.protect(() async {
final user = await getUser();
if (user == null) return [];
@ -72,6 +74,7 @@ Future<List<PreKeyRecord>> signalGetPreKeys() async {
await signalStore.preKeyStore.storePreKey(p.id, p);
}
return preKeys;
});
}
Future<SignalIdentity?> getSignalIdentity() async {
@ -136,6 +139,7 @@ Future<void> createIfNotExistsSignalIdentity() async {
}
Future<SignedPreKeyRecord?> _getNewSignalSignedPreKey() async {
return lockingSignalProtocol.protect(() async {
var identityKeyPair = await getSignalIdentityKeyPair();
final user = await getUser();
final signalStore = await getSignalStore();
@ -158,4 +162,5 @@ Future<SignedPreKeyRecord?> _getNewSignalSignedPreKey() async {
await signalStore.storeSignedPreKey(signedPreKeyId, signedPreKey);
return signedPreKey;
});
}

View file

@ -0,0 +1,12 @@
import 'package:mutex/mutex.dart';
/// Unified lock for all Signal protocol operations (encryption, decryption, session management).
final lockingSignalProtocol = Mutex();
/// Tracking users who have already been resynced in the current session.
final resyncedUsers = <int>{};
/// Reset the resync tracking set.
void resetResyncedUsers() {
resyncedUsers.clear();
}

View file

@ -4,10 +4,12 @@ import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart';
import 'package:twonly/src/services/signal/consts.signal.dart';
import 'package:twonly/src/services/signal/protocol_state.signal.dart';
import 'package:twonly/src/services/signal/utils.signal.dart';
import 'package:twonly/src/utils/log.dart';
Future<bool> processSignalUserData(Response_UserData userData) async {
return lockingSignalProtocol.protect(() async {
final SignalProtocolStore? signalStore = await getSignalStore();
if (signalStore == null) {
@ -72,13 +74,7 @@ Future<bool> processSignalUserData(Response_UserData userData) async {
Log.error('could not process pre key bundle: $e');
return false;
}
}
Future<void> deleteSessionWithTarget(int target) async {
final signalStore = await getSignalStore();
if (signalStore == null) return;
final address = SignalProtocolAddress(target.toString(), defaultDeviceId);
await signalStore.sessionStore.deleteSession(address);
});
}
Future<Uint8List?> getPublicKeyFromContact(int contactId) async {