Support selects a credential identity from the QuickType bar
This commit is contained in:
parent
d4669bbfcb
commit
29d74c48e5
6 changed files with 125 additions and 30 deletions
|
|
@ -110,6 +110,7 @@
|
||||||
9A8F9ECC259ECB410027CE15 /* PasswordSelectionDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A8F9ECB259ECB410027CE15 /* PasswordSelectionDelegate.swift */; };
|
9A8F9ECC259ECB410027CE15 /* PasswordSelectionDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A8F9ECB259ECB410027CE15 /* PasswordSelectionDelegate.swift */; };
|
||||||
9A8F9EE2259EDD520027CE15 /* PasswordTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A8F9EE1259EDD520027CE15 /* PasswordTableViewCell.swift */; };
|
9A8F9EE2259EDD520027CE15 /* PasswordTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A8F9EE1259EDD520027CE15 /* PasswordTableViewCell.swift */; };
|
||||||
9A8F9EF0259EE01A0027CE15 /* PasswordDecryptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A8F9EEF259EE01A0027CE15 /* PasswordDecryptor.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 */; };
|
9ADC954124418A5F0005402E /* PasswordStoreTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9ADC954024418A5F0005402E /* PasswordStoreTest.swift */; };
|
||||||
A20691F41F2A3D0E0096483D /* SecurePasteboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = A20691F31F2A3D0E0096483D /* SecurePasteboard.swift */; };
|
A20691F41F2A3D0E0096483D /* SecurePasteboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = A20691F31F2A3D0E0096483D /* SecurePasteboard.swift */; };
|
||||||
A217ACE41E9BBBBD00A1A6CF /* GitConfigSettingsTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A217ACE31E9BBBBD00A1A6CF /* GitConfigSettingsTableViewController.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 = "<group>"; };
|
9A8F9ECB259ECB410027CE15 /* PasswordSelectionDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasswordSelectionDelegate.swift; sourceTree = "<group>"; };
|
||||||
9A8F9EE1259EDD520027CE15 /* PasswordTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasswordTableViewCell.swift; sourceTree = "<group>"; };
|
9A8F9EE1259EDD520027CE15 /* PasswordTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasswordTableViewCell.swift; sourceTree = "<group>"; };
|
||||||
9A8F9EEF259EE01A0027CE15 /* PasswordDecryptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasswordDecryptor.swift; sourceTree = "<group>"; };
|
9A8F9EEF259EE01A0027CE15 /* PasswordDecryptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasswordDecryptor.swift; sourceTree = "<group>"; };
|
||||||
|
9A8F9F3F25A1A91F0027CE15 /* CredentialProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CredentialProvider.swift; sourceTree = "<group>"; };
|
||||||
9ADC954024418A5F0005402E /* PasswordStoreTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasswordStoreTest.swift; sourceTree = "<group>"; };
|
9ADC954024418A5F0005402E /* PasswordStoreTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasswordStoreTest.swift; sourceTree = "<group>"; };
|
||||||
A20691F31F2A3D0E0096483D /* SecurePasteboard.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SecurePasteboard.swift; sourceTree = "<group>"; };
|
A20691F31F2A3D0E0096483D /* SecurePasteboard.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SecurePasteboard.swift; sourceTree = "<group>"; };
|
||||||
A217ACE31E9BBBBD00A1A6CF /* GitConfigSettingsTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = GitConfigSettingsTableViewController.swift; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
|
A217ACE31E9BBBBD00A1A6CF /* GitConfigSettingsTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = GitConfigSettingsTableViewController.swift; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
|
||||||
|
|
@ -706,6 +708,7 @@
|
||||||
children = (
|
children = (
|
||||||
9A8F9EBC259EA4C50027CE15 /* PasswordsTableDataSource.swift */,
|
9A8F9EBC259EA4C50027CE15 /* PasswordsTableDataSource.swift */,
|
||||||
9A8F9EEF259EE01A0027CE15 /* PasswordDecryptor.swift */,
|
9A8F9EEF259EE01A0027CE15 /* PasswordDecryptor.swift */,
|
||||||
|
9A8F9F3F25A1A91F0027CE15 /* CredentialProvider.swift */,
|
||||||
);
|
);
|
||||||
path = Services;
|
path = Services;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
|
@ -1552,6 +1555,7 @@
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
9A8F9EE2259EDD520027CE15 /* PasswordTableViewCell.swift in Sources */,
|
9A8F9EE2259EDD520027CE15 /* PasswordTableViewCell.swift in Sources */,
|
||||||
|
9A8F9F4025A1A91F0027CE15 /* CredentialProvider.swift in Sources */,
|
||||||
9A8F9ECC259ECB410027CE15 /* PasswordSelectionDelegate.swift in Sources */,
|
9A8F9ECC259ECB410027CE15 /* PasswordSelectionDelegate.swift in Sources */,
|
||||||
30697C5421F63E0B0064FCAC /* CredentialProviderViewController.swift in Sources */,
|
30697C5421F63E0B0064FCAC /* CredentialProviderViewController.swift in Sources */,
|
||||||
9A55C185259E8C5600FA8FD9 /* PasswordsViewController.swift in Sources */,
|
9A55C185259E8C5600FA8FD9 /* PasswordsViewController.swift in Sources */,
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,8 @@ class CredentialProviderViewController: ASCredentialProviderViewController {
|
||||||
embeddedNavigationController.viewControllers.first as! PasswordsViewController
|
embeddedNavigationController.viewControllers.first as! PasswordsViewController
|
||||||
}
|
}
|
||||||
|
|
||||||
|
lazy var credentialProvider = CredentialProvider(viewController: self, extensionContext: self.extensionContext)
|
||||||
|
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
passcodelock.presentPasscodeLockIfNeeded(self)
|
passcodelock.presentPasscodeLockIfNeeded(self)
|
||||||
|
|
@ -33,23 +35,23 @@ class CredentialProviderViewController: ASCredentialProviderViewController {
|
||||||
}
|
}
|
||||||
|
|
||||||
override func prepareCredentialList(for serviceIdentifiers: [ASCredentialServiceIdentifier]) {
|
override func prepareCredentialList(for serviceIdentifiers: [ASCredentialServiceIdentifier]) {
|
||||||
|
credentialProvider.identifier = serviceIdentifiers.first
|
||||||
let url = serviceIdentifiers.first.flatMap { URL(string: $0.identifier) }
|
let url = serviceIdentifiers.first.flatMap { URL(string: $0.identifier) }
|
||||||
passwordsViewController.navigationItem.prompt = url?.host
|
passwordsViewController.navigationItem.prompt = url?.host
|
||||||
let keywords = url?.host?.sanitizedDomain?.components(separatedBy: ".") ?? []
|
let keywords = url?.host?.sanitizedDomain?.components(separatedBy: ".") ?? []
|
||||||
passwordsViewController.showPasswordsWithSuggstion(keywords)
|
passwordsViewController.showPasswordsWithSuggstion(keywords)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func provideCredentialWithoutUserInteraction(for credentialIdentity: ASPasswordCredentialIdentity) {
|
||||||
|
credentialProvider.credentials(for: credentialIdentity)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension CredentialProviderViewController: PasswordSelectionDelegate {
|
extension CredentialProviderViewController: PasswordSelectionDelegate {
|
||||||
func selected(password: PasswordTableEntry) {
|
func selected(password: PasswordTableEntry) {
|
||||||
let passwordEntity = password.passwordEntity
|
let passwordEntity = password.passwordEntity
|
||||||
|
|
||||||
decryptPassword(in: self, with: passwordEntity) { password in
|
credentialProvider.persistAndProvideCredentials(with: passwordEntity.getPath())
|
||||||
let username = password.getUsernameForCompletion()
|
|
||||||
let password = password.password
|
|
||||||
let passwordCredential = ASPasswordCredential(user: username, password: password)
|
|
||||||
self.extensionContext.completeRequest(withSelectedCredential: passwordCredential)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -74,7 +74,16 @@ extension PasswordsViewController: UISearchBarDelegate {
|
||||||
extension PasswordsViewController: UITableViewDelegate {
|
extension PasswordsViewController: UITableViewDelegate {
|
||||||
func tableView(_: UITableView, didSelectRowAt indexPath: IndexPath) {
|
func tableView(_: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||||
tableView.deselectRow(at: indexPath, animated: true)
|
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()
|
UIImpactFeedbackGenerator(style: .medium).impactOccurred()
|
||||||
self.selectionDelegate?.selected(password: entry)
|
self.selectionDelegate?.selected(password: entry)
|
||||||
|
|
|
||||||
70
passAutoFillExtension/Services/CredentialProvider.swift
Normal file
70
passAutoFillExtension/Services/CredentialProvider.swift
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -9,30 +9,22 @@
|
||||||
import UIKit
|
import UIKit
|
||||||
import passKit
|
import passKit
|
||||||
|
|
||||||
func decryptPassword(in controller: UIViewController, with passwordEntity: PasswordEntity, using keyID: String? = nil, completion: @escaping ((Password) -> Void)) {
|
func decryptPassword(in controller: UIViewController, with passwordPath: String, using keyID: String? = nil, completion: @escaping ((Password) -> Void)) {
|
||||||
DispatchQueue.global(qos: .userInteractive).async {
|
|
||||||
do {
|
do {
|
||||||
let requestPGPKeyPassphrase = Utils.createRequestPGPKeyPassphraseHandler(controller: controller)
|
let requestPGPKeyPassphrase = Utils.createRequestPGPKeyPassphraseHandler(controller: controller)
|
||||||
let decryptedPassword = try PasswordStore.shared.decrypt(passwordEntity: passwordEntity, keyID: keyID, requestPGPKeyPassphrase: requestPGPKeyPassphrase)
|
let decryptedPassword = try PasswordStore.shared.decrypt(path: passwordPath, keyID: keyID, requestPGPKeyPassphrase: requestPGPKeyPassphrase)
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
completion(decryptedPassword)
|
completion(decryptedPassword)
|
||||||
}
|
|
||||||
} catch let AppError.pgpPrivateKeyNotFound(keyID: key) {
|
} catch let AppError.pgpPrivateKeyNotFound(keyID: key) {
|
||||||
DispatchQueue.main.async {
|
|
||||||
let alert = UIAlertController(title: "CannotShowPassword".localize(), message: AppError.pgpPrivateKeyNotFound(keyID: key).localizedDescription, preferredStyle: .alert)
|
let alert = UIAlertController(title: "CannotShowPassword".localize(), message: AppError.pgpPrivateKeyNotFound(keyID: key).localizedDescription, preferredStyle: .alert)
|
||||||
alert.addAction(UIAlertAction.cancelAndPopView(controller: controller))
|
alert.addAction(UIAlertAction.cancelAndPopView(controller: controller))
|
||||||
let selectKey = UIAlertAction.selectKey(controller: controller) { action in
|
let selectKey = UIAlertAction.selectKey(controller: controller) { action in
|
||||||
decryptPassword(in: controller, with: passwordEntity, using: action.title, completion: completion)
|
decryptPassword(in: controller, with: passwordPath, using: action.title, completion: completion)
|
||||||
}
|
}
|
||||||
alert.addAction(selectKey)
|
alert.addAction(selectKey)
|
||||||
|
|
||||||
controller.present(alert, animated: true)
|
controller.present(alert, animated: true)
|
||||||
}
|
|
||||||
} catch {
|
} catch {
|
||||||
DispatchQueue.main.async {
|
|
||||||
Utils.alert(title: "CannotCopyPassword".localize(), message: error.localizedDescription, controller: controller)
|
Utils.alert(title: "CannotCopyPassword".localize(), message: error.localizedDescription, controller: controller)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -345,6 +345,17 @@ public class PasswordStore {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func fetchPasswordEntity(with path: String) -> PasswordEntity? {
|
||||||
|
let passwordEntityFetchRequest = NSFetchRequest<NSFetchRequestResult>(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() {
|
public func setAllSynced() {
|
||||||
let passwordEntities = fetchUnsyncedPasswords()
|
let passwordEntities = fetchUnsyncedPasswords()
|
||||||
if !passwordEntities.isEmpty {
|
if !passwordEntities.isEmpty {
|
||||||
|
|
@ -688,6 +699,13 @@ public class PasswordStore {
|
||||||
return Password(name: passwordEntity.getName(), url: url, plainText: plainText)
|
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 {
|
public func encrypt(password: Password, keyID: String? = nil) throws -> Data {
|
||||||
let encryptedDataPath = storeURL.appendingPathComponent(password.url.path)
|
let encryptedDataPath = storeURL.appendingPathComponent(password.url.path)
|
||||||
let keyID = keyID ?? findGPGID(from: encryptedDataPath)
|
let keyID = keyID ?? findGPGID(from: encryptedDataPath)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue