Last active
June 5, 2020 23:41
-
-
Save dmennis/e95f85e2d30e9e08672e2487282d549d to your computer and use it in GitHub Desktop.
iOS Swift view controller to interact with the OATH module of a YubiKey over NFC
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// 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 } | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment