diff --git a/pass.xcodeproj/project.pbxproj b/pass.xcodeproj/project.pbxproj index 63a9cba..be33348 100644 --- a/pass.xcodeproj/project.pbxproj +++ b/pass.xcodeproj/project.pbxproj @@ -101,8 +101,15 @@ 556EC3DA22335D3400934F9C /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 30C25DBF21F3599E00BB27BB /* InfoPlist.strings */; }; 556EC3DB22335D3D00934F9C /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 30C25DBF21F3599E00BB27BB /* InfoPlist.strings */; }; 8BA607EB4C9C8258741AC18C /* Pods_passExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14E955B67C88672AA3A40BA0 /* Pods_passExtension.framework */; }; + 9A55C158259E785600FA8FD9 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DC917BDD1E2E8231000FDF54 /* Assets.xcassets */; }; + 9A55C15F259E785700FA8FD9 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DC917BDD1E2E8231000FDF54 /* Assets.xcassets */; }; + 9A55C185259E8C5600FA8FD9 /* PasswordsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A55C184259E8C5600FA8FD9 /* PasswordsViewController.swift */; }; 9A652414244BB33300DA0A41 /* UIAlertActionExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A652413244BB33300DA0A41 /* UIAlertActionExtension.swift */; }; 9A8A8387402FCCCECB1232A4 /* Pods_passKitTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B2B2F844061EFA534FE9506 /* Pods_passKitTests.framework */; }; + 9A8F9EBD259EA4C50027CE15 /* PasswordsTableDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A8F9EBC259EA4C50027CE15 /* PasswordsTableDataSource.swift */; }; + 9A8F9ECC259ECB410027CE15 /* PasswordSelectionDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A8F9ECB259ECB410027CE15 /* PasswordSelectionDelegate.swift */; }; + 9A8F9EE2259EDD520027CE15 /* PasswordTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A8F9EE1259EDD520027CE15 /* PasswordTableViewCell.swift */; }; + 9A8F9EF0259EE01A0027CE15 /* PasswordDecryptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A8F9EEF259EE01A0027CE15 /* PasswordDecryptor.swift */; }; 9ADC954124418A5F0005402E /* PasswordStoreTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9ADC954024418A5F0005402E /* PasswordStoreTest.swift */; }; A20691F41F2A3D0E0096483D /* SecurePasteboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = A20691F31F2A3D0E0096483D /* SecurePasteboard.swift */; }; A217ACE41E9BBBBD00A1A6CF /* GitConfigSettingsTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A217ACE31E9BBBBD00A1A6CF /* GitConfigSettingsTableViewController.swift */; }; @@ -363,7 +370,12 @@ 9A1EF0B424C50E780074FEAC /* passBetaAutoFillExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = passBetaAutoFillExtension.entitlements; sourceTree = ""; }; 9A1EF0B524C50EE00074FEAC /* passBetaExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = passBetaExtension.entitlements; sourceTree = ""; }; 9A1EF0B624C50FEA0074FEAC /* passBetaShortcuts.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = passBetaShortcuts.entitlements; sourceTree = ""; }; + 9A55C184259E8C5600FA8FD9 /* PasswordsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasswordsViewController.swift; sourceTree = ""; }; 9A652413244BB33300DA0A41 /* UIAlertActionExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIAlertActionExtension.swift; sourceTree = ""; }; + 9A8F9EBC259EA4C50027CE15 /* PasswordsTableDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasswordsTableDataSource.swift; sourceTree = ""; }; + 9A8F9ECB259ECB410027CE15 /* PasswordSelectionDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasswordSelectionDelegate.swift; sourceTree = ""; }; + 9A8F9EE1259EDD520027CE15 /* PasswordTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasswordTableViewCell.swift; sourceTree = ""; }; + 9A8F9EEF259EE01A0027CE15 /* PasswordDecryptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasswordDecryptor.swift; sourceTree = ""; }; 9ADC954024418A5F0005402E /* PasswordStoreTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasswordStoreTest.swift; sourceTree = ""; }; A20691F31F2A3D0E0096483D /* SecurePasteboard.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SecurePasteboard.swift; sourceTree = ""; }; A217ACE31E9BBBBD00A1A6CF /* GitConfigSettingsTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = GitConfigSettingsTableViewController.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; @@ -689,6 +701,31 @@ path = Crypto; sourceTree = ""; }; + 9A8F9EBB259EA4A80027CE15 /* Services */ = { + isa = PBXGroup; + children = ( + 9A8F9EBC259EA4C50027CE15 /* PasswordsTableDataSource.swift */, + 9A8F9EEF259EE01A0027CE15 /* PasswordDecryptor.swift */, + ); + path = Services; + sourceTree = ""; + }; + 9A8F9ECA259ECAFC0027CE15 /* Protocols */ = { + isa = PBXGroup; + children = ( + 9A8F9ECB259ECB410027CE15 /* PasswordSelectionDelegate.swift */, + ); + path = Protocols; + sourceTree = ""; + }; + 9A8F9EE0259EDD390027CE15 /* Views */ = { + isa = PBXGroup; + children = ( + 9A8F9EE1259EDD520027CE15 /* PasswordTableViewCell.swift */, + ); + path = Views; + sourceTree = ""; + }; A2168A801EFD431A005EA873 /* Controllers */ = { isa = PBXGroup; children = ( @@ -709,6 +746,9 @@ A239F5972158C08C00576CBF /* passAutoFillExtension */ = { isa = PBXGroup; children = ( + 9A8F9EE0259EDD390027CE15 /* Views */, + 9A8F9ECA259ECAFC0027CE15 /* Protocols */, + 9A8F9EBB259EA4A80027CE15 /* Services */, A239F5A921591E3700576CBF /* Controllers */, A239F59E2158C08C00576CBF /* passAutoFillExtension.entitlements */, 9A1EF0B424C50E780074FEAC /* passBetaAutoFillExtension.entitlements */, @@ -722,6 +762,7 @@ isa = PBXGroup; children = ( 30697C5221F63E0B0064FCAC /* CredentialProviderViewController.swift */, + 9A55C184259E8C5600FA8FD9 /* PasswordsViewController.swift */, 30697C5121F63E0B0064FCAC /* PasscodeExtensionDisplay.swift */, ); path = Controllers; @@ -1231,6 +1272,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 9A55C15F259E785700FA8FD9 /* Assets.xcassets in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1242,6 +1284,7 @@ 556EC3D322335C5F00934F9C /* Localizable.strings in Resources */, 556EC3D422335C5F00934F9C /* Localizable.stringsdict in Resources */, A239F59C2158C08C00576CBF /* MainInterface.storyboard in Resources */, + 9A55C158259E785600FA8FD9 /* Assets.xcassets in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1508,8 +1551,13 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 9A8F9EE2259EDD520027CE15 /* PasswordTableViewCell.swift in Sources */, + 9A8F9ECC259ECB410027CE15 /* PasswordSelectionDelegate.swift in Sources */, 30697C5421F63E0B0064FCAC /* CredentialProviderViewController.swift in Sources */, + 9A55C185259E8C5600FA8FD9 /* PasswordsViewController.swift in Sources */, + 9A8F9EBD259EA4C50027CE15 /* PasswordsTableDataSource.swift in Sources */, 30697C5321F63E0B0064FCAC /* PasscodeExtensionDisplay.swift in Sources */, + 9A8F9EF0259EE01A0027CE15 /* PasswordDecryptor.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1886,7 +1934,7 @@ "$(SDKROOT)/usr/include/libxml2", "$(SRCROOT)/Carthage/Build/iOS/ObjectiveGit.framework/Headers/", ); - IPHONEOS_DEPLOYMENT_TARGET = 10.2; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = NO; OTHER_SWIFT_FLAGS = "-D BETA"; PRODUCT_BUNDLE_IDENTIFIER = me.mssun.passforiosbeta; @@ -2459,7 +2507,7 @@ "$(SDKROOT)/usr/include/libxml2", "$(SRCROOT)/Carthage/Build/iOS/ObjectiveGit.framework/Headers/", ); - IPHONEOS_DEPLOYMENT_TARGET = 10.2; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; PRODUCT_BUNDLE_IDENTIFIER = me.mssun.passforios; @@ -2524,7 +2572,7 @@ "$(SDKROOT)/usr/include/libxml2", "$(SRCROOT)/Carthage/Build/iOS/ObjectiveGit.framework/Headers/", ); - IPHONEOS_DEPLOYMENT_TARGET = 10.2; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = NO; PRODUCT_BUNDLE_IDENTIFIER = me.mssun.passforios; PRODUCT_NAME = Pass; diff --git a/pass.xcodeproj/xcshareddata/xcschemes/passAutoFillExtension.xcscheme b/pass.xcodeproj/xcshareddata/xcschemes/passAutoFillExtension.xcscheme new file mode 100644 index 0000000..1a738c1 --- /dev/null +++ b/pass.xcodeproj/xcshareddata/xcschemes/passAutoFillExtension.xcscheme @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pass.xcodeproj/xcshareddata/xcschemes/passExtension.xcscheme b/pass.xcodeproj/xcshareddata/xcschemes/passExtension.xcscheme new file mode 100644 index 0000000..8a78f7e --- /dev/null +++ b/pass.xcodeproj/xcshareddata/xcschemes/passExtension.xcscheme @@ -0,0 +1,100 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pass.xcodeproj/xcshareddata/xcschemes/passShortcuts.xcscheme b/pass.xcodeproj/xcshareddata/xcschemes/passShortcuts.xcscheme new file mode 100644 index 0000000..0c9a773 --- /dev/null +++ b/pass.xcodeproj/xcshareddata/xcschemes/passShortcuts.xcscheme @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pass.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/pass.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/pass.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/passAutoFillExtension/Base.lproj/MainInterface.storyboard b/passAutoFillExtension/Base.lproj/MainInterface.storyboard index c4737b6..f5650b6 100644 --- a/passAutoFillExtension/Base.lproj/MainInterface.storyboard +++ b/passAutoFillExtension/Base.lproj/MainInterface.storyboard @@ -1,31 +1,29 @@ - + + + - + - + + - - - - - - - + + @@ -52,34 +50,52 @@ - - - - - - - + - + - - + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -94,4 +110,9 @@ + + + + + diff --git a/passAutoFillExtension/Controllers/CredentialProviderViewController.swift b/passAutoFillExtension/Controllers/CredentialProviderViewController.swift index cc3a5af..25dad0d 100644 --- a/passAutoFillExtension/Controllers/CredentialProviderViewController.swift +++ b/passAutoFillExtension/Controllers/CredentialProviderViewController.swift @@ -9,208 +9,44 @@ import AuthenticationServices import passKit -class CredentialProviderViewController: ASCredentialProviderViewController, UITableViewDataSource, UITableViewDelegate, UISearchBarDelegate { - @IBOutlet var searchBar: UISearchBar! - @IBOutlet var tableView: UITableView! - - private let passwordStore = PasswordStore.shared - private let keychain = AppKeychain.shared - - private var searchActive = false - private var passwordsTableEntries: [PasswordTableEntry] = [] - private var filteredPasswordsTableEntries: [PasswordTableEntry] = [] - - private lazy var passcodelock: PasscodeExtensionDisplay = { - let passcodelock = PasscodeExtensionDisplay(extensionContext: self.extensionContext) - return passcodelock - }() - - /* - Prepare your UI to list available credentials for the user to choose from. The items in - 'serviceIdentifiers' describe the service the user is logging in to, so your extension can - prioritize the most relevant credentials in the list. - */ - override func prepareCredentialList(for serviceIdentifiers: [ASCredentialServiceIdentifier]) { - // clean up the search bar - guard !serviceIdentifiers.isEmpty else { - searchBar.text = "" - searchBar.becomeFirstResponder() - searchBarSearchButtonClicked(searchBar) - return - } - - // get the domain - var identifier = serviceIdentifiers[0].identifier - if !identifier.hasPrefix("http://"), !identifier.hasPrefix("https://") { - identifier = "http://" + identifier - } - let url = URL(string: identifier)?.host ?? "" - - // "click" search - searchBar.text = url - searchBar.becomeFirstResponder() - searchBarSearchButtonClicked(searchBar) +class CredentialProviderViewController: ASCredentialProviderViewController { + var passcodelock: PasscodeExtensionDisplay { + PasscodeExtensionDisplay(extensionContext: self.extensionContext) } - /* - Implement this method if your extension support - s showing credentials in the QuickType bar. - When the user selects a credential from your app, this method will be called with the - ASPasswordCredentialIdentity your app has previously saved to the ASCredentialIdentityStore. - Provide the password by completing the extension request with the associated ASPasswordCredential. - If using the credential would require showing custom UI for authenticating the user, cancel - the request with error code ASExtensionError.userInteractionRequired. - - override func provideCredentialWithoutUserInteraction(for credentialIdentity: ASPasswordCredentialIdentity) { - let databaseIsUnlocked = true - if (databaseIsUnlocked) { - let passwordCredential = ASPasswordCredential(user: "j_appleseed", password: "apple1234") - self.extensionContext.completeRequest(withSelectedCredential: passwordCredential, completionHandler: nil) - } else { - self.extensionContext.cancelRequest(withError: NSError(domain: ASExtensionErrorDomain, code:ASExtensionError.userInteractionRequired.rawValue)) - } - } - */ - - /* - Implement this method if provideCredentialWithoutUserInteraction(for:) can fail with - ASExtensionError.userInteractionRequired. In this case, the system may present your extension's - UI and call this method. Show appropriate UI for authenticating the user then provide the password - by completing the extension request with the associated ASPasswordCredential. - - override func prepareInterfaceToProvideCredential(for credentialIdentity: ASPasswordCredentialIdentity) { - } - */ - - @IBAction - private func cancel(_: AnyObject?) { - extensionContext.cancelRequest(withError: NSError(domain: ASExtensionErrorDomain, code: ASExtensionError.userCanceled.rawValue)) - self.dismiss(animated: true) + var embeddedNavigationController: UINavigationController { + children.first as! UINavigationController } - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - passcodelock.presentPasscodeLockIfNeeded(self) + var passwordsViewController: PasswordsViewController { + embeddedNavigationController.viewControllers.first as! PasswordsViewController } override func viewDidLoad() { super.viewDidLoad() + passcodelock.presentPasscodeLockIfNeeded(self) - // prepare - searchBar.delegate = self - tableView.delegate = self - tableView.dataSource = self - tableView.register(UINib(nibName: "PasswordWithFolderTableViewCell", bundle: nil), forCellReuseIdentifier: "passwordWithFolderTableViewCell") - - // initialize table entries - initPasswordsTableEntries() + let passwordsTableEntries = PasswordStore.shared.fetchPasswordEntityCoreData(withDir: false).compactMap { PasswordTableEntry($0) } + let dataSource = PasswordsTableDataSource(entries: passwordsTableEntries) + passwordsViewController.dataSource = dataSource + passwordsViewController.selectionDelegate = self } - private func initPasswordsTableEntries() { - filteredPasswordsTableEntries.removeAll() - - let passwordEntities = passwordStore.fetchPasswordEntityCoreData(withDir: false) - passwordsTableEntries = passwordEntities.compactMap { - PasswordTableEntry($0) - } + override func prepareCredentialList(for serviceIdentifiers: [ASCredentialServiceIdentifier]) { + let url = serviceIdentifiers.first.flatMap { URL(string: $0.identifier) } + passwordsViewController.navigationItem.prompt = url?.host } +} - // define cell contents - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let cell = tableView.dequeueReusableCell(withIdentifier: "passwordTableViewCell", for: indexPath) - let entry = getPasswordEntry(by: indexPath) - if entry.passwordEntity.synced { - cell.textLabel?.text = entry.title - } else { - cell.textLabel?.text = "↻ \(entry.title)" - } - cell.accessoryType = .none - cell.detailTextLabel?.font = UIFont.preferredFont(forTextStyle: .footnote) - cell.detailTextLabel?.text = entry.categoryText - return cell - } +extension CredentialProviderViewController: PasswordSelectionDelegate { + func selected(password: PasswordTableEntry) { + let passwordEntity = password.passwordEntity - // select row -> extension returns (with username and password) - func tableView(_: UITableView, didSelectRowAt indexPath: IndexPath) { - let entry = getPasswordEntry(by: indexPath) - - guard PGPAgent.shared.isPrepared else { - Utils.alert(title: "CannotCopyPassword".localize(), message: "PgpKeyNotSet.".localize(), controller: self, completion: nil) - return - } - - let passwordEntity = entry.passwordEntity - UIImpactFeedbackGenerator(style: .medium).impactOccurred() - decryptPassword(passwordEntity: passwordEntity) - } - - private func decryptPassword(passwordEntity: PasswordEntity, keyID: String? = nil) { - DispatchQueue.global(qos: .userInteractive).async { - do { - let requestPGPKeyPassphrase = Utils.createRequestPGPKeyPassphraseHandler(controller: self) - let decryptedPassword = try self.passwordStore.decrypt(passwordEntity: passwordEntity, keyID: keyID, requestPGPKeyPassphrase: requestPGPKeyPassphrase) - - let username = decryptedPassword.getUsernameForCompletion() - let password = decryptedPassword.password - DispatchQueue.main.async { - let passwordCredential = ASPasswordCredential(user: username, password: password) - self.extensionContext.completeRequest(withSelectedCredential: passwordCredential) - } - } catch let AppError.pgpPrivateKeyNotFound(keyID: key) { - DispatchQueue.main.async { - let alert = UIAlertController(title: "CannotShowPassword".localize(), message: AppError.pgpPrivateKeyNotFound(keyID: key).localizedDescription, preferredStyle: .alert) - alert.addAction(UIAlertAction.cancelAndPopView(controller: self)) - let selectKey = UIAlertAction.selectKey(controller: self) { action in - self.decryptPassword(passwordEntity: passwordEntity, keyID: action.title) - } - alert.addAction(selectKey) - - self.present(alert, animated: true) - } - } catch { - DispatchQueue.main.async { - Utils.alert(title: "CannotCopyPassword".localize(), message: error.localizedDescription, controller: self) - } - } - } - } - - func numberOfSectionsInTableView(tableView _: UITableView) -> Int { - 1 - } - - func tableView(_: UITableView, numberOfRowsInSection _: Int) -> Int { - if searchActive { - return filteredPasswordsTableEntries.count - } - return passwordsTableEntries.count - } - - func searchBarCancelButtonClicked(_ searchBar: UISearchBar) { - searchBar.text = "" - searchActive = false - tableView.reloadData() - } - - func searchBarSearchButtonClicked(_ searchBar: UISearchBar) { - if let searchText = searchBar.text, searchText.isEmpty == false { - filteredPasswordsTableEntries = passwordsTableEntries.filter { $0.match(searchText) } - searchActive = true - } else { - searchActive = false - } - tableView.reloadData() - } - - func searchBar(_ searchBar: UISearchBar, textDidChange _: String) { - searchBarSearchButtonClicked(searchBar) - } - - private func getPasswordEntry(by indexPath: IndexPath) -> PasswordTableEntry { - if searchActive { - return filteredPasswordsTableEntries[indexPath.row] - } else { - return passwordsTableEntries[indexPath.row] + decryptPassword(in: self, with: passwordEntity) { password in + let username = password.getUsernameForCompletion() + let password = password.password + let passwordCredential = ASPasswordCredential(user: username, password: password) + self.extensionContext.completeRequest(withSelectedCredential: passwordCredential) } } } diff --git a/passAutoFillExtension/Controllers/PasscodeExtensionDisplay.swift b/passAutoFillExtension/Controllers/PasscodeExtensionDisplay.swift index a66b234..dc5f832 100644 --- a/passAutoFillExtension/Controllers/PasscodeExtensionDisplay.swift +++ b/passAutoFillExtension/Controllers/PasscodeExtensionDisplay.swift @@ -12,11 +12,11 @@ import passKit // cancel means cancel the extension class PasscodeLockViewControllerForExtension: PasscodeLockViewController { - var originalExtensionContest: ASCredentialProviderExtensionContext? + var originalExtensionContext: ASCredentialProviderExtensionContext! - convenience init(extensionContext: ASCredentialProviderExtensionContext?) { + convenience init(extensionContext: ASCredentialProviderExtensionContext) { self.init() - self.originalExtensionContest = extensionContext + self.originalExtensionContext = extensionContext } override func viewDidLoad() { @@ -27,7 +27,7 @@ class PasscodeLockViewControllerForExtension: PasscodeLockViewController { @objc func cancelExtension() { - originalExtensionContest?.cancelRequest(withError: NSError(domain: ASExtensionErrorDomain, code: ASExtensionError.userCanceled.rawValue)) + originalExtensionContext.cancelRequest(withError: NSError(domain: ASExtensionErrorDomain, code: ASExtensionError.userCanceled.rawValue)) } } @@ -36,7 +36,7 @@ class PasscodeExtensionDisplay { private let passcodeLockVC: PasscodeLockViewControllerForExtension private let extensionContext: ASCredentialProviderExtensionContext? - init(extensionContext: ASCredentialProviderExtensionContext?) { + init(extensionContext: ASCredentialProviderExtensionContext) { self.extensionContext = extensionContext self.passcodeLockVC = PasscodeLockViewControllerForExtension(extensionContext: extensionContext) passcodeLockVC.dismissCompletionCallback = { [weak self] in diff --git a/passAutoFillExtension/Controllers/PasswordsViewController.swift b/passAutoFillExtension/Controllers/PasswordsViewController.swift new file mode 100644 index 0000000..4559871 --- /dev/null +++ b/passAutoFillExtension/Controllers/PasswordsViewController.swift @@ -0,0 +1,73 @@ +// +// PasswordsViewController.swift +// passAutoFillExtension +// +// Created by Sun, Mingshen on 12/31/20. +// Copyright © 2020 Bob Sun. All rights reserved. +// + +import UIKit +import AuthenticationServices +import passKit + +class PasswordsViewController: UIViewController { + @IBOutlet var tableView: UITableView! + + var dataSource: PasswordsTableDataSource! + weak var selectionDelegate: PasswordSelectionDelegate? + + var searchController: UISearchController { + let uiSearchController = UISearchController(searchResultsController: nil) + uiSearchController.searchBar.isTranslucent = true + uiSearchController.obscuresBackgroundDuringPresentation = false + uiSearchController.searchBar.sizeToFit() + if #available(iOS 13.0, *) { + uiSearchController.searchBar.searchTextField.clearButtonMode = .whileEditing + } + return uiSearchController + } + + override func viewDidLoad() { + super.viewDidLoad() + + navigationItem.searchController = searchController + navigationItem.hidesSearchBarWhenScrolling = false + + searchController.searchBar.delegate = self + + tableView.delegate = self + tableView.dataSource = dataSource + } + + @IBAction + private func cancel(_: AnyObject?) { + self.extensionContext?.cancelRequest(withError: NSError(domain: ASExtensionErrorDomain, code: ASExtensionError.userCanceled.rawValue)) + self.dismiss(animated: true) + } +} + +extension PasswordsViewController: UISearchBarDelegate { + func searchBar(_: UISearchBar, textDidChange searchText: String) { + dataSource.showTableEntries(matching: searchText) + tableView.reloadData() + } + + func searchBarSearchButtonClicked(_ searchBar: UISearchBar) { + searchBar.resignFirstResponder() + } + + func searchBarCancelButtonClicked(_: UISearchBar) { + dataSource.showTableEntries(matching: "") + tableView.reloadData() + } +} + +extension PasswordsViewController: UITableViewDelegate { + func tableView(_: UITableView, didSelectRowAt indexPath: IndexPath) { + tableView.deselectRow(at: indexPath, animated: true) + let entry = dataSource.filteredPasswordsTableEntries[indexPath.row] + + UIImpactFeedbackGenerator(style: .medium).impactOccurred() + self.selectionDelegate?.selected(password: entry) + } +} diff --git a/passAutoFillExtension/Protocols/PasswordSelectionDelegate.swift b/passAutoFillExtension/Protocols/PasswordSelectionDelegate.swift new file mode 100644 index 0000000..dc19c80 --- /dev/null +++ b/passAutoFillExtension/Protocols/PasswordSelectionDelegate.swift @@ -0,0 +1,13 @@ +// +// PasswordSelectionDelegate.swift +// passAutoFillExtension +// +// Created by Sun, Mingshen on 12/31/20. +// Copyright © 2020 Bob Sun. All rights reserved. +// + +import passKit + +protocol PasswordSelectionDelegate: AnyObject { + func selected(password: PasswordTableEntry) +} diff --git a/passAutoFillExtension/Services/PasswordDecryptor.swift b/passAutoFillExtension/Services/PasswordDecryptor.swift new file mode 100644 index 0000000..ea8f291 --- /dev/null +++ b/passAutoFillExtension/Services/PasswordDecryptor.swift @@ -0,0 +1,38 @@ +// +// PasswordDecryptor.swift +// passAutoFillExtension +// +// Created by Sun, Mingshen on 12/31/20. +// Copyright © 2020 Bob Sun. All rights reserved. +// + +import UIKit +import passKit + +func decryptPassword(in controller: UIViewController, with passwordEntity: PasswordEntity, using keyID: String? = nil, completion: @escaping ((Password) -> Void)) { + DispatchQueue.global(qos: .userInteractive).async { + do { + let requestPGPKeyPassphrase = Utils.createRequestPGPKeyPassphraseHandler(controller: controller) + let decryptedPassword = try PasswordStore.shared.decrypt(passwordEntity: passwordEntity, keyID: keyID, requestPGPKeyPassphrase: requestPGPKeyPassphrase) + + DispatchQueue.main.async { + completion(decryptedPassword) + } + } catch let AppError.pgpPrivateKeyNotFound(keyID: key) { + DispatchQueue.main.async { + let alert = UIAlertController(title: "CannotShowPassword".localize(), message: AppError.pgpPrivateKeyNotFound(keyID: key).localizedDescription, preferredStyle: .alert) + alert.addAction(UIAlertAction.cancelAndPopView(controller: controller)) + let selectKey = UIAlertAction.selectKey(controller: controller) { action in + decryptPassword(in: controller, with: passwordEntity, using: action.title, completion: completion) + } + alert.addAction(selectKey) + + controller.present(alert, animated: true) + } + } catch { + DispatchQueue.main.async { + Utils.alert(title: "CannotCopyPassword".localize(), message: error.localizedDescription, controller: controller) + } + } + } +} diff --git a/passAutoFillExtension/Services/PasswordsTableDataSource.swift b/passAutoFillExtension/Services/PasswordsTableDataSource.swift new file mode 100644 index 0000000..d680967 --- /dev/null +++ b/passAutoFillExtension/Services/PasswordsTableDataSource.swift @@ -0,0 +1,42 @@ +// +// PasswordsTableDataSource.swift +// passAutoFillExtension +// +// Created by Sun, Mingshen on 12/31/20. +// Copyright © 2020 Bob Sun. All rights reserved. +// + +import UIKit +import passKit + +class PasswordsTableDataSource: NSObject, UITableViewDataSource { + var passwordTableEntries: [PasswordTableEntry] + var filteredPasswordsTableEntries: [PasswordTableEntry] + + init(entries: [PasswordTableEntry] = []) { + passwordTableEntries = entries + filteredPasswordsTableEntries = passwordTableEntries + } + + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + filteredPasswordsTableEntries.count + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: "passwordTableViewCell", for: indexPath) as! PasswordTableViewCell + + let entry = filteredPasswordsTableEntries[indexPath.row] + cell.configure(with: entry) + + return cell + } + + func showTableEntries(matching text: String) { + guard !text.isEmpty else { + filteredPasswordsTableEntries = passwordTableEntries + return + } + + filteredPasswordsTableEntries = passwordTableEntries.filter { $0.match(text) } + } +} diff --git a/passAutoFillExtension/Views/PasswordTableViewCell.swift b/passAutoFillExtension/Views/PasswordTableViewCell.swift new file mode 100644 index 0000000..3368e8a --- /dev/null +++ b/passAutoFillExtension/Views/PasswordTableViewCell.swift @@ -0,0 +1,22 @@ +// +// PasswordCell.swift +// passAutoFillExtension +// +// Created by Sun, Mingshen on 12/31/20. +// Copyright © 2020 Bob Sun. All rights reserved. +// + +import passKit + +class PasswordTableViewCell: UITableViewCell { + func configure(with entry: PasswordTableEntry) { + if entry.passwordEntity.synced { + textLabel?.text = entry.title + } else { + textLabel?.text = "↻ \(entry.title)" + } + accessoryType = .none + detailTextLabel?.font = UIFont.preferredFont(forTextStyle: .footnote) + detailTextLabel?.text = entry.categoryText + } +}