Update OTP token generation and support HOTP

- Update logic: Only fields otp_secret and otp_type are required for TOTP. Only fields otp_secret, otp_type and otp_counter are required for HOTP. Other fields (i.e., otp_algorithm, otp_digits, otp_period) are optional.
- Support HOTP: (1) passwords are initially concealed; (2) "tap->next" generates a new password and commits the updated password file automatically
This commit is contained in:
Yishi Lin 2017-03-07 00:52:22 +08:00
parent 9580978434
commit 0dccd911fd
3 changed files with 135 additions and 31 deletions

View file

@ -117,7 +117,7 @@ class PasswordDetailTableViewController: UITableViewController, UIGestureRecogni
self.present(alert, animated: true, completion: nil) 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) { Timer.scheduledTimer(withTimeInterval: 1, repeats: true) {
[weak self] timer in [weak self] timer in
// bail out of the timer code if the object has been freed // bail out of the timer code if the object has been freed
@ -236,7 +236,12 @@ class PasswordDetailTableViewController: UITableViewController, UIGestureRecogni
switch token.generator.factor { switch token.generator.factor {
case .counter(_): case .counter(_):
// counter-based one time password // 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): case .timer(let period):
// time-based one time password // time-based one time password
self.tableData.append(TableSection(title: "One time password", item: [])) self.tableData.append(TableSection(title: "One time password", item: []))
@ -306,8 +311,9 @@ class PasswordDetailTableViewController: UITableViewController, UIGestureRecogni
let menuController = UIMenuController.shared let menuController = UIMenuController.shared
let revealItem = UIMenuItem(title: "Reveal", action: #selector(LabelTableViewCell.revealPassword(_:))) let revealItem = UIMenuItem(title: "Reveal", action: #selector(LabelTableViewCell.revealPassword(_:)))
let concealItem = UIMenuItem(title: "Conceal", action: #selector(LabelTableViewCell.concealPassword(_:))) 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(_:))) 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.setTargetRect(tappedCell.contentLabel.frame, in: tappedCell.contentLabel.superview!)
menuController.setMenuVisible(true, animated: true) menuController.setMenuVisible(true, animated: true)
} }
@ -342,9 +348,10 @@ class PasswordDetailTableViewController: UITableViewController, UIGestureRecogni
let cell = tableView.dequeueReusableCell(withIdentifier: "labelCell", for: indexPath) as! LabelTableViewCell let cell = tableView.dequeueReusableCell(withIdentifier: "labelCell", for: indexPath) as! LabelTableViewCell
let titleData = tableData[sectionIndex].item[rowIndex].title let titleData = tableData[sectionIndex].item[rowIndex].title
let contentData = tableData[sectionIndex].item[rowIndex].content let contentData = tableData[sectionIndex].item[rowIndex].content
cell.password = password cell.passwordTableView = self
cell.isPasswordCell = (titleData.lowercased() == "password" ? true : false) cell.isPasswordCell = (titleData.lowercased() == "password" ? true : false)
cell.isURLCell = (titleData.lowercased() == "url" ? true : false) cell.isURLCell = (titleData.lowercased() == "url" ? true : false)
cell.isHOTPCell = (titleData == "HMAC-based" ? true : false)
cell.cellData = LabelTableViewCellData(title: titleData, content: contentData) cell.cellData = LabelTableViewCellData(title: titleData, content: contentData)
return cell return cell
} }

View file

@ -143,15 +143,15 @@ class Password {
Example of TOTP fields Example of TOTP fields
otp_secret: secretsecretsecretsecretsecretsecret otp_secret: secretsecretsecretsecretsecretsecret
otp_type: totp otp_type: totp
otp_algorithm: sha1 otp_algorithm: sha1 (default: sha1, optional)
otp_period: 30 otp_period: 30 (default: 30, optional)
otp_digits: 6 otp_digits: 6 (default: 6, optional)
Example of HOTP fields Example of HOTP fields
otp_secret: secretsecretsecretsecretsecretsecret otp_secret: secretsecretsecretsecretsecretsecret
otp_type: hotp otp_type: hotp
otp_counter: 1 otp_counter: 1
otp_digits: 6 otp_digits: 6 (default: 6, optional)
*/ */
func updateOtpToken() { func updateOtpToken() {
@ -170,12 +170,10 @@ class Password {
return return
} }
// get algorithm // get algorithm (optional)
var algorithm = Generator.Algorithm.sha1 var algorithm = Generator.Algorithm.sha1
if let algoString = getAdditionValue(withKey: "otp_algorithm") { if let algoString = getAdditionValue(withKey: "otp_algorithm") {
switch algoString.lowercased() { switch algoString.lowercased() {
case "sha1":
algorithm = Generator.Algorithm.sha1
case "sha256": case "sha256":
algorithm = Generator.Algorithm.sha256 algorithm = Generator.Algorithm.sha256
case "sha512": case "sha512":
@ -187,20 +185,58 @@ class Password {
// construct the token // construct the token
if type == "totp" { if type == "totp" {
if let digits = Int(getAdditionValue(withKey: "otp_digits") ?? ""), // HOTP
let period = Double(getAdditionValue(withKey: "otp_period") ?? "") { // 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( guard let generator = Generator(
factor: .timer(period: period), factor: .timer(period: period),
secret: secretData, secret: secretData,
algorithm: algorithm, algorithm: algorithm,
digits: digits) else { digits: digits) else {
print("Invalid generator parameters \(self.plainText)") let alertMessage = "Invalid OTP generator parameters."
print(alertMessage)
return
}
self.otpToken = Token(name: self.name, issuer: "", generator: generator)
} else {
// 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 return
} }
self.otpToken = Token(name: self.name, issuer: "", generator: generator) self.otpToken = Token(name: self.name, issuer: "", generator: generator)
} }
} else {
print("We do not support HOTP currently.")
} }
// 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"))
} }
} }

View file

@ -7,6 +7,7 @@
// //
import UIKit import UIKit
import SVProgressHUD
struct LabelTableViewCellData { struct LabelTableViewCellData {
@ -22,15 +23,27 @@ class LabelTableViewCell: UITableViewCell {
var isPasswordCell = false var isPasswordCell = false
var isURLCell = false var isURLCell = false
var isReveal = false var isReveal = false
var password: Password? var isHOTPCell = false
let passwordDots = "••••••••••••" let passwordDots = "••••••••••••"
weak var passwordTableView : PasswordDetailTableViewController?
var cellData: LabelTableViewCellData? { var cellData: LabelTableViewCellData? {
didSet { didSet {
titleLabel.text = cellData?.title titleLabel.text = cellData?.title ?? ""
if isPasswordCell { if isPasswordCell {
if isReveal {
contentLabel.attributedText = Utils.attributedPassword(plainPassword: cellData?.content ?? "")
} else {
contentLabel.text = passwordDots contentLabel.text = passwordDots
}
contentLabel.font = UIFont(name: "Menlo", size: contentLabel.font.pointSize) contentLabel.font = UIFont(name: "Menlo", size: contentLabel.font.pointSize)
} else if isHOTPCell {
if isReveal {
contentLabel.text = cellData?.content ?? ""
} else {
contentLabel.text = passwordDots
}
} else { } else {
contentLabel.text = cellData?.content contentLabel.text = cellData?.content
} }
@ -62,6 +75,13 @@ class LabelTableViewCell: UITableViewCell {
if isURLCell { if isURLCell {
return action == #selector(copy(_:)) || action == #selector(LabelTableViewCell.openLink(_:)) 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(_:)) return action == #selector(copy(_:))
} }
@ -71,7 +91,11 @@ class LabelTableViewCell: UITableViewCell {
func revealPassword(_ sender: Any?) { func revealPassword(_ sender: Any?) {
if let plainPassword = cellData?.content { if let plainPassword = cellData?.content {
if isHOTPCell {
contentLabel.text = plainPassword
} else {
contentLabel.attributedText = Utils.attributedPassword(plainPassword: plainPassword) contentLabel.attributedText = Utils.attributedPassword(plainPassword: plainPassword)
}
} else { } else {
contentLabel.text = "" contentLabel.text = ""
} }
@ -83,8 +107,45 @@ class LabelTableViewCell: UITableViewCell {
isReveal = false 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?) { 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) UIApplication.shared.open(URL(string: cellData!.content)!, options: [:], completionHandler: nil)
} }
} }