From 29d74c48e537abef8d9adfb78d7d69ace39a55d1 Mon Sep 17 00:00:00 2001 From: Mingshen Sun Date: Sun, 3 Jan 2021 15:08:15 -0800 Subject: [PATCH] Support selects a credential identity from the QuickType bar --- pass.xcodeproj/project.pbxproj | 4 ++ .../CredentialProviderViewController.swift | 14 ++-- .../Controllers/PasswordsViewController.swift | 11 ++- .../Services/CredentialProvider.swift | 70 +++++++++++++++++++ .../Services/PasswordDecryptor.swift | 38 ++++------ passKit/Models/PasswordStore.swift | 18 +++++ 6 files changed, 125 insertions(+), 30 deletions(-) create mode 100644 passAutoFillExtension/Services/CredentialProvider.swift diff --git a/pass.xcodeproj/project.pbxproj b/pass.xcodeproj/project.pbxproj index be33348..83b0b57 100644 --- a/pass.xcodeproj/project.pbxproj +++ b/pass.xcodeproj/project.pbxproj @@ -110,6 +110,7 @@ 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 */; }; + 9A8F9F4025A1A91F0027CE15 /* CredentialProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A8F9F3F25A1A91F0027CE15 /* CredentialProvider.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 */; }; @@ -376,6 +377,7 @@ 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 = ""; }; + 9A8F9F3F25A1A91F0027CE15 /* CredentialProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CredentialProvider.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; }; @@ -706,6 +708,7 @@ children = ( 9A8F9EBC259EA4C50027CE15 /* PasswordsTableDataSource.swift */, 9A8F9EEF259EE01A0027CE15 /* PasswordDecryptor.swift */, + 9A8F9F3F25A1A91F0027CE15 /* CredentialProvider.swift */, ); path = Services; sourceTree = ""; @@ -1552,6 +1555,7 @@ buildActionMask = 2147483647; files = ( 9A8F9EE2259EDD520027CE15 /* PasswordTableViewCell.swift in Sources */, + 9A8F9F4025A1A91F0027CE15 /* CredentialProvider.swift in Sources */, 9A8F9ECC259ECB410027CE15 /* PasswordSelectionDelegate.swift in Sources */, 30697C5421F63E0B0064FCAC /* CredentialProviderViewController.swift in Sources */, 9A55C185259E8C5600FA8FD9 /* PasswordsViewController.swift in Sources */, diff --git a/passAutoFillExtension/Controllers/CredentialProviderViewController.swift b/passAutoFillExtension/Controllers/CredentialProviderViewController.swift index e8e85a3..2b87537 100644 --- a/passAutoFillExtension/Controllers/CredentialProviderViewController.swift +++ b/passAutoFillExtension/Controllers/CredentialProviderViewController.swift @@ -22,6 +22,8 @@ class CredentialProviderViewController: ASCredentialProviderViewController { embeddedNavigationController.viewControllers.first as! PasswordsViewController } + lazy var credentialProvider = CredentialProvider(viewController: self, extensionContext: self.extensionContext) + override func viewDidLoad() { super.viewDidLoad() passcodelock.presentPasscodeLockIfNeeded(self) @@ -33,23 +35,23 @@ class CredentialProviderViewController: ASCredentialProviderViewController { } override func prepareCredentialList(for serviceIdentifiers: [ASCredentialServiceIdentifier]) { + credentialProvider.identifier = serviceIdentifiers.first let url = serviceIdentifiers.first.flatMap { URL(string: $0.identifier) } passwordsViewController.navigationItem.prompt = url?.host let keywords = url?.host?.sanitizedDomain?.components(separatedBy: ".") ?? [] passwordsViewController.showPasswordsWithSuggstion(keywords) } + + override func provideCredentialWithoutUserInteraction(for credentialIdentity: ASPasswordCredentialIdentity) { + credentialProvider.credentials(for: credentialIdentity) + } } extension CredentialProviderViewController: PasswordSelectionDelegate { func selected(password: PasswordTableEntry) { let passwordEntity = password.passwordEntity - 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) - } + credentialProvider.persistAndProvideCredentials(with: passwordEntity.getPath()) } } diff --git a/passAutoFillExtension/Controllers/PasswordsViewController.swift b/passAutoFillExtension/Controllers/PasswordsViewController.swift index 6e17b42..9667943 100644 --- a/passAutoFillExtension/Controllers/PasswordsViewController.swift +++ b/passAutoFillExtension/Controllers/PasswordsViewController.swift @@ -74,7 +74,16 @@ extension PasswordsViewController: UISearchBarDelegate { extension PasswordsViewController: UITableViewDelegate { func tableView(_: UITableView, didSelectRowAt indexPath: IndexPath) { tableView.deselectRow(at: indexPath, animated: true) - let entry = dataSource.filteredPasswordsTableEntries[indexPath.row] + var entry: PasswordTableEntry! + if dataSource.showSuggestion { + if indexPath.section == 0 { + entry = dataSource.suggestedPasswordsTableEntries[indexPath.row] + } else { + entry = dataSource.filteredPasswordsTableEntries[indexPath.row] + } + } else { + entry = dataSource.filteredPasswordsTableEntries[indexPath.row] + } UIImpactFeedbackGenerator(style: .medium).impactOccurred() self.selectionDelegate?.selected(password: entry) diff --git a/passAutoFillExtension/Services/CredentialProvider.swift b/passAutoFillExtension/Services/CredentialProvider.swift new file mode 100644 index 0000000..95397bf --- /dev/null +++ b/passAutoFillExtension/Services/CredentialProvider.swift @@ -0,0 +1,70 @@ +// +// CredentialProvider.swift +// passAutoFillExtension +// +// Created by Sun, Mingshen on 1/2/21. +// Copyright © 2021 Bob Sun. All rights reserved. +// + +import AuthenticationServices +import passKit + +class CredentialProvider { + var identifier: ASCredentialServiceIdentifier? + weak var extensionContext: ASCredentialProviderExtensionContext? + weak var viewController: UIViewController? + + init(viewController: UIViewController, extensionContext: ASCredentialProviderExtensionContext) { + self.viewController = viewController + self.extensionContext = extensionContext + } + + func credentials(for identity: ASPasswordCredentialIdentity) { + guard let recordIdentifier = identity.recordIdentifier else { + return + } + guard let pwCredentials = provideCredentials(in: viewController, with: recordIdentifier) else { + return + } + + extensionContext?.completeRequest(withSelectedCredential: pwCredentials) + } + + func persistAndProvideCredentials(with passwordPath: String) { + guard let pwCredentials = provideCredentials(in: viewController, with: passwordPath) else { + return + } + guard let credentialIdentity = provideCredentialIdentity(for: identifier, user: pwCredentials.user, recordIdentifier: passwordPath) else { + return + } + + let store = ASCredentialIdentityStore.shared + store.getState { state in + if state.isEnabled { + ASCredentialIdentityStore.shared.saveCredentialIdentities([credentialIdentity]) + } + } + extensionContext?.completeRequest(withSelectedCredential: pwCredentials) + } +} + +private func provideCredentialIdentity(for identifier: ASCredentialServiceIdentifier?, user: String, recordIdentifier: String?) -> ASPasswordCredentialIdentity? { + guard let serviceIdentifier = identifier else { + return nil + } + return ASPasswordCredentialIdentity(serviceIdentifier: serviceIdentifier, user: user, recordIdentifier: recordIdentifier) +} + +private func provideCredentials(in viewController: UIViewController?, with path: String) -> ASPasswordCredential? { + print(path) + guard let viewController = viewController else { + return nil + } + var credential: ASPasswordCredential? + decryptPassword(in: viewController, with: path) { password in + let username = password.getUsernameForCompletion() + let password = password.password + credential = ASPasswordCredential(user: username, password: password) + } + return credential +} diff --git a/passAutoFillExtension/Services/PasswordDecryptor.swift b/passAutoFillExtension/Services/PasswordDecryptor.swift index ea8f291..b70a505 100644 --- a/passAutoFillExtension/Services/PasswordDecryptor.swift +++ b/passAutoFillExtension/Services/PasswordDecryptor.swift @@ -9,30 +9,22 @@ 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) +func decryptPassword(in controller: UIViewController, with passwordPath: String, using keyID: String? = nil, completion: @escaping ((Password) -> Void)) { + do { + let requestPGPKeyPassphrase = Utils.createRequestPGPKeyPassphraseHandler(controller: controller) + let decryptedPassword = try PasswordStore.shared.decrypt(path: passwordPath, 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) - } + completion(decryptedPassword) + } catch let AppError.pgpPrivateKeyNotFound(keyID: key) { + 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: passwordPath, using: action.title, completion: completion) } + alert.addAction(selectKey) + + controller.present(alert, animated: true) + } catch { + Utils.alert(title: "CannotCopyPassword".localize(), message: error.localizedDescription, controller: controller) } } diff --git a/passKit/Models/PasswordStore.swift b/passKit/Models/PasswordStore.swift index 732e383..323a11c 100644 --- a/passKit/Models/PasswordStore.swift +++ b/passKit/Models/PasswordStore.swift @@ -345,6 +345,17 @@ public class PasswordStore { } } + public func fetchPasswordEntity(with path: String) -> PasswordEntity? { + let passwordEntityFetchRequest = NSFetchRequest(entityName: "PasswordEntity") + passwordEntityFetchRequest.predicate = NSPredicate(format: "path = %@", path) + do { + let passwordEntities = try context.fetch(passwordEntityFetchRequest) as! [PasswordEntity] + return passwordEntities.first + } catch { + fatalError("FailedToFetchPasswords".localize(error)) + } + } + public func setAllSynced() { let passwordEntities = fetchUnsyncedPasswords() if !passwordEntities.isEmpty { @@ -688,6 +699,13 @@ public class PasswordStore { return Password(name: passwordEntity.getName(), url: url, plainText: plainText) } + public func decrypt(path: String, keyID: String? = nil, requestPGPKeyPassphrase: @escaping (String) -> String) throws -> Password { + guard let passwordEntity = fetchPasswordEntity(with: path) else { + throw AppError.decryption + } + return try decrypt(passwordEntity: passwordEntity, keyID: keyID, requestPGPKeyPassphrase: requestPGPKeyPassphrase) + } + public func encrypt(password: Password, keyID: String? = nil) throws -> Data { let encryptedDataPath = storeURL.appendingPathComponent(password.url.path) let keyID = keyID ?? findGPGID(from: encryptedDataPath)