passforios/passExtension/Controllers/ExtensionViewController.swift

245 lines
12 KiB
Swift

//
// PasswordsViewController.swift
// pass
//
// Created by Yishi Lin on 13/6/17.
// Copyright © 2017 Bob Sun. All rights reserved.
//
import Foundation
import MobileCoreServices
import passKit
class ExtensionViewController: UIViewController, UITableViewDataSource, UITableViewDelegate, UISearchBarDelegate, UINavigationBarDelegate {
@IBOutlet var searchBar: UISearchBar!
@IBOutlet var tableView: UITableView!
private let passwordStore = PasswordStore.shared
private let keychain = AppKeychain.shared
private var searchActive = false
private var passwordsTableEntries: [PasswordTableEntry] = []
private var filteredPasswordsTableEntries: [PasswordTableEntry] = []
enum Action {
case findLogin, fillBrowser, unknown
}
private var extensionAction = Action.unknown
private lazy var passcodelock: PasscodeExtensionDisplay = {
let passcodelock = PasscodeExtensionDisplay(extensionContext: self.extensionContext)
return passcodelock
}()
private func initPasswordsTableEntries() {
filteredPasswordsTableEntries.removeAll()
let passwordEntities = passwordStore.fetchPasswordEntityCoreData(withDir: false)
passwordsTableEntries = passwordEntities.map {
PasswordTableEntry($0)
}
}
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()
// get the provider
guard let extensionItems = extensionContext?.inputItems as? [NSExtensionItem] else {
return
}
for extensionItem in extensionItems {
if let itemProviders = extensionItem.attachments {
for provider in itemProviders {
// search using the extensionContext inputs
if provider.hasItemConformingToTypeIdentifier(OnePasswordExtensionActions.findLogin) {
provider.loadItem(forTypeIdentifier: OnePasswordExtensionActions.findLogin, options: nil, completionHandler: { (item, _) -> Void in
let dictionary = item as! NSDictionary
var url: String?
if var urlString = dictionary[OnePasswordExtensionKey.URLStringKey] as? String {
if !urlString.hasPrefix("http://"), !urlString.hasPrefix("https://") {
urlString = "http://" + urlString
}
url = URL(string: urlString)?.host
}
DispatchQueue.main.async { [weak self] in
self?.extensionAction = .findLogin
// force search (set text, set active, force search)
self?.searchBar.text = url
self?.searchBar.becomeFirstResponder()
self?.searchBarSearchButtonClicked((self?.searchBar)!)
}
})
} else if provider.hasItemConformingToTypeIdentifier(kUTTypePropertyList as String) {
provider.loadItem(forTypeIdentifier: kUTTypePropertyList as String, options: nil, completionHandler: { (item, _) -> Void in
var url: String?
if let dictionary = item as? NSDictionary,
let results = dictionary[NSExtensionJavaScriptPreprocessingResultsKey] as? NSDictionary,
var urlString = results[OnePasswordExtensionKey.URLStringKey] as? String {
if !urlString.hasPrefix("http://"), !urlString.hasPrefix("https://") {
urlString = "http://" + urlString
}
url = URL(string: urlString)?.host
}
DispatchQueue.main.async { [weak self] in
self?.extensionAction = .fillBrowser
// force search (set text, set active, force search)
self?.searchBar.text = url
self?.searchBar.becomeFirstResponder()
self?.searchBarSearchButtonClicked((self?.searchBar)!)
}
})
} else if provider.hasItemConformingToTypeIdentifier(kUTTypeURL as String) {
provider.loadItem(forTypeIdentifier: kUTTypeURL as String, options: nil, completionHandler: { (item, _) -> Void in
let url = (item as? NSURL)!.host
DispatchQueue.main.async { [weak self] in
self?.extensionAction = .fillBrowser
// force search (set text, set active, force search)
self?.searchBar.text = url
self?.searchBar.becomeFirstResponder()
self?.searchBarSearchButtonClicked((self?.searchBar)!)
}
})
}
}
}
}
}
// 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.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(_: UITableView, didSelectRowAt indexPath: IndexPath) {
let entry = getPasswordEntry(by: indexPath)
guard PGPAgent.shared.isPrepared else {
Utils.alert(title: "CannotCopyPassword".localize(), message: "PgpKeyNotSet.".localize(), controller: self, completion: nil)
return
}
let passwordEntity = entry.passwordEntity
UIImpactFeedbackGenerator(style: .medium).impactOccurred()
decryptPassword(passwordEntity: passwordEntity)
}
private func decryptPassword(passwordEntity: PasswordEntity, keyID: String? = nil) {
DispatchQueue.global(qos: .userInteractive).async {
do {
let requestPGPKeyPassphrase = Utils.createRequestPGPKeyPassphraseHandler(controller: self)
let decryptedPassword = try self.passwordStore.decrypt(passwordEntity: passwordEntity, keyID: keyID, requestPGPKeyPassphrase: requestPGPKeyPassphrase)
let username = decryptedPassword.getUsernameForCompletion()
let password = decryptedPassword.password
DispatchQueue.main.async { // prepare a dictionary to return
switch self.extensionAction {
case .findLogin:
let extensionItem = NSExtensionItem()
var returnDictionary = [OnePasswordExtensionKey.usernameKey: username,
OnePasswordExtensionKey.passwordKey: password]
if let totpPassword = decryptedPassword.currentOtp {
returnDictionary[OnePasswordExtensionKey.totpKey] = totpPassword
}
extensionItem.attachments = [NSItemProvider(item: returnDictionary as NSSecureCoding, typeIdentifier: String(kUTTypePropertyList))]
self.extensionContext!.completeRequest(returningItems: [extensionItem], completionHandler: nil)
case .fillBrowser:
Utils.copyToPasteboard(textToCopy: decryptedPassword.password)
// return a dictionary for JavaScript for best-effor fill in
let extensionItem = NSExtensionItem()
let returnDictionary = [NSExtensionJavaScriptFinalizeArgumentKey: ["username": username, "password": password]]
extensionItem.attachments = [NSItemProvider(item: returnDictionary as NSSecureCoding, typeIdentifier: String(kUTTypePropertyList))]
self.extensionContext?.completeRequest(returningItems: [extensionItem], completionHandler: nil)
default:
self.extensionContext?.completeRequest(returningItems: nil, completionHandler: nil)
}
}
} catch let AppError.PgpPrivateKeyNotFound(key) {
DispatchQueue.main.async {
// alert: cancel or try again
let alert = UIAlertController(title: "CannotShowPassword".localize(), message: AppError.PgpPrivateKeyNotFound(keyID: key).localizedDescription, preferredStyle: .alert)
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, completion: nil)
}
} catch {
DispatchQueue.main.async {
Utils.alert(title: "CannotCopyPassword".localize(), message: error.localizedDescription, controller: self, completion: nil)
}
}
}
}
func numberOfSectionsInTableView(tableView _: UITableView) -> Int {
1
}
func tableView(_: UITableView, numberOfRowsInSection _: Int) -> Int {
if searchActive {
return filteredPasswordsTableEntries.count
}
return passwordsTableEntries.count
}
@IBAction
func cancelExtension(_: Any) {
extensionContext!.completeRequest(returningItems: [], completionHandler: nil)
}
func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
searchBar.text = ""
searchActive = false
tableView.reloadData()
}
func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
if let searchText = searchBar.text, searchText.isEmpty == false {
filteredPasswordsTableEntries = passwordsTableEntries.filter { $0.match(searchText) }
searchActive = true
} else {
searchActive = false
}
tableView.reloadData()
}
func searchBar(_ searchBar: UISearchBar, textDidChange _: String) {
searchBarSearchButtonClicked(searchBar)
}
private func getPasswordEntry(by indexPath: IndexPath) -> PasswordTableEntry {
if searchActive {
return filteredPasswordsTableEntries[indexPath.row]
} else {
return passwordsTableEntries[indexPath.row]
}
}
}