diff --git a/pass/Controllers/PasswordDetailTableViewController.swift b/pass/Controllers/PasswordDetailTableViewController.swift index 2273e24..13e87f3 100644 --- a/pass/Controllers/PasswordDetailTableViewController.swift +++ b/pass/Controllers/PasswordDetailTableViewController.swift @@ -13,26 +13,25 @@ import SVProgressHUD class PasswordDetailTableViewController: UITableViewController, UIGestureRecognizerDelegate { var passwordEntity: PasswordEntity? - var passwordCategoryText = "" - var password: Password? - var passwordImage: UIImage? - var oneTimePasswordIndexPath : IndexPath? - var shouldPopCurrentView = false - let passwordStore = PasswordStore.shared + private var password: Password? + private var passwordCategoryText = "" + private var passwordImage: UIImage? + private var oneTimePasswordIndexPath : IndexPath? + private var shouldPopCurrentView = false + private let passwordStore = PasswordStore.shared - let indicator: UIActivityIndicatorView = { + private 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 }() - lazy var editUIBarButtonItem: UIBarButtonItem = { + private lazy var editUIBarButtonItem: UIBarButtonItem = { let uiBarButtonItem = UIBarButtonItem(barButtonSystemItem: .edit, target: self, action: #selector(pressEdit(_:))) return uiBarButtonItem }() - - struct TableCell { + private struct TableCell { var title: String var content: String init() { @@ -46,12 +45,12 @@ class PasswordDetailTableViewController: UITableViewController, UIGestureRecogni } } - struct TableSection { + private struct TableSection { var title: String var item: Array } - var tableData = Array() + private var tableData = Array() override func viewDidLoad() { super.viewDidLoad() @@ -67,7 +66,6 @@ class PasswordDetailTableViewController: UITableViewController, UIGestureRecogni tableView.contentInset = UIEdgeInsetsMake(-36, 0, 0, 0); tableView.rowHeight = UITableViewAutomaticDimension tableView.estimatedRowHeight = 52 - indicator.startAnimating() tableView.addSubview(indicator) @@ -96,12 +94,24 @@ class PasswordDetailTableViewController: UITableViewController, UIGestureRecogni self.present(alert, animated: true, completion: nil) } - self.setupUpdateOneTimePassword() - self.addNotificationObservers() - + self.setupOneTimePasswordAutoRefresh() + + NotificationCenter.default.addObserver(self, selector: #selector(setShouldPopCurrentView), name: .passwordStoreChangeDiscarded, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(showPassword), name: .passwordStoreUpdated, object: nil) } - func decryptThenShowPassword(passphrase: String) { + override func viewDidAppear(_ animated: Bool) { + super.viewWillAppear(animated) + if self.shouldPopCurrentView { + let alert = UIAlertController(title: "Notice", message: "All previous local changes have been discarded. Your current Password Store will be shown.", preferredStyle: UIAlertControllerStyle.alert) + alert.addAction(UIAlertAction(title: "OK", style: UIAlertActionStyle.default, handler: {_ in + _ = self.navigationController?.popViewController(animated: true) + })) + self.present(alert, animated: true, completion: nil) + } + } + + private func decryptThenShowPassword(passphrase: String) { if Defaults[.isRememberPassphraseOn] { self.passwordStore.pgpKeyPassphrase = passphrase } @@ -124,7 +134,7 @@ class PasswordDetailTableViewController: UITableViewController, UIGestureRecogni } } - func showPassword() { + @objc private func showPassword() { DispatchQueue.main.async { [weak self] in self?.indicator.stopAnimating() self?.setTableData() @@ -135,14 +145,14 @@ class PasswordDetailTableViewController: UITableViewController, UIGestureRecogni } self?.editUIBarButtonItem.isEnabled = true if let urlString = self?.password?.getURLString() { - if self?.passwordEntity?.image == nil{ + if self?.passwordEntity?.image == nil { self?.updatePasswordImage(urlString: urlString) } } } } - func setupUpdateOneTimePassword() { + 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 @@ -167,16 +177,19 @@ class PasswordDetailTableViewController: UITableViewController, UIGestureRecogni } } - func pressEdit(_ sender: Any?) { - print("pressEdit") + @objc private func pressEdit(_ sender: Any?) { performSegue(withIdentifier: "editPasswordSegue", sender: self) } - @IBAction func cancelEditPassword(segue: UIStoryboardSegue) { + @objc private func setShouldPopCurrentView() { + self.shouldPopCurrentView = true + } + + @IBAction private func cancelEditPassword(segue: UIStoryboardSegue) { } - @IBAction func saveEditPassword(segue: UIStoryboardSegue) { + @IBAction private func saveEditPassword(segue: UIStoryboardSegue) { if self.password!.changed { SVProgressHUD.show(withStatus: "Saving") DispatchQueue.global(qos: .userInitiated).async { @@ -197,7 +210,7 @@ class PasswordDetailTableViewController: UITableViewController, UIGestureRecogni } } - func setTableData() { + private func setTableData() { self.tableData = Array() tableData.append(TableSection(title: "", item: [])) tableData[0].item.append(TableCell()) @@ -259,7 +272,7 @@ class PasswordDetailTableViewController: UITableViewController, UIGestureRecogni } } - func updatePasswordImage(urlString: String) { + private func updatePasswordImage(urlString: String) { var newUrlString = urlString if urlString.lowercased().hasPrefix("http://") { // try to replace http url to https url @@ -298,7 +311,7 @@ class PasswordDetailTableViewController: UITableViewController, UIGestureRecogni } } - func tapMenu(recognizer: UITapGestureRecognizer) { + @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) { @@ -307,9 +320,9 @@ class PasswordDetailTableViewController: UITableViewController, UIGestureRecogni let menuController = UIMenuController.shared let revealItem = UIMenuItem(title: "Reveal", action: #selector(LabelTableViewCell.revealPassword(_:))) let concealItem = UIMenuItem(title: "Conceal", action: #selector(LabelTableViewCell.concealPassword(_:))) - let nextPasswordItem = UIMenuItem(title: "Next Password", action: #selector(LabelTableViewCell.nextPassword(_:))) + let nextHOTPItem = UIMenuItem(title: "Next Password", action: #selector(LabelTableViewCell.getNextHOTP(_:))) let openURLItem = UIMenuItem(title: "Copy Password & Open Link", action: #selector(LabelTableViewCell.openLink(_:))) - menuController.menuItems = [revealItem, concealItem, nextPasswordItem, openURLItem] + menuController.menuItems = [revealItem, concealItem, nextHOTPItem, openURLItem] menuController.setTargetRect(tappedCell.contentLabel.frame, in: tappedCell.contentLabel.superview!) menuController.setMenuVisible(true, animated: true) } @@ -317,6 +330,46 @@ class PasswordDetailTableViewController: UITableViewController, UIGestureRecogni } } + func getNextHOTP() { + guard password != nil, passwordEntity != nil, password?.otpType == .hotp else { + DispatchQueue.main.async { + Utils.alert(title: "Error", message: "Get next password of a non-HOTP entry.", controller: self, completion: nil) + } + return; + } + + // increase HOTP counter + password!.increaseHotpCounter() + + // copy HOTP to pasteboard + if let plainPassword = password!.otpToken?.currentPassword { + Utils.copyToPasteboard(textToCopy: plainPassword) + } + + // commit the change of HOTP counter + if password!.changed { + DispatchQueue.global(qos: .userInitiated).async { + self.passwordStore.update(passwordEntity: self.passwordEntity!, password: self.password!, progressBlock: {_ in }) + DispatchQueue.main.async { + self.passwordEntity!.synced = false + self.passwordStore.saveUpdated(passwordEntity: self.passwordEntity!) + SVProgressHUD.showSuccess(withStatus: "Password Copied\nCounter Updated") + SVProgressHUD.dismiss(withDelay: 1) + } + } + } + } + + func openLink() { + guard let urlString = self.password?.getURLString(), let url = URL(string: urlString) else { + DispatchQueue.main.async { + Utils.alert(title: "Error", message: "Cannot find a valid URL", controller: self, completion: nil) + } + return; + } + Utils.copyToPasteboard(textToCopy: password?.password) + UIApplication.shared.open(url, options: [:], completionHandler: nil) + } override func numberOfSections(in tableView: UITableView) -> Int { return tableData.count @@ -344,7 +397,7 @@ class PasswordDetailTableViewController: UITableViewController, UIGestureRecogni 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.passwordTableView = self + cell.delegatePasswordTableView = self cell.isPasswordCell = (titleData.lowercased() == "password" ? true : false) cell.isURLCell = (titleData.lowercased() == "url" ? true : false) cell.isHOTPCell = (titleData == "HMAC-based" ? true : false) @@ -385,24 +438,4 @@ class PasswordDetailTableViewController: UITableViewController, UIGestureRecogni override func tableView(_ tableView: UITableView, shouldShowMenuForRowAt indexPath: IndexPath) -> Bool { return true } - - private func addNotificationObservers() { - NotificationCenter.default.addObserver(self, selector: #selector(setShouldPopCurrentView), name: .passwordStoreChangeDiscarded, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(showPassword), name: .passwordStoreUpdated, object: nil) - } - - func setShouldPopCurrentView() { - self.shouldPopCurrentView = true - } - - override func viewDidAppear(_ animated: Bool) { - super.viewWillAppear(animated) - if self.shouldPopCurrentView { - let alert = UIAlertController(title: "Notice", message: "All previous local changes have been discarded. Your current Password Store will be shown.", preferredStyle: UIAlertControllerStyle.alert) - alert.addAction(UIAlertAction(title: "OK", style: UIAlertActionStyle.default, handler: {_ in - _ = self.navigationController?.popViewController(animated: true) - })) - self.present(alert, animated: true, completion: nil) - } - } } diff --git a/pass/Helpers/Globals.swift b/pass/Helpers/Globals.swift index 0621464..3959550 100644 --- a/pass/Helpers/Globals.swift +++ b/pass/Helpers/Globals.swift @@ -28,6 +28,9 @@ class Globals { static let passwordMaximumLength = 24 static let passwordDefaultLength = 16 + static let passwordDots = "••••••••••••" + static let passwordFonts = "Menlo" + private init() { } } diff --git a/pass/Models/Password.swift b/pass/Models/Password.swift index 25d20bb..70334f7 100644 --- a/pass/Models/Password.swift +++ b/pass/Models/Password.swift @@ -28,6 +28,24 @@ class Password { var firstLineIsOTPField = false var otpToken: Token? + enum OtpType { + case totp, hotp, none + } + + var otpType: OtpType { + get { + guard let token = self.otpToken else { + return OtpType.none + } + switch token.generator.factor { + case .counter: + return OtpType.hotp + case .timer: + return OtpType.totp + } + } + } + init(name: String, plainText: String) { self.initEverything(name: name, plainText: plainText) } diff --git a/pass/Views/LabelTableViewCell.swift b/pass/Views/LabelTableViewCell.swift index 242ca3b..726b535 100644 --- a/pass/Views/LabelTableViewCell.swift +++ b/pass/Views/LabelTableViewCell.swift @@ -19,15 +19,13 @@ class LabelTableViewCell: UITableViewCell { @IBOutlet weak var contentLabel: UILabel! @IBOutlet weak var titleLabel: UILabel! - let passwordStore = PasswordStore.shared var isPasswordCell = false var isURLCell = false var isReveal = false var isHOTPCell = false - let passwordDots = "••••••••••••" - weak var passwordTableView : PasswordDetailTableViewController? + weak var delegatePasswordTableView : PasswordDetailTableViewController? var cellData: LabelTableViewCellData? { didSet { @@ -36,14 +34,14 @@ class LabelTableViewCell: UITableViewCell { if isReveal { contentLabel.attributedText = Utils.attributedPassword(plainPassword: cellData?.content ?? "") } else { - contentLabel.text = passwordDots + contentLabel.text = Globals.passwordDots } - contentLabel.font = UIFont(name: "Menlo", size: contentLabel.font.pointSize) + contentLabel.font = UIFont(name: Globals.passwordFonts, size: contentLabel.font.pointSize) } else if isHOTPCell { if isReveal { contentLabel.text = cellData?.content ?? "" } else { - contentLabel.text = passwordDots + contentLabel.text = Globals.passwordDots } } else { contentLabel.text = cellData?.content @@ -78,9 +76,9 @@ class LabelTableViewCell: UITableViewCell { } if isHOTPCell { if isReveal { - return action == #selector(copy(_:)) || action == #selector(LabelTableViewCell.concealPassword(_:)) || action == #selector(LabelTableViewCell.nextPassword(_:)) + return action == #selector(copy(_:)) || action == #selector(LabelTableViewCell.concealPassword(_:)) || action == #selector(LabelTableViewCell.getNextHOTP(_:)) } else { - return action == #selector(copy(_:)) || action == #selector(LabelTableViewCell.revealPassword(_:)) || action == #selector(LabelTableViewCell.nextPassword(_:)) + return action == #selector(copy(_:)) || action == #selector(LabelTableViewCell.revealPassword(_:)) || action == #selector(LabelTableViewCell.getNextHOTP(_:)) } } return action == #selector(copy(_:)) @@ -104,48 +102,17 @@ class LabelTableViewCell: UITableViewCell { } func concealPassword(_ sender: Any?) { - contentLabel.text = passwordDots + contentLabel.text = Globals.passwordDots isReveal = false } - func nextPassword(_ sender: Any?) { - guard let password = passwordTableView?.password, - let passwordEntity = passwordTableView?.passwordEntity else { - print("Cannot find password/passwordEntity of a cell") - return; - } - - // increase HOTP counter - password.increaseHotpCounter() - - // only the HOTP password needs update - if let plainPassword = password.otpToken?.currentPassword { - cellData?.content = plainPassword - // contentLabel will be updated automatically - } - - // commit - if password.changed { - DispatchQueue.global(qos: .userInitiated).async { - self.passwordStore.update(passwordEntity: passwordEntity, password: password, progressBlock: {_ in }) - DispatchQueue.main.async { - passwordEntity.synced = false - self.passwordStore.saveUpdated(passwordEntity: passwordEntity) - // reload so that the "unsynced" symbol could be added - self.passwordTableView?.tableView.reloadRows(at: [IndexPath(row: 0, section: 0)], with: UITableViewRowAnimation.automatic) - SVProgressHUD.showSuccess(withStatus: "Password Copied\nCounter Updated") - SVProgressHUD.dismiss(withDelay: 1) - } - } - } + func openLink(_ sender: Any?) { + // if isURLCell, passwordTableView should not be nil + delegatePasswordTableView!.openLink() } - func openLink(_ sender: Any?) { - guard let password = passwordTableView?.password else { - print("Cannot find password of a cell") - return; - } - Utils.copyToPasteboard(textToCopy: password.password) - UIApplication.shared.open(URL(string: cellData!.content)!, options: [:], completionHandler: nil) + func getNextHOTP(_ sender: Any?) { + // if isHOTPCell, passwordTableView should not be nil + delegatePasswordTableView!.getNextHOTP() } }