2018-09-24 15:06:43 +08:00
|
|
|
//
|
|
|
|
|
// CredentialProviderViewController.swift
|
|
|
|
|
// passAutoFillExtension
|
|
|
|
|
//
|
|
|
|
|
// Created by Yishi Lin on 2018/9/24.
|
|
|
|
|
// Copyright © 2018 Bob Sun. All rights reserved.
|
|
|
|
|
//
|
|
|
|
|
|
|
|
|
|
import AuthenticationServices
|
|
|
|
|
import passKit
|
|
|
|
|
|
2018-09-24 22:03:14 +08:00
|
|
|
class CredentialProviderViewController: ASCredentialProviderViewController, UITableViewDataSource, UITableViewDelegate, UISearchBarDelegate {
|
2020-06-28 21:25:40 +02:00
|
|
|
@IBOutlet var searchBar: UISearchBar!
|
|
|
|
|
@IBOutlet var tableView: UITableView!
|
2018-12-09 16:59:07 -08:00
|
|
|
|
2018-09-24 22:03:14 +08:00
|
|
|
private let passwordStore = PasswordStore.shared
|
2019-09-30 02:05:01 +08:00
|
|
|
private let keychain = AppKeychain.shared
|
2018-12-09 16:59:07 -08:00
|
|
|
|
2018-09-24 22:03:14 +08:00
|
|
|
private var searchActive = false
|
2020-02-23 18:05:10 +08:00
|
|
|
private var passwordsTableEntries: [PasswordTableEntry] = []
|
|
|
|
|
private var filteredPasswordsTableEntries: [PasswordTableEntry] = []
|
2018-12-09 16:59:07 -08:00
|
|
|
|
2018-09-24 15:06:43 +08:00
|
|
|
private lazy var passcodelock: PasscodeExtensionDisplay = {
|
|
|
|
|
let passcodelock = PasscodeExtensionDisplay(extensionContext: self.extensionContext)
|
|
|
|
|
return passcodelock
|
|
|
|
|
}()
|
2018-12-09 16:59:07 -08:00
|
|
|
|
2018-09-24 15:06:43 +08:00
|
|
|
/*
|
2020-06-28 21:25:40 +02:00
|
|
|
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.
|
|
|
|
|
*/
|
2018-09-24 15:06:43 +08:00
|
|
|
override func prepareCredentialList(for serviceIdentifiers: [ASCredentialServiceIdentifier]) {
|
2018-09-24 22:03:14 +08:00
|
|
|
// clean up the search bar
|
2020-06-28 21:25:40 +02:00
|
|
|
guard !serviceIdentifiers.isEmpty else {
|
2018-09-24 22:03:14 +08:00
|
|
|
searchBar.text = ""
|
|
|
|
|
searchBar.becomeFirstResponder()
|
|
|
|
|
searchBarSearchButtonClicked(searchBar)
|
|
|
|
|
return
|
|
|
|
|
}
|
2018-12-09 16:59:07 -08:00
|
|
|
|
2018-09-24 22:03:14 +08:00
|
|
|
// get the domain
|
|
|
|
|
var identifier = serviceIdentifiers[0].identifier
|
2020-06-28 21:25:40 +02:00
|
|
|
if !identifier.hasPrefix("http://"), !identifier.hasPrefix("https://") {
|
2018-09-24 22:03:14 +08:00
|
|
|
identifier = "http://" + identifier
|
|
|
|
|
}
|
|
|
|
|
let url = URL(string: identifier)?.host ?? ""
|
2018-12-09 16:59:07 -08:00
|
|
|
|
2018-09-24 22:03:14 +08:00
|
|
|
// "click" search
|
|
|
|
|
searchBar.text = url
|
|
|
|
|
searchBar.becomeFirstResponder()
|
|
|
|
|
searchBarSearchButtonClicked(searchBar)
|
2018-09-24 15:06:43 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/*
|
2020-06-28 21:25:40 +02:00
|
|
|
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))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
*/
|
2018-09-24 15:06:43 +08:00
|
|
|
|
|
|
|
|
/*
|
2020-06-28 21:25:40 +02:00
|
|
|
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.
|
2018-09-24 15:06:43 +08:00
|
|
|
|
2020-06-28 21:25:40 +02:00
|
|
|
override func prepareInterfaceToProvideCredential(for credentialIdentity: ASPasswordCredentialIdentity) {
|
|
|
|
|
}
|
|
|
|
|
*/
|
2018-09-24 15:06:43 +08:00
|
|
|
|
2020-06-28 21:25:40 +02:00
|
|
|
@IBAction
|
2020-07-05 22:49:53 +02:00
|
|
|
private func cancel(_: AnyObject?) {
|
2020-06-28 21:25:40 +02:00
|
|
|
extensionContext.cancelRequest(withError: NSError(domain: ASExtensionErrorDomain, code: ASExtensionError.userCanceled.rawValue))
|
2018-09-24 15:06:43 +08:00
|
|
|
}
|
|
|
|
|
|
2018-09-24 22:03:14 +08:00
|
|
|
override func viewWillAppear(_ animated: Bool) {
|
|
|
|
|
super.viewWillAppear(animated)
|
|
|
|
|
passcodelock.presentPasscodeLockIfNeeded(self)
|
|
|
|
|
}
|
2018-12-09 16:59:07 -08:00
|
|
|
|
2018-09-24 22:03:14 +08:00
|
|
|
override func viewDidLoad() {
|
|
|
|
|
super.viewDidLoad()
|
2018-12-09 16:59:07 -08:00
|
|
|
|
2018-09-24 22:03:14 +08:00
|
|
|
// prepare
|
|
|
|
|
searchBar.delegate = self
|
|
|
|
|
tableView.delegate = self
|
|
|
|
|
tableView.dataSource = self
|
|
|
|
|
tableView.register(UINib(nibName: "PasswordWithFolderTableViewCell", bundle: nil), forCellReuseIdentifier: "passwordWithFolderTableViewCell")
|
2018-12-09 16:59:07 -08:00
|
|
|
|
2018-09-24 22:03:14 +08:00
|
|
|
// initialize table entries
|
|
|
|
|
initPasswordsTableEntries()
|
|
|
|
|
}
|
2018-12-09 16:59:07 -08:00
|
|
|
|
2018-09-24 22:03:14 +08:00
|
|
|
private func initPasswordsTableEntries() {
|
|
|
|
|
filteredPasswordsTableEntries.removeAll()
|
2020-06-28 21:25:40 +02:00
|
|
|
|
|
|
|
|
let passwordEntities = passwordStore.fetchPasswordEntityCoreData(withDir: false)
|
2020-02-23 18:05:10 +08:00
|
|
|
passwordsTableEntries = passwordEntities.compactMap {
|
|
|
|
|
PasswordTableEntry($0)
|
2018-09-24 22:03:14 +08:00
|
|
|
}
|
|
|
|
|
}
|
2018-12-09 16:59:07 -08:00
|
|
|
|
2019-02-27 22:52:48 +10:00
|
|
|
// define cell contents
|
2018-09-24 22:03:14 +08:00
|
|
|
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
|
|
|
|
let cell = tableView.dequeueReusableCell(withIdentifier: "passwordTableViewCell", for: indexPath)
|
|
|
|
|
let entry = getPasswordEntry(by: indexPath)
|
2020-02-23 18:05:10 +08:00
|
|
|
if entry.passwordEntity.synced {
|
2018-09-24 22:03:14 +08:00
|
|
|
cell.textLabel?.text = entry.title
|
|
|
|
|
} else {
|
|
|
|
|
cell.textLabel?.text = "↻ \(entry.title)"
|
|
|
|
|
}
|
|
|
|
|
cell.accessoryType = .none
|
|
|
|
|
cell.detailTextLabel?.font = UIFont.preferredFont(forTextStyle: .footnote)
|
2020-02-23 18:05:10 +08:00
|
|
|
cell.detailTextLabel?.text = entry.categoryText
|
2018-09-24 22:03:14 +08:00
|
|
|
return cell
|
|
|
|
|
}
|
2018-12-09 16:59:07 -08:00
|
|
|
|
2018-09-24 22:03:14 +08:00
|
|
|
// select row -> extension returns (with username and password)
|
2020-06-28 21:25:40 +02:00
|
|
|
func tableView(_: UITableView, didSelectRowAt indexPath: IndexPath) {
|
2018-09-24 22:03:14 +08:00
|
|
|
let entry = getPasswordEntry(by: indexPath)
|
2018-12-09 16:59:07 -08:00
|
|
|
|
2019-09-08 23:00:46 +02:00
|
|
|
guard PGPAgent.shared.isPrepared else {
|
2019-01-14 20:57:45 +01:00
|
|
|
Utils.alert(title: "CannotCopyPassword".localize(), message: "PgpKeyNotSet.".localize(), controller: self, completion: nil)
|
2018-09-24 22:03:14 +08:00
|
|
|
return
|
|
|
|
|
}
|
2018-12-09 16:59:07 -08:00
|
|
|
|
2020-02-23 18:05:10 +08:00
|
|
|
let passwordEntity = entry.passwordEntity
|
2018-09-24 22:03:14 +08:00
|
|
|
UIImpactFeedbackGenerator(style: .medium).impactOccurred()
|
2020-06-28 21:25:40 +02:00
|
|
|
decryptPassword(passwordEntity: passwordEntity)
|
2020-04-18 22:35:17 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private func decryptPassword(passwordEntity: PasswordEntity, keyID: String? = nil) {
|
2018-09-24 22:03:14 +08:00
|
|
|
DispatchQueue.global(qos: .userInteractive).async {
|
|
|
|
|
do {
|
2020-04-13 15:16:03 -07:00
|
|
|
let requestPGPKeyPassphrase = Utils.createRequestPGPKeyPassphraseHandler(controller: self)
|
2020-04-18 22:35:17 -07:00
|
|
|
let decryptedPassword = try self.passwordStore.decrypt(passwordEntity: passwordEntity, keyID: keyID, requestPGPKeyPassphrase: requestPGPKeyPassphrase)
|
|
|
|
|
|
|
|
|
|
let username = decryptedPassword.getUsernameForCompletion()
|
|
|
|
|
let password = decryptedPassword.password
|
|
|
|
|
DispatchQueue.main.async {
|
2018-09-24 22:03:14 +08:00
|
|
|
let passwordCredential = ASPasswordCredential(user: username, password: password)
|
2020-04-18 22:35:17 -07:00
|
|
|
self.extensionContext.completeRequest(withSelectedCredential: passwordCredential)
|
|
|
|
|
}
|
2020-09-20 15:07:18 +02:00
|
|
|
} catch let AppError.pgpPrivateKeyNotFound(keyID: key) {
|
2020-04-18 22:35:17 -07:00
|
|
|
DispatchQueue.main.async {
|
2020-09-20 15:07:18 +02:00
|
|
|
let alert = UIAlertController(title: "CannotShowPassword".localize(), message: AppError.pgpPrivateKeyNotFound(keyID: key).localizedDescription, preferredStyle: .alert)
|
2020-04-18 22:35:17 -07:00
|
|
|
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)
|
2018-09-24 22:03:14 +08:00
|
|
|
}
|
|
|
|
|
} catch {
|
|
|
|
|
DispatchQueue.main.async {
|
2020-04-18 22:35:17 -07:00
|
|
|
Utils.alert(title: "CannotCopyPassword".localize(), message: error.localizedDescription, controller: self)
|
2018-09-24 22:03:14 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2018-12-09 16:59:07 -08:00
|
|
|
|
2020-06-28 21:25:40 +02:00
|
|
|
func numberOfSectionsInTableView(tableView _: UITableView) -> Int {
|
|
|
|
|
1
|
2018-09-24 22:03:14 +08:00
|
|
|
}
|
2018-12-09 16:59:07 -08:00
|
|
|
|
2020-06-28 21:25:40 +02:00
|
|
|
func tableView(_: UITableView, numberOfRowsInSection _: Int) -> Int {
|
2018-09-24 22:03:14 +08:00
|
|
|
if searchActive {
|
|
|
|
|
return filteredPasswordsTableEntries.count
|
|
|
|
|
}
|
2020-06-28 21:25:40 +02:00
|
|
|
return passwordsTableEntries.count
|
2018-09-24 22:03:14 +08:00
|
|
|
}
|
2018-12-09 16:59:07 -08:00
|
|
|
|
2018-09-24 22:03:14 +08:00
|
|
|
func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
|
|
|
|
|
searchBar.text = ""
|
|
|
|
|
searchActive = false
|
2020-06-28 21:25:40 +02:00
|
|
|
tableView.reloadData()
|
2018-09-24 22:03:14 +08:00
|
|
|
}
|
2018-12-09 16:59:07 -08:00
|
|
|
|
2018-09-24 22:03:14 +08:00
|
|
|
func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
|
|
|
|
|
if let searchText = searchBar.text, searchText.isEmpty == false {
|
2020-06-28 21:25:40 +02:00
|
|
|
filteredPasswordsTableEntries = passwordsTableEntries.filter { $0.match(searchText) }
|
2018-09-24 22:03:14 +08:00
|
|
|
searchActive = true
|
|
|
|
|
} else {
|
|
|
|
|
searchActive = false
|
|
|
|
|
}
|
2020-06-28 21:25:40 +02:00
|
|
|
tableView.reloadData()
|
2018-09-24 22:03:14 +08:00
|
|
|
}
|
2018-12-09 16:59:07 -08:00
|
|
|
|
2020-06-28 21:25:40 +02:00
|
|
|
func searchBar(_ searchBar: UISearchBar, textDidChange _: String) {
|
2018-09-24 22:03:14 +08:00
|
|
|
searchBarSearchButtonClicked(searchBar)
|
|
|
|
|
}
|
2018-12-09 16:59:07 -08:00
|
|
|
|
2020-02-23 18:05:10 +08:00
|
|
|
private func getPasswordEntry(by indexPath: IndexPath) -> PasswordTableEntry {
|
2018-09-24 22:03:14 +08:00
|
|
|
if searchActive {
|
|
|
|
|
return filteredPasswordsTableEntries[indexPath.row]
|
|
|
|
|
} else {
|
|
|
|
|
return passwordsTableEntries[indexPath.row]
|
|
|
|
|
}
|
2018-09-24 15:06:43 +08:00
|
|
|
}
|
|
|
|
|
}
|