// _____ _ // |_ _| |_ _ _ ___ ___ _ __ __ _ // | | | ' \| '_/ -_) -_) ' \/ _` |_ // |_| |_||_|_| \___\___|_|_|_\__,_(_) // // Threema iOS Client // Copyright (c) 2012-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 . #import #import "ScanIdentityController.h" #import "AppDelegate.h" #import "NewScannedContactViewController.h" #import "MyIdentityStore.h" #import "NSString+Hex.h" #import "NaClCrypto.h" #import "ContactStore.h" #import "Contact.h" #import "IdentityVerifiedViewController.h" #import "UserSettings.h" #import "ServerAPIConnector.h" #import "MBProgressHUD.h" #import "QRScannerViewController.h" #import "StatusNavigationBar.h" #import "PortraitNavigationController.h" #import "EntityManager.h" #import "BundleUtil.h" #import "URLHandler.h" #ifdef DEBUG static const DDLogLevel ddLogLevel = DDLogLevelVerbose; #else static const DDLogLevel ddLogLevel = DDLogLevelWarning; #endif #define THREEMA_ID_SHARE_LINK @"https://threema.id/" @implementation ScanIdentityController { NSString *scannedIdentity; NSData *scannedPublicKey; Contact *existingContact; } - (id)init { self = [super init]; if (self) { self.popupScanResults = YES; } return self; } + (BOOL)canScan { if ([UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera]) { NSArray *mediaTypes = [UIImagePickerController availableMediaTypesForSourceType:UIImagePickerControllerSourceTypeCamera]; return [mediaTypes containsObject:(NSString *)kUTTypeMovie]; } return NO; } static void soundCompletionCallback(SystemSoundID soundId, void *arg) { AudioServicesRemoveSystemSoundCompletion(soundId); AudioServicesDisposeSystemSoundID(soundId); } - (void)playSuccessSound { if (![UserSettings sharedUserSettings].inAppSounds) return; SystemSoundID scanSuccessSound; NSString *sendPath = [BundleUtil pathForResource:@"scan_success" ofType:@"caf"]; CFURLRef baseURL = (__bridge CFURLRef)[NSURL fileURLWithPath:sendPath]; AudioServicesCreateSystemSoundID(baseURL, &scanSuccessSound); AudioServicesAddSystemSoundCompletion(scanSuccessSound, NULL, NULL, soundCompletionCallback, NULL); AudioServicesPlaySystemSound(scanSuccessSound); } - (void)startScan { [MBProgressHUD showHUDAddedTo:self.containingViewController.view animated:YES]; QRScannerViewController *qrController = [[QRScannerViewController alloc] init]; qrController.delegate = self; qrController.title = NSLocalizedString(@"scan_identity", nil); UINavigationController *nav = [[PortraitNavigationController alloc] initWithNavigationBarClass:[StatusNavigationBar class] toolbarClass:nil]; [nav pushViewController:qrController animated:NO]; nav.modalTransitionStyle = UIModalTransitionStyleCrossDissolve; [self.containingViewController presentViewController:nav animated:YES completion:nil]; } #pragma mark - QRScannerViewControllerDelegate - (void)qrScannerViewController:(QRScannerViewController *)controller didScanResult:(NSString *)result { DDLogVerbose(@"Scanned data: %@", result); [MBProgressHUD hideHUDForView:self.containingViewController.view animated:NO]; [self processScanResult:result controller:controller]; } - (void)qrScannerViewControllerDidCancel:(QRScannerViewController *)controller { DDLogVerbose(@"Scan cancelled"); [MBProgressHUD hideHUDForView:self.containingViewController.view animated:NO]; [self.containingViewController dismissViewControllerAnimated:YES completion:nil]; } #pragma mark - Scan utility functions - (void)processScanResult:(NSString*)result controller:(QRScannerViewController *)controller { /* Is this another Threema identity? */ NSString *_scannedIdentity; NSData *_scannedPublicKey; NSDate *_scannedExpirationDate; if ([self parseScannedContact:result identity:&_scannedIdentity publicKey:&_scannedPublicKey expirationDate:&_scannedExpirationDate]) { if ([UserSettings sharedUserSettings].inAppVibrate) AudioServicesPlayAlertSound(kSystemSoundID_Vibrate); scannedIdentity = _scannedIdentity; scannedPublicKey = _scannedPublicKey; if (self.expectedIdentity != nil && ![self.expectedIdentity isEqualToString:scannedIdentity]) { [self.containingViewController dismissViewControllerAnimated:YES completion:^{ [UIAlertTemplate showAlertWithOwner:[[AppDelegate sharedAppDelegate] currentTopViewController] title:NSLocalizedString(@"scanned_identity_mismatch_title", nil) message:NSLocalizedString(@"scanned_identity_mismatch_message", nil) actionOk:^(UIAlertAction * _Nonnull) { [controller stopRunning]; }]; }]; return; } if ([scannedIdentity isEqualToString:[MyIdentityStore sharedMyIdentityStore].identity]) { [self.containingViewController dismissViewControllerAnimated:YES completion:^{ [UIAlertTemplate showAlertWithOwner:[[AppDelegate sharedAppDelegate] currentTopViewController] title:NSLocalizedString(@"scanned_own_identity_title", nil) message:@"" actionOk:^(UIAlertAction * _Nonnull) { [controller stopRunning]; }]; }]; return; } if (_scannedExpirationDate != nil && [_scannedExpirationDate compare:[NSDate date]] == NSOrderedAscending) { [self.containingViewController dismissViewControllerAnimated:YES completion:^{ [UIAlertTemplate showAlertWithOwner:[[AppDelegate sharedAppDelegate] currentTopViewController] title:NSLocalizedString(@"scan_code_expired_title", nil) message:NSLocalizedString(@"scan_code_expired_message", nil) actionOk:^(UIAlertAction * _Nonnull) { [controller stopRunning]; }]; }]; return; } /* Do we already have a contact record for this identity? */ EntityManager *entityManager = [[EntityManager alloc] init]; existingContact = [entityManager.entityFetcher contactForId:scannedIdentity]; if (existingContact != nil) { /* Check that the public key matches */ if (![existingContact.publicKey isEqualToData:scannedPublicKey]) { /* Not good */ DDLogError(@"Scanned public key doesn't match for existing identity %@!", scannedIdentity); [self.containingViewController dismissViewControllerAnimated:YES completion:^{ [UIAlertTemplate showAlertWithOwner:[[AppDelegate sharedAppDelegate] currentTopViewController] title:NSLocalizedString(@"public_key_mismatch_title", nil) message:NSLocalizedString(@"public_key_mismatch_message", nil) actionOk:nil]; }]; } else { [self playSuccessSound]; [[ContactStore sharedContactStore] upgradeContact:existingContact toVerificationLevel:kVerificationLevelFullyVerified]; [self.containingViewController dismissViewControllerAnimated:YES completion:^{ if (self.popupScanResults) { UIStoryboard *storyboard = [AppDelegate getMainStoryboard]; UINavigationController *idNavVc = [storyboard instantiateViewControllerWithIdentifier:@"VerifyScannedContact"]; IdentityVerifiedViewController *idVc = [idNavVc.viewControllers objectAtIndex:0]; idVc.contact = existingContact; [self.containingViewController presentViewController:idNavVc animated:YES completion:nil]; } }]; } } else { [self playSuccessSound]; [self.containingViewController dismissViewControllerAnimated:YES completion:^{ /* don't blindly trust the public key that we scanned - get the key for this identity from the server and compare */ [MBProgressHUD showHUDAddedTo:self.containingViewController.view animated:YES]; ServerAPIConnector *conn = [[ServerAPIConnector alloc] init]; [conn fetchIdentityInfo:scannedIdentity onCompletion:^(NSData *publicKey, NSNumber *state, NSNumber *type, NSNumber *featureMask) { [MBProgressHUD hideHUDForView:self.containingViewController.view animated:YES]; if (![scannedPublicKey isEqualToData:publicKey]) { DDLogError(@"Scanned public key doesn't match key returned by server for %@!", scannedIdentity); [UIAlertTemplate showAlertWithOwner:[[AppDelegate sharedAppDelegate] currentTopViewController] title:NSLocalizedString(@"public_key_server_mismatch_title", nil) message:NSLocalizedString(@"public_key_server_mismatch_message", nil) actionOk:nil]; } else { UIStoryboard *storyboard = [AppDelegate getMainStoryboard]; UINavigationController *newNavVc = [storyboard instantiateViewControllerWithIdentifier:@"NewScannedContact"]; NewScannedContactViewController *newVc = [newNavVc.viewControllers objectAtIndex:0]; newVc.identity = scannedIdentity; newVc.publicKey = scannedPublicKey; newVc.verificationLevel = kVerificationLevelFullyVerified; newVc.state = state; newVc.type = type; newVc.featureMask = featureMask; [self.containingViewController presentViewController:newNavVc animated:YES completion:nil]; } } onError:^(NSError *error) { [MBProgressHUD hideHUDForView:self.containingViewController.view animated:YES]; [UIAlertTemplate showAlertWithOwner:[[AppDelegate sharedAppDelegate] currentTopViewController] title:error.localizedDescription message:error.localizedFailureReason actionOk:nil]; }]; }]; } } else if ([result hasPrefix:THREEMA_ID_SHARE_LINK]) { [self playSuccessSound]; [self.containingViewController dismissViewControllerAnimated:YES completion:^{ NSURL *shareLink = [NSURL URLWithString:result]; NSString *targetId = [[shareLink.path substringFromIndex:1] uppercaseString]; if (targetId.length != kIdentityLen) { [UIAlertTemplate showAlertWithOwner:[[AppDelegate sharedAppDelegate] currentTopViewController] title:NSLocalizedString(@"identity_not_found_title", nil) message:NSLocalizedString(@"identity_not_found_message", nil) actionOk:^(UIAlertAction * _Nonnull) { [controller stopRunning]; }]; return; } [URLHandler handleThreemaDotIdUrl:[NSURL URLWithString:result] hideAppChooser:true]; }]; } else { NSData *qrCodeWithData = [[NSData alloc] initWithBase64EncodedString:result options:nil]; if (qrCodeWithData != nil) { [UIAlertTemplate showAlertWithOwner:[[AppDelegate sharedAppDelegate] currentTopViewController] title:NSLocalizedString(@"webClient_scan_error_title", nil) message:NSLocalizedString(@"webClient_scan_error_message", nil) actionOk:^(UIAlertAction * _Nonnull) { [controller startRunning]; }]; } DDLogVerbose(@"2D code data not recognized"); } } - (int)parseScannedContact:(NSString*)scanData identity:(NSString**)identity publicKey:(NSData**)publicKey expirationDate:(NSDate**)expirationDate { NSArray *components = [scanData componentsSeparatedByString:@":"]; if (components.count != 2) { DDLogVerbose(@"Wrong number of components: %lu", (unsigned long)components.count); return 0; } if (![components[0] isEqualToString:@"3mid"]) { DDLogVerbose(@"Wrong prefix %@", components[0]); return 0; } NSArray *components2 = [components[1] componentsSeparatedByString:@","]; if (components2.count < 2) { DDLogVerbose(@"Wrong number of components2: %lu", (unsigned long)components2.count); return 0; } *identity = components2[0]; *publicKey = [components2[1] decodeHex]; if (*publicKey == nil || (*publicKey).length != kNaClCryptoPubKeySize) { DDLogVerbose(@"Invalid public key length"); return 0; } if (components2.count >= 3) *expirationDate = [NSDate dateWithTimeIntervalSince1970:[components2[2] doubleValue]]; else *expirationDate = nil; return 1; } @end