123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285 |
- // _____ _
- // |_ _| |_ _ _ ___ ___ _ __ __ _
- // | | | ' \| '_/ -_) -_) ' \/ _` |_
- // |_| |_||_|_| \___\___|_|_|_\__,_(_)
- //
- // 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 <https://www.gnu.org/licenses/>.
- #import <MobileCoreServices/UTCoreTypes.h>
- #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
|