diff --git a/CHANGELOG.md b/CHANGELOG.md index ce639f6e..12ecc08d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ - Improved: Redesigned snackbar notifications - Improved: New backup mechanism to allow larger backup files - Improved: Move keys into a centralized Rust-owned structure stored in secure storage +- Fix: Messages occasionally not received until app restart +- Fix: Multiple smaller issues ## 0.2.10 diff --git a/lib/src/services/api.service.dart b/lib/src/services/api.service.dart index 3e0637e2..64224cdb 100644 --- a/lib/src/services/api.service.dart +++ b/lib/src/services/api.service.dart @@ -95,6 +95,7 @@ class ApiService { try { final channel = IOWebSocketChannel.connect( Uri.parse(apiUrl), + pingInterval: const Duration(seconds: 30), ); _channel = channel; _channel!.stream.listen(_onData, onDone: _onDone, onError: _onError); @@ -247,11 +248,11 @@ class ApiService { try { final msg = server.ServerToClient.fromBuffer(msgBuffer as Uint8List); if (msg.v0.hasResponse()) { - await removeFromRetransmissionBuffer(msg.v0.seq); final completer = _pendingRequests.remove(msg.v0.seq); if (completer != null && !completer.isCompleted) { completer.complete(msg); } + unawaited(removeFromRetransmissionBuffer(msg.v0.seq)); } else { unawaited(handleServerMessage(msg)); } diff --git a/lib/src/services/api/messages.api.dart b/lib/src/services/api/messages.api.dart index 4024635e..a9eabd3a 100644 --- a/lib/src/services/api/messages.api.dart +++ b/lib/src/services/api/messages.api.dart @@ -67,7 +67,6 @@ Future<(Uint8List, Uint8List?)?> tryToSendCompleteMessage({ Receipt? receipt, bool onlyReturnEncryptedData = false, bool blocking = true, - bool useLock = true, }) async { if (apiService.appIsOutdated) return null; @@ -135,7 +134,6 @@ Future<(Uint8List, Uint8List?)?> tryToSendCompleteMessage({ final cipherText = await signalEncryptMessage( receipt.contactId, Uint8List.fromList(message.encryptedContent), - useLock: useLock, ); if (cipherText == null) { Log.error('Could not encrypt the message. Aborting and trying again.'); @@ -340,7 +338,6 @@ Future<(Uint8List, Uint8List?)?> sendCipherText( bool blocking = true, String? messageId, bool onlySendIfNoReceiptsAreOpen = false, - bool useLock = true, }) async { if (onlySendIfNoReceiptsAreOpen) { final openReceipts = await twonlyDB.receiptsDao.getReceiptCountForContact( @@ -402,7 +399,6 @@ Future<(Uint8List, Uint8List?)?> sendCipherText( receipt: receipt, onlyReturnEncryptedData: onlyReturnEncryptedData, blocking: blocking, - useLock: useLock, ); if (!blocking) { return null; diff --git a/lib/src/services/signal/encryption.signal.dart b/lib/src/services/signal/encryption.signal.dart index d8b4e1b5..0065eb4a 100644 --- a/lib/src/services/signal/encryption.signal.dart +++ b/lib/src/services/signal/encryption.signal.dart @@ -12,15 +12,11 @@ import 'package:twonly/src/utils/log.dart'; Future signalEncryptMessage( int target, - Uint8List plaintextContent, { - bool useLock = true, -}) async { - if (useLock) { - return lockingSignalProtocol.protect(() async { - return _signalEncryptMessage(target, plaintextContent); - }); - } - return _signalEncryptMessage(target, plaintextContent); + Uint8List plaintextContent, +) async { + return lockingSignalProtocol.protect(() async { + return _signalEncryptMessage(target, plaintextContent); + }); } Future _signalEncryptMessage( @@ -44,63 +40,83 @@ signalDecryptMessage( Uint8List encryptedContentRaw, int type, ) async { - return lockingSignalProtocol.protect(() async { - try { - final session = SessionCipher.fromStore( - (await getSignalStore())!, - getSignalAddress(fromUserId), - ); - - Uint8List plaintext; - - switch (type) { - case CiphertextMessage.prekeyType: - plaintext = await session.decrypt( - PreKeySignalMessage(encryptedContentRaw), + // Hold the lock only for the cryptographic operation, not for network I/O + final (decryptedContent, errorType, needsResync) = await lockingSignalProtocol + .protect(() async { + try { + final session = SessionCipher.fromStore( + (await getSignalStore())!, + getSignalAddress(fromUserId), ); - case CiphertextMessage.whisperType: - plaintext = await session.decryptFromSignal( - SignalMessage.fromSerialized(encryptedContentRaw), + + 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, + false, + ); + } + + return (EncryptedContent.fromBuffer(plaintext), null, false); + } on InvalidKeyIdException catch (e) { + Log.warn(e); + return ( + null, + PlaintextContent_DecryptionErrorMessage_Type.PREKEY_UNKNOWN, + false, ); - 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 DuplicateMessageException catch (e) { - Log.info(e.toString()); - return (null, null); - } on InvalidMessageException catch (e) { - Log.warn(e); - if (!resyncedUsers.contains(fromUserId)) { - if (await handleSessionResync(fromUserId, useLock: false)) { - // 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, - ), - ), - useLock: false, + } on DuplicateMessageException catch (e) { + Log.info(e.toString()); + return (null, null, false); + } on InvalidMessageException catch (e) { + Log.warn(e); + return ( + null, + PlaintextContent_DecryptionErrorMessage_Type.UNKNOWN, + true, + ); + } catch (e) { + Log.error(e); + return ( + null, + PlaintextContent_DecryptionErrorMessage_Type.UNKNOWN, + false, ); } - } - return (null, PlaintextContent_DecryptionErrorMessage_Type.UNKNOWN); - } catch (e) { - Log.error(e); - return (null, PlaintextContent_DecryptionErrorMessage_Type.UNKNOWN); + }); + + // Handle session resync OUTSIDE the lock to avoid holding it during + // network round-trips (which can block for up to 60 seconds) + if (needsResync && !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, + ), + ), + ); } - }); + } + + return (decryptedContent, errorType); } diff --git a/lib/src/services/signal/session.signal.dart b/lib/src/services/signal/session.signal.dart index f23236b5..a26d4b4c 100644 --- a/lib/src/services/signal/session.signal.dart +++ b/lib/src/services/signal/session.signal.dart @@ -8,16 +8,10 @@ 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 processSignalUserData( - Response_UserData userData, { - bool useLock = true, -}) async { - if (useLock) { - return lockingSignalProtocol.protect(() async { - return _processSignalUserData(userData); - }); - } - return _processSignalUserData(userData); +Future processSignalUserData(Response_UserData userData) async { + return lockingSignalProtocol.protect(() async { + return _processSignalUserData(userData); + }); } Future _processSignalUserData(Response_UserData userData) async { @@ -106,14 +100,11 @@ Future getPublicKeyFromContact(int contactId) async { } } -Future handleSessionResync( - int fromUserId, { - bool useLock = true, -}) async { +Future handleSessionResync(int fromUserId) async { final userData = await apiService.getUserById(fromUserId); if (userData != null) { Log.info('Got new session data from the server to re-sync the session'); - return processSignalUserData(userData, useLock: useLock); + return processSignalUserData(userData); } Log.info('Could not download userdata from the server.'); return false;