ScanIdentityController.mm 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285
  1. // _____ _
  2. // |_ _| |_ _ _ ___ ___ _ __ __ _
  3. // | | | ' \| '_/ -_) -_) ' \/ _` |_
  4. // |_| |_||_|_| \___\___|_|_|_\__,_(_)
  5. //
  6. // Threema iOS Client
  7. // Copyright (c) 2012-2020 Threema GmbH
  8. //
  9. // This program is free software: you can redistribute it and/or modify
  10. // it under the terms of the GNU Affero General Public License, version 3,
  11. // as published by the Free Software Foundation.
  12. //
  13. // This program is distributed in the hope that it will be useful,
  14. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. // GNU Affero General Public License for more details.
  17. //
  18. // You should have received a copy of the GNU Affero General Public License
  19. // along with this program. If not, see <https://www.gnu.org/licenses/>.
  20. #import <MobileCoreServices/UTCoreTypes.h>
  21. #import "ScanIdentityController.h"
  22. #import "AppDelegate.h"
  23. #import "NewScannedContactViewController.h"
  24. #import "MyIdentityStore.h"
  25. #import "NSString+Hex.h"
  26. #import "NaClCrypto.h"
  27. #import "ContactStore.h"
  28. #import "Contact.h"
  29. #import "IdentityVerifiedViewController.h"
  30. #import "UserSettings.h"
  31. #import "ServerAPIConnector.h"
  32. #import "MBProgressHUD.h"
  33. #import "QRScannerViewController.h"
  34. #import "StatusNavigationBar.h"
  35. #import "PortraitNavigationController.h"
  36. #import "EntityManager.h"
  37. #import "BundleUtil.h"
  38. #import "URLHandler.h"
  39. #ifdef DEBUG
  40. static const DDLogLevel ddLogLevel = DDLogLevelVerbose;
  41. #else
  42. static const DDLogLevel ddLogLevel = DDLogLevelWarning;
  43. #endif
  44. #define THREEMA_ID_SHARE_LINK @"https://threema.id/"
  45. @implementation ScanIdentityController {
  46. NSString *scannedIdentity;
  47. NSData *scannedPublicKey;
  48. Contact *existingContact;
  49. }
  50. - (id)init
  51. {
  52. self = [super init];
  53. if (self) {
  54. self.popupScanResults = YES;
  55. }
  56. return self;
  57. }
  58. + (BOOL)canScan {
  59. if ([UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera]) {
  60. NSArray *mediaTypes = [UIImagePickerController availableMediaTypesForSourceType:UIImagePickerControllerSourceTypeCamera];
  61. return [mediaTypes containsObject:(NSString *)kUTTypeMovie];
  62. }
  63. return NO;
  64. }
  65. static void soundCompletionCallback(SystemSoundID soundId, void *arg) {
  66. AudioServicesRemoveSystemSoundCompletion(soundId);
  67. AudioServicesDisposeSystemSoundID(soundId);
  68. }
  69. - (void)playSuccessSound {
  70. if (![UserSettings sharedUserSettings].inAppSounds)
  71. return;
  72. SystemSoundID scanSuccessSound;
  73. NSString *sendPath = [BundleUtil pathForResource:@"scan_success" ofType:@"caf"];
  74. CFURLRef baseURL = (__bridge CFURLRef)[NSURL fileURLWithPath:sendPath];
  75. AudioServicesCreateSystemSoundID(baseURL, &scanSuccessSound);
  76. AudioServicesAddSystemSoundCompletion(scanSuccessSound, NULL, NULL, soundCompletionCallback, NULL);
  77. AudioServicesPlaySystemSound(scanSuccessSound);
  78. }
  79. - (void)startScan {
  80. [MBProgressHUD showHUDAddedTo:self.containingViewController.view animated:YES];
  81. QRScannerViewController *qrController = [[QRScannerViewController alloc] init];
  82. qrController.delegate = self;
  83. qrController.title = NSLocalizedString(@"scan_identity", nil);
  84. UINavigationController *nav = [[PortraitNavigationController alloc] initWithNavigationBarClass:[StatusNavigationBar class] toolbarClass:nil];
  85. [nav pushViewController:qrController animated:NO];
  86. nav.modalTransitionStyle = UIModalTransitionStyleCrossDissolve;
  87. [self.containingViewController presentViewController:nav animated:YES completion:nil];
  88. }
  89. #pragma mark - QRScannerViewControllerDelegate
  90. - (void)qrScannerViewController:(QRScannerViewController *)controller didScanResult:(NSString *)result {
  91. DDLogVerbose(@"Scanned data: %@", result);
  92. [MBProgressHUD hideHUDForView:self.containingViewController.view animated:NO];
  93. [self processScanResult:result controller:controller];
  94. }
  95. - (void)qrScannerViewControllerDidCancel:(QRScannerViewController *)controller {
  96. DDLogVerbose(@"Scan cancelled");
  97. [MBProgressHUD hideHUDForView:self.containingViewController.view animated:NO];
  98. [self.containingViewController dismissViewControllerAnimated:YES completion:nil];
  99. }
  100. #pragma mark - Scan utility functions
  101. - (void)processScanResult:(NSString*)result controller:(QRScannerViewController *)controller {
  102. /* Is this another Threema identity? */
  103. NSString *_scannedIdentity;
  104. NSData *_scannedPublicKey;
  105. NSDate *_scannedExpirationDate;
  106. if ([self parseScannedContact:result identity:&_scannedIdentity publicKey:&_scannedPublicKey expirationDate:&_scannedExpirationDate]) {
  107. if ([UserSettings sharedUserSettings].inAppVibrate)
  108. AudioServicesPlayAlertSound(kSystemSoundID_Vibrate);
  109. scannedIdentity = _scannedIdentity;
  110. scannedPublicKey = _scannedPublicKey;
  111. if (self.expectedIdentity != nil && ![self.expectedIdentity isEqualToString:scannedIdentity]) {
  112. [self.containingViewController dismissViewControllerAnimated:YES completion:^{
  113. [UIAlertTemplate showAlertWithOwner:[[AppDelegate sharedAppDelegate] currentTopViewController] title:NSLocalizedString(@"scanned_identity_mismatch_title", nil) message:NSLocalizedString(@"scanned_identity_mismatch_message", nil) actionOk:^(UIAlertAction * _Nonnull) {
  114. [controller stopRunning];
  115. }];
  116. }];
  117. return;
  118. }
  119. if ([scannedIdentity isEqualToString:[MyIdentityStore sharedMyIdentityStore].identity]) {
  120. [self.containingViewController dismissViewControllerAnimated:YES completion:^{
  121. [UIAlertTemplate showAlertWithOwner:[[AppDelegate sharedAppDelegate] currentTopViewController] title:NSLocalizedString(@"scanned_own_identity_title", nil) message:@"" actionOk:^(UIAlertAction * _Nonnull) {
  122. [controller stopRunning];
  123. }];
  124. }];
  125. return;
  126. }
  127. if (_scannedExpirationDate != nil && [_scannedExpirationDate compare:[NSDate date]] == NSOrderedAscending) {
  128. [self.containingViewController dismissViewControllerAnimated:YES completion:^{
  129. [UIAlertTemplate showAlertWithOwner:[[AppDelegate sharedAppDelegate] currentTopViewController] title:NSLocalizedString(@"scan_code_expired_title", nil) message:NSLocalizedString(@"scan_code_expired_message", nil) actionOk:^(UIAlertAction * _Nonnull) {
  130. [controller stopRunning];
  131. }];
  132. }];
  133. return;
  134. }
  135. /* Do we already have a contact record for this identity? */
  136. EntityManager *entityManager = [[EntityManager alloc] init];
  137. existingContact = [entityManager.entityFetcher contactForId:scannedIdentity];
  138. if (existingContact != nil) {
  139. /* Check that the public key matches */
  140. if (![existingContact.publicKey isEqualToData:scannedPublicKey]) {
  141. /* Not good */
  142. DDLogError(@"Scanned public key doesn't match for existing identity %@!", scannedIdentity);
  143. [self.containingViewController dismissViewControllerAnimated:YES completion:^{
  144. [UIAlertTemplate showAlertWithOwner:[[AppDelegate sharedAppDelegate] currentTopViewController] title:NSLocalizedString(@"public_key_mismatch_title", nil) message:NSLocalizedString(@"public_key_mismatch_message", nil) actionOk:nil];
  145. }];
  146. } else {
  147. [self playSuccessSound];
  148. [[ContactStore sharedContactStore] upgradeContact:existingContact toVerificationLevel:kVerificationLevelFullyVerified];
  149. [self.containingViewController dismissViewControllerAnimated:YES completion:^{
  150. if (self.popupScanResults) {
  151. UIStoryboard *storyboard = [AppDelegate getMainStoryboard];
  152. UINavigationController *idNavVc = [storyboard instantiateViewControllerWithIdentifier:@"VerifyScannedContact"];
  153. IdentityVerifiedViewController *idVc = [idNavVc.viewControllers objectAtIndex:0];
  154. idVc.contact = existingContact;
  155. [self.containingViewController presentViewController:idNavVc animated:YES completion:nil];
  156. }
  157. }];
  158. }
  159. } else {
  160. [self playSuccessSound];
  161. [self.containingViewController dismissViewControllerAnimated:YES completion:^{
  162. /* don't blindly trust the public key that we scanned - get the key for this
  163. identity from the server and compare */
  164. [MBProgressHUD showHUDAddedTo:self.containingViewController.view animated:YES];
  165. ServerAPIConnector *conn = [[ServerAPIConnector alloc] init];
  166. [conn fetchIdentityInfo:scannedIdentity onCompletion:^(NSData *publicKey, NSNumber *state, NSNumber *type, NSNumber *featureMask) {
  167. [MBProgressHUD hideHUDForView:self.containingViewController.view animated:YES];
  168. if (![scannedPublicKey isEqualToData:publicKey]) {
  169. DDLogError(@"Scanned public key doesn't match key returned by server for %@!", scannedIdentity);
  170. [UIAlertTemplate showAlertWithOwner:[[AppDelegate sharedAppDelegate] currentTopViewController] title:NSLocalizedString(@"public_key_server_mismatch_title", nil) message:NSLocalizedString(@"public_key_server_mismatch_message", nil) actionOk:nil];
  171. } else {
  172. UIStoryboard *storyboard = [AppDelegate getMainStoryboard];
  173. UINavigationController *newNavVc = [storyboard instantiateViewControllerWithIdentifier:@"NewScannedContact"];
  174. NewScannedContactViewController *newVc = [newNavVc.viewControllers objectAtIndex:0];
  175. newVc.identity = scannedIdentity;
  176. newVc.publicKey = scannedPublicKey;
  177. newVc.verificationLevel = kVerificationLevelFullyVerified;
  178. newVc.state = state;
  179. newVc.type = type;
  180. newVc.featureMask = featureMask;
  181. [self.containingViewController presentViewController:newNavVc animated:YES completion:nil];
  182. }
  183. } onError:^(NSError *error) {
  184. [MBProgressHUD hideHUDForView:self.containingViewController.view animated:YES];
  185. [UIAlertTemplate showAlertWithOwner:[[AppDelegate sharedAppDelegate] currentTopViewController] title:error.localizedDescription message:error.localizedFailureReason actionOk:nil];
  186. }];
  187. }];
  188. }
  189. }
  190. else if ([result hasPrefix:THREEMA_ID_SHARE_LINK]) {
  191. [self playSuccessSound];
  192. [self.containingViewController dismissViewControllerAnimated:YES completion:^{
  193. NSURL *shareLink = [NSURL URLWithString:result];
  194. NSString *targetId = [[shareLink.path substringFromIndex:1] uppercaseString];
  195. if (targetId.length != kIdentityLen) {
  196. [UIAlertTemplate showAlertWithOwner:[[AppDelegate sharedAppDelegate] currentTopViewController] title:NSLocalizedString(@"identity_not_found_title", nil) message:NSLocalizedString(@"identity_not_found_message", nil) actionOk:^(UIAlertAction * _Nonnull) {
  197. [controller stopRunning];
  198. }];
  199. return;
  200. }
  201. [URLHandler handleThreemaDotIdUrl:[NSURL URLWithString:result] hideAppChooser:true];
  202. }];
  203. }
  204. else {
  205. NSData *qrCodeWithData = [[NSData alloc] initWithBase64EncodedString:result options:nil];
  206. if (qrCodeWithData != nil) {
  207. [UIAlertTemplate showAlertWithOwner:[[AppDelegate sharedAppDelegate] currentTopViewController] title:NSLocalizedString(@"webClient_scan_error_title", nil) message:NSLocalizedString(@"webClient_scan_error_message", nil) actionOk:^(UIAlertAction * _Nonnull) {
  208. [controller startRunning];
  209. }];
  210. }
  211. DDLogVerbose(@"2D code data not recognized");
  212. }
  213. }
  214. - (int)parseScannedContact:(NSString*)scanData identity:(NSString**)identity publicKey:(NSData**)publicKey expirationDate:(NSDate**)expirationDate {
  215. NSArray *components = [scanData componentsSeparatedByString:@":"];
  216. if (components.count != 2) {
  217. DDLogVerbose(@"Wrong number of components: %lu", (unsigned long)components.count);
  218. return 0;
  219. }
  220. if (![components[0] isEqualToString:@"3mid"]) {
  221. DDLogVerbose(@"Wrong prefix %@", components[0]);
  222. return 0;
  223. }
  224. NSArray *components2 = [components[1] componentsSeparatedByString:@","];
  225. if (components2.count < 2) {
  226. DDLogVerbose(@"Wrong number of components2: %lu", (unsigned long)components2.count);
  227. return 0;
  228. }
  229. *identity = components2[0];
  230. *publicKey = [components2[1] decodeHex];
  231. if (*publicKey == nil || (*publicKey).length != kNaClCryptoPubKeySize) {
  232. DDLogVerbose(@"Invalid public key length");
  233. return 0;
  234. }
  235. if (components2.count >= 3)
  236. *expirationDate = [NSDate dateWithTimeIntervalSince1970:[components2[2] doubleValue]];
  237. else
  238. *expirationDate = nil;
  239. return 1;
  240. }
  241. @end