This disables loading of favicon images associated with password entries and hides any images that are already loaded, using the generic icon instead. The key benefit to this option is to prevent passforios revealing that a given device has a password in its store, which could be gleaned from the fact that favicons are being loaded in this manner.
496 lines
21 KiB
Swift
496 lines
21 KiB
Swift
//
|
|
// PasswordDetailTableViewController.swift
|
|
// pass
|
|
//
|
|
// Created by Mingshen Sun on 2/2/2017.
|
|
// Copyright © 2017 Bob Sun. All rights reserved.
|
|
//
|
|
|
|
import UIKit
|
|
import FavIcon
|
|
import SVProgressHUD
|
|
import passKit
|
|
|
|
class PasswordDetailTableViewController: UITableViewController, UIGestureRecognizerDelegate {
|
|
var passwordEntity: PasswordEntity?
|
|
private var password: Password?
|
|
private var passwordImage: UIImage?
|
|
private var oneTimePasswordIndexPath : IndexPath?
|
|
private var shouldPopCurrentView = false
|
|
private let passwordStore = PasswordStore.shared
|
|
|
|
private lazy var editUIBarButtonItem: UIBarButtonItem = {
|
|
let uiBarButtonItem = UIBarButtonItem(barButtonSystemItem: .edit, target: self, action: #selector(pressEdit(_:)))
|
|
return uiBarButtonItem
|
|
}()
|
|
|
|
private struct TableSection {
|
|
var type: PasswordDetailTableViewControllerSectionType
|
|
var header: String?
|
|
var item: [AdditionField] = []
|
|
|
|
init(type: PasswordDetailTableViewControllerSectionType, header: String? = nil) {
|
|
self.type = type
|
|
self.header = header
|
|
}
|
|
}
|
|
|
|
private var tableData = [TableSection]()
|
|
|
|
private enum PasswordDetailTableViewControllerSectionType {
|
|
case name, main, addition, misc
|
|
}
|
|
|
|
override func viewDidLoad() {
|
|
super.viewDidLoad()
|
|
tableView.register(UINib(nibName: "LabelTableViewCell", bundle: nil), forCellReuseIdentifier: "labelCell")
|
|
tableView.register(UINib(nibName: "PasswordDetailTitleTableViewCell", bundle: nil), forCellReuseIdentifier: "passwordDetailTitleTableViewCell")
|
|
|
|
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(PasswordDetailTableViewController.tapMenu(recognizer:)))
|
|
tapGesture.cancelsTouchesInView = false
|
|
tableView.addGestureRecognizer(tapGesture)
|
|
tapGesture.delegate = self
|
|
|
|
tableView.contentInset = UIEdgeInsetsMake(-36, 0, 44, 0);
|
|
tableView.rowHeight = UITableViewAutomaticDimension
|
|
tableView.estimatedRowHeight = 52
|
|
|
|
editUIBarButtonItem.isEnabled = false
|
|
navigationItem.rightBarButtonItem = editUIBarButtonItem
|
|
if #available(iOS 11.0, *) {
|
|
navigationItem.largeTitleDisplayMode = .never
|
|
}
|
|
|
|
if let imageData = passwordEntity?.getImage() {
|
|
let image = UIImage(data: imageData as Data)
|
|
passwordImage = image
|
|
}
|
|
self.decryptThenShowPassword()
|
|
self.setupOneTimePasswordAutoRefresh()
|
|
|
|
// pop the current view because this password might be "discarded"
|
|
NotificationCenter.default.addObserver(self, selector: #selector(setShouldPopCurrentView), name: .passwordStoreChangeDiscarded, object: nil)
|
|
|
|
// reset the data table if some password (maybe another one) has been updated
|
|
NotificationCenter.default.addObserver(self, selector: #selector(decryptThenShowPassword), name: .passwordStoreUpdated, object: nil)
|
|
|
|
// reset the data table if the disaply settings have been changed
|
|
NotificationCenter.default.addObserver(self, selector: #selector(decryptThenShowPassword), name: .passwordDetailDisplaySettingChanged, object: nil)
|
|
}
|
|
|
|
override func viewDidAppear(_ animated: Bool) {
|
|
super.viewWillAppear(animated)
|
|
if self.shouldPopCurrentView {
|
|
let alert = UIAlertController(title: "Notice".localize(), message: "PreviousChangesDiscarded.".localize(), preferredStyle: UIAlertControllerStyle.alert)
|
|
alert.addAction(UIAlertAction(title: "Ok".localize(), style: UIAlertActionStyle.default, handler: {_ in
|
|
_ = self.navigationController?.popViewController(animated: true)
|
|
}))
|
|
self.present(alert, animated: true, completion: nil)
|
|
}
|
|
}
|
|
|
|
private func requestPGPKeyPassphrase() -> String {
|
|
let sem = DispatchSemaphore(value: 0)
|
|
var passphrase = ""
|
|
DispatchQueue.main.async {
|
|
let alert = UIAlertController(title: "Passphrase".localize(), message: "FillInPgpPassphrase.".localize(), preferredStyle: UIAlertControllerStyle.alert)
|
|
alert.addAction(UIAlertAction(title: "Ok".localize(), style: UIAlertActionStyle.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
|
|
}
|
|
|
|
@objc private func decryptThenShowPassword() {
|
|
guard let passwordEntity = passwordEntity else {
|
|
Utils.alert(title: "CannotShowPassword".localize(), message: "PasswordDoesNotExist".localize(), controller: self, handler: {(UIAlertAction) -> Void in
|
|
self.navigationController!.popViewController(animated: true)
|
|
})
|
|
return
|
|
}
|
|
DispatchQueue.global(qos: .userInitiated).async {
|
|
// decrypt password
|
|
do {
|
|
self.password = try self.passwordStore.decrypt(passwordEntity: passwordEntity, requestPGPKeyPassphrase: self.requestPGPKeyPassphrase)
|
|
} catch {
|
|
DispatchQueue.main.async {
|
|
// remove the wrong passphrase so that users could enter it next time
|
|
self.passwordStore.pgpKeyPassphrase = nil
|
|
// alert: cancel or try again
|
|
let alert = UIAlertController(title: "CannotShowPassword".localize(), message: error.localizedDescription, preferredStyle: UIAlertControllerStyle.alert)
|
|
alert.addAction(UIAlertAction(title: "Cancel".localize(), style: UIAlertActionStyle.default) { _ in
|
|
self.navigationController!.popViewController(animated: true)
|
|
})
|
|
alert.addAction(UIAlertAction(title: "TryAgain".localize(), style: UIAlertActionStyle.destructive) {_ in
|
|
self.decryptThenShowPassword()
|
|
})
|
|
self.present(alert, animated: true, completion: nil)
|
|
}
|
|
return
|
|
}
|
|
// display password
|
|
self.showPassword()
|
|
}
|
|
}
|
|
|
|
private func showPassword() {
|
|
DispatchQueue.main.async { [weak self] in
|
|
self?.setTableData()
|
|
self?.tableView.reloadData()
|
|
self?.editUIBarButtonItem.isEnabled = true
|
|
if !SharedDefaults[.isHidePasswordImagesOn] {
|
|
if let urlString = self?.password?.urlString {
|
|
if self?.passwordEntity?.getImage() == nil {
|
|
self?.updatePasswordImage(urlString: urlString)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func setupOneTimePasswordAutoRefresh() {
|
|
Timer.scheduledTimer(withTimeInterval: 1, repeats: true) {
|
|
[weak self] timer in
|
|
// bail out of the timer code if the object has been freed
|
|
guard let strongSelf = self,
|
|
let otpType = strongSelf.password?.otpType,
|
|
otpType != .none,
|
|
let indexPath = strongSelf.oneTimePasswordIndexPath,
|
|
let cell = strongSelf.tableView.cellForRow(at: indexPath) as? LabelTableViewCell else {
|
|
return
|
|
}
|
|
switch otpType {
|
|
case .totp:
|
|
if let (title, otp) = strongSelf.password?.getOtpStrings() {
|
|
strongSelf.tableData[indexPath.section].item[indexPath.row] = title => otp
|
|
cell.cellData?.title = title
|
|
cell.cellData?.content = otp
|
|
}
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
@objc private func pressEdit(_ sender: Any?) {
|
|
performSegue(withIdentifier: "editPasswordSegue", sender: self)
|
|
}
|
|
|
|
@objc private func setShouldPopCurrentView() {
|
|
self.shouldPopCurrentView = true
|
|
}
|
|
|
|
@IBAction private func cancelEditPassword(segue: UIStoryboardSegue) {
|
|
|
|
}
|
|
|
|
@IBAction private func saveEditPassword(segue: UIStoryboardSegue) {
|
|
if self.password!.changed != 0 {
|
|
SVProgressHUD.show(withStatus: "Saving".localize())
|
|
do {
|
|
self.passwordEntity = try self.passwordStore.edit(passwordEntity: self.passwordEntity!, password: self.password!)
|
|
} catch {
|
|
Utils.alert(title: "Error".localize(), message: error.localizedDescription, controller: self, completion: nil)
|
|
}
|
|
self.setTableData()
|
|
self.tableView.reloadData()
|
|
SVProgressHUD.showSuccess(withStatus: "Success".localize())
|
|
SVProgressHUD.dismiss(withDelay: 1)
|
|
}
|
|
}
|
|
|
|
@IBAction private func deletePassword(segue: UIStoryboardSegue) {
|
|
do {
|
|
try passwordStore.delete(passwordEntity: passwordEntity!)
|
|
} catch {
|
|
Utils.alert(title: "Error".localize(), message: error.localizedDescription, controller: self, completion: nil)
|
|
}
|
|
let _ = navigationController?.popViewController(animated: true)
|
|
}
|
|
|
|
private func setTableData() {
|
|
self.tableData = Array<TableSection>()
|
|
|
|
// name section
|
|
var section = TableSection(type: .name)
|
|
section.item.append(AdditionField())
|
|
tableData.append(section)
|
|
|
|
// main section
|
|
section = TableSection(type: .main)
|
|
let password = self.password!
|
|
if let username = password.username {
|
|
section.item.append(Constants.USERNAME_KEYWORD => username)
|
|
}
|
|
if let login = password.login {
|
|
section.item.append(Constants.LOGIN_KEYWORD => login)
|
|
}
|
|
section.item.append(Constants.PASSWORD_KEYWORD => password.password)
|
|
tableData.append(section)
|
|
|
|
|
|
// addition section
|
|
|
|
// show one time password
|
|
if password.otpType != .none {
|
|
if let (title, otp) = self.password?.getOtpStrings() {
|
|
section = TableSection(type: .addition, header: "OneTimePassword".localize())
|
|
section.item.append(title => otp)
|
|
tableData.append(section)
|
|
oneTimePasswordIndexPath = IndexPath(row: 0, section: tableData.count - 1)
|
|
}
|
|
}
|
|
|
|
// show additional information
|
|
let filteredAdditionKeys = password.getFilteredAdditions()
|
|
if filteredAdditionKeys.count > 0 {
|
|
section = TableSection(type: .addition, header: "Additions".localize())
|
|
section.item.append(contentsOf: filteredAdditionKeys)
|
|
tableData.append(section)
|
|
}
|
|
|
|
// misc section
|
|
section = TableSection(type: .misc)
|
|
section.item.append(AdditionField(title: "ShowRaw".localize()))
|
|
tableData.append(section)
|
|
|
|
}
|
|
|
|
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
|
|
if segue.identifier == "editPasswordSegue" {
|
|
if let controller = segue.destination as? UINavigationController {
|
|
if let editController = controller.viewControllers.first as? EditPasswordTableViewController {
|
|
editController.password = password
|
|
}
|
|
}
|
|
} else if segue.identifier == "showRawPasswordSegue" {
|
|
if let controller = segue.destination as? UINavigationController {
|
|
if let controller = controller.viewControllers.first as? RawPasswordViewController {
|
|
controller.password = password
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func updatePasswordImage(urlString: String) {
|
|
var newUrlString = urlString
|
|
if urlString.lowercased().hasPrefix("http://") {
|
|
// try to replace http url to https url
|
|
newUrlString = urlString.replacingOccurrences(of: "http://",
|
|
with: "https://",
|
|
options: .caseInsensitive,
|
|
range: urlString.range(of: "http://"))
|
|
} else if urlString.lowercased().hasPrefix("https://") {
|
|
// do nothing here
|
|
} else {
|
|
// if a url does not start with http or https, try to add https
|
|
newUrlString = "https://\(urlString)"
|
|
}
|
|
|
|
try? FavIcon.downloadPreferred(newUrlString) { [weak self] result in
|
|
if case let .success(image) = result {
|
|
let indexPath = IndexPath(row: 0, section: 0)
|
|
self?.passwordImage = image
|
|
self?.tableView.reloadRows(at: [indexPath], with: UITableViewRowAnimation.automatic)
|
|
let imageData = UIImageJPEGRepresentation(image, 1)
|
|
if let entity = self?.passwordEntity {
|
|
self?.passwordStore.updateImage(passwordEntity: entity, image: imageData)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@objc private func tapMenu(recognizer: UITapGestureRecognizer) {
|
|
if recognizer.state == UIGestureRecognizerState.ended {
|
|
let tapLocation = recognizer.location(in: self.tableView)
|
|
if let tapIndexPath = self.tableView.indexPathForRow(at: tapLocation) {
|
|
if let tappedCell = self.tableView.cellForRow(at: tapIndexPath) as? LabelTableViewCell {
|
|
tappedCell.becomeFirstResponder()
|
|
let menuController = UIMenuController.shared
|
|
let revealItem = UIMenuItem(title: "Reveal".localize(), action: #selector(LabelTableViewCell.revealPassword(_:)))
|
|
let concealItem = UIMenuItem(title: "Conceal".localize(), action: #selector(LabelTableViewCell.concealPassword(_:)))
|
|
let nextHOTPItem = UIMenuItem(title: "NextPassword".localize(), action: #selector(LabelTableViewCell.getNextHOTP(_:)))
|
|
let openURLItem = UIMenuItem(title: "CopyAndOpen".localize(), action: #selector(LabelTableViewCell.openLink(_:)))
|
|
menuController.menuItems = [revealItem, concealItem, nextHOTPItem, openURLItem]
|
|
menuController.setTargetRect(tappedCell.contentLabel.frame, in: tappedCell.contentLabel.superview!)
|
|
menuController.setMenuVisible(true, animated: true)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
|
|
if touch.view!.isKind(of: UIButton.classForCoder()) {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
@IBAction func back(segue:UIStoryboardSegue) {
|
|
}
|
|
|
|
func getNextHOTP() {
|
|
guard password != nil, passwordEntity != nil, password?.otpType == .hotp else {
|
|
DispatchQueue.main.async {
|
|
Utils.alert(title: "Error".localize(), message: "GetNextPasswordOfNonHotp.".localize(), controller: self, completion: nil)
|
|
}
|
|
return;
|
|
}
|
|
|
|
// copy HOTP to pasteboard (will update counter)
|
|
if let plainPassword = password!.getNextHotp() {
|
|
SecurePasteboard.shared.copy(textToCopy: plainPassword)
|
|
}
|
|
|
|
// commit the change of HOTP counter
|
|
if password!.changed != 0 {
|
|
do {
|
|
self.passwordEntity = try self.passwordStore.edit(passwordEntity: self.passwordEntity!, password: self.password!)
|
|
} catch {
|
|
Utils.alert(title: "Error".localize(), message: error.localizedDescription, controller: self, completion: nil)
|
|
}
|
|
SVProgressHUD.showSuccess(withStatus: "PasswordCopied".localize() | "CounterUpdated".localize())
|
|
SVProgressHUD.dismiss(withDelay: 1)
|
|
}
|
|
}
|
|
|
|
func openLink(to address: String?) {
|
|
guard address != nil, let url = URL(string: formActualWebAddress(from: address!)) else {
|
|
return DispatchQueue.main.async {
|
|
Utils.alert(title: "Error".localize(), message: "CannotFindValidUrl".localize(), controller: self, completion: nil)
|
|
}
|
|
}
|
|
SecurePasteboard.shared.copy(textToCopy: password?.password)
|
|
UIApplication.shared.open(url, options: [:], completionHandler: nil)
|
|
}
|
|
|
|
private func formActualWebAddress(from: String) -> String {
|
|
let lowercased = from.lowercased()
|
|
if !(lowercased.starts(with: "https://") || lowercased.starts(with: "http://")) {
|
|
return "https://\(from)"
|
|
}
|
|
return from
|
|
}
|
|
|
|
override func numberOfSections(in tableView: UITableView) -> Int {
|
|
return tableData.count
|
|
}
|
|
|
|
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
|
return tableData[section].item.count
|
|
}
|
|
|
|
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
|
let sectionIndex = indexPath.section
|
|
let rowIndex = indexPath.row
|
|
let tableDataItem = tableData[sectionIndex].item[rowIndex]
|
|
switch(tableData[sectionIndex].type) {
|
|
case .name:
|
|
let cell = tableView.dequeueReusableCell(withIdentifier: "passwordDetailTitleTableViewCell", for: indexPath) as! PasswordDetailTitleTableViewCell
|
|
if !SharedDefaults[.isHidePasswordImagesOn] {
|
|
cell.passwordImageImageView.image = passwordImage ?? #imageLiteral(resourceName: "PasswordImagePlaceHolder")
|
|
} else {
|
|
cell.passwordImageImageView.image = #imageLiteral(resourceName: "PasswordImagePlaceHolder")
|
|
}
|
|
let passwordName = passwordEntity!.getName()
|
|
if passwordEntity!.synced == false {
|
|
cell.nameLabel.text = "\(passwordName) ↻"
|
|
} else {
|
|
cell.nameLabel.text = passwordName
|
|
}
|
|
cell.categoryLabel.text = passwordEntity!.getCategoryText()
|
|
cell.selectionStyle = .none
|
|
return cell
|
|
case .main, .addition:
|
|
let cell = tableView.dequeueReusableCell(withIdentifier: "labelCell", for: indexPath) as! LabelTableViewCell
|
|
let titleData = tableDataItem.title
|
|
let contentData = tableDataItem.content
|
|
cell.delegatePasswordTableView = self
|
|
cell.cellData = LabelTableViewCellData(title: titleData, content: contentData)
|
|
cell.selectionStyle = .none
|
|
return cell
|
|
case .misc:
|
|
let cell = UITableViewCell(style: .value1, reuseIdentifier: nil)
|
|
cell.textLabel?.text = tableDataItem.title
|
|
cell.selectionStyle = .default
|
|
addHiddenFieldInformation(to: cell)
|
|
return cell
|
|
}
|
|
}
|
|
|
|
private func addHiddenFieldInformation(to cell: UITableViewCell) {
|
|
guard password != nil, let detailTextLabel = cell.detailTextLabel else {
|
|
return
|
|
}
|
|
|
|
var numberOfHiddenFields = 0
|
|
numberOfHiddenFields += SharedDefaults[.isHideUnknownOn] ? password!.numberOfUnknowns : 0
|
|
numberOfHiddenFields += SharedDefaults[.isHideOTPOn] ? password!.numberOfOtpRelated : 0
|
|
guard numberOfHiddenFields > 0 else {
|
|
return
|
|
}
|
|
|
|
detailTextLabel.textAlignment = .center
|
|
detailTextLabel.textColor = .gray
|
|
detailTextLabel.text = "HiddenFields(%d)".localize(numberOfHiddenFields)
|
|
}
|
|
|
|
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
|
|
return tableData[section].header
|
|
}
|
|
|
|
override func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
|
|
if section == tableData.count - 1 {
|
|
let view = UIView()
|
|
let footerLabel = UILabel(frame: CGRect(x: 15, y: 15, width: tableView.frame.width, height: 60))
|
|
footerLabel.numberOfLines = 0
|
|
footerLabel.font = UIFont.preferredFont(forTextStyle: .footnote)
|
|
footerLabel.textColor = UIColor.gray
|
|
let dateString = self.passwordStore.getLatestUpdateInfo(filename: password!.url.path)
|
|
footerLabel.text = "LastUpdated".localize(dateString)
|
|
view.addSubview(footerLabel)
|
|
return view
|
|
}
|
|
return nil
|
|
}
|
|
|
|
override func tableView(_ tableView: UITableView, performAction action: Selector, forRowAt indexPath: IndexPath, withSender sender: Any?) {
|
|
if action == #selector(copy(_:)) {
|
|
SecurePasteboard.shared.copy(textToCopy: tableData[indexPath.section].item[indexPath.row].content)
|
|
}
|
|
}
|
|
|
|
override func tableView(_ tableView: UITableView, canPerformAction action: Selector, forRowAt indexPath: IndexPath, withSender sender: Any?) -> Bool {
|
|
let section = tableData[indexPath.section]
|
|
switch(section.type) {
|
|
case .main, .addition:
|
|
return action == #selector(UIResponderStandardEditActions.copy(_:))
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
override func tableView(_ tableView: UITableView, shouldShowMenuForRowAt indexPath: IndexPath) -> Bool {
|
|
return true
|
|
}
|
|
|
|
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
|
let section = tableData[indexPath.section]
|
|
if section.type == .misc {
|
|
if section.item[indexPath.row].title == "ShowRaw".localize() {
|
|
performSegue(withIdentifier: "showRawPasswordSegue", sender: self)
|
|
}
|
|
}
|
|
tableView.deselectRow(at: indexPath, animated: true)
|
|
}
|
|
}
|