If the switch is on, users need to type passphrase of secret key once. The key will be stored in the Keychain. If the switch is off, users have to fill in passphrase for each decryption including show password detail and long press to copy.
321 lines
14 KiB
Swift
321 lines
14 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 SwiftyUserDefaults
|
|
import SVProgressHUD
|
|
|
|
class PasswordDetailTableViewController: UITableViewController, UIGestureRecognizerDelegate {
|
|
var passwordEntity: PasswordEntity?
|
|
var passwordCategoryEntities: [PasswordCategoryEntity]?
|
|
var passwordCategoryText = ""
|
|
var password: Password?
|
|
var passwordImage: UIImage?
|
|
|
|
let indicatorLable: UILabel = {
|
|
let label = UILabel(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: 21))
|
|
label.center = CGPoint(x: UIScreen.main.bounds.width / 2, y: UIScreen.main.bounds.height * 0.382 + 22)
|
|
label.backgroundColor = UIColor.clear
|
|
label.textColor = UIColor.gray
|
|
label.text = "decrypting password"
|
|
label.textAlignment = .center
|
|
label.font = UIFont.preferredFont(forTextStyle: .footnote)
|
|
return label
|
|
}()
|
|
|
|
let indicator: UIActivityIndicatorView = {
|
|
let indicator = UIActivityIndicatorView(activityIndicatorStyle: .gray)
|
|
indicator.center = CGPoint(x: UIScreen.main.bounds.width / 2, y: UIScreen.main.bounds.height * 0.382)
|
|
return indicator
|
|
}()
|
|
|
|
let editUIBarButtonItem = UIBarButtonItem(barButtonSystemItem: .edit, target: self, action: #selector(pressEdit(_:)))
|
|
|
|
|
|
struct TableCell {
|
|
var title: String
|
|
var content: String
|
|
init() {
|
|
title = ""
|
|
content = ""
|
|
}
|
|
|
|
init(title: String, content: String) {
|
|
self.title = title
|
|
self.content = content
|
|
}
|
|
}
|
|
|
|
struct TableSection {
|
|
var title: String
|
|
var item: Array<TableCell>
|
|
}
|
|
|
|
var tableData = Array<TableSection>()
|
|
|
|
override func viewDidLoad() {
|
|
super.viewDidLoad()
|
|
tableView.register(UINib(nibName: "LabelTableViewCell", bundle: nil), forCellReuseIdentifier: "labelCell")
|
|
tableView.register(UINib(nibName: "PasswordDetailTitleTableViewCell", bundle: nil), forCellReuseIdentifier: "passwordDetailTitleTableViewCell")
|
|
|
|
let passwordCategoryArray = passwordCategoryEntities?.map { $0.category! }
|
|
passwordCategoryText = (passwordCategoryArray?.joined(separator: " > "))!
|
|
|
|
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(PasswordDetailTableViewController.tapMenu(recognizer:)))
|
|
tableView.addGestureRecognizer(tapGesture)
|
|
tapGesture.delegate = self
|
|
|
|
tableView.contentInset = UIEdgeInsetsMake(-36, 0, 0, 0);
|
|
tableView.rowHeight = UITableViewAutomaticDimension
|
|
tableView.estimatedRowHeight = 52
|
|
|
|
|
|
indicator.startAnimating()
|
|
tableView.addSubview(indicator)
|
|
tableView.addSubview(indicatorLable)
|
|
editUIBarButtonItem.isEnabled = false
|
|
navigationItem.rightBarButtonItem = editUIBarButtonItem
|
|
|
|
if let imageData = passwordEntity?.image {
|
|
let image = UIImage(data: imageData as Data)
|
|
passwordImage = image
|
|
}
|
|
|
|
var passphrase = ""
|
|
if Defaults[.isRememberPassphraseOn] && PasswordStore.shared.pgpKeyPassphrase != nil {
|
|
passphrase = PasswordStore.shared.pgpKeyPassphrase!
|
|
self.decryptThenShowPassword(passphrase: passphrase)
|
|
} else {
|
|
let alert = UIAlertController(title: "Passphrase", message: "Please fill in the passphrase of your PGP secret key.", preferredStyle: UIAlertControllerStyle.alert)
|
|
alert.addAction(UIAlertAction(title: "OK", style: UIAlertActionStyle.default, handler: {_ in
|
|
passphrase = alert.textFields!.first!.text!
|
|
self.decryptThenShowPassword(passphrase: passphrase)
|
|
}))
|
|
alert.addTextField(configurationHandler: {(textField: UITextField!) in
|
|
textField.text = ""
|
|
textField.isSecureTextEntry = true
|
|
})
|
|
self.present(alert, animated: true, completion: nil)
|
|
}
|
|
|
|
|
|
}
|
|
|
|
func decryptThenShowPassword(passphrase: String) {
|
|
if Defaults[.isRememberPassphraseOn] {
|
|
PasswordStore.shared.pgpKeyPassphrase = passphrase
|
|
}
|
|
DispatchQueue.global(qos: .userInitiated).async {
|
|
do {
|
|
self.password = try self.passwordEntity!.decrypt(passphrase: passphrase)!
|
|
} catch {
|
|
DispatchQueue.main.async {
|
|
let alert = UIAlertController(title: "Cannot Show Password", message: error.localizedDescription, preferredStyle: UIAlertControllerStyle.alert)
|
|
alert.addAction(UIAlertAction(title: "OK", style: UIAlertActionStyle.default, handler: {(UIAlertAction) -> Void in
|
|
self.navigationController!.popViewController(animated: true)
|
|
}))
|
|
self.present(alert, animated: true, completion: nil)
|
|
}
|
|
return
|
|
}
|
|
|
|
let password = self.password!
|
|
DispatchQueue.main.async { [weak self] in
|
|
self?.showPassword(password: password)
|
|
}
|
|
}
|
|
}
|
|
|
|
func showPassword(password: Password) {
|
|
setTableData()
|
|
self.tableView.reloadData()
|
|
indicator.stopAnimating()
|
|
indicatorLable.isHidden = true
|
|
editUIBarButtonItem.isEnabled = true
|
|
if let url = password.getURL() {
|
|
if self.passwordEntity?.image == nil{
|
|
self.updatePasswordImage(url: url)
|
|
}
|
|
}
|
|
}
|
|
|
|
func pressEdit(_ sender: Any?) {
|
|
performSegue(withIdentifier: "editPasswordSegue", sender: self)
|
|
}
|
|
|
|
@IBAction func cancelEditPassword(segue: UIStoryboardSegue) {
|
|
|
|
}
|
|
|
|
@IBAction func saveEditPassword(segue: UIStoryboardSegue) {
|
|
if self.password!.changed {
|
|
SVProgressHUD.show(withStatus: "Saving")
|
|
DispatchQueue.global(qos: .userInitiated).async {
|
|
PasswordStore.shared.update(passwordEntity: self.passwordEntity!, password: self.password!, progressBlock: { progress in
|
|
DispatchQueue.main.async {
|
|
SVProgressHUD.showProgress(progress, status: "Encrypting")
|
|
}
|
|
})
|
|
DispatchQueue.main.async {
|
|
self.passwordEntity!.synced = false
|
|
PasswordStore.shared.saveUpdated(passwordEntity: self.passwordEntity!)
|
|
NotificationCenter.default.post(Notification(name: Notification.Name("passwordUpdated")))
|
|
self.setTableData()
|
|
self.tableView.reloadData()
|
|
SVProgressHUD.showSuccess(withStatus: "Success")
|
|
SVProgressHUD.dismiss(withDelay: 1)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func setTableData() {
|
|
self.tableData = Array<TableSection>()
|
|
tableData.append(TableSection(title: "", item: []))
|
|
tableData[0].item.append(TableCell())
|
|
var tableDataIndex = 1
|
|
self.tableData.append(TableSection(title: "", item: []))
|
|
let password = self.password!
|
|
if let username = password.getUsername() {
|
|
self.tableData[tableDataIndex].item.append(TableCell(title: "username", content: username))
|
|
}
|
|
self.tableData[tableDataIndex].item.append(TableCell(title: "password", content: password.password))
|
|
if password.additions.count > 0 {
|
|
self.tableData.append(TableSection(title: "additions", item: []))
|
|
tableDataIndex += 1
|
|
for additionKey in password.additionKeys {
|
|
if (!additionKey.hasPrefix("unknown") || !Defaults[.isHideUnknownOn]) &&
|
|
additionKey.lowercased() != "username" &&
|
|
additionKey.lowercased() != "password" {
|
|
self.tableData[tableDataIndex].item.append(TableCell(title: additionKey, content: password.additions[additionKey]!))
|
|
|
|
}
|
|
|
|
}
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func updatePasswordImage(url: String) {
|
|
do {
|
|
try FavIcon.downloadPreferred(url) { [weak self] result in
|
|
switch result {
|
|
case .success(let image):
|
|
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 {
|
|
PasswordStore.shared.updateImage(passwordEntity: entity, image: imageData)
|
|
}
|
|
case .failure(let error):
|
|
print(error)
|
|
}
|
|
}
|
|
} catch {
|
|
print(error)
|
|
}
|
|
}
|
|
|
|
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", action: #selector(LabelTableViewCell.revealPassword(_:)))
|
|
let concealItem = UIMenuItem(title: "Conceal", action: #selector(LabelTableViewCell.concealPassword(_:)))
|
|
let openURLItem = UIMenuItem(title: "Copy Password & Open Link", action: #selector(LabelTableViewCell.openLink(_:)))
|
|
menuController.menuItems = [revealItem, concealItem, openURLItem]
|
|
menuController.setTargetRect(tappedCell.contentLabel.frame, in: tappedCell.contentLabel.superview!)
|
|
menuController.setMenuVisible(true, animated: true)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
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
|
|
|
|
if sectionIndex == 0 && rowIndex == 0 {
|
|
let cell = tableView.dequeueReusableCell(withIdentifier: "passwordDetailTitleTableViewCell", for: indexPath) as! PasswordDetailTitleTableViewCell
|
|
cell.passwordImageImageView.image = passwordImage ?? #imageLiteral(resourceName: "PasswordImagePlaceHolder")
|
|
var passwordName = passwordEntity!.name!
|
|
if passwordEntity!.synced == false {
|
|
passwordName = "\(passwordName) ↻"
|
|
}
|
|
cell.nameLabel.text = passwordName
|
|
cell.categoryLabel.text = passwordCategoryText
|
|
return cell
|
|
} else {
|
|
let cell = tableView.dequeueReusableCell(withIdentifier: "labelCell", for: indexPath) as! LabelTableViewCell
|
|
let titleData = tableData[sectionIndex].item[rowIndex].title
|
|
let contentData = tableData[sectionIndex].item[rowIndex].content
|
|
cell.password = password
|
|
cell.isPasswordCell = (titleData.lowercased() == "password" ? true : false)
|
|
cell.isURLCell = (titleData.lowercased() == "url" ? true : false)
|
|
cell.cellData = LabelTableViewCellData(title: titleData, content: contentData)
|
|
return cell
|
|
}
|
|
}
|
|
|
|
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
|
|
return tableData[section].title
|
|
}
|
|
|
|
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 = PasswordStore.shared.getLatestCommitDate(filename: (passwordEntity?.rawPath)!)
|
|
footerLabel.text = "Last Updated: \(dateString ?? "Unknown")"
|
|
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(_:)) {
|
|
Utils.copyToPasteboard(textToCopy: tableData[indexPath.section].item[indexPath.row].content)
|
|
}
|
|
}
|
|
|
|
override func tableView(_ tableView: UITableView, canPerformAction action: Selector, forRowAt indexPath: IndexPath, withSender sender: Any?) -> Bool {
|
|
return action == #selector(UIResponderStandardEditActions.copy(_:))
|
|
}
|
|
|
|
override func tableView(_ tableView: UITableView, shouldShowMenuForRowAt indexPath: IndexPath) -> Bool {
|
|
return true
|
|
}
|
|
|
|
}
|