diff --git a/pass/Controllers/PasswordDetailTableViewController.swift b/pass/Controllers/PasswordDetailTableViewController.swift index 47515ef..3237263 100644 --- a/pass/Controllers/PasswordDetailTableViewController.swift +++ b/pass/Controllers/PasswordDetailTableViewController.swift @@ -117,7 +117,7 @@ class PasswordDetailTableViewController: UITableViewController, UIGestureRecogni self.present(alert, animated: true, completion: nil) } - self.setupUdateOneTimePassword() + self.setupUpdateOneTimePassword() } @@ -159,7 +159,7 @@ class PasswordDetailTableViewController: UITableViewController, UIGestureRecogni } } - func setupUdateOneTimePassword() { + func setupUpdateOneTimePassword() { Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] timer in // bail out of the timer code if the object has been freed @@ -236,7 +236,12 @@ class PasswordDetailTableViewController: UITableViewController, UIGestureRecogni switch token.generator.factor { case .counter(_): // counter-based one time password - break + self.tableData.append(TableSection(title: "One time password", item: [])) + tableDataIndex += 1 + oneTimePasswordIndexPath = IndexPath(row: 0, section: tableDataIndex) + if let crtPassword = password.otpToken?.currentPassword { + self.tableData[tableDataIndex].item.append(TableCell(title: "HMAC-based", content: crtPassword)) + } case .timer(let period): // time-based one time password self.tableData.append(TableSection(title: "One time password", item: [])) @@ -306,8 +311,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 openURLItem = UIMenuItem(title: "Copy Password & Open Link", action: #selector(LabelTableViewCell.openLink(_:))) - menuController.menuItems = [revealItem, concealItem, openURLItem] + menuController.menuItems = [revealItem, concealItem, nextPasswordItem, openURLItem] menuController.setTargetRect(tappedCell.contentLabel.frame, in: tappedCell.contentLabel.superview!) menuController.setMenuVisible(true, animated: true) } @@ -342,9 +348,10 @@ 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.password = password + cell.passwordTableView = self cell.isPasswordCell = (titleData.lowercased() == "password" ? true : false) cell.isURLCell = (titleData.lowercased() == "url" ? true : false) + cell.isHOTPCell = (titleData == "HMAC-based" ? true : false) cell.cellData = LabelTableViewCellData(title: titleData, content: contentData) return cell } diff --git a/pass/Models/Password.swift b/pass/Models/Password.swift index f9ae934..52b74ec 100644 --- a/pass/Models/Password.swift +++ b/pass/Models/Password.swift @@ -143,15 +143,15 @@ class Password { Example of TOTP fields otp_secret: secretsecretsecretsecretsecretsecret otp_type: totp - otp_algorithm: sha1 - otp_period: 30 - otp_digits: 6 + otp_algorithm: sha1 (default: sha1, optional) + otp_period: 30 (default: 30, optional) + otp_digits: 6 (default: 6, optional) Example of HOTP fields otp_secret: secretsecretsecretsecretsecretsecret otp_type: hotp otp_counter: 1 - otp_digits: 6 + otp_digits: 6 (default: 6, optional) */ func updateOtpToken() { @@ -166,16 +166,14 @@ class Password { // get type guard let type = getAdditionValue(withKey: "otp_type")?.lowercased(), (type == "totp" || type == "hotp") else { - // print("Missing / Invalid otp type") - return + // print("Missing / Invalid otp type") + return } - // get algorithm + // get algorithm (optional) var algorithm = Generator.Algorithm.sha1 if let algoString = getAdditionValue(withKey: "otp_algorithm") { switch algoString.lowercased() { - case "sha1": - algorithm = Generator.Algorithm.sha1 case "sha256": algorithm = Generator.Algorithm.sha256 case "sha512": @@ -187,20 +185,58 @@ class Password { // construct the token if type == "totp" { - if let digits = Int(getAdditionValue(withKey: "otp_digits") ?? ""), - let period = Double(getAdditionValue(withKey: "otp_period") ?? "") { - guard let generator = Generator( - factor: .timer(period: period), - secret: secretData, - algorithm: algorithm, - digits: digits) else { - print("Invalid generator parameters \(self.plainText)") - return - } - self.otpToken = Token(name: self.name, issuer: "", generator: generator) + // HOTP + // default: 6 digits, 30 seconds + guard let digits = Int(getAdditionValue(withKey: "otp_digits") ?? "6"), + let period = Double(getAdditionValue(withKey: "otp_period") ?? "30.0") else { + let alertMessage = "Invalid otp_digits or otp_period." + print(alertMessage) + return } + guard let generator = Generator( + factor: .timer(period: period), + secret: secretData, + algorithm: algorithm, + digits: digits) else { + let alertMessage = "Invalid OTP generator parameters." + print(alertMessage) + return + } + self.otpToken = Token(name: self.name, issuer: "", generator: generator) } else { - print("We do not support HOTP currently.") + // HOTP + // default: 6 digits + guard let digits = Int(getAdditionValue(withKey: "otp_digits") ?? "6"), + let counter = UInt64(getAdditionValue(withKey: "otp_counter") ?? "") else { + let alertMessage = "Invalid otp_digits or otp_counter." + print(alertMessage) + return + } + guard let generator = Generator( + factor: .counter(counter), + secret: secretData, + algorithm: algorithm, + digits: digits) else { + let alertMessage = "Invalid OTP generator parameters." + print(alertMessage) + return + } + self.otpToken = Token(name: self.name, issuer: "", generator: generator) } } + + // it is guaranteed that it is a HOTP password when we call this + func increaseHotpCounter() { + var lines : [String] = [] + self.plainText.enumerateLines() { line, _ in + let (key, value) = Password.getKeyValuePair(from: line) + if key == "otp_counter", let newValue = UInt64(value)?.advanced(by: 1) { + let newLine = "\(key!): \(newValue)" + lines.append(newLine) + } else { + lines.append(line) + } + } + self.updatePassword(name: self.name, plainText: lines.joined(separator: "\n")) + } } diff --git a/pass/Views/LabelTableViewCell.swift b/pass/Views/LabelTableViewCell.swift index 0bf0601..e01cd18 100644 --- a/pass/Views/LabelTableViewCell.swift +++ b/pass/Views/LabelTableViewCell.swift @@ -7,6 +7,7 @@ // import UIKit +import SVProgressHUD struct LabelTableViewCellData { @@ -22,15 +23,27 @@ class LabelTableViewCell: UITableViewCell { var isPasswordCell = false var isURLCell = false var isReveal = false - var password: Password? + var isHOTPCell = false let passwordDots = "••••••••••••" + weak var passwordTableView : PasswordDetailTableViewController? + var cellData: LabelTableViewCellData? { didSet { - titleLabel.text = cellData?.title + titleLabel.text = cellData?.title ?? "" if isPasswordCell { - contentLabel.text = passwordDots + if isReveal { + contentLabel.attributedText = Utils.attributedPassword(plainPassword: cellData?.content ?? "") + } else { + contentLabel.text = passwordDots + } contentLabel.font = UIFont(name: "Menlo", size: contentLabel.font.pointSize) + } else if isHOTPCell { + if isReveal { + contentLabel.text = cellData?.content ?? "" + } else { + contentLabel.text = passwordDots + } } else { contentLabel.text = cellData?.content } @@ -62,6 +75,13 @@ class LabelTableViewCell: UITableViewCell { if isURLCell { return action == #selector(copy(_:)) || action == #selector(LabelTableViewCell.openLink(_:)) } + if isHOTPCell { + if isReveal { + return action == #selector(copy(_:)) || action == #selector(LabelTableViewCell.concealPassword(_:)) || action == #selector(LabelTableViewCell.nextPassword(_:)) + } else { + return action == #selector(copy(_:)) || action == #selector(LabelTableViewCell.revealPassword(_:)) || action == #selector(LabelTableViewCell.nextPassword(_:)) + } + } return action == #selector(copy(_:)) } @@ -71,7 +91,11 @@ class LabelTableViewCell: UITableViewCell { func revealPassword(_ sender: Any?) { if let plainPassword = cellData?.content { - contentLabel.attributedText = Utils.attributedPassword(plainPassword: plainPassword) + if isHOTPCell { + contentLabel.text = plainPassword + } else { + contentLabel.attributedText = Utils.attributedPassword(plainPassword: plainPassword) + } } else { contentLabel.text = "" } @@ -83,8 +107,45 @@ class LabelTableViewCell: UITableViewCell { 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 { + PasswordStore.shared.update(passwordEntity: passwordEntity, password: password, progressBlock: {_ in }) + DispatchQueue.main.async { + passwordEntity.synced = false + PasswordStore.shared.saveUpdated(passwordEntity: passwordEntity) + NotificationCenter.default.post(Notification(name: Notification.Name("passwordUpdated"))) + // 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?) { - Utils.copyToPasteboard(textToCopy: password?.password) + 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) } }