235 lines
10 KiB
Swift
235 lines
10 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 {
|
|
guard let itemProviders = extensionItem.attachments else {
|
|
continue
|
|
}
|
|
for provider in itemProviders {
|
|
// search using the extensionContext inputs
|
|
if provider.hasItemConformingToTypeIdentifier(OnePasswordExtensionActions.findLogin) {
|
|
provider.loadItem(forTypeIdentifier: OnePasswordExtensionActions.findLogin, options: nil) { item, _ in
|
|
self.updateExtension(with: self.getUrl(from: item as! NSDictionary), action: .findLogin)
|
|
}
|
|
} else if provider.hasItemConformingToTypeIdentifier(kUTTypePropertyList as String) {
|
|
provider.loadItem(forTypeIdentifier: kUTTypePropertyList as String, options: nil) { item, _ in
|
|
if let dictionary = item as? NSDictionary, let results = dictionary[NSExtensionJavaScriptPreprocessingResultsKey] as? NSDictionary {
|
|
self.updateExtension(with: self.getUrl(from: results))
|
|
}
|
|
}
|
|
} else if provider.hasItemConformingToTypeIdentifier(kUTTypeURL as String) {
|
|
provider.loadItem(forTypeIdentifier: kUTTypeURL as String, options: nil) { item, _ in
|
|
self.updateExtension(with: (item as? NSURL)!.host)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func getUrl(from dictionary: NSDictionary) -> String? {
|
|
if var urlString = dictionary[OnePasswordExtensionKey.URLStringKey] as? String {
|
|
if !urlString.hasPrefix("http://"), !urlString.hasPrefix("https://") {
|
|
urlString = "http://" + urlString
|
|
}
|
|
return URL(string: urlString)?.host
|
|
}
|
|
return nil
|
|
}
|
|
|
|
private func updateExtension(with url: String?, action: Action = .fillBrowser) {
|
|
// Set text, set active, and force search.
|
|
DispatchQueue.main.async { [weak self] in
|
|
self?.extensionAction = action
|
|
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(keyID: 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
|
|
private 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]
|
|
}
|
|
}
|
|
}
|