From a849b667dc638ed51d594d9a3e5acba4076753c7 Mon Sep 17 00:00:00 2001 From: Yishi Lin Date: Mon, 24 Sep 2018 22:03:14 +0800 Subject: [PATCH] Support auto fill (no quicktype bar support) --- pass.xcodeproj/project.pbxproj | 36 +++- .../Base.lproj/MainInterface.storyboard | 51 +++-- .../CredentialProviderViewController.swift | 196 +++++++++++++++++- .../PasscodeExtensionDisplay.swift | 56 +++++ passExtension/PasscodeExtensionDisplay.swift | 3 +- 5 files changed, 309 insertions(+), 33 deletions(-) create mode 100644 passAutoFillExtension/PasscodeExtensionDisplay.swift diff --git a/pass.xcodeproj/project.pbxproj b/pass.xcodeproj/project.pbxproj index 670c062..f5c065a 100644 --- a/pass.xcodeproj/project.pbxproj +++ b/pass.xcodeproj/project.pbxproj @@ -19,13 +19,14 @@ A2367BA01EF0387000C8FE8B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A2367B9F1EF0387000C8FE8B /* Assets.xcassets */; }; A239F51F2157B72700576CBF /* StringExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A239F51E2157B72700576CBF /* StringExtension.swift */; }; A239F5212157B75E00576CBF /* FileManagerExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A239F5202157B75E00576CBF /* FileManagerExtension.swift */; }; - A239F5612157EEB000576CBF /* PasscodeExtensionDisplay.swift in Sources */ = {isa = PBXBuildFile; fileRef = A28C66671EF10EC900A398A1 /* PasscodeExtensionDisplay.swift */; }; A239F5902158C07D00576CBF /* AuthenticationServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A239F58F2158C07D00576CBF /* AuthenticationServices.framework */; }; A239F5962158C08C00576CBF /* AuthenticationServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A239F58F2158C07D00576CBF /* AuthenticationServices.framework */; }; A239F5992158C08C00576CBF /* CredentialProviderViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A239F5982158C08C00576CBF /* CredentialProviderViewController.swift */; }; A239F59C2158C08C00576CBF /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = A239F59A2158C08C00576CBF /* MainInterface.storyboard */; }; A239F5A12158C08C00576CBF /* passAutoFillExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = A239F5952158C08B00576CBF /* passAutoFillExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; A239F5A52158C3F400576CBF /* passKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A26075781EEC6F34005DB03E /* passKit.framework */; }; + A239F5A621591C3200576CBF /* PasscodeExtensionDisplay.swift in Sources */ = {isa = PBXBuildFile; fileRef = A28C66671EF10EC900A398A1 /* PasscodeExtensionDisplay.swift */; }; + A239F5A821591C5C00576CBF /* PasscodeExtensionDisplay.swift in Sources */ = {isa = PBXBuildFile; fileRef = A239F5A721591C5C00576CBF /* PasscodeExtensionDisplay.swift */; }; A26075811EEC6F34005DB03E /* passKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A26075781EEC6F34005DB03E /* passKit.framework */; }; A26075881EEC6F34005DB03E /* passKitTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A26075871EEC6F34005DB03E /* passKitTests.swift */; }; A260758A1EEC6F34005DB03E /* passKit.h in Headers */ = {isa = PBXBuildFile; fileRef = A260757A1EEC6F34005DB03E /* passKit.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -200,6 +201,7 @@ A239F59B2158C08C00576CBF /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = ""; }; A239F59D2158C08C00576CBF /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; A239F59E2158C08C00576CBF /* passAutoFillExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = passAutoFillExtension.entitlements; sourceTree = ""; }; + A239F5A721591C5C00576CBF /* PasscodeExtensionDisplay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasscodeExtensionDisplay.swift; sourceTree = ""; }; A26075781EEC6F34005DB03E /* passKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = passKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; A260757A1EEC6F34005DB03E /* passKit.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = passKit.h; sourceTree = ""; }; A260757B1EEC6F34005DB03E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -216,7 +218,7 @@ A26700351EEC475600176B8A /* passProcessor.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = passProcessor.js; sourceTree = ""; }; A2802BF71E70813A00879216 /* SliderTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SliderTableViewCell.swift; sourceTree = ""; }; A2802BF81E70813A00879216 /* SliderTableViewCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = SliderTableViewCell.xib; sourceTree = ""; }; - A28C66671EF10EC900A398A1 /* PasscodeExtensionDisplay.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = PasscodeExtensionDisplay.swift; path = ../passExtension/PasscodeExtensionDisplay.swift; sourceTree = ""; }; + A28C66671EF10EC900A398A1 /* PasscodeExtensionDisplay.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PasscodeExtensionDisplay.swift; sourceTree = ""; }; A2A61C0C1EEF8DFE00CFE063 /* libPods-passExtension.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = "libPods-passExtension.a"; path = "../../Library/Developer/Xcode/DerivedData/pass-fwlmfsjroyvbfhdyqmglrwfhvjli/Build/Products/Debug-iphonesimulator/libPods-passExtension.a"; sourceTree = ""; }; A2A61C101EEF8E3500CFE063 /* libPods-passKit.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = "libPods-passKit.a"; path = "Pods/../build/Debug-iphoneos/libPods-passKit.a"; sourceTree = ""; }; A2A61C1F1EEFABAD00CFE063 /* UtilsExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UtilsExtension.swift; sourceTree = ""; }; @@ -363,6 +365,7 @@ isa = PBXGroup; children = ( A2A61C2B1EEFDF3300CFE063 /* ExtensionViewController.swift */, + A28C66671EF10EC900A398A1 /* PasscodeExtensionDisplay.swift */, ); name = Controllers; sourceTree = ""; @@ -378,7 +381,8 @@ A239F5972158C08C00576CBF /* passAutoFillExtension */ = { isa = PBXGroup; children = ( - A239F5982158C08C00576CBF /* CredentialProviderViewController.swift */, + A239F5A921591E3700576CBF /* Controllers */, + A239F5AA21591E3D00576CBF /* Helpers */, A239F59A2158C08C00576CBF /* MainInterface.storyboard */, A239F59D2158C08C00576CBF /* Info.plist */, A239F59E2158C08C00576CBF /* passAutoFillExtension.entitlements */, @@ -386,6 +390,22 @@ path = passAutoFillExtension; sourceTree = ""; }; + A239F5A921591E3700576CBF /* Controllers */ = { + isa = PBXGroup; + children = ( + A239F5982158C08C00576CBF /* CredentialProviderViewController.swift */, + A239F5A721591C5C00576CBF /* PasscodeExtensionDisplay.swift */, + ); + name = Controllers; + sourceTree = ""; + }; + A239F5AA21591E3D00576CBF /* Helpers */ = { + isa = PBXGroup; + children = ( + ); + name = Helpers; + sourceTree = ""; + }; A26075791EEC6F34005DB03E /* passKit */ = { isa = PBXGroup; children = ( @@ -428,7 +448,6 @@ children = ( A2C532BD201E5AA100DB9F53 /* PasscodeLockPresenter.swift */, A2C532BC201E5AA000DB9F53 /* PasscodeLockViewController.swift */, - A28C66671EF10EC900A398A1 /* PasscodeExtensionDisplay.swift */, ); name = Controllers; sourceTree = ""; @@ -559,11 +578,11 @@ isa = PBXGroup; children = ( DC917BD51E2E8231000FDF54 /* pass */, - DC13B14F1E8640810097803F /* passTests */, - A26700251EEC466A00176B8A /* passExtension */, A26075791EEC6F34005DB03E /* passKit */, - A26075861EEC6F34005DB03E /* passKitTests */, + A26700251EEC466A00176B8A /* passExtension */, A239F5972158C08C00576CBF /* passAutoFillExtension */, + DC13B14F1E8640810097803F /* passTests */, + A26075861EEC6F34005DB03E /* passKitTests */, DC917BD41E2E8231000FDF54 /* Products */, DC917BED1E2F38C4000FDF54 /* Frameworks */, A51B01737D08DB47BB58F85A /* Pods */, @@ -1059,6 +1078,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + A239F5A821591C5C00576CBF /* PasscodeExtensionDisplay.swift in Sources */, A239F5992158C08C00576CBF /* CredentialProviderViewController.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1067,7 +1087,6 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - A239F5612157EEB000576CBF /* PasscodeExtensionDisplay.swift in Sources */, A2BEC1BB207D2EFE00F3051C /* UIViewExtension.swift in Sources */, A2C532BB201E5A9600DB9F53 /* PasscodeLock.swift in Sources */, A2F4E2151EED800F0011986E /* Password.swift in Sources */, @@ -1101,6 +1120,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + A239F5A621591C3200576CBF /* PasscodeExtensionDisplay.swift in Sources */, A2A61C2C1EEFDF3300CFE063 /* ExtensionViewController.swift in Sources */, A2168A7F1EFD40D5005EA873 /* OnePasswordExtensionConstants.swift in Sources */, ); diff --git a/passAutoFillExtension/Base.lproj/MainInterface.storyboard b/passAutoFillExtension/Base.lproj/MainInterface.storyboard index 64adbaf..14b8942 100644 --- a/passAutoFillExtension/Base.lproj/MainInterface.storyboard +++ b/passAutoFillExtension/Base.lproj/MainInterface.storyboard @@ -1,11 +1,10 @@ - + - - + @@ -13,7 +12,7 @@ - + @@ -30,28 +29,52 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + + + diff --git a/passAutoFillExtension/CredentialProviderViewController.swift b/passAutoFillExtension/CredentialProviderViewController.swift index 3d4f90e..e547fe5 100644 --- a/passAutoFillExtension/CredentialProviderViewController.swift +++ b/passAutoFillExtension/CredentialProviderViewController.swift @@ -9,24 +9,61 @@ import AuthenticationServices import passKit -class CredentialProviderViewController: ASCredentialProviderViewController { +fileprivate class PasswordsTableEntry : NSObject { + var title: String + var categoryText: String + var categoryArray: [String] + var passwordEntity: PasswordEntity? + init(_ entity: PasswordEntity) { + self.title = entity.name! + self.categoryText = entity.getCategoryText() + self.categoryArray = entity.getCategoryArray() + self.passwordEntity = entity + } +} +class CredentialProviderViewController: ASCredentialProviderViewController, UITableViewDataSource, UITableViewDelegate, UISearchBarDelegate { + @IBOutlet weak var searchBar: UISearchBar! + @IBOutlet weak var tableView: UITableView! + + private let passwordStore = PasswordStore.shared + + private var searchActive = false + private var passwordsTableEntries: [PasswordsTableEntry] = [] + private var filteredPasswordsTableEntries: [PasswordsTableEntry] = [] + private lazy var passcodelock: PasscodeExtensionDisplay = { let passcodelock = PasscodeExtensionDisplay(extensionContext: self.extensionContext) return passcodelock }() - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - passcodelock.presentPasscodeLockIfNeeded(self) - } - /* 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]) { + print("prepareCredentialList") + + // clean up the search bar + guard serviceIdentifiers.count > 0 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) } /* @@ -63,9 +100,148 @@ class CredentialProviderViewController: ASCredentialProviderViewController { self.extensionContext.cancelRequest(withError: NSError(domain: ASExtensionErrorDomain, code: ASExtensionError.userCanceled.rawValue)) } - @IBAction func passwordSelected(_ sender: AnyObject?) { - let passwordCredential = ASPasswordCredential(user: "j_appleseed", password: "apple1234") - self.extensionContext.completeRequest(withSelectedCredential: passwordCredential, completionHandler: nil) + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + passcodelock.presentPasscodeLockIfNeeded(self) + } + + override func viewDidLoad() { + super.viewDidLoad() + + // prepare + searchBar.delegate = self + tableView.delegate = self + tableView.dataSource = self + tableView.register(UINib(nibName: "PasswordWithFolderTableViewCell", bundle: nil), forCellReuseIdentifier: "passwordWithFolderTableViewCell") + + // initialize table entries + initPasswordsTableEntries() + } + + private func initPasswordsTableEntries() { + passwordsTableEntries.removeAll() + filteredPasswordsTableEntries.removeAll() + var passwordEntities = [PasswordEntity]() + passwordEntities = self.passwordStore.fetchPasswordEntityCoreData(withDir: false) + passwordsTableEntries = passwordEntities.map { + PasswordsTableEntry($0) + } + } + + // define cell contents, and set long press action + 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 + } + + // select row -> extension returns (with username and password) + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + let entry = getPasswordEntry(by: indexPath) + + guard self.passwordStore.privateKey != nil else { + Utils.alert(title: "Cannot Copy Password", message: "PGP Key is not set. Please set your PGP Key first.", controller: self, completion: nil) + return + } + + let passwordEntity = entry.passwordEntity! + UIImpactFeedbackGenerator(style: .medium).impactOccurred() + DispatchQueue.global(qos: .userInteractive).async { + var decryptedPassword: Password? + do { + decryptedPassword = try self.passwordStore.decrypt(passwordEntity: passwordEntity, requestPGPKeyPassphrase: self.requestPGPKeyPassphrase) + let username = decryptedPassword?.username ?? decryptedPassword?.login ?? "" + let password = decryptedPassword?.password ?? "" + DispatchQueue.main.async {// prepare a dictionary to return + let passwordCredential = ASPasswordCredential(user: username, password: password) + self.extensionContext.completeRequest(withSelectedCredential: passwordCredential, completionHandler: nil) + } + } catch { + print(error) + DispatchQueue.main.async { + // remove the wrong passphrase so that users could enter it next time + self.passwordStore.pgpKeyPassphrase = nil + Utils.alert(title: "Cannot Copy Password", message: error.localizedDescription, controller: self, completion: nil) + } + } + } + } + + func numberOfSectionsInTableView(tableView: UITableView) -> Int { + return 1 + } + + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + if searchActive { + return filteredPasswordsTableEntries.count + } + return passwordsTableEntries.count; + } + + private func requestPGPKeyPassphrase() -> String { + let sem = DispatchSemaphore(value: 0) + var passphrase = "" + DispatchQueue.main.async { + let alert = UIAlertController(title: "Passphrase", message: "Please fill in the passphrase of your PGP secret key.", preferredStyle: UIAlertController.Style.alert) + alert.addAction(UIAlertAction(title: "OK", style: UIAlertAction.Style.default, handler: {_ in + passphrase = alert.textFields!.first!.text! + sem.signal() + })) + alert.addTextField(configurationHandler: {(textField: UITextField!) in + textField.text = "" + textField.isSecureTextEntry = true + }) + self.present(alert, animated: true, completion: nil) + } + let _ = sem.wait(timeout: DispatchTime.distantFuture) + if SharedDefaults[.isRememberPGPPassphraseOn] { + self.passwordStore.pgpKeyPassphrase = passphrase + } + return passphrase + } + + func searchBarCancelButtonClicked(_ searchBar: UISearchBar) { + searchBar.text = "" + searchActive = false + self.tableView.reloadData() + } + + func searchBarSearchButtonClicked(_ searchBar: UISearchBar) { + if let searchText = searchBar.text, searchText.isEmpty == false { + filteredPasswordsTableEntries = passwordsTableEntries.filter { entry in + var matched = false + matched = matched || entry.title.range(of: searchText, options: .caseInsensitive) != nil + matched = matched || searchText.range(of: entry.title, options: .caseInsensitive) != nil + entry.categoryArray.forEach({ (category) in + matched = matched || category.range(of: searchText, options: .caseInsensitive) != nil + matched = matched || searchText.range(of: category, options: .caseInsensitive) != nil + }) + return matched + } + searchActive = true + } else { + searchActive = false + } + self.tableView.reloadData() + } + + func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) { + searchBarSearchButtonClicked(searchBar) + } + + private func getPasswordEntry(by indexPath: IndexPath) -> PasswordsTableEntry { + if searchActive { + return filteredPasswordsTableEntries[indexPath.row] + } else { + return passwordsTableEntries[indexPath.row] + } } - } diff --git a/passAutoFillExtension/PasscodeExtensionDisplay.swift b/passAutoFillExtension/PasscodeExtensionDisplay.swift new file mode 100644 index 0000000..8130a7b --- /dev/null +++ b/passAutoFillExtension/PasscodeExtensionDisplay.swift @@ -0,0 +1,56 @@ +// +// PasscodeLockDisplay.swift +// pass +// +// Created by Yishi Lin on 14/6/17. +// Copyright © 2017 Bob Sun. All rights reserved. +// + +import Foundation +import passKit +import AuthenticationServices + +// cancel means cancel the extension +class PasscodeLockViewControllerForExtension: PasscodeLockViewController { + var originalExtensionContest: ASCredentialProviderExtensionContext? + public convenience init(extensionContext: ASCredentialProviderExtensionContext?) { + self.init() + originalExtensionContest = extensionContext + } + override func viewDidLoad() { + super.viewDidLoad() + cancelButton?.removeTarget(nil, action: nil, for: .allEvents) + cancelButton?.addTarget(self, action: #selector(cancelExtension), for: .touchUpInside) + } + @objc func cancelExtension() { + originalExtensionContest?.cancelRequest(withError: NSError(domain: ASExtensionErrorDomain, code: ASExtensionError.userCanceled.rawValue)) + } +} + +class PasscodeExtensionDisplay { + private var isPasscodePresented = false + private let passcodeLockVC: PasscodeLockViewControllerForExtension + private let extensionContext: ASCredentialProviderExtensionContext? + + public init(extensionContext: ASCredentialProviderExtensionContext?) { + self.extensionContext = extensionContext + passcodeLockVC = PasscodeLockViewControllerForExtension(extensionContext: extensionContext) + passcodeLockVC.dismissCompletionCallback = { [weak self] in + self?.dismiss() + } + passcodeLockVC.setCancellable(true) + } + + // present the passcode lock view if passcode is set and the view controller is not presented + public func presentPasscodeLockIfNeeded(_ extensionVC: UIViewController) { + guard PasscodeLock.shared.hasPasscode && !isPasscodePresented == true else { + return + } + isPasscodePresented = true + extensionVC.present(passcodeLockVC, animated: true, completion: nil) + } + + public func dismiss(animated: Bool = true) { + isPasscodePresented = false + } +} diff --git a/passExtension/PasscodeExtensionDisplay.swift b/passExtension/PasscodeExtensionDisplay.swift index 0878df6..11de111 100644 --- a/passExtension/PasscodeExtensionDisplay.swift +++ b/passExtension/PasscodeExtensionDisplay.swift @@ -7,6 +7,7 @@ // import Foundation +import passKit // cancel means cancel the extension class PasscodeLockViewControllerForExtension: PasscodeLockViewController { @@ -25,7 +26,7 @@ class PasscodeLockViewControllerForExtension: PasscodeLockViewController { } } -open class PasscodeExtensionDisplay { +class PasscodeExtensionDisplay { private var isPasscodePresented = false private let passcodeLockVC: PasscodeLockViewControllerForExtension private let extensionContext: NSExtensionContext?