Skip to content

Instantly share code, notes, and snippets.

@dmennis
Last active June 5, 2020 23:41
Show Gist options
  • Save dmennis/e95f85e2d30e9e08672e2487282d549d to your computer and use it in GitHub Desktop.
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
//
// 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