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,62 +27,62 @@ Future<CiphertextMessage?> signalEncryptMessage(
});
}
bool alreadyPerformedAnResync = false;
Future<(EncryptedContent?, PlaintextContent_DecryptionErrorMessage_Type?)>
signalDecryptMessage(
int fromUserId,
Uint8List encryptedContentRaw,
int type,
) async {
try {
final session = SessionCipher.fromStore(
(await getSignalStore())!,
getSignalAddress(fromUserId),
);
return lockingSignalProtocol.protect(() async {
try {
final session = SessionCipher.fromStore(
(await getSignalStore())!,
getSignalAddress(fromUserId),
);
Uint8List plaintext;
Uint8List plaintext;
switch (type) {
case CiphertextMessage.prekeyType:
plaintext = await session.decrypt(
PreKeySignalMessage(encryptedContentRaw),
);
case CiphertextMessage.whisperType:
plaintext = await session.decryptFromSignal(
SignalMessage.fromSerialized(encryptedContentRaw),
);
default:
Log.error('Unknown Message Decryption Type: $type');
return (null, PlaintextContent_DecryptionErrorMessage_Type.UNKNOWN);
}
return (EncryptedContent.fromBuffer(plaintext), null);
} on InvalidKeyIdException catch (e) {
Log.warn(e);
return (null, PlaintextContent_DecryptionErrorMessage_Type.PREKEY_UNKNOWN);
} on InvalidMessageException catch (e) {
if (!alreadyPerformedAnResync) {
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;
// This message contains a new PreKeyBundle establishing a new signal session
await sendCipherText(
fromUserId,
EncryptedContent(
errorMessages: EncryptedContent_ErrorMessages(
type: EncryptedContent_ErrorMessages_Type.SESSION_OUT_OF_SYNC,
),
),
);
switch (type) {
case CiphertextMessage.prekeyType:
plaintext = await session.decrypt(
PreKeySignalMessage(encryptedContentRaw),
);
case CiphertextMessage.whisperType:
plaintext = await session.decryptFromSignal(
SignalMessage.fromSerialized(encryptedContentRaw),
);
default:
Log.error('Unknown Message Decryption Type: $type');
return (null, PlaintextContent_DecryptionErrorMessage_Type.UNKNOWN);
}
return (EncryptedContent.fromBuffer(plaintext), null);
} on InvalidKeyIdException catch (e) {
Log.warn(e);
return (null, PlaintextContent_DecryptionErrorMessage_Type.PREKEY_UNKNOWN);
} on InvalidMessageException catch (e) {
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
resyncedUsers.add(fromUserId);
// This message contains a new PreKeyBundle establishing a new signal session
await sendCipherText(
fromUserId,
EncryptedContent(
errorMessages: EncryptedContent_ErrorMessages(
type: EncryptedContent_ErrorMessages_Type.SESSION_OUT_OF_SYNC,
),
),
);
}
}
Log.warn(e);
return (null, PlaintextContent_DecryptionErrorMessage_Type.UNKNOWN);
} catch (e) {
Log.error(e);
return (null, PlaintextContent_DecryptionErrorMessage_Type.UNKNOWN);
}
Log.warn(e);
return (null, PlaintextContent_DecryptionErrorMessage_Type.UNKNOWN);
} catch (e) {
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,21 +58,23 @@ Future<void> signalHandleNewServerConnection() async {
}
Future<List<PreKeyRecord>> signalGetPreKeys() async {
final user = await getUser();
if (user == null) return [];
return lockingSignalProtocol.protect(() async {
final user = await getUser();
if (user == null) return [];
final start = user.currentPreKeyIndexStart;
await updateUser((user) {
user.currentPreKeyIndexStart =
(user.currentPreKeyIndexStart + 200) % maxValue;
final start = user.currentPreKeyIndexStart;
await updateUser((user) {
user.currentPreKeyIndexStart =
(user.currentPreKeyIndexStart + 200) % maxValue;
});
final preKeys = generatePreKeys(start, 200);
final signalStore = await getSignalStore();
if (signalStore == null) return [];
for (final p in preKeys) {
await signalStore.preKeyStore.storePreKey(p.id, p);
}
return preKeys;
});
final preKeys = generatePreKeys(start, 200);
final signalStore = await getSignalStore();
if (signalStore == null) return [];
for (final p in preKeys) {
await signalStore.preKeyStore.storePreKey(p.id, p);
}
return preKeys;
}
Future<SignalIdentity?> getSignalIdentity() async {
@ -136,26 +139,28 @@ Future<void> createIfNotExistsSignalIdentity() async {
}
Future<SignedPreKeyRecord?> _getNewSignalSignedPreKey() async {
var identityKeyPair = await getSignalIdentityKeyPair();
final user = await getUser();
final signalStore = await getSignalStore();
if (identityKeyPair == null || signalStore == null || user == null) {
return null;
}
return lockingSignalProtocol.protect(() async {
var identityKeyPair = await getSignalIdentityKeyPair();
final user = await getUser();
final signalStore = await getSignalStore();
if (identityKeyPair == null || signalStore == null || user == null) {
return null;
}
final signedPreKeyId = user.currentSignedPreKeyIndexStart;
await updateUser((user) {
user.currentSignedPreKeyIndexStart += 1;
final signedPreKeyId = user.currentSignedPreKeyIndexStart;
await updateUser((user) {
user.currentSignedPreKeyIndexStart += 1;
});
final signedPreKey = generateSignedPreKey(
identityKeyPair,
signedPreKeyId,
);
identityKeyPair = null;
await signalStore.storeSignedPreKey(signedPreKeyId, signedPreKey);
return signedPreKey;
});
final signedPreKey = generateSignedPreKey(
identityKeyPair,
signedPreKeyId,
);
identityKeyPair = null;
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,81 +4,77 @@ 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 {
final SignalProtocolStore? signalStore = await getSignalStore();
return lockingSignalProtocol.protect(() async {
final SignalProtocolStore? signalStore = await getSignalStore();
if (signalStore == null) {
return false;
}
if (signalStore == null) {
return false;
}
final targetAddress = getSignalAddress(userData.userId.toInt());
final targetAddress = getSignalAddress(userData.userId.toInt());
final sessionBuilder = SessionBuilder.fromSignalStore(
signalStore,
targetAddress,
);
final sessionBuilder = SessionBuilder.fromSignalStore(
signalStore,
targetAddress,
);
ECPublicKey? tempPrePublicKey;
int? tempPreKeyId;
ECPublicKey? tempPrePublicKey;
int? tempPreKeyId;
if (userData.prekeys.isNotEmpty) {
tempPrePublicKey = Curve.decodePoint(
DjbECPublicKey(
Uint8List.fromList(userData.prekeys.first.prekey),
).serialize(),
if (userData.prekeys.isNotEmpty) {
tempPrePublicKey = Curve.decodePoint(
DjbECPublicKey(
Uint8List.fromList(userData.prekeys.first.prekey),
).serialize(),
1,
);
tempPreKeyId = userData.prekeys.first.id.toInt();
}
final tempSignedPreKeyId = userData.signedPrekeyId.toInt();
final tempSignedPreKeyPublic = Curve.decodePoint(
DjbECPublicKey(Uint8List.fromList(userData.signedPrekey)).serialize(),
1,
);
tempPreKeyId = userData.prekeys.first.id.toInt();
}
final tempSignedPreKeyId = userData.signedPrekeyId.toInt();
final tempSignedPreKeySignature = Uint8List.fromList(
userData.signedPrekeySignature,
);
final tempSignedPreKeyPublic = Curve.decodePoint(
DjbECPublicKey(Uint8List.fromList(userData.signedPrekey)).serialize(),
1,
);
final tempIdentityKey = IdentityKey(
Curve.decodePoint(
DjbECPublicKey(
Uint8List.fromList(userData.publicIdentityKey),
).serialize(),
1,
),
);
final tempSignedPreKeySignature = Uint8List.fromList(
userData.signedPrekeySignature,
);
final preKeyBundle = PreKeyBundle(
userData.registrationId.toInt(),
defaultDeviceId,
tempPreKeyId,
tempPrePublicKey,
tempSignedPreKeyId,
tempSignedPreKeyPublic,
tempSignedPreKeySignature,
tempIdentityKey,
);
final tempIdentityKey = IdentityKey(
Curve.decodePoint(
DjbECPublicKey(
Uint8List.fromList(userData.publicIdentityKey),
).serialize(),
1,
),
);
final preKeyBundle = PreKeyBundle(
userData.registrationId.toInt(),
defaultDeviceId,
tempPreKeyId,
tempPrePublicKey,
tempSignedPreKeyId,
tempSignedPreKeyPublic,
tempSignedPreKeySignature,
tempIdentityKey,
);
try {
await sessionBuilder.processPreKeyBundle(preKeyBundle);
return true;
} catch (e) {
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);
try {
await sessionBuilder.processPreKeyBundle(preKeyBundle);
return true;
} catch (e) {
Log.error('could not process pre key bundle: $e');
return false;
}
});
}
Future<Uint8List?> getPublicKeyFromContact(int contactId) async {