// // ViewController.swift // OATH-NFC // // Created by Dennis Hills on 6/5/20. // Copyright © 2020 Dennis Hills. All rights reserved. // // Description: This is sample app demonstrating NFC interaction with the OATH module of a YubiKey using the Yubico iOS SDK // The app establishes an open NFC connection with the YubiKey, reads the credentials from the OATH module and retrieves a TOTP code based on the first OATH credential retrieved. // // There are three main NFC components to this class: // 1. StartNFC Session - Line 49 -> Triggered by a button click that starts the NFC reader session // 2. NFC Session (OBSERVER) - Line 66 -> Registers to receive callbacks sent from an NFC tag reader session. // 3. NFC Session (HANDLER) - Line 94 -> This handles callbacks when the NFC tag reader session detects an ISO 7816 tag // // The other two main functions are communicating with the OATH module of the YubiKey over the NFCReaderSession. // 1. getOATHCredentials() - Line 113 gets the list of all OATH credentials stored on that key // 2. getOTPCode() - Line 152 calls the YKFKeyOATHCalculateRequest which asks the YubiKey to generate a TOTP code for given credential // Updated Github Gist on June 5 here: https://gist.github.com/dmennis/e95f85e2d30e9e08672e2487282d549d import UIKit import YubiKit class ViewController: UIViewController { // UI Stuff @IBOutlet weak var btnStartNFC: UIButton! // Tap this button to start the NFC Session @IBOutlet weak var lblCode: UILabel! // Displays the TOTP code // NFC Session observation private var isNFCObservingSessionStateUpdates = false private var nfcSesionStateObservation: NSKeyValueObservation? var oathService: YKFKeyOATHServiceProtocol? = nil override func viewWillAppear(_ animated: Bool) { if (YubiKitDeviceCapabilities.supportsISO7816NFCTags) { guard #available(iOS 13.0, *) else { fatalError() } observeNFCSessionStateUpdates = true } } override func viewDidLoad() { super.viewDidLoad() } // Initiates the NFCTagReaderSession for interfacing with an ISO 7816 tag @IBAction func startNFCSession(_ sender: Any) { guard #available(iOS 13.0, *) else { fatalError() } if YubiKitDeviceCapabilities.supportsISO7816NFCTags { if YubiKitManager.shared.nfcSession.iso7816SessionState != .closed { YubiKitManager.shared.nfcSession.stopIso7816Session() } YubiKitManager.shared.nfcSession.startIso7816Session() } } /******************** NFC Session Observer ********************/ @available(iOS 13.0, *) var observeNFCSessionStateUpdates: Bool { get { return isNFCObservingSessionStateUpdates } set { guard newValue != isNFCObservingSessionStateUpdates else { return } isNFCObservingSessionStateUpdates = newValue let nfcSession = YubiKitManager.shared.nfcSession as! YKFNFCSession if isNFCObservingSessionStateUpdates { self.nfcSesionStateObservation = nfcSession.observe(\.iso7816SessionState, changeHandler: { [weak self] session, change in DispatchQueue.main.async { [weak self] in self?.nfcSessionStateDidChange() } }) } else { self.nfcSesionStateObservation = nil } } } /******************* NFC Session Handler ********************/ @available(iOS 13.0, *) func nfcSessionStateDidChange() { switch YubiKitManager.shared.nfcSession.iso7816SessionState { case .open: DispatchQueue.global(qos: .default).async { [weak self] in if (YubiKitManager.shared.nfcSession.iso7816SessionState != .closed) { guard let self = self else { return } // Take action here to begin communicating with the YubiKey self.getOATHCredentials() } } case .closed: break default: break } } // Get the list of OATH credentials from the OATH module over the open NFC session // This is called by nfcSessionStateDidChange() when the NFCSession is active and open func getOATHCredentials() { var credCount: Int = 0 // Using the oathService instance over the open NFC session oathService = YubiKitManager.shared.nfcSession.oathService oathService?.executeListRequest { (response, error) in guard error == nil else { print("The list request ended in error \(error!.localizedDescription)") return } // If the error is nil, the response cannot be empty. guard response != nil else { print("Error in getting list of OATH credentials: \(String(describing: response))") fatalError() } let credentials = response!.credentials credCount = credentials.count print("The key has \(credentials.count) stored credentials.") DispatchQueue.main.async { self.lblCode.text = "\(credCount) credentials found." } // Get the first YKFOATHCredential from the list of YKFOATHCredential's, if not empty if(!credentials.isEmpty) { let credential = credentials.first as! YKFOATHCredential self.getOTPCode(oathCredential: credential) } else { // Nothing to calculate, close the NFC session. YubiKitManager.shared.nfcSession.stopIso7816Session() } } } // Ask YubiKey for the TOTP of the provided OATH credential func getOTPCode(oathCredential: YKFOATHCredential) { // Create the CALCULATE request guard let calculateRequest = YKFKeyOATHCalculateRequest(credential: oathCredential) else { return } // Initialize the OATH Service oathService = YubiKitManager.shared.nfcSession.oathService // Execute the calculateRequest oathService?.execute(calculateRequest) { (response, error) in guard error == nil else { print("The calculateRequest ended in error: \(error!.localizedDescription)") return } // If the error is nil, the response cannot be empty. guard response != nil else { fatalError() } // Retrieve the generated TOTP code let otp = response!.otp print("The OTP value for credential [\(String(describing: oathCredential.label))] is \(otp)") // Display the TOTP code DispatchQueue.main.async { self.lblCode.text = "\(otp)" } // Done calculting, close the NFC session. YubiKitManager.shared.nfcSession.stopIso7816Session() } } override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) if (YubiKitDeviceCapabilities.supportsISO7816NFCTags) { guard #available(iOS 13.0, *) else { fatalError() } observeNFCSessionStateUpdates = false } } deinit { if #available(iOS 13.0, *) { observeNFCSessionStateUpdates = false } } }