// // Copyright 2023 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only // import SignalMessaging import SignalServiceKit import UIKit class FingerprintScanViewController: OWSViewController { private let recipientAddress: SignalServiceAddress private let recipientIdentity: OWSRecipientIdentity private let contactName: String private let identityKey: IdentityKey private let fingerprint: OWSFingerprint private lazy var qrCodeScanViewController = QRCodeScanViewController(appearance: .normal) init(recipientAddress: SignalServiceAddress, recipientIdentity: OWSRecipientIdentity) { owsAssertDebug(recipientAddress.isValid) self.recipientAddress = recipientAddress self.recipientIdentity = recipientIdentity self.identityKey = recipientIdentity.identityKey self.fingerprint = OWSFingerprintBuilder( accountManager: TSAccountManager.shared, contactsManager: SSKEnvironment.shared.contactsManagerRef ).fingerprint(withTheirSignal: recipientAddress, theirIdentityKey: recipientIdentity.identityKey) self.contactName = SSKEnvironment.shared.contactsManagerRef.displayName(for: recipientAddress) super.init() title = NSLocalizedString("SCAN_QR_CODE_VIEW_TITLE", comment: "Title for the 'scan QR code' view.") } override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .black qrCodeScanViewController.delegate = self view.addSubview(qrCodeScanViewController.view) qrCodeScanViewController.view.autoPinWidthToSuperview() qrCodeScanViewController.view.autoPin(toTopLayoutGuideOf: self, withInset: 0) addChild(qrCodeScanViewController) let footerView = UIView() footerView.backgroundColor = .ows_gray75 view.addSubview(footerView) footerView.autoPinWidthToSuperview() footerView.autoPinEdge(.top, to: .bottom, of: qrCodeScanViewController.view) footerView.autoPinEdge(toSuperviewEdge: .bottom) let cameraInstructionLabel = UILabel() cameraInstructionLabel.text = NSLocalizedString( "SCAN_CODE_INSTRUCTIONS", comment: "label presented once scanning (camera) view is visible." ) cameraInstructionLabel.font = .systemFont(ofSize: ScaleFromIPhone5To7Plus(14, 18)) cameraInstructionLabel.textColor = .white cameraInstructionLabel.textAlignment = .center cameraInstructionLabel.numberOfLines = 0 cameraInstructionLabel.lineBreakMode = .byWordWrapping footerView.addSubview(cameraInstructionLabel) cameraInstructionLabel.autoPinWidthToSuperview(withMargin: ScaleFromIPhone5To7Plus(16, 30)) let instructionsVMargin = ScaleFromIPhone5To7Plus(10, 20) cameraInstructionLabel.autoPin(toBottomLayoutGuideOf: self, withInset: instructionsVMargin) cameraInstructionLabel.autoPinEdge(toSuperviewEdge: .top, withInset: instructionsVMargin) } override var supportedInterfaceOrientations: UIInterfaceOrientationMask { UIDevice.current.isIPad ? .all : .portrait } override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) { qrCodeScanViewController.view.isHidden = true super.dismiss(animated: flag, completion: completion) } // MARK: - private func verifyCombinedFingerprintData(_ combinedFingerprintData: Data) { AssertIsOnMainThread() do { try fingerprint.matchesLogicalFingerprintsData(combinedFingerprintData) FingerprintScanViewController.showVerificationSucceeded( from: self, identityKey: identityKey, recipientAddress: recipientAddress, contactName: contactName, tag: logTag ) } catch { FingerprintScanViewController.showVerificationFailed( from: self, error: error, retry: { self.qrCodeScanViewController.tryToStartScanning() }, cancel: { self.navigationController?.popViewController(animated: true) }, tag: logTag ) } } static func showVerificationSucceeded( from viewController: UIViewController, identityKey: Data, recipientAddress: SignalServiceAddress, contactName: String, tag: String ) { AssertIsOnMainThread() owsAssertDebug(recipientAddress.isValid) Logger.info("\(tag) Successfully verified safety numbers.") let successTitle = NSLocalizedString("SUCCESSFUL_VERIFICATION_TITLE", comment: "") let descriptionFormat = NSLocalizedString( "SUCCESSFUL_VERIFICATION_DESCRIPTION", comment: "Alert body after verifying privacy with {{other user's name}}" ) let successDescription = String(format: descriptionFormat, contactName) let actionSheet = ActionSheetController(title: successTitle, message: successDescription) actionSheet.addAction(ActionSheetAction( title: NSLocalizedString( "FINGERPRINT_SCAN_VERIFY_BUTTON", comment: "Button that marks user as verified after a successful fingerprint scan." ), style: .default, handler: { _ in OWSIdentityManager.shared.setVerificationState( .verified, identityKey: identityKey, address: recipientAddress, isUserInitiatedChange: true ) viewController.dismiss(animated: true) } )) actionSheet.addAction(ActionSheetAction( title: CommonStrings.dismissButton, style: .cancel, handler: { _ in viewController.dismiss(animated: true) } )) viewController.presentActionSheet(actionSheet) } static func showVerificationFailed( from viewController: UIViewController, error: Error, retry: (() -> Void)? = nil, cancel: (() -> Void)? = nil, tag: String ) { Logger.info("\(tag) Failed to verify safety numbers.") let failureTitle: String? if (error as NSError).code != OWSErrorCode.userError.rawValue { failureTitle = NSLocalizedString("FAILED_VERIFICATION_TITLE", comment: "alert title") } else { // else no title. We don't want to show a big scary "VERIFICATION FAILED" when it's just user error. failureTitle = nil } let actionSheet = ActionSheetController(title: failureTitle, message: error.userErrorDescription) if let retry { actionSheet.addAction(ActionSheetAction( title: CommonStrings.retryButton, style: .default, handler: { _ in retry() } )) } actionSheet.addAction(OWSActionSheets.cancelAction) viewController.presentActionSheet(actionSheet) Logger.warn("\(tag) Identity verification failed with error: \(error)") } } extension FingerprintScanViewController: QRCodeScanDelegate { func qrCodeScanViewScanned( _ qrCodeScanViewController: QRCodeScanViewController, qrCodeData: Data?, qrCodeString: String? ) -> QRCodeScanOutcome { guard let qrCodeData else { // Only accept QR codes with a valid data (not string) payload. return .continueScanning } verifyCombinedFingerprintData(qrCodeData) // Stop scanning even if verification failed. return .stopScanning } func qrCodeScanViewDismiss(_ qrCodeScanViewController: QRCodeScanViewController) { AssertIsOnMainThread() navigationController?.popViewController(animated: true) } }