【问题标题】:How to maintain sync between time based OTPs of apps like Google authenticator, Authy in iOS如何在基于时间的应用程序 OTP 之间保持同步,例如 Google 身份验证器、iOS 中的 Authy
【发布时间】:2018-05-17 07:45:48
【问题描述】:

我正在开发一个使用计时器每 30 秒生成一次顶部的应用程序。我从扫描的 QR 码中获取密钥并将它们附加到一个模型数组中,对于每个密钥我生成 tOTP 并将它们附加到另一个模型数组中。使用该模型数组,我正在填充 tableview。

tableviewcell 包含用于显示 otp 的标签和用于跟踪进度的自定义循环进度视图。主要问题是我无法与其他 TOTP 应用程序(如 Google Authenticator)保持同步。当我每秒运行计时器时,我可以每秒从 TOTP 生成器库生成 otps,并且可以通过重新加载 tableview 来更新标签。但是这个功能会影响进度视图,当我每秒运行计时器以生成 otp 并重新加载 tableview 时删除编辑 tableview 单元格。 希望有人帮助...

这是我的代码...

class ViewController: UIViewController,UITableViewDelegate, UITableViewDataSource,AddTOTPDelegate, UIGestureRecognizerDelegate {

    var tOTPS = [TOTP]()
    var tOTPModel = [TOTP]()

    var secretKeys = [String]()
    var generator: OTPGenerator?
    var timer: Timer?
    var currentTimeInterval = TimeInterval()

    @IBOutlet weak var tOtpTableView: UITableView!
    @IBOutlet weak var btnInfo: UIBarButtonItem!
    @IBOutlet weak var btnAddQRCode: UIButton!

    override func viewDidLoad() {
        super.viewDidLoad()
        self.tOtpTableView.tableFooterView = UIView()
        self.emptyDataString = "Click + to add new account"
        let longPressGesture:UILongPressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(ViewController.longPress(_:)))
        longPressGesture.minimumPressDuration = 1.0
        longPressGesture.delegate = self
        self.tOtpTableView.addGestureRecognizer(longPressGesture)
        setUpViews()
        getTotps()
    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(true)
      //  let period = (Date().timeIntervalSince1970 / 1000).truncatingRemainder(dividingBy: 30)
        if self.tOTPS.isEmpty == false {
            self.timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(generateTOTP), userInfo: nil, repeats: true)
            self.timer?.fire()
        }
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }

    func getTotps() {
        if let decodedData = KeychainWrapper.standard.data(forKey: "tOtps") {
            self.tOTPS = NSKeyedUnarchiver.unarchiveObject(with: decodedData) as! [TOTP]
        }
    }

    @IBAction func handleAddQRCode(_ sender: UIButton) {
        let controllerToPresent = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "QRScannerController") as! QRScannerController
        controllerToPresent.delegate = self
        controllerToPresent.tOTPS = self.tOTPS
        self.timer?.invalidate()
        DispatchQueue.main.async {
            self.navigationController?.pushViewController(controllerToPresent, animated: true)
        }
    }

    @objc func generateTOTP() {
        self.tOTPModel = []
        for tOtpObject in self.tOTPS {
            self.generator = Generator.generatorWithSecretKey(key: tOtpObject.secretKey)
            let tOtp = (self.generator as! TOTPGenerator).generateOTP()
            self.tOTPModel.append(TOTP(secretKey: tOtp!, issuer: tOtpObject.issuer, scheme: tOtpObject.scheme, createdDate: tOtpObject.createdDate))
        }
        self.tOtpTableView.reloadData()
    }

    func numberOfSections(in tableView: UITableView) -> Int {
         return self.tOTPModel.count
    }

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return 1
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "totpCell", for: indexPath) as! TotpViewCell
        let tOTP = self.tOTPModel[indexPath.section]
        cell.lblTOTP.text = tOTP.secretKey.separate(every: 3, with: " ")
        cell.lblIssuer.text = tOTP.issuer
        cell.lblCreatedDate.text = "Created Date: \(tOTP.createdDate)"

        cell.lblCreatedDate.isHidden = true
        cell.issuerConstraint.isActive = true

      // let period = (Date().timeIntervalSince1970 / 1000).truncatingRemainder(dividingBy: 30)

        currentTimeInterval = (self.timer?.fireDate.timeIntervalSince(Date()))!

        let fromValue = 1
        let toValue = 0

        cell.progressView.handleAnimation(fromValue: fromValue, tVal: toValue, duration: currentTimeInterval)
        return cell
    }

    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        tableView.deselectRow(at: indexPath, animated: false)
    }

    func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? {
        let editAction = UITableViewRowAction(style: .normal, title: "Edit") { (rowAction, indexPath) in
            let alertController = UIAlertController(title: "Authenticator", message: "Enter the issuer", preferredStyle: .alert)
            alertController.addAction(UIAlertAction(title: "Save", style: .default, handler: { alert -> Void in
                let textField = alertController.textFields![0] as UITextField

                self.tOTPModel[indexPath.section].issuer = textField.text!
                self.tOTPS[indexPath.section].issuer = textField.text!

                let encodedData: Data = NSKeyedArchiver.archivedData(withRootObject: self.tOTPS)
                KeychainWrapper.standard.set(encodedData, forKey: "tOtps")
                tableView.reloadData()

            }))
            alertController.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
            alertController.addTextField(configurationHandler: {(textField : UITextField!) -> Void in
                let tOTP = self.tOTPModel[indexPath.section]
                textField.placeholder = "Enter Issuer"
                textField.text = tOTP.issuer

            })
            self.present(alertController, animated: true, completion: nil)
        }
        editAction.backgroundColor = UIColor(red: 0/255, green: 145/255, blue: 147/255, alpha: 1.0)

        let deleteAction = UITableViewRowAction(style: .normal, title: "Delete") { (rowAction, indexPath) in

            let alertController = UIAlertController(title: "Authenticator", message: "Are you sure you want remove this account?", preferredStyle: .alert)
            let okAction = UIAlertAction(title: "OK", style: .default, handler: { (action) in
                self.tOTPS.remove(at: indexPath.section)
                self.tOTPModel.remove(at: indexPath.section)

                let encodedData: Data = NSKeyedArchiver.archivedData(withRootObject: self.tOTPS)
                KeychainWrapper.standard.set(encodedData, forKey: "tOtps")

                tableView.deleteSections([indexPath.section], with: .automatic)
                tableView.reloadData()
            })
            let cancelAction = UIAlertAction(title: "Cancel", style: .destructive, handler: { (action) in
                self.dismiss(animated: true, completion: nil)
            })
            alertController.addAction(cancelAction)
            alertController.addAction(okAction)
            self.present(alertController, animated: true, completion: nil)

        }
        deleteAction.backgroundColor = .red

        return [editAction,deleteAction]
    }

    func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
        return true
    }

    // Delegate Method that adds tOtp from QRCode scanner controller
    func addTOTP(withSecret secret: String, issuer: String, scheme: String,createdDate: String) {
        self.tOTPS.append(TOTP(secretKey: secret, issuer: issuer, scheme: scheme, createdDate: createdDate))
        let encodedData: Data = NSKeyedArchiver.archivedData(withRootObject: self.tOTPS)
        KeychainWrapper.standard.set(encodedData, forKey: "tOtps")

    }
}

这是模型对象...

class TOTP: NSObject, NSCoding {

    var secretKey: String
    var issuer: String
    var scheme: String
    var createdDate: String

    init(secretKey: String, issuer: String, scheme: String, createdDate: String) {
        self.secretKey = secretKey
        self.issuer = issuer
        self.scheme = scheme
        self.createdDate = createdDate
    }

    func encode(with aCoder: NSCoder) {
        aCoder.encode(secretKey, forKey: "secretKey")
        aCoder.encode(issuer, forKey: "issuer")
        aCoder.encode(scheme, forKey: "scheme")
        aCoder.encode(createdDate, forKey: "timeInterval")
    }

    required init?(coder aDecoder: NSCoder) {
        secretKey = aDecoder.decodeObject(forKey: "secretKey") as! String
        issuer = aDecoder.decodeObject(forKey: "issuer") as! String
        scheme = aDecoder.decodeObject(forKey: "scheme") as! String
        createdDate = aDecoder.decodeObject(forKey: "timeInterval") as! String
    }
}

这是生成 TOTP 的生成器类...

class Generator {
    static func generatorWithSecretKey(key: String) -> OTPGenerator {
      //  let period = (Date().timeIntervalSince1970 / 1000).truncatingRemainder(dividingBy: 30)
        let secretKey = MF_Base32Codec.data(fromBase32String: key)
        return TOTPGenerator(secret: secretKey, algorithm: OTPGenerator.defaultAlgorithm(), digits: 6, period: 30)
    }
}

【问题讨论】:

  • 为什么要重新加载整个表?从 tableview 中获取可见单元格并更新它们。
  • 我正在更新模型数组并使用该数组填充 tableview。我正在调用计时器选择器方法来做到这一点。
  • 如果我重新加载可见单元格,我会崩溃
  • 你应该解决崩溃。仅当底层数据集发生重大更改时才应重新加载整个 tableview。
  • generateOTP 是在每次调用它时创建一个新的 OTP,还是在内部使用当前时间以便 OTP 与当前时间而不是对 generateOTP 的调用次数相协调?

标签: ios tableview totp


【解决方案1】:

我不明白您为什么需要单独的 tOTPModel 数组。我会删除它并只使用tOTPS 数组,然后将生成器放在它所属的TOTP 对象中。

现在您可以在每次计时器计时时重新加载可见行。

class TOTP: NSObject, NSCoding {

    var secretKey: String
    var issuer: String
    var scheme: String
    var createdDate: String
    private var generator: OTPGenerator

    var otp: String = {
        return generator.generateOTP()
    }

    init(secretKey: String, issuer: String, scheme: String, createdDate: String) {
        self.secretKey = secretKey
        self.issuer = issuer
        self.scheme = scheme
        self.createdDate = createdDate
        self.generator =  Generator.generatorWithSecretKey(key: secretKey)
    }

    func encode(with aCoder: NSCoder) {
        aCoder.encode(secretKey, forKey: "secretKey")
        aCoder.encode(issuer, forKey: "issuer")
        aCoder.encode(scheme, forKey: "scheme")
        aCoder.encode(createdDate, forKey: "timeInterval")
    }

    required init?(coder aDecoder: NSCoder) {
        secretKey = aDecoder.decodeObject(forKey: "secretKey") as! String
        issuer = aDecoder.decodeObject(forKey: "issuer") as! String
        scheme = aDecoder.decodeObject(forKey: "scheme") as! String
        createdDate = aDecoder.decodeObject(forKey: "timeInterval") as! String
        generator =  Generator.generatorWithSecretKey(key: secretKey)
    }
}


class ViewController: UIViewController,UITableViewDelegate, UITableViewDataSource,AddTOTPDelegate, UIGestureRecognizerDelegate {

    var tOTPS = [TOTP]()

    var secretKeys = [String]()
    var timer: Timer?
    var currentTimeInterval = TimeInterval()

    @IBOutlet weak var tOtpTableView: UITableView!
    @IBOutlet weak var btnInfo: UIBarButtonItem!
    @IBOutlet weak var btnAddQRCode: UIButton!

    override func viewDidLoad() {
        super.viewDidLoad()
        self.tOtpTableView.tableFooterView = UIView()
        self.emptyDataString = "Click + to add new account"
        let longPressGesture:UILongPressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(ViewController.longPress(_:)))
        longPressGesture.minimumPressDuration = 1.0
        longPressGesture.delegate = self
        self.tOtpTableView.addGestureRecognizer(longPressGesture)
        setUpViews()
        getTotps()
    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(true)
      //  let period = (Date().timeIntervalSince1970 / 1000).truncatingRemainder(dividingBy: 30)
        if !self.tOTPS.isEmpty {
            self.timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(generateTOTP), userInfo: nil, repeats: true)
            self.timer?.fire()
        }
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }

    func getTotps() {
        if let decodedData = KeychainWrapper.standard.data(forKey: "tOtps") {
            self.tOTPS = NSKeyedUnarchiver.unarchiveObject(with: decodedData) as! [TOTP]
        }
    }

    @IBAction func handleAddQRCode(_ sender: UIButton) {
        let controllerToPresent = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "QRScannerController") as! QRScannerController
        controllerToPresent.delegate = self
        controllerToPresent.tOTPS = self.tOTPS
        self.timer?.invalidate()
        DispatchQueue.main.async {
            self.navigationController?.pushViewController(controllerToPresent, animated: true)
        }
    }

    @objc func generateTOTP() {
        tableView.reloadRows(at:tableView.indexPathsForVisibleRows, with:.none)

    }

    func numberOfSections(in tableView: UITableView) -> Int {
         return self.tOTPModel.count
    }

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return 1
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "totpCell", for: indexPath) as! TotpViewCell
        let tOTP = self.tOTPs[indexPath.section]
        cell.lblTOTP.text = tOTP.otp.separate(every: 3, with: " ")
        cell.lblIssuer.text = tOTP.issuer
        cell.lblCreatedDate.text = "Created Date: \(tOTP.createdDate)"

        cell.lblCreatedDate.isHidden = true
        cell.issuerConstraint.isActive = true

      // let period = (Date().timeIntervalSince1970 / 1000).truncatingRemainder(dividingBy: 30)

        currentTimeInterval = (self.timer?.fireDate.timeIntervalSince(Date()))!

        let fromValue = 1
        let toValue = 0

        cell.progressView.handleAnimation(fromValue: fromValue, tVal: toValue, duration: currentTimeInterval)
        return cell
    }

    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        tableView.deselectRow(at: indexPath, animated: false)
    }

    func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? {
        let editAction = UITableViewRowAction(style: .normal, title: "Edit") { (rowAction, indexPath) in
            let alertController = UIAlertController(title: "Authenticator", message: "Enter the issuer", preferredStyle: .alert)
            alertController.addAction(UIAlertAction(title: "Save", style: .default, handler: { alert -> Void in
                let textField = alertController.textFields![0] as UITextField

                self.tOTPModel[indexPath.section].issuer = textField.text!
                self.tOTPS[indexPath.section].issuer = textField.text!

                let encodedData: Data = NSKeyedArchiver.archivedData(withRootObject: self.tOTPS)
                KeychainWrapper.standard.set(encodedData, forKey: "tOtps")
                tableView.reloadData()

            }))
            alertController.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
            alertController.addTextField(configurationHandler: {(textField : UITextField!) -> Void in
                let tOTP = self.tOTPModel[indexPath.section]
                textField.placeholder = "Enter Issuer"
                textField.text = tOTP.issuer

            })
            self.present(alertController, animated: true, completion: nil)
        }
        editAction.backgroundColor = UIColor(red: 0/255, green: 145/255, blue: 147/255, alpha: 1.0)

        let deleteAction = UITableViewRowAction(style: .normal, title: "Delete") { (rowAction, indexPath) in

            let alertController = UIAlertController(title: "Authenticator", message: "Are you sure you want remove this account?", preferredStyle: .alert)
            let okAction = UIAlertAction(title: "OK", style: .default, handler: { (action) in
                self.tOTPS.remove(at: indexPath.section)
                self.tOTPModel.remove(at: indexPath.section)

                let encodedData: Data = NSKeyedArchiver.archivedData(withRootObject: self.tOTPS)
                KeychainWrapper.standard.set(encodedData, forKey: "tOtps")

                tableView.deleteSections([indexPath.section], with: .automatic)
                tableView.reloadData()
            })
            let cancelAction = UIAlertAction(title: "Cancel", style: .destructive, handler: { (action) in
                self.dismiss(animated: true, completion: nil)
            })
            alertController.addAction(cancelAction)
            alertController.addAction(okAction)
            self.present(alertController, animated: true, completion: nil)

        }
        deleteAction.backgroundColor = .red

        return [editAction,deleteAction]
    }

    func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
        return true
    }

    // Delegate Method that adds tOtp from QRCode scanner controller
    func addTOTP(withSecret secret: String, issuer: String, scheme: String,createdDate: String) {
        self.tOTPS.append(TOTP(secretKey: secret, issuer: issuer, scheme: scheme, createdDate: createdDate))
        let encodedData: Data = NSKeyedArchiver.archivedData(withRootObject: self.tOTPS)
        KeychainWrapper.standard.set(encodedData, forKey: "tOtps")

    }
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2019-05-19
    • 1970-01-01
    • 2016-03-14
    • 1970-01-01
    • 2017-01-16
    • 1970-01-01
    • 1970-01-01
    • 2013-03-09
    相关资源
    最近更新 更多