// _____ _ // |_ _| |_ _ _ ___ ___ _ __ __ _ // | | | ' \| '_/ -_) -_) ' \/ _` |_ // |_| |_||_|_| \___\___|_|_|_\__,_(_) // // Threema iOS Client // Copyright (c) 2015-2020 Threema GmbH // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License, version 3, // as published by the Free Software Foundation. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . #include #import "URLHandler.h" #import "ServerAPIConnector.h" #import "AppDelegate.h" #import "MyIdentityStore.h" #import "UIDefines.h" #import "ContactStore.h" #import "ShareController.h" #import "NSString+Hex.h" #import "ScanIdentityController.h" #import "LicenseStore.h" #import "BundleUtil.h" #import "MDMSetup.h" #import "WorkDataFetcher.h" #import "Threema-Swift.h" #ifdef DEBUG static const DDLogLevel ddLogLevel = DDLogLevelVerbose; #else static const DDLogLevel ddLogLevel = DDLogLevelWarning; #endif @implementation URLHandler + (BOOL)handleURL:(NSURL *)url { if ([url.scheme hasPrefix:@"threema"]) { if ([url.host isEqualToString:@"link_mobileno"]) { NSString *code = [url.query stringByReplacingOccurrencesOfString:@"code=" withString:@""]; DDLogVerbose(@"code: %@", code); ServerAPIConnector *conn = [[ServerAPIConnector alloc] init]; [conn linkMobileNoWithStore:[MyIdentityStore sharedMyIdentityStore] code:code onCompletion:^(BOOL linked) { UITabBarController *mainTabBar = [AppDelegate getMainTabBarController]; mainTabBar.selectedIndex = kMyIdentityTabBarIndex; [UIAlertTemplate showAlertWithOwner:[[AppDelegate sharedAppDelegate] currentTopViewController] title:NSLocalizedString(@"mobileno_linked_title", nil) message:NSLocalizedString(@"mobileno_linked_message", nil) actionOk:nil]; } onError:^(NSError *error) { [UIAlertTemplate showAlertWithOwner:[[AppDelegate sharedAppDelegate] currentTopViewController] title:error.localizedDescription message:error.localizedFailureReason actionOk:nil]; }]; return YES; } else if ([url.host isEqualToString:@"restore"]) { /* only react to restore URLs if we're currently presenting the generate key view controller */ AppDelegate *appDelegate = [AppDelegate sharedAppDelegate]; if ([appDelegate isPresentingKeyGeneration]) { appDelegate.urlRestoreData = [url.query stringByReplacingOccurrencesOfString:@"backup=" withString:@""]; [appDelegate presentIDBackupRestore]; } return YES; } else if ([url.host isEqualToString:@"add"] || [url.host isEqualToString:@"compose"]) { NSDictionary *query = [url.query dictionaryFromQueryComponents]; NSString *targetId = [[query objectForKey:@"id"][0] uppercaseString]; if (targetId.length == kIdentityLen) { [URLHandler handleAddIdentity:targetId compose:[url.host isEqualToString:@"compose"] query:query]; } else { /* share with unspecified contact */ if ([url.host isEqualToString:@"compose"]) { ShareController *shareController = [[ShareController alloc] init]; shareController.text = [query objectForKey:@"text"][0]; if ([[query objectForKey:@"image"][0] isEqualToString:@"pasteboard"]) shareController.image = [self decryptPasteboardImageWithKey:[query objectForKey:@"key"][0]]; [shareController startShare]; } } return YES; } else if ([url.host isEqualToString:@"license"]) { NSDictionary *query = [url.query dictionaryFromQueryComponents]; LicenseStore *licenseStore = [LicenseStore sharedLicenseStore]; if ([licenseStore isValid] == NO) { [licenseStore performLicenseCheckWithCompletion:^(BOOL success) { if (success) { if ([LicenseStore requiresLicenseKey] == true) { NSString *errorDescription = [BundleUtil localizedStringForKey:@"already_licensed"]; [UIAlertTemplate showAlertWithOwner:[[AppDelegate sharedAppDelegate] currentTopViewController] title:errorDescription message:@"" actionOk:nil]; } } else { NSString *username = [query objectForKey:@"username"][0]; NSString *password = [query objectForKey:@"password"][0]; [licenseStore setLicenseUsername:username]; [licenseStore setLicensePassword:password]; [licenseStore performLicenseCheckWithCompletion:^(BOOL success) { dispatch_async(dispatch_get_main_queue(), ^{ if (success) { [WorkDataFetcher checkUpdateThreemaMDM:^{ dispatch_async(dispatch_get_main_queue(), ^{ if ([[AppDelegate sharedAppDelegate] isPresentingEnterLicense]) { UIViewController *currentVC = [AppDelegate sharedAppDelegate].window.rootViewController; [currentVC dismissViewControllerAnimated:YES completion:^{ NSLog(@"Url handler"); [AppDelegate setupConnection]; }]; } }); } onError:^(NSError *error) { [[NSNotificationCenter defaultCenter] postNotificationName:kNotificationLicenseMissing object:nil]; }]; } else { [[NSNotificationCenter defaultCenter] postNotificationName:kNotificationLicenseMissing object:nil]; } }); }]; } }]; } else { if ([LicenseStore requiresLicenseKey] == true) { NSString *errorDescription = [BundleUtil localizedStringForKey:@"already_licensed"]; [UIAlertTemplate showAlertWithOwner:[[AppDelegate sharedAppDelegate] currentTopViewController] title:errorDescription message:@"" actionOk:nil]; } } return YES; } } else if ([url.scheme isEqualToString:@"file"]) { ShareController *shareController = [[ShareController alloc] init]; shareController.url = url; [shareController startShare]; return YES; } else if (([url.scheme isEqualToString:@"http"] || [url.scheme isEqualToString:@"https"]) && [[url.host lowercaseString] isEqualToString:@"threema.id"]) { [self handleThreemaDotIdUrl:url hideAppChooser:false]; return YES; } return NO; } + (void)handleThreemaDotIdUrl:(NSURL*)url hideAppChooser:(BOOL)hideAppChooser { BOOL composeWithChooser = false; NSString *targetId = [[url.path substringFromIndex:1] uppercaseString]; if (targetId.length != kIdentityLen) { if (![[[url.path substringFromIndex:1] lowercaseString] isEqualToString:@"compose"]) { return; } composeWithChooser = true; } // Check if the "other" app (Work if we are not the Work app, or vice versa) is also installed. // If so, we need to prompt the user for what to do. BOOL mustDisplayAppChooser = NO; BOOL isWorkApp = [LicenseStore requiresLicenseKey]; if (isWorkApp) { // This is the Work app. Check if the regular app is installed. if ([[UIApplication sharedApplication] canOpenURL:[NSURL URLWithString:@"threema://app"]]) { mustDisplayAppChooser = YES; } } else { // This is the regular app. Check if the Work app is installed. if ([[UIApplication sharedApplication] canOpenURL:[NSURL URLWithString:@"threemawork://app"]]) { mustDisplayAppChooser = YES; } } NSString *text = [[url.query dictionaryFromQueryComponents] objectForKey:@"text"][0]; text = [text stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]]; if (mustDisplayAppChooser && hideAppChooser == false) { NSString *newQuery; if (composeWithChooser) { if (text) { newQuery = [NSString stringWithFormat:@"text=%@", text]; } else { // there is no text, we can't show the chooser return; } } else { if (text) { newQuery = [NSString stringWithFormat:@"id=%@&text=%@", targetId, text]; } else { newQuery = [NSString stringWithFormat:@"id=%@", targetId]; } } UIAlertController *alertController = [UIAlertController alertControllerWithTitle:NSLocalizedString(@"Open in ...", nil) message:url.absoluteString preferredStyle:UIAlertControllerStyleAlert]; [alertController addAction:[UIAlertAction actionWithTitle:@"Threema" style:0 handler:^(UIAlertAction * _Nonnull action) { if (!isWorkApp) { if (composeWithChooser) { [URLHandler handleComposeWithChooser:text query:[url.query dictionaryFromQueryComponents]]; } else { [URLHandler handleAddIdentity:targetId compose:YES query:[url.query dictionaryFromQueryComponents]]; } } else { NSURL *newUrl = [NSURL URLWithString:[NSString stringWithFormat:@"threema://compose?%@", newQuery]]; [[UIApplication sharedApplication] openURL:newUrl options:@{} completionHandler:nil]; } }]]; [alertController addAction:[UIAlertAction actionWithTitle:@"Threema Work" style:0 handler:^(UIAlertAction * _Nonnull action) { if (isWorkApp) { if (composeWithChooser) { [URLHandler handleComposeWithChooser:text query:[url.query dictionaryFromQueryComponents]]; } else { [URLHandler handleAddIdentity:targetId compose:YES query:[url.query dictionaryFromQueryComponents]]; } } else { NSURL *newUrl = [NSURL URLWithString:[NSString stringWithFormat:@"threemawork://compose?%@", newQuery]]; [[UIApplication sharedApplication] openURL:newUrl options:@{} completionHandler:nil]; } }]]; [alertController addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"cancel", nil) style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) {}]]; [[[AppDelegate sharedAppDelegate] currentTopViewController] presentViewController:alertController animated:YES completion:nil]; } else { if (composeWithChooser) { [URLHandler handleComposeWithChooser:text query:[url.query dictionaryFromQueryComponents]]; } else { [URLHandler handleAddIdentity:targetId compose:YES query:[url.query dictionaryFromQueryComponents]]; } } } + (void)handleAddIdentity:(NSString*)targetId compose:(BOOL)compose query:(NSDictionary*)query { MDMSetup *mdmSetup = [[MDMSetup alloc] initWithSetup:NO]; if ([mdmSetup disableAddContact]) { /* Ensure this contact already exists, as we are not allowed to add any new ones */ if ([[ContactStore sharedContactStore] contactForIdentity:targetId] == nil) { return; } } if ([targetId isEqualToString:[[MyIdentityStore sharedMyIdentityStore] identity]]) { return; } /* add this ID to the contacts */ [[ContactStore sharedContactStore] addContactWithIdentity:targetId verificationLevel:kVerificationLevelUnverified onCompletion:^(Contact *contact, BOOL alreadyExists) { if (compose && [query objectForKey:@"text"][0] != nil) { ShareController *shareController = [[ShareController alloc] init]; shareController.contact = contact; shareController.text = [query objectForKey:@"text"][0]; if ([[query objectForKey:@"image"][0] isEqualToString:@"pasteboard"]) { shareController.image = [self decryptPasteboardImageWithKey:[query objectForKey:@"key"][0]]; } [shareController startShare]; } else { /* just show contact details */ [[NSNotificationCenter defaultCenter] postNotificationName:kNotificationShowContact object:nil userInfo:[NSDictionary dictionaryWithObject:contact forKey:kKeyContact]]; } } onError:^(NSError *error) { if ([error.domain isEqualToString:NSURLErrorDomain] && error.code == 404) { [UIAlertTemplate showAlertWithOwner:[[AppDelegate sharedAppDelegate] currentTopViewController] title:NSLocalizedString(@"identity_not_found_title", nil) message:NSLocalizedString(@"identity_not_found_message", nil) actionOk:nil]; } else { [UIAlertTemplate showAlertWithOwner:[[AppDelegate sharedAppDelegate] currentTopViewController] title:error.localizedDescription message:error.localizedFailureReason actionOk:nil]; } }]; } + (void)handleComposeWithChooser:(NSString *)text query:(NSDictionary*)query { ShareController *shareController = [[ShareController alloc] init]; shareController.text = [query objectForKey:@"text"][0]; if ([[query objectForKey:@"image"][0] isEqualToString:@"pasteboard"]) { shareController.image = [self decryptPasteboardImageWithKey:[query objectForKey:@"key"][0]]; } [shareController startShare]; } + (BOOL)handleShortCutItem:(UIApplicationShortcutItem *)shortCutItem { if ([shortCutItem.type isEqualToString:@"ch.threema.newmessage"]) { [self composeMessage]; return YES; } else if ([shortCutItem.type isEqualToString:@"ch.threema.myid"]) { UITabBarController *mainTabBar = [AppDelegate getMainTabBarController]; mainTabBar.selectedIndex = kMyIdentityTabBarIndex; return YES; } else if ([shortCutItem.type isEqualToString:@"ch.threema.scanid"]) { if ([ScanIdentityController canScan] == NO) { return NO; } MDMSetup *mdmSetup = [[MDMSetup alloc] initWithSetup:false]; if (mdmSetup.disableAddContact == true) { return NO; } UITabBarController *mainTabBar = [AppDelegate getMainTabBarController]; ScanIdentityController *scanController = [[ScanIdentityController alloc] init]; scanController.containingViewController = mainTabBar; scanController.expectedIdentity = nil; scanController.popupScanResults = YES; [scanController startScan]; return YES; } return NO; } + (void)composeMessage { ShareController *shareController = [[ShareController alloc] init]; [shareController startShare]; } + (UIImage*)decryptPasteboardImageWithKey:(NSString*)keyHex { if (keyHex.length == 0) { /* no key - assume unencrypted image on pasteboard */ return [[UIPasteboard generalPasteboard] image]; } NSData *key = [keyHex decodeHex]; if (key.length != kCCKeySizeAES256) return nil; NSData *imageDataEncrypted = [[UIPasteboard generalPasteboard] dataForPasteboardType:PASTEBOARD_IMAGE_UTI]; if (imageDataEncrypted == nil) return nil; char *imagebuf = malloc(imageDataEncrypted.length); if (imagebuf == NULL) return nil; size_t size_out = 0; if (CCCrypt(kCCDecrypt, kCCAlgorithmAES, kCCOptionPKCS7Padding, key.bytes, key.length, NULL, imageDataEncrypted.bytes, imageDataEncrypted.length, imagebuf, imageDataEncrypted.length, &size_out) != kCCSuccess) { DDLogWarn(@"Pasteboard image decryption failed"); free(imagebuf); return nil; } DDLogInfo(@"Pasteboard image decrypted successfully (%zu bytes)", size_out); NSData *imageData = [NSData dataWithBytesNoCopy:imagebuf length:size_out freeWhenDone:YES]; return [UIImage imageWithData:imageData]; } @end