Previously, the right-hand side of the autofill extension UI was empty; the category/folder text for a password entry wasn't showing. This adjusts the storyboard so the detail is being shown and updates the underlying Swift code to support the fix.
244 lines
10 KiB
Swift
244 lines
10 KiB
Swift
//
|
|
// CredentialProviderViewController.swift
|
|
// passAutoFillExtension
|
|
//
|
|
// Created by Yishi Lin on 2018/9/24.
|
|
// Copyright © 2018 Bob Sun. All rights reserved.
|
|
//
|
|
|
|
import AuthenticationServices
|
|
import passKit
|
|
|
|
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
|
|
}()
|
|
|
|
/*
|
|
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.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)
|
|
}
|
|
|
|
/*
|
|
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 func cancel(_ sender: AnyObject?) {
|
|
self.extensionContext.cancelRequest(withError: NSError(domain: ASExtensionErrorDomain, code: ASExtensionError.userCanceled.rawValue))
|
|
}
|
|
|
|
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
|
|
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.passwordEntity?.getCategoryText()
|
|
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: "CannotCopyPassword".localize(), message: "PgpKeyNotSet.".localize(), 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 {
|
|
DispatchQueue.main.async {
|
|
// remove the wrong passphrase so that users could enter it next time
|
|
self.passwordStore.pgpKeyPassphrase = nil
|
|
Utils.alert(title: "CannotCopyPassword".localize(), 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".localize(), message: "FillInPgpPassphrase.".localize(), preferredStyle: UIAlertController.Style.alert)
|
|
alert.addAction(UIAlertAction(title: "Ok".localize(), 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]
|
|
}
|
|
}
|
|
}
|