From 054542fa2f6890d6095180fcf66059271b41103a Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Fri, 2 Aug 2024 08:00:54 +0200 Subject: [PATCH 01/22] Make macos review team happy --- Monal/Classes/chatViewController.m | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/Monal/Classes/chatViewController.m b/Monal/Classes/chatViewController.m index e61a94ac3b..615add3ff2 100644 --- a/Monal/Classes/chatViewController.m +++ b/Monal/Classes/chatViewController.m @@ -1545,26 +1545,9 @@ -(IBAction) attach:(id) sender } else if(gpsStatus == kCLAuthorizationStatusNotDetermined || gpsStatus == kCLAuthorizationStatusRestricted) { -#if TARGET_OS_MACCATALYST - UIAlertController* alert = [UIAlertController alertControllerWithTitle:NSLocalizedString(@"Location Access Needed", @"") message:NSLocalizedString(@"Monal uses your location when you send a location message in a conversation.", @"") preferredStyle:UIAlertControllerStyleAlert]; - - [alert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Close", @"") style:UIAlertActionStyleCancel handler:^(UIAlertAction* _Nonnull action) { - [alert dismissViewControllerAnimated:YES completion:nil]; - }]]; - - UIAlertAction* allow = [UIAlertAction actionWithTitle:NSLocalizedString(@"Allow", @"") style:UIAlertActionStyleDefault handler:^(UIAlertAction* _Nonnull action) { - [self makeLocationManager]; - self.sendLocation=YES; - [self.locationManager requestWhenInUseAuthorization]; - }]; - [alert addAction:allow]; - - [self presentViewController:alert animated:YES completion:nil]; -#else [self makeLocationManager]; self.sendLocation = YES; [self.locationManager requestWhenInUseAuthorization]; -#endif } else { From 89795bae1b4205f0ddab4ef721cc457ba179efcd Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Fri, 2 Aug 2024 05:33:02 +0200 Subject: [PATCH 02/22] Fix quicksy build --- .github/workflows/quicksy.build-push.yml | 15 ++++++++------- appstore_quicksy_metadata/en-US/description.txt | 5 +++++ appstore_quicksy_metadata/en-US/keywords.txt | 1 + appstore_quicksy_metadata/en-US/marketing_url.txt | 1 + appstore_quicksy_metadata/en-US/privacy_url.txt | 1 + appstore_quicksy_metadata/en-US/support_url.txt | 1 + scripts/build.sh | 2 +- 7 files changed, 18 insertions(+), 8 deletions(-) create mode 100644 appstore_quicksy_metadata/en-US/description.txt create mode 100644 appstore_quicksy_metadata/en-US/keywords.txt create mode 100644 appstore_quicksy_metadata/en-US/marketing_url.txt create mode 100644 appstore_quicksy_metadata/en-US/privacy_url.txt create mode 100644 appstore_quicksy_metadata/en-US/support_url.txt diff --git a/.github/workflows/quicksy.build-push.yml b/.github/workflows/quicksy.build-push.yml index 3342dee47a..e723bd6e57 100644 --- a/.github/workflows/quicksy.build-push.yml +++ b/.github/workflows/quicksy.build-push.yml @@ -18,8 +18,7 @@ jobs: env: APP_NAME: "Quicksy" BUILD_SCHEME: "Quicksy" - APP_DIR: "Monal.app" - BUILD_TYPE: "AppStore" + BUILD_TYPE: "AppStore-Quicksy" EXPORT_OPTIONS_IOS: "../scripts/exportOptions/Quicksy_Stable_iOS_ExportOptions.plist" # Steps represent a sequence of tasks that will be executed as part of the job steps: @@ -60,7 +59,9 @@ jobs: } buildNumber="$(git tag --sort="v:refname" | grep "Quicksy_Build_iOS" | tail -n1 | sed 's/Quicksy_Build_iOS_//g')" version="$(git log -n 1 --merges --pretty=format:%s | sed -E 's/^[\t\n ]*([^\n\t ]+)[\t\n ]+\(([^\n\t ]+)\)[\t\n ]*$/\1/g')" - version="6.4.2" + if [ "${{ github.ref }}" != "refs/heads/stable" ]; then + version="1.$buildNumber" + fi mkdir -p /Users/ci/releases OUTPUT_FILE="/Users/ci/releases/$buildNumber.output" touch "$OUTPUT_FILE" @@ -103,7 +104,7 @@ jobs: - uses: actions/upload-artifact@v4 with: name: monal-ios - path: Monal/build/ipa/Monal.ipa + path: Monal/build/ipa/Quicksy.ipa if-no-files-found: error # - uses: actions/upload-artifact@v4 # with: @@ -111,7 +112,7 @@ jobs: # path: Monal/build/ios_Monal.xcarchive/dSYMs # if-no-files-found: error - name: validate ios app - run: xcrun altool --validate-app --file ./Monal/build/ipa/Monal.ipa --type ios -u $(cat /Users/ci/apple_connect_upload_mail.txt) -p "$(cat /Users/ci/apple_connect_upload_secret.txt)" + run: xcrun altool --validate-app --file ./Monal/build/ipa/Quicksy.ipa --type ios -u $(cat /Users/ci/apple_connect_upload_mail.txt) -p "$(cat /Users/ci/apple_connect_upload_secret.txt)" - name: push tag to stable repo run: | buildNumber=$(git tag --sort="v:refname" | grep "Quicksy_Build_iOS" | tail -n1 | sed 's/Quicksy_Build_iOS_//g') @@ -132,11 +133,11 @@ jobs: done echo "path_ios=$path_ios" | tee /dev/stderr >> "$GITHUB_OUTPUT" - name: Publish ios to appstore connect - #run: xcrun altool --upload-app --file ./Monal/build/ipa/Monal.ipa --type ios --asc-provider S8D843U34Y --team-id S8D843U34Y -u $(cat /Users/ci/apple_connect_upload_mail.txt) -p "$(cat /Users/ci/apple_connect_upload_secret.txt)" + #run: xcrun altool --upload-app --file ./Monal/build/ipa/Quicksy.ipa --type ios --asc-provider S8D843U34Y --team-id S8D843U34Y -u $(cat /Users/ci/apple_connect_upload_mail.txt) -p "$(cat /Users/ci/apple_connect_upload_secret.txt)" env: DELIVER_METADATA_PATH: ${{ steps.metadata.outputs.path_ios }} run: | - fastlane run upload_to_app_store api_key_path:"/Users/ci/appstoreconnect/key.json" team_id:"S8D843U34Y" ipa:"./Monal/build/ipa/Monal.ipa" app_version:"${{ steps.releasenotes.outputs.version }}" platform:ios reject_if_possible:true submit_for_review:true automatic_release:true skip_metadata:false skip_screenshots:true precheck_include_in_app_purchases:false version_check_wait_retry_limit:10 force:true + fastlane run upload_to_app_store api_key_path:"/Users/ci/appstoreconnect/key.json" team_id:"S8D843U34Y" ipa:"./Monal/build/ipa/Quicksy.ipa" app_version:"${{ steps.releasenotes.outputs.version }}" platform:ios reject_if_possible:true submit_for_review:true automatic_release:true skip_metadata:false skip_screenshots:true precheck_include_in_app_purchases:false version_check_wait_retry_limit:10 force:true - name: Remove fastlane metadata directory run: | rm -rf "${{ steps.metadata.outputs.path }}" diff --git a/appstore_quicksy_metadata/en-US/description.txt b/appstore_quicksy_metadata/en-US/description.txt new file mode 100644 index 0000000000..a9a9ba9455 --- /dev/null +++ b/appstore_quicksy_metadata/en-US/description.txt @@ -0,0 +1,5 @@ +Quicksy is a spin off of the popular XMPP client Monal with automatic contact discovery. + +You sign up with your phone number and Quicksy will automatically — based on the phone numbers in your address book — suggest possible contacts to you. Under the hood Quicksy is a full-fledged XMPP client that lets you communicate with any user on any publicly federating server. Likewise users on Quicksy can be contacted from the outside simply by adding +phonenumber@quicksy.im to your contact list. + +Aside from the contact sync the user interface is deliberately as close to Monal as possible. This allows users to eventually migrate from Quicksy to Monal without having to relearn how the app works. \ No newline at end of file diff --git a/appstore_quicksy_metadata/en-US/keywords.txt b/appstore_quicksy_metadata/en-US/keywords.txt new file mode 100644 index 0000000000..cc2016d901 --- /dev/null +++ b/appstore_quicksy_metadata/en-US/keywords.txt @@ -0,0 +1 @@ +xmpp, jabber, chat, instant messaging, messaging, ejabberd, prosody, OMEMO \ No newline at end of file diff --git a/appstore_quicksy_metadata/en-US/marketing_url.txt b/appstore_quicksy_metadata/en-US/marketing_url.txt new file mode 100644 index 0000000000..b53627eba2 --- /dev/null +++ b/appstore_quicksy_metadata/en-US/marketing_url.txt @@ -0,0 +1 @@ +https://quicksy.im/ \ No newline at end of file diff --git a/appstore_quicksy_metadata/en-US/privacy_url.txt b/appstore_quicksy_metadata/en-US/privacy_url.txt new file mode 100644 index 0000000000..bb7d1c0a62 --- /dev/null +++ b/appstore_quicksy_metadata/en-US/privacy_url.txt @@ -0,0 +1 @@ +https://quicksy.im/privacy.htm \ No newline at end of file diff --git a/appstore_quicksy_metadata/en-US/support_url.txt b/appstore_quicksy_metadata/en-US/support_url.txt new file mode 100644 index 0000000000..b53627eba2 --- /dev/null +++ b/appstore_quicksy_metadata/en-US/support_url.txt @@ -0,0 +1 @@ +https://quicksy.im/ \ No newline at end of file diff --git a/scripts/build.sh b/scripts/build.sh index d0b31bdf3e..b89ca6517a 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -122,4 +122,4 @@ xcodebuild \ -allowProvisioningDeviceRegistration echo "build dir:" -ls -l "build" +find build From 7a62461e37b9d8f7781317c865c898e0d451b79b Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Sat, 3 Aug 2024 01:33:56 +0200 Subject: [PATCH 03/22] Make sure to open the receivers chat when sharing via sharesheet --- Monal/Classes/ActiveChatsViewController.h | 2 + Monal/Classes/MonalAppDelegate.m | 178 +++++++++++----------- 2 files changed, 92 insertions(+), 88 deletions(-) diff --git a/Monal/Classes/ActiveChatsViewController.h b/Monal/Classes/ActiveChatsViewController.h index 71944fd32c..8541382c59 100644 --- a/Monal/Classes/ActiveChatsViewController.h +++ b/Monal/Classes/ActiveChatsViewController.h @@ -49,6 +49,8 @@ NS_ASSUME_NONNULL_BEGIN -(void) showAddContact; -(void) sheetDismissed; +-(void) dismissCompleteViewChainWithAnimation:(BOOL) animation andCompletion:(monal_void_block_t _Nullable) completion; + @end NS_ASSUME_NONNULL_END diff --git a/Monal/Classes/MonalAppDelegate.m b/Monal/Classes/MonalAppDelegate.m index 2add71ae4d..0206838a0a 100644 --- a/Monal/Classes/MonalAppDelegate.m +++ b/Monal/Classes/MonalAppDelegate.m @@ -1941,97 +1941,99 @@ -(void) sendAllOutboxes return; } - //open the destination chat only once - for(NSDictionary* payload in [[DataLayer sharedInstance] getShareSheetPayload]) - { - DDLogInfo(@"Sending outbox entry: %@", payload); - xmpp* account = [[MLXMPPManager sharedInstance] getConnectedAccountForID:payload[@"account_id"]]; - if(account == nil) + [(ActiveChatsViewController*)self.activeChats dismissCompleteViewChainWithAnimation:YES andCompletion:^{ + //open the destination chat only once + for(NSDictionary* payload in [[DataLayer sharedInstance] getShareSheetPayload]) { - UIAlertController* messageAlert = [UIAlertController alertControllerWithTitle:NSLocalizedString(@"Sharing failed", @"") message:[NSString stringWithFormat:NSLocalizedString(@"Cannot share something with disabled/deleted account, destination: %@, internal account id: %@", @""), payload[@"recipient"], payload[@"account_id"]] preferredStyle:UIAlertControllerStyleAlert]; - [messageAlert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Close", @"") style:UIAlertActionStyleCancel handler:^(UIAlertAction* action __unused) { - }]]; - [self.activeChats presentViewController:messageAlert animated:YES completion:nil]; - [[DataLayer sharedInstance] deleteShareSheetPayloadWithId:payload[@"id"]]; - continue; - } - MLContact* contact = [MLContact createContactFromJid:payload[@"recipient"] andAccountNo:account.accountNo]; - - monal_id_block_t cleanup = ^(NSDictionary* payload) { - [[DataLayer sharedInstance] deleteShareSheetPayloadWithId:payload[@"id"]]; - [[MLNotificationQueue currentQueue] postNotificationName:kMonalRefresh object:nil userInfo:nil]; - if(self.activeChats.currentChatViewController != nil) - { - [self.activeChats.currentChatViewController scrollToBottomAnimated:NO]; - [self.activeChats.currentChatViewController hideUploadHUD]; - } - //send next item (if there is one left) - [self sendAllOutboxes]; - }; - - monal_id_block_t sendItem = ^(id dummy __unused){ - BOOL encrypted = [[DataLayer sharedInstance] shouldEncryptForJid:contact.contactJid andAccountNo:contact.accountId]; - if([payload[@"type"] isEqualToString:@"text"]) - { - [[MLXMPPManager sharedInstance] sendMessageAndAddToHistory:payload[@"data"] havingType:kMessageTypeText toContact:contact isEncrypted:encrypted uploadInfo:nil withCompletionHandler:^(BOOL successSendObject, NSString* messageIdSentObject) { - DDLogInfo(@"SHARESHEET_SEND_DATA success=%@, account=%@, messageIdSentObject=%@", bool2str(successSendObject), account.accountNo, messageIdSentObject); - cleanup(payload); - }]; - } - else if([payload[@"type"] isEqualToString:@"url"]) - { - [[MLXMPPManager sharedInstance] sendMessageAndAddToHistory:payload[@"data"] havingType:kMessageTypeUrl toContact:contact isEncrypted:encrypted uploadInfo:nil withCompletionHandler:^(BOOL successSendObject, NSString* messageIdSentObject) { - DDLogInfo(@"SHARESHEET_SEND_DATA success=%@, account=%@, messageIdSentObject=%@", bool2str(successSendObject), account.accountNo, messageIdSentObject); - cleanup(payload); - }]; - } - else if([payload[@"type"] isEqualToString:@"geo"]) - { - [[MLXMPPManager sharedInstance] sendMessageAndAddToHistory:payload[@"data"] havingType:kMessageTypeGeo toContact:contact isEncrypted:encrypted uploadInfo:nil withCompletionHandler:^(BOOL successSendObject, NSString* messageIdSentObject) { - DDLogInfo(@"SHARESHEET_SEND_DATA success=%@, account=%@, messageIdSentObject=%@", bool2str(successSendObject), account.accountNo, messageIdSentObject); - cleanup(payload); - }]; - } - else if([payload[@"type"] isEqualToString:@"image"] || [payload[@"type"] isEqualToString:@"file"] || [payload[@"type"] isEqualToString:@"contact"] || [payload[@"type"] isEqualToString:@"audiovisual"]) + DDLogInfo(@"Sending outbox entry: %@", payload); + xmpp* account = [[MLXMPPManager sharedInstance] getConnectedAccountForID:payload[@"account_id"]]; + if(account == nil) { - DDLogInfo(@"Got %@ upload: %@", payload[@"type"], payload[@"data"]); - [self.activeChats.currentChatViewController showUploadHUD]; - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - $call(payload[@"data"], $ID(account), $BOOL(encrypted), $ID(completion, (^(NSString* url, NSString* mimeType, NSNumber* size, NSError* error) { - dispatch_async(dispatch_get_main_queue(), ^{ - if(error != nil) - { - DDLogError(@"Failed to upload outbox file: %@", error); - NSMutableDictionary* payloadCopy = [NSMutableDictionary dictionaryWithDictionary:payload]; - cleanup(payloadCopy); - - UIAlertController* messageAlert = [UIAlertController alertControllerWithTitle:NSLocalizedString(@"Failed to share file", @"") message:[NSString stringWithFormat:NSLocalizedString(@"Error: %@", @""), error] preferredStyle:UIAlertControllerStyleAlert]; - [messageAlert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Close", @"") style:UIAlertActionStyleCancel handler:^(UIAlertAction* action __unused) { - }]]; - [self.activeChats presentViewController:messageAlert animated:YES completion:nil]; - } - else - [[MLXMPPManager sharedInstance] sendMessageAndAddToHistory:url havingType:kMessageTypeFiletransfer toContact:contact isEncrypted:encrypted uploadInfo:@{@"mimeType": mimeType, @"size": size} withCompletionHandler:^(BOOL successSendObject, NSString* messageIdSentObject) { - DDLogInfo(@"SHARESHEET_SEND_DATA success=%@, account=%@, messageIdSentObject=%@", bool2str(successSendObject), account.accountNo, messageIdSentObject); - cleanup(payload); - }]; - }); - }))); - }); + UIAlertController* messageAlert = [UIAlertController alertControllerWithTitle:NSLocalizedString(@"Sharing failed", @"") message:[NSString stringWithFormat:NSLocalizedString(@"Cannot share something with disabled/deleted account, destination: %@, internal account id: %@", @""), payload[@"recipient"], payload[@"account_id"]] preferredStyle:UIAlertControllerStyleAlert]; + [messageAlert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Close", @"") style:UIAlertActionStyleCancel handler:^(UIAlertAction* action __unused) { + }]]; + [self.activeChats presentViewController:messageAlert animated:YES completion:nil]; + [[DataLayer sharedInstance] deleteShareSheetPayloadWithId:payload[@"id"]]; + continue; } - else - unreachable(@"Outbox payload type unknown", payload); - }; - - DDLogVerbose(@"Trying to open chat of outbox receiver: %@", contact); - [[DataLayer sharedInstance] addActiveBuddies:contact.contactJid forAccount:contact.accountId]; - //don't use [self openChatOfContact:withCompletion:] because it's asynchronous and can only handle one contact at a time (e.g. until the asynchronous execution finished) - //we can invoke the activeChats interface directly instead, because we already did the necessary preparations ourselves - [(ActiveChatsViewController*)self.activeChats presentChatWithContact:contact andCompletion:sendItem]; - - //only send one item at a time (this method will be invoked again when sending completed) - break; - } + MLContact* contact = [MLContact createContactFromJid:payload[@"recipient"] andAccountNo:account.accountNo]; + + monal_id_block_t cleanup = ^(NSDictionary* payload) { + [[DataLayer sharedInstance] deleteShareSheetPayloadWithId:payload[@"id"]]; + [[MLNotificationQueue currentQueue] postNotificationName:kMonalRefresh object:nil userInfo:nil]; + if(self.activeChats.currentChatViewController != nil) + { + [self.activeChats.currentChatViewController scrollToBottomAnimated:NO]; + [self.activeChats.currentChatViewController hideUploadHUD]; + } + //send next item (if there is one left) + [self sendAllOutboxes]; + }; + + monal_id_block_t sendItem = ^(id dummy __unused){ + BOOL encrypted = [[DataLayer sharedInstance] shouldEncryptForJid:contact.contactJid andAccountNo:contact.accountId]; + if([payload[@"type"] isEqualToString:@"text"]) + { + [[MLXMPPManager sharedInstance] sendMessageAndAddToHistory:payload[@"data"] havingType:kMessageTypeText toContact:contact isEncrypted:encrypted uploadInfo:nil withCompletionHandler:^(BOOL successSendObject, NSString* messageIdSentObject) { + DDLogInfo(@"SHARESHEET_SEND_DATA success=%@, account=%@, messageIdSentObject=%@", bool2str(successSendObject), account.accountNo, messageIdSentObject); + cleanup(payload); + }]; + } + else if([payload[@"type"] isEqualToString:@"url"]) + { + [[MLXMPPManager sharedInstance] sendMessageAndAddToHistory:payload[@"data"] havingType:kMessageTypeUrl toContact:contact isEncrypted:encrypted uploadInfo:nil withCompletionHandler:^(BOOL successSendObject, NSString* messageIdSentObject) { + DDLogInfo(@"SHARESHEET_SEND_DATA success=%@, account=%@, messageIdSentObject=%@", bool2str(successSendObject), account.accountNo, messageIdSentObject); + cleanup(payload); + }]; + } + else if([payload[@"type"] isEqualToString:@"geo"]) + { + [[MLXMPPManager sharedInstance] sendMessageAndAddToHistory:payload[@"data"] havingType:kMessageTypeGeo toContact:contact isEncrypted:encrypted uploadInfo:nil withCompletionHandler:^(BOOL successSendObject, NSString* messageIdSentObject) { + DDLogInfo(@"SHARESHEET_SEND_DATA success=%@, account=%@, messageIdSentObject=%@", bool2str(successSendObject), account.accountNo, messageIdSentObject); + cleanup(payload); + }]; + } + else if([payload[@"type"] isEqualToString:@"image"] || [payload[@"type"] isEqualToString:@"file"] || [payload[@"type"] isEqualToString:@"contact"] || [payload[@"type"] isEqualToString:@"audiovisual"]) + { + DDLogInfo(@"Got %@ upload: %@", payload[@"type"], payload[@"data"]); + [self.activeChats.currentChatViewController showUploadHUD]; + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + $call(payload[@"data"], $ID(account), $BOOL(encrypted), $ID(completion, (^(NSString* url, NSString* mimeType, NSNumber* size, NSError* error) { + dispatch_async(dispatch_get_main_queue(), ^{ + if(error != nil) + { + DDLogError(@"Failed to upload outbox file: %@", error); + NSMutableDictionary* payloadCopy = [NSMutableDictionary dictionaryWithDictionary:payload]; + cleanup(payloadCopy); + + UIAlertController* messageAlert = [UIAlertController alertControllerWithTitle:NSLocalizedString(@"Failed to share file", @"") message:[NSString stringWithFormat:NSLocalizedString(@"Error: %@", @""), error] preferredStyle:UIAlertControllerStyleAlert]; + [messageAlert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Close", @"") style:UIAlertActionStyleCancel handler:^(UIAlertAction* action __unused) { + }]]; + [self.activeChats presentViewController:messageAlert animated:YES completion:nil]; + } + else + [[MLXMPPManager sharedInstance] sendMessageAndAddToHistory:url havingType:kMessageTypeFiletransfer toContact:contact isEncrypted:encrypted uploadInfo:@{@"mimeType": mimeType, @"size": size} withCompletionHandler:^(BOOL successSendObject, NSString* messageIdSentObject) { + DDLogInfo(@"SHARESHEET_SEND_DATA success=%@, account=%@, messageIdSentObject=%@", bool2str(successSendObject), account.accountNo, messageIdSentObject); + cleanup(payload); + }]; + }); + }))); + }); + } + else + unreachable(@"Outbox payload type unknown", payload); + }; + + DDLogVerbose(@"Trying to open chat of outbox receiver: %@", contact); + [[DataLayer sharedInstance] addActiveBuddies:contact.contactJid forAccount:contact.accountId]; + //don't use [self openChatOfContact:withCompletion:] because it's asynchronous and can only handle one contact at a time (e.g. until the asynchronous execution finished) + //we can invoke the activeChats interface directly instead, because we already did the necessary preparations ourselves + [(ActiveChatsViewController*)self.activeChats presentChatWithContact:contact andCompletion:sendItem]; + + //only send one item at a time (this method will be invoked again when sending completed) + break; + } + }]; } @end From 87e8fa52f47e6c19d453ebd5e1ef6a096acc6409 Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Sat, 3 Aug 2024 01:54:57 +0200 Subject: [PATCH 04/22] Make sure to not create deadlocks when using our active chats view stack --- Monal/Classes/ActiveChatsViewController.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Monal/Classes/ActiveChatsViewController.m b/Monal/Classes/ActiveChatsViewController.m index e616e4dec5..d6d98979cd 100755 --- a/Monal/Classes/ActiveChatsViewController.m +++ b/Monal/Classes/ActiveChatsViewController.m @@ -186,7 +186,7 @@ -(void) replaceIdOnViewQueue:(MLViewID) previousId withBlock:(view_queue_block_t -(void) processViewQueue { //we are using uikit api all over the place: make sure we always run in the main queue - [HelperTools dispatchAsync:NO reentrantOnQueue:dispatch_get_main_queue() withBlock:^{ + [HelperTools dispatchAsync:YES reentrantOnQueue:dispatch_get_main_queue() withBlock:^{ NSMutableArray* viewControllerHierarchy = [self getCurrentViewControllerHierarchy]; //don't show the next entry if there is still the previous one From ddc28b649fa3d338f9245b216ea4ef7aec5f8da3 Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Sat, 3 Aug 2024 02:33:13 +0200 Subject: [PATCH 05/22] Don't show notifications for outgoing media files sent from 2nd device --- Monal/Classes/MLNotificationManager.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Monal/Classes/MLNotificationManager.m b/Monal/Classes/MLNotificationManager.m index 75ebb7a0c2..b1d28917b6 100644 --- a/Monal/Classes/MLNotificationManager.m +++ b/Monal/Classes/MLNotificationManager.m @@ -224,7 +224,7 @@ -(void) handleFiletransferUpdate:(NSNotification*) notification //do this asynchronous on a background thread [self notificationStateForMessage:message].thenInBackground(^(NSNumber* _state) { MLNotificationState state = _state.integerValue; - if(state == MLNotificationStatePending || state == MLNotificationStateNone) + if(state == MLNotificationStatePending) { DDLogDebug(@"Already pending or unknown notification '%@', updating/posting it...", idval); [self internalMessageHandlerWithMessage:message andAccount:xmppAccount showAlert:YES andSound:YES andLMCReplaced:NO]; From 4ff0cd690b6dd6936d1bc024aad94c28f38d5c5e Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Sat, 3 Aug 2024 22:59:02 +0200 Subject: [PATCH 06/22] Fix SASL1 to SASL2 upgrade path for LDAP, fixes #1186 --- Monal/Classes/DataLayerMigrations.m | 6 ++++++ Monal/Classes/xmpp.m | 10 +++++----- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/Monal/Classes/DataLayerMigrations.m b/Monal/Classes/DataLayerMigrations.m index 12e924d8f0..7288d88609 100644 --- a/Monal/Classes/DataLayerMigrations.m +++ b/Monal/Classes/DataLayerMigrations.m @@ -1080,6 +1080,12 @@ FOREIGN KEY('account_id') REFERENCES 'account'('account_id') ON DELETE CASCADE \ [db executeNonQuery:@"UPDATE buddylist SET muc_type=NULL;"]; }]; + //reactivate PLAIN auth on all accounts to allow proper upgrades to servers only supporting PLAIN even with SASL2 + //this should fix issue #1186 + [self updateDB:db withDataLayer:dataLayer toVersion:6.405 withBlock:^{ + [db executeNonQuery:@"UPDATE account SET plain_activated=true, supports_sasl2=false;"]; + }]; + //check if device id changed and invalidate state, if so //but do so only for non-sandbox (e.g. non-development) installs diff --git a/Monal/Classes/xmpp.m b/Monal/Classes/xmpp.m index fb245b58fc..2cc25fa1da 100644 --- a/Monal/Classes/xmpp.m +++ b/Monal/Classes/xmpp.m @@ -2679,6 +2679,11 @@ -(void) processInput:(MLXMLNode*) parsedStanza withDelayedReplay:(BOOL) delayedR } self->_loggedInOnce = YES; + //pin sasl2 support for this account (this is done only after successful auth to prevent DOS MITM attacks simulating SASL2 support) + //downgrading to SASL1 would mean PLAIN instead of SCRAM and no protocol agility for channel-bindings, + //if XEP-0440 is not supported by server + [[DataLayer sharedInstance] pinSasl2ForAccount:self.accountNo]; + //NOTE: we don't have any stream restart when using SASL2 //NOTE: we don't need to pipeline anything here, because SASL2 sends out the new stream features immediately without a stream restart _cachedStreamFeaturesAfterAuth = nil; //make sure we don't accidentally try to do pipelining @@ -3016,11 +3021,6 @@ -(void) handleFeaturesBeforeAuth:(MLXMLNode*) parsedStanza noAuthSupported(); }; - //pin sasl2 support for this account - //downgrading to SASL1 would mean PLAIN instead of SCRAM and no protocol agility for channel-bindings - //if XEP-0440 is not supported by server - [[DataLayer sharedInstance] pinSasl2ForAccount:self.accountNo]; - //extract menchanisms presented _supportedSaslMechanisms = [NSSet setWithArray:[parsedStanza find:@"{urn:xmpp:sasl:2}authentication/mechanism#"]]; From 0a293dab0b5f9596794fd6cf6015442eeba9e7d8 Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Sun, 4 Aug 2024 01:07:10 +0200 Subject: [PATCH 07/22] Improve monal_id_returning_void_block_t type --- Monal/Classes/MLConstants.h | 2 +- Monal/Classes/SwiftHelpers.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Monal/Classes/MLConstants.h b/Monal/Classes/MLConstants.h index 2af2b6e0e9..f5532f429e 100644 --- a/Monal/Classes/MLConstants.h +++ b/Monal/Classes/MLConstants.h @@ -76,7 +76,7 @@ typedef void (^contactCompletion)(MLContact* _Nonnull selectedContact) NS_SWIFT_ typedef void (^accountCompletion)(NSInteger accountRow) NS_SWIFT_UNAVAILABLE("To be redefined in swift."); typedef void (^monal_void_block_t)(void) NS_SWIFT_UNAVAILABLE("To be redefined in swift."); typedef void (^monal_id_block_t)(id _Nonnull) NS_SWIFT_UNAVAILABLE("To be redefined in swift."); -typedef id _Nullable (^monal_id_returning_block_t)(id _Nonnull) NS_SWIFT_UNAVAILABLE("To be redefined in swift."); +typedef id _Nullable (^monal_id_returning_void_block_t)(void) NS_SWIFT_UNAVAILABLE("To be redefined in swift."); typedef id _Nullable (^monal_id_returning_id_block_t)(id _Nonnull) NS_SWIFT_UNAVAILABLE("To be redefined in swift."); typedef void (^monal_upload_completion_t)(NSString* _Nullable url, NSString* _Nullable mimeType, NSNumber* _Nullable size, NSError* _Nullable error) NS_SWIFT_UNAVAILABLE("To be redefined in swift."); diff --git a/Monal/Classes/SwiftHelpers.swift b/Monal/Classes/SwiftHelpers.swift index 7e6a85c642..dbc85413f0 100644 --- a/Monal/Classes/SwiftHelpers.swift +++ b/Monal/Classes/SwiftHelpers.swift @@ -32,7 +32,7 @@ let BGFETCH_DEFAULT_INTERVAL = HelperTools.getObjcDefinedValue(.BGFETCH_DEFAULT_ public typealias monal_timer_block_t = @convention(block) (MLDelayableTimer?) -> Void; public typealias monal_void_block_t = @convention(block) () -> Void; public typealias monal_id_block_t = @convention(block) (AnyObject?) -> Void; -public typealias monal_id_returning_block_t = @convention(block) () -> AnyObject?; +public typealias monal_id_returning_void_block_t = @convention(block) () -> AnyObject?; public typealias monal_id_returning_id_block_t = @convention(block) (AnyObject?) -> AnyObject?; //see https://stackoverflow.com/a/40629365/3528174 From bc51396977ac37d0b64d13e8f1ee3b4bf676c054 Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Sun, 4 Aug 2024 00:57:20 +0200 Subject: [PATCH 08/22] Streamline code for better SASL2 DoS protection --- Monal/Classes/DataLayer.h | 3 -- Monal/Classes/DataLayer.m | 22 +---------- Monal/Classes/DataLayerMigrations.m | 5 +++ Monal/Classes/XMPPEdit.m | 6 --- Monal/Classes/xmpp.m | 59 ++++++++++++++++------------- 5 files changed, 39 insertions(+), 56 deletions(-) diff --git a/Monal/Classes/DataLayer.h b/Monal/Classes/DataLayer.h index ef9b77818c..e0ec4578a4 100644 --- a/Monal/Classes/DataLayer.h +++ b/Monal/Classes/DataLayer.h @@ -22,7 +22,6 @@ extern NSString* const kAccountState; extern NSString* const kDomain; extern NSString* const kEnabled; extern NSString* const kNeedsPasswordMigration; -extern NSString* const kSupportsSasl2; extern NSString* const kPlainActivated; extern NSString* const kServer; @@ -165,8 +164,6 @@ extern NSString* const kMessageTypeFiletransfer; -(BOOL) disableAccountForPasswordMigration:(NSNumber*) accountNo; -(NSArray*) accountListNeedingPasswordMigration; --(BOOL) pinSasl2ForAccount:(NSNumber*) accountNo; --(BOOL) isSasl2PinnedForAccount:(NSNumber*) accountNo; -(BOOL) isPlainActivatedForAccount:(NSNumber*) accountNo; -(BOOL) deactivatePlainForAccount:(NSNumber*) accountNo; diff --git a/Monal/Classes/DataLayer.m b/Monal/Classes/DataLayer.m index 22e80889e5..362db4a462 100644 --- a/Monal/Classes/DataLayer.m +++ b/Monal/Classes/DataLayer.m @@ -33,7 +33,6 @@ @implementation DataLayer NSString *const kDomain = @"domain"; NSString *const kEnabled = @"enabled"; NSString *const kNeedsPasswordMigration = @"needs_password_migration"; -NSString *const kSupportsSasl2 = @"supports_sasl2"; NSString *const kPlainActivated = @"plain_activated"; NSString *const kServer = @"server"; @@ -245,7 +244,7 @@ -(BOOL) updateAccounWithDictionary:(NSDictionary*) dictionary { return [self.db boolWriteTransaction:^{ DDLogVerbose(@"Updating account with: %@", dictionary); - NSString* query = @"UPDATE account SET server=?, other_port=?, username=?, resource=?, domain=?, enabled=?, directTLS=?, rosterName=?, statusMessage=?, needs_password_migration=?, supports_sasl2=? WHERE account_id=?;"; + NSString* query = @"UPDATE account SET server=?, other_port=?, username=?, resource=?, domain=?, enabled=?, directTLS=?, rosterName=?, statusMessage=?, needs_password_migration=? WHERE account_id=?;"; NSString* server = (NSString*)[dictionary objectForKey:kServer]; NSString* port = (NSString*)[dictionary objectForKey:kPort]; NSArray* params = @[ @@ -259,7 +258,6 @@ -(BOOL) updateAccounWithDictionary:(NSDictionary*) dictionary [dictionary objectForKey:kRosterName] ? ((NSString*)[dictionary objectForKey:kRosterName]) : @"", [dictionary objectForKey:@"statusMessage"] ? ((NSString*)[dictionary objectForKey:@"statusMessage"]) : @"", [dictionary objectForKey:kNeedsPasswordMigration], - [dictionary objectForKey:kSupportsSasl2], [dictionary objectForKey:kAccountID], ]; BOOL retval = [self.db executeNonQuery:query andArguments:params]; @@ -335,24 +333,6 @@ -(NSArray*) accountListNeedingPasswordMigration }]; } --(BOOL) pinSasl2ForAccount:(NSNumber*) accountNo -{ - return [self.db boolWriteTransaction:^{ - return [self.db executeNonQuery:@"UPDATE account SET supports_sasl2=1 WHERE account_id=?;" andArguments:@[accountNo]]; - }]; -} - --(BOOL) isSasl2PinnedForAccount:(NSNumber*) accountNo -{ - return [self.db boolReadTransaction:^{ - NSNumber* sasl2Pinned = (NSNumber*)[self.db executeScalar:@"SELECT supports_sasl2 FROM account WHERE account_id=?;" andArguments:@[accountNo]]; - if(sasl2Pinned == nil) - return NO; - else - return [sasl2Pinned boolValue]; - }]; -} - -(BOOL) isPlainActivatedForAccount:(NSNumber*) accountNo { return [self.db boolReadTransaction:^{ diff --git a/Monal/Classes/DataLayerMigrations.m b/Monal/Classes/DataLayerMigrations.m index 7288d88609..5644c50777 100644 --- a/Monal/Classes/DataLayerMigrations.m +++ b/Monal/Classes/DataLayerMigrations.m @@ -1086,6 +1086,11 @@ FOREIGN KEY('account_id') REFERENCES 'account'('account_id') ON DELETE CASCADE \ [db executeNonQuery:@"UPDATE account SET plain_activated=true, supports_sasl2=false;"]; }]; + //streamlined code with only plain_activated column + [self updateDB:db withDataLayer:dataLayer toVersion:6.406 withBlock:^{ + [db executeNonQuery:@"ALTER TABLE account DROP COLUMN 'supports_sasl2';"]; + }]; + //check if device id changed and invalidate state, if so //but do so only for non-sandbox (e.g. non-development) installs diff --git a/Monal/Classes/XMPPEdit.m b/Monal/Classes/XMPPEdit.m index 2fff9c5c26..5643fc753f 100644 --- a/Monal/Classes/XMPPEdit.m +++ b/Monal/Classes/XMPPEdit.m @@ -112,7 +112,6 @@ @interface XMPPEdit() @property (nonatomic) BOOL statusMessageChanged; @property (nonatomic) BOOL detailsChanged; -@property (nonatomic) BOOL sasl2Supported; @property (nonatomic) BOOL plainActivated; @property (nonatomic) BOOL deactivateSave; @@ -204,8 +203,6 @@ -(void) viewDidLoad self.rosterName = [settings objectForKey:kRosterName]; self.statusMessage = [settings objectForKey:@"statusMessage"]; - self.sasl2Supported = [[settings objectForKey:kSupportsSasl2] boolValue]; - self.plainActivated = [[settings objectForKey:kPlainActivated] boolValue]; //overwrite account section heading in edit mode @@ -220,7 +217,6 @@ -(void) viewDidLoad self.rosterName = @""; self.statusMessage = @""; self.enabled = YES; - self.sasl2Supported = NO; self.plainActivated = NO; //overwrite account section heading in new mode @@ -343,8 +339,6 @@ -(IBAction) save:(id) sender if(self.statusMessage) [dic setObject:[self.statusMessage stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]] forKey:@"statusMessage"]; - [dic setObject:[NSNumber numberWithBool:self.sasl2Supported] forKey:kSupportsSasl2]; - [dic setObject:[NSNumber numberWithBool:self.plainActivated] forKey:kPlainActivated]; if(!self.editMode) diff --git a/Monal/Classes/xmpp.m b/Monal/Classes/xmpp.m index 2cc25fa1da..3ca7727354 100644 --- a/Monal/Classes/xmpp.m +++ b/Monal/Classes/xmpp.m @@ -2682,7 +2682,7 @@ -(void) processInput:(MLXMLNode*) parsedStanza withDelayedReplay:(BOOL) delayedR //pin sasl2 support for this account (this is done only after successful auth to prevent DOS MITM attacks simulating SASL2 support) //downgrading to SASL1 would mean PLAIN instead of SCRAM and no protocol agility for channel-bindings, //if XEP-0440 is not supported by server - [[DataLayer sharedInstance] pinSasl2ForAccount:self.accountNo]; + [[DataLayer sharedInstance] deactivatePlainForAccount:self.accountNo]; //NOTE: we don't have any stream restart when using SASL2 //NOTE: we don't need to pipeline anything here, because SASL2 sends out the new stream features immediately without a stream restart @@ -2897,6 +2897,21 @@ -(void) processInput:(MLXMLNode*) parsedStanza withDelayedReplay:(BOOL) delayedR -(void) handleFeaturesBeforeAuth:(MLXMLNode*) parsedStanza { + return [self handleFeaturesBeforeAuth:parsedStanza withForceSasl2:NO]; +} + +-(void) handleFeaturesBeforeAuth:(MLXMLNode*) parsedStanza withForceSasl2:(BOOL) forceSasl2 +{ + monal_id_returning_void_block_t checkProperSasl2Support = ^{ + //check if we SASL2 is supported with something better than PLAIN and, if so, switch off plain_activated + NSSet* supportedSasl2Mechanisms = [NSSet setWithArray:[parsedStanza find:@"{urn:xmpp:sasl:2}authentication/mechanism#"]]; + for(NSString* mechanism in [SCRAM supportedMechanismsIncludingChannelBinding:YES]) + if([supportedSasl2Mechanisms containsObject:mechanism]) + { + return @YES; + } + return @NO; + }; monal_id_block_t clearPipelineCacheOrReportSevereError = ^(NSString* msg) { DDLogWarn(@"Clearing auth pipeline due to error..."); @@ -2925,23 +2940,22 @@ -(void) handleFeaturesBeforeAuth:(MLXMLNode*) parsedStanza //display scary warning message if sasl2 is pinned and login was successful at least once //or display a message pointing to the advanced account creation menu if sasl2 is pinned and login was NOT successful at least once //(e.g. we are trying to create this account just now) - if([[DataLayer sharedInstance] isSasl2PinnedForAccount:self.accountNo]) + if(![[DataLayer sharedInstance] isPlainActivatedForAccount:self.accountNo]) { if(self->_loggedInOnce) { clearPipelineCacheOrReportSevereError(NSLocalizedString(@"Server suddenly lacks support for SASL2-SCRAM, ongoing MITM attack highly likely, aborting authentication and disabling account to limit damage. You should try to reenable your account once you are in a clean networking environment again.", @"")); + return; } else if([self->_supportedSaslMechanisms containsObject:@"PLAIN"]) { clearPipelineCacheOrReportSevereError(NSLocalizedString(@"Server only supports authentication methods not safe against man-in-the-middle attacks! Use the advanced account creation menu if you absolutely must use this server.", @"")); + return; } } - else - clearPipelineCacheOrReportSevereError(NSLocalizedString(@"No supported auth mechanism found, disabling account!", @"")); + clearPipelineCacheOrReportSevereError(NSLocalizedString(@"No supported auth mechanism found, disabling account!", @"")); }; - MLAssert(!([[DataLayer sharedInstance] isSasl2PinnedForAccount:self.accountNo] && [[DataLayer sharedInstance] isPlainActivatedForAccount:self.accountNo]), @"SASL2 pinned AND plain auth enabled, that should never happen!", @{@"account": self}); - if(![parsedStanza check:@"{urn:xmpp:ibr-token:0}register"]) DDLogWarn(@"Server NOT supporting Pre-Authenticated IBR"); if(_registration) @@ -2963,7 +2977,7 @@ -(void) handleFeaturesBeforeAuth:(MLXMLNode*) parsedStanza [self submitRegForm]; } //prefer SASL2 over SASL1 - else if([parsedStanza check:@"{urn:xmpp:sasl:2}authentication/mechanism"] && ![[DataLayer sharedInstance] isPlainActivatedForAccount:self.accountNo]) + else if([parsedStanza check:@"{urn:xmpp:sasl:2}authentication/mechanism"] && (![[DataLayer sharedInstance] isPlainActivatedForAccount:self.accountNo] || forceSasl2)) { weakify(self); _blockToCallOnTCPOpen = ^{ @@ -3050,19 +3064,17 @@ -(void) handleFeaturesBeforeAuth:(MLXMLNode*) parsedStanza else DDLogWarn(@"Waiting until TLS stream is connected before pipelining the auth element due to channel binding..."); } + //check if the server activated SASL2 after previously only upporting SASL1 + else if([[DataLayer sharedInstance] isPlainActivatedForAccount:self.accountNo] && ((NSNumber*)checkProperSasl2Support()).boolValue) + { + DDLogInfo(@"We detected SASL2 SCRAM support, deactivating forced SASL1 PLAIN fallback and retrying using SASL2..."); + [[DataLayer sharedInstance] deactivatePlainForAccount:self.accountNo]; + //try again, this time using sasl2 + return [self handleFeaturesBeforeAuth:parsedStanza withForceSasl2:YES]; + } //SASL1 is fallback only if SASL2 isn't supported with something better than PLAIN - else if([parsedStanza check:@"{urn:ietf:params:xml:ns:xmpp-sasl}mechanisms/mechanism"] && ![[DataLayer sharedInstance] isSasl2PinnedForAccount:self.accountNo]) + else if([parsedStanza check:@"{urn:ietf:params:xml:ns:xmpp-sasl}mechanisms/mechanism"] && [[DataLayer sharedInstance] isPlainActivatedForAccount:self.accountNo]) { - //check if we SASL2 is supported with something better than PLAIN and, if so, switch off plain_activated - NSSet* supportedSasl2Mechanisms = [NSSet setWithArray:[parsedStanza find:@"{urn:xmpp:sasl:2}authentication/mechanism#"]]; - for(NSString* mechanism in [SCRAM supportedMechanismsIncludingChannelBinding:YES]) - if([supportedSasl2Mechanisms containsObject:mechanism]) - { - DDLogInfo(@"We detected SASL2 SCRAM support, deactivating forced SASL1 PLAIN fallback and retrying using SASL2..."); - [[DataLayer sharedInstance] deactivatePlainForAccount:self.accountNo]; - //try again, this time using sasl2 - return [self handleFeaturesBeforeAuth:parsedStanza]; - } //extract menchanisms presented NSSet* supportedSaslMechanisms = [NSSet setWithArray:[parsedStanza find:@"{urn:ietf:params:xml:ns:xmpp-sasl}mechanisms/mechanism#"]]; @@ -3104,15 +3116,10 @@ -(void) handleFeaturesBeforeAuth:(MLXMLNode*) parsedStanza } else { - //this is not a downgrade but something weird going on, report it as "no supported auth" + //this is not a downgrade but something weird going on, log it as such if(![parsedStanza check:@"{urn:xmpp:sasl:2}authentication/mechanism"] && ![parsedStanza check:@"{urn:ietf:params:xml:ns:xmpp-sasl}mechanisms/mechanism"]) - { - noAuthSupported(); - return; - } - - //if the above case didn't trigger, this is a downgrade attack downgrading from SASL2 to SASL1, report is as such - clearPipelineCacheOrReportSevereError(NSLocalizedString(@"SASL2 to SASL1 downgrade attack detected, aborting authentication and disabling account to limit damage. You should try to reenable your account once you are in a clean networking environment again.", @"")); + DDLogError(@"Something weird happened: neither SASL1 nor SASL2 auth supported by this server!"); + noAuthSupported(); } } From efa0b9c652efd2901184775f42c6954ee4d06954 Mon Sep 17 00:00:00 2001 From: Friedrich Altheide <11352905+FriedrichAltheide@users.noreply.github.com> Date: Sun, 4 Aug 2024 07:15:01 +0200 Subject: [PATCH 09/22] only set plain_activated to true when supports_sasl2 is false --- Monal/Classes/DataLayerMigrations.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Monal/Classes/DataLayerMigrations.m b/Monal/Classes/DataLayerMigrations.m index 5644c50777..d34b302d9b 100644 --- a/Monal/Classes/DataLayerMigrations.m +++ b/Monal/Classes/DataLayerMigrations.m @@ -1083,7 +1083,7 @@ FOREIGN KEY('account_id') REFERENCES 'account'('account_id') ON DELETE CASCADE \ //reactivate PLAIN auth on all accounts to allow proper upgrades to servers only supporting PLAIN even with SASL2 //this should fix issue #1186 [self updateDB:db withDataLayer:dataLayer toVersion:6.405 withBlock:^{ - [db executeNonQuery:@"UPDATE account SET plain_activated=true, supports_sasl2=false;"]; + [db executeNonQuery:@"UPDATE account SET plain_activated=true WHERE supports_sasl2=false;"]; }]; //streamlined code with only plain_activated column From f13caf0c9374dac6270ae1a976c549174a5146f5 Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Sun, 4 Aug 2024 07:59:58 +0200 Subject: [PATCH 10/22] Fix non-alpha build upload script --- .github/workflows/beta.build-push.yml | 5 ++++- .github/workflows/stable.build-push.yml | 5 ++++- scripts/push_xmpp.org.sh | 2 +- scripts/uploadNonAlpha.sh | 3 --- 4 files changed, 9 insertions(+), 6 deletions(-) diff --git a/.github/workflows/beta.build-push.yml b/.github/workflows/beta.build-push.yml index f5e5465d23..790700f5f0 100644 --- a/.github/workflows/beta.build-push.yml +++ b/.github/workflows/beta.build-push.yml @@ -203,7 +203,10 @@ jobs: path: Monal/build/app/Monal.pkg if-no-files-found: error - name: Upload new catalyst beta to monal-im.org - run: ./scripts/uploadNonAlpha.sh beta + env: + UPLOAD_TYPE: beta + buildNumber: ${{ steps.releasenotes.outputs.buildNumber }} + run: ./scripts/uploadNonAlpha.sh - name: Publish catalyst to appstore connect #run: xcrun altool --upload-app --file ./Monal/build/app/Monal.pkg --type macos --asc-provider S8D843U34Y -u "$(cat /Users/ci/apple_connect_upload_mail.txt)" -p "$(cat /Users/ci/apple_connect_upload_secret.txt)" --primary-bundle-id org.monal-im.prod.catalyst.monal env: diff --git a/.github/workflows/stable.build-push.yml b/.github/workflows/stable.build-push.yml index da24ed9a53..bd29023fbc 100644 --- a/.github/workflows/stable.build-push.yml +++ b/.github/workflows/stable.build-push.yml @@ -175,7 +175,10 @@ jobs: path: Monal/build/app/Monal.pkg if-no-files-found: error - name: Upload new catalyst stable to monal-im.org - run: ./scripts/uploadNonAlpha.sh stable + env: + UPLOAD_TYPE: stable + buildNumber: ${{ steps.releasenotes.outputs.buildNumber }} + run: ./scripts/uploadNonAlpha.sh - name: Publish catalyst to appstore connect #run: xcrun altool --upload-app --file ./Monal/build/app/Monal.pkg --type macos --asc-provider S8D843U34Y -u "$(cat /Users/ci/apple_connect_upload_mail.txt)" -p "$(cat /Users/ci/apple_connect_upload_secret.txt)" --primary-bundle-id maccatalyst.G7YU7X7KRJ.SworIM env: diff --git a/scripts/push_xmpp.org.sh b/scripts/push_xmpp.org.sh index b2d3db3471..dc9fda2875 100755 --- a/scripts/push_xmpp.org.sh +++ b/scripts/push_xmpp.org.sh @@ -9,7 +9,7 @@ echo "" echo "**********************************************" echo "* Reading buildNumber and creating timestamp *" echo "**********************************************" -buildNumber=$(git tag --sort="v:refname" |grep "Build_iOS" | tail -n1 | sed 's/Build_iOS_//g') +buildNumber=$(git tag --sort="v:refname" | grep -v "Quicksy_Build_iOS" | grep "Build_iOS" | tail -n1 | sed 's/Build_iOS_//g') timestamp="$(date -u +%FT%T)" echo "" diff --git a/scripts/uploadNonAlpha.sh b/scripts/uploadNonAlpha.sh index 0e327a3478..da39562ece 100755 --- a/scripts/uploadNonAlpha.sh +++ b/scripts/uploadNonAlpha.sh @@ -1,9 +1,6 @@ #!/bin/sh -UPLOAD_TYPE=$1 - function sftp_upload { - buildNumber=$(git tag --sort="v:refname" |grep "Build_iOS" | tail -n1 | sed 's/Build_iOS_//g') echo "${buildNumber}" > build/app/latest.txt sftp ${1} < Date: Mon, 5 Aug 2024 02:00:28 +0200 Subject: [PATCH 11/22] Allow PLAIN auth after account registration --- Monal/Classes/MLXMPPManager.m | 20 ++++++++++++++------ Monal/Classes/RegisterAccount.swift | 9 ++++++++- Monal/Classes/XMPPEdit.m | 3 ++- Monal/Classes/xmpp.m | 13 ++++++++++--- 4 files changed, 34 insertions(+), 11 deletions(-) diff --git a/Monal/Classes/MLXMPPManager.m b/Monal/Classes/MLXMPPManager.m index c316366767..1aa9fa13a6 100644 --- a/Monal/Classes/MLXMPPManager.m +++ b/Monal/Classes/MLXMPPManager.m @@ -706,16 +706,16 @@ -(void) sendChatState:(BOOL) isTyping toContact:(MLContact*) contact //this will NOT set plain_activated to YES, only using the advanced account creation ui can do this -(NSNumber*) login:(NSString*) jid password:(NSString*) password { - //if it is a JID + //check if it is a JID NSArray* elements = [jid componentsSeparatedByString:@"@"]; MLAssert([elements count] > 1, @"Got invalid jid", (@{@"jid": nilWrapper(jid), @"elements": elements})); NSString* domain; NSString* user; - user = [elements objectAtIndex:0]; - domain = [elements objectAtIndex:1]; + user = ((NSString*)[elements objectAtIndex:0]).lowercaseString; + domain = ((NSString*)[elements objectAtIndex:1]).lowercaseString; - if([[DataLayer sharedInstance] doesAccountExistUser:user.lowercaseString andDomain:domain.lowercaseString]) + if([[DataLayer sharedInstance] doesAccountExistUser:user andDomain:domain]) { [[MLNotificationQueue currentQueue] postNotificationName:kXMPPError object:nil userInfo:@{ @"title": NSLocalizedString(@"Duplicate Account", @""), @@ -725,11 +725,19 @@ -(NSNumber*) login:(NSString*) jid password:(NSString*) password } NSMutableDictionary* dic = [NSMutableDictionary new]; - [dic setObject:domain.lowercaseString forKey:kDomain]; - [dic setObject:user.lowercaseString forKey:kUsername]; + [dic setObject:domain forKey:kDomain]; + [dic setObject:user forKey:kUsername]; [dic setObject:[HelperTools encodeRandomResource] forKey:kResource]; [dic setObject:@YES forKey:kEnabled]; [dic setObject:@NO forKey:kDirectTLS]; + //we don't want to set kPlainActivated (not even according to our preload list) and default to plain_activated=false, + //because the error message will warn the user and direct them to the advanced account creation menu to activate PLAIN + //if they still want to connect to this server + //only exception: yax.im --> we don't want to suggest a server during account creation that has a scary warning + //when logging in using another device afterwards + //TODO: to be removed once yax.im supports SASL2 and SSDP!! + //TODO: use preload list and allow PLAIN for all others once enough domains are on this list + [dic setObject:([domain isEqualToString:@"yax.im"] ? @YES : @NO) forKey:kPlainActivated]; NSNumber* accountNo = [[DataLayer sharedInstance] addAccountWithDictionary:dic]; if(accountNo == nil) diff --git a/Monal/Classes/RegisterAccount.swift b/Monal/Classes/RegisterAccount.swift index 6a29d81238..70cfc3c9a5 100644 --- a/Monal/Classes/RegisterAccount.swift +++ b/Monal/Classes/RegisterAccount.swift @@ -201,7 +201,14 @@ struct RegisterAccount: View { kUsername: self.username, kResource: HelperTools.encodeRandomResource(), kEnabled: true, - kDirectTLS: false + kDirectTLS: false, + //creating an account involves transfering the password in cleartext only secured by TLS + //--> logging in directly afterwards using PLAIN doesn't make the situation any worse ==> allow it + //conversations.im already supports sasl2 and scram ## TODO: use SCRAM preload list + //using the preload list in this case won't solve the situation, but increase the attack cost because + //stripping off SASL2 won't suffice anymore (the attacker will have to use the password sniffed during account creation + //to fake the SCRAM HMAC sent to both client and server) + kPlainActivated: self.actualServer == "conversations.im" ? false : true, ] as [String : Any] let accountNo = DataLayer.sharedInstance().addAccount(with: dic); diff --git a/Monal/Classes/XMPPEdit.m b/Monal/Classes/XMPPEdit.m index 5643fc753f..92b0b585e6 100644 --- a/Monal/Classes/XMPPEdit.m +++ b/Monal/Classes/XMPPEdit.m @@ -339,7 +339,8 @@ -(IBAction) save:(id) sender if(self.statusMessage) [dic setObject:[self.statusMessage stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]] forKey:@"statusMessage"]; - [dic setObject:[NSNumber numberWithBool:self.plainActivated] forKey:kPlainActivated]; + //conversations.im already supports sasl2 and scram ## TODO: use SCRAM preload list + [dic setObject:([domain.lowercaseString isEqualToString:@"conversations.im"] ? @NO : @(self.plainActivated)) forKey:kPlainActivated]; if(!self.editMode) { diff --git a/Monal/Classes/xmpp.m b/Monal/Classes/xmpp.m index 3ca7727354..79f930b923 100644 --- a/Monal/Classes/xmpp.m +++ b/Monal/Classes/xmpp.m @@ -2934,7 +2934,7 @@ -(void) handleFeaturesBeforeAuth:(MLXMLNode*) parsedStanza withForceSasl2:(BOOL) }; //called below, if neither SASL1 nor SASL2 could be used to negotiate a valid SASL mechanism monal_void_block_t noAuthSupported = ^{ - DDLogWarn(@"No supported auth mechanism!"); + DDLogWarn(@"No supported auth mechanism: %@", self->_supportedSaslMechanisms); //sasl2 will be pinned if we saw sasl2 support and PLAIN was NOT allowed by creating this account using the advanced account creation menu //display scary warning message if sasl2 is pinned and login was successful at least once @@ -2942,14 +2942,16 @@ -(void) handleFeaturesBeforeAuth:(MLXMLNode*) parsedStanza withForceSasl2:(BOOL) //(e.g. we are trying to create this account just now) if(![[DataLayer sharedInstance] isPlainActivatedForAccount:self.accountNo]) { + DDLogDebug(@"Plain is not activated for this account..."); if(self->_loggedInOnce) { clearPipelineCacheOrReportSevereError(NSLocalizedString(@"Server suddenly lacks support for SASL2-SCRAM, ongoing MITM attack highly likely, aborting authentication and disabling account to limit damage. You should try to reenable your account once you are in a clean networking environment again.", @"")); return; } - else if([self->_supportedSaslMechanisms containsObject:@"PLAIN"]) + //_supportedSaslMechanisms==nil indicates SASL1 support only + else if([self->_supportedSaslMechanisms containsObject:@"PLAIN"] || self->_supportedSaslMechanisms == nil) { - clearPipelineCacheOrReportSevereError(NSLocalizedString(@"Server only supports authentication methods not safe against man-in-the-middle attacks! Use the advanced account creation menu if you absolutely must use this server.", @"")); + clearPipelineCacheOrReportSevereError(NSLocalizedString(@"This server isn't additionally hardened against man-in-the-middle attacks on the TLS encryption layer by using authentication methods that are secure against such attacks! This indicates an ongoing attack if the server is supposed to support SASL2 and SCRAM and is harmless otherwise. Use the advanced account creation menu and turn on the PLAIN switch there if you still want to log in to this server.", @"")); return; } } @@ -2979,6 +2981,8 @@ -(void) handleFeaturesBeforeAuth:(MLXMLNode*) parsedStanza withForceSasl2:(BOOL) //prefer SASL2 over SASL1 else if([parsedStanza check:@"{urn:xmpp:sasl:2}authentication/mechanism"] && (![[DataLayer sharedInstance] isPlainActivatedForAccount:self.accountNo] || forceSasl2)) { + DDLogDebug(@"Trying SASL2..."); + weakify(self); _blockToCallOnTCPOpen = ^{ strongify(self); @@ -3075,6 +3079,7 @@ -(void) handleFeaturesBeforeAuth:(MLXMLNode*) parsedStanza withForceSasl2:(BOOL) //SASL1 is fallback only if SASL2 isn't supported with something better than PLAIN else if([parsedStanza check:@"{urn:ietf:params:xml:ns:xmpp-sasl}mechanisms/mechanism"] && [[DataLayer sharedInstance] isPlainActivatedForAccount:self.accountNo]) { + DDLogDebug(@"Trying SASL1..."); //extract menchanisms presented NSSet* supportedSaslMechanisms = [NSSet setWithArray:[parsedStanza find:@"{urn:ietf:params:xml:ns:xmpp-sasl}mechanisms/mechanism#"]]; @@ -3116,6 +3121,8 @@ -(void) handleFeaturesBeforeAuth:(MLXMLNode*) parsedStanza withForceSasl2:(BOOL) } else { + DDLogDebug(@"Neither SASL2 nor SASL1 worked..."); + //this is not a downgrade but something weird going on, log it as such if(![parsedStanza check:@"{urn:xmpp:sasl:2}authentication/mechanism"] && ![parsedStanza check:@"{urn:ietf:params:xml:ns:xmpp-sasl}mechanisms/mechanism"]) DDLogError(@"Something weird happened: neither SASL1 nor SASL2 auth supported by this server!"); From df02c016483c373be734303a97538f0a6da075e4 Mon Sep 17 00:00:00 2001 From: Licaon_Kter Date: Wed, 7 Aug 2024 10:59:04 +0000 Subject: [PATCH 12/22] Quicksy - RO fastlane --- appstore_quicksy_metadata/ro/description.txt | 5 +++++ appstore_quicksy_metadata/ro/keywords.txt | 1 + appstore_quicksy_metadata/ro/marketing_url.txt | 1 + appstore_quicksy_metadata/ro/privacy_url.txt | 1 + appstore_quicksy_metadata/ro/support_url.txt | 1 + 5 files changed, 9 insertions(+) create mode 100644 appstore_quicksy_metadata/ro/description.txt create mode 100644 appstore_quicksy_metadata/ro/keywords.txt create mode 100644 appstore_quicksy_metadata/ro/marketing_url.txt create mode 100644 appstore_quicksy_metadata/ro/privacy_url.txt create mode 100644 appstore_quicksy_metadata/ro/support_url.txt diff --git a/appstore_quicksy_metadata/ro/description.txt b/appstore_quicksy_metadata/ro/description.txt new file mode 100644 index 0000000000..f49b9b9c31 --- /dev/null +++ b/appstore_quicksy_metadata/ro/description.txt @@ -0,0 +1,5 @@ +Quicksy este un derivat al popularului client XMPP Monal cu descoperire automată a contactelor. + +Vă înscrieți cu numărul de telefon, iar Quicksy vă va sugera automat, pe baza numerelor de telefon din agenda dvs., posibile contacte. Sub capota Quicksy este un client XMPP complet care vă permite să comunicați cu orice utilizator de pe orice server public federat. De asemenea, utilizatorii de pe Quicksy pot fi contactați din exterior prin simpla adăugare a +numărdetelefon@quicksy.im la lista dvs. de contacte. + +În afară de sincronizarea contactelor, interfața utilizatorului este în mod deliberat cât mai apropiată de Monal. Acest lucru permite utilizatorilor să migreze în cele din urmă de la Quicksy la Monal fără a fi nevoiți să învețe din nou cum funcționează aplicația. \ No newline at end of file diff --git a/appstore_quicksy_metadata/ro/keywords.txt b/appstore_quicksy_metadata/ro/keywords.txt new file mode 100644 index 0000000000..db666557fb --- /dev/null +++ b/appstore_quicksy_metadata/ro/keywords.txt @@ -0,0 +1 @@ +xmpp, jabber, discutie, mesagerie instantanee, mesagerie, ejabberd, prosody, OMEMO diff --git a/appstore_quicksy_metadata/ro/marketing_url.txt b/appstore_quicksy_metadata/ro/marketing_url.txt new file mode 100644 index 0000000000..b53627eba2 --- /dev/null +++ b/appstore_quicksy_metadata/ro/marketing_url.txt @@ -0,0 +1 @@ +https://quicksy.im/ \ No newline at end of file diff --git a/appstore_quicksy_metadata/ro/privacy_url.txt b/appstore_quicksy_metadata/ro/privacy_url.txt new file mode 100644 index 0000000000..bb7d1c0a62 --- /dev/null +++ b/appstore_quicksy_metadata/ro/privacy_url.txt @@ -0,0 +1 @@ +https://quicksy.im/privacy.htm \ No newline at end of file diff --git a/appstore_quicksy_metadata/ro/support_url.txt b/appstore_quicksy_metadata/ro/support_url.txt new file mode 100644 index 0000000000..b53627eba2 --- /dev/null +++ b/appstore_quicksy_metadata/ro/support_url.txt @@ -0,0 +1 @@ +https://quicksy.im/ \ No newline at end of file From 31a835200facb71df7b6db4c0e0482d111760896 Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Wed, 7 Aug 2024 18:47:28 +0200 Subject: [PATCH 13/22] Improve github workflows and mail2webhook script --- .github/workflows/beta.build-push.yml | 7 ++++++ .github/workflows/publish-quicksy-release.yml | 12 +++++----- .github/workflows/publish-stable-release.yml | 10 ++++---- .github/workflows/quicksy.build-push.yml | 11 +++++++-- .github/workflows/stable.build-push.yml | 7 ++++++ scripts/mail2webhook.py | 24 ++++++++++++++----- scripts/prepare-alpha-certs.sh | 0 7 files changed, 52 insertions(+), 19 deletions(-) mode change 100644 => 100755 scripts/mail2webhook.py mode change 100644 => 100755 scripts/prepare-alpha-certs.sh diff --git a/.github/workflows/beta.build-push.yml b/.github/workflows/beta.build-push.yml index 790700f5f0..3ea31874e1 100644 --- a/.github/workflows/beta.build-push.yml +++ b/.github/workflows/beta.build-push.yml @@ -39,6 +39,13 @@ jobs: lfs: true - name: Checkout submodules run: git submodule update -f --init --remote + - name: Check for proper semantic versioning + run: | + version="$(git log -n 1 --merges --pretty=format:%s | sed -E 's/^[\t\n ]*([^\n\t ]+)[\t\n ]+\(([^\n\t ]+)\)[\t\n ]*$/\1/g')" + if ! [[ "$version" =~ ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-((0|[1-9][0-9]*|[0-9]*[a-zA-Z-][0-9a-zA-Z-]*)(\.(0|[1-9][0-9]*|[0-9]*[a-zA-Z-][0-9a-zA-Z-]*))*))?(\+([0-9a-zA-Z-]+(\.[0-9a-zA-Z-]+)*))?$ ]]; then + echo "Invalid semver: '$version'!" + exit 1 + fi - name: Get last build tag and increment it run: | oldBuildNumber=$(git tag --sort="v:refname" | grep "Build_iOS" | grep -v "Quicksy_Build_iOS" | tail -n1 | sed 's/Build_iOS_//g') diff --git a/.github/workflows/publish-quicksy-release.yml b/.github/workflows/publish-quicksy-release.yml index 9b673afe50..5bfaaa0fae 100644 --- a/.github/workflows/publish-quicksy-release.yml +++ b/.github/workflows/publish-quicksy-release.yml @@ -13,18 +13,18 @@ jobs: release-notes: ${{ steps.releasenotes.outputs.notes }} release-notes_ios: ${{ steps.releasenotes.outputs.notes_ios }} # create release only if the ios app made it to the appstore and ignore the macos appstore state - if: github.event.client_payload.Platform == 'iOS' + if: github.event.client_payload.platform == 'iOS' steps: # - run: | - # echo ${{ github.event.client_payload.AppName }} - # echo ${{ github.event.client_payload.Platform }} - # echo ${{ github.event.client_payload.AppVersionNumber }} + # echo ${{ github.event.client_payload.appName }} + # echo ${{ github.event.client_payload.platform }} + # echo ${{ github.event.client_payload.appVersionNumber }} - name: Load release info id: releasenotes run: | - buildNumber="$(fastlane run app_store_build_number api_key_path:"/Users/ci/appstoreconnect/key.json" team_id:"S8D843U34Y" app_identifier:"G7YU7X7KRJ.SworIM" live:false version:"${{ github.event.client_payload.AppVersionNumber }}" 2>&1 | tee /dev/stderr | grep Result | sed -E 's/^.*Result: ([0-9]+).*$/\1/g')" + buildNumber="$(fastlane run app_store_build_number api_key_path:"/Users/ci/appstoreconnect/key.json" team_id:"S8D843U34Y" app_identifier:"G7YU7X7KRJ.SworIM" live:false version:"${{ github.event.client_payload.appVersionNumber }}" 2>&1 | tee /dev/stderr | grep Result | sed -E 's/^.*Result: ([0-9]+).*$/\1/g')" mkdir -p /Users/ci/releases - OUTPUT_FILE="/Users/ci/releases/$buildNumber.output" + OUTPUT_FILE="/Users/ci/quicksy_releases/$buildNumber.output" touch "$OUTPUT_FILE" cat "$OUTPUT_FILE" >> "$GITHUB_OUTPUT" diff --git a/.github/workflows/publish-stable-release.yml b/.github/workflows/publish-stable-release.yml index 09abd9ef90..1e4a178725 100644 --- a/.github/workflows/publish-stable-release.yml +++ b/.github/workflows/publish-stable-release.yml @@ -15,16 +15,16 @@ jobs: release-notes_macos: ${{ steps.releasenotes.outputs.notes_macos }} release-id: ${{ steps.releasenotes.outputs.releaseID }} # create release only if the ios app made it to the appstore and ignore the macos appstore state - if: github.event.client_payload.Platform == 'iOS' + if: github.event.client_payload.platform == 'iOS' steps: # - run: | - # echo ${{ github.event.client_payload.AppName }} - # echo ${{ github.event.client_payload.Platform }} - # echo ${{ github.event.client_payload.AppVersionNumber }} + # echo ${{ github.event.client_payload.appName }} + # echo ${{ github.event.client_payload.platform }} + # echo ${{ github.event.client_payload.appVersionNumber }} - name: Load release info id: releasenotes run: | - buildNumber="$(fastlane run app_store_build_number api_key_path:"/Users/ci/appstoreconnect/key.json" team_id:"S8D843U34Y" app_identifier:"G7YU7X7KRJ.SworIM" live:false version:"${{ github.event.client_payload.AppVersionNumber }}" 2>&1 | tee /dev/stderr | grep Result | sed -E 's/^.*Result: ([0-9]+).*$/\1/g')" + buildNumber="$(fastlane run app_store_build_number api_key_path:"/Users/ci/appstoreconnect/key.json" team_id:"S8D843U34Y" app_identifier:"G7YU7X7KRJ.SworIM" live:false version:"${{ github.event.client_payload.appVersionNumber }}" 2>&1 | tee /dev/stderr | grep Result | sed -E 's/^.*Result: ([0-9]+).*$/\1/g')" mkdir -p /Users/ci/releases OUTPUT_FILE="/Users/ci/releases/$buildNumber.output" touch "$OUTPUT_FILE" diff --git a/.github/workflows/quicksy.build-push.yml b/.github/workflows/quicksy.build-push.yml index e723bd6e57..59167208a5 100644 --- a/.github/workflows/quicksy.build-push.yml +++ b/.github/workflows/quicksy.build-push.yml @@ -32,6 +32,13 @@ jobs: lfs: true - name: Checkout submodules run: git submodule update -f --init --remote + - name: Check for proper semantic versioning + run: | + version="$(git log -n 1 --merges --pretty=format:%s | sed -E 's/^[\t\n ]*([^\n\t ]+)[\t\n ]+\(([^\n\t ]+)\)[\t\n ]*$/\1/g')" + if ! [[ "$version" =~ ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-((0|[1-9][0-9]*|[0-9]*[a-zA-Z-][0-9a-zA-Z-]*)(\.(0|[1-9][0-9]*|[0-9]*[a-zA-Z-][0-9a-zA-Z-]*))*))?(\+([0-9a-zA-Z-]+(\.[0-9a-zA-Z-]+)*))?$ ]]; then + echo "Invalid semver: '$version'!" + exit 1 + fi - name: Get last build tag and increment it run: | oldBuildNumber=$(git tag --sort="v:refname" | grep "Quicksy_Build_iOS" | tail -n1 | sed 's/Quicksy_Build_iOS_//g') @@ -62,8 +69,8 @@ jobs: if [ "${{ github.ref }}" != "refs/heads/stable" ]; then version="1.$buildNumber" fi - mkdir -p /Users/ci/releases - OUTPUT_FILE="/Users/ci/releases/$buildNumber.output" + mkdir -p /Users/ci/quicksy_releases + OUTPUT_FILE="/Users/ci/quicksy_releases/$buildNumber.output" touch "$OUTPUT_FILE" echo "OUTPUT_FILE=$OUTPUT_FILE" | tee /dev/stderr >> "$GITHUB_OUTPUT" diff --git a/.github/workflows/stable.build-push.yml b/.github/workflows/stable.build-push.yml index bd29023fbc..93cb4603cb 100644 --- a/.github/workflows/stable.build-push.yml +++ b/.github/workflows/stable.build-push.yml @@ -35,6 +35,13 @@ jobs: lfs: true - name: Checkout submodules run: git submodule update -f --init --remote + - name: Check for proper semantic versioning + run: | + version="$(git log -n 1 --merges --pretty=format:%s | sed -E 's/^[\t\n ]*([^\n\t ]+)[\t\n ]+\(([^\n\t ]+)\)[\t\n ]*$/\1/g')" + if ! [[ "$version" =~ ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-((0|[1-9][0-9]*|[0-9]*[a-zA-Z-][0-9a-zA-Z-]*)(\.(0|[1-9][0-9]*|[0-9]*[a-zA-Z-][0-9a-zA-Z-]*))*))?(\+([0-9a-zA-Z-]+(\.[0-9a-zA-Z-]+)*))?$ ]]; then + echo "Invalid semver: '$version'!" + exit 1 + fi - name: Get last build tag and increment it run: | oldBuildNumber=$(git tag --sort="v:refname" | grep "Build_iOS" | grep -v "Quicksy_Build_iOS" | tail -n1 | sed 's/Build_iOS_//g') diff --git a/scripts/mail2webhook.py b/scripts/mail2webhook.py old mode 100644 new mode 100755 index 69c984a589..88623891be --- a/scripts/mail2webhook.py +++ b/scripts/mail2webhook.py @@ -12,7 +12,7 @@ def to_camel_case(text): s = s.split() if len(text) == 0: return text - return s[0] + ''.join(i.capitalize() for i in s[1:]) + return s[0].lower() + ''.join(i.capitalize() for i in s[1:]) # parse commandline parser = argparse.ArgumentParser(formatter_class=argparse.RawTextHelpFormatter, description="Simple python script to trigger a github ") @@ -26,7 +26,7 @@ def to_camel_case(text): parser = email.parser.BytesParser() message = parser.parse(sys.stdin.buffer) -subject = message["subject"] +subject = re.sub(r'\s+', ' ', message["subject"]).strip() date = message["date"] # python > 3.9 variant @@ -45,16 +45,28 @@ def to_camel_case(text): else: body = message.get_payload(decode=True) -# transform body in an array of stripped strings +# transform body into an array of stripped strings body = [s.strip() for s in str(body, 'UTF-8').split("\n")] # parse app properties properties = {to_camel_case(k.strip()): v.strip() for k, v in [line.split(": ", 1) for line in body if len(line.split(": ", 1)) > 1]} -# sanity checks -if "The following app has been approved for distribution:" not in body: - print("Wrong state mentioned in mail", file=sys.stderr) +# sanity checks and state extraction +match = re.match(r"^The status of your \((?P.+)\) app, (?P.+), is now \"(?P.+)\"$", subject) +if match == None: + print(f"Mail subject does not contain proper state: '{subject}'", file=sys.stderr) sys.exit(0) +state = {"_"+to_camel_case(k.strip()): v.strip() for k, v in match.groupdict().items()} +state["_state"] = to_camel_case(state["_state"].strip()) +if state["_appName"] != properties["appName"]: + print(f"Mail subject states different app name than properties in mail body: stateAppName='{state['_appName']}', appName='{properties['appName']}'", file=sys.stderr) + sys.exit(0) + +# merge body properties and extracted state +properties = state | {"_datetime": date} | properties +#print(properties) + +# filter everything using the given commandline arguments for entry in args.filter: k, v = entry.split("=", 1) if k not in properties: diff --git a/scripts/prepare-alpha-certs.sh b/scripts/prepare-alpha-certs.sh old mode 100644 new mode 100755 From 418542f69e776357210a8294fca7ac79f35c91d1 Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Thu, 8 Aug 2024 03:37:57 +0200 Subject: [PATCH 14/22] Fix crash on incoming filetransfers without pending notification --- Monal/Classes/MLNotificationManager.m | 2 -- 1 file changed, 2 deletions(-) diff --git a/Monal/Classes/MLNotificationManager.m b/Monal/Classes/MLNotificationManager.m index b1d28917b6..86d6586a16 100644 --- a/Monal/Classes/MLNotificationManager.m +++ b/Monal/Classes/MLNotificationManager.m @@ -234,8 +234,6 @@ -(void) handleFiletransferUpdate:(NSNotification*) notification DDLogDebug(@"Already displayed notification '%@', updating it...", idval); [self internalMessageHandlerWithMessage:message andAccount:xmppAccount showAlert:YES andSound:NO andLMCReplaced:NO]; } - else - unreachable(@"Unknown MLNotificationState!", @{@"state": @(state)}); }); } From 3327a79bfd15c941f68126c634dfaa15430c5dbe Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Sun, 11 Aug 2024 06:53:21 +0200 Subject: [PATCH 15/22] Fix opening of chats when other views are in foreground --- Monal/Classes/AVCallUI.swift | 9 +- Monal/Classes/ActiveChatsViewController.m | 106 ++++++++++------------ 2 files changed, 53 insertions(+), 62 deletions(-) diff --git a/Monal/Classes/AVCallUI.swift b/Monal/Classes/AVCallUI.swift index 76ba379ed5..11fb3e1810 100644 --- a/Monal/Classes/AVCallUI.swift +++ b/Monal/Classes/AVCallUI.swift @@ -284,9 +284,14 @@ struct AVCallUI: View { VStack { Spacer().frame(height: 8) Button(action: { - self.delegate.dismissWithoutAnimation() if let activeChats = self.appDelegate.obj.activeChats { - activeChats.presentChat(with:self.contact.obj) + //make sure we don't animate anything + activeChats.dismissCompleteViewChain(withAnimation: false) { + activeChats.presentChat(with:self.contact.obj) + } + } else { + //self.delegate.dismissWithoutAnimation() + unreachable("active chats should always be accessible from AVCallUI!") } }, label: { Image(systemName: "text.bubble") diff --git a/Monal/Classes/ActiveChatsViewController.m b/Monal/Classes/ActiveChatsViewController.m index d6d98979cd..aea4b03286 100755 --- a/Monal/Classes/ActiveChatsViewController.m +++ b/Monal/Classes/ActiveChatsViewController.m @@ -598,22 +598,14 @@ -(void) showAddContactWithJid:(NSString*) jid preauthToken:(NSString* _Nullable) MLContact* checkContact = [MLContact createContactFromJid:jid andAccountNo:checkAccount.accountNo]; if(checkContact.isInRoster) { - dispatch_async(dispatch_get_main_queue(), ^{ - [self dismissCompleteViewChainWithAnimation:YES andCompletion:^{ - [self presentChatWithContact:checkContact]; - }]; - }); + [self presentChatWithContact:checkContact]; return; } } appendToViewQueue((^(PMKResolver resolve) { UIViewController* addContactMenuView = [[SwiftuiInterface new] makeAddContactViewForJid:jid preauthToken:preauthToken prefillAccount:account andOmemoFingerprints:fingerprints withDismisser:^(MLContact* _Nonnull newContact) { - dispatch_async(dispatch_get_main_queue(), ^{ - [self dismissCompleteViewChainWithAnimation:YES andCompletion:^{ - [self presentChatWithContact:newContact]; - }]; - }); + [self presentChatWithContact:newContact]; }]; addContactMenuView.ml_disposeCallback = ^{ [self sheetDismissed]; @@ -628,11 +620,7 @@ -(void) showAddContact { appendToViewQueue((^(PMKResolver resolve) { UIViewController* addContactMenuView = [[SwiftuiInterface new] makeAddContactViewWithDismisser:^(MLContact* _Nonnull newContact) { - dispatch_async(dispatch_get_main_queue(), ^{ - [self dismissCompleteViewChainWithAnimation:YES andCompletion:^{ - [self presentChatWithContact:newContact]; - }]; - }); + [self presentChatWithContact:newContact]; }]; addContactMenuView.ml_disposeCallback = ^{ [self sheetDismissed]; @@ -1014,50 +1002,52 @@ -(void) presentChatWithContact:(MLContact*) contact -(void) presentChatWithContact:(MLContact*) contact andCompletion:(monal_id_block_t _Nullable) completion { DDLogVerbose(@"presenting chat with contact: %@, stacktrace: %@", contact, [NSThread callStackSymbols]); - dispatch_async(dispatch_get_main_queue(), ^{ - // only open contact chat when it is not opened yet (needed for opening via notifications and for macOS) - if([contact isEqualToContact:[MLNotificationManager sharedInstance].currentContact]) - { - // make sure the already open chat is reloaded and return - [[MLNotificationQueue currentQueue] postNotificationName:kMonalRefresh object:nil userInfo:nil]; - if(completion != nil) - completion(@YES); - return; - } - - // clear old chat before opening a new one (but not for splitView == YES) - if(self.splitViewController.collapsed) - [self.navigationController popViewControllerAnimated:NO]; - - // show placeholder if contact is nil, open chat otherwise - if(contact == nil) - { - [self presentSplitPlaceholder]; - if(completion != nil) - completion(@NO); - return; - } + [HelperTools dispatchAsync:YES reentrantOnQueue:dispatch_get_main_queue() withBlock:^{ + [self dismissCompleteViewChainWithAnimation:YES andCompletion:^{ + // only open contact chat when it is not opened yet (needed for opening via notifications and for macOS) + if([contact isEqualToContact:[MLNotificationManager sharedInstance].currentContact]) + { + // make sure the already open chat is reloaded and return + [[MLNotificationQueue currentQueue] postNotificationName:kMonalRefresh object:nil userInfo:nil]; + if(completion != nil) + completion(@YES); + return; + } + + // clear old chat before opening a new one (but not for splitView == YES) + if(self.splitViewController.collapsed) + [self.navigationController popViewControllerAnimated:NO]; + + // show placeholder if contact is nil, open chat otherwise + if(contact == nil) + { + [self presentSplitPlaceholder]; + if(completion != nil) + completion(@NO); + return; + } - //open chat (make sure we have an active buddy for it and add it to our ui, if needed) - //but don't animate this if the contact is already present in our list - [[DataLayer sharedInstance] addActiveBuddies:contact.contactJid forAccount:contact.accountId]; - if([[self getChatArrayForSection:pinnedChats] containsObject:contact] || [[self getChatArrayForSection:unpinnedChats] containsObject:contact]) - { - [self scrollToContact:contact]; - [self performSegueWithIdentifier:@"showConversation" sender:contact]; - if(completion != nil) - completion(@YES); - } - else - { - [self insertOrMoveContact:contact completion:^(BOOL finished __unused) { + //open chat (make sure we have an active buddy for it and add it to our ui, if needed) + //but don't animate this if the contact is already present in our list + [[DataLayer sharedInstance] addActiveBuddies:contact.contactJid forAccount:contact.accountId]; + if([[self getChatArrayForSection:pinnedChats] containsObject:contact] || [[self getChatArrayForSection:unpinnedChats] containsObject:contact]) + { [self scrollToContact:contact]; [self performSegueWithIdentifier:@"showConversation" sender:contact]; if(completion != nil) completion(@YES); - }]; - } - }); + } + else + { + [self insertOrMoveContact:contact completion:^(BOOL finished __unused) { + [self scrollToContact:contact]; + [self performSegueWithIdentifier:@"showConversation" sender:contact]; + if(completion != nil) + completion(@YES); + }]; + } + }]; + }]; } /* @@ -1119,12 +1109,8 @@ -(void) prepareForSegue:(UIStoryboardSegue*) segue sender:(id) sender UINavigationController* nav = segue.destinationViewController; ContactsViewController* contacts = (ContactsViewController*)nav.topViewController; contacts.selectContact = ^(MLContact* selectedContact) { - dispatch_async(dispatch_get_main_queue(), ^{ - DDLogVerbose(@"Got selected contact from contactlist ui: %@", selectedContact); - [self dismissCompleteViewChainWithAnimation:YES andCompletion:^{ - [self presentChatWithContact:selectedContact]; - }]; - }); + DDLogVerbose(@"Got selected contact from contactlist ui: %@", selectedContact); + [self presentChatWithContact:selectedContact]; }; } } From 1538c8aeec0a222cb322c0bd1856fbdf9d8c7ebe Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Mon, 12 Aug 2024 10:56:48 +0200 Subject: [PATCH 16/22] Improve SSDP handling Alert about ongoing MITM before sending out client-final-message to finish authentication. --- Monal/Classes/SCRAM.h | 4 +++- Monal/Classes/SCRAM.m | 11 +++++++++-- Monal/Classes/xmpp.m | 42 +++++++++++++++++++++++------------------- 3 files changed, 35 insertions(+), 22 deletions(-) diff --git a/Monal/Classes/SCRAM.h b/Monal/Classes/SCRAM.h index e2732daf3c..a4fce09368 100644 --- a/Monal/Classes/SCRAM.h +++ b/Monal/Classes/SCRAM.h @@ -12,15 +12,16 @@ NS_ASSUME_NONNULL_BEGIN typedef NS_ENUM(NSUInteger, MLScramStatus) { - MLScramStatusOK, //server-first-message MLScramStatusNonceError, MLScramStatusUnsupportedMAttribute, MLScramStatusSSDPTriggered, MLScramStatusIterationCountInsecure, + MLScramStatusServerFirstOK, //server-final-message MLScramStatusWrongServerProof, MLScramStatusServerError, + MLScramStatusServerFinalOK, }; @interface SCRAM : NSObject @@ -35,6 +36,7 @@ typedef NS_ENUM(NSUInteger, MLScramStatus) { -(NSData*) hashPasswordWithSalt:(NSData*) salt andIterationCount:(uint32_t) iterationCount; @property (nonatomic, readonly) NSString* method; +@property (nonatomic, readonly) BOOL serverFirstMessageParsed; @property (nonatomic, readonly) BOOL finishedSuccessfully; @property (nonatomic, readonly) BOOL ssdpSupported; diff --git a/Monal/Classes/SCRAM.m b/Monal/Classes/SCRAM.m index e83eeb80a4..79dbf77557 100644 --- a/Monal/Classes/SCRAM.m +++ b/Monal/Classes/SCRAM.m @@ -58,6 +58,7 @@ -(instancetype) initWithUsername:(NSString*) username password:(NSString*) passw _password = password; _nonce = [NSUUID UUID].UUIDString; _ssdpString = nil; + _serverFirstMessageParsed = NO; _finishedSuccessfully = NO; return self; } @@ -65,6 +66,7 @@ -(instancetype) initWithUsername:(NSString*) username password:(NSString*) passw -(void) setSSDPMechanisms:(NSArray*) mechanisms andChannelBindingTypes:(NSArray* _Nullable) cbTypes { MLAssert(!_finishedSuccessfully, @"SCRAM handler finished already!"); + MLAssert(!_serverFirstMessageParsed, @"SCRAM handler already parsed server-first-message!"); DDLogVerbose(@"Creating SDDP string: %@\n%@", mechanisms, cbTypes); NSMutableString* ssdpString = [NSMutableString new]; [ssdpString appendString:[[mechanisms sortedArrayUsingSelector:@selector(compare:)] componentsJoinedByString:@","]]; @@ -80,6 +82,7 @@ -(void) setSSDPMechanisms:(NSArray*) mechanisms andChannelBindingType -(NSString*) clientFirstMessageWithChannelBinding:(NSString* _Nullable) channelBindingType { MLAssert(!_finishedSuccessfully, @"SCRAM handler finished already!"); + MLAssert(!_serverFirstMessageParsed, @"SCRAM handler already parsed server-first-message!"); if(channelBindingType == nil) _gssHeader = @"n,,"; //not supported by us else if(!_usingChannelBinding) @@ -94,7 +97,9 @@ -(NSString*) clientFirstMessageWithChannelBinding:(NSString* _Nullable) channelB -(MLScramStatus) parseServerFirstMessage:(NSString*) str { MLAssert(!_finishedSuccessfully, @"SCRAM handler finished already!"); + MLAssert(!_serverFirstMessageParsed, @"SCRAM handler already parsed server-first-message!"); NSDictionary* msg = [self parseScramString:str]; + _serverFirstMessageParsed = YES; //server nonce MUST start with our client nonce if(![msg[@"r"] hasPrefix:_nonce]) return MLScramStatusNonceError; @@ -117,13 +122,14 @@ -(MLScramStatus) parseServerFirstMessage:(NSString*) str } if(_iterationCount < 4096) return MLScramStatusIterationCountInsecure; - return MLScramStatusOK; + return MLScramStatusServerFirstOK; } //see https://stackoverflow.com/a/29299946/3528174 -(NSString*) clientFinalMessageWithChannelBindingData:(NSData* _Nullable) channelBindingData { MLAssert(!_finishedSuccessfully, @"SCRAM handler finished already!"); + MLAssert(_serverFirstMessageParsed, @"SCRAM handler did not parsed server-first-message yet!"); //calculate gss header with optional channel binding data NSMutableData* gssHeaderWithChannelBindingData = [NSMutableData new]; [gssHeaderWithChannelBindingData appendData:[_gssHeader dataUsingEncoding:NSUTF8StringEncoding]]; @@ -162,6 +168,7 @@ -(NSString*) clientFinalMessageWithChannelBindingData:(NSData* _Nullable) channe -(MLScramStatus) parseServerFinalMessage:(NSString*) str { MLAssert(!_finishedSuccessfully, @"SCRAM handler finished already!"); + MLAssert(_serverFirstMessageParsed, @"SCRAM handler did not parsed server-first-message yet!"); NSDictionary* msg = [self parseScramString:str]; //wrong v-value if(![HelperTools constantTimeCompareAttackerString:msg[@"v"] withKnownString:_expectedServerSignature]) @@ -174,7 +181,7 @@ -(MLScramStatus) parseServerFinalMessage:(NSString*) str } //everything was successful _finishedSuccessfully = YES; - return MLScramStatusOK; + return MLScramStatusServerFinalOK; } -(NSData*) hashPasswordWithSalt:(NSData*) salt andIterationCount:(uint32_t) iterationCount diff --git a/Monal/Classes/xmpp.m b/Monal/Classes/xmpp.m index 79f930b923..97e1e8341e 100644 --- a/Monal/Classes/xmpp.m +++ b/Monal/Classes/xmpp.m @@ -2505,9 +2505,24 @@ -(void) processInput:(MLXMLNode*) parsedStanza withDelayedReplay:(BOOL) delayedR case MLScramStatusNonceError: deactivate_account = NO; message = NSLocalizedString(@"Error handling SASL challenge of server (nonce error), disconnecting!", @"parenthesis should be verbatim"); break; case MLScramStatusUnsupportedMAttribute: deactivate_account = NO; message = NSLocalizedString(@"Error handling SASL challenge of server (m-attr error), disconnecting!", @"parenthesis should be verbatim"); break; case MLScramStatusIterationCountInsecure: deactivate_account = NO; message = NSLocalizedString(@"Error handling SASL challenge of server (iteration count too low), disconnecting!", @"parenthesis should be verbatim"); break; - case MLScramStatusOK: deactivate_account = NO; message = nil; break; //everything is okay + case MLScramStatusServerFirstOK: deactivate_account = NO; message = nil; break; //everything is okay default: unreachable(@"wrong status for scram message!"); break; } + + //check for incomplete XEP-0440 support (not implementing mandatory tls-server-end-point channel-binding) not mitigated by SSDP + //(we allow either support for tls-server-end-point or SSDP signed non-support) + if([kServerDoesNotFollowXep0440Error isEqualToString:[self channelBindingToUse]]) + { + MLXMLNode* streamError = [[MLXMLNode alloc] initWithElement:@"stream:error" withAttributes:@{@"type": @"cancel"} andChildren:@[ + [[MLXMLNode alloc] initWithElement:@"undefined-condition" andNamespace:@"urn:ietf:params:xml:ns:xmpp-streams" withAttributes:@{} andChildren:@[] andData:nil], + [[MLXMLNode alloc] initWithElement:@"text" andNamespace:@"urn:ietf:params:xml:ns:xmpp-streams" withAttributes:@{} andChildren:@[] andData:kServerDoesNotFollowXep0440Error], + ] andData:nil]; + [self disconnectWithStreamError:streamError andExplicitLogout:YES]; + + //make sure this error is reported, even if there are other SRV records left (we disconnect here and won't try again) + [HelperTools postError:NSLocalizedString(@"Either this is a man-in-the-middle attack OR your server neither implements XEP-0474 nor does it fully implement XEP-0440 which mandates support for tls-server-end-point channel-binding. In either case you should inform your server admin! Account disabled now.", @"") withNode:nil andAccount:self andIsSevere:YES andDisableAccount:YES]; + } + if(message != nil) { DDLogError(@"SCRAM says this server-first message was wrong!"); @@ -2654,20 +2669,6 @@ -(void) processInput:(MLXMLNode*) parsedStanza withDelayedReplay:(BOOL) delayedR //record TLS version self.connectionProperties.tlsVersion = [((MLStream*)self->_oStream) isTLS13] ? @"1.3" : @"1.2"; - //check for incomplete XEP-0440 support (not implementing mandatory tls-server-end-point channel-binding) not mitigated by SSDP - //(we allow either support for tls-server-end-point or SSDP signed non-support) - if([kServerDoesNotFollowXep0440Error isEqualToString:[self channelBindingToUse]]) - { - MLXMLNode* streamError = [[MLXMLNode alloc] initWithElement:@"stream:error" withAttributes:@{@"type": @"cancel"} andChildren:@[ - [[MLXMLNode alloc] initWithElement:@"undefined-condition" andNamespace:@"urn:ietf:params:xml:ns:xmpp-streams" withAttributes:@{} andChildren:@[] andData:nil], - [[MLXMLNode alloc] initWithElement:@"text" andNamespace:@"urn:ietf:params:xml:ns:xmpp-streams" withAttributes:@{} andChildren:@[] andData:kServerDoesNotFollowXep0440Error], - ] andData:nil]; - [self disconnectWithStreamError:streamError andExplicitLogout:YES]; - - //make sure this error is reported, even if there are other SRV records left (we disconnect here and won't try again) - [HelperTools postError:NSLocalizedString(@"Either this is a man-in-the-middle attack OR your server neither implements XEP-0474 nor does it fully implement XEP-0440 which mandates support for tls-server-end-point channel-binding. In either case you should inform your server admin! Account disabled now.", @"") withNode:nil andAccount:self andIsSevere:YES andDisableAccount:YES]; - } - self->_scramHandler = nil; self->_blockToCallOnTCPOpen = nil; //just to be sure but not strictly necessary self->_accountState = kStateLoggedIn; @@ -2951,7 +2952,10 @@ -(void) handleFeaturesBeforeAuth:(MLXMLNode*) parsedStanza withForceSasl2:(BOOL) //_supportedSaslMechanisms==nil indicates SASL1 support only else if([self->_supportedSaslMechanisms containsObject:@"PLAIN"] || self->_supportedSaslMechanisms == nil) { - clearPipelineCacheOrReportSevereError(NSLocalizedString(@"This server isn't additionally hardened against man-in-the-middle attacks on the TLS encryption layer by using authentication methods that are secure against such attacks! This indicates an ongoing attack if the server is supposed to support SASL2 and SCRAM and is harmless otherwise. Use the advanced account creation menu and turn on the PLAIN switch there if you still want to log in to this server.", @"")); + //leave that in for translators, we might use it at a later time + while(!NSLocalizedString(@"This server isn't additionally hardened against man-in-the-middle attacks on the TLS encryption layer by using authentication methods that are secure against such attacks! This indicates an ongoing attack if the server is supposed to support SASL2 and SCRAM and is harmless otherwise. Use the advanced account creation menu and turn on the PLAIN switch there if you still want to log in to this server.", @"")); + + clearPipelineCacheOrReportSevereError(NSLocalizedString(@"This server lacks support for SASL2 and SCRAM, additionally hardening authentication against man-in-the-middle attacks on the TLS encryption layer. Since this server is listed as supporting both at https://github.com/monal-im/SCRAM_PreloadList, an ongoing MITM attack is highly likely! You should try again once you are in a clean networking environment.", @"")); return; } } @@ -3180,7 +3184,7 @@ -(void) handleScramInSuccessOrContinue:(MLXMLNode*) parsedStanza switch([self->_scramHandler parseServerFinalMessage:innerSASLData]) { case MLScramStatusWrongServerProof: deactivate_account = YES; message = NSLocalizedString(@"SCRAM server proof wrong, ongoing MITM attack highly likely, aborting authentication and disabling account to limit damage. You should try to reenable your account once you are in a clean networking environment again.", @""); break; case MLScramStatusServerError: deactivate_account = NO; message = NSLocalizedString(@"Unexpected error authenticating server using SASL2 (does your server have a bug?), disconnecting!", @""); break; - case MLScramStatusOK: deactivate_account = NO; message = nil; break; //everything is okay + case MLScramStatusServerFinalOK: deactivate_account = NO; message = nil; break; //everything is okay default: unreachable(@"wrong status for scram message!"); break; } @@ -3221,12 +3225,12 @@ -(NSString* _Nullable) channelBindingToUse //if our scram handshake is not finished yet and no mutually supported channel-binding can be found --> ignore that for now (see below) //if our scram handshake finished without negotiating a mutually supported channel-binding and this was not backed by SSDP --> report error - if(self->_scramHandler.finishedSuccessfully && !self->_scramHandler.ssdpSupported) + if(self->_scramHandler.serverFirstMessageParsed && !self->_scramHandler.ssdpSupported) { DDLogWarn(@"Could not find any supported channel-binding type, this MUST be a mitm attack, because tls-server-end-point is mandatory via XEP-0440!"); return kServerDoesNotFollowXep0440Error; //this will trigger a disconnect } - if(!self->_scramHandler.finishedSuccessfully) + if(!self->_scramHandler.serverFirstMessageParsed) DDLogWarn(@"Could not find any supported channel-binding type, this COULD be a mitm attack (check via XEP-0474 pending)!"); return nil; } From 47972ec1b5e02f7f0f93d951835d9c7d09e5b6b5 Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Mon, 12 Aug 2024 11:26:07 +0200 Subject: [PATCH 17/22] Fix crash on macOS when removing account or clearing history --- Monal/Classes/XMPPEdit.m | 21 ++++++--------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/Monal/Classes/XMPPEdit.m b/Monal/Classes/XMPPEdit.m index 92b0b585e6..9f7c669401 100644 --- a/Monal/Classes/XMPPEdit.m +++ b/Monal/Classes/XMPPEdit.m @@ -480,11 +480,8 @@ - (IBAction) removeAccountClicked: (id) sender [questionAlert addAction:yesAction]; UIPopoverPresentationController* popPresenter = [questionAlert popoverPresentationController]; - if(@available(iOS 16.0, macCatalyst 16.0, *)) - popPresenter.sourceItem = sender; - else - popPresenter.barButtonItem = sender; - + popPresenter.sourceView = self.view; + [self presentViewController:questionAlert animated:YES completion:nil]; } @@ -542,11 +539,8 @@ -(IBAction) deleteAccountClicked:(id) sender [questionAlert addAction:yesAction]; UIPopoverPresentationController* popPresenter = [questionAlert popoverPresentationController]; - if(@available(iOS 16.0, macCatalyst 16.0, *)) - popPresenter.sourceItem = sender; - else - popPresenter.barButtonItem = sender; - + popPresenter.sourceView = self.view; + [self presentViewController:questionAlert animated:YES completion:nil]; } @@ -577,11 +571,8 @@ - (IBAction) clearHistoryClicked: (id) sender [questionAlert addAction:yesAction]; UIPopoverPresentationController* popPresenter = [questionAlert popoverPresentationController]; - if(@available(iOS 16.0, macCatalyst 16.0, *)) - popPresenter.sourceItem = sender; - else - popPresenter.barButtonItem = sender; - + popPresenter.sourceView = self.view; + [self presentViewController:questionAlert animated:YES completion:nil]; } From 8ea3a4fea39bb862ef79b66b0f97f91e986d3132 Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Mon, 12 Aug 2024 14:40:17 +0200 Subject: [PATCH 18/22] Fix semver check in quicksy workflow --- .github/workflows/quicksy.build-push.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/quicksy.build-push.yml b/.github/workflows/quicksy.build-push.yml index 59167208a5..80301f89c4 100644 --- a/.github/workflows/quicksy.build-push.yml +++ b/.github/workflows/quicksy.build-push.yml @@ -34,7 +34,12 @@ jobs: run: git submodule update -f --init --remote - name: Check for proper semantic versioning run: | + buildNumber="$(git tag --sort="v:refname" | grep "Quicksy_Build_iOS" | tail -n1 | sed 's/Quicksy_Build_iOS_//g')" version="$(git log -n 1 --merges --pretty=format:%s | sed -E 's/^[\t\n ]*([^\n\t ]+)[\t\n ]+\(([^\n\t ]+)\)[\t\n ]*$/\1/g')" + if [ "${{ github.ref }}" != "refs/heads/stable" ]; then + version="1.$buildNumber.0" + fi + if ! [[ "$version" =~ ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-((0|[1-9][0-9]*|[0-9]*[a-zA-Z-][0-9a-zA-Z-]*)(\.(0|[1-9][0-9]*|[0-9]*[a-zA-Z-][0-9a-zA-Z-]*))*))?(\+([0-9a-zA-Z-]+(\.[0-9a-zA-Z-]+)*))?$ ]]; then echo "Invalid semver: '$version'!" exit 1 From 85f8b2614ab6954268aefb5394d6c2f7aaa5a146 Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Mon, 12 Aug 2024 16:36:35 +0200 Subject: [PATCH 19/22] Fix omemo devicelist fetch handling --- Monal/Classes/MLOMEMO.m | 1 + 1 file changed, 1 insertion(+) diff --git a/Monal/Classes/MLOMEMO.m b/Monal/Classes/MLOMEMO.m index 8c60dc3a0d..35cb9a9f2c 100644 --- a/Monal/Classes/MLOMEMO.m +++ b/Monal/Classes/MLOMEMO.m @@ -338,6 +338,7 @@ -(void) queryOMEMODevices:(NSString*) jid withSubscribe:(BOOL) subscribe { //fetch newest devicelist (this is needed even after a subscribe on at least prosody) [self.account.pubsub fetchNode:@"eu.siacs.conversations.axolotl.devicelist" from:jid withItemsList:nil andHandler:$newHandlerWithInvalidation(self, handleDevicelistFetch, handleDevicelistFetchInvalidation, $BOOL(subscribe))]; + [self.state.openDevicelistFetches addObject:jid]; } } From f2ea552b7d3179d51c025f895a9bbc6afe72722a Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Mon, 12 Aug 2024 16:37:36 +0200 Subject: [PATCH 20/22] Add new kMonalOmemoFetchingStateUpdate notification --- Monal/Classes/MLConstants.h | 1 + Monal/Classes/MLOMEMO.m | 36 ++++++++++++++++++++++++++++++++++-- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/Monal/Classes/MLConstants.h b/Monal/Classes/MLConstants.h index f5532f429e..864be98d59 100644 --- a/Monal/Classes/MLConstants.h +++ b/Monal/Classes/MLConstants.h @@ -172,6 +172,7 @@ static inline NSString* _Nonnull LocalizationNotNeeded(NSString* _Nonnull s) #define kMonalFinishedOmemoBundleFetch @"kMonalFinishedOmemoBundleFetch" #define kMonalOmemoStateUpdated @"kMonalOmemoStateUpdated" #define kMonalUpdateBundleFetchStatus @"kMonalUpdateBundleFetchStatus" +#define kMonalOmemoFetchingStateUpdate @"kMonalOmemoFetchingStateUpdate" #define kMonalIdle @"kMonalIdle" #define kMonalFiletransfersIdle @"kMonalFiletransfersIdle" #define kMonalNotIdle @"kMonalNotIdle" diff --git a/Monal/Classes/MLOMEMO.m b/Monal/Classes/MLOMEMO.m index 35cb9a9f2c..450399c992 100644 --- a/Monal/Classes/MLOMEMO.m +++ b/Monal/Classes/MLOMEMO.m @@ -339,12 +339,16 @@ -(void) queryOMEMODevices:(NSString*) jid withSubscribe:(BOOL) subscribe //fetch newest devicelist (this is needed even after a subscribe on at least prosody) [self.account.pubsub fetchNode:@"eu.siacs.conversations.axolotl.devicelist" from:jid withItemsList:nil andHandler:$newHandlerWithInvalidation(self, handleDevicelistFetch, handleDevicelistFetchInvalidation, $BOOL(subscribe))]; [self.state.openDevicelistFetches addObject:jid]; + + [self sendFetchUpdateNotificationForJid:jid]; } } $$instance_handler(handleDevicelistSubscribeInvalidation, account.omemo, $$ID(xmpp*, account), $$ID(NSString*, jid)) //mark devicelist subscription as done [self.state.openDevicelistSubscriptions removeObject:jid]; + + [self sendFetchUpdateNotificationForJid:jid]; $$ $$instance_handler(handleDevicelistSubscribe, account.omemo, $$ID(xmpp*, account), $$ID(NSString*, jid), $$BOOL(success), $_ID(XMPPIQ*, errorIq), $_ID(NSString*, errorReason)) @@ -352,12 +356,14 @@ -(void) queryOMEMODevices:(NSString*) jid withSubscribe:(BOOL) subscribe if(success == NO) { + // TODO: improve error handling if(errorIq) DDLogError(@"Error while subscribe to omemo deviceslist from: %@ - %@", jid, errorIq); else DDLogError(@"Error while subscribe to omemo deviceslist from: %@ - %@", jid, errorReason); } - // TODO: improve error handling + + [self sendFetchUpdateNotificationForJid:jid]; $$ $$instance_handler(handleDevicelistFetchInvalidation, account.omemo, $$ID(xmpp*, account), $$ID(NSString*, jid)) @@ -371,6 +377,8 @@ -(void) queryOMEMODevices:(NSString*) jid withSubscribe:(BOOL) subscribe //retrigger queued key transport elements for this jid (if any) [self retriggerKeyTransportElementsForJid:jid]; + + [self sendFetchUpdateNotificationForJid:jid]; $$ $$instance_handler(handleDevicelistFetch, account.omemo, $$ID(xmpp*, account), $$BOOL(subscribe), $$ID(NSString*, jid), $$BOOL(success), $_ID(XMPPIQ*, errorIq), $_ID(NSString*, errorReason), $_ID((NSDictionary*), data)) @@ -424,6 +432,8 @@ -(void) queryOMEMODevices:(NSString*) jid withSubscribe:(BOOL) subscribe [self repairQueuedSessions]; //now try to repair all broken sessions (our catchup is now really done) else [self retriggerKeyTransportElementsForJid:jid]; //retrigger queued key transport elements for this jid (if any) + + [self sendFetchUpdateNotificationForJid:jid]; $$ -(void) processOMEMODevices:(NSSet*) receivedDevices from:(NSString*) source @@ -572,6 +582,7 @@ -(void) queryOMEMOBundleFrom:(NSString*) jid andDevice:(NSNumber*) deviceid self.state.openBundleFetches[jid] = [NSMutableSet new]; [self.state.openBundleFetches[jid] addObject:deviceid]; + [self sendFetchUpdateNotificationForJid:jid]; } //don't mark any devices as deleted in this invalidation handler (like we do for an error in the normal handler below), @@ -589,6 +600,8 @@ -(void) queryOMEMOBundleFrom:(NSString*) jid andDevice:(NSNumber*) deviceid //retrigger queued key transport elements for this jid (if any) [self retriggerKeyTransportElementsForJid:jid]; + + [self sendFetchUpdateNotificationForJid:jid]; $$ $$instance_handler(handleBundleFetchResult, account.omemo, $$ID(xmpp*, account), $$ID(NSString*, jid), $$BOOL(success), $_ID(XMPPIQ*, errorIq), $_ID(NSString*, errorReason), $_ID((NSDictionary*), data), $$ID(NSNumber*, rid)) @@ -640,6 +653,8 @@ -(void) queryOMEMOBundleFrom:(NSString*) jid andDevice:(NSNumber*) deviceid //retrigger queued key transport elements for this jid (if any) [self retriggerKeyTransportElementsForJid:jid]; + + [self sendFetchUpdateNotificationForJid:jid]; $$ -(void) handleBundleWithInvalidEntryForJid:(NSString*) jid andRid:(NSNumber*) rid @@ -1306,10 +1321,14 @@ -(void) subscribeAndFetchDevicelistIfNoSessionExistsForJid:(NSString*) buddyJid { if([self.monalSignalStore sessionsExistForBuddy:buddyJid] == NO) { + DDLogVerbose(@"No omemo session for %@", buddyJid); MLContact* contact = [MLContact createContactFromJid:buddyJid andAccountNo:self.account.accountNo]; //only do so if we don't receive automatic headline pushes of the devicelist if(!contact.isSubscribedTo) + { + DDLogVerbose(@"Fetching devicelist with subscribe from contact: %@", contact); [self queryOMEMODevices:buddyJid withSubscribe:YES]; + } } } @@ -1322,10 +1341,11 @@ -(void) checkIfSessionIsStillNeeded:(NSString*) buddyJid isMuc:(BOOL) isMuc else if([self.monalSignalStore checkIfSessionIsStillNeeded:buddyJid] == NO) [danglingJids addObject:buddyJid]; - [self notifyKnownDevicesUpdated:buddyJid]; DDLogVerbose(@"Unsubscribing from dangling jids: %@", danglingJids); for(NSString* jid in danglingJids) [self.account.pubsub unsubscribeFromNode:@"eu.siacs.conversations.axolotl.devicelist" forJid:jid withHandler:$newHandler(self, handleDevicelistUnsubscribe)]; + + [self notifyKnownDevicesUpdated:buddyJid]; } //interfaces for UI @@ -1403,6 +1423,18 @@ -(void) clearAllSessionsForJid:(NSString*) jid [self.account.pubsub fetchNode:@"eu.siacs.conversations.axolotl.devicelist" from:jid withItemsList:nil andHandler:$newHandlerWithInvalidation(self, handleDevicelistFetch, handleDevicelistFetchInvalidation, $BOOL(subscribe, NO))]; } +-(void) sendFetchUpdateNotificationForJid:(NSString*) jid +{ + BOOL isFetching = self.state.openBundleFetches[jid] != nil || [self.state.openDevicelistFetches containsObject:jid] || [self.state.openDevicelistSubscriptions containsObject:jid]; + [[MLNotificationQueue currentQueue] postNotificationName:kMonalOmemoFetchingStateUpdate object:self.account userInfo:@{ + @"jid": jid, + @"isFetching": @(isFetching), + @"fetchingBundle": @(self.state.openBundleFetches[jid] != nil), + @"fetchingDevicelist": @([self.state.openDevicelistFetches containsObject:jid]), + @"subscribingDevicelist": @([self.state.openDevicelistSubscriptions containsObject:jid]), + }]; +} + @end NS_ASSUME_NONNULL_END From 77b5642ec27bdbde1630a5631077af6aa4e3c9f8 Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Mon, 12 Aug 2024 16:41:56 +0200 Subject: [PATCH 21/22] Show HUD while loading omemo keys the first time When opening a new chat and not having any omemo keys/devices for this contact, we try to fetch the omemo devicelist+bundles while showing a progress HUD. If we do not succeed fetching those, we display a warning to the user and disable encryption. --- Monal/Classes/MLChatViewHelper.m | 2 +- Monal/Classes/MLContact.m | 1 + Monal/Classes/MLOMEMO.m | 14 +-- Monal/Classes/chatViewController.m | 164 +++++++++++++++++++---------- 4 files changed, 121 insertions(+), 60 deletions(-) diff --git a/Monal/Classes/MLChatViewHelper.m b/Monal/Classes/MLChatViewHelper.m index e8738e41b5..b9cea10f12 100644 --- a/Monal/Classes/MLChatViewHelper.m +++ b/Monal/Classes/MLChatViewHelper.m @@ -20,7 +20,7 @@ +(void) toggleEncryptionForContact:(MLContact*) contact withSelf:(id) andSelf af if(![contact toggleEncryption:!contact.isEncrypted]) { // Show a warning when no device keys could be found and the user tries to enable encryption -> encryption is not possible - UIAlertController* alert = [UIAlertController alertControllerWithTitle:NSLocalizedString(@"Encryption Not Supported", @"") message:NSLocalizedString(@"This contact does not appear to have any devices that support encryption.", @"") preferredStyle:UIAlertControllerStyleAlert]; + UIAlertController* alert = [UIAlertController alertControllerWithTitle:NSLocalizedString(@"Encryption Not Supported", @"") message:NSLocalizedString(@"This contact does not appear to have any devices that support encryption, please try again later if you think this is wrong.", @"") preferredStyle:UIAlertControllerStyleAlert]; [alert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Close", @"") style:UIAlertActionStyleDefault handler:^(UIAlertAction* action __unused) { [alert dismissViewControllerAnimated:YES completion:nil]; }]]; diff --git a/Monal/Classes/MLContact.m b/Monal/Classes/MLContact.m index 5532729a75..5e55a1058b 100644 --- a/Monal/Classes/MLContact.m +++ b/Monal/Classes/MLContact.m @@ -620,6 +620,7 @@ -(BOOL) toggleEncryption:(BOOL) encrypt if(self.isGroup == NO) { NSSet* knownDevices = [account.omemo knownDevicesForAddressName:self.contactJid]; + DDLogVerbose(@"Current isEncrypted=%@, encrypt=%@, knownDevices=%@", bool2str(self.isEncrypted), bool2str(encrypt), knownDevices); if(!self.isEncrypted && encrypt && knownDevices.count == 0) { // request devicelist again diff --git a/Monal/Classes/MLOMEMO.m b/Monal/Classes/MLOMEMO.m index 450399c992..cf127794b7 100644 --- a/Monal/Classes/MLOMEMO.m +++ b/Monal/Classes/MLOMEMO.m @@ -1323,12 +1323,14 @@ -(void) subscribeAndFetchDevicelistIfNoSessionExistsForJid:(NSString*) buddyJid { DDLogVerbose(@"No omemo session for %@", buddyJid); MLContact* contact = [MLContact createContactFromJid:buddyJid andAccountNo:self.account.accountNo]; - //only do so if we don't receive automatic headline pushes of the devicelist - if(!contact.isSubscribedTo) - { - DDLogVerbose(@"Fetching devicelist with subscribe from contact: %@", contact); - [self queryOMEMODevices:buddyJid withSubscribe:YES]; - } + //only subscribe if we don't receive automatic headline pushes of the devicelist + DDLogVerbose(@"Fetching devicelist %@ from contact: %@", !contact.isSubscribedTo ? @"with subscribe" : @"without subscribe", contact); + [self queryOMEMODevices:buddyJid withSubscribe:!contact.isSubscribedTo]; + } + else + { + //make sure we don't show the omemo key fetching hud forever + [self sendFetchUpdateNotificationForJid:buddyJid]; } } diff --git a/Monal/Classes/chatViewController.m b/Monal/Classes/chatViewController.m index 615add3ff2..d5fc278cd4 100644 --- a/Monal/Classes/chatViewController.m +++ b/Monal/Classes/chatViewController.m @@ -66,6 +66,7 @@ @interface chatViewController()* messageList; @@ -781,7 +782,7 @@ -(void) viewWillAppear:(BOOL)animated [nc addObserver:self selector:@selector(handleDeletedMessage:) name:kMonalDeletedMessageNotice object:nil]; [nc addObserver:self selector:@selector(handleSentMessage:) name:kMonalSentMessageNotice object:nil]; [nc addObserver:self selector:@selector(handleMessageError:) name:kMonalMessageErrorNotice object:nil]; - + [nc addObserver:self selector:@selector(handleOmemoFetchStateUpdate:) name:kMonalOmemoFetchingStateUpdate object:nil]; [nc addObserver:self selector:@selector(dismissKeyboard:) name:UIApplicationDidEnterBackgroundNotification object:nil]; [nc addObserver:self selector:@selector(handleForeGround) name:kMonalRefresh object:nil]; @@ -847,58 +848,8 @@ -(void) viewWillAppear:(BOOL)animated -(void) viewDidAppear:(BOOL)animated { [super viewDidAppear:animated]; -#ifndef DISABLE_OMEMO - if(self.xmppAccount && [[DataLayer sharedInstance] isAccountEnabled:self.xmppAccount.accountNo]) - { - BOOL omemoDeviceForContactFound = NO; - if(!self.contact.isGroup) - omemoDeviceForContactFound = [self.xmppAccount.omemo knownDevicesForAddressName:self.contact.contactJid].count > 0; - else - { - omemoDeviceForContactFound = NO; - for(NSDictionary* participant in [[DataLayer sharedInstance] getMembersAndParticipantsOfMuc:self.contact.contactJid forAccountId:self.xmppAccount.accountNo]) - { - if(participant[@"participant_jid"]) - omemoDeviceForContactFound |= [self.xmppAccount.omemo knownDevicesForAddressName:participant[@"participant_jid"]].count > 0; - else if(participant[@"member_jid"]) - omemoDeviceForContactFound |= [self.xmppAccount.omemo knownDevicesForAddressName:participant[@"member_jid"]].count > 0; - if(omemoDeviceForContactFound) - break; - } - } - if(!omemoDeviceForContactFound && self.contact.isEncrypted) - { - if(!self.contact.isGroup && [[HelperTools splitJid:self.contact.contactJid][@"host"] isEqualToString:@"cheogram.com"]) - { - // cheogram.com does not support OMEMO encryption as it is a PSTN gateway - // --> disable it - self.contact.isEncrypted = NO; - [[DataLayer sharedInstance] disableEncryptForJid:self.contact.contactJid andAccountNo:self.contact.accountId]; - } - else if(self.contact.isGroup && ![self.contact.mucType isEqualToString:@"group"]) - { - // a channel type muc has OMEMO encryption enabled, but channels don't support encryption - // --> disable it - self.contact.isEncrypted = NO; - [[DataLayer sharedInstance] disableEncryptForJid:self.contact.contactJid andAccountNo:self.contact.accountId]; - } - else if(!self.contact.isGroup || (self.contact.isGroup && [self.contact.mucType isEqualToString:@"group"])) - { - // a 1:1 contact or a group type muc has OMEMO encryption enabled - UIAlertController* alert = [UIAlertController alertControllerWithTitle:NSLocalizedString(@"No OMEMO keys found", @"") message:NSLocalizedString(@"This contact may not support OMEMO encrypted messages. Please try to enable encryption again in a few seconds, if you think this is wrong.", @"") preferredStyle:UIAlertControllerStyleAlert]; - [alert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Disable Encryption", @"") style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) { - // Disable encryption - self.contact.isEncrypted = NO; - [self updateUIElements]; - [[DataLayer sharedInstance] disableEncryptForJid:self.contact.contactJid andAccountNo:self.contact.accountId]; - [alert dismissViewControllerAnimated:YES completion:nil]; - }]]; - - [self presentViewController:alert animated:YES completion:nil]; - } - } - } -#endif + [self checkOmemoSupportWithAlert:NO]; + [self refreshCounter]; //init the floating last message button @@ -3196,6 +3147,113 @@ -(void) stopEditing self.editingCallback(nil); //dismiss swipe action } +-(void) checkOmemoSupportWithAlert:(BOOL) showWarning +{ +#ifndef DISABLE_OMEMO + if(self.xmppAccount && [[DataLayer sharedInstance] isAccountEnabled:self.xmppAccount.accountNo]) + { + BOOL omemoDeviceForContactFound = NO; + if(!self.contact.isGroup) + omemoDeviceForContactFound = [self.xmppAccount.omemo knownDevicesForAddressName:self.contact.contactJid].count > 0; + else + { + omemoDeviceForContactFound = NO; + for(NSDictionary* participant in [[DataLayer sharedInstance] getMembersAndParticipantsOfMuc:self.contact.contactJid forAccountId:self.xmppAccount.accountNo]) + { + if(participant[@"participant_jid"]) + omemoDeviceForContactFound |= [self.xmppAccount.omemo knownDevicesForAddressName:participant[@"participant_jid"]].count > 0; + else if(participant[@"member_jid"]) + omemoDeviceForContactFound |= [self.xmppAccount.omemo knownDevicesForAddressName:participant[@"member_jid"]].count > 0; + if(omemoDeviceForContactFound) + break; + } + } + if(!omemoDeviceForContactFound && self.contact.isEncrypted) + { + if(!self.contact.isGroup && [[HelperTools splitJid:self.contact.contactJid][@"host"] isEqualToString:@"cheogram.com"]) + { + // cheogram.com does not support OMEMO encryption as it is a PSTN gateway + // --> disable it + self.contact.isEncrypted = NO; + [[DataLayer sharedInstance] disableEncryptForJid:self.contact.contactJid andAccountNo:self.contact.accountId]; + } + else if(self.contact.isGroup && ![self.contact.mucType isEqualToString:@"group"]) + { + // a channel type muc has OMEMO encryption enabled, but channels don't support encryption + // --> disable it + self.contact.isEncrypted = NO; + [[DataLayer sharedInstance] disableEncryptForJid:self.contact.contactJid andAccountNo:self.contact.accountId]; + } + else if(!self.contact.isGroup || (self.contact.isGroup && [self.contact.mucType isEqualToString:@"group"])) + { + [self hideOmemoHUD]; + if(showWarning) + { + DDLogWarn(@"Showing omemo not supported alert for: %@", self.contact); + UIAlertController* alert = [UIAlertController alertControllerWithTitle:NSLocalizedString(@"No OMEMO keys found", @"") message:NSLocalizedString(@"This contact may not support OMEMO encrypted messages. Please try to enable encryption again in a few seconds, if you think this is wrong.", @"") preferredStyle:UIAlertControllerStyleAlert]; + [alert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Disable Encryption", @"") style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) { + // Disable encryption + self.contact.isEncrypted = NO; + [self updateUIElements]; + [[DataLayer sharedInstance] disableEncryptForJid:self.contact.contactJid andAccountNo:self.contact.accountId]; + [alert dismissViewControllerAnimated:YES completion:nil]; + }]]; + [self presentViewController:alert animated:YES completion:nil]; + } + else + { + // async dispatch is needed to show hud on chat open + // we won't do this twice, because the user won't be able to change isEncrypted to YES, + // unless we have omemo devices for that contact + dispatch_async(dispatch_get_main_queue(), ^{ + [self showOmemoHUD]; + }); + // request omemo devicelist + [self.xmppAccount.omemo subscribeAndFetchDevicelistIfNoSessionExistsForJid:self.contact.contactJid]; + } + } + } + else + [self hideOmemoHUD]; + } +#endif +} + +-(void) showOmemoHUD +{ + DDLogVerbose(@"Showing omemo HUD..."); + if(!self.omemoHUD) + { + self.omemoHUD = [MBProgressHUD showHUDAddedTo:self.view animated:YES]; + self.omemoHUD.removeFromSuperViewOnHide = YES; + self.omemoHUD.label.text = NSLocalizedString(@"Loading OMEMO keys", @""); + } + else + self.omemoHUD.hidden = NO; +} + +-(void) hideOmemoHUD +{ + DDLogVerbose(@"Hiding omemo HUD..."); + self.omemoHUD.hidden = YES; +} + +-(void) handleOmemoFetchStateUpdate:(NSNotification*) notification +{ + xmpp* account = notification.object; + MLContact* contact = [MLContact createContactFromJid:notification.userInfo[@"jid"] andAccountNo:account.accountNo]; + if(self.contact && [self.contact isEqualToContact:contact]) + { + DDLogDebug(@"Got omemo fetching update: %@ --> %@", contact, notification.userInfo); + if(!((NSNumber*)notification.userInfo[@"isFetching"]).boolValue) + dispatch_async(dispatch_get_main_queue(), ^{ + //recheck support and show alert if needed + DDLogVerbose(@"Rechecking omemo support with alert, if needed..."); + [self checkOmemoSupportWithAlert:YES]; + }); + } +} + -(void) showUploadHUD { if(!self.uploadHUD) From cee38226379bfd3cdc6eb94fb6556e3ba4605307 Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Mon, 12 Aug 2024 17:06:03 +0200 Subject: [PATCH 22/22] Use workaround for ios bug not properly removing share intentions When removing or disabling a contact, all share intentions for all contacts on this account should be properly removed to nos provide dangling share extension in ios' ui. --- Monal/Classes/HelperTools.h | 1 + Monal/Classes/HelperTools.m | 8 ++++++++ Monal/Classes/MLXMPPManager.m | 5 +---- Monal/Classes/XMPPEdit.m | 10 ++-------- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Monal/Classes/HelperTools.h b/Monal/Classes/HelperTools.h index 636809c640..1765c66694 100644 --- a/Monal/Classes/HelperTools.h +++ b/Monal/Classes/HelperTools.h @@ -130,6 +130,7 @@ void swizzle(Class c, SEL orig, SEL new); +(void) configureFileProtection:(NSString*) protectionLevel forFile:(NSString*) file; +(void) configureFileProtectionFor:(NSString*) file; +(BOOL) isContactBlacklistedForEncryption:(MLContact*) contact; ++(void) removeAllShareInteractionsForAccountNo:(NSNumber*) accountNo; +(NSDictionary*) splitJid:(NSString*) jid; +(void) scheduleBackgroundTask:(BOOL) force; diff --git a/Monal/Classes/HelperTools.m b/Monal/Classes/HelperTools.m index a58a0a58c9..6c1fcb886f 100644 --- a/Monal/Classes/HelperTools.m +++ b/Monal/Classes/HelperTools.m @@ -1529,6 +1529,14 @@ +(BOOL) isContactBlacklistedForEncryption:(MLContact*) contact return blacklisted; } ++(void) removeAllShareInteractionsForAccountNo:(NSNumber*) accountNo +{ + DDLogInfo(@"Removing share interaction for all contacts on account id %@", accountNo); + for(MLContact* contact in [[DataLayer sharedInstance] contactList]) + if(contact.accountId.intValue == accountNo.intValue) + [contact removeShareInteractions]; +} + +(void) scheduleBackgroundTask:(BOOL) force { DDLogInfo(@"Scheduling new BackgroundTask with force=%s...", force ? "yes" : "no"); diff --git a/Monal/Classes/MLXMPPManager.m b/Monal/Classes/MLXMPPManager.m index 1aa9fa13a6..d2c979b14e 100644 --- a/Monal/Classes/MLXMPPManager.m +++ b/Monal/Classes/MLXMPPManager.m @@ -761,10 +761,7 @@ -(void) removeAccountForAccountNo:(NSNumber*) accountNo [self disconnectAccount:accountNo withExplicitLogout:YES]; [[DataLayer sharedInstance] removeAccount:accountNo]; [SAMKeychain deletePasswordForService:kMonalKeychainName account:accountNo.stringValue]; - [INInteraction deleteAllInteractionsWithCompletion:^(NSError* error) { - if(error != nil) - DDLogError(@"Could not delete all SiriKit interactions: %@", error); - }]; + [HelperTools removeAllShareInteractionsForAccountNo:accountNo]; // trigger UI removal [[MLNotificationQueue currentQueue] postNotificationName:kMonalRefresh object:nil userInfo:nil]; } diff --git a/Monal/Classes/XMPPEdit.m b/Monal/Classes/XMPPEdit.m index 9f7c669401..c769b70b10 100644 --- a/Monal/Classes/XMPPEdit.m +++ b/Monal/Classes/XMPPEdit.m @@ -376,10 +376,7 @@ -(IBAction) save:(id) sender { DDLogVerbose(@"Making sure newly created account is not connected and deleting all SiriKit interactions: %@", self.accountNo); [[MLXMPPManager sharedInstance] disconnectAccount:self.accountNo withExplicitLogout:YES]; - [INInteraction deleteAllInteractionsWithCompletion:^(NSError* error) { - if(error != nil) - DDLogError(@"Could not delete all SiriKit interactions: %@", error); - }]; + [HelperTools removeAllShareInteractionsForAccountNo:self.accountNo]; } //trigger view updates to make sure enabled/disabled account state propagates to all ui elements [[MLNotificationQueue currentQueue] postNotificationName:kMonalRefresh object:nil userInfo:nil]; @@ -400,10 +397,7 @@ -(IBAction) save:(id) sender { DDLogVerbose(@"Account is not enabled anymore, deleting all SiriKit interactions and making sure it's disconnected: %@", self.accountNo); [[MLXMPPManager sharedInstance] disconnectAccount:self.accountNo withExplicitLogout:YES]; - [INInteraction deleteAllInteractionsWithCompletion:^(NSError* error) { - if(error != nil) - DDLogError(@"Could not delete all SiriKit interactions: %@", error); - }]; + [HelperTools removeAllShareInteractionsForAccountNo:self.accountNo]; } //this case makes sure we recreate a completely new account instance below (using our new settings) if the account details changed else if(self.detailsChanged)