如何在 Google 验证器、iOS 中的 Authy 等基于时间的 OTP 之间保持同步

How to maintain sync between time based OTPs of apps like Google authenticator, Authy in iOS

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

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

这是我的代码...

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...

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)
    }
}

我不明白你为什么需要单独的 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")

    }
}