0

I am working on an application that generates tops for every 30 seconds using a timer. I am getting secret keys from scanned QR codes and appending them to one model array and for each secret key I am generating tOTP and appending them into another Model array. With that model array, I am populating tableview.

The tableviewcell contains a label for displaying otp and custom circular progress view for tracking the progress. The main issue is I cannot maintain sync with other TOTP apps like Google Authenticator. when I run timer for every second I can generate otps from TOTP generator library every second and can update the label with reloading tableview. But this functionality affects progress view, deleting an editing tableview cells as I am running timer every second to generate otp and reloading tableview. Hope someone helps...

Here is my code...

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

    }
}

This is model object...

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

This is generator class which generates 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)
    }
}
Paulw11
  • 108,386
  • 14
  • 159
  • 186
Purna chandra
  • 107
  • 11
  • Why are you reloading the whole table? Get the visible cells from the tableview and update them. – Paulw11 May 17 '18 at 08:05
  • I am updating model array and populating tableview with that array. I am calling timer selector method to do that. – Purna chandra May 17 '18 at 08:24
  • If i am reloading visible cells i am getting crash – Purna chandra May 17 '18 at 08:26
  • You should solve the crash then. Reloading the whole tableview should only be done when there is a major change to the underlying data set. – Paulw11 May 17 '18 at 08:30
  • Does `generateOTP` create a new OTP each time it is called, or does it use the current time internally so that the OTP is coordinated with the current time and not the number of calls to `generateOTP`? – Paulw11 May 17 '18 at 08:48
  • generateOTP creates OTP every time but it gives new OTP after 30 sec. Before 30 sec even if I call that method every second it will give the same otp. I want to call timer every second so that generateOTP triggers every second and after 30 sec it will give the same otp which will be in sync with otps in other apps. – Purna chandra May 17 '18 at 09:35
  • That’s what I thought, so that is how structured the code in my answer. Your code for calculating the progress doesn’t look right though – Paulw11 May 17 '18 at 09:42
  • Thanks for your help.. I ll try with your approach – Purna chandra May 17 '18 at 09:48

1 Answers1

0

I can't see why you need the separate tOTPModel array. I would delete that and just use the tOTPS array, and I would put the generator in the TOTP object, where it belongs.

Now you can just reload the visible rows each time the timer ticks.

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

    }
}
Paulw11
  • 108,386
  • 14
  • 159
  • 186