diff --git a/CHANGELOG.md b/CHANGELOG.md
index ebf8b6e23..20d3966df 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,11 @@
 #CHANGELOG
 
+## [1.09.50] - 2020-01-14
+
+- Internal beta release
+- Invalid: missing camera usage description
+
+
 ## [1.09.49] - 2020-01-14
 
 ### Added
diff --git a/KeePassium AutoFill/MainCoordinator.swift b/KeePassium AutoFill/MainCoordinator.swift
index f2a9e1445..97702bca4 100644
--- a/KeePassium AutoFill/MainCoordinator.swift	
+++ b/KeePassium AutoFill/MainCoordinator.swift	
@@ -141,30 +141,46 @@ class MainCoordinator: NSObject, Coordinator {
         (topVC as? KeyFileChooserVC)?.refresh()
     }
     
+    
+    private func challengeHandler(
+        challenge: SecureByteArray,
+        responseHandler: @escaping ResponseHandler)
+    {
+        Diag.warning("YubiKey is not available in AutoFill")
+        responseHandler(SecureByteArray(), .notAvailableInAutoFill)
+    }
+    
     private func tryToUnlockDatabase(
         database: URLReference,
         password: String,
-        keyFile: URLReference?)
+        keyFile: URLReference?,
+        yubiKey: YubiKey?)
     {
         Settings.current.isAutoFillFinishedOK = false
         
+        let _challengeHandler = (yubiKey != nil) ? challengeHandler : nil
         isLoadingUsingStoredDatabaseKey = false
         DatabaseManager.shared.startLoadingDatabase(
             database: database,
             password: password,
-            keyFile: keyFile)
+            keyFile: keyFile,
+            challengeHandler: _challengeHandler
+        )
     }
     
     private func tryToUnlockDatabase(
         database: URLReference,
-        compositeKey: SecureByteArray)
+        compositeKey: CompositeKey,
+        yubiKey: YubiKey?)
     {
         Settings.current.isAutoFillFinishedOK = false
         
+        compositeKey.challengeHandler = (yubiKey != nil) ? challengeHandler : nil
         isLoadingUsingStoredDatabaseKey = true
         DatabaseManager.shared.startLoadingDatabase(
             database: database,
-            compositeKey: compositeKey)
+            compositeKey: compositeKey
+        )
     }
     
     
@@ -250,7 +266,11 @@ class MainCoordinator: NSObject, Coordinator {
         navigationController.pushViewController(vc, animated: animated)
         completion?()
         if let storedDatabaseKey = storedDatabaseKey {
-            tryToUnlockDatabase(database: database, compositeKey: storedDatabaseKey)
+            tryToUnlockDatabase(
+                database: database,
+                compositeKey: storedDatabaseKey,
+                yubiKey: dbSettings?.associatedYubiKey
+            )
         }
     }
     
@@ -394,10 +414,26 @@ extension MainCoordinator: DatabaseUnlockerDelegate {
         _ sender: DatabaseUnlockerVC,
         database: URLReference,
         password: String,
-        keyFile: URLReference?)
+        keyFile: URLReference?,
+        yubiKey: YubiKey?)
     {
         watchdog.restart()
-        tryToUnlockDatabase(database: database, password: password, keyFile: keyFile)
+        tryToUnlockDatabase(
+            database: database,
+            password: password,
+            keyFile: keyFile,
+            yubiKey: yubiKey)
+    }
+    
+    func didPressSelectHardwareKey(in databaseUnlocker: DatabaseUnlockerVC, at popoverAnchor: PopoverAnchor) {
+        let hardwareKeyPicker = HardwareKeyPicker.create(delegate: self)
+        hardwareKeyPicker.modalPresentationStyle = .popover
+        if let popover = hardwareKeyPicker.popoverPresentationController {
+            popoverAnchor.apply(to: popover)
+            popover.delegate = hardwareKeyPicker.dismissablePopoverDelegate
+        }
+        hardwareKeyPicker.key = databaseUnlocker.yubiKey
+        navigationController.present(hardwareKeyPicker, animated: true, completion: nil)
     }
     
     func didPressNewsItem(in databaseUnlocker: DatabaseUnlockerVC, newsItem: NewsItem) {
@@ -405,6 +441,19 @@ extension MainCoordinator: DatabaseUnlockerDelegate {
     }
 }
 
+extension MainCoordinator: HardwareKeyPickerDelegate {
+    func didDismiss(_ picker: HardwareKeyPicker) {
+    }
+    func didSelectKey(yubiKey: YubiKey?, in picker: HardwareKeyPicker) {
+        watchdog.restart()
+        if let databaseUnlockerVC = navigationController.topViewController as? DatabaseUnlockerVC {
+            databaseUnlockerVC.setYubiKey(yubiKey)
+        } else {
+            assertionFailure()
+        }
+    }
+}
+
 extension MainCoordinator: KeyFileChooserDelegate {
     
     func didSelectFile(in keyFileChooser: KeyFileChooserVC, urlRef: URLReference?) {
diff --git a/KeePassium AutoFill/controllers/Base.lproj/DatabaseUnlockerVC.storyboard b/KeePassium AutoFill/controllers/Base.lproj/DatabaseUnlockerVC.storyboard
index 0ecfa2a1e..b9b927f24 100644
--- a/KeePassium AutoFill/controllers/Base.lproj/DatabaseUnlockerVC.storyboard	
+++ b/KeePassium AutoFill/controllers/Base.lproj/DatabaseUnlockerVC.storyboard	
@@ -1,9 +1,9 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="15400" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="bxv-kR-0w1">
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="15702" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="bxv-kR-0w1">
     <device id="retina4_0" orientation="portrait" appearance="dark"/>
     <dependencies>
         <deployment identifier="iOS"/>
-        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15404"/>
+        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15704"/>
         <capability name="Named colors" minToolsVersion="9.0"/>
         <capability name="Safe area layout guides" minToolsVersion="9.0"/>
         <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
@@ -124,7 +124,7 @@
                                                     <constraint firstAttribute="height" constant="2" id="rNj-qR-5f7"/>
                                                 </constraints>
                                             </view>
-                                            <textField opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" contentHorizontalAlignment="left" contentVerticalAlignment="center" placeholder="No Key File" textAlignment="natural" adjustsFontForContentSizeCategory="YES" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="Dnl-vm-GMg">
+                                            <textField opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" contentHorizontalAlignment="left" contentVerticalAlignment="center" placeholder="No Key File" textAlignment="natural" adjustsFontForContentSizeCategory="YES" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="Dnl-vm-GMg" customClass="KeyFileTextField" customModule="KeePassiumPro_AutoFill" customModuleProvider="target">
                                                 <rect key="frame" x="8" y="46" width="264" height="44"/>
                                                 <constraints>
                                                     <constraint firstAttribute="height" constant="44" id="Sao-Cr-ebW"/>
@@ -224,7 +224,7 @@
                         <outlet property="errorMessageLabel" destination="dID-zL-AtZ" id="yty-hV-df1"/>
                         <outlet property="errorMessagePanel" destination="Crk-4o-Fqu" id="WOd-6g-If7"/>
                         <outlet property="inputPanel" destination="Q5g-RY-uXD" id="6S7-1l-TIX"/>
-                        <outlet property="keyFileField" destination="Dnl-vm-GMg" id="frD-BC-dbG"/>
+                        <outlet property="keyFileField" destination="Dnl-vm-GMg" id="k6z-2m-zpB"/>
                         <outlet property="passwordField" destination="U6j-Ph-AWS" id="eKJ-Bd-ciZ"/>
                         <outlet property="unlockButton" destination="89h-8b-LRO" id="f59-RP-Gbr"/>
                     </connections>
diff --git a/KeePassium AutoFill/controllers/Base.lproj/EntryFinderVC.storyboard b/KeePassium AutoFill/controllers/Base.lproj/EntryFinderVC.storyboard
index 3c2488987..2cd736484 100644
--- a/KeePassium AutoFill/controllers/Base.lproj/EntryFinderVC.storyboard	
+++ b/KeePassium AutoFill/controllers/Base.lproj/EntryFinderVC.storyboard	
@@ -1,9 +1,9 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="15400" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="15702" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
     <device id="retina4_0" orientation="portrait" appearance="dark"/>
     <dependencies>
         <deployment identifier="iOS"/>
-        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15404"/>
+        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15704"/>
         <capability name="Named colors" minToolsVersion="9.0"/>
         <capability name="Safe area layout guides" minToolsVersion="9.0"/>
         <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
@@ -43,14 +43,14 @@
                                 </tableViewCellContentView>
                             </tableViewCell>
                             <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" reuseIdentifier="EntryFinderCell" id="RaK-Ag-MEi" customClass="EntryFinderCell" customModule="KeePassiumPro_AutoFill" customModuleProvider="target">
-                                <rect key="frame" x="0.0" y="99" width="320" height="43.5"/>
+                                <rect key="frame" x="0.0" y="99" width="320" height="51.5"/>
                                 <autoresizingMask key="autoresizingMask"/>
                                 <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="RaK-Ag-MEi" id="LNj-AK-QdV">
-                                    <rect key="frame" x="0.0" y="0.0" width="320" height="43.5"/>
+                                    <rect key="frame" x="0.0" y="0.0" width="320" height="51.5"/>
                                     <autoresizingMask key="autoresizingMask"/>
                                     <subviews>
                                         <imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="db-icons/kpbIcon00" translatesAutoresizingMaskIntoConstraints="NO" id="bgc-qJ-rf9">
-                                            <rect key="frame" x="16" y="7.5" width="29" height="29"/>
+                                            <rect key="frame" x="16" y="11.5" width="29" height="29"/>
                                             <color key="tintColor" name="IconTint"/>
                                             <constraints>
                                                 <constraint firstAttribute="height" constant="29" id="P1E-9B-6XL"/>
@@ -58,7 +58,7 @@
                                             </constraints>
                                         </imageView>
                                         <stackView opaque="NO" contentMode="scaleToFill" axis="vertical" translatesAutoresizingMaskIntoConstraints="NO" id="NKy-ya-6Eb">
-                                            <rect key="frame" x="61" y="4.5" width="243" height="35"/>
+                                            <rect key="frame" x="61" y="8.5" width="243" height="35"/>
                                             <subviews>
                                                 <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="249" verticalHuggingPriority="249" text="{Entry Title}" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="1xY-Ij-CPq">
                                                     <rect key="frame" x="0.0" y="0.0" width="243" height="20.5"/>
@@ -85,6 +85,8 @@
                                         <constraint firstItem="bgc-qJ-rf9" firstAttribute="centerY" secondItem="LNj-AK-QdV" secondAttribute="centerY" id="2YH-bM-UdN"/>
                                         <constraint firstItem="NKy-ya-6Eb" firstAttribute="centerY" secondItem="LNj-AK-QdV" secondAttribute="centerY" id="UB2-bl-32x"/>
                                         <constraint firstItem="bgc-qJ-rf9" firstAttribute="leading" secondItem="LNj-AK-QdV" secondAttribute="leadingMargin" id="ekQ-4n-Pls"/>
+                                        <constraint firstAttribute="bottomMargin" relation="greaterThanOrEqual" secondItem="bgc-qJ-rf9" secondAttribute="bottom" id="gu1-U2-SJ6"/>
+                                        <constraint firstItem="bgc-qJ-rf9" firstAttribute="top" relation="greaterThanOrEqual" secondItem="LNj-AK-QdV" secondAttribute="topMargin" id="jbp-3h-dRc"/>
                                         <constraint firstItem="NKy-ya-6Eb" firstAttribute="leading" secondItem="bgc-qJ-rf9" secondAttribute="trailing" constant="16" id="ttY-eH-7wn"/>
                                         <constraint firstAttribute="trailingMargin" secondItem="NKy-ya-6Eb" secondAttribute="trailing" id="tuY-3B-nOL"/>
                                     </constraints>
@@ -174,10 +176,10 @@
             <color red="0.23499999940395355" green="0.23499999940395355" blue="0.2630000114440918" alpha="0.60000002384185791" colorSpace="custom" customColorSpace="sRGB"/>
         </namedColor>
         <namedColor name="IconTint">
-            <color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
+            <color red="0.0" green="0.41176470588235292" blue="0.85098039215686272" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
         </namedColor>
         <namedColor name="PrimaryText">
-            <color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
+            <color white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
         </namedColor>
     </resources>
 </document>
diff --git a/KeePassium AutoFill/controllers/DatabaseUnlockerVC.swift b/KeePassium AutoFill/controllers/DatabaseUnlockerVC.swift
index ba66ca800..4d21574c7 100644
--- a/KeePassium AutoFill/controllers/DatabaseUnlockerVC.swift	
+++ b/KeePassium AutoFill/controllers/DatabaseUnlockerVC.swift	
@@ -13,8 +13,12 @@ protocol DatabaseUnlockerDelegate: class {
         _ sender: DatabaseUnlockerVC,
         database: URLReference,
         password: String,
-        keyFile: URLReference?)
+        keyFile: URLReference?,
+        yubiKey: YubiKey?)
     func didPressNewsItem(in databaseUnlocker: DatabaseUnlockerVC, newsItem: NewsItem)
+    func didPressSelectHardwareKey(
+        in databaseUnlocker: DatabaseUnlockerVC,
+        at popoverAnchor: PopoverAnchor)
 }
 
 class DatabaseUnlockerVC: UIViewController, Refreshable {
@@ -26,7 +30,7 @@ class DatabaseUnlockerVC: UIViewController, Refreshable {
     @IBOutlet weak var databaseFileNameLabel: UILabel!
     @IBOutlet weak var inputPanel: UIView!
     @IBOutlet weak var passwordField: ProtectedTextField!
-    @IBOutlet weak var keyFileField: UITextField!
+    @IBOutlet weak var keyFileField: KeyFileTextField!
     @IBOutlet weak var announcementButton: UIButton!
     @IBOutlet weak var unlockButton: UIButton!
     
@@ -37,6 +41,7 @@ class DatabaseUnlockerVC: UIViewController, Refreshable {
         didSet { refresh() }
     }
     private(set) var keyFileRef: URLReference?
+    private(set) var yubiKey: YubiKey?
     
     override func viewDidLoad() {
         super.viewDidLoad()
@@ -50,8 +55,17 @@ class DatabaseUnlockerVC: UIViewController, Refreshable {
         
         refresh()
         
-        keyFileField.delegate = self
         passwordField.delegate = self
+        keyFileField.delegate = self
+        
+        keyFileField.yubikeyHandler = {
+            [weak self] (field) in
+            guard let self = self else { return }
+            let popoverAnchor = PopoverAnchor(
+                sourceView: self.keyFileField,
+                sourceRect: self.keyFileField.bounds)
+            self.delegate?.didPressSelectHardwareKey(in: self, at: popoverAnchor)
+        }
     }
     
     override func viewDidAppear(_ animated: Bool) {
@@ -161,6 +175,25 @@ class DatabaseUnlockerVC: UIViewController, Refreshable {
                 setKeyFile(urlRef: availableKeyFileRef)
             }
         }
+
+        if let associatedYubiKey = dbSettings?.associatedYubiKey {
+            setYubiKey(associatedYubiKey)
+        }
+    }
+    
+    func setYubiKey(_ yubiKey: YubiKey?) {
+        self.yubiKey = yubiKey
+        keyFileField.isYubiKeyActive = (yubiKey != nil)
+
+        guard let databaseRef = databaseRef else { assertionFailure(); return }
+        DatabaseSettingsManager.shared.updateSettings(for: databaseRef) { (dbSettings) in
+            dbSettings.maybeSetAssociatedYubiKey(yubiKey)
+        }
+        if let _yubiKey = yubiKey {
+            Diag.info("Hardware key selected [key: \(_yubiKey)]")
+        } else {
+            Diag.info("No hardware key selected")
+        }
     }
     
     func setKeyFile(urlRef: URLReference?) {
@@ -248,7 +281,8 @@ class DatabaseUnlockerVC: UIViewController, Refreshable {
             self,
             database: databaseRef,
             password: passwordField.text ?? "",
-            keyFile: keyFileRef)
+            keyFile: keyFileRef,
+            yubiKey: yubiKey)
     }
     
     @IBAction func didPressAnouncementButton(_ sender: Any) {
diff --git a/KeePassium-Bridging-Header.h b/KeePassium-Bridging-Header.h
new file mode 100644
index 000000000..5587c0a79
--- /dev/null
+++ b/KeePassium-Bridging-Header.h
@@ -0,0 +1,13 @@
+//  KeePassium Password Manager
+//  Copyright © 2018–2019 Andrei Popleteev <info@keepassium.com>
+//
+//  This program is free software: you can redistribute it and/or modify it
+//  under the terms of the GNU General Public License version 3 as published
+//  by the Free Software Foundation: https://www.gnu.org/licenses/).
+//  For commercial licensing, please contact the author.
+
+#ifndef KeePassiumBridgingHeader_h
+#define KeePassiumBridgingHeader_h
+
+#import "YubiKit/YubiKit.h"
+#endif /* KeePassiumBridgingHeader_h */
diff --git a/KeePassium.xcodeproj/project.pbxproj b/KeePassium.xcodeproj/project.pbxproj
index c641b017d..e3ff29016 100755
--- a/KeePassium.xcodeproj/project.pbxproj
+++ b/KeePassium.xcodeproj/project.pbxproj
@@ -105,11 +105,19 @@
 		754BF08A2082A6BD00E3A292 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 754BF0892082A6BD00E3A292 /* Assets.xcassets */; };
 		754BF08D2082A6BD00E3A292 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 754BF08B2082A6BD00E3A292 /* LaunchScreen.storyboard */; };
 		754BF09A20984DEE00E3A292 /* UnlockDatabaseVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 754BF09920984DEE00E3A292 /* UnlockDatabaseVC.swift */; };
+		754D7C09238C2135005652E1 /* KeyFileTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 754D7C08238C2135005652E1 /* KeyFileTextField.swift */; };
+		754D7C0A238C2135005652E1 /* KeyFileTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 754D7C08238C2135005652E1 /* KeyFileTextField.swift */; };
+		754D7C0B238C2135005652E1 /* KeyFileTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 754D7C08238C2135005652E1 /* KeyFileTextField.swift */; };
+		754D7C0C238C2135005652E1 /* KeyFileTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 754D7C08238C2135005652E1 /* KeyFileTextField.swift */; };
+		754D7C1C238C3553005652E1 /* libYubiKit.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 754D7C18238C2C0E005652E1 /* libYubiKit.a */; };
+		754D7C1D238C355B005652E1 /* libYubiKit.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 754D7C18238C2C0E005652E1 /* libYubiKit.a */; };
 		754DE31220A5C4A4002ED5F9 /* ChooseKeyFileVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 754DE31120A5C4A4002ED5F9 /* ChooseKeyFileVC.swift */; };
 		7550D22B215CC99100360E0C /* ChangeMasterKeyVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7550D22A215CC99100360E0C /* ChangeMasterKeyVC.swift */; };
 		7550D242215D792000360E0C /* AuthenticationServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7550D231215D758800360E0C /* AuthenticationServices.framework */; settings = {ATTRIBUTES = (Weak, ); }; };
 		7550D24C215FDEC500360E0C /* AppCoverVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7550D24B215FDEC500360E0C /* AppCoverVC.swift */; };
 		75514A3420DF5B6200202340 /* EditEntryFieldsCells.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75514A3320DF5B6200202340 /* EditEntryFieldsCells.swift */; };
+		7558D06223CE2F3E00547621 /* ChallengeResponseManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7558D06123CE2F3E00547621 /* ChallengeResponseManager.swift */; };
+		7558D06323CE2F4100547621 /* ChallengeResponseManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7558D06123CE2F3E00547621 /* ChallengeResponseManager.swift */; };
 		755B862E23054FA200E9FC83 /* AboutVC.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 755B863023054FA200E9FC83 /* AboutVC.storyboard */; };
 		755B863123054FAD00E9FC83 /* PasscodeInputVC.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 755B863423054FAD00E9FC83 /* PasscodeInputVC.storyboard */; };
 		755B863223054FAD00E9FC83 /* PasscodeInputVC.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 755B863423054FAD00E9FC83 /* PasscodeInputVC.storyboard */; };
@@ -148,6 +156,14 @@
 		7578487E22C02A9100BBEF9A /* UITableViewCell+demoEditActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7578487C22C02A9100BBEF9A /* UITableViewCell+demoEditActions.swift */; };
 		7578488622C165C900BBEF9A /* SettingsBackupVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7578488522C165C900BBEF9A /* SettingsBackupVC.swift */; };
 		7578488F22C1DFF400BBEF9A /* SettingsBackupTimeoutPickerVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7578488E22C1DFF400BBEF9A /* SettingsBackupTimeoutPickerVC.swift */; };
+		757ADC4523A1047600D708D2 /* HardwareKeyPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 757ADC4423A1047600D708D2 /* HardwareKeyPicker.swift */; };
+		757ADC4623A1047600D708D2 /* HardwareKeyPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 757ADC4423A1047600D708D2 /* HardwareKeyPicker.swift */; };
+		757ADC4723A1047600D708D2 /* HardwareKeyPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 757ADC4423A1047600D708D2 /* HardwareKeyPicker.swift */; };
+		757ADC4823A1047600D708D2 /* HardwareKeyPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 757ADC4423A1047600D708D2 /* HardwareKeyPicker.swift */; };
+		757ADC4A23A12DD100D708D2 /* HardwareKeyPicker.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 757ADC4923A12DD100D708D2 /* HardwareKeyPicker.storyboard */; };
+		757ADC4B23A12DD100D708D2 /* HardwareKeyPicker.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 757ADC4923A12DD100D708D2 /* HardwareKeyPicker.storyboard */; };
+		757ADC4C23A12DD100D708D2 /* HardwareKeyPicker.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 757ADC4923A12DD100D708D2 /* HardwareKeyPicker.storyboard */; };
+		757ADC4D23A12DD100D708D2 /* HardwareKeyPicker.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 757ADC4923A12DD100D708D2 /* HardwareKeyPicker.storyboard */; };
 		757ADC5023A5180600D708D2 /* DismissablePopover.swift in Sources */ = {isa = PBXBuildFile; fileRef = 757ADC4E23A5180600D708D2 /* DismissablePopover.swift */; };
 		757ADC5123A5180600D708D2 /* DismissablePopover.swift in Sources */ = {isa = PBXBuildFile; fileRef = 757ADC4E23A5180600D708D2 /* DismissablePopover.swift */; };
 		757ADC5223A5180600D708D2 /* DismissablePopover.swift in Sources */ = {isa = PBXBuildFile; fileRef = 757ADC4E23A5180600D708D2 /* DismissablePopover.swift */; };
@@ -589,6 +605,9 @@
 		754BF08C2082A6BD00E3A292 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
 		754BF08E2082A6BD00E3A292 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
 		754BF09920984DEE00E3A292 /* UnlockDatabaseVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnlockDatabaseVC.swift; sourceTree = "<group>"; };
+		754D7C08238C2135005652E1 /* KeyFileTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyFileTextField.swift; sourceTree = "<group>"; };
+		754D7C18238C2C0E005652E1 /* libYubiKit.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; path = libYubiKit.a; sourceTree = BUILT_PRODUCTS_DIR; };
+		754D7C1E238C384A005652E1 /* KeePassium-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "KeePassium-Bridging-Header.h"; sourceTree = "<group>"; };
 		754DE31120A5C4A4002ED5F9 /* ChooseKeyFileVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChooseKeyFileVC.swift; sourceTree = "<group>"; };
 		7550D22A215CC99100360E0C /* ChangeMasterKeyVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChangeMasterKeyVC.swift; sourceTree = "<group>"; };
 		7550D231215D758800360E0C /* AuthenticationServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AuthenticationServices.framework; path = System/Library/Frameworks/AuthenticationServices.framework; sourceTree = SDKROOT; };
@@ -596,6 +615,7 @@
 		75514A3320DF5B6200202340 /* EditEntryFieldsCells.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditEntryFieldsCells.swift; sourceTree = "<group>"; };
 		75530C8E233E603000EC557E /* KeePassiumLibPro.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = KeePassiumLibPro.framework; sourceTree = BUILT_PRODUCTS_DIR; };
 		75530C94233E604E00EC557E /* KeePassiumLibPro.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = KeePassiumLibPro.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+		7558D06123CE2F3E00547621 /* ChallengeResponseManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChallengeResponseManager.swift; sourceTree = "<group>"; };
 		755B862F23054FA200E9FC83 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/AboutVC.storyboard; sourceTree = "<group>"; };
 		755B863323054FAD00E9FC83 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/PasscodeInputVC.storyboard; sourceTree = "<group>"; };
 		755B863623054FBF00E9FC83 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/ViewDiagnosticsVC.storyboard; sourceTree = "<group>"; };
@@ -637,6 +657,8 @@
 		7578487C22C02A9100BBEF9A /* UITableViewCell+demoEditActions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UITableViewCell+demoEditActions.swift"; sourceTree = "<group>"; };
 		7578488522C165C900BBEF9A /* SettingsBackupVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsBackupVC.swift; sourceTree = "<group>"; };
 		7578488E22C1DFF400BBEF9A /* SettingsBackupTimeoutPickerVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsBackupTimeoutPickerVC.swift; sourceTree = "<group>"; };
+		757ADC4423A1047600D708D2 /* HardwareKeyPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HardwareKeyPicker.swift; sourceTree = "<group>"; };
+		757ADC4923A12DD100D708D2 /* HardwareKeyPicker.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = HardwareKeyPicker.storyboard; sourceTree = "<group>"; };
 		757ADC4E23A5180600D708D2 /* DismissablePopover.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DismissablePopover.swift; sourceTree = "<group>"; };
 		757ADC4F23A5180600D708D2 /* DismissableNavigationController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DismissableNavigationController.swift; sourceTree = "<group>"; };
 		757D336020EF382D000A47F0 /* SettingsDatabaseTimeoutVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsDatabaseTimeoutVC.swift; sourceTree = "<group>"; };
@@ -1051,6 +1073,7 @@
 			isa = PBXFrameworksBuildPhase;
 			buildActionMask = 2147483647;
 			files = (
+				754D7C1C238C3553005652E1 /* libYubiKit.a in Frameworks */,
 				75FEC2232175292C0034334B /* KeePassiumLib.framework in Frameworks */,
 				7550D242215D792000360E0C /* AuthenticationServices.framework in Frameworks */,
 				75CD363F21521FAF0018AF46 /* LocalAuthentication.framework in Frameworks */,
@@ -1062,6 +1085,7 @@
 			isa = PBXFrameworksBuildPhase;
 			buildActionMask = 2147483647;
 			files = (
+				754D7C1D238C355B005652E1 /* libYubiKit.a in Frameworks */,
 				75B38C4D233E5B7C006BDEFC /* AuthenticationServices.framework in Frameworks */,
 				7598963F2341590300DF39AA /* KeePassiumLib.framework in Frameworks */,
 				75B38C4E233E5B7C006BDEFC /* LocalAuthentication.framework in Frameworks */,
@@ -1084,6 +1108,7 @@
 		16DE7C3A873A37E1B4636309 /* Frameworks */ = {
 			isa = PBXGroup;
 			children = (
+				754D7C18238C2C0E005652E1 /* libYubiKit.a */,
 				751146E5233F9E2900443BCD /* KeePassiumLib.framework */,
 				751146E2233F9E1500443BCD /* KeePassiumLib.framework */,
 				75530C94233E604E00EC557E /* KeePassiumLibPro.framework */,
@@ -1179,6 +1204,7 @@
 		754BF0722082A6BD00E3A292 = {
 			isa = PBXGroup;
 			children = (
+				754D7C1E238C384A005652E1 /* KeePassium-Bridging-Header.h */,
 				754BF07D2082A6BD00E3A292 /* KeePassium */,
 				750CFBC921BA9ACD00E62B09 /* KeePassium AutoFill */,
 				754BF07C2082A6BD00E3A292 /* Products */,
@@ -1201,6 +1227,7 @@
 			isa = PBXGroup;
 			children = (
 				758BF0D920D21EE500897C83 /* general */,
+				757ADC4323A1045C00D708D2 /* challenge-response */,
 				756FF64720BAB56A00FF890B /* database */,
 				753F9247229055E1005E496F /* premium */,
 				7546FBC420C1024200C5D834 /* settings */,
@@ -1342,6 +1369,16 @@
 			path = backup;
 			sourceTree = "<group>";
 		};
+		757ADC4323A1045C00D708D2 /* challenge-response */ = {
+			isa = PBXGroup;
+			children = (
+				7558D06123CE2F3E00547621 /* ChallengeResponseManager.swift */,
+				757ADC4423A1047600D708D2 /* HardwareKeyPicker.swift */,
+				757ADC4923A12DD100D708D2 /* HardwareKeyPicker.storyboard */,
+			);
+			path = "challenge-response";
+			sourceTree = "<group>";
+		};
 		758BF0D920D21EE500897C83 /* general */ = {
 			isa = PBXGroup;
 			children = (
@@ -1383,6 +1420,7 @@
 				7561850220E033DB00EEFAEE /* WatchdogAwareTextView.swift */,
 				7548BC1721B07BFE001B00E9 /* ProtectedTextField.swift */,
 				7573FD8A2321664F00DCDA5A /* MultilineButton.swift */,
+				754D7C08238C2135005652E1 /* KeyFileTextField.swift */,
 				757ADC4F23A5180600D708D2 /* DismissableNavigationController.swift */,
 				757ADC4E23A5180600D708D2 /* DismissablePopover.swift */,
 			);
@@ -1612,6 +1650,7 @@
 				751E0D352309AFFC0088BB10 /* EntryFinderVC.storyboard in Resources */,
 				751E0D3B2309B01A0088BB10 /* DiagnosticsViewerVC.storyboard in Resources */,
 				75802632222EBC3800F4525F /* ProgressVC.storyboard in Resources */,
+				757ADC4B23A12DD100D708D2 /* HardwareKeyPicker.storyboard in Resources */,
 				750CFBCE21BA9ACD00E62B09 /* MainInterface.storyboard in Resources */,
 				755B863923054FC700E9FC83 /* WelcomeVC.storyboard in Resources */,
 				755E7D9F232008300010E199 /* Localizable.strings in Resources */,
@@ -1629,6 +1668,7 @@
 				7533030F23C52A1500F38CC7 /* DestinationGroupPickerVC.storyboard in Resources */,
 				751E0D102309AF4C0088BB10 /* ViewEntryHistoryVC.storyboard in Resources */,
 				751E0D132309AF540088BB10 /* ChooseDatabaseVC.storyboard in Resources */,
+				757ADC4A23A12DD100D708D2 /* HardwareKeyPicker.storyboard in Resources */,
 				755B862E23054FA200E9FC83 /* AboutVC.storyboard in Resources */,
 				754BF08D2082A6BD00E3A292 /* LaunchScreen.storyboard in Resources */,
 				751E0CEF2309AE920088BB10 /* SettingsBackupVC.storyboard in Resources */,
@@ -1678,6 +1718,7 @@
 				7533031023C52A1500F38CC7 /* DestinationGroupPickerVC.storyboard in Resources */,
 				75B38C51233E5B7C006BDEFC /* ViewEntryHistoryVC.storyboard in Resources */,
 				75B38C52233E5B7C006BDEFC /* ChooseDatabaseVC.storyboard in Resources */,
+				757ADC4C23A12DD100D708D2 /* HardwareKeyPicker.storyboard in Resources */,
 				75B38C53233E5B7C006BDEFC /* AboutVC.storyboard in Resources */,
 				75B38C54233E5B7C006BDEFC /* LaunchScreen.storyboard in Resources */,
 				75B38C55233E5B7C006BDEFC /* SettingsBackupVC.storyboard in Resources */,
@@ -1732,6 +1773,7 @@
 				75D7DFB0233E5E310029AD87 /* EntryFinderVC.storyboard in Resources */,
 				75D7DFB1233E5E310029AD87 /* DiagnosticsViewerVC.storyboard in Resources */,
 				75D7DFB3233E5E310029AD87 /* ProgressVC.storyboard in Resources */,
+				757ADC4D23A12DD100D708D2 /* HardwareKeyPicker.storyboard in Resources */,
 				75D7DFB4233E5E310029AD87 /* MainInterface.storyboard in Resources */,
 				75D7DFB5233E5E310029AD87 /* WelcomeVC.storyboard in Resources */,
 				75D7DFB6233E5E310029AD87 /* Localizable.strings in Resources */,
@@ -1799,6 +1841,7 @@
 				757FB3CF21F9C51C00F016FA /* UITextView+border.swift in Sources */,
 				75CBAB9F21C1671F00B8AFE2 /* MainCoordinator.swift in Sources */,
 				7514CC9F22D5D14B00A0D7A4 /* LongPressAwareNavigationController.swift in Sources */,
+				757ADC4623A1047600D708D2 /* HardwareKeyPicker.swift in Sources */,
 				75679ACF22FD897000372301 /* NewsCenter.swift in Sources */,
 				75D3A3B121BAE66F00255FC3 /* DatabaseFileListCell.swift in Sources */,
 				7523F71521C94DD90025C706 /* DiagnosticsViewerVC.swift in Sources */,
@@ -1819,6 +1862,7 @@
 				75E446CA2330307300E24E70 /* Xcode11GM_LocalizedLabel.swift in Sources */,
 				75802635222ED11D00F4525F /* ProgressVC.swift in Sources */,
 				75C11F1F22208D6B0042FB2B /* LAContext+biometrics.swift in Sources */,
+				754D7C0A238C2135005652E1 /* KeyFileTextField.swift in Sources */,
 				757FB3CC21F9B4AF00F016FA /* ProtectedTextField.swift in Sources */,
 				750CFBDB21BAD7FF00E62B09 /* UIAlertController+extensions.swift in Sources */,
 				75CBAB9D21C166D600B8AFE2 /* Coordinator.swift in Sources */,
@@ -1888,6 +1932,7 @@
 				754DE31220A5C4A4002ED5F9 /* ChooseKeyFileVC.swift in Sources */,
 				757D336320EF5081000A47F0 /* SettingsClipboardTimeoutVC.swift in Sources */,
 				75B5FB7920D6101100D402D9 /* ItemCategory.swift in Sources */,
+				754D7C09238C2135005652E1 /* KeyFileTextField.swift in Sources */,
 				75D1674A21464333005E33C3 /* AboutVC.swift in Sources */,
 				750C683220D767F6001A6AA0 /* EditEntryVC.swift in Sources */,
 				75151D3323268AB300F3C411 /* UILabel+strikethrough.swift in Sources */,
@@ -1937,7 +1982,9 @@
 				75C5C3762129DE0800CD7FE3 /* UIAlertController+extensions.swift in Sources */,
 				75A5312823C0DFE900900ED1 /* ItemRelocationCoordinator.swift in Sources */,
 				75F4ACFC21776F4800B22D8B /* LString.swift in Sources */,
+				7558D06223CE2F3E00547621 /* ChallengeResponseManager.swift in Sources */,
 				750D127E20CF071600BE0CCE /* EditGroupVC.swift in Sources */,
+				757ADC4523A1047600D708D2 /* HardwareKeyPicker.swift in Sources */,
 				75C94C5B20B4B8C2004FA29D /* ViewEntryVC.swift in Sources */,
 				7578487D22C02A9100BBEF9A /* UITableViewCell+demoEditActions.swift in Sources */,
 				75779864223023D3006671BD /* ProgressViewHost.swift in Sources */,
@@ -1994,6 +2041,7 @@
 				75B38C16233E5B7C006BDEFC /* ChooseKeyFileVC.swift in Sources */,
 				75B38C17233E5B7C006BDEFC /* SettingsClipboardTimeoutVC.swift in Sources */,
 				75B38C18233E5B7C006BDEFC /* ItemCategory.swift in Sources */,
+				754D7C0B238C2135005652E1 /* KeyFileTextField.swift in Sources */,
 				75B38C19233E5B7C006BDEFC /* AboutVC.swift in Sources */,
 				75B38C1A233E5B7C006BDEFC /* EditEntryVC.swift in Sources */,
 				75B38C1B233E5B7C006BDEFC /* UILabel+strikethrough.swift in Sources */,
@@ -2043,7 +2091,9 @@
 				75B38C42233E5B7C006BDEFC /* UIAlertController+extensions.swift in Sources */,
 				75A5312923C0DFE900900ED1 /* ItemRelocationCoordinator.swift in Sources */,
 				75B38C43233E5B7C006BDEFC /* LString.swift in Sources */,
+				7558D06323CE2F4100547621 /* ChallengeResponseManager.swift in Sources */,
 				75B38C44233E5B7C006BDEFC /* EditGroupVC.swift in Sources */,
+				757ADC4723A1047600D708D2 /* HardwareKeyPicker.swift in Sources */,
 				75B38C46233E5B7C006BDEFC /* ViewEntryVC.swift in Sources */,
 				75B38C47233E5B7C006BDEFC /* UITableViewCell+demoEditActions.swift in Sources */,
 				75B38C48233E5B7C006BDEFC /* ProgressViewHost.swift in Sources */,
@@ -2069,6 +2119,7 @@
 				75D7DF82233E5E310029AD87 /* UITextView+border.swift in Sources */,
 				75D7DF83233E5E310029AD87 /* MainCoordinator.swift in Sources */,
 				75D7DF84233E5E310029AD87 /* LongPressAwareNavigationController.swift in Sources */,
+				757ADC4823A1047600D708D2 /* HardwareKeyPicker.swift in Sources */,
 				75D7DF85233E5E310029AD87 /* NewsCenter.swift in Sources */,
 				75D7DF86233E5E310029AD87 /* DatabaseFileListCell.swift in Sources */,
 				75D7DF87233E5E310029AD87 /* DiagnosticsViewerVC.swift in Sources */,
@@ -2089,6 +2140,7 @@
 				75D7DF94233E5E310029AD87 /* Xcode11GM_LocalizedLabel.swift in Sources */,
 				75D7DF95233E5E310029AD87 /* ProgressVC.swift in Sources */,
 				75D7DF96233E5E310029AD87 /* LAContext+biometrics.swift in Sources */,
+				754D7C0C238C2135005652E1 /* KeyFileTextField.swift in Sources */,
 				75D7DF97233E5E310029AD87 /* ProtectedTextField.swift in Sources */,
 				75D7DF98233E5E310029AD87 /* UIAlertController+extensions.swift in Sources */,
 				75D7DF99233E5E310029AD87 /* Coordinator.swift in Sources */,
@@ -2869,7 +2921,7 @@
 				CLANG_ENABLE_OBJC_WEAK = YES;
 				CODE_SIGN_ENTITLEMENTS = "KeePassium AutoFill/KeePassium_AutoFill.entitlements";
 				CODE_SIGN_STYLE = Automatic;
-				CURRENT_PROJECT_VERSION = 49;
+				CURRENT_PROJECT_VERSION = 50;
 				DEVELOPMENT_TEAM = 84W4PHXS24;
 				INFOPLIST_FILE = "KeePassium AutoFill/Info.plist";
 				IPHONEOS_DEPLOYMENT_TARGET = 12.0;
@@ -2893,7 +2945,7 @@
 				CLANG_ENABLE_OBJC_WEAK = YES;
 				CODE_SIGN_ENTITLEMENTS = "KeePassium AutoFill/KeePassium_AutoFill.entitlements";
 				CODE_SIGN_STYLE = Automatic;
-				CURRENT_PROJECT_VERSION = 49;
+				CURRENT_PROJECT_VERSION = 50;
 				DEVELOPMENT_TEAM = 84W4PHXS24;
 				INFOPLIST_FILE = "KeePassium AutoFill/Info.plist";
 				IPHONEOS_DEPLOYMENT_TARGET = 12.0;
@@ -2962,9 +3014,11 @@
 				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
 				GCC_WARN_UNUSED_FUNCTION = YES;
 				GCC_WARN_UNUSED_VARIABLE = YES;
+				HEADER_SEARCH_PATHS = "";
 				IPHONEOS_DEPLOYMENT_TARGET = 11.0;
 				MTL_ENABLE_DEBUG_INFO = YES;
 				ONLY_ACTIVE_ARCH = YES;
+				OTHER_LDFLAGS = "";
 				SDKROOT = iphoneos;
 				SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
 				SWIFT_ENABLE_BATCH_MODE = NO;
@@ -3018,8 +3072,10 @@
 				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
 				GCC_WARN_UNUSED_FUNCTION = YES;
 				GCC_WARN_UNUSED_VARIABLE = YES;
+				HEADER_SEARCH_PATHS = "";
 				IPHONEOS_DEPLOYMENT_TARGET = 11.0;
 				MTL_ENABLE_DEBUG_INFO = NO;
+				OTHER_LDFLAGS = "";
 				SDKROOT = iphoneos;
 				SWIFT_ENABLE_BATCH_MODE = NO;
 				SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
@@ -3034,19 +3090,21 @@
 				ASSETCATALOG_COMPILER_APPICON_NAME = "app-icon";
 				CODE_SIGN_ENTITLEMENTS = KeePassium/KeePassium.entitlements;
 				CODE_SIGN_STYLE = Automatic;
-				CURRENT_PROJECT_VERSION = 49;
+				CURRENT_PROJECT_VERSION = 50;
 				DEVELOPMENT_TEAM = 84W4PHXS24;
 				GCC_C_LANGUAGE_STANDARD = gnu11;
 				GCC_OPTIMIZATION_LEVEL = fast;
 				GCC_UNROLL_LOOPS = YES;
+				HEADER_SEARCH_PATHS = "../YubiKit/**";
 				INFOPLIST_FILE = KeePassium/Info.plist;
 				IPHONEOS_DEPLOYMENT_TARGET = 11.0;
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
 				MARKETING_VERSION = 1.09;
+				OTHER_LDFLAGS = "-ObjC";
 				PRODUCT_BUNDLE_IDENTIFIER = com.keepassium.ios;
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG MAIN_APP";
-				SWIFT_OBJC_BRIDGING_HEADER = "";
+				SWIFT_OBJC_BRIDGING_HEADER = "KeePassium-Bridging-Header.h";
 				SWIFT_VERSION = 4.2;
 				TARGETED_DEVICE_FAMILY = "1,2";
 			};
@@ -3059,19 +3117,21 @@
 				ASSETCATALOG_COMPILER_APPICON_NAME = "app-icon";
 				CODE_SIGN_ENTITLEMENTS = KeePassium/KeePassium.entitlements;
 				CODE_SIGN_STYLE = Automatic;
-				CURRENT_PROJECT_VERSION = 49;
+				CURRENT_PROJECT_VERSION = 50;
 				DEVELOPMENT_TEAM = 84W4PHXS24;
 				GCC_C_LANGUAGE_STANDARD = gnu11;
 				GCC_OPTIMIZATION_LEVEL = fast;
 				GCC_UNROLL_LOOPS = YES;
+				HEADER_SEARCH_PATHS = "../YubiKit/**";
 				INFOPLIST_FILE = KeePassium/Info.plist;
 				IPHONEOS_DEPLOYMENT_TARGET = 11.0;
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
 				MARKETING_VERSION = 1.09;
+				OTHER_LDFLAGS = "-ObjC";
 				PRODUCT_BUNDLE_IDENTIFIER = com.keepassium.ios;
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				SWIFT_ACTIVE_COMPILATION_CONDITIONS = MAIN_APP;
-				SWIFT_OBJC_BRIDGING_HEADER = "";
+				SWIFT_OBJC_BRIDGING_HEADER = "KeePassium-Bridging-Header.h";
 				SWIFT_VERSION = 4.2;
 				TARGETED_DEVICE_FAMILY = "1,2";
 			};
@@ -3084,19 +3144,21 @@
 				ASSETCATALOG_COMPILER_APPICON_NAME = "app-icon-pro";
 				CODE_SIGN_ENTITLEMENTS = KeePassium/KeePassium.entitlements;
 				CODE_SIGN_STYLE = Automatic;
-				CURRENT_PROJECT_VERSION = 49;
+				CURRENT_PROJECT_VERSION = 50;
 				DEVELOPMENT_TEAM = 84W4PHXS24;
 				GCC_C_LANGUAGE_STANDARD = gnu11;
 				GCC_OPTIMIZATION_LEVEL = fast;
 				GCC_UNROLL_LOOPS = YES;
+				HEADER_SEARCH_PATHS = "../YubiKit/**";
 				INFOPLIST_FILE = "$(SRCROOT)/KeePassium/Info.plist";
 				IPHONEOS_DEPLOYMENT_TARGET = 11.0;
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
 				MARKETING_VERSION = 1.09;
+				OTHER_LDFLAGS = "-ObjC";
 				PRODUCT_BUNDLE_IDENTIFIER = com.keepassium.ios.pro;
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG MAIN_APP PREPAID_VERSION";
-				SWIFT_OBJC_BRIDGING_HEADER = "";
+				SWIFT_OBJC_BRIDGING_HEADER = "KeePassium-Bridging-Header.h";
 				SWIFT_VERSION = 4.2;
 				TARGETED_DEVICE_FAMILY = "1,2";
 			};
@@ -3109,19 +3171,21 @@
 				ASSETCATALOG_COMPILER_APPICON_NAME = "app-icon-pro";
 				CODE_SIGN_ENTITLEMENTS = KeePassium/KeePassium.entitlements;
 				CODE_SIGN_STYLE = Automatic;
-				CURRENT_PROJECT_VERSION = 49;
+				CURRENT_PROJECT_VERSION = 50;
 				DEVELOPMENT_TEAM = 84W4PHXS24;
 				GCC_C_LANGUAGE_STANDARD = gnu11;
 				GCC_OPTIMIZATION_LEVEL = fast;
 				GCC_UNROLL_LOOPS = YES;
+				HEADER_SEARCH_PATHS = "../YubiKit/**";
 				INFOPLIST_FILE = "$(SRCROOT)/KeePassium/Info.plist";
 				IPHONEOS_DEPLOYMENT_TARGET = 11.0;
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
 				MARKETING_VERSION = 1.09;
+				OTHER_LDFLAGS = "-ObjC";
 				PRODUCT_BUNDLE_IDENTIFIER = com.keepassium.ios.pro;
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				SWIFT_ACTIVE_COMPILATION_CONDITIONS = "MAIN_APP PREPAID_VERSION";
-				SWIFT_OBJC_BRIDGING_HEADER = "";
+				SWIFT_OBJC_BRIDGING_HEADER = "KeePassium-Bridging-Header.h";
 				SWIFT_VERSION = 4.2;
 				TARGETED_DEVICE_FAMILY = "1,2";
 			};
@@ -3133,7 +3197,7 @@
 				CLANG_ENABLE_OBJC_WEAK = YES;
 				CODE_SIGN_ENTITLEMENTS = "KeePassium AutoFill/KeePassium_AutoFill.entitlements";
 				CODE_SIGN_STYLE = Automatic;
-				CURRENT_PROJECT_VERSION = 49;
+				CURRENT_PROJECT_VERSION = 50;
 				DEVELOPMENT_TEAM = 84W4PHXS24;
 				INFOPLIST_FILE = "$(SRCROOT)/KeePassium AutoFill/Info.plist";
 				IPHONEOS_DEPLOYMENT_TARGET = 12.0;
@@ -3157,7 +3221,7 @@
 				CLANG_ENABLE_OBJC_WEAK = YES;
 				CODE_SIGN_ENTITLEMENTS = "KeePassium AutoFill/KeePassium_AutoFill.entitlements";
 				CODE_SIGN_STYLE = Automatic;
-				CURRENT_PROJECT_VERSION = 49;
+				CURRENT_PROJECT_VERSION = 50;
 				DEVELOPMENT_TEAM = 84W4PHXS24;
 				INFOPLIST_FILE = "$(SRCROOT)/KeePassium AutoFill/Info.plist";
 				IPHONEOS_DEPLOYMENT_TARGET = 12.0;
diff --git a/KeePassium.xcworkspace/contents.xcworkspacedata b/KeePassium.xcworkspace/contents.xcworkspacedata
index 5f9f162ef..27675a53b 100755
--- a/KeePassium.xcworkspace/contents.xcworkspacedata
+++ b/KeePassium.xcworkspace/contents.xcworkspacedata
@@ -4,6 +4,9 @@
    <FileRef
       location = "group:/Users/andrei/Dropbox/Projects/XCode/KeePassium/KeePassiumLib/KeePassiumLib.xcodeproj">
    </FileRef>
+   <FileRef
+      location = "group:YubiKit/YubiKit.xcodeproj">
+   </FileRef>
    <FileRef
       location = "group:KeePassium.xcodeproj">
    </FileRef>
diff --git a/KeePassium/Assets.xcassets/yubikey/Contents.json b/KeePassium/Assets.xcassets/yubikey/Contents.json
new file mode 100644
index 000000000..da4a164c9
--- /dev/null
+++ b/KeePassium/Assets.xcassets/yubikey/Contents.json
@@ -0,0 +1,6 @@
+{
+  "info" : {
+    "version" : 1,
+    "author" : "xcode"
+  }
+}
\ No newline at end of file
diff --git a/KeePassium/Assets.xcassets/yubikey/yubikey-off-accessory.imageset/Contents.json b/KeePassium/Assets.xcassets/yubikey/yubikey-off-accessory.imageset/Contents.json
new file mode 100644
index 000000000..be9d3edfb
--- /dev/null
+++ b/KeePassium/Assets.xcassets/yubikey/yubikey-off-accessory.imageset/Contents.json
@@ -0,0 +1,26 @@
+{
+  "images" : [
+    {
+      "idiom" : "universal",
+      "filename" : "Usb Memory Stick.png",
+      "scale" : "1x"
+    },
+    {
+      "idiom" : "universal",
+      "filename" : "Usb Memory Stick-1.png",
+      "scale" : "2x"
+    },
+    {
+      "idiom" : "universal",
+      "filename" : "Usb Memory Stick-2.png",
+      "scale" : "3x"
+    }
+  ],
+  "info" : {
+    "version" : 1,
+    "author" : "xcode"
+  },
+  "properties" : {
+    "template-rendering-intent" : "template"
+  }
+}
\ No newline at end of file
diff --git a/KeePassium/Assets.xcassets/yubikey/yubikey-off-accessory.imageset/Usb Memory Stick-1.png b/KeePassium/Assets.xcassets/yubikey/yubikey-off-accessory.imageset/Usb Memory Stick-1.png
new file mode 100644
index 000000000..d0662111f
Binary files /dev/null and b/KeePassium/Assets.xcassets/yubikey/yubikey-off-accessory.imageset/Usb Memory Stick-1.png differ
diff --git a/KeePassium/Assets.xcassets/yubikey/yubikey-off-accessory.imageset/Usb Memory Stick-2.png b/KeePassium/Assets.xcassets/yubikey/yubikey-off-accessory.imageset/Usb Memory Stick-2.png
new file mode 100644
index 000000000..b34409df2
Binary files /dev/null and b/KeePassium/Assets.xcassets/yubikey/yubikey-off-accessory.imageset/Usb Memory Stick-2.png differ
diff --git a/KeePassium/Assets.xcassets/yubikey/yubikey-off-accessory.imageset/Usb Memory Stick.png b/KeePassium/Assets.xcassets/yubikey/yubikey-off-accessory.imageset/Usb Memory Stick.png
new file mode 100644
index 000000000..1458fa919
Binary files /dev/null and b/KeePassium/Assets.xcassets/yubikey/yubikey-off-accessory.imageset/Usb Memory Stick.png differ
diff --git a/KeePassium/Assets.xcassets/yubikey/yubikey-on-accessory.imageset/Contents.json b/KeePassium/Assets.xcassets/yubikey/yubikey-on-accessory.imageset/Contents.json
new file mode 100644
index 000000000..be9d3edfb
--- /dev/null
+++ b/KeePassium/Assets.xcassets/yubikey/yubikey-on-accessory.imageset/Contents.json
@@ -0,0 +1,26 @@
+{
+  "images" : [
+    {
+      "idiom" : "universal",
+      "filename" : "Usb Memory Stick.png",
+      "scale" : "1x"
+    },
+    {
+      "idiom" : "universal",
+      "filename" : "Usb Memory Stick-1.png",
+      "scale" : "2x"
+    },
+    {
+      "idiom" : "universal",
+      "filename" : "Usb Memory Stick-2.png",
+      "scale" : "3x"
+    }
+  ],
+  "info" : {
+    "version" : 1,
+    "author" : "xcode"
+  },
+  "properties" : {
+    "template-rendering-intent" : "template"
+  }
+}
\ No newline at end of file
diff --git a/KeePassium/Assets.xcassets/yubikey/yubikey-on-accessory.imageset/Usb Memory Stick-1.png b/KeePassium/Assets.xcassets/yubikey/yubikey-on-accessory.imageset/Usb Memory Stick-1.png
new file mode 100644
index 000000000..57bbb0b6f
Binary files /dev/null and b/KeePassium/Assets.xcassets/yubikey/yubikey-on-accessory.imageset/Usb Memory Stick-1.png differ
diff --git a/KeePassium/Assets.xcassets/yubikey/yubikey-on-accessory.imageset/Usb Memory Stick-2.png b/KeePassium/Assets.xcassets/yubikey/yubikey-on-accessory.imageset/Usb Memory Stick-2.png
new file mode 100644
index 000000000..d3978b14b
Binary files /dev/null and b/KeePassium/Assets.xcassets/yubikey/yubikey-on-accessory.imageset/Usb Memory Stick-2.png differ
diff --git a/KeePassium/Assets.xcassets/yubikey/yubikey-on-accessory.imageset/Usb Memory Stick.png b/KeePassium/Assets.xcassets/yubikey/yubikey-on-accessory.imageset/Usb Memory Stick.png
new file mode 100644
index 000000000..9670aaa77
Binary files /dev/null and b/KeePassium/Assets.xcassets/yubikey/yubikey-on-accessory.imageset/Usb Memory Stick.png differ
diff --git a/KeePassium/Info.plist b/KeePassium/Info.plist
index 05f8d8bdb..6ee2fedce 100644
--- a/KeePassium/Info.plist
+++ b/KeePassium/Info.plist
@@ -2,6 +2,19 @@
 <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
 <plist version="1.0">
 <dict>
+	<key>UISupportedExternalAccessoryProtocols</key>
+	<array>
+		<string>com.yubico.ylp</string>
+	</array>
+	<key>com.apple.developer.nfc.readersession.iso7816.select-identifiers</key>
+	<array>
+		<string>A000000527471117</string>
+		<string>A0000006472F0001</string>
+		<string>A0000005272101</string>
+		<string>A000000308</string>
+		<string>A000000527200101</string>
+        <string>A0000005272001</string>
+	</array>
 	<key>CFBundleDevelopmentRegion</key>
 	<string>$(DEVELOPMENT_LANGUAGE)</string>
 	<key>CFBundleDisplayName</key>
@@ -80,6 +93,8 @@
 	<string>Use FaceID to unlock KeePassium</string>
 	<key>NSPhotoLibraryAddUsageDescription</key>
 	<string>KeePassium needs permission to export files to your Photo Library.</string>
+    <key>NFCReaderUsageDescription</key>
+    <string>KeePassium needs access to NFC to communicate with your YubiKey.</string>
 	<key>UIFileSharingEnabled</key>
 	<true/>
 	<key>UILaunchStoryboardName</key>
diff --git a/KeePassium/KeePassium.entitlements b/KeePassium/KeePassium.entitlements
index 28084db5a..0987dde4e 100755
--- a/KeePassium/KeePassium.entitlements
+++ b/KeePassium/KeePassium.entitlements
@@ -16,6 +16,11 @@
 	<array>
 		<string>CloudDocuments</string>
 	</array>
+	<key>com.apple.developer.nfc.readersession.formats</key>
+	<array>
+		<string>NDEF</string>
+		<string>TAG</string>
+	</array>
 	<key>com.apple.developer.ubiquity-container-identifiers</key>
 	<array>
 		<string>iCloud.$(CFBundleIdentifier)</string>
diff --git a/KeePassium/challenge-response/ChallengeResponseManager.swift b/KeePassium/challenge-response/ChallengeResponseManager.swift
new file mode 100644
index 000000000..9515ba800
--- /dev/null
+++ b/KeePassium/challenge-response/ChallengeResponseManager.swift
@@ -0,0 +1,391 @@
+//  KeePassium Password Manager
+//  Copyright © 2018–2019 Andrei Popleteev <info@keepassium.com>
+//
+//  This program is free software: you can redistribute it and/or modify it
+//  under the terms of the GNU General Public License version 3 as published
+//  by the Free Software Foundation: https://www.gnu.org/licenses/).
+//  For commercial licensing, please contact the author.
+
+import KeePassiumLib
+
+fileprivate let YUBIKEY_SUCCESS: UInt16 = 0x9000
+
+class ChallengeResponseManager {
+    static let instance = ChallengeResponseManager()
+    
+    private var accessorySessionStateObservation: NSKeyValueObservation?
+    private var accessoryConnectedStateObservation: NSKeyValueObservation?
+    private var nfcSessionStateObservation: NSKeyValueObservation?
+    
+    public private(set) var supportsNFC = false
+    public private(set) var supportsMFI = false
+    
+    private var challenge: SecureByteArray?
+    private var responseHandler: ResponseHandler?
+    private var currentKey: YubiKey?
+    private var isResponseSent = false
+    
+    private var queue: DispatchQueue
+        
+    private init() {
+        queue = DispatchQueue(label: "ChallengeResponseManager")
+        initSessionObservers()
+    }
+
+    deinit {
+        accessorySessionStateObservation = nil
+        accessoryConnectedStateObservation = nil
+        nfcSessionStateObservation = nil
+    }
+    
+    public static func makeHandler(for yubiKey: YubiKey?) -> ChallengeHandler? {
+        guard let yubiKey = yubiKey else {
+            Diag.debug("Challenge-response is not used")
+            return nil
+        }
+        let challengeHandler: ChallengeHandler = {
+            (challenge, responseHandler) in
+            instance.perform(
+                with: yubiKey,
+                challenge: challenge,
+                responseHandler: responseHandler
+            )
+        }
+        return challengeHandler
+    }
+    
+    
+    private func initSessionObservers() {
+        supportsMFI = YubiKitDeviceCapabilities.supportsMFIAccessoryKey
+        if supportsMFI {
+            initMFISessionObserver()
+        }
+        
+        guard #available(iOS 13.0, *) else { return }
+        supportsNFC = YubiKitDeviceCapabilities.supportsISO7816NFCTags
+        if supportsNFC {
+            initNFCSessionObserver()
+        }
+    }
+    
+    private func initMFISessionObserver() {
+        let accessorySession = YubiKitManager.shared.accessorySession as! YKFAccessorySession
+        accessorySessionStateObservation = accessorySession.observe(
+            \.sessionState,
+            changeHandler: {
+                [weak self] (session, observedChange) in
+                self?.accessorySessionStateDidChange()
+            }
+        )
+    }
+    
+    @available(iOS 13.0, *)
+    private func initNFCSessionObserver() {
+        let nfcSession = YubiKitManager.shared.nfcSession as! YKFNFCSession
+        nfcSessionStateObservation = nfcSession.observe(
+            \.iso7816SessionState,
+            changeHandler: { [weak self] (session, newValue) in
+                self?.nfcSessionStateDidChange()
+            }
+        )
+    }
+    
+
+
+    private func accessorySessionStateDidChange() {
+        let keySession = YubiKitManager.shared.accessorySession as! YKFAccessorySession
+        switch keySession.sessionState {
+        case .opening:
+            print("Accessory session -> opening")
+        case .open:
+            print("Accessory session -> open")
+            queue.async { [weak self] in
+                guard let key = self?.currentKey else { assertionFailure(); return }
+                self?.performChallengeResponse(keySession, slot: key.slot)
+                keySession.stopSessionSync()
+            }
+        case .closing:
+            print("Accessory session -> closing")
+        case .closed:
+            print("Accessory session -> closed")
+            accessoryConnectedStateObservation = nil 
+        }
+    }
+    
+    @available(iOS 13.0, *)
+    private func nfcSessionStateDidChange() {
+        let keySession = YubiKitManager.shared.nfcSession as! YKFNFCSession
+        switch keySession.iso7816SessionState {
+        case .opening:
+            print("NFC session -> opening")
+        case .open:
+            print("NFC session -> open")
+            queue.async { [weak self] in
+                guard let key = self?.currentKey else { assertionFailure(); return }
+                self?.performChallengeResponse(keySession, slot: key.slot)
+                keySession.stopIso7816Session()
+            }
+        case .pooling:
+            print("NFC session -> pooling")
+        case .closed:
+            print("NFC session -> closed")
+            if !isResponseSent {
+                returnError(.cancelled)
+            }
+        }
+    }
+    
+    
+    public func perform(
+        with yubiKey: YubiKey,
+        challenge: SecureByteArray,
+        responseHandler: @escaping ResponseHandler)
+    {
+        self.challenge = challenge.secureClone()
+        self.responseHandler = responseHandler
+        
+        isResponseSent = false
+        switch yubiKey.interface {
+        case .nfc:
+            startNFCSession(with: yubiKey, challenge: challenge, responseHandler: responseHandler)
+        case .mfi:
+            startMFISession(with: yubiKey, challenge: challenge, responseHandler: responseHandler)
+        }
+    }
+    
+    private func returnResponse(_ response: SecureByteArray) {
+        queue.async { [weak self] in
+            guard let self = self else { return }
+            self.responseHandler?(response, nil)
+            self.isResponseSent = true
+            self.cancel()
+        }
+    }
+
+    private func returnError(_ error: ChallengeResponseError) {
+        queue.async { [weak self] in
+            guard let self = self else { return }
+            self.responseHandler?(SecureByteArray(), error)
+            self.isResponseSent = true
+            self.cancel()
+        }
+    }
+    
+    
+    private func startMFISession(
+        with yubiKey: YubiKey,
+        challenge: SecureByteArray,
+        responseHandler: @escaping ResponseHandler)
+    {
+        guard supportsMFI else {
+            returnError(.notSupportedByDeviceOrSystem(interface: yubiKey.interface.description))
+            return
+        }
+        currentKey = yubiKey
+        let keySession = YubiKitManager.shared.accessorySession
+        if keySession.isKeyConnected {
+            keySession.startSessionSync()
+        } else {
+            returnError(.keyNotConnected)
+        }
+    }
+    
+    private func startNFCSession(
+        with yubiKey: YubiKey,
+        challenge: SecureByteArray,
+        responseHandler: @escaping ResponseHandler)
+    {
+        guard #available(iOS 13, *), supportsNFC else {
+            returnError(.notSupportedByDeviceOrSystem(interface: yubiKey.interface.description))
+            return
+        }
+        currentKey = yubiKey
+        let nfcSession = YubiKitManager.shared.nfcSession as! YKFNFCSession
+        Watchdog.shared.ignoreMinimizationOnce()
+        nfcSession.startIso7816Session()
+    }
+    
+    private func cancel() {
+        guard let currentKey = currentKey else {
+            challenge?.erase()
+            responseHandler = nil
+            return
+        }
+        
+        switch currentKey.interface {
+        case .mfi:
+            cancelMFISession()
+        case .nfc:
+            cancelNFCSession()
+        }
+        challenge?.erase()
+        responseHandler = nil
+        self.currentKey = nil
+    }
+    
+    private func cancelMFISession() {
+        let accessorySession = YubiKitManager.shared.accessorySession
+        if accessorySession.sessionState == .opening || accessorySession.sessionState == .open {
+            accessorySession.stopSession()
+        }
+    }
+    
+    private func cancelNFCSession() {
+        guard #available(iOS 13, *) else { assertionFailure(); return }
+    
+        let nfcSession = YubiKitManager.shared.nfcSession
+        nfcSession.cancelCommands()
+        nfcSession.stopIso7816Session()
+    }
+    
+    
+    private func performChallengeResponse(
+        _ accessorySession: YKFAccessorySession,
+        slot: YubiKey.Slot)
+    {
+        assert(accessorySession.sessionState == .open)
+        let keyName = accessorySession.accessoryDescription?.name ?? "(unknown)"
+        Diag.info("Connecting to \(keyName)")
+        guard let rawCommandService = accessorySession.rawCommandService else {
+            let message = "YubiKey raw command service is not available"
+            Diag.error(message)
+            returnError(.communicationError(message: message))
+            return
+        }
+        performChallengeResponse(rawCommandService: rawCommandService, slot: slot)
+    }
+    
+    @available(iOS 13.0, *)
+    private func performChallengeResponse(
+        _ nfcSession: YKFNFCSession,
+        slot: YubiKey.Slot)
+    {
+        assert(nfcSession.iso7816SessionState == .open)
+        let keyName = nfcSession.tagDescription?.identifier.description ?? "(unknown)"
+        Diag.info("Found NFC tag \(keyName)")
+        guard let rawCommandService = nfcSession.rawCommandService else {
+            let message = "YubiKey raw command service is not available"
+            Diag.error(message)
+            returnError(.communicationError(message: message))
+            return
+        }
+        performChallengeResponse(rawCommandService: rawCommandService, slot: slot)
+    }
+
+    
+    private func performChallengeResponse(
+        rawCommandService: YKFKeyRawCommandServiceProtocol,
+        slot: YubiKey.Slot)
+    {
+        let appletID = Data([0xA0, 0x00, 0x00, 0x05, 0x27, 0x20, 0x01])
+        guard let selectAppletAPDU =
+            YKFAPDU(cla: 0x00, ins: 0xA4, p1: 0x04, p2: 0x00, data: appletID, type: .short)
+            else { fatalError() }
+        
+        rawCommandService.executeSyncCommand(selectAppletAPDU) {
+            [weak self] (response, error) in
+            guard let self = self else { return }
+            if let error = error {
+                Diag.error("YubiKey select applet failed [message: \(error.localizedDescription)]")
+                self.returnError(.communicationError(message: error.localizedDescription))
+                return
+            }
+            
+            let responseParser = RawResponseParser(response: response!)
+            let statusCode = responseParser.statusCode
+            if statusCode == YUBIKEY_SUCCESS {
+                guard let responseData = responseParser.responseData else {
+                    let message = "YubiKey response is empty"
+                    Diag.error(message)
+                    self.returnError(.communicationError(message: message))
+                    return
+                }
+                let responseHexString = ByteArray(data: responseData).asHexString
+                print("Applet selection result: \(responseHexString)") 
+            } else {
+                let message = "YubiKey select applet failed with code \(String(format: "%04X", statusCode))"
+                Diag.error(message)
+                self.returnError(.communicationError(message: message))
+            }
+        }
+
+        guard var challengeBytes = challenge?.bytesCopy(),
+            challengeBytes.count <= 64
+            else { fatalError() }
+        
+        let paddingLength = 64 - challengeBytes.count
+        let pkcs7padding: [UInt8] = Array(repeating: UInt8(paddingLength), count: paddingLength)
+        challengeBytes.append(contentsOf: pkcs7padding)
+        let challengeData = Data(challengeBytes)
+            
+        let slotID = getSlotID(for: slot)
+        guard let chalRespAPDU =
+            YKFAPDU(cla: 0x00, ins: 0x01, p1: slotID, p2: 0x00, data: challengeData, type: .short)
+            else { fatalError() }
+        
+        rawCommandService.executeSyncCommand(chalRespAPDU) {
+            [weak self] (response, error) in
+            guard let self = self else { return }
+            if let error = error {
+                Diag.error("YubiKey error while executing command [message: \(error.localizedDescription)]")
+                self.returnError(.communicationError(message: error.localizedDescription))
+                return
+            }
+            
+            let responseParser = RawResponseParser(response: response!)
+            let statusCode = responseParser.statusCode
+            if statusCode == YUBIKEY_SUCCESS {
+                guard let responseData = responseParser.responseData else {
+                    let message = "YubiKey response is empty. Slot not configured?"
+                    Diag.error(message)
+                    self.returnError(.keyNotConfigured)
+                    return
+                }
+                let responseHexString = ByteArray(data: responseData).asHexString
+                print("Response: \(responseHexString)") 
+                let response = SecureByteArray(data: responseData)
+                self.returnResponse(response)
+            } else {
+                let message = "YubiKey challenge failed with code \(String(format: "%04X", statusCode))"
+                Diag.error(message)
+                self.returnError(.communicationError(message: message))
+            }
+        }
+    }
+    
+    private func getSlotID(for slot: YubiKey.Slot) -> UInt8 {
+        switch slot {
+        case .slot1:
+            return 0x30
+        case .slot2:
+            return 0x38
+        }
+    }
+}
+
+
+fileprivate class RawResponseParser {
+    private var response: Data
+
+    init(response: Data) {
+        self.response = response
+    }
+    
+    var statusCode: UInt16 {
+        get {
+            guard response.count >= 2 else {
+                return 0
+            }
+            return UInt16(response[response.count - 2]) << 8 + UInt16(response[response.count - 1])
+        }
+    }
+    
+    var responseData: Data? {
+        get {
+            guard response.count > 2 else {
+                return nil
+            }
+            return response.subdata(in: 0..<response.count - 2)
+        }
+    }
+}
diff --git a/KeePassium/challenge-response/HardwareKeyPicker.storyboard b/KeePassium/challenge-response/HardwareKeyPicker.storyboard
new file mode 100644
index 000000000..eb47ab9fb
--- /dev/null
+++ b/KeePassium/challenge-response/HardwareKeyPicker.storyboard
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="15702" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
+    <device id="retina4_0" orientation="portrait" appearance="light"/>
+    <dependencies>
+        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15704"/>
+        <capability name="Named colors" minToolsVersion="9.0"/>
+        <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
+    </dependencies>
+    <scenes>
+        <!--Choose Hardware Key-->
+        <scene sceneID="bxV-Vd-G1b">
+            <objects>
+                <tableViewController storyboardIdentifier="HardwareKeyPicker" title="Choose Hardware Key" id="fH6-bS-mDZ" customClass="HardwareKeyPicker" customModule="KeePassium" customModuleProvider="target" sceneMemberID="viewController">
+                    <tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="insetGrouped" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="18" sectionFooterHeight="18" id="RQr-72-zYs">
+                        <rect key="frame" x="0.0" y="0.0" width="320" height="568"/>
+                        <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+                        <prototypes>
+                            <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" reuseIdentifier="cell" textLabel="v2F-pa-TaY" style="IBUITableViewCellStyleDefault" id="5gB-go-AcK">
+                                <rect key="frame" x="16" y="55.5" width="288" height="43.5"/>
+                                <autoresizingMask key="autoresizingMask"/>
+                                <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="5gB-go-AcK" id="5PY-vX-Ieb">
+                                    <rect key="frame" x="0.0" y="0.0" width="288" height="43.5"/>
+                                    <autoresizingMask key="autoresizingMask"/>
+                                    <subviews>
+                                        <label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="{Title}" textAlignment="natural" lineBreakMode="wordWrap" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" id="v2F-pa-TaY">
+                                            <rect key="frame" x="15" y="0.0" width="258" height="43.5"/>
+                                            <autoresizingMask key="autoresizingMask"/>
+                                            <accessibility key="accessibilityConfiguration" hint="#bc-ignore!"/>
+                                            <fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
+                                            <color key="textColor" name="PrimaryText"/>
+                                            <nil key="highlightedColor"/>
+                                        </label>
+                                    </subviews>
+                                </tableViewCellContentView>
+                            </tableViewCell>
+                        </prototypes>
+                        <connections>
+                            <outlet property="dataSource" destination="fH6-bS-mDZ" id="3qh-Xw-FWo"/>
+                            <outlet property="delegate" destination="fH6-bS-mDZ" id="90J-Da-e5K"/>
+                        </connections>
+                    </tableView>
+                    <attributedString key="userComments">
+                        <fragment content="Title of the hardware key selector (e.g. for YubiKey)"/>
+                    </attributedString>
+                </tableViewController>
+                <placeholder placeholderIdentifier="IBFirstResponder" id="vJh-u7-gI9" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
+            </objects>
+            <point key="canvasLocation" x="-141" y="77"/>
+        </scene>
+    </scenes>
+    <resources>
+        <namedColor name="PrimaryText">
+            <color white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
+        </namedColor>
+    </resources>
+</document>
diff --git a/KeePassium/challenge-response/HardwareKeyPicker.swift b/KeePassium/challenge-response/HardwareKeyPicker.swift
new file mode 100644
index 000000000..d9dafd38c
--- /dev/null
+++ b/KeePassium/challenge-response/HardwareKeyPicker.swift
@@ -0,0 +1,207 @@
+//  KeePassium Password Manager
+//  Copyright © 2018–2019 Andrei Popleteev <info@keepassium.com>
+//
+//  This program is free software: you can redistribute it and/or modify it
+//  under the terms of the GNU General Public License version 3 as published
+//  by the Free Software Foundation: https://www.gnu.org/licenses/).
+//  For commercial licensing, please contact the author.
+
+import KeePassiumLib
+
+protocol HardwareKeyPickerDelegate: class {
+    func didDismiss(_ picker: HardwareKeyPicker)
+    func didSelectKey(yubiKey: YubiKey?, in picker: HardwareKeyPicker)
+}
+
+class HardwareKeyPicker: UITableViewController, Refreshable {
+    weak var delegate: HardwareKeyPickerDelegate?
+    
+    public var key: YubiKey? {
+        didSet { refresh() }
+    }
+    
+    public let dismissablePopoverDelegate = DismissablePopover(leftButton: .cancel, rightButton: nil)
+    
+    private let nfcKeys: [YubiKey] = [
+        YubiKey(interface: .nfc, slot: .slot1),
+        YubiKey(interface: .nfc, slot: .slot2)]
+    private let mfiKeys: [YubiKey] = [
+        YubiKey(interface: .mfi, slot: .slot1),
+        YubiKey(interface: .mfi, slot: .slot2)]
+
+    private enum Section: Int {
+        static let allValues = [.noHardwareKey, yubiKeyNFC, yubiKeyMFI]
+        case noHardwareKey
+        case yubiKeyNFC
+        case yubiKeyMFI
+        var title: String? {
+            switch self {
+            case .noHardwareKey:
+                return nil
+            case .yubiKeyNFC:
+                return "NFC"
+            case .yubiKeyMFI:
+                return "Lightning"
+            }
+        }
+    }
+    private var isNFCAvailable = false
+    private var isMFIAvailable = false
+    
+    private var isChoiceMade = false
+    
+    
+    public static func create(delegate: HardwareKeyPickerDelegate?=nil) -> HardwareKeyPicker {
+        let vc = HardwareKeyPicker.instantiateFromStoryboard()
+        vc.delegate = delegate
+        return vc
+    }
+    
+    override func viewWillAppear(_ animated: Bool) {
+        super.viewWillAppear(animated)
+        #if MAIN_APP
+        isNFCAvailable = ChallengeResponseManager.instance.supportsNFC
+        isMFIAvailable = ChallengeResponseManager.instance.supportsMFI
+        #elseif AUTOFILL_EXT
+        isNFCAvailable = false
+        isMFIAvailable = false
+        #endif
+    }
+    
+    override func viewWillDisappear(_ animated: Bool) {
+        super.viewWillDisappear(animated)
+        if !isChoiceMade {
+            delegate?.didDismiss(self)
+        }
+    }
+    
+    func refresh() {
+        tableView.reloadData()
+    }
+    
+    
+    override func numberOfSections(in tableView: UITableView) -> Int {
+        return Section.allValues.count
+    }
+    
+    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
+        guard let section = Section(rawValue: section) else {
+            assertionFailure()
+            return 0
+        }
+        switch section {
+        case .noHardwareKey:
+            return 1
+        case .yubiKeyNFC:
+            return nfcKeys.count
+        case .yubiKeyMFI:
+            return mfiKeys.count
+        }
+    }
+    
+    override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
+        guard let section = Section(rawValue: section) else {
+            assertionFailure()
+            return nil
+        }
+        return section.title
+    }
+    
+    override func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? {
+        guard let _section = Section(rawValue: section) else { assertionFailure(); return nil }
+
+        if _section == .noHardwareKey && !AppGroup.isMainApp {
+            return NSLocalizedString(
+                "[HardwareKey/AutoFill/NotAvailable] Hardware keys are not available in AutoFill.",
+                value: "Hardware keys are not available in AutoFill.",
+                comment: "A notification that hardware keys (e.g. YubiKey) cannot be used in AutoFill (the OS does not allow the AutoFill to use NFC/MFI).")
+        }
+        return super.tableView(tableView, titleForFooterInSection: section)
+    }
+    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
+        guard let section = Section(rawValue: indexPath.section) else {
+            fatalError()
+        }
+        
+        let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
+
+        let key: YubiKey?
+        switch section {
+        case .noHardwareKey:
+            key = nil
+            cell.setEnabled(true)
+            cell.isUserInteractionEnabled = true
+        case .yubiKeyNFC:
+            key = nfcKeys[indexPath.row]
+            cell.setEnabled(isNFCAvailable)
+            cell.isUserInteractionEnabled = isNFCAvailable
+        case .yubiKeyMFI:
+            key = mfiKeys[indexPath.row]
+            cell.setEnabled(isMFIAvailable)
+            cell.isUserInteractionEnabled = isMFIAvailable
+        }
+        cell.textLabel?.text = getKeyDescription(key)
+        cell.accessoryType = (key == self.key) ? .checkmark : .none
+        return cell
+    }
+    
+    private func getKeyDescription(_ key: YubiKey?) -> String {
+        guard let key = key else {
+            return NSLocalizedString(
+                "[HardwareKey/None] No Hardware Key",
+                value: "No Hardware Key",
+                comment: "Master key/unlock option: don't use hardware keys")
+        }
+        
+        let template = NSLocalizedString(
+            "[HardwareKey/YubiKey/Slot] YubiKey Slot #%d",
+            value: "YubiKey Slot %d",
+            comment: "Master key/unlock option: use given slot of YubiKey")
+        let result = String.localizedStringWithFormat(template, key.slot.number)
+        return result
+    }
+    
+    
+    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
+        guard let section = Section(rawValue: indexPath.section) else {
+            assertionFailure()
+            return
+        }
+        
+        let selectedKey: YubiKey?
+        switch section {
+        case .noHardwareKey:
+            selectedKey = nil
+        case .yubiKeyNFC:
+            selectedKey = nfcKeys[indexPath.row]
+        case .yubiKeyMFI:
+            selectedKey = mfiKeys[indexPath.row]
+        }
+        isChoiceMade = true
+        delegate?.didSelectKey(yubiKey: selectedKey, in: self)
+        dismiss(animated: true, completion: nil)
+    }
+}
+
+extension HardwareKeyPicker: UIPopoverPresentationControllerDelegate {
+
+    func presentationController(
+        _ controller: UIPresentationController,
+        viewControllerForAdaptivePresentationStyle style: UIModalPresentationStyle
+        ) -> UIViewController?
+    {
+        if style != .popover {
+            let navVC = controller.presentedViewController as? UINavigationController
+            let cancelButton = UIBarButtonItem(
+                barButtonSystemItem: .cancel,
+                target: self,
+                action: #selector(dismissPopover))
+            navVC?.topViewController?.navigationItem.leftBarButtonItem = cancelButton
+        }
+        return nil // "keep existing"
+    }
+    
+    @objc func dismissPopover() {
+        dismiss(animated: true, completion: nil)
+    }
+}
diff --git a/KeePassium/database/Base.lproj/ChangeMasterKeyVC.storyboard b/KeePassium/database/Base.lproj/ChangeMasterKeyVC.storyboard
index 92fc9ef8a..7614b4404 100755
--- a/KeePassium/database/Base.lproj/ChangeMasterKeyVC.storyboard
+++ b/KeePassium/database/Base.lproj/ChangeMasterKeyVC.storyboard
@@ -79,7 +79,7 @@
                                                     <constraint firstAttribute="height" constant="2" id="Zy2-rC-4aE"/>
                                                 </constraints>
                                             </view>
-                                            <textField opaque="NO" contentMode="scaleToFill" horizontalCompressionResistancePriority="751" contentHorizontalAlignment="left" contentVerticalAlignment="center" placeholder="No Key File" adjustsFontForContentSizeCategory="YES" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="M9Q-v8-e6Q" customClass="ValidatingTextField" customModule="KeePassium" customModuleProvider="target">
+                                            <textField opaque="NO" contentMode="scaleToFill" horizontalCompressionResistancePriority="751" contentHorizontalAlignment="left" contentVerticalAlignment="center" placeholder="No Key File" adjustsFontForContentSizeCategory="YES" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="M9Q-v8-e6Q" customClass="KeyFileTextField" customModule="KeePassium" customModuleProvider="target">
                                                 <rect key="frame" x="8" y="92" width="234" height="44"/>
                                                 <constraints>
                                                     <constraint firstAttribute="height" constant="44" id="t31-lC-6jx"/>
diff --git a/KeePassium/database/Base.lproj/DatabaseCreatorVC.storyboard b/KeePassium/database/Base.lproj/DatabaseCreatorVC.storyboard
index b35365e7e..d28fc9372 100644
--- a/KeePassium/database/Base.lproj/DatabaseCreatorVC.storyboard
+++ b/KeePassium/database/Base.lproj/DatabaseCreatorVC.storyboard
@@ -1,9 +1,9 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14868" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="Nri-WU-XVv">
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="15702" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="Nri-WU-XVv">
     <device id="retina4_0" orientation="portrait" appearance="dark"/>
     <dependencies>
         <deployment identifier="iOS"/>
-        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14824"/>
+        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15704"/>
         <capability name="Named colors" minToolsVersion="9.0"/>
         <capability name="Safe area layout guides" minToolsVersion="9.0"/>
         <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
@@ -141,7 +141,7 @@
                                                             <constraint firstAttribute="height" constant="2" id="iWU-bx-lh7"/>
                                                         </constraints>
                                                     </view>
-                                                    <textField opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" contentHorizontalAlignment="left" contentVerticalAlignment="center" placeholder="No Key File" adjustsFontForContentSizeCategory="YES" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="pvt-FS-0Yx" customClass="WatchdogAwareTextField" customModule="KeePassium" customModuleProvider="target">
+                                                    <textField opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" contentHorizontalAlignment="left" contentVerticalAlignment="center" placeholder="No Key File" adjustsFontForContentSizeCategory="YES" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="pvt-FS-0Yx" customClass="KeyFileTextField" customModule="KeePassium" customModuleProvider="target">
                                                         <rect key="frame" x="8" y="46" width="264" height="44"/>
                                                         <constraints>
                                                             <constraint firstAttribute="height" constant="44" id="br3-ld-CHa"/>
@@ -278,7 +278,7 @@
                         <outlet property="errorLabel" destination="wb0-s2-V8n" id="VgJ-h5-pvV"/>
                         <outlet property="errorMessagePanel" destination="udd-lE-iHF" id="GMW-WV-XCF"/>
                         <outlet property="fileNameField" destination="XNL-lc-2Ti" id="iKo-Vc-RF3"/>
-                        <outlet property="keyFileField" destination="pvt-FS-0Yx" id="BlR-qx-R5P"/>
+                        <outlet property="keyFileField" destination="pvt-FS-0Yx" id="BCD-MV-f0s"/>
                         <outlet property="keyboardLayoutConstraint" destination="Vn9-PM-nmb" id="fUd-Xa-ZpD"/>
                         <outlet property="passwordField" destination="uHk-Yt-mBh" id="JiK-BH-kCB"/>
                         <outlet property="scrollView" destination="UAC-3N-WvO" id="Re8-AB-rKc"/>
diff --git a/KeePassium/database/Base.lproj/UnlockDatabaseVC.storyboard b/KeePassium/database/Base.lproj/UnlockDatabaseVC.storyboard
index b9c9c122f..1426f22c0 100755
--- a/KeePassium/database/Base.lproj/UnlockDatabaseVC.storyboard
+++ b/KeePassium/database/Base.lproj/UnlockDatabaseVC.storyboard
@@ -1,9 +1,9 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="15400" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="eAa-Z5-pdq">
-    <device id="retina4_0" orientation="portrait" appearance="dark"/>
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="15702" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="eAa-Z5-pdq">
+    <device id="retina4_0" orientation="portrait" appearance="light"/>
     <dependencies>
         <deployment identifier="iOS"/>
-        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15404"/>
+        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15704"/>
         <capability name="Named colors" minToolsVersion="9.0"/>
         <capability name="Safe area layout guides" minToolsVersion="9.0"/>
         <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
@@ -50,7 +50,7 @@
                                         </subviews>
                                     </stackView>
                                     <stackView opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="252" horizontalCompressionResistancePriority="751" verticalCompressionResistancePriority="751" axis="vertical" translatesAutoresizingMaskIntoConstraints="NO" id="kpI-3v-C4a">
-                                        <rect key="frame" x="8" y="106" width="304" height="50"/>
+                                        <rect key="frame" x="8" y="88" width="304" height="50"/>
                                         <subviews>
                                             <view contentMode="scaleToFill" horizontalHuggingPriority="251" horizontalCompressionResistancePriority="751" verticalCompressionResistancePriority="751" translatesAutoresizingMaskIntoConstraints="NO" id="1da-3k-RQm" userLabel="Error Message Panel">
                                                 <rect key="frame" x="0.0" y="0.0" width="304" height="50"/>
@@ -99,7 +99,7 @@
                                         </subviews>
                                     </stackView>
                                     <view contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="Dr0-TU-sMX" userLabel="Database Name Panel">
-                                        <rect key="frame" x="63.5" y="164" width="187.5" height="29"/>
+                                        <rect key="frame" x="63.5" y="146" width="187.5" height="29"/>
                                         <subviews>
                                             <imageView userInteractionEnabled="NO" contentMode="center" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="database-cloud-listitem" translatesAutoresizingMaskIntoConstraints="NO" id="MLZ-P0-rnn">
                                                 <rect key="frame" x="0.0" y="0.0" width="29" height="29"/>
@@ -133,7 +133,7 @@
                                         </constraints>
                                     </view>
                                     <stackView opaque="NO" contentMode="scaleToFill" axis="vertical" distribution="equalSpacing" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="G3Q-hY-M1b">
-                                        <rect key="frame" x="20" y="201" width="280" height="166"/>
+                                        <rect key="frame" x="20" y="183" width="280" height="202"/>
                                         <subviews>
                                             <view contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" horizontalCompressionResistancePriority="749" verticalCompressionResistancePriority="749" translatesAutoresizingMaskIntoConstraints="NO" id="3xv-vB-0Jn">
                                                 <rect key="frame" x="0.0" y="0.0" width="280" height="90"/>
@@ -171,7 +171,7 @@
                                                             <constraint firstAttribute="height" constant="2" id="1li-ig-vuf"/>
                                                         </constraints>
                                                     </view>
-                                                    <textField opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" contentHorizontalAlignment="left" contentVerticalAlignment="center" placeholder="No Key File" adjustsFontForContentSizeCategory="YES" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="mYY-hP-3vH" customClass="WatchdogAwareTextField" customModule="KeePassium" customModuleProvider="target">
+                                                    <textField opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" contentHorizontalAlignment="left" contentVerticalAlignment="center" placeholder="No Key File" adjustsFontForContentSizeCategory="YES" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="mYY-hP-3vH" customClass="KeyFileTextField" customModule="KeePassium" customModuleProvider="target">
                                                         <rect key="frame" x="8" y="46" width="264" height="44"/>
                                                         <constraints>
                                                             <constraint firstAttribute="height" constant="44" id="zgu-ja-9pO"/>
@@ -235,6 +235,14 @@
                                                     <fragment content="Hint shown below the Unlock button, when not asking for master password/key file."/>
                                                 </attributedString>
                                             </label>
+                                            <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="gji-1x-co5">
+                                                <rect key="frame" x="0.0" y="174" width="280" height="28"/>
+                                                <fontDescription key="fontDescription" style="UICTFontTextStyleFootnote"/>
+                                                <state key="normal" title="Lock Database"/>
+                                                <connections>
+                                                    <action selector="didPressLockDatabase:" destination="eAa-Z5-pdq" eventType="touchUpInside" id="oFA-jn-sB0"/>
+                                                </connections>
+                                            </button>
                                         </subviews>
                                         <constraints>
                                             <constraint firstAttribute="width" relation="greaterThanOrEqual" constant="250" id="2vz-Ao-z4Z"/>
@@ -298,6 +306,7 @@
                         <outlet property="inputPanel" destination="3xv-vB-0Jn" id="dpu-5T-Uzb"/>
                         <outlet property="keyFileField" destination="mYY-hP-3vH" id="ITo-zF-Ead"/>
                         <outlet property="keyboardAdjView" destination="cdl-k8-gTg" id="gDU-xh-QP9"/>
+                        <outlet property="lockDatabaseButton" destination="gji-1x-co5" id="18J-2h-LcQ"/>
                         <outlet property="masterKeyKnownLabel" destination="K3Y-ON-C19" id="biz-bo-isU"/>
                         <outlet property="passwordField" destination="CuN-tE-lAG" id="8Cx-tM-tfp"/>
                         <outlet property="watchdogTimeoutLabel" destination="epu-yU-Lyg" id="8Mz-oL-08S"/>
@@ -320,7 +329,7 @@
             <color red="0.23499999940395355" green="0.23499999940395355" blue="0.2630000114440918" alpha="0.60000002384185791" colorSpace="custom" customColorSpace="sRGB"/>
         </namedColor>
         <namedColor name="PrimaryText">
-            <color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
+            <color white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
         </namedColor>
         <namedColor name="WarningBackground">
             <color red="0.94199997186660767" green="0.59700000286102295" blue="0.21699999272823334" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
diff --git a/KeePassium/database/ChangeMasterKeyVC.swift b/KeePassium/database/ChangeMasterKeyVC.swift
index 1c8d20d19..ac0bf0d01 100755
--- a/KeePassium/database/ChangeMasterKeyVC.swift
+++ b/KeePassium/database/ChangeMasterKeyVC.swift
@@ -15,11 +15,12 @@ class ChangeMasterKeyVC: UIViewController {
     @IBOutlet weak var databaseIcon: UIImageView!
     @IBOutlet weak var passwordField: ValidatingTextField!
     @IBOutlet weak var repeatPasswordField: ValidatingTextField!
-    @IBOutlet weak var keyFileField: ValidatingTextField!
+    @IBOutlet weak var keyFileField: KeyFileTextField!
     @IBOutlet weak var passwordMismatchImage: UIImageView!
     
     private var databaseRef: URLReference!
     private var keyFileRef: URLReference?
+    private var yubiKey: YubiKey?
     
     static func make(dbRef: URLReference) -> UIViewController {
         let vc = ChangeMasterKeyVC.instantiateFromStoryboard()
@@ -44,6 +45,7 @@ class ChangeMasterKeyVC: UIViewController {
         repeatPasswordField.validityDelegate = self
         keyFileField.delegate = self
         keyFileField.validityDelegate = self
+        setupHardwareKeyPicker()
         
         view.backgroundColor = UIColor(patternImage: UIImage(asset: .backgroundPattern))
         view.layer.isOpaque = false
@@ -57,6 +59,29 @@ class ChangeMasterKeyVC: UIViewController {
     }
     
     
+    private func setupHardwareKeyPicker() {
+        keyFileField.yubikeyHandler = {
+            [weak self] (field) in
+            guard let self = self else { return }
+            let popoverAnchor = PopoverAnchor(
+                sourceView: self.keyFileField,
+                sourceRect: self.keyFileField.bounds)
+            self.showYubiKeyPicker(at: popoverAnchor)
+        }
+    }
+    
+    private func showYubiKeyPicker(at popoverAnchor: PopoverAnchor) {
+        let hardwareKeyPicker = HardwareKeyPicker.create(delegate: self)
+        hardwareKeyPicker.modalPresentationStyle = .popover
+        if let popover = hardwareKeyPicker.popoverPresentationController {
+            popoverAnchor.apply(to: popover)
+            popover.delegate = hardwareKeyPicker.dismissablePopoverDelegate
+        }
+        hardwareKeyPicker.key = yubiKey
+        present(hardwareKeyPicker, animated: true, completion: nil)
+    }
+    
+    
     @IBAction func didPressCancel(_ sender: Any) {
         dismiss(animated: true, completion: nil)
     }
@@ -67,17 +92,25 @@ class ChangeMasterKeyVC: UIViewController {
             return
         }
         
+        let _challengeHandler = ChallengeResponseManager.makeHandler(for: yubiKey)
         DatabaseManager.createCompositeKey(
             keyHelper: db.keyHelper,
             password: passwordField.text ?? "",
             keyFile: keyFileRef,
+            challengeHandler: _challengeHandler,
             success: {
-                [weak self] (_ newCompositeKey: SecureByteArray) -> Void in
-                guard let _self = self else { return }
+                [weak self] (_ newCompositeKey: CompositeKey) -> Void in
+                guard let self = self else { return }
                 let dbm = DatabaseManager.shared
                 dbm.changeCompositeKey(to: newCompositeKey)
-                try? dbm.rememberDatabaseKey(onlyIfExists: true) 
-                dbm.addObserver(_self)
+                DatabaseSettingsManager.shared.updateSettings(for: self.databaseRef) {
+                    [weak self] (dbSettings) in
+                    guard let self = self else { return }
+                    dbSettings.maybeSetMasterKey(newCompositeKey)
+                    dbSettings.maybeSetAssociatedKeyFile(self.keyFileRef)
+                    dbSettings.maybeSetAssociatedYubiKey(self.yubiKey)
+                }
+                dbm.addObserver(self)
                 dbm.startSavingDatabase()
             },
             error: {
@@ -210,6 +243,28 @@ extension ChangeMasterKeyVC: KeyFileChooserDelegate {
     }
 }
 
+extension ChangeMasterKeyVC: HardwareKeyPickerDelegate {
+    func didDismiss(_ picker: HardwareKeyPicker) {
+    }
+    func didSelectKey(yubiKey: YubiKey?, in picker: HardwareKeyPicker) {
+        setYubiKey(yubiKey)
+    }
+    
+    func setYubiKey(_ yubiKey: YubiKey?) {
+        self.yubiKey = yubiKey
+        keyFileField.isYubiKeyActive = (yubiKey != nil)
+
+        DatabaseSettingsManager.shared.updateSettings(for: databaseRef) { (dbSettings) in
+            dbSettings.maybeSetAssociatedYubiKey(yubiKey)
+        }
+        if let _yubiKey = yubiKey {
+            Diag.info("Hardware key selected [key: \(_yubiKey)]")
+        } else {
+            Diag.info("No hardware key selected")
+        }
+    }
+}
+
 extension ChangeMasterKeyVC: DatabaseManagerObserver {
     func databaseManager(willSaveDatabase urlRef: URLReference) {
         showProgressOverlay()
diff --git a/KeePassium/database/DatabaseCreatorCoordinator.swift b/KeePassium/database/DatabaseCreatorCoordinator.swift
index 580364151..1161a8f0b 100644
--- a/KeePassium/database/DatabaseCreatorCoordinator.swift
+++ b/KeePassium/database/DatabaseCreatorCoordinator.swift
@@ -76,10 +76,12 @@ class DatabaseCreatorCoordinator: NSObject {
             return
         }
         
+        let _challengeHandler = ChallengeResponseManager.makeHandler(for: databaseCreatorVC.yubiKey)
         DatabaseManager.shared.createDatabase(
             databaseURL: tmpFileURL,
             password: databaseCreatorVC.password,
             keyFile: databaseCreatorVC.keyFile,
+            challengeHandler: _challengeHandler,
             template: { [weak self] (rootGroup2) in
                 rootGroup2.name = fileName // override default "/" with a meaningful name
                 self?.addTemplateItems(to: rootGroup2)
@@ -218,6 +220,13 @@ extension DatabaseCreatorCoordinator: DatabaseCreatorDelegate {
         let keyFileChooser = ChooseKeyFileVC.make(popoverSourceView: popoverSource, delegate: self)
         navigationController.present(keyFileChooser, animated: true, completion: nil)
     }
+    
+    func didPressPickHardwareKey(
+        in databaseCreatorVC: DatabaseCreatorVC,
+        at popoverAnchor: PopoverAnchor)
+    {
+        showHardwareKeyPicker(at: popoverAnchor)
+    }
 }
 
 extension DatabaseCreatorCoordinator: KeyFileChooserDelegate {
@@ -300,3 +309,33 @@ extension DatabaseCreatorCoordinator: UIDocumentPickerDelegate {
         }
     }
 }
+
+extension DatabaseCreatorCoordinator: HardwareKeyPickerDelegate {
+    func showHardwareKeyPicker(at popoverAnchor: PopoverAnchor) {
+        let hardwareKeyPicker = HardwareKeyPicker.create(delegate: self)
+        hardwareKeyPicker.modalPresentationStyle = .popover
+        if let popover = hardwareKeyPicker.popoverPresentationController {
+            popoverAnchor.apply(to: popover)
+            popover.delegate = hardwareKeyPicker.dismissablePopoverDelegate
+        }
+        hardwareKeyPicker.key = databaseCreatorVC.yubiKey
+        databaseCreatorVC.present(hardwareKeyPicker, animated: true, completion: nil)
+    }
+
+    func didDismiss(_ picker: HardwareKeyPicker) {
+    }
+    
+    func didSelectKey(yubiKey: YubiKey?, in picker: HardwareKeyPicker) {
+        setYubiKey(yubiKey)
+        databaseCreatorVC.becomeFirstResponder()
+    }
+    
+    func setYubiKey(_ yubiKey: YubiKey?) {
+        databaseCreatorVC.yubiKey = yubiKey
+        if let _yubiKey = yubiKey {
+            Diag.info("Hardware key selected [key: \(_yubiKey)]")
+        } else {
+            Diag.info("No hardware key selected")
+        }
+    }
+}
diff --git a/KeePassium/database/DatabaseCreatorVC.swift b/KeePassium/database/DatabaseCreatorVC.swift
index a7ccf61b6..8b647ad69 100644
--- a/KeePassium/database/DatabaseCreatorVC.swift
+++ b/KeePassium/database/DatabaseCreatorVC.swift
@@ -13,6 +13,9 @@ protocol DatabaseCreatorDelegate: class {
     func didPressCancel(in databaseCreatorVC: DatabaseCreatorVC)
     func didPressContinue(in databaseCreatorVC: DatabaseCreatorVC)
     func didPressPickKeyFile(in databaseCreatorVC: DatabaseCreatorVC, popoverSource: UIView)
+    func didPressPickHardwareKey(
+        in databaseCreatorVC: DatabaseCreatorVC,
+        at popoverAnchor: PopoverAnchor)
 }
 
 class DatabaseCreatorVC: UIViewController {
@@ -24,10 +27,15 @@ class DatabaseCreatorVC: UIViewController {
             showKeyFile(keyFile)
         }
     }
+    public var yubiKey: YubiKey? {
+        didSet {
+            keyFileField?.isYubiKeyActive = (yubiKey != nil)
+        }
+    }
 
     @IBOutlet weak var fileNameField: ValidatingTextField!
     @IBOutlet weak var passwordField: ProtectedTextField!
-    @IBOutlet weak var keyFileField: WatchdogAwareTextField!
+    @IBOutlet weak var keyFileField: KeyFileTextField!
     @IBOutlet weak var continueButton: UIButton!
     @IBOutlet var errorMessagePanel: UIView!
     @IBOutlet weak var errorLabel: UILabel!
@@ -61,6 +69,16 @@ class DatabaseCreatorVC: UIViewController {
         passwordField.delegate = self
         keyFileField.delegate = self
         
+        keyFileField.yubikeyHandler = {
+            [weak self] (field) in
+            guard let self = self else { return }
+            let popoverAnchor = PopoverAnchor(
+                sourceView: self.keyFileField,
+                sourceRect: self.keyFileField.bounds)
+            self.delegate?.didPressPickHardwareKey(in: self, at: popoverAnchor)
+        }
+        keyFileField.isYubiKeyActive = (yubiKey != nil)
+        
         passwordField.becomeFirstResponder()
     }
 
diff --git a/KeePassium/database/UnlockDatabaseVC.swift b/KeePassium/database/UnlockDatabaseVC.swift
index 8e9c1d34c..f09e82741 100644
--- a/KeePassium/database/UnlockDatabaseVC.swift
+++ b/KeePassium/database/UnlockDatabaseVC.swift
@@ -13,7 +13,7 @@ class UnlockDatabaseVC: UIViewController, Refreshable {
     @IBOutlet private weak var databaseNameLabel: UILabel!
     @IBOutlet private weak var inputPanel: UIView!
     @IBOutlet private weak var passwordField: UITextField!
-    @IBOutlet private weak var keyFileField: UITextField!
+    @IBOutlet private weak var keyFileField: KeyFileTextField!
     @IBOutlet private weak var keyboardAdjView: UIView!
     @IBOutlet private weak var errorMessagePanel: UIView!
     @IBOutlet private weak var errorLabel: UILabel!
@@ -21,6 +21,7 @@ class UnlockDatabaseVC: UIViewController, Refreshable {
     @IBOutlet private weak var watchdogTimeoutLabel: UILabel!
     @IBOutlet private weak var databaseIconImage: UIImageView!
     @IBOutlet weak var masterKeyKnownLabel: UILabel!
+    @IBOutlet weak var lockDatabaseButton: UIButton!
     @IBOutlet weak var getPremiumButton: UIButton!
     @IBOutlet weak var announcementButton: UIButton!
     
@@ -33,6 +34,7 @@ class UnlockDatabaseVC: UIViewController, Refreshable {
     }
     
     private var keyFileRef: URLReference?
+    private var yubiKey: YubiKey?
     private var fileKeeperNotifications: FileKeeperNotifications!
     
     var isAutoUnlockEnabled = true
@@ -59,10 +61,19 @@ class UnlockDatabaseVC: UIViewController, Refreshable {
 
         view.backgroundColor = UIColor(patternImage: UIImage(asset: .backgroundPattern))
         view.layer.isOpaque = false
-        
+
         watchdogTimeoutLabel.alpha = 0.0
         errorMessagePanel.alpha = 0.0
         errorMessagePanel.isHidden = true
+        
+        keyFileField.yubikeyHandler = {
+            [weak self] (field) in
+            guard let self = self else { return }
+            let popoverAnchor = PopoverAnchor(
+                sourceView: self.keyFileField,
+                sourceRect: self.keyFileField.bounds)
+            self.showHardwareKeyPicker(at: popoverAnchor)
+        }
 
         passwordField.inputAssistantItem.leadingBarButtonGroups = []
         passwordField.inputAssistantItem.trailingBarButtonGroups = []
@@ -159,6 +170,9 @@ class UnlockDatabaseVC: UIViewController, Refreshable {
                 setKeyFile(urlRef: availableKeyFileRef)
             }
         }
+        if let associatedYubiKey = dbSettings?.associatedYubiKey {
+            setYubiKey(associatedYubiKey)
+        }
         refreshNews()
         refreshInputMode()
     }
@@ -182,6 +196,7 @@ class UnlockDatabaseVC: UIViewController, Refreshable {
         
         let shouldInputMasterKey = !isDatabaseKeyStored
         masterKeyKnownLabel.isHidden = shouldInputMasterKey
+        lockDatabaseButton.isHidden = masterKeyKnownLabel.isHidden
         inputPanel.isHidden = !shouldInputMasterKey
     }
 
@@ -196,6 +211,19 @@ class UnlockDatabaseVC: UIViewController, Refreshable {
     }
     
     
+    func showHardwareKeyPicker(at popoverAnchor: PopoverAnchor) {
+        let hardwareKeyPicker = HardwareKeyPicker.create(delegate: self)
+        hardwareKeyPicker.modalPresentationStyle = .popover
+        if let popover = hardwareKeyPicker.popoverPresentationController {
+            popoverAnchor.apply(to: popover)
+            popover.delegate = hardwareKeyPicker.dismissablePopoverDelegate
+        }
+        hardwareKeyPicker.key = yubiKey
+        present(hardwareKeyPicker, animated: true, completion: nil)
+    }
+    
+    
+    
     private var newsItem: NewsItem?
     
     private func refreshNews() {
@@ -368,6 +396,13 @@ class UnlockDatabaseVC: UIViewController, Refreshable {
         tryToUnlockDatabase(isAutomaticUnlock: false)
     }
     
+    @IBAction func didPressLockDatabase(_ sender: Any) {
+        DatabaseSettingsManager.shared.updateSettings(for: databaseRef) {
+            $0.clearMasterKey()
+        }
+        refreshInputMode()
+    }
+    
     private var premiumCoordinator: PremiumCoordinator?
     @IBAction func didPressUpgradeToPremium(_ sender: Any) {
         assert(premiumCoordinator == nil)
@@ -395,12 +430,14 @@ class UnlockDatabaseVC: UIViewController, Refreshable {
         passwordField.resignFirstResponder()
         hideWatchdogTimeoutMessage(animated: true)
         DatabaseManager.shared.addObserver(self)
-
+        
+        let _challengeHandler = ChallengeResponseManager.makeHandler(for: yubiKey)
         let dbSettings = DatabaseSettingsManager.shared.getSettings(for: databaseRef)
         if let databaseKey = dbSettings?.masterKey {
+            databaseKey.challengeHandler = _challengeHandler
             DatabaseManager.shared.startLoadingDatabase(
                 database: databaseRef,
-                compositeKey: databaseKey.secureClone())
+                compositeKey: databaseKey)
         } else {
             guard !isAutomaticUnlock else {
                 Diag.debug("Aborting auto-unlock, there is no stored key")
@@ -408,11 +445,11 @@ class UnlockDatabaseVC: UIViewController, Refreshable {
                 hideProgressOverlay(quickly: true)
                 return
             }
-
             DatabaseManager.shared.startLoadingDatabase(
                 database: databaseRef,
                 password: password,
-                keyFile: keyFileRef)
+                keyFile: keyFileRef,
+                challengeHandler: _challengeHandler)
         }
     }
     
@@ -437,6 +474,29 @@ class UnlockDatabaseVC: UIViewController, Refreshable {
     }
 }
 
+extension UnlockDatabaseVC: HardwareKeyPickerDelegate {
+    func didDismiss(_ picker: HardwareKeyPicker) {
+    }
+    
+    func didSelectKey(yubiKey: YubiKey?, in picker: HardwareKeyPicker) {
+        setYubiKey(yubiKey)
+    }
+    
+    func setYubiKey(_ yubiKey: YubiKey?) {
+        self.yubiKey = yubiKey
+        keyFileField.isYubiKeyActive = (yubiKey != nil)
+
+        DatabaseSettingsManager.shared.updateSettings(for: databaseRef) { (dbSettings) in
+            dbSettings.maybeSetAssociatedYubiKey(yubiKey)
+        }
+        if let _yubiKey = yubiKey {
+            Diag.info("Hardware key selected [key: \(_yubiKey)]")
+        } else {
+            Diag.info("No hardware key selected")
+        }
+    }
+}
+
 extension UnlockDatabaseVC: KeyFileChooserDelegate {
     func setKeyFile(urlRef: URLReference?) {
         keyFileRef = urlRef
diff --git a/KeePassium/database/group/Base.lproj/ViewGroupVC.storyboard b/KeePassium/database/group/Base.lproj/ViewGroupVC.storyboard
index bf5c98f9a..c4b08db94 100755
--- a/KeePassium/database/group/Base.lproj/ViewGroupVC.storyboard
+++ b/KeePassium/database/group/Base.lproj/ViewGroupVC.storyboard
@@ -1,9 +1,9 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="15400" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="Rxg-rI-fqC">
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="15702" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="Rxg-rI-fqC">
     <device id="retina4_7" orientation="portrait" appearance="light"/>
     <dependencies>
         <deployment identifier="iOS"/>
-        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15404"/>
+        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15704"/>
         <capability name="Named colors" minToolsVersion="9.0"/>
         <capability name="Safe area layout guides" minToolsVersion="9.0"/>
         <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
@@ -67,14 +67,14 @@
                                 </attributedString>
                             </tableViewCell>
                             <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" reuseIdentifier="GroupCell" id="MoA-V2-ZR5" customClass="GroupViewListCell" customModule="KeePassium" customModuleProvider="target">
-                                <rect key="frame" x="0.0" y="115" width="375" height="51"/>
+                                <rect key="frame" x="0.0" y="115" width="375" height="51.5"/>
                                 <autoresizingMask key="autoresizingMask"/>
                                 <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="MoA-V2-ZR5" id="rfV-hX-YdO">
-                                    <rect key="frame" x="0.0" y="0.0" width="348" height="51"/>
+                                    <rect key="frame" x="0.0" y="0.0" width="348" height="51.5"/>
                                     <autoresizingMask key="autoresizingMask"/>
                                     <subviews>
                                         <imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="db-icons/kpbIcon48" translatesAutoresizingMaskIntoConstraints="NO" id="raf-Bf-GLo">
-                                            <rect key="frame" x="16" y="11" width="29" height="29"/>
+                                            <rect key="frame" x="16" y="11" width="29.5" height="29.5"/>
                                             <color key="tintColor" name="IconTint"/>
                                             <constraints>
                                                 <constraint firstAttribute="width" secondItem="raf-Bf-GLo" secondAttribute="height" multiplier="1:1" id="HHG-4b-tQV"/>
@@ -82,7 +82,7 @@
                                             </constraints>
                                         </imageView>
                                         <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalCompressionResistancePriority="749" layoutMarginsFollowReadableWidth="YES" text="{Group Name}" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="7uR-HK-1Dd">
-                                            <rect key="frame" x="61" y="15.5" width="109" height="20.5"/>
+                                            <rect key="frame" x="61.5" y="15.5" width="109" height="20.5"/>
                                             <fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
                                             <color key="textColor" name="PrimaryText"/>
                                             <nil key="highlightedColor"/>
@@ -103,6 +103,7 @@
                                     <constraints>
                                         <constraint firstAttribute="trailingMargin" secondItem="ttY-yZ-yRg" secondAttribute="trailing" id="3v4-43-FUA"/>
                                         <constraint firstItem="ttY-yZ-yRg" firstAttribute="top" relation="greaterThanOrEqual" secondItem="rfV-hX-YdO" secondAttribute="topMargin" constant="4" id="EqS-jc-DJF"/>
+                                        <constraint firstItem="raf-Bf-GLo" firstAttribute="top" relation="greaterThanOrEqual" secondItem="rfV-hX-YdO" secondAttribute="topMargin" id="Ttz-S5-UxA"/>
                                         <constraint firstItem="raf-Bf-GLo" firstAttribute="leading" secondItem="rfV-hX-YdO" secondAttribute="leadingMargin" id="W0W-gF-7gb"/>
                                         <constraint firstItem="7uR-HK-1Dd" firstAttribute="top" relation="greaterThanOrEqual" secondItem="rfV-hX-YdO" secondAttribute="topMargin" constant="4" id="YO5-cY-mfX"/>
                                         <constraint firstItem="7uR-HK-1Dd" firstAttribute="centerY" secondItem="rfV-hX-YdO" secondAttribute="centerY" id="f8U-SN-xJe"/>
@@ -112,6 +113,7 @@
                                         <constraint firstItem="ttY-yZ-yRg" firstAttribute="centerY" secondItem="rfV-hX-YdO" secondAttribute="centerY" id="nP8-K0-43f"/>
                                         <constraint firstItem="ttY-yZ-yRg" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="7uR-HK-1Dd" secondAttribute="trailing" constant="16" id="qge-Ze-pDx"/>
                                         <constraint firstAttribute="bottomMargin" relation="greaterThanOrEqual" secondItem="ttY-yZ-yRg" secondAttribute="bottom" constant="4" id="qsC-0h-wnl"/>
+                                        <constraint firstItem="raf-Bf-GLo" firstAttribute="bottom" relation="greaterThanOrEqual" secondItem="rfV-hX-YdO" secondAttribute="bottomMargin" id="zKw-7Q-MzU"/>
                                     </constraints>
                                 </tableViewCellContentView>
                                 <connections>
@@ -121,7 +123,7 @@
                                 </connections>
                             </tableViewCell>
                             <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" reuseIdentifier="EntryCell" id="mqP-Mr-NTh" customClass="GroupViewListCell" customModule="KeePassium" customModuleProvider="target">
-                                <rect key="frame" x="0.0" y="166" width="375" height="57.5"/>
+                                <rect key="frame" x="0.0" y="166.5" width="375" height="57.5"/>
                                 <autoresizingMask key="autoresizingMask"/>
                                 <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="mqP-Mr-NTh" id="gG2-77-V2H">
                                     <rect key="frame" x="0.0" y="0.0" width="375" height="57.5"/>
@@ -138,7 +140,7 @@
                                         <stackView opaque="NO" contentMode="scaleToFill" axis="vertical" translatesAutoresizingMaskIntoConstraints="NO" id="zQD-hA-ZaY">
                                             <rect key="frame" x="61" y="11.5" width="298" height="35"/>
                                             <subviews>
-                                                <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="249" verticalHuggingPriority="249" text="{Entry Title}" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="OjW-aR-wD3">
+                                                <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="249" verticalHuggingPriority="249" ambiguous="YES" text="{Entry Title}" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="OjW-aR-wD3">
                                                     <rect key="frame" x="0.0" y="0.0" width="298" height="20.5"/>
                                                     <fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
                                                     <color key="textColor" name="PrimaryText"/>
@@ -147,7 +149,7 @@
                                                         <fragment content="#bc-ignore!"/>
                                                     </attributedString>
                                                 </label>
-                                                <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="{Entry Subtitle}" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="5Db-9q-PM7">
+                                                <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="{Entry Subtitle}" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="5Db-9q-PM7">
                                                     <rect key="frame" x="0.0" y="20.5" width="298" height="14.5"/>
                                                     <fontDescription key="fontDescription" style="UICTFontTextStyleCaption1"/>
                                                     <color key="textColor" name="AuxiliaryText"/>
@@ -162,11 +164,13 @@
                                     <constraints>
                                         <constraint firstItem="XYa-LO-MrH" firstAttribute="centerY" secondItem="gG2-77-V2H" secondAttribute="centerY" id="3EO-rV-jrv"/>
                                         <constraint firstItem="zQD-hA-ZaY" firstAttribute="centerY" secondItem="gG2-77-V2H" secondAttribute="centerY" id="VBS-cs-wY3"/>
+                                        <constraint firstItem="XYa-LO-MrH" firstAttribute="top" relation="greaterThanOrEqual" secondItem="gG2-77-V2H" secondAttribute="topMargin" id="aFq-fh-RIH"/>
                                         <constraint firstAttribute="trailingMargin" secondItem="zQD-hA-ZaY" secondAttribute="trailing" id="df6-V5-uJz"/>
                                         <constraint firstItem="XYa-LO-MrH" firstAttribute="leading" secondItem="gG2-77-V2H" secondAttribute="leadingMargin" id="hlr-Ut-vxA"/>
                                         <constraint firstItem="zQD-hA-ZaY" firstAttribute="top" relation="greaterThanOrEqual" secondItem="gG2-77-V2H" secondAttribute="topMargin" id="iQa-5B-Tin"/>
                                         <constraint firstAttribute="bottomMargin" relation="greaterThanOrEqual" secondItem="zQD-hA-ZaY" secondAttribute="bottom" id="p1W-qP-61o"/>
                                         <constraint firstItem="zQD-hA-ZaY" firstAttribute="leading" secondItem="XYa-LO-MrH" secondAttribute="trailing" constant="16" id="sAj-yu-Kl5"/>
+                                        <constraint firstAttribute="bottomMargin" relation="greaterThanOrEqual" secondItem="XYa-LO-MrH" secondAttribute="bottom" id="vEF-xS-qsV"/>
                                     </constraints>
                                 </tableViewCellContentView>
                                 <connections>
@@ -304,13 +308,13 @@
             <color red="0.23499999940395355" green="0.23499999940395355" blue="0.2630000114440918" alpha="0.60000002384185791" colorSpace="custom" customColorSpace="sRGB"/>
         </namedColor>
         <namedColor name="DisabledText">
-            <color white="0.42300000786781311" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
+            <color red="0.75300002098083496" green="0.75300002098083496" blue="0.75300002098083496" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
         </namedColor>
         <namedColor name="IconTint">
-            <color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
+            <color red="0.0" green="0.41176470588235292" blue="0.85098039215686272" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
         </namedColor>
         <namedColor name="PrimaryText">
-            <color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
+            <color white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
         </namedColor>
     </resources>
     <color key="tintColor" name="ActionTint"/>
diff --git a/KeePassium/settings/SettingsDataProtectionVC.swift b/KeePassium/settings/SettingsDataProtectionVC.swift
index 3c9378166..fa3f45046 100644
--- a/KeePassium/settings/SettingsDataProtectionVC.swift
+++ b/KeePassium/settings/SettingsDataProtectionVC.swift
@@ -86,7 +86,7 @@ class SettingsDataProtectionVC: UITableViewController, Refreshable {
     }
     
     @IBAction func didPressClearKeyFileAssociations(_ sender: Any) {
-        Settings.current.removeAllKeyFileAssociations()
+        DatabaseSettingsManager.shared.forgetAllKeyFiles()
         let confirmationAlert = UIAlertController.make(
             title: NSLocalizedString(
                 "[Settings/ClearKeyFileAssociations/Cleared/title] Cleared",
diff --git a/KeePassium/util/LString.swift b/KeePassium/util/LString.swift
index 472cdaa25..12f0c1f3d 100755
--- a/KeePassium/util/LString.swift
+++ b/KeePassium/util/LString.swift
@@ -282,4 +282,15 @@ public enum LString {
         "[Support/template] (Please describe the problem here)",
         value: "(Please describe the problem here)",
         comment: "Template text of a bug report email")
+    
+    
+    public static let dontUseYubikey = NSLocalizedString(
+        "[YubiKey] Don't use YubiKey",
+        value: "Without YubiKey",
+        comment: "Selector choice: don't use YubiKey to encrypt/decrypt database")
+    
+    public static let useYubikeySlotN = NSLocalizedString(
+        "[YubiKey] Use YubiKey Slot %d",
+        value: "Use YubiKey Slot %d",
+        comment: "Selector choice: use YubiKey to encrypt/decrypt database. For example: `Use YubiKey Slot 1`. [slotID: Int]")
 }
diff --git a/KeePassium/util/UIImage+extensions.swift b/KeePassium/util/UIImage+extensions.swift
index 3cbf8bac6..bcb5f2f78 100755
--- a/KeePassium/util/UIImage+extensions.swift
+++ b/KeePassium/util/UIImage+extensions.swift
@@ -37,6 +37,8 @@ enum ImageAsset: String {
     case premiumBenefitShiny = "premium-benefit-shiny"
     case expandRowCellAccessory = "expand-row-cellaccessory"
     case collapseRowCellAccessory = "collapse-row-cellaccessory"
+    case yubikeyOnAccessory = "yubikey-on-accessory"
+    case yubikeyOffAccessory = "yubikey-off-accessory"
 }
 
 extension UIImage {
diff --git a/KeePassium/util/Watchdog.swift b/KeePassium/util/Watchdog.swift
index 8fbfc6fde..ce910b985 100644
--- a/KeePassium/util/Watchdog.swift
+++ b/KeePassium/util/Watchdog.swift
@@ -59,6 +59,7 @@ class Watchdog {
     
     private var appLockTimer: Timer?
     private var databaseLockTimer: Timer?
+    private var isIgnoringMinimizationOnce = false
     
     init() {
         NotificationCenter.default.addObserver(
@@ -82,7 +83,12 @@ class Watchdog {
         print("App did become active")
         restartAppTimer()
         restartDatabaseTimer()
-        maybeLockSomething()
+        if isIgnoringMinimizationOnce {
+            Diag.debug("Self-backgrounding ignored.")
+            isIgnoringMinimizationOnce = false
+        } else {
+            maybeLockSomething()
+        }
         delegate?.hideAppCover(self)
     }
     
@@ -97,13 +103,13 @@ class Watchdog {
         if delegate.isAppLocked { return }
 
         let databaseTimeout = Settings.current.premiumDatabaseLockTimeout
-        if databaseTimeout == .immediately {
+        if databaseTimeout == .immediately && !isIgnoringMinimizationOnce {
             Diag.debug("Going to background: Database Lock engaged")
             engageDatabaseLock()
         }
         
         let appTimeout = Settings.current.appLockTimeout
-        if appTimeout.triggerMode == .appMinimized {
+        if appTimeout.triggerMode == .appMinimized && !isIgnoringMinimizationOnce {
             Diag.debug("Going to background: App Lock engaged")
             Watchdog.shared.restart() 
         }
@@ -132,6 +138,11 @@ class Watchdog {
         }
     }
     
+    open func ignoreMinimizationOnce() {
+        assert(!isIgnoringMinimizationOnce)
+        isIgnoringMinimizationOnce = true
+    }
+    
     open func restart() {
         guard let delegate = delegate else { return }
         guard !delegate.isAppLocked else { return }
diff --git a/KeePassium/views/KeyFileTextField.swift b/KeePassium/views/KeyFileTextField.swift
new file mode 100644
index 000000000..69eb8a023
--- /dev/null
+++ b/KeePassium/views/KeyFileTextField.swift
@@ -0,0 +1,76 @@
+//
+//  KeyFileTextField.swift
+//  KeePassium
+//
+//  Created by Andrei on 25/11/2019.
+//  Copyright © 2019 Andrei Popleteev. All rights reserved.
+//
+
+import UIKit
+
+typealias YubiHandler = ((KeyFileTextField)->Void)
+
+class KeyFileTextField: ValidatingTextField {
+    private let horizontalInsets = CGFloat(8.0)
+    private let verticalInsets = CGFloat(2.0)
+    
+    private let yubiKeyOnImage = UIImage(asset: .yubikeyOnAccessory)
+    private let yubiKeyOffImage = UIImage(asset: .yubikeyOffAccessory)
+    
+    private var yubiButton: UIButton! 
+    public var isYubiKeyActive: Bool = false {
+        didSet {
+            guard let button = rightView as? UIButton else { return }
+            button.isSelected = isYubiKeyActive
+            setNeedsDisplay()
+        }
+    }
+    
+    public var yubikeyHandler: YubiHandler? = nil
+    
+    override func awakeFromNib() {
+        super.awakeFromNib()
+        setupYubiButton()
+    }
+    
+    private func setupYubiButton() {
+        let yubiButton = UIButton(type: .custom)
+        yubiButton.tintColor = UIColor.actionTint
+        yubiButton.addTarget(self, action: #selector(didPressYubiButton), for: .touchUpInside)
+        yubiButton.setImage(yubiKeyOffImage, for: .normal)
+        yubiButton.setImage(yubiKeyOnImage, for: .selected)
+        
+        let horizontalInsets = CGFloat(8.0)
+        let verticalInsets = CGFloat(2.0)
+        yubiButton.imageEdgeInsets = UIEdgeInsets(
+            top: verticalInsets,
+            left: horizontalInsets,
+            bottom: verticalInsets,
+            right: horizontalInsets)
+        yubiButton.frame = CGRect(
+            x: 0.0,
+            y: 0.0,
+            width: yubiKeyOffImage.size.width + 2 * horizontalInsets,
+            height: yubiKeyOffImage.size.height + 2 * verticalInsets)
+        yubiButton.isAccessibilityElement = true
+        yubiButton.accessibilityLabel = NSLocalizedString(
+            "[Database/Unlock] YubiKey",
+            value: "YubiKey",
+            comment: "Action/button to setup YubiKey key component")
+        self.yubiButton = yubiButton
+        self.rightView = yubiButton
+        self.rightViewMode = .always
+    }
+    
+    override func rightViewRect(forBounds bounds: CGRect) -> CGRect {
+        return CGRect(
+            x: bounds.maxX - yubiKeyOffImage.size.width - 2 * horizontalInsets,
+            y: bounds.midY - yubiKeyOffImage.size.height / 2 - verticalInsets,
+            width: yubiKeyOffImage.size.width + 2 * horizontalInsets,
+            height: yubiKeyOffImage.size.height + 2 * verticalInsets)
+    }
+    
+    @objc private func didPressYubiButton(_ sender: Any) {
+        self.yubikeyHandler?(self)
+    }
+}
diff --git a/KeePassiumLib/KeePassiumLib.xcodeproj/project.pbxproj b/KeePassiumLib/KeePassiumLib.xcodeproj/project.pbxproj
index 8224c3522..adb03290a 100755
--- a/KeePassiumLib/KeePassiumLib.xcodeproj/project.pbxproj
+++ b/KeePassiumLib/KeePassiumLib.xcodeproj/project.pbxproj
@@ -18,8 +18,11 @@
 		75205FA5239C01CF00C1DEAA /* DatabaseSettingsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75205FA4239C01CF00C1DEAA /* DatabaseSettingsManager.swift */; };
 		7538167E22B15FC7000FA4BD /* Settings+premium.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7538167D22B15FC7000FA4BD /* Settings+premium.swift */; };
 		75384B9922CCB76800157598 /* UsageMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75384B9822CCB76800157598 /* UsageMonitor.swift */; };
+		75393ABA238D3AC400C63C54 /* ChallengeResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75393AB9238D3AC400C63C54 /* ChallengeResponse.swift */; };
+		754D7C0E238C26A7005652E1 /* YubiKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 754D7C0D238C26A7005652E1 /* YubiKey.swift */; };
 		7558700722E5857C00E1DD89 /* TOTPGeneratorFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7558700622E5857C00E1DD89 /* TOTPGeneratorFactory.swift */; };
 		755E7DA7232021460010E199 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 755E7DA5232021460010E199 /* InfoPlist.strings */; };
+		757CB44223993E48000867F0 /* DatabaseSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 757CB44123993E48000867F0 /* DatabaseSettings.swift */; };
 		759896432341CED300DF39AA /* BusinessModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 759896422341CED300DF39AA /* BusinessModel.swift */; };
 		75A26770228DAC440097C693 /* PremiumManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75A2676F228DAC430097C693 /* PremiumManager.swift */; };
 		75A2677B229045A60097C693 /* PremiumFeature.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75A2677A229045A60097C693 /* PremiumFeature.swift */; };
@@ -107,6 +110,7 @@
 		75D09C602174F81900F7DEF2 /* Parser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75D09C562174F81900F7DEF2 /* Parser.swift */; };
 		75D09C622174F89A00F7DEF2 /* ProgressEx.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75D09C612174F89A00F7DEF2 /* ProgressEx.swift */; };
 		75D0C77B2174AF2000C64C93 /* KeePassiumLib.h in Headers */ = {isa = PBXBuildFile; fileRef = 75D0C76D2174AF1F00C64C93 /* KeePassiumLib.h */; settings = {ATTRIBUTES = (Public, ); }; };
+		75D4080523952FD40042122A /* CompositeKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75D4080423952FD40042122A /* CompositeKey.swift */; };
 		75EA2D1721B2FB4000826FC6 /* Diag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75EA2D1621B2FB4000826FC6 /* Diag.swift */; };
 		75F4ACD22177456200B22D8B /* PasswordGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75F4ACD12177456200B22D8B /* PasswordGenerator.swift */; };
 		75F4ACD5217745B300B22D8B /* DatabaseManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75F4ACD3217745B300B22D8B /* DatabaseManager.swift */; };
@@ -141,8 +145,11 @@
 		75205FA4239C01CF00C1DEAA /* DatabaseSettingsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseSettingsManager.swift; sourceTree = "<group>"; };
 		7538167D22B15FC7000FA4BD /* Settings+premium.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Settings+premium.swift"; sourceTree = "<group>"; };
 		75384B9822CCB76800157598 /* UsageMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UsageMonitor.swift; sourceTree = "<group>"; };
+		75393AB9238D3AC400C63C54 /* ChallengeResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChallengeResponse.swift; sourceTree = "<group>"; };
+		754D7C0D238C26A7005652E1 /* YubiKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = YubiKey.swift; path = ../../../../../../projects/xcode/KeePassium/KeePassiumLib/KeePassiumLib/util/YubiKey.swift; sourceTree = "<group>"; };
 		7558700622E5857C00E1DD89 /* TOTPGeneratorFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TOTPGeneratorFactory.swift; sourceTree = "<group>"; };
 		755E7DA6232021460010E199 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/InfoPlist.strings; sourceTree = "<group>"; };
+		757CB44123993E48000867F0 /* DatabaseSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseSettings.swift; sourceTree = "<group>"; };
 		759896422341CED300DF39AA /* BusinessModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = BusinessModel.swift; path = ../../../../../../projects/xcode/KeePassium/KeePassiumLib/KeePassiumLib/premium/BusinessModel.swift; sourceTree = "<group>"; };
 		75A2676F228DAC430097C693 /* PremiumManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PremiumManager.swift; sourceTree = "<group>"; };
 		75A2677A229045A60097C693 /* PremiumFeature.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PremiumFeature.swift; sourceTree = "<group>"; };
@@ -240,6 +247,7 @@
 		75D0C76A2174AF1F00C64C93 /* KeePassiumLib.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = KeePassiumLib.framework; sourceTree = BUILT_PRODUCTS_DIR; };
 		75D0C76D2174AF1F00C64C93 /* KeePassiumLib.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KeePassiumLib.h; sourceTree = "<group>"; };
 		75D0C76E2174AF1F00C64C93 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
+		75D4080423952FD40042122A /* CompositeKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompositeKey.swift; sourceTree = "<group>"; };
 		75E4CA89232D0B2200EB6805 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = "<group>"; };
 		75EA2D1621B2FB4000826FC6 /* Diag.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Diag.swift; sourceTree = "<group>"; };
 		75F4ACC1217743BF00B22D8B /* DiagLib.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = DiagLib.framework; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -294,6 +302,14 @@
 			path = base32;
 			sourceTree = "<group>";
 		};
+		7571D5882393F7420045E760 /* challenge */ = {
+			isa = PBXGroup;
+			children = (
+				75393AB9238D3AC400C63C54 /* ChallengeResponse.swift */,
+			);
+			path = challenge;
+			sourceTree = "<group>";
+		};
 		75A267792290458E0097C693 /* premium */ = {
 			isa = PBXGroup;
 			children = (
@@ -395,6 +411,7 @@
 			children = (
 				7505E8CC2236E69F00E5202E /* totp */,
 				75D09BE82174F77000F7DEF2 /* kdf */,
+				7571D5882393F7420045E760 /* challenge */,
 				75D09C082174F77000F7DEF2 /* cipher */,
 				75D09C122174F77000F7DEF2 /* kp1 */,
 				75D09BEE2174F77000F7DEF2 /* kp2 */,
@@ -406,6 +423,7 @@
 				75D09C072174F77000F7DEF2 /* KeyHelper.swift */,
 				75D09BED2174F77000F7DEF2 /* Attachment.swift */,
 				75D09BFB2174F77000F7DEF2 /* IconID.swift */,
+				75D4080423952FD40042122A /* CompositeKey.swift */,
 			);
 			path = db;
 			sourceTree = "<group>";
@@ -459,6 +477,8 @@
 				750935C3235219B900A362C8 /* Observable.swift */,
 				75205FA2239BDB6700C1DEAA /* DatabaseSettings.swift */,
 				75205FA4239C01CF00C1DEAA /* DatabaseSettingsManager.swift */,
+				754D7C0D238C26A7005652E1 /* YubiKey.swift */,
+				757CB44123993E48000867F0 /* DatabaseSettings.swift */,
 			);
 			path = util;
 			sourceTree = "<group>";
@@ -734,6 +754,7 @@
 				7505E8CF2236ED6E00E5202E /* Base32.swift in Sources */,
 				75F4ACF321774A6400B22D8B /* Clipboard.swift in Sources */,
 				75D09C292174F77000F7DEF2 /* Group2.swift in Sources */,
+				757CB44223993E48000867F0 /* DatabaseSettings.swift in Sources */,
 				75D09C342174F77000F7DEF2 /* Bool+extension.swift in Sources */,
 				75D09C392174F77000F7DEF2 /* TwofishDataCipher.swift in Sources */,
 				75D09C2B2174F77000F7DEF2 /* Meta2.swift in Sources */,
@@ -759,6 +780,7 @@
 				75D09C282174F77000F7DEF2 /* CustomIcon2.swift in Sources */,
 				75F4ACE42177469500B22D8B /* FileKeeperObserver.swift in Sources */,
 				75D09B8E2174F02F00F7DEF2 /* Salsa20.swift in Sources */,
+				75393ABA238D3AC400C63C54 /* ChallengeResponse.swift in Sources */,
 				75F4ACEC2177485D00B22D8B /* Weak.swift in Sources */,
 				75D09C1D2174F77000F7DEF2 /* KDFParams.swift in Sources */,
 				75D09BE42174F76500F7DEF2 /* encoding.c in Sources */,
@@ -771,6 +793,7 @@
 				75D09BD42174F76500F7DEF2 /* twofish.cpp in Sources */,
 				75D09BD32174F76500F7DEF2 /* Twofish.swift in Sources */,
 				75205FA5239C01CF00C1DEAA /* DatabaseSettingsManager.swift in Sources */,
+				754D7C0E238C26A7005652E1 /* YubiKey.swift in Sources */,
 				7505E8D12236FA0F00E5202E /* StringExtension.swift in Sources */,
 				75D09C432174F77000F7DEF2 /* DateExtension-kp1.swift in Sources */,
 				75D09C322174F77000F7DEF2 /* String+extension.swift in Sources */,
@@ -800,6 +823,7 @@
 				75D09BA52174F02F00F7DEF2 /* CryptoError.swift in Sources */,
 				7505E8CB2236C93300E5202E /* TOTPGenerator.swift in Sources */,
 				75D09C402174F77000F7DEF2 /* Eraseable.swift in Sources */,
+				75D4080523952FD40042122A /* CompositeKey.swift in Sources */,
 				75D09C5C2174F81900F7DEF2 /* Error.swift in Sources */,
 				75D09C3B2174F77000F7DEF2 /* DataCipherFactory.swift in Sources */,
 				75D09C272174F77000F7DEF2 /* KeyHelper2.swift in Sources */,
diff --git a/KeePassiumLib/KeePassiumLib/DatabaseManager.swift b/KeePassiumLib/KeePassiumLib/DatabaseManager.swift
index 0c2a8a686..6ca52ba7f 100755
--- a/KeePassiumLib/KeePassiumLib/DatabaseManager.swift
+++ b/KeePassiumLib/KeePassiumLib/DatabaseManager.swift
@@ -36,6 +36,9 @@ public class DatabaseManager {
     public var isDatabaseOpen: Bool { return database != nil }
     
     private var databaseDocument: DatabaseDocument?
+    private var databaseLoader: DatabaseLoader?
+    private var databaseSaver: DatabaseSaver?
+    
     private var serialDispatchQueue = DispatchQueue(
         label: "com.keepassium.DatabaseManager",
         qos: .userInitiated)
@@ -102,28 +105,29 @@ public class DatabaseManager {
     public func startLoadingDatabase(
         database dbRef: URLReference,
         password: String,
-        keyFile keyFileRef: URLReference?)
+        keyFile keyFileRef: URLReference?,
+        challengeHandler: ChallengeHandler?)
     {
         Diag.verbose("Will queue load database")
+        let compositeKey = CompositeKey(
+            password: password,
+            keyFileRef: keyFileRef,
+            challengeHandler: challengeHandler
+        )
         serialDispatchQueue.async {
-            self._loadDatabase(dbRef: dbRef, compositeKey: nil, password: password, keyFileRef: keyFileRef)
+            self._loadDatabase(dbRef: dbRef, compositeKey: compositeKey)
         }
     }
     
-    public func startLoadingDatabase(database dbRef: URLReference, compositeKey: SecureByteArray) {
+    public func startLoadingDatabase(database dbRef: URLReference, compositeKey: CompositeKey) {
         Diag.verbose("Will queue load database")
-        let compositeKeyClone = compositeKey.secureClone()
+        let compositeKeyClone = compositeKey.clone()
         serialDispatchQueue.async {
-            self._loadDatabase(dbRef: dbRef, compositeKey: compositeKeyClone, password: "", keyFileRef: nil)
+            self._loadDatabase(dbRef: dbRef, compositeKey: compositeKeyClone)
         }
     }
     
-    private func _loadDatabase(
-        dbRef: URLReference,
-        compositeKey: SecureByteArray?,
-        password: String,
-        keyFileRef: URLReference?)
-    {
+    private func _loadDatabase(dbRef: URLReference, compositeKey: CompositeKey) {
         precondition(database == nil, "Can only load one database at a time")
 
         Diag.info("Will load database")
@@ -131,19 +135,19 @@ public class DatabaseManager {
         progress.totalUnitCount = ProgressSteps.all
         progress.completedUnitCount = 0
         
-        let dbLoader = DatabaseLoader(
+        precondition(databaseLoader == nil)
+        databaseLoader = DatabaseLoader(
             dbRef: dbRef,
             compositeKey: compositeKey,
-            password: password,
-            keyFileRef: keyFileRef,
             progress: progress,
-            completion: databaseLoaded)
-        dbLoader.load()
+            completion: databaseLoaderFinished)
+        databaseLoader!.load()
     }
     
-    private func databaseLoaded(_ dbDoc: DatabaseDocument, _ dbRef: URLReference) {
-        self.databaseDocument = dbDoc
+    private func databaseLoaderFinished(_ dbRef: URLReference, _ dbDoc: DatabaseDocument?) {
         self.databaseRef = dbRef
+        self.databaseDocument = dbDoc
+        self.databaseLoader = nil
     }
 
     public func rememberDatabaseKey(onlyIfExists: Bool = false) throws {
@@ -171,7 +175,10 @@ public class DatabaseManager {
         }
     }
     
-    private func _saveDatabase(_ dbDoc: DatabaseDocument, dbRef: URLReference) {
+    private func _saveDatabase(
+        _ dbDoc: DatabaseDocument,
+        dbRef: URLReference)
+    {
         precondition(database != nil, "No database to save")
         Diag.info("Saving database")
         
@@ -180,18 +187,20 @@ public class DatabaseManager {
         progress.completedUnitCount = 0
         notifyDatabaseWillSave(database: dbRef)
         
-        let dbSaver = DatabaseSaver(
+        precondition(databaseSaver == nil)
+        databaseSaver = DatabaseSaver(
             databaseDocument: dbDoc,
             databaseRef: dbRef,
             progress: progress,
-            completion: databaseSaved)
-        dbSaver.save()
+            completion: databaseSaverFinished)
+        databaseSaver!.save()
     }
     
-    private func databaseSaved(_ dbDoc: DatabaseDocument) {
+    private func databaseSaverFinished(_ urlRef: URLReference, _ dbDoc: DatabaseDocument) {
+        databaseSaver = nil
     }
     
-    public func changeCompositeKey(to newKey: SecureByteArray) {
+    public func changeCompositeKey(to newKey: CompositeKey) {
         database?.changeCompositeKey(to: newKey)
         Diag.info("Database composite key changed")
     }
@@ -200,7 +209,8 @@ public class DatabaseManager {
         keyHelper: KeyHelper,
         password: String,
         keyFile keyFileRef: URLReference?,
-        success successHandler: @escaping((_ combinedKey: SecureByteArray) -> Void),
+        challengeHandler: ChallengeHandler?,
+        success successHandler: @escaping((_ compositeKey: CompositeKey) -> Void),
         error errorHandler: @escaping((_ errorMessage: String) -> Void))
     {
         let dataReadyHandler = { (keyFileData: ByteArray) -> Void in
@@ -214,10 +224,11 @@ public class DatabaseManager {
                     comment: "Error message"))
                 return
             }
-            let compositeKey = keyHelper.makeCompositeKey(
-                passwordData: passwordData,
-                keyFileData: keyFileData)
             Diag.debug("New composite key created successfully")
+            let compositeKey = CompositeKey(
+                passwordData: passwordData,
+                keyFileData: keyFileData,
+                challengeHandler: challengeHandler)
             successHandler(compositeKey)
         }
         
@@ -256,6 +267,7 @@ public class DatabaseManager {
         databaseURL: URL,
         password: String,
         keyFile: URLReference?,
+        challengeHandler: ChallengeHandler?,
         template templateSetupHandler: @escaping (Group2) -> Void,
         success successHandler: @escaping () -> Void,
         error errorHandler: @escaping ((String?) -> Void))
@@ -272,6 +284,7 @@ public class DatabaseManager {
             keyHelper: db2.keyHelper,
             password: password,
             keyFile: keyFile,
+            challengeHandler: challengeHandler,
             success: { 
                 (newCompositeKey) in
                 DatabaseManager.shared.changeCompositeKey(to: newCompositeKey)
@@ -492,28 +505,25 @@ public class DatabaseManager {
 
 
 fileprivate class DatabaseLoader {
+    typealias CompletionHandler = (URLReference, DatabaseDocument?) -> Void
+    
     private let dbRef: URLReference
-    private let compositeKey: SecureByteArray?
-    private let password: String
-    private let keyFileRef: URLReference?
+    private let compositeKey: CompositeKey
     private let progress: ProgressEx
     private var progressKVO: NSKeyValueObservation?
     private unowned var notifier: DatabaseManager
     private let warnings: DatabaseLoadingWarnings
-    private let completion: ((DatabaseDocument, URLReference) -> Void)
+    private let completion: CompletionHandler
     
     init(
         dbRef: URLReference,
-        compositeKey: SecureByteArray?,
-        password: String,
-        keyFileRef: URLReference?,
+        compositeKey: CompositeKey,
         progress: ProgressEx,
-        completion: @escaping((DatabaseDocument, URLReference) -> Void))
+        completion: @escaping(CompletionHandler))
     {
+        assert(compositeKey.state != .empty)
         self.dbRef = dbRef
         self.compositeKey = compositeKey
-        self.password = password
-        self.keyFileRef = keyFileRef
         self.progress = progress
         self.completion = completion
         self.warnings = DatabaseLoadingWarnings()
@@ -597,6 +607,7 @@ fileprivate class DatabaseLoader {
                     value: "Cannot find database file",
                     comment: "Error message"),
                 reason: error.localizedDescription)
+            completion(dbRef, nil)
             endBackgroundTask()
             return
         }
@@ -624,6 +635,7 @@ fileprivate class DatabaseLoader {
                         value: "Cannot open database file",
                         comment: "Error message"),
                     reason: errorMessage)
+                self.completion(self.dbRef, nil)
                 self.endBackgroundTask()
             }
         )
@@ -644,19 +656,21 @@ fileprivate class DatabaseLoader {
                     value: "Unrecognized database format",
                     comment: "Error message"),
                 reason: nil)
+            completion(dbRef, nil)
             endBackgroundTask()
             return
         }
         
         dbDoc.database = db
-        if let compositeKey = compositeKey {
+        guard compositeKey.state == .rawComponents else {
+            
             progress.completedUnitCount += ProgressSteps.readKeyFile
             Diag.info("Using a ready composite key")
-            onCompositeKeyReady(dbDoc: dbDoc, compositeKey: compositeKey)
+            onCompositeKeyComponentsProcessed(dbDoc: dbDoc, compositeKey: compositeKey)
             return
         }
         
-        if let keyFileRef = keyFileRef {
+        if let keyFileRef = compositeKey.keyFileRef {
             Diag.debug("Loading key file")
             progress.localizedDescription = NSLocalizedString(
                 "[Database/Progress] Loading key file...",
@@ -679,6 +693,7 @@ fileprivate class DatabaseLoader {
                         value: "Cannot find key file",
                         comment: "Error message"),
                     reason: error.localizedDescription)
+                completion(dbRef, nil)
                 endBackgroundTask()
                 return
             }
@@ -701,6 +716,7 @@ fileprivate class DatabaseLoader {
                             value: "Cannot open key file",
                             comment: "Error message"),
                         reason: error.localizedDescription)
+                    self.completion(self.dbRef, nil)
                     self.endBackgroundTask()
                 }
             )
@@ -714,7 +730,7 @@ fileprivate class DatabaseLoader {
         
         progress.completedUnitCount += ProgressSteps.readKeyFile
         let keyHelper = database.keyHelper
-        let passwordData = keyHelper.getPasswordData(password: password)
+        let passwordData = keyHelper.getPasswordData(password: compositeKey.password)
         if passwordData.isEmpty && keyFileData.isEmpty {
             Diag.error("Both password and key file are empty")
             stopObservingProgress()
@@ -725,16 +741,16 @@ fileprivate class DatabaseLoader {
                     bundle: Bundle.framework,
                     value: "Please provide at least a password or a key file",
                     comment: "Error shown when both master password and key file are empty"))
+            completion(dbRef, nil)
             endBackgroundTask()
             return
         }
-        let compositeKey = keyHelper.makeCompositeKey(
-            passwordData: passwordData,
-            keyFileData: keyFileData)
-        onCompositeKeyReady(dbDoc: dbDoc, compositeKey: compositeKey)
+        compositeKey.setProcessedComponents(passwordData: passwordData, keyFileData: keyFileData)
+        onCompositeKeyComponentsProcessed(dbDoc: dbDoc, compositeKey: compositeKey)
     }
     
-    func onCompositeKeyReady(dbDoc: DatabaseDocument, compositeKey: SecureByteArray) {
+    func onCompositeKeyComponentsProcessed(dbDoc: DatabaseDocument, compositeKey: CompositeKey) {
+        assert(compositeKey.state >= .processedComponents)
         guard let db = dbDoc.database else { fatalError() }
         do {
             progress.addChild(db.initProgress(), withPendingUnitCount: ProgressSteps.decryptDatabase)
@@ -743,18 +759,18 @@ fileprivate class DatabaseLoader {
                 dbFileName: dbDoc.fileURL.lastPathComponent,
                 dbFileData: dbDoc.encryptedData,
                 compositeKey: compositeKey,
-                warnings: warnings
-            )
+                warnings: warnings)
             Diag.info("Database loaded OK")
             progress.localizedDescription = NSLocalizedString(
                 "[Database/Progress] Done",
                 bundle: Bundle.framework,
                 value: "Done",
                 comment: "Progress status: finished loading database")
-            completion(dbDoc, dbRef)
+            completion(dbRef, dbDoc)
             stopObservingProgress()
             notifier.notifyDatabaseDidLoad(database: dbRef, warnings: warnings)
             endBackgroundTask()
+            
         } catch let error as DatabaseError {
             dbDoc.database = nil
             dbDoc.close(completionHandler: nil)
@@ -772,18 +788,18 @@ fileprivate class DatabaseLoader {
                     isCancelled: progress.isCancelled,
                     message: error.localizedDescription,
                     reason: error.failureReason)
-                endBackgroundTask()
             case .invalidKey:
                 Diag.error("Invalid master key. [message: \(error.localizedDescription)]")
                 stopObservingProgress()
                 notifier.notifyDatabaseInvalidMasterKey(
                     database: dbRef,
                     message: error.localizedDescription)
-                endBackgroundTask()
             case .saveError:
                 Diag.error("saveError while loading?!")
                 fatalError("Database saving error while loading?!")
             }
+            completion(dbRef, nil)
+            endBackgroundTask()
         } catch let error as ProgressInterruption {
             dbDoc.database = nil
             dbDoc.close(completionHandler: nil)
@@ -805,9 +821,11 @@ fileprivate class DatabaseLoader {
                         message: error.localizedDescription,
                         reason: nil)
                 }
+                completion(dbRef, nil)
                 endBackgroundTask()
             }
         } catch {
+            assertionFailure("Unprocessed exception")
             dbDoc.database = nil
             dbDoc.close(completionHandler: nil)
             Diag.error("Unexpected error [message: \(error.localizedDescription)]")
@@ -817,6 +835,7 @@ fileprivate class DatabaseLoader {
                 isCancelled: progress.isCancelled,
                 message: error.localizedDescription,
                 reason: nil)
+            completion(dbRef, nil)
             endBackgroundTask()
         }
     }
@@ -824,18 +843,20 @@ fileprivate class DatabaseLoader {
 
 
 fileprivate class DatabaseSaver {
+    typealias CompletionHandler = (URLReference, DatabaseDocument) -> Void
+    
     private let dbDoc: DatabaseDocument
     private let dbRef: URLReference
     private let progress: ProgressEx
     private var progressKVO: NSKeyValueObservation?
     private unowned var notifier: DatabaseManager
-    private let completion: ((DatabaseDocument) -> Void)
+    private let completion: CompletionHandler
 
     init(
         databaseDocument dbDoc: DatabaseDocument,
         databaseRef dbRef: URLReference,
         progress: ProgressEx,
-        completion: @escaping((DatabaseDocument) -> Void))
+        completion: @escaping(CompletionHandler))
     {
         assert(dbDoc.documentState.contains(.normal))
         self.dbDoc = dbDoc
@@ -890,6 +911,7 @@ fileprivate class DatabaseSaver {
     
     func save() {
         guard let database = dbDoc.database else { fatalError("Database is nil") }
+        
         startBackgroundTask()
         startObservingProgress()
         do {
@@ -912,7 +934,7 @@ fileprivate class DatabaseSaver {
                     Diag.info("Database saved OK")
                     self.stopObservingProgress()
                     self.notifier.notifyDatabaseDidSave(database: self.dbRef)
-                    self.completion(self.dbDoc)
+                    self.completion(self.dbRef, self.dbDoc)
                     self.endBackgroundTask()
                 },
                 errorHandler: {
@@ -924,6 +946,7 @@ fileprivate class DatabaseSaver {
                         isCancelled: self.progress.isCancelled,
                         message: errorMessage ?? "",
                         reason: nil)
+                    self.completion(self.dbRef, self.dbDoc)
                     self.endBackgroundTask()
                 }
             )
@@ -940,6 +963,7 @@ fileprivate class DatabaseSaver {
                 isCancelled: progress.isCancelled,
                 message: error.localizedDescription,
                 reason: error.failureReason)
+            completion(dbRef, dbDoc)
             endBackgroundTask()
         } catch let error as ProgressInterruption {
             stopObservingProgress()
@@ -960,6 +984,7 @@ fileprivate class DatabaseSaver {
                         message: error.localizedDescription,
                         reason: nil)
                 }
+                completion(dbRef, dbDoc)
                 endBackgroundTask()
             }
         } catch { 
@@ -970,6 +995,7 @@ fileprivate class DatabaseSaver {
                 isCancelled: progress.isCancelled,
                 message: error.localizedDescription,
                 reason: nil)
+            completion(dbRef, dbDoc)
             endBackgroundTask()
         }
     }
diff --git a/KeePassiumLib/KeePassiumLib/db/CompositeKey.swift b/KeePassiumLib/KeePassiumLib/db/CompositeKey.swift
new file mode 100644
index 000000000..4fbd24bdd
--- /dev/null
+++ b/KeePassiumLib/KeePassiumLib/db/CompositeKey.swift
@@ -0,0 +1,198 @@
+//  KeePassium Password Manager
+//  Copyright © 2018–2019 Andrei Popleteev <info@keepassium.com>
+//
+//  This program is free software: you can redistribute it and/or modify it
+//  under the terms of the GNU General Public License version 3 as published
+//  by the Free Software Foundation: https://www.gnu.org/licenses/).
+//  For commercial licensing, please contact the author.
+
+import Foundation
+
+public class CompositeKey: Codable {
+    public enum State: Int, Comparable, Codable {
+        case empty               = 0 
+        case rawComponents       = 1 
+        case processedComponents = 2 
+        case combinedComponents  = 3 
+        case final = 4
+        
+        public static func < (lhs: CompositeKey.State, rhs: CompositeKey.State) -> Bool {
+            return lhs.rawValue < rhs.rawValue
+        }
+    }
+    
+    static let empty = CompositeKey()
+
+    internal private(set) var state: State
+    
+    internal private(set) var password: String = ""
+    internal private(set) var keyFileRef: URLReference?
+    public var challengeHandler: ChallengeHandler? 
+    
+    internal private(set) var passwordData: SecureByteArray?
+    internal private(set) var keyFileData: ByteArray?
+    
+    internal private(set) var combinedStaticComponents: SecureByteArray?
+    
+    internal private(set) var finalKey: SecureByteArray?
+    
+    
+    init() {
+        self.password = ""
+        self.keyFileRef = nil
+        self.challengeHandler = nil
+        state = .empty
+    }
+    
+    init(password: String, keyFileRef: URLReference?, challengeHandler: ChallengeHandler?) {
+        self.password = password
+        self.keyFileRef = keyFileRef
+        self.challengeHandler = challengeHandler
+        state = .rawComponents
+    }
+    
+    init(
+        passwordData: SecureByteArray,
+        keyFileData: ByteArray,
+        challengeHandler: ChallengeHandler?)
+    {
+        self.password = ""
+        self.keyFileRef = nil
+        self.passwordData = passwordData
+        self.keyFileData = keyFileData
+        self.challengeHandler = challengeHandler
+        
+        state = .processedComponents
+    }
+    
+    init(staticComponents: SecureByteArray, challengeHandler: ChallengeHandler?) {
+        self.password = ""
+        self.keyFileRef = nil
+        self.passwordData = nil
+        self.keyFileData = nil
+        self.combinedStaticComponents = staticComponents
+        self.challengeHandler = challengeHandler
+        state = .combinedComponents
+    }
+    
+    deinit {
+        erase()
+    }
+    
+    func erase() {
+        keyFileRef = nil
+        challengeHandler = nil
+        passwordData = nil
+        keyFileData = nil
+        combinedStaticComponents = nil
+        
+        state = .empty
+    }
+
+    
+    private enum CodingKeys: String, CodingKey {
+        case state
+        case passwordData
+        case keyFileData
+        case combinedStaticComponents = "staticComponents"
+        case finalKey
+    }
+    
+    internal func serialize() -> SecureByteArray {
+        let encoder = JSONEncoder()
+        let encodedBytes = SecureByteArray(data: try! encoder.encode(self))
+        return encodedBytes
+    }
+    
+    internal static func deserialize(from bytes: SecureByteArray?) -> CompositeKey? {
+        guard let data = bytes?.asData else { return nil }
+        let decoder = JSONDecoder()
+        let result = try? decoder.decode(CompositeKey.self, from: data)
+        return result
+    }
+    
+    
+    public func clone() -> CompositeKey {
+        let clone = CompositeKey(
+            password: self.password,
+            keyFileRef: self.keyFileRef,
+            challengeHandler: self.challengeHandler)
+        clone.passwordData = self.passwordData?.secureClone()
+        clone.keyFileData = self.keyFileData?.clone()
+        clone.combinedStaticComponents = self.combinedStaticComponents?.secureClone()
+        clone.finalKey = self.finalKey?.secureClone()
+        clone.state = self.state
+        return clone
+    }
+    
+    func setProcessedComponents(passwordData: SecureByteArray, keyFileData: ByteArray) {
+        assert(state == .rawComponents)
+        self.passwordData = passwordData.secureClone()
+        self.keyFileData = keyFileData.clone()
+        state = .processedComponents
+        
+        self.password.erase()
+        self.keyFileRef = nil
+        self.finalKey?.erase()
+        self.finalKey = nil
+    }
+    
+    func setCombinedStaticComponents(_ staticComponents: SecureByteArray) {
+        assert(state <= .combinedComponents)
+        self.combinedStaticComponents = staticComponents.secureClone()
+        state = .combinedComponents
+        
+        self.password.erase()
+        self.keyFileRef = nil
+        self.passwordData?.erase()
+        self.passwordData = nil
+        self.keyFileData?.erase()
+        self.keyFileData = nil
+        
+        self.finalKey?.erase()
+        self.finalKey = nil
+    }
+    
+    func setFinalKey(_ finalKey: SecureByteArray) {
+        assert(state >= .combinedComponents)
+        self.finalKey = finalKey.secureClone()
+        state = .final
+    }
+    
+    func getResponse(challenge: SecureByteArray) throws -> SecureByteArray  {
+        guard let handler = self.challengeHandler else {
+            return SecureByteArray()
+        }
+        
+        
+        var response: SecureByteArray?
+        var challengeError: ChallengeResponseError?
+        let responseReadySemaphore = DispatchSemaphore(value: 0)
+        DispatchQueue.global(qos: .default).async {
+            handler(challenge) {
+                (_response, _error) in
+                if let _error = _error {
+                    challengeError = _error
+                    responseReadySemaphore.signal()
+                    return
+                }
+                response = _response
+                responseReadySemaphore.signal()
+            }
+        }
+        responseReadySemaphore.wait()
+        
+        if let challengeError = challengeError {
+            switch challengeError {
+            case .cancelled:
+                throw ProgressInterruption.cancelled(reason: ProgressEx.CancellationReason.userRequest)
+            default:
+                throw challengeError 
+            }
+        }
+        if let response = response {
+            return response.sha256
+        }
+        preconditionFailure("You should not be here")
+    }
+}
diff --git a/KeePassiumLib/KeePassiumLib/db/Database.swift b/KeePassiumLib/KeePassiumLib/db/Database.swift
index 18685dacb..be6a359fb 100755
--- a/KeePassiumLib/KeePassiumLib/db/Database.swift
+++ b/KeePassiumLib/KeePassiumLib/db/Database.swift
@@ -89,7 +89,7 @@ open class Database: Eraseable {
 
     public internal(set) var progress = ProgressEx()
 
-    internal var compositeKey = SecureByteArray()
+    internal var compositeKey = CompositeKey.empty
     
     public func initProgress() -> ProgressEx {
         progress = ProgressEx()
@@ -121,7 +121,7 @@ open class Database: Eraseable {
     public func load(
         dbFileName: String,
         dbFileData: ByteArray,
-        compositeKey: SecureByteArray,
+        compositeKey: CompositeKey,
         warnings: DatabaseLoadingWarnings
     ) throws {
         fatalError("Pure virtual method")
@@ -131,7 +131,7 @@ open class Database: Eraseable {
         fatalError("Pure virtual method")
     }
     
-    public func changeCompositeKey(to newKey: SecureByteArray) {
+    public func changeCompositeKey(to newKey: CompositeKey) {
         fatalError("Pure virtual method")
     }
     
diff --git a/KeePassiumLib/KeePassiumLib/db/KeyHelper.swift b/KeePassiumLib/KeePassiumLib/db/KeyHelper.swift
index 1fb827b03..536aaf9be 100755
--- a/KeePassiumLib/KeePassiumLib/db/KeyHelper.swift
+++ b/KeePassiumLib/KeePassiumLib/db/KeyHelper.swift
@@ -12,7 +12,11 @@ public class KeyHelper {
     public static let compositeKeyLength = 32
     internal let keyFileKeyLength = 32
     
-    public func makeCompositeKey(passwordData: ByteArray, keyFileData: ByteArray) -> SecureByteArray {
+    public func combineComponents(passwordData: SecureByteArray, keyFileData: ByteArray) -> SecureByteArray {
+        fatalError("Pure virtual method")
+    }
+    
+    public func getKey(fromCombinedComponents combinedComponents: SecureByteArray) -> SecureByteArray {
         fatalError("Pure virtual method")
     }
     
diff --git a/KeePassiumLib/KeePassiumLib/db/challenge/ChallengeResponse.swift b/KeePassiumLib/KeePassiumLib/db/challenge/ChallengeResponse.swift
new file mode 100644
index 000000000..75d317fed
--- /dev/null
+++ b/KeePassiumLib/KeePassiumLib/db/challenge/ChallengeResponse.swift
@@ -0,0 +1,73 @@
+//  KeePassium Password Manager
+//  Copyright © 2018–2019 Andrei Popleteev <info@keepassium.com>
+//
+//  This program is free software: you can redistribute it and/or modify it
+//  under the terms of the GNU General Public License version 3 as published
+//  by the Free Software Foundation: https://www.gnu.org/licenses/).
+//  For commercial licensing, please contact the author.
+
+import Foundation
+
+public enum ChallengeResponseError: LocalizedError {
+    case notSupportedByDeviceOrSystem(interface: String)
+    case notSupportedByDatabaseFormat
+    case notAvailableInAutoFill
+    case keyNotConnected
+    case keyNotConfigured
+    
+    case cancelled
+    
+    case communicationError(message: String)
+    
+    
+    public var errorDescription: String? {
+        switch self {
+        case .notSupportedByDeviceOrSystem(let interface):
+            return String.localizedStringWithFormat(
+                NSLocalizedString(
+                    "[ChallengeResponseError] notSupportedByDeviceOrSystem",
+                    bundle: Bundle.framework,
+                    value: "This device or iOS version does not support hardware keys (%@)",
+                    comment: "Error message when trying to use a hardware challenge-response key on unsupported device/iOS version. %@ will be the name of the hardware interface, such as `NFC` or `Lightning`."),
+                interface)
+        case .notSupportedByDatabaseFormat:
+            return NSLocalizedString(
+                "[ChallengeResponseError] notSupportedByDatabaseFormat",
+                bundle: Bundle.framework,
+                value: "Hardware keys are not supported by this database format.",
+                comment: "Error message when trying to use challenge-response with a kdb database.")
+        case .notAvailableInAutoFill:
+            return NSLocalizedString(
+                "[ChallengeResponseError] notAvailableInAutoFill",
+                bundle: Bundle.framework,
+                value: "Hardware keys are not available in AutoFill.",
+                comment: "Error message when trying to use challenge-response hardware in AutoFill.")
+        case .keyNotConnected:
+            return NSLocalizedString(
+                "[ChallengeResponseError] keyNotConnected",
+                bundle: Bundle.framework,
+                value: "Hardware key is not connected.",
+                comment: "Error message when the hardware key is not plugged in.")
+        case .keyNotConfigured:
+            return NSLocalizedString(
+                "[ChallengeResponseError] slotNotConfigured",
+                bundle: Bundle.framework,
+                value: "Hardware key is not configured for challenge-response.",
+                comment: "Error message: the hardware key (or its slot) was not configured for challenge-response operations.")
+        case .cancelled:
+            return NSLocalizedString(
+                "[ChallengeResponseError] cancelled",
+                bundle: Bundle.framework,
+                value: "Cancelled by the user.",
+                comment: "Error message when the challenge-response communication has been cancelled by the user.")
+        case .communicationError(let message):
+            return message
+        }
+    }
+}
+
+public typealias ChallengeHandler =
+    (_ challenge: SecureByteArray, _ responseHandler: @escaping ResponseHandler) -> Void
+
+public typealias ResponseHandler =
+    (_ response: SecureByteArray, _ error: ChallengeResponseError?) -> Void
diff --git a/KeePassiumLib/KeePassiumLib/db/kdf/AESKDF.swift b/KeePassiumLib/KeePassiumLib/db/kdf/AESKDF.swift
index 7b5d3d0ad..cf70fcc8f 100755
--- a/KeePassiumLib/KeePassiumLib/db/kdf/AESKDF.swift
+++ b/KeePassiumLib/KeePassiumLib/db/kdf/AESKDF.swift
@@ -48,6 +48,14 @@ class AESKDF: KeyDerivationFunction {
         return progress
     }
     
+    
+    func getChallenge(_ params: KDFParams) throws -> ByteArray {
+        guard let transformSeed = params.getValue(key: AESKDF.transformSeedParam)?.asByteArray() else {
+            throw CryptoError.invalidKDFParam(kdfName: name, paramName: AESKDF.transformSeedParam)
+        }
+        return transformSeed
+    }
+    
     func randomize(params: inout KDFParams) throws {
         let transformSeed = try CryptoManager.getRandomBytes(count: SHA256_SIZE)
         params.setValue(
diff --git a/KeePassiumLib/KeePassiumLib/db/kdf/Argon2KDF.swift b/KeePassiumLib/KeePassiumLib/db/kdf/Argon2KDF.swift
index e35a37261..87b244eb8 100755
--- a/KeePassiumLib/KeePassiumLib/db/kdf/Argon2KDF.swift
+++ b/KeePassiumLib/KeePassiumLib/db/kdf/Argon2KDF.swift
@@ -62,6 +62,13 @@ final class Argon2KDF: KeyDerivationFunction {
         return progress
     }
     
+    func getChallenge(_ params: KDFParams) throws -> ByteArray {
+        guard let salt = params.getValue(key: Argon2KDF.saltParam)?.asByteArray() else {
+            throw CryptoError.invalidKDFParam(kdfName: name, paramName: AESKDF.transformSeedParam)
+        }
+        return salt
+    }
+    
     func randomize(params: inout KDFParams) throws { 
         let salt = try CryptoManager.getRandomBytes(count: 32)
         params.setValue(key: Argon2KDF.saltParam, value: VarDict.TypedValue(value: salt))
diff --git a/KeePassiumLib/KeePassiumLib/db/kdf/KDFFactory.swift b/KeePassiumLib/KeePassiumLib/db/kdf/KDFFactory.swift
index 126c355e2..5685447b1 100755
--- a/KeePassiumLib/KeePassiumLib/db/kdf/KDFFactory.swift
+++ b/KeePassiumLib/KeePassiumLib/db/kdf/KDFFactory.swift
@@ -19,6 +19,8 @@ protocol KeyDerivationFunction {
     
     func transform(key: SecureByteArray, params: KDFParams) throws -> SecureByteArray
     
+    func getChallenge(_ params: KDFParams) throws -> ByteArray
+    
     func randomize(params: inout KDFParams) throws
 }
 
diff --git a/KeePassiumLib/KeePassiumLib/db/kp1/Database1.swift b/KeePassiumLib/KeePassiumLib/db/kp1/Database1.swift
index fd31059d0..20017b594 100755
--- a/KeePassiumLib/KeePassiumLib/db/kp1/Database1.swift
+++ b/KeePassiumLib/KeePassiumLib/db/kp1/Database1.swift
@@ -135,14 +135,14 @@ public class Database1: Database {
         return Header1.isSignatureMatches(data: data)
     }
 
-    override public func changeCompositeKey(to newKey: SecureByteArray) {
+    override public func changeCompositeKey(to newKey: CompositeKey) {
         compositeKey = newKey
     }
     
     override public func load(
         dbFileName: String,
         dbFileData: ByteArray,
-        compositeKey: SecureByteArray,
+        compositeKey: CompositeKey,
         warnings: DatabaseLoadingWarnings
     ) throws {
         Diag.info("Loading KP1 database")
@@ -173,14 +173,22 @@ public class Database1: Database {
         } catch let error as CryptoError {
             Diag.error("Crypto error [reason: \(error.localizedDescription)]")
             throw DatabaseError.loadError(reason: error.localizedDescription)
+        } catch let error as ChallengeResponseError {
+            Diag.error("Challenge-response error [reason: \(error.localizedDescription)]")
+            throw DatabaseError.loadError(reason: error.localizedDescription)
         } catch let error as FormatError {
             Diag.error("Format error [reason: \(error.localizedDescription)]")
             throw DatabaseError.loadError(reason: error.localizedDescription)
         } 
     }
     
-    func deriveMasterKey(compositeKey: SecureByteArray) throws {
+    func deriveMasterKey(compositeKey: CompositeKey) throws {
         Diag.debug("Start key derivation")
+        
+        guard compositeKey.challengeHandler == nil else {
+            throw ChallengeResponseError.notSupportedByDatabaseFormat
+        }
+        
         let kdf = AESKDF()
         progress.addChild(kdf.initProgress(), withPendingUnitCount: ProgressSteps.keyDerivation)
         let kdfParams = kdf.defaultParams
@@ -191,8 +199,24 @@ public class Database1: Database {
             key: AESKDF.transformRoundsParam,
             value: VarDict.TypedValue(value: UInt64(header.transformRounds)))
         
-        let transformedKey = try kdf.transform(key: compositeKey, params: kdfParams)
-        masterKey = SecureByteArray(ByteArray.concat(header.masterSeed, transformedKey).sha256)
+        let combinedComponents: SecureByteArray
+        if compositeKey.state == .processedComponents {
+            combinedComponents = keyHelper.combineComponents(
+                passwordData: compositeKey.passwordData!, 
+                keyFileData: compositeKey.keyFileData!    
+            )
+            compositeKey.setCombinedStaticComponents(combinedComponents)
+        } else if compositeKey.state >= .combinedComponents {
+            combinedComponents = compositeKey.combinedStaticComponents! 
+        } else {
+            preconditionFailure("Unexpected key state")
+        }
+        
+        let keyToTransform = keyHelper.getKey(fromCombinedComponents: combinedComponents)
+        let transformedKey = try kdf.transform(key: keyToTransform, params: kdfParams)
+        let secureMasterSeed = SecureByteArray(header.masterSeed)
+        masterKey = SecureByteArray.concat(secureMasterSeed, transformedKey).sha256
+        compositeKey.setFinalKey(masterKey)
     }
     
     private func loadContent(data: ByteArray, dbFileName: String) throws {
@@ -353,6 +377,10 @@ public class Database1: Database {
             outStream.write(data: encryptedContent)
             return outStream.data!
         } catch let error as CryptoError {
+            Diag.error("Crypto error [reason: \(error.localizedDescription)]")
+            throw DatabaseError.saveError(reason: error.localizedDescription)
+        } catch let error as ChallengeResponseError {
+            Diag.error("Challenge-response error [reason: \(error.localizedDescription)]")
             throw DatabaseError.saveError(reason: error.localizedDescription)
         } 
     }
diff --git a/KeePassiumLib/KeePassiumLib/db/kp1/KeyHelper1.swift b/KeePassiumLib/KeePassiumLib/db/kp1/KeyHelper1.swift
index 129c61c9b..f5be97173 100755
--- a/KeePassiumLib/KeePassiumLib/db/kp1/KeyHelper1.swift
+++ b/KeePassiumLib/KeePassiumLib/db/kp1/KeyHelper1.swift
@@ -20,12 +20,10 @@ class KeyHelper1: KeyHelper {
         return SecureByteArray(data: data)
     }
     
-    
-    override func makeCompositeKey(
-        passwordData: ByteArray,
+    override func combineComponents(
+        passwordData: SecureByteArray,
         keyFileData: ByteArray
-        ) -> SecureByteArray
-    {
+    ) -> SecureByteArray {
         let hasPassword = !passwordData.isEmpty
         let hasKeyFile = !keyFileData.isEmpty
         
@@ -33,13 +31,13 @@ class KeyHelper1: KeyHelper {
         
         if hasPassword && hasKeyFile {
             Diag.info("Using password and key file")
-            let preKey = ByteArray.concat(
+            let preKey = SecureByteArray.concat(
                 passwordData.sha256,
                 processKeyFile(keyFileData: keyFileData))
-            return SecureByteArray(preKey.sha256)
+            return preKey.sha256
         } else if hasPassword {
             Diag.info("Using password")
-            return SecureByteArray(passwordData.sha256)
+            return passwordData.sha256
         } else if hasKeyFile {
             Diag.info("Using key file")
             return processKeyFile(keyFileData: keyFileData) 
@@ -48,6 +46,10 @@ class KeyHelper1: KeyHelper {
         }
     }
     
+    override func getKey(fromCombinedComponents combinedComponents: SecureByteArray) -> SecureByteArray {
+        return combinedComponents 
+    }
+    
     override func processXmlKeyFile(keyFileData: ByteArray) -> SecureByteArray? {
         return nil
     }
diff --git a/KeePassiumLib/KeePassiumLib/db/kp2/Database2.swift b/KeePassiumLib/KeePassiumLib/db/kp2/Database2.swift
index fcd32e84f..fa42177f5 100644
--- a/KeePassiumLib/KeePassiumLib/db/kp2/Database2.swift
+++ b/KeePassiumLib/KeePassiumLib/db/kp2/Database2.swift
@@ -102,7 +102,7 @@ public class Database2: Database {
     public var customIcons: [UUID: CustomIcon2] { return meta.customIcons }
     public var defaultUserName: String { return meta.defaultUserName }
     private var cipherKey = SecureByteArray()
-    private var hmacKey = ByteArray()
+    private var hmacKey = SecureByteArray()
     private var deletedObjects: ContiguousArray<DeletedObject2> = []
     
     override public var keyHelper: KeyHelper { return _keyHelper }
@@ -156,7 +156,7 @@ public class Database2: Database {
     override public func load(
         dbFileName: String,
         dbFileData: ByteArray,
-        compositeKey: SecureByteArray,
+        compositeKey: CompositeKey,
         warnings: DatabaseLoadingWarnings
     ) throws {
         Diag.info("Loading KP2 database")
@@ -232,6 +232,9 @@ public class Database2: Database {
         } catch let error as CryptoError {
             Diag.error("Crypto error [reason: \(error.localizedDescription)]")
             throw DatabaseError.loadError(reason: error.localizedDescription)
+        } catch let error as ChallengeResponseError {
+            Diag.error("Challenge-response error [reason: \(error.localizedDescription)]")
+            throw DatabaseError.loadError(reason: error.localizedDescription)
         } catch let error as FormatError {
             Diag.error("Format error [reason: \(error.localizedDescription)]")
             throw DatabaseError.loadError(reason: error.localizedDescription)
@@ -539,18 +542,59 @@ public class Database2: Database {
         }
     }
     
-    func deriveMasterKey(compositeKey: SecureByteArray, cipher: DataCipher) throws {
+    func deriveMasterKey(compositeKey: CompositeKey, cipher: DataCipher) throws {
         Diag.debug("Start key derivation")
         progress.addChild(header.kdf.initProgress(), withPendingUnitCount: ProgressSteps.keyDerivation)
-        let transformedKey = try header.kdf.transform(key: compositeKey, params: header.kdfParams)
-        let joinedKey = ByteArray.concat(header.masterSeed, transformedKey)
+        
+        var combinedComponents: SecureByteArray
+        if compositeKey.state == .processedComponents {
+            combinedComponents = keyHelper.combineComponents(
+                passwordData: compositeKey.passwordData!, 
+                keyFileData: compositeKey.keyFileData!    
+            )
+            compositeKey.setCombinedStaticComponents(combinedComponents)
+        } else if compositeKey.state >= .combinedComponents {
+            combinedComponents = compositeKey.combinedStaticComponents! 
+        } else {
+            preconditionFailure("Unexpected key state")
+        }
+        
+        let secureMasterSeed = SecureByteArray(header.masterSeed)
+        let joinedKey: SecureByteArray
+        switch header.formatVersion {
+        case .v3:
+            
+            let keyToTransform = keyHelper.getKey(fromCombinedComponents: combinedComponents)
+            
+            let transformedKey = try header.kdf.transform(
+                key: keyToTransform,
+                params: header.kdfParams)
+            
+            let challengeResponse = try compositeKey.getResponse(challenge: secureMasterSeed) 
+            joinedKey = SecureByteArray.concat(secureMasterSeed, challengeResponse, transformedKey)
+        case .v4:
+            
+            let challenge = try header.kdf.getChallenge(header.kdfParams) 
+            let secureChallenge = SecureByteArray(challenge)
+
+            let challengeResponse = try compositeKey.getResponse(challenge: secureChallenge) 
+            combinedComponents = SecureByteArray.concat(combinedComponents, challengeResponse)
+            
+            let keyToTransform = keyHelper.getKey(fromCombinedComponents: combinedComponents)
+            
+            let transformedKey = try header.kdf.transform(
+                key: keyToTransform,
+                params: header.kdfParams)
+            joinedKey = SecureByteArray.concat(secureMasterSeed, transformedKey)
+        }
         self.cipherKey = cipher.resizeKey(key: joinedKey)
-        let one = ByteArray(bytes: [1])
-        self.hmacKey = ByteArray.concat(joinedKey, one).sha512
+        let one = SecureByteArray(bytes: [1])
+        self.hmacKey = SecureByteArray.concat(joinedKey, one).sha512
+        compositeKey.setFinalKey(hmacKey)
     }
     
-    override public func changeCompositeKey(to newKey: SecureByteArray) {
-        compositeKey = newKey
+    override public func changeCompositeKey(to newKey: CompositeKey) {
+        compositeKey = newKey.clone()
     }
     
     override public func getBackupGroup(createIfMissing: Bool) -> Group? {
@@ -819,6 +863,9 @@ public class Database2: Database {
         } catch let error as CryptoError {
             Diag.error("Crypto error [reason: \(error.localizedDescription)]")
             throw DatabaseError.saveError(reason: error.localizedDescription)
+        } catch let error as ChallengeResponseError {
+            Diag.error("Challenge-response error [reason: \(error.localizedDescription)]")
+            throw DatabaseError.saveError(reason: error.localizedDescription)
         }
 
         
diff --git a/KeePassiumLib/KeePassiumLib/db/kp2/KeyHelper2.swift b/KeePassiumLib/KeePassiumLib/db/kp2/KeyHelper2.swift
index ae3a6fad9..c34661bfe 100755
--- a/KeePassiumLib/KeePassiumLib/db/kp2/KeyHelper2.swift
+++ b/KeePassiumLib/KeePassiumLib/db/kp2/KeyHelper2.swift
@@ -18,28 +18,35 @@ final class KeyHelper2: KeyHelper {
         return SecureByteArray(data: Data(password.utf8))
     }
     
-    override func makeCompositeKey(passwordData: ByteArray, keyFileData: ByteArray) -> SecureByteArray {
+    override func combineComponents(
+        passwordData: SecureByteArray,
+        keyFileData: ByteArray
+    ) -> SecureByteArray {
         let hasPassword = !passwordData.isEmpty
         let hasKeyFile = !keyFileData.isEmpty
-
+        
         precondition(hasPassword || hasKeyFile)
         
-        let preKey: ByteArray
-        if hasPassword && hasKeyFile {
-            Diag.info("Using password and key file")
+        var preKey = SecureByteArray()
+        if hasPassword {
+            Diag.info("Using password")
+            preKey = SecureByteArray.concat(preKey, passwordData.sha256)
+        }
+        if hasKeyFile {
+            Diag.info("Using key file")
             preKey = SecureByteArray.concat(
-                passwordData.sha256,
-                processKeyFile(keyFileData: keyFileData))
-        } else if hasPassword {
-            Diag.info("Using password only")
-            preKey = passwordData.sha256
-        } else if hasKeyFile {
-            Diag.info("Using key file only")
-            preKey = processKeyFile(keyFileData: keyFileData)
-        } else {
-            fatalError("Both password and key file are empty after being checked.")
+                preKey,
+                processKeyFile(keyFileData: keyFileData)
+            )
         }
-        return SecureByteArray(preKey.sha256)
+        if preKey.isEmpty {
+            fatalError("All key components are empty after being checked.")
+        }
+        return preKey 
+    }
+    
+    override func getKey(fromCombinedComponents combinedComponents: SecureByteArray) -> SecureByteArray {
+        return combinedComponents.sha256
     }
     
     internal override func processXmlKeyFile(keyFileData: ByteArray) -> SecureByteArray? {
diff --git a/KeePassiumLib/KeePassiumLib/db/util/ByteArray.swift b/KeePassiumLib/KeePassiumLib/db/util/ByteArray.swift
index cb373c578..de6c4d1c4 100755
--- a/KeePassiumLib/KeePassiumLib/db/util/ByteArray.swift
+++ b/KeePassiumLib/KeePassiumLib/db/util/ByteArray.swift
@@ -8,8 +8,8 @@
 
 import Foundation
 
-public class ByteArray: Eraseable, Codable {
-    
+public class ByteArray: Eraseable, Codable, CustomDebugStringConvertible {
+
     public class InputStream {
         fileprivate let base: Foundation.InputStream
         var hasBytesAvailable: Bool { return base.hasBytesAvailable }
@@ -137,6 +137,10 @@ public class ByteArray: Eraseable, Codable {
         return ByteArray(bytes: self.bytes[range])
     }
 
+    public var debugDescription: String {
+        return asHexString
+    }
+    
     public init() {
         bytes = []
     }
@@ -349,6 +353,7 @@ extension ByteArray: Hashable {
 
 public class SecureByteArray: ByteArray {
     override public var sha256: SecureByteArray { return SecureByteArray(CryptoManager.sha256(of: self)) }
+    override public var sha512: SecureByteArray { return SecureByteArray(CryptoManager.sha512(of: self)) }
 
     override convenience public init() {
         self.init(bytes: [])
@@ -356,7 +361,7 @@ public class SecureByteArray: ByteArray {
     convenience public init(_ source: ByteArray) {
         self.init(bytes: source.bytesCopy())
     }
-    override private init(bytes: [UInt8]) {
+    override public init(bytes: [UInt8]) {
         super.init(bytes: bytes)
         self.bytes.withUnsafeBufferPointer { (ptr) -> Void in
             mlock(ptr.baseAddress, ptr.count)
diff --git a/KeePassiumLib/KeePassiumLib/util/DatabaseSettings.swift b/KeePassiumLib/KeePassiumLib/util/DatabaseSettings.swift
index 94cb26377..d45bf45a0 100644
--- a/KeePassiumLib/KeePassiumLib/util/DatabaseSettings.swift
+++ b/KeePassiumLib/KeePassiumLib/util/DatabaseSettings.swift
@@ -21,11 +21,14 @@ public class DatabaseSettings: Eraseable, Codable {
     public var accessMode: AccessMode
     
     public var isRememberMasterKey: Bool?
-    public private(set) var masterKey: SecureByteArray?
+    public private(set) var masterKey: CompositeKey?
     public var hasMasterKey: Bool { return masterKey != nil }
     
     public var isRememberKeyFile: Bool?
     public private(set) var associatedKeyFile: URLReference?
+    
+    public var isRememberHardwareKey: Bool?
+    public private(set) var associatedYubiKey: YubiKey?
 
     private enum CodingKeys: String, CodingKey {
         case databaseRef
@@ -34,6 +37,8 @@ public class DatabaseSettings: Eraseable, Codable {
         case masterKey
         case isRememberKeyFile
         case associatedKeyFile
+        case isRememberHardwareKey
+        case associatedYubiKey
     }
     
     init(for databaseRef: URLReference) {
@@ -68,14 +73,14 @@ public class DatabaseSettings: Eraseable, Codable {
         return result
     }
 
-    public func setMasterKey(_ key: SecureByteArray) {
-        masterKey = key.secureClone()
+    public func setMasterKey(_ key: CompositeKey) {
+        masterKey = key.clone()
     }
     
-    public func maybeSetMasterKey(_ key: SecureByteArray) {
-        if isRememberKeyFile ?? Settings.current.isRememberDatabaseKey {
-            setMasterKey(key)
-        }
+    public func maybeSetMasterKey(_ key: CompositeKey) {
+        guard isRememberKeyFile ?? Settings.current.isRememberDatabaseKey else { return }
+        guard key.state >= .combinedComponents else { return }
+        setMasterKey(key)
     }
 
     public func clearMasterKey() {
@@ -88,9 +93,17 @@ public class DatabaseSettings: Eraseable, Codable {
     }
     
     public func maybeSetAssociatedKeyFile(_ urlRef: URLReference?) {
-        if isRememberKeyFile ?? Settings.current.isKeepKeyFileAssociations {
-            setAssociatedKeyFile(urlRef)
-        }
+        guard isRememberKeyFile ?? Settings.current.isKeepKeyFileAssociations else { return }
+        setAssociatedKeyFile(urlRef)
+    }
+
+    public func setAssociatedYubiKey(_ yubiKey: YubiKey?) {
+        associatedYubiKey = yubiKey
+    }
+
+    public func maybeSetAssociatedYubiKey(_ yubiKey: YubiKey?) {
+        guard isRememberHardwareKey ?? Settings.current.isKeepHardwareKeyAssociations else { return }
+        setAssociatedYubiKey(yubiKey)
     }
 }
 
diff --git a/KeePassiumLib/KeePassiumLib/util/DatabaseSettingsManager.swift b/KeePassiumLib/KeePassiumLib/util/DatabaseSettingsManager.swift
index 8919b52df..c8e0ac031 100644
--- a/KeePassiumLib/KeePassiumLib/util/DatabaseSettingsManager.swift
+++ b/KeePassiumLib/KeePassiumLib/util/DatabaseSettingsManager.swift
@@ -68,6 +68,18 @@ public class DatabaseSettingsManager {
         }
     }
     
+    public func forgetAllHardwareKeys() {
+        let allDatabaseRefs = FileKeeper.shared.getAllReferences(
+            fileType: .database,
+            includeBackup: true
+        )
+        for dbRef in allDatabaseRefs {
+            guard let dbSettings = getSettings(for: dbRef) else { continue }
+            dbSettings.setAssociatedYubiKey(nil)
+            setSettings(dbSettings, for: dbRef)
+        }
+    }
+
     public func removeAllAssociations(of keyFileRef: URLReference) {
         guard let keyFileDescriptor = keyFileRef.getDescriptor() else { return }
         let allDatabaseRefs = FileKeeper.shared.getAllReferences(
diff --git a/KeePassiumLib/KeePassiumLib/util/Settings.swift b/KeePassiumLib/KeePassiumLib/util/Settings.swift
index 49afa1161..cd549bb8c 100644
--- a/KeePassiumLib/KeePassiumLib/util/Settings.swift
+++ b/KeePassiumLib/KeePassiumLib/util/Settings.swift
@@ -61,7 +61,8 @@ public class Settings {
         case startupDatabase
         case rememberDatabaseKey
         case keepKeyFileAssociations
-        case keyFileAssociations
+        case keepHardwareKeyAssociations
+        case hardwareKeyAssociations
 
         case appLockEnabled
         case biometricAppLockEnabled
@@ -809,7 +810,7 @@ public class Settings {
             let oldValue = isKeepKeyFileAssociations
             UserDefaults.appGroupShared.set(newValue, forKey: Keys.keepKeyFileAssociations.rawValue)
             if !newValue {
-                removeAllKeyFileAssociations()
+                DatabaseSettingsManager.shared.forgetAllKeyFiles()
             }
             if newValue != oldValue {
                 postChangeNotification(changedKey: Keys.keepKeyFileAssociations)
@@ -817,12 +818,26 @@ public class Settings {
         }
     }
     
-    public func removeAllKeyFileAssociations() {
-        UserDefaults.appGroupShared.setValue(
-            Dictionary<String, Data>(),
-            forKey: Keys.keyFileAssociations.rawValue)
+    public var isKeepHardwareKeyAssociations: Bool {
+        get {
+            if contains(key: Keys.keepHardwareKeyAssociations) {
+                return UserDefaults.appGroupShared.bool(forKey: Keys.keepHardwareKeyAssociations.rawValue)
+            } else {
+                return true
+            }
+        }
+        set {
+            let oldValue = isKeepHardwareKeyAssociations
+            UserDefaults.appGroupShared.set(newValue, forKey: Keys.keepHardwareKeyAssociations.rawValue)
+            if !newValue {
+                DatabaseSettingsManager.shared.forgetAllHardwareKeys()
+            }
+            if newValue != oldValue {
+                postChangeNotification(changedKey: Keys.keepHardwareKeyAssociations)
+            }
+        }
     }
-
+    
     
     public var isAppLockEnabled: Bool {
         get {
diff --git a/KeePassiumLib/KeePassiumLib/util/YubiKey.swift b/KeePassiumLib/KeePassiumLib/util/YubiKey.swift
new file mode 100644
index 000000000..ea85b3bcb
--- /dev/null
+++ b/KeePassiumLib/KeePassiumLib/util/YubiKey.swift
@@ -0,0 +1,54 @@
+//  KeePassium Password Manager
+//  Copyright © 2018–2019 Andrei Popleteev <info@keepassium.com>
+//
+//  This program is free software: you can redistribute it and/or modify it
+//  under the terms of the GNU General Public License version 3 as published
+//  by the Free Software Foundation: https://www.gnu.org/licenses/).
+//  For commercial licensing, please contact the author.
+
+import Foundation
+
+public class YubiKey: Codable, Equatable, CustomStringConvertible {
+    public let name = "YubiKey"
+    
+    public enum Slot: Int, Codable {
+        case slot1 = 1
+        case slot2 = 2
+        public var number: Int {
+            return rawValue
+        }
+    }
+    
+    public enum Interface: Int, Codable, CustomStringConvertible {
+        case nfc
+        case mfi
+        public var description: String {
+            switch self {
+            case .nfc: return "NFC"
+            case .mfi: return "MFI"
+            }
+        }
+    }
+    
+    public var slot: Slot
+    public var interface: Interface
+    
+    private enum CodingKeys: String, CodingKey {
+        case slot
+        case interface
+    }
+    
+    public init(interface: Interface, slot: Slot) {
+        self.interface = interface
+        self.slot = slot
+    }
+    
+    public static func == (lhs: YubiKey, rhs: YubiKey) -> Bool {
+        return (lhs.slot == rhs.slot) && (lhs.interface == rhs.interface)
+    }
+    
+    public var description: String {
+        return "YubiKey \(interface) Slot \(slot.number)"
+    }
+}
+
diff --git a/YubiKit/Changelog.md b/YubiKit/Changelog.md
new file mode 100755
index 000000000..9d0edbf29
--- /dev/null
+++ b/YubiKit/Changelog.md
@@ -0,0 +1,224 @@
+# YubiKit Changelog
+
+#### 3.0.0-Preview2 [3.0.0-Preview1 -> 3.0.0-Preview2]
+
+- Adds support for OATH protocol over NFC on devices running iOS 13 or newer. Also wraps and simplifies raw APDU communication with NFC-Enabled YubiKeys.
+
+#### 3.0.0-Preview1 [2.0.1 -> 3.0.0-Preview1]
+
+`BREAKING CHANGES`: 
+- See `Refactoring Changes` section under [NFC-Notes](./NFC-Notes.md)
+
+---
+
+- This version now supports NFC-Enabled YubiKeys for FIDO2. `Note`: All NFC capabilities (except Yubico OTP) require iOS 13+ on the user's device. See [NFC-Notes.md](./NFC-Notes.md) for more details on the addition of NFC support and notable changes to the key sessions.
+
+- The YubiKit 3.0.0-Preview1 adds support for ISO 7816 tags which allows your application to use the FIDO2 functionality of the YubiKey over NFC on devices running iOS 13 or newer. 
+
+- The FIDO2 protocol implementation now supports any NFC-Enabled YubiKey, in addition to the YubiKey 5Ci. The library provides examples for implementing FIDO2 over an accessory (YubiKey 5Ci) or NFC. In addition, the [YubiKit Demo](./YubiKitDemo/README.md) application provides an end-to-end solution for both protocols.
+
+- To use YubiKit 3.0.0-Preview1, the application needs to be compiled with Xcode 11 or newer (iOS 13 SDK).
+
+---
+
+#### 2.0.1 [2.0.0 -> 2.0.1]
+
+- Fixed an edge case for CCID when the WTX responses are concatenated with the payload. This issue mostly affects the calculation of OATH credentials with touch.
+
+- Added the possibility to attach a custom application logger to process the library logs. For more details check the `Examples/CustomLogger` in the YubiKit Demo application. 
+
+- Several other improvements to OATH, including the possibility to create OATH credentials without issuer and the ability to read and add credentials with 7 digits.
+
+---
+
+#### 2.0.0 [2.0.0 RC1 -> 2.0.0]
+
+- The internal CBOR encoder used by the FIDO2 API is now sorting the map keys according to canonical CBOR rules when the keys are text strings. This fixes a bug with the order of the keys in the `webauthnAttestationObject` returned by the `YKFKeyFIDO2MakeCredentialResponse`.
+
+- Improved the error handling when the applications are disabled on the YubiKey. In case of FIDO (FIDO2 and U2F) the application is shared (the CTAP specifications use the same AID). In this specific scenario, when only one of them is enabled, YubiKit was returning `YKFKeyAPDUErrorCodeInsNotSupported`. Now the library will return `YKFKeySessionErrorMissingApplicationCode` when trying to use the disabled application, similar with the scenario when both applications are disabled.
+
+- Added a new constant, `YKFKeyFIDO2GetInfoResponseOptionUserVerification`, which can be used to test if the authenticator supports UV (User Verification). Removed from the YubiKit Demo application the explicit set of the UV options flag when creating FIDO2 credentials or getting assertions because the YubiKey 5Ci is not capable of verifying the user within itself. This update is available from firmware version 5.2.x and reflects the latest [CTAP2 specifications](https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#authenticatorGetInfo).
+
+- Renamed the `supportsLightningKey` property from `YubiKitDeviceCapabilities` to `supportsMFIAccessoryKey`. The property will also return `NO/false` when the iOS device has an USB-C connector, such as the iPad Pro 3rd generation. These devices do not officially support MFi external accessories.
+
+- Renamed several classes, properties and UI labels in the YubiKit Demo application to not use the term *Lightning*. This change was made to avoid possible trademark issues with this term. 
+
+- Some minor improvements to the PC/SC API to dynamically read some properties, like the name and the model of the key, from the `YKFKeySession` instead of returning hardcoded values.
+
+- Several OATH improvements, including support for touch credentials and improved compatibility with other libraries/applications which implement the YOATH protocol, such as Yubico Authenticator for Android and desktop.
+
+- Improved the ability to manually build OATH credentials using the `YKFOATHCredential` model provided by the library.
+
+- The `build.sh` script will generate a `release-universal` flavour of the library, together with the previous flavours (`release` and `debug-universal`).
+
+---
+
+#### 2.0.0 RC1 [2.0.0 B8 -> 2.0.0 RC1]
+
+- The `YKFKeyFIDO2MakeCredentialResponse` has two new properties: `ctapAttestationObject` and `webauthnAttestationObject`: 
+	- The `ctapAttestationObject` is identical to the `rawResponse` from the key. This attestation format follows the [CTAP2 specifications](https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#responses) for packing the attestation object from the authenticator. In this format the top level CBOR map is using numeric keys for `authData`, `fmt` and `attStmt`.
+	- The `webauthnAttestationObject` is similar with the `ctapAttestationObject`. The only difference is in the top level CBOR map keys which are text, as defined in the [WebAuthN Attestation Object specifications](https://developer.mozilla.org/en-US/docs/Web/API/AuthenticatorAttestationResponse/attestationObject).
+
+- The `attStmt` property from the `YKFKeyFIDO2MakeCredentialResponse` is an opaque object now (NSData/Data) instead of a parsed CBOR map to comply with the CTAP2 specifications on how the clients need to handle this object.
+
+- The **U2F** external accessory protocol support has been removed from both YubiKit and YubiKit Demo application. The library supports from this version only the **com.yubico.ylp** external accessory protocol. Make sure to remove the **U2F** protocol from the application *Info.plist* file before submitting the application for an AppStore review.
+
+- The `YubiKitDeviceCapabilities` contains a new property: `supportsLightningKey`. This property should be used in the application before starting the key session. If the check is not performed, in debug builds the library will assert when trying to start the key session on an unsupported iOS version. This property returns `YES`/`true` when: 
+	- the iOS version is iOS 10 or newer.
+	- the iOS version is not in a blacklist of versions where the external accessories don't work due to iOS bugs.
+
+- Moved the WebAuthN clientData into the library. The new class provided by the library is called `YKFWebAuthnClientData`. This change avoids duplicate implementations of the Client Data in every application which could lead to different results when using the FIDO2 APIs. When using Swift 5, this change avoids a random memory corruption of the old implementation from the YubiKit Demo application, when creating and passing the data to the library. **Make sure to use the new implementation** if the demo application code was reused.
+
+- Several improvements and bug fixes to the logging of the library in debug builds. The library check in debug builds if the application is configured properly when starting the key session by looking at the application external accessory protocols.
+
+- The firmware version, available in `YKFKeyDescription.firmwareRevision` returns now the format `[major].[minor].[patch]` instead of a number.
+
+- Improvements and bug fixes to the YubiKit Demo application:
+	- The `WebAuthnClientData` is using an updated Swift 5 version of `Data.withUnsafeBytes` with the memory bound explicitly specified to avoid some possible data corruption when hashing.
+	- Removed a bug in the Other demos, Raw Commands where the logs were wiped immediately after running a demo, if the flow was successful.
+	
+- Several internal library improvements related to: debug assertions, unit testability and performance.
+
+---
+
+#### 2.0.0 B8 [2.0.0 B7 -> 2.0.0 B8]
+
+- The YubiKit Demo application was updated to Xcode 10.2 and Swift 5. This version (or newer) of Xcode is required to compile and run the application.
+
+- Added support for CTAP2/FIDO2 PIN management, including verification, getting the number of retries, setting and changing the PIN. The FIDO2 requests (`YKFKeyFIDO2MakeCredentialRequest` and `YKFKeyFIDO2GetAssertionRequest`) work with the CTAP2 PIN APIs.
+
+- Replaced the U2F demo tab in the demo application with a new FIDO2/WebAuthN demo. The WebAuthN demo communicates with the Yubico WebAuthN demo website. The U2F demo was moved into a self-contained demo in the Other demos tab.
+
+- The self-contained FIDO2 demo in the Other demos tab provides the ability to manage the PIN.
+
+- The FIDO2 Make Credential and Get Assertion requests return also the raw CBOR response from the key. These responses can be sent directly to the server when the server does the parsing of the payload.
+
+- Added support for CTAP2 Get Next Assertion request.
+
+- Improved the management of the session when the applications are terminated or backgrounded, to reflect the newest changes in the hardware Rev2 of the YubiKey 5Ci. 
+
+- Fixed a bug with the key state on the FIDO2 and U2F services being unnecessary updated to the same value, triggering unnecessary KVO notifications. 
+
+- The YubiKit Demo application includes two reusable helper classes, `KeySessionObserver` and `FIDO2ServiceObserver` in `Examples/Observers`, which show an example on how to translate from a KVO observation pattern to a delegate pattern, when a delegate pattern is preferred for the target application.
+
+---
+
+#### 2.0.0 B7 [2.0.0 B6 -> 2.0.0 B7]
+
+- This version adds compatibility with the hardware Rev2 of the YubiKey 5Ci. This includes support for CTAP2/FIDO2 requests against the key with some limitations (PIN authentication not supported yet by the library). Note that this new functionality is not supported by the hardware Rev1 devices. To determine the hardware revision, run the demo application (wireless debugging enabled) and insert the key. The application will show in the console logs the information about the accessory, including the hardware revision.
+
+- Updated the Other demos to include an API demo on how to use the FIDO2 functionality provided by the library.
+
+- Minor bug fixes and improved session handling when multiple applications try to access the key concurrently.
+
+- For more details on how to use these new interfaces check the documentation from *Readme.md*.
+
+---
+
+#### 2.0.0 B6 [2.0.0 B5 -> 2.0.0 B6]
+
+- Updated the PC/SC interface to receive pre-allocated buffers, similar to the original PC/SC API. This new implementation adds support for ask-for-size and optional buffers. Removed the `A` suffix from some of the methods and refer in the API header documentation to the PCSCLite documentation which is more concise and cross-platform.
+
+- Added a new PC/SC function, similar to `pcsc_stringify_error` from PCSCLite, `YKFPCSCStringifyError`, which returns a human readable error description for a given, known, PC/SC error code.
+
+- The PC/SC interface is exposing basic support for the PC/SC method `SCardGetStatusChange`, YubiKit version: `YKFSCardGetStatusChange`, which returns immediately the status of the card.
+
+- The PC/SC interface tracks better contexts and cards and returns errors when a context or a card is invalid.
+
+- Minor updates to the YubiKit Demo application and bug fixes.
+
+---
+
+#### 2.0.0 B5 [2.0.0 B4 -> 2.0.0 B5]
+
+- The `YKFKeyRawCommandService` provides the ability to execute sync commands against the key. 
+The `YKFKeySession` provides the ability to check if the key is connected to the device regardless of the session state. New APIs for opening and closing synchronously the session have been added to ease the development when using the raw interface.
+
+- The YubiKit Demo application has been updated to provide a demo for the raw interface when using the sync API from `YKFKeyRawCommandService`.
+
+- The YubiKit Demo application was improved for iPad. Now the application allows to test the OTP reading using the YubiKey for Lightning when the device does not support NFC reading. The application has an improved UI for the Lightning action sheet which can be easier reused.
+
+---
+
+#### 2.0.0 B4 [2.0.0 B3 -> 2.0.0 B4]
+
+- The library provides the possibility to run raw commands against the YubiKey 5Ci. To allow this, a new service, `YKFKeyRawCommandService` was introduced. This service allows to execute custom built APDU commands when the host application needs a very specific interaction with the key.
+
+- Together with the `YKFKeyRawCommandService` the library provides a new, PC/SC like decoupled interface to interact with the key. This interface is still in a prototype stage (POC).
+
+- The YubiKit Demo application includes a new tab, Other, which is collection of miscellaneous small demos. Currently the list has only one demo, for the Raw Command interface.
+
+- For more details on how to use these new interfaces check the documentation from *Readme.md*.
+
+---
+
+#### 2.0.0 B3 [2.0.0 B2 -> 2.0.0 B3]
+
+- The `YKFKeySession` is exposing a new service for OATH credentials, `oathService`. The OATH service allows to interact with the OATH application from the key by using the [YOATH protocol](https://developers.yubico.com/OATH/YKOATH_Protocol.html). For a complete description of the new functionality check the *Readme.md* file and the header documentation for `YKFKeyOATHService`.
+
+- The YubiKit Demo application contains now a demo on how to read an OTP from the YubiKey 5Ci. 
+
+- A QuickStart guide has beed added to the documentation.
+
+---
+
+#### 2.0.0 B2 [2.0.0 B1 -> 2.0.0 B2]
+
+- The `YKFKeySession` has a new property, `keyDescription`, which provides a list of properties about the connected key, like firmware version, device name, etc. For the complete list of properties check `YKFKeyDescription`.
+
+- The library can connect to newer version of the firmware which is using the **com.yubico.ylp** protocol name instead of **U2F**. To add support for this protocol add **com.yubico.ylp** to the list of supported external accessories protocols. U2F protocol name is deprecated starting from this version. The library still works with the U2F protocol devices.
+
+- The `YKFKeyConnectionError` has been renamed to `YKFKeySessionError` to have a consistent naming with `YKFKeySession`. The library provides a few more detailed errors for the session operations. Check the error codes from `YKFKeySessionError` for more details.
+
+---
+
+#### 2.0.0 B1 [1.1.1 -> 2.0.0 B1]
+
+- This release is a major update which adds initial support for YubiKeys with lightning connector. 
+
+- This version provides functionality for performing only U2F operations. Read the integration documentation to see how to add support for the YubiKeys with lightning connector.
+
+---
+
+#### 1.1.1 [1.1.0 -> 1.1.1]
+
+- This is a minor update which adds support for a new default URI format when reading the OTP over NFC. This update is required to allow the applications to support future YubiKey firmware revisions. 
+
+- The new supported format of the URL is: [https://my.yubico.com/yk/#[otp_value]]()
+
+---
+
+#### 1.1.0 [1.0.0 -> 1.1.0]
+
+This version has a few improvements on the NFC APIs and to the demo application:
+
+- The check for NFC capabilities does a pre-check for devices with NFC chip or newer devices before interrogating the OS for the NFC capabilities to avoid a very rare CoreNFC crash on devices which do not have a NFC reader.
+- The OTP token interface was updated and the `payload` property was removed because it can be inferred from the other properties of the token and it's not essential in the context of YubiKit.
+- The `uri` and `text` properties from the `YKFOTPToken` provide now the full parsed URI/Text from the device (including the prepended protocol in case or URI).
+- The demo application has a few UI updates and fixes a few layout issues on small screen devices (iPhone 5/5c/5s/SE)
+- The demo application can now run on iOS 10.
+
+---
+
+#### 1.0.0 [1.0.0 RC2 -> 1.0.0]
+
+This version does a few changes to the library interface. The provided interface should  provide from now on a final API for capabilities check, NFC and QR code scanning:
+
+- Renamed the `YKFDeviceCapabilities` as `YubiKitDeviceCapabilities`, as the capabilities type becomes a top level library interface object, on par with `YubiKitManager`, `YubiKitConfiguration` and `YubiKitExternalLocalization`.
+- The capabilities change allows a direct check without retrieving them from the shared instance of the YubiKitManager as in RC2: `YubiKitDeviceCapabilities.supportsNFCScanning`  and `YubiKitDeviceCapabilities.supportsQRCodeScanning`. For a complete example read the documentation (README.md file) for RC3 and consult the code of the demo application.
+- The `YubiKitManager` type provides from now several types of _sessions_, each one of them being responsible to only one type of communication. This change allows for future extensibility and consistency of the APIs without transforming `YubiKitManager` into a mixed responsibility type, responsible for various types of requests. RC3 provides two sessions: `nfcReaderSession` and `qrReaderSession`. The previous calls on the managers are now part of these sessions so `YubiKitManager.shared.<method_call>` becomes `YubiKitManager.shared.[nfcReaderSession/qrReaderSession].<method_call>`. For a complete example read the documentation (README.md file) for RC3 and consult the code of the demo application.
+ 
+---
+
+#### 1.0.0 RC2 [1.0.0 RC1 -> 1.0.0 RC2]
+
+- Exposing the cancel user action from the NFC OS action sheet which is returned as an error by CoreNFC APIs: `NFCReaderError.readerSessionInvalidationErrorUserCanceled`
+
+---
+
+#### 1.0.0 RC1
+
+Initial release with support for: 
+
+ - Reading OTPs (YubicoOTP and HOTP) from NFC enabled YubiKeys.
+ - Raw QR code scanning.
\ No newline at end of file
diff --git a/YubiKit/LICENSE b/YubiKit/LICENSE
new file mode 100755
index 000000000..5ae7d11cf
--- /dev/null
+++ b/YubiKit/LICENSE
@@ -0,0 +1,177 @@
+Apache License
+
+Version 2.0, January 2004
+
+http://www.apache.org/licenses/
+
+TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+1. Definitions.
+
+"License" shall mean the terms and conditions for use, reproduction,
+and distribution as defined by Sections 1 through 9 of this document.
+
+"Licensor" shall mean the copyright owner or entity authorized by the
+copyright owner that is granting the License.
+
+"Legal Entity" shall mean the union of the acting entity and all
+other entities that control, are controlled by, or are under common
+control with that entity. For the purposes of this definition,
+"control" means (i) the power, direct or indirect, to cause the
+direction or management of such entity, whether by contract or
+otherwise, or (ii) ownership of fifty percent (50%) or more of the
+outstanding shares, or (iii) beneficial ownership of such entity.
+
+"You" (or "Your") shall mean an individual or Legal Entity exercising
+permissions granted by this License.
+
+"Source" form shall mean the preferred form for making modifications,
+including but not limited to software source code, documentation
+source, and configuration files.
+
+"Object" form shall mean any form resulting from mechanical
+transformation or translation of a Source form, including but not
+limited to compiled object code, generated documentation, and
+conversions to other media types.
+
+"Work" shall mean the work of authorship, whether in Source or Object
+form, made available under the License, as indicated by a copyright
+notice that is included in or attached to the work (an example is
+provided in the Appendix below).
+
+"Derivative Works" shall mean any work, whether in Source or Object
+form, that is based on (or derived from) the Work and for which the
+editorial revisions, annotations, elaborations, or other modifications
+represent, as a whole, an original work of authorship. For the
+purposes of this License, Derivative Works shall not include works
+that remain separable from, or merely link (or bind by name) to the
+interfaces of, the Work and Derivative Works thereof.
+
+"Contribution" shall mean any work of authorship, including the
+original version of the Work and any modifications or additions to
+that Work or Derivative Works thereof, that is intentionally submitted
+to Licensor for inclusion in the Work by the copyright owner or by an
+individual or Legal Entity authorized to submit on behalf of the
+copyright owner. For the purposes of this definition, "submitted"
+means any form of electronic, verbal, or written communication sent to
+the Licensor or its representatives, including but not limited to
+communication on electronic mailing lists, source code control
+systems, and issue tracking systems that are managed by, or on behalf
+of, the Licensor for the purpose of discussing and improving the Work,
+but excluding communication that is conspicuously marked or otherwise
+designated in writing by the copyright owner as "Not a Contribution."
+
+"Contributor" shall mean Licensor and any individual or Legal Entity
+on behalf of whom a Contribution has been received by Licensor and
+subsequently incorporated within the Work.
+
+2. Grant of Copyright License. Subject to the terms and conditions of
+this License, each Contributor hereby grants to You a perpetual,
+worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+copyright license to reproduce, prepare Derivative Works of, publicly
+display, publicly perform, sublicense, and distribute the Work and
+such Derivative Works in Source or Object form.
+
+3. Grant of Patent License. Subject to the terms and conditions of
+this License, each Contributor hereby grants to You a perpetual,
+worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except
+as stated in this section) patent license to make, have made, use,
+offer to sell, sell, import, and otherwise transfer the Work, where
+such license applies only to those patent claims licensable by such
+Contributor that are necessarily infringed by their Contribution(s)
+alone or by combination of their Contribution(s) with the Work to
+which such Contribution(s) was submitted. If You institute patent
+litigation against any entity (including a cross-claim or counterclaim
+in a lawsuit) alleging that the Work or a Contribution incorporated
+within the Work constitutes direct or contributory patent
+infringement, then any patent licenses granted to You under this
+License for that Work shall terminate as of the date such litigation
+is filed.
+
+4. Redistribution. You may reproduce and distribute copies of the Work
+or Derivative Works thereof in any medium, with or without
+modifications, and in Source or Object form, provided that You meet
+the following conditions:
+
+    You must give any other recipients of the Work or Derivative Works
+    a copy of this License; and
+
+    You must cause any modified files to carry prominent notices
+    stating that You changed the files; and
+
+    You must retain, in the Source form of any Derivative Works that
+    You distribute, all copyright, patent, trademark, and attribution
+    notices from the Source form of the Work, excluding those notices
+    that do not pertain to any part of the Derivative Works; and
+
+    If the Work includes a "NOTICE" text file as part of its
+    distribution, then any Derivative Works that You distribute must
+    include a readable copy of the attribution notices contained
+    within such NOTICE file, excluding those notices that do not
+    pertain to any part of the Derivative Works, in at least one of
+    the following places: within a NOTICE text file distributed as
+    part of the Derivative Works; within the Source form or
+    documentation, if provided along with the Derivative Works; or,
+    within a display generated by the Derivative Works, if and
+    wherever such third-party notices normally appear. The contents of
+    the NOTICE file are for informational purposes only and do not
+    modify the License. You may add Your own attribution notices
+    within Derivative Works that You distribute, alongside or as an
+    addendum to the NOTICE text from the Work, provided that such
+    additional attribution notices cannot be construed as modifying
+    the License.
+
+    You may add Your own copyright statement to Your modifications and
+    may provide additional or different license terms and conditions
+    for use, reproduction, or distribution of Your modifications, or
+    for any such Derivative Works as a whole, provided Your use,
+    reproduction, and distribution of the Work otherwise complies with
+    the conditions stated in this License.
+
+5. Submission of Contributions. Unless You explicitly state otherwise,
+any Contribution intentionally submitted for inclusion in the Work by
+You to the Licensor shall be under the terms and conditions of this
+License, without any additional terms or conditions. Notwithstanding
+the above, nothing herein shall supersede or modify the terms of any
+separate license agreement you may have executed with Licensor
+regarding such Contributions.
+
+6. Trademarks. This License does not grant permission to use the trade
+names, trademarks, service marks, or product names of the Licensor,
+except as required for reasonable and customary use in describing the
+origin of the Work and reproducing the content of the NOTICE file.
+
+7. Disclaimer of Warranty. Unless required by applicable law or agreed
+to in writing, Licensor provides the Work (and each Contributor
+provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR
+CONDITIONS OF ANY KIND, either express or implied, including, without
+limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT,
+MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely
+responsible for determining the appropriateness of using or
+redistributing the Work and assume any risks associated with Your
+exercise of permissions under this License.
+
+8. Limitation of Liability. In no event and under no legal theory,
+whether in tort (including negligence), contract, or otherwise, unless
+required by applicable law (such as deliberate and grossly negligent
+acts) or agreed to in writing, shall any Contributor be liable to You
+for damages, including any direct, indirect, special, incidental, or
+consequential damages of any character arising as a result of this
+License or out of the use or inability to use the Work (including but
+not limited to damages for loss of goodwill, work stoppage, computer
+failure or malfunction, or any and all other commercial damages or
+losses), even if such Contributor has been advised of the possibility
+of such damages.
+
+9. Accepting Warranty or Additional Liability. While redistributing
+the Work or Derivative Works thereof, You may choose to offer, and
+charge a fee for, acceptance of support, warranty, indemnity, or other
+liability obligations and/or rights consistent with this License.
+However, in accepting such obligations, You may act only on Your own
+behalf and on Your sole responsibility, not on behalf of any other
+Contributor, and only if You agree to indemnify, defend, and hold each
+Contributor harmless for any liability incurred by, or claims asserted
+against, such Contributor by reason of your accepting any such
+warranty or additional liability.
+
+END OF TERMS AND CONDITIONS
\ No newline at end of file
diff --git a/YubiKit/README.md b/YubiKit/README.md
new file mode 100755
index 000000000..e49b9a604
--- /dev/null
+++ b/YubiKit/README.md
@@ -0,0 +1,203 @@
+# Yubico Mobile iOS SDK (YubiKit)
+
+**YubiKit** is an iOS library provided by Yubico to interact with YubiKeys on iOS devices. 
+
+The library is provided with a [demo application](./YubiKitDemo/README.md) which shows complete examples of how the library can be integrated and demonstrates all the features of this library in an iOS project.
+
+Changes to this library are documented in this [Changelog](Changelog.md).
+
+## **About**
+
+**YubiKit** requires a physical key to test its features. Before running the included [demo application](./YubiKitDemo/README.md) or integrating YubiKit into your own app, you need an NFC-Enabled YubiKey or a YubiKey 5Ci to test functionality.
+
+The host application can build the library as a dependency of the application target when used inside a Xcode workspace. In addition, the  library can be packed using the `build.sh` script, which is provided in the root folder of this project.
+
+## **Getting Started**
+
+To get started, you can try the [demo](./YubiKitDemo/README.md) as part of this library or start integrating the library into your own application. 
+
+## Try the Demo
+The library is provided with a demo application, [**YubiKitDemo**](./YubiKitDemo). The application is implemented in Swift and it shows several examples of how to use YubiKit, including WebAuthn/FIDO2 over the accessory or NFC YubiKeys.
+
+The YubiKit Demo application shows how the library is linked with a project so it can be used for a side-by-side comparison when adding the library to your own project.
+
+## Integrate the library
+
+This section is intended for developers that want to start with their own iOS app and add  the YubiKit manually.
+
+<details><summary><strong>Step-by-step instructions</strong></summary><p>
+
+YubiKit SDK is currently available as a library and can be added to any new or existing iOS Xcode project.
+
+**Download or Clone YubiKit SDK**
+
+1. The library is archived into a Zip file named YubiKit[version] where version is the version number of the packed library. 
+
+    [Download](https://github.com/Yubico/yubikit-ios/releases/) the latest YubiKit SDK (.zip) to your desktop `or` 
+
+    `git clone https://github.com/Yubico/yubikit-ios.git`
+
+2. Unzip the library archive.
+
+**Add YubiKit folder to your Xcode project**
+
+3. Drag the entire `/YubiKit[version]/YubiKit` folder to your Xcode project. Check the option *Copy items if needed*. 
+
+**Linked Frameworks and Libraries**
+
+4. `Project Settings` > `General` > `Linked Frameworks and Libraries`.
+Click + and select Add Other. Locate the ``libYubiKit.a`` in YubiKit/debug_universal folder and add it.
+
+**Library Search Paths**
+
+5. ``Build Settings`` > Filter by 'Library Search Paths', expand to show debug & release.
+Set Release to ``YubiKit/release`` folder and
+Set Debug to ``YubiKit/debug_universal`` folder.
+
+**Header Search Paths**
+
+6. ``Build Settings`` > Filter by 'Header Search Path'. Set both Debug & Release to ``./YubiKit/**`` (recursive)
+
+**-ObjC flag**
+
+7. Add -ObjC flag
+``Build Settings`` > Filter by 'Other Linker Flags'. Add the ``-ObjC`` flag to Debug and Release.
+
+**Bridging-Header**
+
+8. If your target project is written in Swift, you need to provide a bridge to the YubiKit library by adding ``#import <YubiKit/YubiKit.h>`` to your bridging header. If a bridging header does not exist within your project, you can add one by following this [documentation](https://developer.apple.com/library/content/documentation/Swift/Conceptual/BuildingCocoaApps/MixandMatch.html).
+    
+
+**Enable Custom Lightning Protocol**
+
+`REQUIRED` if you are supporting the YubiKey 5Ci over the Lightning connector.
+
+> The YubiKey 5Ci is an Apple MFi external accessory and communicates over iAP2. You are telling your app that all communication with the 5Ci as a supported external accessory is via `com.yubico.ylp`.
+
+Open info.plist and add `com.yubico.ylp` as a new item under `Supported external accessory protocols`
+
+**Grant accesss to NFC**
+
+`REQUIRED` if you are supporting NFC-Enabled YubiKeys.
+
+Open info.plist and add the following usage:
+'Privacy - NFC Scan Usage Description' - "This application needs access to NFC"
+
+**Grant accesss to CAMERA**
+
+Optional: if you are planning to use the camera to read QR codes for OTP
+Open info.plist and add the following usage:
+'Privacy - Camera Usage Description' - "This application needs access to Camera for reading QR codes."
+
+</p>
+</details>
+
+## Documentation
+YubiKit headers are documented and the documentation is available either by reading the header file or by using the QuickHelp from Xcode (Option + Click symbol). Use this documentation for a more detailed explanation of all the methods, properties, and parameters from the API. If you are interested in implementation details for a specific category like U2F, FIDO2, or OATH, checkout the [./docs](./docs/) section.
+
+## **Customize the Library**
+YubiKit allows customizing some of its behavior by using `YubiKitConfiguration` and `YubiKitExternalLocalization`.
+<details><summary><strong>Customizing YubiKit Behavior</strong></summary><p>
+
+For providing localized strings for the user facing messages shown by the library, YubiKit provides a collection of properties in `YubiKitExternalLocalization`.
+
+One example of a localized string is the message shown in the NFC scanning UI while the device waits for a YubiKey to be scanned. This message can be localized by setting the value of `nfcScanAlertMessage`:
+	
+##### Swift
+
+```swift
+let localizedAlertMessage = NSLocalizedString("NFC_SCAN_MESSAGE", comment: "Scan your YubiKey.")
+YubiKitExternalLocalization.nfcScanAlertMessage = localizedAlertMessage
+```
+
+##### Objective-C
+
+```objective-c
+#import <YubiKit/YubiKit.h>
+...
+NSString *localizedAlertMessage = NSLocalizedString(@"NFC_SCAN_MESSAGE", @"Scan your YubiKey.");
+YubiKitExternalLocalization.nfcScanAlertMessage = localizedNfcScanAlertMessage;
+```
+
+For all the available properties and their use look at the code documentation for `YubiKitExternalLocalization`.
+
+---
+
+**Note:**
+`YubiKitExternalLocalization` provides default values in English (en-US), which are useful only for debugging and prototyping. For production code always provide localized values.
+
+---
+
+
+</p>
+</details>
+
+## **Using the Library**
+Once you have integrated the library, you can implement many of the features documented below:
+
+- [FIDO](./docs/fido2.md) - Provides FIDO2 operations accessible via the *YKFKeyFIDO2Service*.
+
+- [U2F](./docs/u2f.md) - Provides U2F operations accessible via the *YKFKeyU2FService*.
+
+- [OATH](./docs/oath.md) - Allows applications, such as an authenticator app to store OATH TOTP and HOTP secrets on a YubiKey and generate one-time passwords.
+
+- [OTP](./docs/otp.md) - Provides implementation classes to obtain YubiKey OTP via accessory (5Ci) or NFC.
+
+- [RAW](./docs/raw.md) - Allows sending raw commands to YubiKeys over two channels: *YKFKeyRawCommandService* or over a [PC/SC](https://en.wikipedia.org/wiki/PC/SC) like interface.
+
+## **YubiKit FAQ**
+
+<details><summary><strong>Frequently Asked Questions About YubiKit</strong></summary><p>
+
+#### Q1. Does YubiKit store any data on the device?
+
+Yubikit doesn't store any data locally on the device. This includes NSUserDefaults, application sandbox folders and Keychain. All the data required to perform an operation is stored in memory for the duration of the operation and then discarded.
+
+#### Q2. Does YubiKit communicate with any services?
+
+Yubikit doesn't communicate with any services, like web services or other type of network communication. YubiKit is a library for sending, receiving and processing the data from a YubiKey.
+
+#### Q3. Can I use YubiKit with other devices which are not from Yubico?
+
+YubiKit is a library which should be used only to interact with a device manufactured by Yubico. While some parts of it may work with other devices, the library was developed and tested to work with YubiKeys. When attaching a MFI accessory, YubiKit will always check if the manufacturer of the device is Yubico before connecting to it.
+
+#### Q4. Is YubiKit compiled with support for Bitcode and Position Independent code?
+
+Yes, YubiKit is compiled to accommodate any modern iOS project. The supplied library is compiled with Position Independent code and Bitcode. The release version of the library is optimized (Fastest, smallest).
+
+#### Q5. Is YubiKit logging or asserting in release mode?
+
+No, YubiKit is not logging in release mode. The logs from YubiKit will show only in debug builds to help the developer to see what YubiKit does. The same stands for assertions. YubiKit will assert in debug mode to warn the developer when invalid parameters are passed to the library or when something unexpected happened with the key. In release, the library will handle invalid states in different ways (e.g. returning nil if the object was not properly initialized, returning errors, etc.).
+
+#### Q6. Are there any versions of iOS where YubiKit does not work?
+
+YubiKit should work on any modern version of iOS (10+) with a few exceptions\*. It's recommended to always ask the users to upgrade to the latest version of iOS to protect them from known, old iOS issues. Supporting the last 2 version of iOS (n and n-1) is usually a good practice to keep the old versions of iOS out. According to [Apple statistics](https://developer.apple.com/support/app-store/), ~90-95% of all iOS devices run the latest 2 versions of iOS because upgrading the OS is free and Apple usually provides a device with upgrades for 5 years.
+
+\* Some versions of iOS had bugs affecting all external accessories. iOS 11.2 was one of them where the applications could not communicate with accessories due to some bugs in the XPC communication. The bug was fixed by Apple in iOS 11.2.6. For these reasons it's recommended to take in consideration rare but possible iOS bugs when designing the application. 
+
+#### Q7. How can I debug the application while using a MFi accessory YubiKey?
+
+Starting from Xcode 9, the IDE provides the ability to debug the application wirelessly. In this way the physical connector is not used for connecting the device to the computer, for debugging the application. This [WWDC session](https://developer.apple.com/videos/play/wwdc2017/404/) explains the wireless debugging functionality in Xcode.
+
+#### Q8. Are the USB-C type iOS devices supported by the YubiKey 5Ci?
+
+The USB-C type iOS devices, such as the iPad Pro 3rd generation, have limited support when using the YubiKey 5Ci or another type of YubiKey with USB-C connector. The OS is not officially supporting external accessories on these devices. However these devices support external USB keyboards, so the OTP functionality of the key will work and the key can be used to generate Yubico OTPs and HOTPs. 
+
+</p>
+</details>
+
+## **Additional resources**
+
+1. Xcode Help - [Add a capability to a target](http://help.apple.com/xcode/mac/current/#/dev88ff319e7)
+2. Xcode Help - [Build settings reference](http://help.apple.com/xcode/mac/current/#/itcaec37c2a6)
+3. Technical Q&A QA1490 -
+[Building Objective-C static libraries with categories](https://developer.apple.com/library/content/qa/qa1490/_index.html)
+4. Apple Developer - [Swift and Objective-C in the Same Project](https://developer.apple.com/library/content/documentation/Swift/Conceptual/BuildingCocoaApps/MixandMatch.html)
+5. Yubico - [Developers website](https://developers.yubico.com)
+6. Yubico - [Online Demo](https://demo.yubico.com) for OTP and U2F
+7. Yubico - [OTP documentation](https://developers.yubico.com/OTP)
+8. Yubico - [What is U2F?](https://developers.yubico.com/U2F)
+9. Yubico - [YKOATH Protocol Specifications](https://developers.yubico.com/OATH/YKOATH_Protocol.html)
+10. FIDO Alliance - [CTAP2 specifications](https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html)
+11. W3.org - [Web Authentication:
+An API for accessing Public Key Credentials](https://www.w3.org/TR/webauthn/)
diff --git a/YubiKit/YubiKit.xcodeproj/project.pbxproj b/YubiKit/YubiKit.xcodeproj/project.pbxproj
new file mode 100755
index 000000000..906c22804
--- /dev/null
+++ b/YubiKit/YubiKit.xcodeproj/project.pbxproj
@@ -0,0 +1,1715 @@
+// !$*UTF8*$!
+{
+	archiveVersion = 1;
+	classes = {
+	};
+	objectVersion = 48;
+	objects = {
+
+/* Begin PBXBuildFile section */
+		816C684B2343126100209342 /* YKFNFCTagDescription.m in Sources */ = {isa = PBXBuildFile; fileRef = 816C684A2343126100209342 /* YKFNFCTagDescription.m */; };
+		816C684E23431BE600209342 /* YKFNFCTagDescription.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 816C68492343126100209342 /* YKFNFCTagDescription.h */; };
+		95081DEE2214255B006CD08C /* YKFKeyRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = 95081DED2214255B006CD08C /* YKFKeyRequest.m */; };
+		95081DEF22142581006CD08C /* YKFKeyRequest.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 95081DEC2214255B006CD08C /* YKFKeyRequest.h */; };
+		950C70092298095F00E48458 /* YubiKitDeviceCapabilitiesTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 950C70082298095F00E48458 /* YubiKitDeviceCapabilitiesTests.m */; };
+		950C700C22980CFE00E48458 /* FakeUIDevice.m in Sources */ = {isa = PBXBuildFile; fileRef = 950C700B22980CFE00E48458 /* FakeUIDevice.m */; };
+		951446A021873D43002BB3C5 /* YKFNSMutableDataAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 95D5E2DB2187159700AA1C11 /* YKFNSMutableDataAdditions.m */; };
+		951446A421874CC2002BB3C5 /* YKFKeyCommandConfiguration.m in Sources */ = {isa = PBXBuildFile; fileRef = 951446A321874CC2002BB3C5 /* YKFKeyCommandConfiguration.m */; };
+		951446A521876060002BB3C5 /* YKFKeyRawCommandService.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 95D5E2D7218712BF00AA1C11 /* YKFKeyRawCommandService.h */; };
+		951446AA2188592C002BB3C5 /* YKFPCSCLayer.m in Sources */ = {isa = PBXBuildFile; fileRef = 951446A92188592C002BB3C5 /* YKFPCSCLayer.m */; };
+		951446AD21885962002BB3C5 /* YKFPCSC.m in Sources */ = {isa = PBXBuildFile; fileRef = 951446AC21885962002BB3C5 /* YKFPCSC.m */; };
+		95233E552330DEE000C51F92 /* YubiKitLogger.m in Sources */ = {isa = PBXBuildFile; fileRef = 95233E532330DEDF00C51F92 /* YubiKitLogger.m */; };
+		95233E562330DEF200C51F92 /* YubiKitLogger.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 95233E542330DEDF00C51F92 /* YubiKitLogger.h */; };
+		952564C8216F699500BC6F64 /* YKFOATHResetAPDU.m in Sources */ = {isa = PBXBuildFile; fileRef = 952564C7216F699500BC6F64 /* YKFOATHResetAPDU.m */; };
+		95294B46231527F50014C30B /* YKFNFCSession.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 95E152A52315257600C4B7E7 /* YKFNFCSession.h */; };
+		9529CBBA214903FA0041D2F8 /* YKFAccessorySessionConfigurationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 9529CBB9214903FA0041D2F8 /* YKFAccessorySessionConfigurationTests.m */; };
+		9529CBBD214905770041D2F8 /* FakeEAAccessory.m in Sources */ = {isa = PBXBuildFile; fileRef = 9529CBBC214905770041D2F8 /* FakeEAAccessory.m */; };
+		9529CBBF2149105F0041D2F8 /* YKFAccessoryDescriptionTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 9529CBBE2149105F0041D2F8 /* YKFAccessoryDescriptionTests.m */; };
+		9529CBC1214927D80041D2F8 /* YKFKeyU2FServiceTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 9529CBC0214927D80041D2F8 /* YKFKeyU2FServiceTests.m */; };
+		9529CBC421492A3E0041D2F8 /* FakeYKFKeyConnectionController.m in Sources */ = {isa = PBXBuildFile; fileRef = 9529CBC321492A3E0041D2F8 /* FakeYKFKeyConnectionController.m */; };
+		952CB9EC220D835F004A7624 /* YKFFIDO2PinAuthKey.m in Sources */ = {isa = PBXBuildFile; fileRef = 952CB9EB220D835F004A7624 /* YKFFIDO2PinAuthKey.m */; };
+		953267F6225B3DB200965E75 /* YKFKeyFIDO2Error.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 95B0CAAB21EF53E1009C6A34 /* YKFKeyFIDO2Error.h */; };
+		9533068A2088CB9F00A625C8 /* UIWindowAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 953306892088CB9F00A625C8 /* UIWindowAdditions.m */; };
+		9535F0122175FFB600A6D617 /* YKFKeyOATHValidateResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = 9535F0112175FFB600A6D617 /* YKFKeyOATHValidateResponse.m */; };
+		953A5079213E9F4600929ABB /* YKFAccessoryDescription.m in Sources */ = {isa = PBXBuildFile; fileRef = 953A5078213E9F4600929ABB /* YKFAccessoryDescription.m */; };
+		953A507D213EA16600929ABB /* YKFAccessoryDescription.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 953A5077213E9F4600929ABB /* YKFAccessoryDescription.h */; };
+		953A5085213FCDA100929ABB /* FakeEASession.m in Sources */ = {isa = PBXBuildFile; fileRef = 953A5084213FCDA100929ABB /* FakeEASession.m */; };
+		953A6FC221F733D8003B2477 /* YKFFIDO2GetAssertionAPDU.m in Sources */ = {isa = PBXBuildFile; fileRef = 953A6FC121F733D8003B2477 /* YKFFIDO2GetAssertionAPDU.m */; };
+		9541143521908A8C00D39C7B /* YKFPCSC.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 951446AB21885962002BB3C5 /* YKFPCSC.h */; };
+		9541143621908A8C00D39C7B /* YKFPCSCErrors.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 951446AE21886966002BB3C5 /* YKFPCSCErrors.h */; };
+		9541143721908A8C00D39C7B /* YKFPCSCTypes.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 951446AF218876D9002BB3C5 /* YKFPCSCTypes.h */; };
+		9547C9DD216B59E2001E1F4A /* YKFKeyOATHCalculateResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = 9547C9DC216B59E2001E1F4A /* YKFKeyOATHCalculateResponse.m */; };
+		9547C9E1216B6ECE001E1F4A /* YKFKeyOATHCalculateRequest.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 95EEEF672167A93900BE7D7B /* YKFKeyOATHCalculateRequest.h */; };
+		9547C9E2216B6ECE001E1F4A /* YKFKeyOATHCalculateResponse.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 9547C9DB216B59E2001E1F4A /* YKFKeyOATHCalculateResponse.h */; };
+		954E2C512211A34900720D2B /* YKFKeyFIDO2ClientPinRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = 954E2C502211A34900720D2B /* YKFKeyFIDO2ClientPinRequest.m */; };
+		954E2C542211AA5600720D2B /* YKFFIDO2ClientPinAPDU.m in Sources */ = {isa = PBXBuildFile; fileRef = 954E2C532211AA5600720D2B /* YKFFIDO2ClientPinAPDU.m */; };
+		954E2C572211B53100720D2B /* YKFKeyFIDO2ClientPinResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = 954E2C562211B53100720D2B /* YKFKeyFIDO2ClientPinResponse.m */; };
+		954E2C5A2211C64900720D2B /* YKFKeyFIDO2VerifyPinRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = 954E2C592211C64900720D2B /* YKFKeyFIDO2VerifyPinRequest.m */; };
+		954E2C5B2211C6A900720D2B /* YKFKeyFIDO2VerifyPinRequest.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 954E2C582211C64900720D2B /* YKFKeyFIDO2VerifyPinRequest.h */; };
+		955188282265E4B9001A4191 /* YKFKeyU2FError.m in Sources */ = {isa = PBXBuildFile; fileRef = 955188272265E4B9001A4191 /* YKFKeyU2FError.m */; };
+		955188292265E70B001A4191 /* YKFKeyU2FError.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 955188262265E4B9001A4191 /* YKFKeyU2FError.h */; };
+		9551882C2265E8DE001A4191 /* YKFKeyOATHError.m in Sources */ = {isa = PBXBuildFile; fileRef = 9551882B2265E8DE001A4191 /* YKFKeyOATHError.m */; };
+		9551882D2265EAEA001A4191 /* YKFKeyOATHError.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 9551882A2265E8DE001A4191 /* YKFKeyOATHError.h */; };
+		955188302265F4EE001A4191 /* YKFKeyAPDUError.m in Sources */ = {isa = PBXBuildFile; fileRef = 9551882F2265F4EE001A4191 /* YKFKeyAPDUError.m */; };
+		955188312265FC89001A4191 /* YKFKeyAPDUError.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 9551882E2265F4EE001A4191 /* YKFKeyAPDUError.h */; };
+		955BCBF5215A33CA00C2EA2B /* YKFOATHPutAPDU.m in Sources */ = {isa = PBXBuildFile; fileRef = 955BCBF4215A33CA00C2EA2B /* YKFOATHPutAPDU.m */; };
+		955BCC01215A436100C2EA2B /* YKFKeyOATHPutRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = 955BCC00215A436100C2EA2B /* YKFKeyOATHPutRequest.m */; };
+		955BCC04215A43A000C2EA2B /* YKFKeyOATHRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = 955BCC03215A43A000C2EA2B /* YKFKeyOATHRequest.m */; };
+		955BCC0A215A463E00C2EA2B /* YKFOATHCredential.m in Sources */ = {isa = PBXBuildFile; fileRef = 955BCC09215A463E00C2EA2B /* YKFOATHCredential.m */; };
+		955BCC0F215A9A4D00C2EA2B /* MF_Base32Additions.m in Sources */ = {isa = PBXBuildFile; fileRef = 955BCC0E215A9A4C00C2EA2B /* MF_Base32Additions.m */; };
+		955DF94A21F8BDE800CED8F1 /* YKFKeyFIDO2GetAssertionResponse.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 95BA204321F7483100EED927 /* YKFKeyFIDO2GetAssertionResponse.h */; };
+		9564333220A58EDA007621BD /* YKFOTPURIParserTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 9564333120A58EDA007621BD /* YKFOTPURIParserTests.m */; };
+		9564333420A5B99F007621BD /* YKFOTPTextParserTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 9564333320A5B99F007621BD /* YKFOTPTextParserTests.m */; };
+		9564333620A5C03C007621BD /* YKFOTPTokenParserTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 9564333520A5C03C007621BD /* YKFOTPTokenParserTests.m */; };
+		956884C120AAD98500E0F72C /* YKFNFCOTPServiceTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 956884C020AAD98500E0F72C /* YKFNFCOTPServiceTests.m */; };
+		956884C520AADA5A00E0F72C /* FakeNFCNDEFReaderSession.m in Sources */ = {isa = PBXBuildFile; fileRef = 956884C420AADA5A00E0F72C /* FakeNFCNDEFReaderSession.m */; };
+		956884C820AAE6F200E0F72C /* YKFNSDataAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 95DD40772099A4EB00363FEE /* YKFNSDataAdditions.m */; };
+		956884C920AAE70E00E0F72C /* UIDeviceAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 95C29634206259050091318B /* UIDeviceAdditions.m */; };
+		956884CD20AAFB3F00E0F72C /* FakeYubiKitDeviceCapabilities.m in Sources */ = {isa = PBXBuildFile; fileRef = 956884CC20AAFB3F00E0F72C /* FakeYubiKitDeviceCapabilities.m */; };
+		956884D020AAFE9300E0F72C /* FakeYKFOTPTextParser.m in Sources */ = {isa = PBXBuildFile; fileRef = 956884CF20AAFE9300E0F72C /* FakeYKFOTPTextParser.m */; };
+		956884D320AB012200E0F72C /* FakeYKFOTPURIParser.m in Sources */ = {isa = PBXBuildFile; fileRef = 956884D220AB012200E0F72C /* FakeYKFOTPURIParser.m */; };
+		956991F422C224BC00C5EB02 /* YKFWebAuthnClientData.m in Sources */ = {isa = PBXBuildFile; fileRef = 956991F322C224BC00C5EB02 /* YKFWebAuthnClientData.m */; };
+		956DB66B206391C5006B1738 /* YKFQRCodeScanError.m in Sources */ = {isa = PBXBuildFile; fileRef = 956DB66A206391C5006B1738 /* YKFQRCodeScanError.m */; };
+		956DB66F20639C2B006B1738 /* YKFQRCodeScanOverlayView.m in Sources */ = {isa = PBXBuildFile; fileRef = 956DB66E20639C2B006B1738 /* YKFQRCodeScanOverlayView.m */; };
+		956DB67220639C7D006B1738 /* YKFQRCodeScanViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 956DB67120639C7D006B1738 /* YKFQRCodeScanViewController.m */; };
+		956DB6742063B41E006B1738 /* AudioToolbox.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 956DB6732063B41E006B1738 /* AudioToolbox.framework */; };
+		956DB6762063DEF1006B1738 /* YubiKitManager.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 95C296582062A5970091318B /* YubiKitManager.h */; };
+		956DB6772063DEF7006B1738 /* YubiKitConfiguration.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 95C296552062A46A0091318B /* YubiKitConfiguration.h */; };
+		956DB6782063DEFC006B1738 /* YubiKitExternalLocalization.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 95C2964E206295A00091318B /* YubiKitExternalLocalization.h */; };
+		956DB6792063DF57006B1738 /* YubiKitDeviceCapabilities.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 95C2965220629FD10091318B /* YubiKitDeviceCapabilities.h */; };
+		956DB67A2063DF7E006B1738 /* YKFQRCodeScanError.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 956DB669206391C5006B1738 /* YKFQRCodeScanError.h */; };
+		956DB67D2063DFA1006B1738 /* YKFOTPToken.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 95C29621206247920091318B /* YKFOTPToken.h */; };
+		956DB67F2063E073006B1738 /* YKFNFCError.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 95C2964820627F2F0091318B /* YKFNFCError.h */; };
+		956DB6892068EE9D006B1738 /* YKFOTPURIParserProtocol.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 956DB6872068EE02006B1738 /* YKFOTPURIParserProtocol.h */; };
+		956DB68A2068EE9D006B1738 /* YKFOTPTextParserProtocol.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 956DB6882068EE64006B1738 /* YKFOTPTextParserProtocol.h */; };
+		956DBB8421EDEA1D004D6EE3 /* YKFKeyFIDO2Service.m in Sources */ = {isa = PBXBuildFile; fileRef = 956DBB8321EDEA1D004D6EE3 /* YKFKeyFIDO2Service.m */; };
+		956DBB8921EDFE50004D6EE3 /* YKFSelectFIDO2ApplicationAPDU.m in Sources */ = {isa = PBXBuildFile; fileRef = 956DBB8821EDFE50004D6EE3 /* YKFSelectFIDO2ApplicationAPDU.m */; };
+		956DBB8D21EE0A0B004D6EE3 /* YKFFIDO2CommandAPDU.m in Sources */ = {isa = PBXBuildFile; fileRef = 956DBB8C21EE0A0B004D6EE3 /* YKFFIDO2CommandAPDU.m */; };
+		956DBB9021EE1E5E004D6EE3 /* YKFFIDO2GetInfoAPDU.m in Sources */ = {isa = PBXBuildFile; fileRef = 956DBB8F21EE1E5E004D6EE3 /* YKFFIDO2GetInfoAPDU.m */; };
+		956DBB9121EE22F5004D6EE3 /* YKFKeyFIDO2Service.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 956DBB8221EDEA1D004D6EE3 /* YKFKeyFIDO2Service.h */; };
+		956DBB9421EE2B29004D6EE3 /* YKFFIDO2ResetAPDU.m in Sources */ = {isa = PBXBuildFile; fileRef = 956DBB9321EE2B29004D6EE3 /* YKFFIDO2ResetAPDU.m */; };
+		956DBB9721EE32E5004D6EE3 /* YKFKeyFIDO2Request.m in Sources */ = {isa = PBXBuildFile; fileRef = 956DBB9621EE32E5004D6EE3 /* YKFKeyFIDO2Request.m */; };
+		9578A60A21F0BFAB00349DCF /* YKFKeyFIDO2GetInfoResponse.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 95B0CAAE21F098C6009C6A34 /* YKFKeyFIDO2GetInfoResponse.h */; };
+		9578A60D21F1D63000349DCF /* YKFKeyFIDO2MakeCredentialRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = 9578A60C21F1D63000349DCF /* YKFKeyFIDO2MakeCredentialRequest.m */; };
+		9578A61021F1D79400349DCF /* YKFFIDO2Type.m in Sources */ = {isa = PBXBuildFile; fileRef = 9578A60F21F1D79400349DCF /* YKFFIDO2Type.m */; };
+		9578A61121F1FF0800349DCF /* YKFFIDO2Type.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 9578A60E21F1D79400349DCF /* YKFFIDO2Type.h */; };
+		9578A61221F2053C00349DCF /* YKFKeyFIDO2MakeCredentialRequest.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 9578A60B21F1D63000349DCF /* YKFKeyFIDO2MakeCredentialRequest.h */; };
+		9578A61521F20BA400349DCF /* YKFFIDO2MakeCredentialAPDU.m in Sources */ = {isa = PBXBuildFile; fileRef = 9578A61421F20BA400349DCF /* YKFFIDO2MakeCredentialAPDU.m */; };
+		957BDF4F21F5C3A700899B5B /* YKFKeyFIDO2MakeCredentialResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = 957BDF4E21F5C3A700899B5B /* YKFKeyFIDO2MakeCredentialResponse.m */; };
+		957BDF5121F5E94700899B5B /* YKFKeyFIDO2MakeCredentialResponse.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 957BDF4D21F5C3A700899B5B /* YKFKeyFIDO2MakeCredentialResponse.h */; };
+		957BDF5421F7130900899B5B /* YKFKeyFIDO2GetAssertionRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = 957BDF5321F7130900899B5B /* YKFKeyFIDO2GetAssertionRequest.m */; };
+		957BDF5521F7131F00899B5B /* YKFKeyFIDO2GetAssertionRequest.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 957BDF5221F7130900899B5B /* YKFKeyFIDO2GetAssertionRequest.h */; };
+		957D869921B825B4004ABF86 /* YKFKeyRawCommandServiceTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 957D869821B825B4004ABF86 /* YKFKeyRawCommandServiceTests.m */; };
+		9581395421591DE1008558F3 /* YKFSelectOATHApplicationAPDU.m in Sources */ = {isa = PBXBuildFile; fileRef = 9581395321591DE1008558F3 /* YKFSelectOATHApplicationAPDU.m */; };
+		9581395921592870008558F3 /* YKFKeyOATHService.m in Sources */ = {isa = PBXBuildFile; fileRef = 9581395821592870008558F3 /* YKFKeyOATHService.m */; };
+		958491732130286900D7E2A3 /* YKFAccessorySessionConfiguration.m in Sources */ = {isa = PBXBuildFile; fileRef = 958491722130286900D7E2A3 /* YKFAccessorySessionConfiguration.m */; };
+		958793E7216CE35F001A0406 /* YKFOATHListAPDU.m in Sources */ = {isa = PBXBuildFile; fileRef = 958793E6216CE35F001A0406 /* YKFOATHListAPDU.m */; };
+		958793EA216CE6B9001A0406 /* YKFKeyOATHListRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = 958793E9216CE6B9001A0406 /* YKFKeyOATHListRequest.m */; };
+		958793EE216E0DB2001A0406 /* YKFKeyOATHListResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = 958793ED216E0DB2001A0406 /* YKFKeyOATHListResponse.m */; };
+		958793F1216E25C8001A0406 /* YKFKeyOATHListResponse.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 958793EC216E0DB2001A0406 /* YKFKeyOATHListResponse.h */; };
+		95885B0820A07ED800828D02 /* YKFQRReaderSession.m in Sources */ = {isa = PBXBuildFile; fileRef = 95885B0720A07ED800828D02 /* YKFQRReaderSession.m */; };
+		95885B0920A081F000828D02 /* YKFQRReaderSession.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 95885B0620A07ED800828D02 /* YKFQRReaderSession.h */; };
+		95885B0A20A081FD00828D02 /* YKFNFCOTPService.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 95C2964B20628EBE0091318B /* YKFNFCOTPService.h */; };
+		95885B0E20A098E600828D02 /* YKFKeySessionError.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 95DD408C2099A88400363FEE /* YKFKeySessionError.h */; };
+		95885B0F20A0992400828D02 /* YKFKeyU2FRegisterRequest.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 95DD40902099A89500363FEE /* YKFKeyU2FRegisterRequest.h */; };
+		95885B1020A0992400828D02 /* YKFKeyU2FRegisterResponse.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 95DD40982099A89600363FEE /* YKFKeyU2FRegisterResponse.h */; };
+		95885B1120A0992400828D02 /* YKFKeyU2FSignRequest.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 95DD40932099A89600363FEE /* YKFKeyU2FSignRequest.h */; };
+		95885B1220A0992400828D02 /* YKFKeyU2FSignResponse.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 95DD40942099A89600363FEE /* YKFKeyU2FSignResponse.h */; };
+		95885B1320A0992400828D02 /* YKFKeyU2FRequest.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 95DD40952099A89600363FEE /* YKFKeyU2FRequest.h */; };
+		95885B1420A0994F00828D02 /* YKFAccessorySession.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 95DD40A02099A8A300363FEE /* YKFAccessorySession.h */; };
+		95885B1520A0994F00828D02 /* YKFKeyU2FService.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 95DD40A22099A8A300363FEE /* YKFKeyU2FService.h */; };
+		95885B1620A0994F00828D02 /* YKFNSDataAdditions.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 95DD40762099A4EB00363FEE /* YKFNSDataAdditions.h */; };
+		95885B1720A2F92D00828D02 /* YKFSelectU2FApplicationAPDU.m in Sources */ = {isa = PBXBuildFile; fileRef = 95DD40812099A86900363FEE /* YKFSelectU2FApplicationAPDU.m */; };
+		95885B1820A2F94700828D02 /* YKFAccessoryConnectionController.m in Sources */ = {isa = PBXBuildFile; fileRef = 95DD40A52099A8A400363FEE /* YKFAccessoryConnectionController.m */; };
+		95885B1920A2F95800828D02 /* YKFKeyU2FRegisterRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = 95DD40912099A89500363FEE /* YKFKeyU2FRegisterRequest.m */; };
+		95885B1A20A2F96100828D02 /* YKFKeyU2FRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = 95DD40922099A89600363FEE /* YKFKeyU2FRequest.m */; };
+		95885B2A20A3187700828D02 /* libYubiKit.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 95C29611206247210091318B /* libYubiKit.a */; };
+		95885B3120A31EFF00828D02 /* YKFOTPTokenValidatorTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 95885B3020A31EFF00828D02 /* YKFOTPTokenValidatorTests.m */; };
+		95885B3220A31F1D00828D02 /* YKFOTPTokenValidator.m in Sources */ = {isa = PBXBuildFile; fileRef = 95C296252062497C0091318B /* YKFOTPTokenValidator.m */; };
+		958D0B64215D106F00942CB9 /* YKFKeyService.m in Sources */ = {isa = PBXBuildFile; fileRef = 958D0B63215D106F00942CB9 /* YKFKeyService.m */; };
+		958D0B68215D10F200942CB9 /* YKFKeyService.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 958D0B62215D106F00942CB9 /* YKFKeyService.h */; };
+		958D0B69215D129E00942CB9 /* YKFKeyOATHRequest.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 955BCC02215A43A000C2EA2B /* YKFKeyOATHRequest.h */; };
+		958D0B6A215D129E00942CB9 /* YKFKeyOATHPutRequest.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 955BCBFF215A436100C2EA2B /* YKFKeyOATHPutRequest.h */; };
+		958D0B6B215D129E00942CB9 /* YKFKeyOATHService.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 958139572159286F008558F3 /* YKFKeyOATHService.h */; };
+		958D0B6C215D129E00942CB9 /* YKFOATHCredential.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 955BCC08215A463E00C2EA2B /* YKFOATHCredential.h */; };
+		95A04D1E2253920B008E3036 /* YKFFIDO2GetNextAssertionAPDU.m in Sources */ = {isa = PBXBuildFile; fileRef = 95A04D1D2253920B008E3036 /* YKFFIDO2GetNextAssertionAPDU.m */; };
+		95A456692177639C00AD5A94 /* YKFOATHCalculateAllAPDU.m in Sources */ = {isa = PBXBuildFile; fileRef = 95A456682177639C00AD5A94 /* YKFOATHCalculateAllAPDU.m */; };
+		95A4566C2177663800AD5A94 /* YKFKeyOATHCalculateAllResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = 95A4566B2177663800AD5A94 /* YKFKeyOATHCalculateAllResponse.m */; };
+		95A4566E2177725B00AD5A94 /* YKFKeyOATHCalculateAllResponse.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 95A4566A2177663800AD5A94 /* YKFKeyOATHCalculateAllResponse.h */; };
+		95A4567121777D0E00AD5A94 /* YKFKeyOATHCalculateAllRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = 95A4567021777D0E00AD5A94 /* YKFKeyOATHCalculateAllRequest.m */; };
+		95B0CAAA21EF397F009C6A34 /* YKFKeyFIDO2Request.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 956DBB9521EE32E5004D6EE3 /* YKFKeyFIDO2Request.h */; };
+		95B0CAAD21EF53E1009C6A34 /* YKFKeyFIDO2Error.m in Sources */ = {isa = PBXBuildFile; fileRef = 95B0CAAC21EF53E1009C6A34 /* YKFKeyFIDO2Error.m */; };
+		95B0CAB021F098C6009C6A34 /* YKFKeyFIDO2GetInfoResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = 95B0CAAF21F098C6009C6A34 /* YKFKeyFIDO2GetInfoResponse.m */; };
+		95B4C66922C35B12000BD6E0 /* YKFWebAuthnClientData.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 956991F222C224BC00C5EB02 /* YKFWebAuthnClientData.h */; };
+		95B58B8B229C03AE00199F8E /* YKFAccessorySession+Debugging.m in Sources */ = {isa = PBXBuildFile; fileRef = 95B58B8A229C03AE00199F8E /* YKFAccessorySession+Debugging.m */; };
+		95B5F0942241257700DE7C96 /* YKFKeyFIDO2SetPinRequest.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 95CCAE5D2241220600B567D2 /* YKFKeyFIDO2SetPinRequest.h */; };
+		95B5F0952241257700DE7C96 /* YKFKeyFIDO2ChangePinRequest.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 95CCAE60224122FD00B567D2 /* YKFKeyFIDO2ChangePinRequest.h */; };
+		95B67185216F6A6300FA20E6 /* YKFKeyOATHResetRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = 95B67184216F6A6300FA20E6 /* YKFKeyOATHResetRequest.m */; };
+		95B8547C21E628BE000D6D7A /* YKFCBOREncoderTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 95B8547B21E628BE000D6D7A /* YKFCBOREncoderTests.m */; };
+		95B8547E21E898F3000D6D7A /* YKFCBORDecoderTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 95B8547D21E898F3000D6D7A /* YKFCBORDecoderTests.m */; };
+		95BA204521F7483100EED927 /* YKFKeyFIDO2GetAssertionResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = 95BA204421F7483100EED927 /* YKFKeyFIDO2GetAssertionResponse.m */; };
+		95BA204821F877BA00EED927 /* YKFFIDO2TouchPoolingAPDU.m in Sources */ = {isa = PBXBuildFile; fileRef = 95BA204721F877BA00EED927 /* YKFFIDO2TouchPoolingAPDU.m */; };
+		95C29617206247210091318B /* YubiKit.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 95C29614206247210091318B /* YubiKit.h */; };
+		95C2961F206247450091318B /* CoreNFC.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 95C2961E206247450091318B /* CoreNFC.framework */; };
+		95C29623206247920091318B /* YKFOTPToken.m in Sources */ = {isa = PBXBuildFile; fileRef = 95C29622206247920091318B /* YKFOTPToken.m */; };
+		95C296262062497C0091318B /* YKFOTPTokenValidator.m in Sources */ = {isa = PBXBuildFile; fileRef = 95C296252062497C0091318B /* YKFOTPTokenValidator.m */; };
+		95C29628206250820091318B /* AVFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 95C29627206250820091318B /* AVFoundation.framework */; };
+		95C2962C206250D90091318B /* YKFPermissions.m in Sources */ = {isa = PBXBuildFile; fileRef = 95C2962B206250D90091318B /* YKFPermissions.m */; };
+		95C29632206256120091318B /* YKFLogger.m in Sources */ = {isa = PBXBuildFile; fileRef = 95C29631206256120091318B /* YKFLogger.m */; };
+		95C29635206259050091318B /* UIDeviceAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 95C29634206259050091318B /* UIDeviceAdditions.m */; };
+		95C29637206259660091318B /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 95C29636206259660091318B /* UIKit.framework */; };
+		95C2963A20625A180091318B /* YKFView.m in Sources */ = {isa = PBXBuildFile; fileRef = 95C2963920625A180091318B /* YKFView.m */; };
+		95C2963D2062634C0091318B /* YKFViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 95C2963C2062634C0091318B /* YKFViewController.m */; };
+		95C296412062653E0091318B /* YKFOTPTokenParser.m in Sources */ = {isa = PBXBuildFile; fileRef = 95C296402062653E0091318B /* YKFOTPTokenParser.m */; };
+		95C296442062656C0091318B /* YKFOTPURIParser.m in Sources */ = {isa = PBXBuildFile; fileRef = 95C296432062656C0091318B /* YKFOTPURIParser.m */; };
+		95C29647206268C90091318B /* YKFOTPTextParser.m in Sources */ = {isa = PBXBuildFile; fileRef = 95C29646206268C90091318B /* YKFOTPTextParser.m */; };
+		95C2964A20627F2F0091318B /* YKFNFCError.m in Sources */ = {isa = PBXBuildFile; fileRef = 95C2964920627F2F0091318B /* YKFNFCError.m */; };
+		95C2964D20628EBE0091318B /* YKFNFCOTPService.m in Sources */ = {isa = PBXBuildFile; fileRef = 95C2964C20628EBE0091318B /* YKFNFCOTPService.m */; };
+		95C29650206295A00091318B /* YubiKitExternalLocalization.m in Sources */ = {isa = PBXBuildFile; fileRef = 95C2964F206295A00091318B /* YubiKitExternalLocalization.m */; };
+		95C2965420629FD10091318B /* YubiKitDeviceCapabilities.m in Sources */ = {isa = PBXBuildFile; fileRef = 95C2965320629FD10091318B /* YubiKitDeviceCapabilities.m */; };
+		95C296572062A46B0091318B /* YubiKitConfiguration.m in Sources */ = {isa = PBXBuildFile; fileRef = 95C296562062A46B0091318B /* YubiKitConfiguration.m */; };
+		95C2965A2062A5970091318B /* YubiKitManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 95C296592062A5970091318B /* YubiKitManager.m */; };
+		95CCAE5F2241220600B567D2 /* YKFKeyFIDO2SetPinRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = 95CCAE5E2241220600B567D2 /* YKFKeyFIDO2SetPinRequest.m */; };
+		95CCAE62224122FD00B567D2 /* YKFKeyFIDO2ChangePinRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = 95CCAE61224122FD00B567D2 /* YKFKeyFIDO2ChangePinRequest.m */; };
+		95D5E2D9218712BF00AA1C11 /* YKFKeyRawCommandService.m in Sources */ = {isa = PBXBuildFile; fileRef = 95D5E2D8218712BF00AA1C11 /* YKFKeyRawCommandService.m */; };
+		95D5E2DC2187159700AA1C11 /* YKFNSMutableDataAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 95D5E2DB2187159700AA1C11 /* YKFNSMutableDataAdditions.m */; };
+		95D5E2DD2187173D00AA1C11 /* YKFAPDU.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 95DD407F2099A86900363FEE /* YKFAPDU.h */; };
+		95D61A04216F9159001E7AC8 /* YKFOATHCredentialValidatorTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 95D61A03216F9159001E7AC8 /* YKFOATHCredentialValidatorTests.m */; };
+		95D61A072170B26F001E7AC8 /* YKFKeyOATHSelectApplicationResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = 95D61A062170B26F001E7AC8 /* YKFKeyOATHSelectApplicationResponse.m */; };
+		95D61A0A2170DEEF001E7AC8 /* YKFKeyOATHSetCodeRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = 95D61A092170DEEF001E7AC8 /* YKFKeyOATHSetCodeRequest.m */; };
+		95D61A0D2170E0C3001E7AC8 /* YKFOATHSetCodeAPDU.m in Sources */ = {isa = PBXBuildFile; fileRef = 95D61A0C2170E0C3001E7AC8 /* YKFOATHSetCodeAPDU.m */; };
+		95D61A0E2174D065001E7AC8 /* YKFKeyOATHSetCodeRequest.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 95D61A082170DEEE001E7AC8 /* YKFKeyOATHSetCodeRequest.h */; };
+		95D7364A21C9119D0039141A /* YKFPCSCErrorMap.m in Sources */ = {isa = PBXBuildFile; fileRef = 95D7364921C9119D0039141A /* YKFPCSCErrorMap.m */; };
+		95D7364D21C94BFD0039141A /* FakeYKFPCSCLayer.m in Sources */ = {isa = PBXBuildFile; fileRef = 95D7364C21C94BFD0039141A /* FakeYKFPCSCLayer.m */; };
+		95D7364F21CA44EF0039141A /* YKFPCSCTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 95D7364E21CA44EF0039141A /* YKFPCSCTests.m */; };
+		95D9D3DD21D5110100473888 /* YKFCBOREncoder.m in Sources */ = {isa = PBXBuildFile; fileRef = 95D9D3DC21D5110100473888 /* YKFCBOREncoder.m */; };
+		95D9D3E021D5111500473888 /* YKFCBORDecoder.m in Sources */ = {isa = PBXBuildFile; fileRef = 95D9D3DF21D5111500473888 /* YKFCBORDecoder.m */; };
+		95D9D3E321D67AAA00473888 /* YKFCBORType.m in Sources */ = {isa = PBXBuildFile; fileRef = 95D9D3E221D67AAA00473888 /* YKFCBORType.m */; };
+		95DD40782099A4EB00363FEE /* YKFNSDataAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 95DD40772099A4EB00363FEE /* YKFNSDataAdditions.m */; };
+		95DD407B2099A64C00363FEE /* YKFDispatch.m in Sources */ = {isa = PBXBuildFile; fileRef = 95DD407A2099A64C00363FEE /* YKFDispatch.m */; };
+		95DD40872099A86A00363FEE /* YKFU2FSignAPDU.m in Sources */ = {isa = PBXBuildFile; fileRef = 95DD40802099A86900363FEE /* YKFU2FSignAPDU.m */; };
+		95DD40892099A86A00363FEE /* YKFAPDU.m in Sources */ = {isa = PBXBuildFile; fileRef = 95DD40822099A86900363FEE /* YKFAPDU.m */; };
+		95DD408A2099A86A00363FEE /* YKFU2FRegisterAPDU.m in Sources */ = {isa = PBXBuildFile; fileRef = 95DD40852099A86900363FEE /* YKFU2FRegisterAPDU.m */; };
+		95DD408E2099A88500363FEE /* YKFKeySessionError.m in Sources */ = {isa = PBXBuildFile; fileRef = 95DD408D2099A88400363FEE /* YKFKeySessionError.m */; };
+		95DD409C2099A89600363FEE /* YKFKeyU2FSignResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = 95DD40962099A89600363FEE /* YKFKeyU2FSignResponse.m */; };
+		95DD409D2099A89600363FEE /* YKFKeyU2FRegisterResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = 95DD40972099A89600363FEE /* YKFKeyU2FRegisterResponse.m */; };
+		95DD409E2099A89600363FEE /* YKFKeyU2FSignRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = 95DD40992099A89600363FEE /* YKFKeyU2FSignRequest.m */; };
+		95DD40A62099A8A400363FEE /* YKFKeyU2FService.m in Sources */ = {isa = PBXBuildFile; fileRef = 95DD40A12099A8A300363FEE /* YKFKeyU2FService.m */; };
+		95DD40A72099A8A400363FEE /* YKFAccessorySession.m in Sources */ = {isa = PBXBuildFile; fileRef = 95DD40A32099A8A300363FEE /* YKFAccessorySession.m */; };
+		95DD6585216635CA00BA85C9 /* YKFOATHCredentialValidator.m in Sources */ = {isa = PBXBuildFile; fileRef = 95DD6584216635CA00BA85C9 /* YKFOATHCredentialValidator.m */; };
+		95DD658B21663B0400BA85C9 /* YKFKeyOATHDeleteRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = 95DD658A21663B0400BA85C9 /* YKFKeyOATHDeleteRequest.m */; };
+		95DD658E21663C3C00BA85C9 /* YKFOATHDeleteAPDU.m in Sources */ = {isa = PBXBuildFile; fileRef = 95DD658D21663C3C00BA85C9 /* YKFOATHDeleteAPDU.m */; };
+		95DD658F216648B300BA85C9 /* YKFKeyOATHDeleteRequest.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 95DD658921663B0400BA85C9 /* YKFKeyOATHDeleteRequest.h */; };
+		95DD659121664B6800BA85C9 /* YKFOATHCredentialTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 95DD659021664B6800BA85C9 /* YKFOATHCredentialTests.m */; };
+		95DF11922317C60600CF0C39 /* YKFNFCConnectionController.m in Sources */ = {isa = PBXBuildFile; fileRef = 95DF11912317C60600CF0C39 /* YKFNFCConnectionController.m */; };
+		95E152A72315257600C4B7E7 /* YKFNFCSession.m in Sources */ = {isa = PBXBuildFile; fileRef = 95E152A62315257600C4B7E7 /* YKFNFCSession.m */; };
+		95E1B259219EE2D300E349E3 /* YKFKVOObservation.m in Sources */ = {isa = PBXBuildFile; fileRef = 95E1B258219EE2D300E349E3 /* YKFKVOObservation.m */; };
+		95E3934420C137770027E7B4 /* YKFURIIdentifierCode.m in Sources */ = {isa = PBXBuildFile; fileRef = 95E3934320C137770027E7B4 /* YKFURIIdentifierCode.m */; };
+		95EA81E72178805D0020595D /* YKFNSStringAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 95EA81E62178805D0020595D /* YKFNSStringAdditions.m */; };
+		95EEEF6321664E4600BE7D7B /* MF_Base32Additions.m in Sources */ = {isa = PBXBuildFile; fileRef = 955BCC0E215A9A4C00C2EA2B /* MF_Base32Additions.m */; };
+		95EEEF6621676B7F00BE7D7B /* YKFOATHSendRemainingAPDU.m in Sources */ = {isa = PBXBuildFile; fileRef = 95EEEF6521676B7F00BE7D7B /* YKFOATHSendRemainingAPDU.m */; };
+		95EEEF692167A93900BE7D7B /* YKFKeyOATHCalculateRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = 95EEEF682167A93900BE7D7B /* YKFKeyOATHCalculateRequest.m */; };
+		95EEEF6C2167AB6A00BE7D7B /* YKFOATHCalculateAPDU.m in Sources */ = {isa = PBXBuildFile; fileRef = 95EEEF6B2167AB6A00BE7D7B /* YKFOATHCalculateAPDU.m */; };
+		95EF75BE213FE9F200059C79 /* YKFAccessoryConnectionControllerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 95EF75BD213FE9F200059C79 /* YKFAccessoryConnectionControllerTests.m */; };
+		95EF75C3213FEF0500059C79 /* YKFTestCase.m in Sources */ = {isa = PBXBuildFile; fileRef = 95EF75C2213FEF0500059C79 /* YKFTestCase.m */; };
+		95F75BAB2175D63D00C13DC5 /* YKFKeyOATHValidateRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = 95F75BAA2175D63D00C13DC5 /* YKFKeyOATHValidateRequest.m */; };
+		95F75BAE2175D6D600C13DC5 /* YKFOATHValidateAPDU.m in Sources */ = {isa = PBXBuildFile; fileRef = 95F75BAD2175D6D600C13DC5 /* YKFOATHValidateAPDU.m */; };
+		95F75BAF2175D8E300C13DC5 /* YKFKeyOATHValidateRequest.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 95F75BA92175D63D00C13DC5 /* YKFKeyOATHValidateRequest.h */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXContainerItemProxy section */
+		95885B2B20A3187700828D02 /* PBXContainerItemProxy */ = {
+			isa = PBXContainerItemProxy;
+			containerPortal = 95C29609206247210091318B /* Project object */;
+			proxyType = 1;
+			remoteGlobalIDString = 95C29610206247210091318B;
+			remoteInfo = YubiKit;
+		};
+/* End PBXContainerItemProxy section */
+
+/* Begin PBXCopyFilesBuildPhase section */
+		95C2960F206247210091318B /* CopyFiles */ = {
+			isa = PBXCopyFilesBuildPhase;
+			buildActionMask = 2147483647;
+			dstPath = "include/$(PRODUCT_NAME)";
+			dstSubfolderSpec = 16;
+			files = (
+				816C684E23431BE600209342 /* YKFNFCTagDescription.h in CopyFiles */,
+				95294B46231527F50014C30B /* YKFNFCSession.h in CopyFiles */,
+				95233E562330DEF200C51F92 /* YubiKitLogger.h in CopyFiles */,
+				95B4C66922C35B12000BD6E0 /* YKFWebAuthnClientData.h in CopyFiles */,
+				955188312265FC89001A4191 /* YKFKeyAPDUError.h in CopyFiles */,
+				9551882D2265EAEA001A4191 /* YKFKeyOATHError.h in CopyFiles */,
+				955188292265E70B001A4191 /* YKFKeyU2FError.h in CopyFiles */,
+				953267F6225B3DB200965E75 /* YKFKeyFIDO2Error.h in CopyFiles */,
+				95B5F0942241257700DE7C96 /* YKFKeyFIDO2SetPinRequest.h in CopyFiles */,
+				95B5F0952241257700DE7C96 /* YKFKeyFIDO2ChangePinRequest.h in CopyFiles */,
+				95081DEF22142581006CD08C /* YKFKeyRequest.h in CopyFiles */,
+				954E2C5B2211C6A900720D2B /* YKFKeyFIDO2VerifyPinRequest.h in CopyFiles */,
+				955DF94A21F8BDE800CED8F1 /* YKFKeyFIDO2GetAssertionResponse.h in CopyFiles */,
+				957BDF5521F7131F00899B5B /* YKFKeyFIDO2GetAssertionRequest.h in CopyFiles */,
+				957BDF5121F5E94700899B5B /* YKFKeyFIDO2MakeCredentialResponse.h in CopyFiles */,
+				9578A61221F2053C00349DCF /* YKFKeyFIDO2MakeCredentialRequest.h in CopyFiles */,
+				9578A61121F1FF0800349DCF /* YKFFIDO2Type.h in CopyFiles */,
+				9578A60A21F0BFAB00349DCF /* YKFKeyFIDO2GetInfoResponse.h in CopyFiles */,
+				95B0CAAA21EF397F009C6A34 /* YKFKeyFIDO2Request.h in CopyFiles */,
+				956DBB9121EE22F5004D6EE3 /* YKFKeyFIDO2Service.h in CopyFiles */,
+				9541143521908A8C00D39C7B /* YKFPCSC.h in CopyFiles */,
+				9541143621908A8C00D39C7B /* YKFPCSCErrors.h in CopyFiles */,
+				9541143721908A8C00D39C7B /* YKFPCSCTypes.h in CopyFiles */,
+				951446A521876060002BB3C5 /* YKFKeyRawCommandService.h in CopyFiles */,
+				95D5E2DD2187173D00AA1C11 /* YKFAPDU.h in CopyFiles */,
+				95A4566E2177725B00AD5A94 /* YKFKeyOATHCalculateAllResponse.h in CopyFiles */,
+				95F75BAF2175D8E300C13DC5 /* YKFKeyOATHValidateRequest.h in CopyFiles */,
+				95D61A0E2174D065001E7AC8 /* YKFKeyOATHSetCodeRequest.h in CopyFiles */,
+				958793F1216E25C8001A0406 /* YKFKeyOATHListResponse.h in CopyFiles */,
+				9547C9E1216B6ECE001E1F4A /* YKFKeyOATHCalculateRequest.h in CopyFiles */,
+				9547C9E2216B6ECE001E1F4A /* YKFKeyOATHCalculateResponse.h in CopyFiles */,
+				95DD658F216648B300BA85C9 /* YKFKeyOATHDeleteRequest.h in CopyFiles */,
+				958D0B69215D129E00942CB9 /* YKFKeyOATHRequest.h in CopyFiles */,
+				958D0B6A215D129E00942CB9 /* YKFKeyOATHPutRequest.h in CopyFiles */,
+				958D0B6B215D129E00942CB9 /* YKFKeyOATHService.h in CopyFiles */,
+				958D0B6C215D129E00942CB9 /* YKFOATHCredential.h in CopyFiles */,
+				958D0B68215D10F200942CB9 /* YKFKeyService.h in CopyFiles */,
+				953A507D213EA16600929ABB /* YKFAccessoryDescription.h in CopyFiles */,
+				95885B1420A0994F00828D02 /* YKFAccessorySession.h in CopyFiles */,
+				95885B1520A0994F00828D02 /* YKFKeyU2FService.h in CopyFiles */,
+				95885B1620A0994F00828D02 /* YKFNSDataAdditions.h in CopyFiles */,
+				95885B0F20A0992400828D02 /* YKFKeyU2FRegisterRequest.h in CopyFiles */,
+				95885B1020A0992400828D02 /* YKFKeyU2FRegisterResponse.h in CopyFiles */,
+				95885B1120A0992400828D02 /* YKFKeyU2FSignRequest.h in CopyFiles */,
+				95885B1220A0992400828D02 /* YKFKeyU2FSignResponse.h in CopyFiles */,
+				95885B1320A0992400828D02 /* YKFKeyU2FRequest.h in CopyFiles */,
+				95885B0E20A098E600828D02 /* YKFKeySessionError.h in CopyFiles */,
+				95885B0A20A081FD00828D02 /* YKFNFCOTPService.h in CopyFiles */,
+				95885B0920A081F000828D02 /* YKFQRReaderSession.h in CopyFiles */,
+				956DB6892068EE9D006B1738 /* YKFOTPURIParserProtocol.h in CopyFiles */,
+				956DB68A2068EE9D006B1738 /* YKFOTPTextParserProtocol.h in CopyFiles */,
+				956DB67F2063E073006B1738 /* YKFNFCError.h in CopyFiles */,
+				956DB67D2063DFA1006B1738 /* YKFOTPToken.h in CopyFiles */,
+				956DB67A2063DF7E006B1738 /* YKFQRCodeScanError.h in CopyFiles */,
+				956DB6792063DF57006B1738 /* YubiKitDeviceCapabilities.h in CopyFiles */,
+				956DB6782063DEFC006B1738 /* YubiKitExternalLocalization.h in CopyFiles */,
+				956DB6772063DEF7006B1738 /* YubiKitConfiguration.h in CopyFiles */,
+				956DB6762063DEF1006B1738 /* YubiKitManager.h in CopyFiles */,
+				95C29617206247210091318B /* YubiKit.h in CopyFiles */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXCopyFilesBuildPhase section */
+
+/* Begin PBXFileReference section */
+		816C68492343126100209342 /* YKFNFCTagDescription.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YKFNFCTagDescription.h; sourceTree = "<group>"; };
+		816C684A2343126100209342 /* YKFNFCTagDescription.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YKFNFCTagDescription.m; sourceTree = "<group>"; };
+		816C684C234315CC00209342 /* YKFNFCTagDescription+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "YKFNFCTagDescription+Private.h"; sourceTree = "<group>"; };
+		95081DEC2214255B006CD08C /* YKFKeyRequest.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YKFKeyRequest.h; sourceTree = "<group>"; };
+		95081DED2214255B006CD08C /* YKFKeyRequest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YKFKeyRequest.m; sourceTree = "<group>"; };
+		950C70072298051700E48458 /* UIDevice+Testing.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "UIDevice+Testing.h"; sourceTree = "<group>"; };
+		950C70082298095F00E48458 /* YubiKitDeviceCapabilitiesTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YubiKitDeviceCapabilitiesTests.m; sourceTree = "<group>"; };
+		950C700A22980CFE00E48458 /* FakeUIDevice.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FakeUIDevice.h; sourceTree = "<group>"; };
+		950C700B22980CFE00E48458 /* FakeUIDevice.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FakeUIDevice.m; sourceTree = "<group>"; };
+		950FB9302179D8F6003F712E /* YKFKeyOATHCalculateRequest+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "YKFKeyOATHCalculateRequest+Private.h"; sourceTree = "<group>"; };
+		9513760921512824006C843F /* EAAccessoryManager+Testing.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "EAAccessoryManager+Testing.h"; sourceTree = "<group>"; };
+		951446A121874013002BB3C5 /* YKFKeyRawCommandService+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "YKFKeyRawCommandService+Private.h"; sourceTree = "<group>"; };
+		951446A221874CC2002BB3C5 /* YKFKeyCommandConfiguration.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YKFKeyCommandConfiguration.h; sourceTree = "<group>"; };
+		951446A321874CC2002BB3C5 /* YKFKeyCommandConfiguration.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YKFKeyCommandConfiguration.m; sourceTree = "<group>"; };
+		951446A82188592C002BB3C5 /* YKFPCSCLayer.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YKFPCSCLayer.h; sourceTree = "<group>"; };
+		951446A92188592C002BB3C5 /* YKFPCSCLayer.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YKFPCSCLayer.m; sourceTree = "<group>"; };
+		951446AB21885962002BB3C5 /* YKFPCSC.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YKFPCSC.h; sourceTree = "<group>"; };
+		951446AC21885962002BB3C5 /* YKFPCSC.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YKFPCSC.m; sourceTree = "<group>"; };
+		951446AE21886966002BB3C5 /* YKFPCSCErrors.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YKFPCSCErrors.h; sourceTree = "<group>"; };
+		951446AF218876D9002BB3C5 /* YKFPCSCTypes.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YKFPCSCTypes.h; sourceTree = "<group>"; };
+		951EA6F92315B95500E35C8C /* YKFKeyConnectionControllerProtocol.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YKFKeyConnectionControllerProtocol.h; sourceTree = "<group>"; };
+		95233E532330DEDF00C51F92 /* YubiKitLogger.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YubiKitLogger.m; sourceTree = "<group>"; };
+		95233E542330DEDF00C51F92 /* YubiKitLogger.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YubiKitLogger.h; sourceTree = "<group>"; };
+		952564C6216F699500BC6F64 /* YKFOATHResetAPDU.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YKFOATHResetAPDU.h; sourceTree = "<group>"; };
+		952564C7216F699500BC6F64 /* YKFOATHResetAPDU.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YKFOATHResetAPDU.m; sourceTree = "<group>"; };
+		9529CBB9214903FA0041D2F8 /* YKFAccessorySessionConfigurationTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YKFAccessorySessionConfigurationTests.m; sourceTree = "<group>"; };
+		9529CBBB214905770041D2F8 /* FakeEAAccessory.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FakeEAAccessory.h; sourceTree = "<group>"; };
+		9529CBBC214905770041D2F8 /* FakeEAAccessory.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FakeEAAccessory.m; sourceTree = "<group>"; };
+		9529CBBE2149105F0041D2F8 /* YKFAccessoryDescriptionTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YKFAccessoryDescriptionTests.m; sourceTree = "<group>"; };
+		9529CBC0214927D80041D2F8 /* YKFKeyU2FServiceTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YKFKeyU2FServiceTests.m; sourceTree = "<group>"; };
+		9529CBC221492A3E0041D2F8 /* FakeYKFKeyConnectionController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FakeYKFKeyConnectionController.h; sourceTree = "<group>"; };
+		9529CBC321492A3E0041D2F8 /* FakeYKFKeyConnectionController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FakeYKFKeyConnectionController.m; sourceTree = "<group>"; };
+		9529CBC5214AA8A10041D2F8 /* YKFAPDUCommandInstruction.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YKFAPDUCommandInstruction.h; sourceTree = "<group>"; };
+		952CB9EA220D835F004A7624 /* YKFFIDO2PinAuthKey.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YKFFIDO2PinAuthKey.h; sourceTree = "<group>"; };
+		952CB9EB220D835F004A7624 /* YKFFIDO2PinAuthKey.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YKFFIDO2PinAuthKey.m; sourceTree = "<group>"; };
+		953306882088CB9F00A625C8 /* UIWindowAdditions.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = UIWindowAdditions.h; sourceTree = "<group>"; };
+		953306892088CB9F00A625C8 /* UIWindowAdditions.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = UIWindowAdditions.m; sourceTree = "<group>"; };
+		9535F0102175FFB600A6D617 /* YKFKeyOATHValidateResponse.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YKFKeyOATHValidateResponse.h; sourceTree = "<group>"; };
+		9535F0112175FFB600A6D617 /* YKFKeyOATHValidateResponse.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YKFKeyOATHValidateResponse.m; sourceTree = "<group>"; };
+		953A5077213E9F4600929ABB /* YKFAccessoryDescription.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YKFAccessoryDescription.h; sourceTree = "<group>"; };
+		953A5078213E9F4600929ABB /* YKFAccessoryDescription.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YKFAccessoryDescription.m; sourceTree = "<group>"; };
+		953A507A213EA10F00929ABB /* YKFAccessoryDescription+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "YKFAccessoryDescription+Private.h"; sourceTree = "<group>"; };
+		953A5080213FC77F00929ABB /* EASession+Testing.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "EASession+Testing.h"; sourceTree = "<group>"; };
+		953A5083213FCDA100929ABB /* FakeEASession.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FakeEASession.h; sourceTree = "<group>"; };
+		953A5084213FCDA100929ABB /* FakeEASession.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FakeEASession.m; sourceTree = "<group>"; };
+		953A5086213FD0E600929ABB /* EAAccessory+Testing.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "EAAccessory+Testing.h"; sourceTree = "<group>"; };
+		953A6FC021F733D8003B2477 /* YKFFIDO2GetAssertionAPDU.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YKFFIDO2GetAssertionAPDU.h; sourceTree = "<group>"; };
+		953A6FC121F733D8003B2477 /* YKFFIDO2GetAssertionAPDU.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YKFFIDO2GetAssertionAPDU.m; sourceTree = "<group>"; };
+		9547C9DB216B59E2001E1F4A /* YKFKeyOATHCalculateResponse.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YKFKeyOATHCalculateResponse.h; sourceTree = "<group>"; };
+		9547C9DC216B59E2001E1F4A /* YKFKeyOATHCalculateResponse.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YKFKeyOATHCalculateResponse.m; sourceTree = "<group>"; };
+		9547C9DE216B5AA6001E1F4A /* YKFKeyOATHCalculateResponse+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "YKFKeyOATHCalculateResponse+Private.h"; sourceTree = "<group>"; };
+		9547C9E3216CB648001E1F4A /* YKFAssert.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YKFAssert.h; sourceTree = "<group>"; };
+		954C600A21246253003A8497 /* YKFKeyU2FRegisterResponse+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "YKFKeyU2FRegisterResponse+Private.h"; sourceTree = "<group>"; };
+		954C601021247345003A8497 /* YKFKeyU2FSignResponse+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "YKFKeyU2FSignResponse+Private.h"; sourceTree = "<group>"; };
+		954E2C4F2211A34900720D2B /* YKFKeyFIDO2ClientPinRequest.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YKFKeyFIDO2ClientPinRequest.h; sourceTree = "<group>"; };
+		954E2C502211A34900720D2B /* YKFKeyFIDO2ClientPinRequest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YKFKeyFIDO2ClientPinRequest.m; sourceTree = "<group>"; };
+		954E2C522211AA5600720D2B /* YKFFIDO2ClientPinAPDU.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YKFFIDO2ClientPinAPDU.h; sourceTree = "<group>"; };
+		954E2C532211AA5600720D2B /* YKFFIDO2ClientPinAPDU.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YKFFIDO2ClientPinAPDU.m; sourceTree = "<group>"; };
+		954E2C552211B53100720D2B /* YKFKeyFIDO2ClientPinResponse.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YKFKeyFIDO2ClientPinResponse.h; sourceTree = "<group>"; };
+		954E2C562211B53100720D2B /* YKFKeyFIDO2ClientPinResponse.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YKFKeyFIDO2ClientPinResponse.m; sourceTree = "<group>"; };
+		954E2C582211C64900720D2B /* YKFKeyFIDO2VerifyPinRequest.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YKFKeyFIDO2VerifyPinRequest.h; sourceTree = "<group>"; };
+		954E2C592211C64900720D2B /* YKFKeyFIDO2VerifyPinRequest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YKFKeyFIDO2VerifyPinRequest.m; sourceTree = "<group>"; };
+		955188262265E4B9001A4191 /* YKFKeyU2FError.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YKFKeyU2FError.h; sourceTree = "<group>"; };
+		955188272265E4B9001A4191 /* YKFKeyU2FError.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YKFKeyU2FError.m; sourceTree = "<group>"; };
+		9551882A2265E8DE001A4191 /* YKFKeyOATHError.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YKFKeyOATHError.h; sourceTree = "<group>"; };
+		9551882B2265E8DE001A4191 /* YKFKeyOATHError.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YKFKeyOATHError.m; sourceTree = "<group>"; };
+		9551882E2265F4EE001A4191 /* YKFKeyAPDUError.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YKFKeyAPDUError.h; sourceTree = "<group>"; };
+		9551882F2265F4EE001A4191 /* YKFKeyAPDUError.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YKFKeyAPDUError.m; sourceTree = "<group>"; };
+		955BCBF3215A33CA00C2EA2B /* YKFOATHPutAPDU.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YKFOATHPutAPDU.h; sourceTree = "<group>"; };
+		955BCBF4215A33CA00C2EA2B /* YKFOATHPutAPDU.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YKFOATHPutAPDU.m; sourceTree = "<group>"; };
+		955BCBFF215A436100C2EA2B /* YKFKeyOATHPutRequest.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YKFKeyOATHPutRequest.h; sourceTree = "<group>"; };
+		955BCC00215A436100C2EA2B /* YKFKeyOATHPutRequest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YKFKeyOATHPutRequest.m; sourceTree = "<group>"; };
+		955BCC02215A43A000C2EA2B /* YKFKeyOATHRequest.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YKFKeyOATHRequest.h; sourceTree = "<group>"; };
+		955BCC03215A43A000C2EA2B /* YKFKeyOATHRequest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YKFKeyOATHRequest.m; sourceTree = "<group>"; };
+		955BCC05215A43CC00C2EA2B /* YKFKeyOATHRequest+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "YKFKeyOATHRequest+Private.h"; sourceTree = "<group>"; };
+		955BCC08215A463E00C2EA2B /* YKFOATHCredential.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YKFOATHCredential.h; sourceTree = "<group>"; };
+		955BCC09215A463E00C2EA2B /* YKFOATHCredential.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YKFOATHCredential.m; sourceTree = "<group>"; };
+		955BCC0D215A9A4C00C2EA2B /* MF_Base32Additions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MF_Base32Additions.h; sourceTree = "<group>"; };
+		955BCC0E215A9A4C00C2EA2B /* MF_Base32Additions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MF_Base32Additions.m; sourceTree = "<group>"; };
+		955DF94921F8AE3700CED8F1 /* YKFKeyFIDO2GetAssertionResponse+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "YKFKeyFIDO2GetAssertionResponse+Private.h"; sourceTree = "<group>"; };
+		95612622217A1B0D009826BC /* YKFKeyOATHCalculateAllRequest+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "YKFKeyOATHCalculateAllRequest+Private.h"; sourceTree = "<group>"; };
+		9564333120A58EDA007621BD /* YKFOTPURIParserTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YKFOTPURIParserTests.m; sourceTree = "<group>"; };
+		9564333320A5B99F007621BD /* YKFOTPTextParserTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YKFOTPTextParserTests.m; sourceTree = "<group>"; };
+		9564333520A5C03C007621BD /* YKFOTPTokenParserTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YKFOTPTokenParserTests.m; sourceTree = "<group>"; };
+		956884BF20AAD0FB00E0F72C /* YKFNFCOTPService+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "YKFNFCOTPService+Private.h"; sourceTree = "<group>"; };
+		956884C020AAD98500E0F72C /* YKFNFCOTPServiceTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YKFNFCOTPServiceTests.m; sourceTree = "<group>"; };
+		956884C320AADA5A00E0F72C /* FakeNFCNDEFReaderSession.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FakeNFCNDEFReaderSession.h; sourceTree = "<group>"; };
+		956884C420AADA5A00E0F72C /* FakeNFCNDEFReaderSession.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FakeNFCNDEFReaderSession.m; sourceTree = "<group>"; };
+		956884C720AADD7100E0F72C /* NFCNDEFReaderSession+Testing.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NFCNDEFReaderSession+Testing.h"; sourceTree = "<group>"; };
+		956884CA20AAF83300E0F72C /* YubiKitDeviceCapabilities+Testing.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "YubiKitDeviceCapabilities+Testing.h"; sourceTree = "<group>"; };
+		956884CB20AAFB3F00E0F72C /* FakeYubiKitDeviceCapabilities.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FakeYubiKitDeviceCapabilities.h; sourceTree = "<group>"; };
+		956884CC20AAFB3F00E0F72C /* FakeYubiKitDeviceCapabilities.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FakeYubiKitDeviceCapabilities.m; sourceTree = "<group>"; };
+		956884CE20AAFE9300E0F72C /* FakeYKFOTPTextParser.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FakeYKFOTPTextParser.h; sourceTree = "<group>"; };
+		956884CF20AAFE9300E0F72C /* FakeYKFOTPTextParser.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FakeYKFOTPTextParser.m; sourceTree = "<group>"; };
+		956884D120AB012200E0F72C /* FakeYKFOTPURIParser.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FakeYKFOTPURIParser.h; sourceTree = "<group>"; };
+		956884D220AB012200E0F72C /* FakeYKFOTPURIParser.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FakeYKFOTPURIParser.m; sourceTree = "<group>"; };
+		956991F222C224BC00C5EB02 /* YKFWebAuthnClientData.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YKFWebAuthnClientData.h; sourceTree = "<group>"; };
+		956991F322C224BC00C5EB02 /* YKFWebAuthnClientData.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YKFWebAuthnClientData.m; sourceTree = "<group>"; };
+		956DB669206391C5006B1738 /* YKFQRCodeScanError.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YKFQRCodeScanError.h; sourceTree = "<group>"; };
+		956DB66A206391C5006B1738 /* YKFQRCodeScanError.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YKFQRCodeScanError.m; sourceTree = "<group>"; };
+		956DB66D20639C2B006B1738 /* YKFQRCodeScanOverlayView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YKFQRCodeScanOverlayView.h; sourceTree = "<group>"; };
+		956DB66E20639C2B006B1738 /* YKFQRCodeScanOverlayView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YKFQRCodeScanOverlayView.m; sourceTree = "<group>"; };
+		956DB67020639C7C006B1738 /* YKFQRCodeScanViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YKFQRCodeScanViewController.h; sourceTree = "<group>"; };
+		956DB67120639C7D006B1738 /* YKFQRCodeScanViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YKFQRCodeScanViewController.m; sourceTree = "<group>"; };
+		956DB6732063B41E006B1738 /* AudioToolbox.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AudioToolbox.framework; path = System/Library/Frameworks/AudioToolbox.framework; sourceTree = SDKROOT; };
+		956DB6802063EF27006B1738 /* YKFBlockMacros.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YKFBlockMacros.h; sourceTree = "<group>"; };
+		956DB6862068E2DD006B1738 /* YKFQRCodeScanError+Errors.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "YKFQRCodeScanError+Errors.h"; sourceTree = "<group>"; };
+		956DB6872068EE02006B1738 /* YKFOTPURIParserProtocol.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YKFOTPURIParserProtocol.h; sourceTree = "<group>"; };
+		956DB6882068EE64006B1738 /* YKFOTPTextParserProtocol.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YKFOTPTextParserProtocol.h; sourceTree = "<group>"; };
+		956DB691206A89A4006B1738 /* YKFNFCError+Errors.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "YKFNFCError+Errors.h"; sourceTree = "<group>"; };
+		956DBB8221EDEA1D004D6EE3 /* YKFKeyFIDO2Service.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YKFKeyFIDO2Service.h; sourceTree = "<group>"; };
+		956DBB8321EDEA1D004D6EE3 /* YKFKeyFIDO2Service.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YKFKeyFIDO2Service.m; sourceTree = "<group>"; };
+		956DBB8521EDF841004D6EE3 /* YKFKeyFIDO2Service+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "YKFKeyFIDO2Service+Private.h"; sourceTree = "<group>"; };
+		956DBB8721EDFE50004D6EE3 /* YKFSelectFIDO2ApplicationAPDU.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YKFSelectFIDO2ApplicationAPDU.h; sourceTree = "<group>"; };
+		956DBB8821EDFE50004D6EE3 /* YKFSelectFIDO2ApplicationAPDU.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YKFSelectFIDO2ApplicationAPDU.m; sourceTree = "<group>"; };
+		956DBB8B21EE0A0B004D6EE3 /* YKFFIDO2CommandAPDU.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YKFFIDO2CommandAPDU.h; sourceTree = "<group>"; };
+		956DBB8C21EE0A0B004D6EE3 /* YKFFIDO2CommandAPDU.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YKFFIDO2CommandAPDU.m; sourceTree = "<group>"; };
+		956DBB8E21EE1E5E004D6EE3 /* YKFFIDO2GetInfoAPDU.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YKFFIDO2GetInfoAPDU.h; sourceTree = "<group>"; };
+		956DBB8F21EE1E5E004D6EE3 /* YKFFIDO2GetInfoAPDU.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YKFFIDO2GetInfoAPDU.m; sourceTree = "<group>"; };
+		956DBB9221EE2B29004D6EE3 /* YKFFIDO2ResetAPDU.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YKFFIDO2ResetAPDU.h; sourceTree = "<group>"; };
+		956DBB9321EE2B29004D6EE3 /* YKFFIDO2ResetAPDU.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YKFFIDO2ResetAPDU.m; sourceTree = "<group>"; };
+		956DBB9521EE32E5004D6EE3 /* YKFKeyFIDO2Request.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YKFKeyFIDO2Request.h; sourceTree = "<group>"; };
+		956DBB9621EE32E5004D6EE3 /* YKFKeyFIDO2Request.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YKFKeyFIDO2Request.m; sourceTree = "<group>"; };
+		9578A60921F0ADBD00349DCF /* YKFKeyFIDO2GetInfoResponse+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "YKFKeyFIDO2GetInfoResponse+Private.h"; sourceTree = "<group>"; };
+		9578A60B21F1D63000349DCF /* YKFKeyFIDO2MakeCredentialRequest.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YKFKeyFIDO2MakeCredentialRequest.h; sourceTree = "<group>"; };
+		9578A60C21F1D63000349DCF /* YKFKeyFIDO2MakeCredentialRequest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YKFKeyFIDO2MakeCredentialRequest.m; sourceTree = "<group>"; };
+		9578A60E21F1D79400349DCF /* YKFFIDO2Type.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YKFFIDO2Type.h; sourceTree = "<group>"; };
+		9578A60F21F1D79400349DCF /* YKFFIDO2Type.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YKFFIDO2Type.m; sourceTree = "<group>"; };
+		9578A61321F20BA400349DCF /* YKFFIDO2MakeCredentialAPDU.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YKFFIDO2MakeCredentialAPDU.h; sourceTree = "<group>"; };
+		9578A61421F20BA400349DCF /* YKFFIDO2MakeCredentialAPDU.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YKFFIDO2MakeCredentialAPDU.m; sourceTree = "<group>"; };
+		957BDF4D21F5C3A700899B5B /* YKFKeyFIDO2MakeCredentialResponse.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YKFKeyFIDO2MakeCredentialResponse.h; sourceTree = "<group>"; };
+		957BDF4E21F5C3A700899B5B /* YKFKeyFIDO2MakeCredentialResponse.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YKFKeyFIDO2MakeCredentialResponse.m; sourceTree = "<group>"; };
+		957BDF5021F5D4A300899B5B /* YKFKeyFIDO2MakeCredentialResponse+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "YKFKeyFIDO2MakeCredentialResponse+Private.h"; sourceTree = "<group>"; };
+		957BDF5221F7130900899B5B /* YKFKeyFIDO2GetAssertionRequest.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YKFKeyFIDO2GetAssertionRequest.h; sourceTree = "<group>"; };
+		957BDF5321F7130900899B5B /* YKFKeyFIDO2GetAssertionRequest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YKFKeyFIDO2GetAssertionRequest.m; sourceTree = "<group>"; };
+		957D869821B825B4004ABF86 /* YKFKeyRawCommandServiceTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YKFKeyRawCommandServiceTests.m; sourceTree = "<group>"; };
+		9581395221591DE1008558F3 /* YKFSelectOATHApplicationAPDU.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YKFSelectOATHApplicationAPDU.h; sourceTree = "<group>"; };
+		9581395321591DE1008558F3 /* YKFSelectOATHApplicationAPDU.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YKFSelectOATHApplicationAPDU.m; sourceTree = "<group>"; };
+		958139572159286F008558F3 /* YKFKeyOATHService.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YKFKeyOATHService.h; sourceTree = "<group>"; };
+		9581395821592870008558F3 /* YKFKeyOATHService.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YKFKeyOATHService.m; sourceTree = "<group>"; };
+		9581395A215A302B008558F3 /* YKFKeyOATHService+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "YKFKeyOATHService+Private.h"; sourceTree = "<group>"; };
+		958491712130286900D7E2A3 /* YKFAccessorySessionConfiguration.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YKFAccessorySessionConfiguration.h; sourceTree = "<group>"; };
+		958491722130286900D7E2A3 /* YKFAccessorySessionConfiguration.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YKFAccessorySessionConfiguration.m; sourceTree = "<group>"; };
+		958793E5216CE35F001A0406 /* YKFOATHListAPDU.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YKFOATHListAPDU.h; sourceTree = "<group>"; };
+		958793E6216CE35F001A0406 /* YKFOATHListAPDU.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YKFOATHListAPDU.m; sourceTree = "<group>"; };
+		958793E8216CE6B9001A0406 /* YKFKeyOATHListRequest.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YKFKeyOATHListRequest.h; sourceTree = "<group>"; };
+		958793E9216CE6B9001A0406 /* YKFKeyOATHListRequest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YKFKeyOATHListRequest.m; sourceTree = "<group>"; };
+		958793EB216DEF96001A0406 /* YKFOATHCredential+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "YKFOATHCredential+Private.h"; sourceTree = "<group>"; };
+		958793EC216E0DB2001A0406 /* YKFKeyOATHListResponse.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YKFKeyOATHListResponse.h; sourceTree = "<group>"; };
+		958793ED216E0DB2001A0406 /* YKFKeyOATHListResponse.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YKFKeyOATHListResponse.m; sourceTree = "<group>"; };
+		958793EF216E0E53001A0406 /* YKFKeyOATHListResponse+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "YKFKeyOATHListResponse+Private.h"; sourceTree = "<group>"; };
+		9588490522156A5500559024 /* YKFKeyFIDO2MakeCredentialRequest+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "YKFKeyFIDO2MakeCredentialRequest+Private.h"; sourceTree = "<group>"; };
+		95884906221576BE00559024 /* YKFKeyFIDO2GetAssertionRequest+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "YKFKeyFIDO2GetAssertionRequest+Private.h"; sourceTree = "<group>"; };
+		95885B0620A07ED800828D02 /* YKFQRReaderSession.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YKFQRReaderSession.h; sourceTree = "<group>"; };
+		95885B0720A07ED800828D02 /* YKFQRReaderSession.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YKFQRReaderSession.m; sourceTree = "<group>"; };
+		95885B0B20A097A400828D02 /* YKFKeyU2FRequest+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "YKFKeyU2FRequest+Private.h"; sourceTree = "<group>"; };
+		95885B1B20A30DBD00828D02 /* YKFKeySessionError+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "YKFKeySessionError+Private.h"; sourceTree = "<group>"; };
+		95885B2520A3187700828D02 /* YubiKitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = YubiKitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
+		95885B2920A3187700828D02 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
+		95885B3020A31EFF00828D02 /* YKFOTPTokenValidatorTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YKFOTPTokenValidatorTests.m; sourceTree = "<group>"; };
+		958D0B62215D106F00942CB9 /* YKFKeyService.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YKFKeyService.h; sourceTree = "<group>"; };
+		958D0B63215D106F00942CB9 /* YKFKeyService.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YKFKeyService.m; sourceTree = "<group>"; };
+		958D0B65215D10AD00942CB9 /* YKFKeyService+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "YKFKeyService+Private.h"; sourceTree = "<group>"; };
+		95A04D1C2253920B008E3036 /* YKFFIDO2GetNextAssertionAPDU.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YKFFIDO2GetNextAssertionAPDU.h; sourceTree = "<group>"; };
+		95A04D1D2253920B008E3036 /* YKFFIDO2GetNextAssertionAPDU.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YKFFIDO2GetNextAssertionAPDU.m; sourceTree = "<group>"; };
+		95A456672177639C00AD5A94 /* YKFOATHCalculateAllAPDU.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YKFOATHCalculateAllAPDU.h; sourceTree = "<group>"; };
+		95A456682177639C00AD5A94 /* YKFOATHCalculateAllAPDU.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YKFOATHCalculateAllAPDU.m; sourceTree = "<group>"; };
+		95A4566A2177663800AD5A94 /* YKFKeyOATHCalculateAllResponse.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YKFKeyOATHCalculateAllResponse.h; sourceTree = "<group>"; };
+		95A4566B2177663800AD5A94 /* YKFKeyOATHCalculateAllResponse.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YKFKeyOATHCalculateAllResponse.m; sourceTree = "<group>"; };
+		95A4566D217766E300AD5A94 /* YKFKeyOATHCalculateAllResponse+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "YKFKeyOATHCalculateAllResponse+Private.h"; sourceTree = "<group>"; };
+		95A4566F21777D0E00AD5A94 /* YKFKeyOATHCalculateAllRequest.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YKFKeyOATHCalculateAllRequest.h; sourceTree = "<group>"; };
+		95A4567021777D0E00AD5A94 /* YKFKeyOATHCalculateAllRequest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YKFKeyOATHCalculateAllRequest.m; sourceTree = "<group>"; };
+		95A67D7C218729E5008C0B59 /* YKFAPDU+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "YKFAPDU+Private.h"; sourceTree = "<group>"; };
+		95A6C92320ADAC09004F43CC /* YKFAccessorySession+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "YKFAccessorySession+Private.h"; sourceTree = "<group>"; };
+		95B0CAA721EF36D9009C6A34 /* YKFKeyFIDO2Request+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "YKFKeyFIDO2Request+Private.h"; sourceTree = "<group>"; };
+		95B0CAAB21EF53E1009C6A34 /* YKFKeyFIDO2Error.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YKFKeyFIDO2Error.h; sourceTree = "<group>"; };
+		95B0CAAC21EF53E1009C6A34 /* YKFKeyFIDO2Error.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YKFKeyFIDO2Error.m; sourceTree = "<group>"; };
+		95B0CAAE21F098C6009C6A34 /* YKFKeyFIDO2GetInfoResponse.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YKFKeyFIDO2GetInfoResponse.h; sourceTree = "<group>"; };
+		95B0CAAF21F098C6009C6A34 /* YKFKeyFIDO2GetInfoResponse.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YKFKeyFIDO2GetInfoResponse.m; sourceTree = "<group>"; };
+		95B58B89229C03AE00199F8E /* YKFAccessorySession+Debugging.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "YKFAccessorySession+Debugging.h"; sourceTree = "<group>"; };
+		95B58B8A229C03AE00199F8E /* YKFAccessorySession+Debugging.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "YKFAccessorySession+Debugging.m"; sourceTree = "<group>"; };
+		95B67183216F6A6300FA20E6 /* YKFKeyOATHResetRequest.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YKFKeyOATHResetRequest.h; sourceTree = "<group>"; };
+		95B67184216F6A6300FA20E6 /* YKFKeyOATHResetRequest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YKFKeyOATHResetRequest.m; sourceTree = "<group>"; };
+		95B8547B21E628BE000D6D7A /* YKFCBOREncoderTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YKFCBOREncoderTests.m; sourceTree = "<group>"; };
+		95B8547D21E898F3000D6D7A /* YKFCBORDecoderTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YKFCBORDecoderTests.m; sourceTree = "<group>"; };
+		95BA204321F7483100EED927 /* YKFKeyFIDO2GetAssertionResponse.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YKFKeyFIDO2GetAssertionResponse.h; sourceTree = "<group>"; };
+		95BA204421F7483100EED927 /* YKFKeyFIDO2GetAssertionResponse.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YKFKeyFIDO2GetAssertionResponse.m; sourceTree = "<group>"; };
+		95BA204621F877BA00EED927 /* YKFFIDO2TouchPoolingAPDU.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YKFFIDO2TouchPoolingAPDU.h; sourceTree = "<group>"; };
+		95BA204721F877BA00EED927 /* YKFFIDO2TouchPoolingAPDU.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YKFFIDO2TouchPoolingAPDU.m; sourceTree = "<group>"; };
+		95C29611206247210091318B /* libYubiKit.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libYubiKit.a; sourceTree = BUILT_PRODUCTS_DIR; };
+		95C29614206247210091318B /* YubiKit.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YubiKit.h; sourceTree = "<group>"; };
+		95C2961E206247450091318B /* CoreNFC.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreNFC.framework; path = System/Library/Frameworks/CoreNFC.framework; sourceTree = SDKROOT; };
+		95C29621206247920091318B /* YKFOTPToken.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YKFOTPToken.h; sourceTree = "<group>"; };
+		95C29622206247920091318B /* YKFOTPToken.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YKFOTPToken.m; sourceTree = "<group>"; };
+		95C296242062497C0091318B /* YKFOTPTokenValidator.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YKFOTPTokenValidator.h; sourceTree = "<group>"; };
+		95C296252062497C0091318B /* YKFOTPTokenValidator.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YKFOTPTokenValidator.m; sourceTree = "<group>"; };
+		95C29627206250820091318B /* AVFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AVFoundation.framework; path = System/Library/Frameworks/AVFoundation.framework; sourceTree = SDKROOT; };
+		95C2962A206250D90091318B /* YKFPermissions.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YKFPermissions.h; sourceTree = "<group>"; };
+		95C2962B206250D90091318B /* YKFPermissions.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YKFPermissions.m; sourceTree = "<group>"; };
+		95C29630206256120091318B /* YKFLogger.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YKFLogger.h; sourceTree = "<group>"; };
+		95C29631206256120091318B /* YKFLogger.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YKFLogger.m; sourceTree = "<group>"; };
+		95C29633206259050091318B /* UIDeviceAdditions.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = UIDeviceAdditions.h; sourceTree = "<group>"; };
+		95C29634206259050091318B /* UIDeviceAdditions.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = UIDeviceAdditions.m; sourceTree = "<group>"; };
+		95C29636206259660091318B /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; };
+		95C2963820625A180091318B /* YKFView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YKFView.h; sourceTree = "<group>"; };
+		95C2963920625A180091318B /* YKFView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YKFView.m; sourceTree = "<group>"; };
+		95C2963B2062634C0091318B /* YKFViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YKFViewController.h; sourceTree = "<group>"; };
+		95C2963C2062634C0091318B /* YKFViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YKFViewController.m; sourceTree = "<group>"; };
+		95C2963F2062653E0091318B /* YKFOTPTokenParser.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YKFOTPTokenParser.h; sourceTree = "<group>"; };
+		95C296402062653E0091318B /* YKFOTPTokenParser.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YKFOTPTokenParser.m; sourceTree = "<group>"; };
+		95C296422062656C0091318B /* YKFOTPURIParser.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YKFOTPURIParser.h; sourceTree = "<group>"; };
+		95C296432062656C0091318B /* YKFOTPURIParser.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YKFOTPURIParser.m; sourceTree = "<group>"; };
+		95C29645206268C90091318B /* YKFOTPTextParser.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YKFOTPTextParser.h; sourceTree = "<group>"; };
+		95C29646206268C90091318B /* YKFOTPTextParser.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YKFOTPTextParser.m; sourceTree = "<group>"; };
+		95C2964820627F2F0091318B /* YKFNFCError.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YKFNFCError.h; sourceTree = "<group>"; };
+		95C2964920627F2F0091318B /* YKFNFCError.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YKFNFCError.m; sourceTree = "<group>"; };
+		95C2964B20628EBE0091318B /* YKFNFCOTPService.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YKFNFCOTPService.h; sourceTree = "<group>"; };
+		95C2964C20628EBE0091318B /* YKFNFCOTPService.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YKFNFCOTPService.m; sourceTree = "<group>"; };
+		95C2964E206295A00091318B /* YubiKitExternalLocalization.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YubiKitExternalLocalization.h; sourceTree = "<group>"; };
+		95C2964F206295A00091318B /* YubiKitExternalLocalization.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YubiKitExternalLocalization.m; sourceTree = "<group>"; };
+		95C2965220629FD10091318B /* YubiKitDeviceCapabilities.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YubiKitDeviceCapabilities.h; sourceTree = "<group>"; };
+		95C2965320629FD10091318B /* YubiKitDeviceCapabilities.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YubiKitDeviceCapabilities.m; sourceTree = "<group>"; };
+		95C296552062A46A0091318B /* YubiKitConfiguration.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YubiKitConfiguration.h; sourceTree = "<group>"; };
+		95C296562062A46B0091318B /* YubiKitConfiguration.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YubiKitConfiguration.m; sourceTree = "<group>"; };
+		95C296582062A5970091318B /* YubiKitManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YubiKitManager.h; sourceTree = "<group>"; };
+		95C296592062A5970091318B /* YubiKitManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YubiKitManager.m; sourceTree = "<group>"; };
+		95C65A672201A2DE0004A991 /* YKFFIDO2Type+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "YKFFIDO2Type+Private.h"; sourceTree = "<group>"; };
+		95CCAE5D2241220600B567D2 /* YKFKeyFIDO2SetPinRequest.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YKFKeyFIDO2SetPinRequest.h; sourceTree = "<group>"; };
+		95CCAE5E2241220600B567D2 /* YKFKeyFIDO2SetPinRequest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YKFKeyFIDO2SetPinRequest.m; sourceTree = "<group>"; };
+		95CCAE60224122FD00B567D2 /* YKFKeyFIDO2ChangePinRequest.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YKFKeyFIDO2ChangePinRequest.h; sourceTree = "<group>"; };
+		95CCAE61224122FD00B567D2 /* YKFKeyFIDO2ChangePinRequest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YKFKeyFIDO2ChangePinRequest.m; sourceTree = "<group>"; };
+		95D433E7215E662A004463F5 /* YKFNSDataAdditions+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "YKFNSDataAdditions+Private.h"; sourceTree = "<group>"; };
+		95D5E2D7218712BF00AA1C11 /* YKFKeyRawCommandService.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YKFKeyRawCommandService.h; sourceTree = "<group>"; };
+		95D5E2D8218712BF00AA1C11 /* YKFKeyRawCommandService.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YKFKeyRawCommandService.m; sourceTree = "<group>"; };
+		95D5E2DA2187159700AA1C11 /* YKFNSMutableDataAdditions.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YKFNSMutableDataAdditions.h; sourceTree = "<group>"; };
+		95D5E2DB2187159700AA1C11 /* YKFNSMutableDataAdditions.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YKFNSMutableDataAdditions.m; sourceTree = "<group>"; };
+		95D61A03216F9159001E7AC8 /* YKFOATHCredentialValidatorTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YKFOATHCredentialValidatorTests.m; sourceTree = "<group>"; };
+		95D61A052170B26F001E7AC8 /* YKFKeyOATHSelectApplicationResponse.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YKFKeyOATHSelectApplicationResponse.h; sourceTree = "<group>"; };
+		95D61A062170B26F001E7AC8 /* YKFKeyOATHSelectApplicationResponse.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YKFKeyOATHSelectApplicationResponse.m; sourceTree = "<group>"; };
+		95D61A082170DEEE001E7AC8 /* YKFKeyOATHSetCodeRequest.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YKFKeyOATHSetCodeRequest.h; sourceTree = "<group>"; };
+		95D61A092170DEEF001E7AC8 /* YKFKeyOATHSetCodeRequest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YKFKeyOATHSetCodeRequest.m; sourceTree = "<group>"; };
+		95D61A0B2170E0C3001E7AC8 /* YKFOATHSetCodeAPDU.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YKFOATHSetCodeAPDU.h; sourceTree = "<group>"; };
+		95D61A0C2170E0C3001E7AC8 /* YKFOATHSetCodeAPDU.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YKFOATHSetCodeAPDU.m; sourceTree = "<group>"; };
+		95D7364821C9119D0039141A /* YKFPCSCErrorMap.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YKFPCSCErrorMap.h; sourceTree = "<group>"; };
+		95D7364921C9119D0039141A /* YKFPCSCErrorMap.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YKFPCSCErrorMap.m; sourceTree = "<group>"; };
+		95D7364B21C94BFD0039141A /* FakeYKFPCSCLayer.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FakeYKFPCSCLayer.h; sourceTree = "<group>"; };
+		95D7364C21C94BFD0039141A /* FakeYKFPCSCLayer.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FakeYKFPCSCLayer.m; sourceTree = "<group>"; };
+		95D7364E21CA44EF0039141A /* YKFPCSCTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YKFPCSCTests.m; sourceTree = "<group>"; };
+		95D9D3DB21D5110100473888 /* YKFCBOREncoder.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YKFCBOREncoder.h; sourceTree = "<group>"; };
+		95D9D3DC21D5110100473888 /* YKFCBOREncoder.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YKFCBOREncoder.m; sourceTree = "<group>"; };
+		95D9D3DE21D5111500473888 /* YKFCBORDecoder.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YKFCBORDecoder.h; sourceTree = "<group>"; };
+		95D9D3DF21D5111500473888 /* YKFCBORDecoder.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YKFCBORDecoder.m; sourceTree = "<group>"; };
+		95D9D3E121D67AAA00473888 /* YKFCBORType.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YKFCBORType.h; sourceTree = "<group>"; };
+		95D9D3E221D67AAA00473888 /* YKFCBORType.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YKFCBORType.m; sourceTree = "<group>"; };
+		95D9D3E421D6800D00473888 /* YKFCBORTag.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YKFCBORTag.h; sourceTree = "<group>"; };
+		95DD40762099A4EB00363FEE /* YKFNSDataAdditions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YKFNSDataAdditions.h; sourceTree = "<group>"; };
+		95DD40772099A4EB00363FEE /* YKFNSDataAdditions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YKFNSDataAdditions.m; sourceTree = "<group>"; };
+		95DD40792099A64C00363FEE /* YKFDispatch.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YKFDispatch.h; sourceTree = "<group>"; };
+		95DD407A2099A64C00363FEE /* YKFDispatch.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YKFDispatch.m; sourceTree = "<group>"; };
+		95DD407F2099A86900363FEE /* YKFAPDU.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YKFAPDU.h; sourceTree = "<group>"; };
+		95DD40802099A86900363FEE /* YKFU2FSignAPDU.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YKFU2FSignAPDU.m; sourceTree = "<group>"; };
+		95DD40812099A86900363FEE /* YKFSelectU2FApplicationAPDU.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YKFSelectU2FApplicationAPDU.m; sourceTree = "<group>"; };
+		95DD40822099A86900363FEE /* YKFAPDU.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YKFAPDU.m; sourceTree = "<group>"; };
+		95DD40832099A86900363FEE /* YKFU2FRegisterAPDU.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YKFU2FRegisterAPDU.h; sourceTree = "<group>"; };
+		95DD40842099A86900363FEE /* YKFU2FSignAPDU.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YKFU2FSignAPDU.h; sourceTree = "<group>"; };
+		95DD40852099A86900363FEE /* YKFU2FRegisterAPDU.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YKFU2FRegisterAPDU.m; sourceTree = "<group>"; };
+		95DD40862099A86900363FEE /* YKFSelectU2FApplicationAPDU.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YKFSelectU2FApplicationAPDU.h; sourceTree = "<group>"; };
+		95DD408C2099A88400363FEE /* YKFKeySessionError.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YKFKeySessionError.h; sourceTree = "<group>"; };
+		95DD408D2099A88400363FEE /* YKFKeySessionError.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YKFKeySessionError.m; sourceTree = "<group>"; };
+		95DD40902099A89500363FEE /* YKFKeyU2FRegisterRequest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YKFKeyU2FRegisterRequest.h; sourceTree = "<group>"; };
+		95DD40912099A89500363FEE /* YKFKeyU2FRegisterRequest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YKFKeyU2FRegisterRequest.m; sourceTree = "<group>"; };
+		95DD40922099A89600363FEE /* YKFKeyU2FRequest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YKFKeyU2FRequest.m; sourceTree = "<group>"; };
+		95DD40932099A89600363FEE /* YKFKeyU2FSignRequest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YKFKeyU2FSignRequest.h; sourceTree = "<group>"; };
+		95DD40942099A89600363FEE /* YKFKeyU2FSignResponse.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YKFKeyU2FSignResponse.h; sourceTree = "<group>"; };
+		95DD40952099A89600363FEE /* YKFKeyU2FRequest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YKFKeyU2FRequest.h; sourceTree = "<group>"; };
+		95DD40962099A89600363FEE /* YKFKeyU2FSignResponse.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YKFKeyU2FSignResponse.m; sourceTree = "<group>"; };
+		95DD40972099A89600363FEE /* YKFKeyU2FRegisterResponse.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YKFKeyU2FRegisterResponse.m; sourceTree = "<group>"; };
+		95DD40982099A89600363FEE /* YKFKeyU2FRegisterResponse.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YKFKeyU2FRegisterResponse.h; sourceTree = "<group>"; };
+		95DD40992099A89600363FEE /* YKFKeyU2FSignRequest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YKFKeyU2FSignRequest.m; sourceTree = "<group>"; };
+		95DD409F2099A8A300363FEE /* YKFAccessoryConnectionController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YKFAccessoryConnectionController.h; sourceTree = "<group>"; };
+		95DD40A02099A8A300363FEE /* YKFAccessorySession.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YKFAccessorySession.h; sourceTree = "<group>"; };
+		95DD40A12099A8A300363FEE /* YKFKeyU2FService.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YKFKeyU2FService.m; sourceTree = "<group>"; };
+		95DD40A22099A8A300363FEE /* YKFKeyU2FService.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YKFKeyU2FService.h; sourceTree = "<group>"; };
+		95DD40A32099A8A300363FEE /* YKFAccessorySession.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YKFAccessorySession.m; sourceTree = "<group>"; };
+		95DD40A42099A8A400363FEE /* YKFKeyU2FService+Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "YKFKeyU2FService+Private.h"; sourceTree = "<group>"; };
+		95DD40A52099A8A400363FEE /* YKFAccessoryConnectionController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YKFAccessoryConnectionController.m; sourceTree = "<group>"; };
+		95DD6583216635CA00BA85C9 /* YKFOATHCredentialValidator.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YKFOATHCredentialValidator.h; sourceTree = "<group>"; };
+		95DD6584216635CA00BA85C9 /* YKFOATHCredentialValidator.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YKFOATHCredentialValidator.m; sourceTree = "<group>"; };
+		95DD658921663B0400BA85C9 /* YKFKeyOATHDeleteRequest.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YKFKeyOATHDeleteRequest.h; sourceTree = "<group>"; };
+		95DD658A21663B0400BA85C9 /* YKFKeyOATHDeleteRequest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YKFKeyOATHDeleteRequest.m; sourceTree = "<group>"; };
+		95DD658C21663C3C00BA85C9 /* YKFOATHDeleteAPDU.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YKFOATHDeleteAPDU.h; sourceTree = "<group>"; };
+		95DD658D21663C3C00BA85C9 /* YKFOATHDeleteAPDU.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YKFOATHDeleteAPDU.m; sourceTree = "<group>"; };
+		95DD659021664B6800BA85C9 /* YKFOATHCredentialTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YKFOATHCredentialTests.m; sourceTree = "<group>"; };
+		95DF11902317C60600CF0C39 /* YKFNFCConnectionController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YKFNFCConnectionController.h; sourceTree = "<group>"; };
+		95DF11912317C60600CF0C39 /* YKFNFCConnectionController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YKFNFCConnectionController.m; sourceTree = "<group>"; };
+		95E152A52315257600C4B7E7 /* YKFNFCSession.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YKFNFCSession.h; sourceTree = "<group>"; };
+		95E152A62315257600C4B7E7 /* YKFNFCSession.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YKFNFCSession.m; sourceTree = "<group>"; };
+		95E1B257219EE2D300E349E3 /* YKFKVOObservation.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YKFKVOObservation.h; sourceTree = "<group>"; };
+		95E1B258219EE2D300E349E3 /* YKFKVOObservation.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YKFKVOObservation.m; sourceTree = "<group>"; };
+		95E3934220C137770027E7B4 /* YKFURIIdentifierCode.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YKFURIIdentifierCode.h; sourceTree = "<group>"; };
+		95E3934320C137770027E7B4 /* YKFURIIdentifierCode.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YKFURIIdentifierCode.m; sourceTree = "<group>"; };
+		95EA81E52178805D0020595D /* YKFNSStringAdditions.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YKFNSStringAdditions.h; sourceTree = "<group>"; };
+		95EA81E62178805D0020595D /* YKFNSStringAdditions.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YKFNSStringAdditions.m; sourceTree = "<group>"; };
+		95EEEF6421676B7F00BE7D7B /* YKFOATHSendRemainingAPDU.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YKFOATHSendRemainingAPDU.h; sourceTree = "<group>"; };
+		95EEEF6521676B7F00BE7D7B /* YKFOATHSendRemainingAPDU.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YKFOATHSendRemainingAPDU.m; sourceTree = "<group>"; };
+		95EEEF672167A93900BE7D7B /* YKFKeyOATHCalculateRequest.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YKFKeyOATHCalculateRequest.h; sourceTree = "<group>"; };
+		95EEEF682167A93900BE7D7B /* YKFKeyOATHCalculateRequest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YKFKeyOATHCalculateRequest.m; sourceTree = "<group>"; };
+		95EEEF6A2167AB6A00BE7D7B /* YKFOATHCalculateAPDU.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YKFOATHCalculateAPDU.h; sourceTree = "<group>"; };
+		95EEEF6B2167AB6A00BE7D7B /* YKFOATHCalculateAPDU.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YKFOATHCalculateAPDU.m; sourceTree = "<group>"; };
+		95EF75BD213FE9F200059C79 /* YKFAccessoryConnectionControllerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YKFAccessoryConnectionControllerTests.m; sourceTree = "<group>"; };
+		95EF75C1213FEF0500059C79 /* YKFTestCase.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YKFTestCase.h; sourceTree = "<group>"; };
+		95EF75C2213FEF0500059C79 /* YKFTestCase.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YKFTestCase.m; sourceTree = "<group>"; };
+		95F75BA92175D63D00C13DC5 /* YKFKeyOATHValidateRequest.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YKFKeyOATHValidateRequest.h; sourceTree = "<group>"; };
+		95F75BAA2175D63D00C13DC5 /* YKFKeyOATHValidateRequest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YKFKeyOATHValidateRequest.m; sourceTree = "<group>"; };
+		95F75BAC2175D6D600C13DC5 /* YKFOATHValidateAPDU.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YKFOATHValidateAPDU.h; sourceTree = "<group>"; };
+		95F75BAD2175D6D600C13DC5 /* YKFOATHValidateAPDU.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YKFOATHValidateAPDU.m; sourceTree = "<group>"; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+		95885B2220A3187700828D02 /* Frameworks */ = {
+			isa = PBXFrameworksBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				95885B2A20A3187700828D02 /* libYubiKit.a in Frameworks */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+		95C2960E206247210091318B /* Frameworks */ = {
+			isa = PBXFrameworksBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				956DB6742063B41E006B1738 /* AudioToolbox.framework in Frameworks */,
+				95C29637206259660091318B /* UIKit.framework in Frameworks */,
+				95C29628206250820091318B /* AVFoundation.framework in Frameworks */,
+				95C2961F206247450091318B /* CoreNFC.framework in Frameworks */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+		951446A6218858F0002BB3C5 /* Layers */ = {
+			isa = PBXGroup;
+			children = (
+				951446B121889A21002BB3C5 /* PCSC */,
+			);
+			path = Layers;
+			sourceTree = "<group>";
+		};
+		951446B121889A21002BB3C5 /* PCSC */ = {
+			isa = PBXGroup;
+			children = (
+				951446AB21885962002BB3C5 /* YKFPCSC.h */,
+				951446AC21885962002BB3C5 /* YKFPCSC.m */,
+				95D7364821C9119D0039141A /* YKFPCSCErrorMap.h */,
+				95D7364921C9119D0039141A /* YKFPCSCErrorMap.m */,
+				951446AE21886966002BB3C5 /* YKFPCSCErrors.h */,
+				951446A82188592C002BB3C5 /* YKFPCSCLayer.h */,
+				951446A92188592C002BB3C5 /* YKFPCSCLayer.m */,
+				951446AF218876D9002BB3C5 /* YKFPCSCTypes.h */,
+			);
+			path = PCSC;
+			sourceTree = "<group>";
+		};
+		952CB9E9220D82C4004A7624 /* Crypto */ = {
+			isa = PBXGroup;
+			children = (
+				952CB9EA220D835F004A7624 /* YKFFIDO2PinAuthKey.h */,
+				952CB9EB220D835F004A7624 /* YKFFIDO2PinAuthKey.m */,
+			);
+			path = Crypto;
+			sourceTree = "<group>";
+		};
+		953306872088CB8400A625C8 /* Additions */ = {
+			isa = PBXGroup;
+			children = (
+				95C29633206259050091318B /* UIDeviceAdditions.h */,
+				95C29634206259050091318B /* UIDeviceAdditions.m */,
+				953306882088CB9F00A625C8 /* UIWindowAdditions.h */,
+				953306892088CB9F00A625C8 /* UIWindowAdditions.m */,
+				95DD40762099A4EB00363FEE /* YKFNSDataAdditions.h */,
+				95DD40772099A4EB00363FEE /* YKFNSDataAdditions.m */,
+				95D433E7215E662A004463F5 /* YKFNSDataAdditions+Private.h */,
+				95D5E2DA2187159700AA1C11 /* YKFNSMutableDataAdditions.h */,
+				95D5E2DB2187159700AA1C11 /* YKFNSMutableDataAdditions.m */,
+				95EA81E52178805D0020595D /* YKFNSStringAdditions.h */,
+				95EA81E62178805D0020595D /* YKFNSStringAdditions.m */,
+			);
+			path = Additions;
+			sourceTree = "<group>";
+		};
+		953A507E213EE92F00929ABB /* TestExtentions */ = {
+			isa = PBXGroup;
+			children = (
+				956884C720AADD7100E0F72C /* NFCNDEFReaderSession+Testing.h */,
+				953A5080213FC77F00929ABB /* EASession+Testing.h */,
+				953A5086213FD0E600929ABB /* EAAccessory+Testing.h */,
+				9513760921512824006C843F /* EAAccessoryManager+Testing.h */,
+				950C70072298051700E48458 /* UIDevice+Testing.h */,
+			);
+			path = TestExtentions;
+			sourceTree = "<group>";
+		};
+		953A507F213EECED00929ABB /* Sessions */ = {
+			isa = PBXGroup;
+			children = (
+				95DD407D2099A84700363FEE /* AccessorySession */,
+				95C2963E206264FB0091318B /* NFCSession */,
+				956DB6682063919D006B1738 /* QRReaderSession */,
+				95A2AD77230EA12500A4A568 /* Shared */,
+			);
+			path = Sessions;
+			sourceTree = "<group>";
+		};
+		955BCBFD215A35C900C2EA2B /* U2F */ = {
+			isa = PBXGroup;
+			children = (
+				95DD40952099A89600363FEE /* YKFKeyU2FRequest.h */,
+				95DD40922099A89600363FEE /* YKFKeyU2FRequest.m */,
+				95885B0B20A097A400828D02 /* YKFKeyU2FRequest+Private.h */,
+				95DD40902099A89500363FEE /* YKFKeyU2FRegisterRequest.h */,
+				95DD40912099A89500363FEE /* YKFKeyU2FRegisterRequest.m */,
+				95DD40982099A89600363FEE /* YKFKeyU2FRegisterResponse.h */,
+				95DD40972099A89600363FEE /* YKFKeyU2FRegisterResponse.m */,
+				954C600A21246253003A8497 /* YKFKeyU2FRegisterResponse+Private.h */,
+				95DD40932099A89600363FEE /* YKFKeyU2FSignRequest.h */,
+				95DD40992099A89600363FEE /* YKFKeyU2FSignRequest.m */,
+				95DD40942099A89600363FEE /* YKFKeyU2FSignResponse.h */,
+				95DD40962099A89600363FEE /* YKFKeyU2FSignResponse.m */,
+				954C601021247345003A8497 /* YKFKeyU2FSignResponse+Private.h */,
+			);
+			path = U2F;
+			sourceTree = "<group>";
+		};
+		955BCBFE215A431200C2EA2B /* OATH */ = {
+			isa = PBXGroup;
+			children = (
+				955BCC02215A43A000C2EA2B /* YKFKeyOATHRequest.h */,
+				955BCC03215A43A000C2EA2B /* YKFKeyOATHRequest.m */,
+				955BCC05215A43CC00C2EA2B /* YKFKeyOATHRequest+Private.h */,
+				95A4566F21777D0E00AD5A94 /* YKFKeyOATHCalculateAllRequest.h */,
+				95A4567021777D0E00AD5A94 /* YKFKeyOATHCalculateAllRequest.m */,
+				95612622217A1B0D009826BC /* YKFKeyOATHCalculateAllRequest+Private.h */,
+				95A4566A2177663800AD5A94 /* YKFKeyOATHCalculateAllResponse.h */,
+				95A4566B2177663800AD5A94 /* YKFKeyOATHCalculateAllResponse.m */,
+				95A4566D217766E300AD5A94 /* YKFKeyOATHCalculateAllResponse+Private.h */,
+				95EEEF672167A93900BE7D7B /* YKFKeyOATHCalculateRequest.h */,
+				95EEEF682167A93900BE7D7B /* YKFKeyOATHCalculateRequest.m */,
+				950FB9302179D8F6003F712E /* YKFKeyOATHCalculateRequest+Private.h */,
+				9547C9DB216B59E2001E1F4A /* YKFKeyOATHCalculateResponse.h */,
+				9547C9DC216B59E2001E1F4A /* YKFKeyOATHCalculateResponse.m */,
+				9547C9DE216B5AA6001E1F4A /* YKFKeyOATHCalculateResponse+Private.h */,
+				95DD658921663B0400BA85C9 /* YKFKeyOATHDeleteRequest.h */,
+				95DD658A21663B0400BA85C9 /* YKFKeyOATHDeleteRequest.m */,
+				958793E8216CE6B9001A0406 /* YKFKeyOATHListRequest.h */,
+				958793E9216CE6B9001A0406 /* YKFKeyOATHListRequest.m */,
+				958793EC216E0DB2001A0406 /* YKFKeyOATHListResponse.h */,
+				958793ED216E0DB2001A0406 /* YKFKeyOATHListResponse.m */,
+				958793EF216E0E53001A0406 /* YKFKeyOATHListResponse+Private.h */,
+				955BCBFF215A436100C2EA2B /* YKFKeyOATHPutRequest.h */,
+				955BCC00215A436100C2EA2B /* YKFKeyOATHPutRequest.m */,
+				95B67183216F6A6300FA20E6 /* YKFKeyOATHResetRequest.h */,
+				95B67184216F6A6300FA20E6 /* YKFKeyOATHResetRequest.m */,
+				95D61A052170B26F001E7AC8 /* YKFKeyOATHSelectApplicationResponse.h */,
+				95D61A062170B26F001E7AC8 /* YKFKeyOATHSelectApplicationResponse.m */,
+				95D61A082170DEEE001E7AC8 /* YKFKeyOATHSetCodeRequest.h */,
+				95D61A092170DEEF001E7AC8 /* YKFKeyOATHSetCodeRequest.m */,
+				95F75BA92175D63D00C13DC5 /* YKFKeyOATHValidateRequest.h */,
+				95F75BAA2175D63D00C13DC5 /* YKFKeyOATHValidateRequest.m */,
+				9535F0102175FFB600A6D617 /* YKFKeyOATHValidateResponse.h */,
+				9535F0112175FFB600A6D617 /* YKFKeyOATHValidateResponse.m */,
+			);
+			path = OATH;
+			sourceTree = "<group>";
+		};
+		955BCC0B215A999400C2EA2B /* ThirdParties */ = {
+			isa = PBXGroup;
+			children = (
+				955BCC0D215A9A4C00C2EA2B /* MF_Base32Additions.h */,
+				955BCC0E215A9A4C00C2EA2B /* MF_Base32Additions.m */,
+			);
+			path = ThirdParties;
+			sourceTree = "<group>";
+		};
+		956884C220AADA1800E0F72C /* Fakes */ = {
+			isa = PBXGroup;
+			children = (
+				9529CBBB214905770041D2F8 /* FakeEAAccessory.h */,
+				9529CBBC214905770041D2F8 /* FakeEAAccessory.m */,
+				953A5083213FCDA100929ABB /* FakeEASession.h */,
+				953A5084213FCDA100929ABB /* FakeEASession.m */,
+				956884C320AADA5A00E0F72C /* FakeNFCNDEFReaderSession.h */,
+				956884C420AADA5A00E0F72C /* FakeNFCNDEFReaderSession.m */,
+				950C700A22980CFE00E48458 /* FakeUIDevice.h */,
+				950C700B22980CFE00E48458 /* FakeUIDevice.m */,
+				9529CBC221492A3E0041D2F8 /* FakeYKFKeyConnectionController.h */,
+				9529CBC321492A3E0041D2F8 /* FakeYKFKeyConnectionController.m */,
+				956884CE20AAFE9300E0F72C /* FakeYKFOTPTextParser.h */,
+				956884CF20AAFE9300E0F72C /* FakeYKFOTPTextParser.m */,
+				956884D120AB012200E0F72C /* FakeYKFOTPURIParser.h */,
+				956884D220AB012200E0F72C /* FakeYKFOTPURIParser.m */,
+				95D7364B21C94BFD0039141A /* FakeYKFPCSCLayer.h */,
+				95D7364C21C94BFD0039141A /* FakeYKFPCSCLayer.m */,
+				956884CB20AAFB3F00E0F72C /* FakeYubiKitDeviceCapabilities.h */,
+				956884CC20AAFB3F00E0F72C /* FakeYubiKitDeviceCapabilities.m */,
+			);
+			path = Fakes;
+			sourceTree = "<group>";
+		};
+		956884C620AADA7900E0F72C /* Tests */ = {
+			isa = PBXGroup;
+			children = (
+				95B8547D21E898F3000D6D7A /* YKFCBORDecoderTests.m */,
+				95B8547B21E628BE000D6D7A /* YKFCBOREncoderTests.m */,
+				95EF75BD213FE9F200059C79 /* YKFAccessoryConnectionControllerTests.m */,
+				9529CBBE2149105F0041D2F8 /* YKFAccessoryDescriptionTests.m */,
+				957D869821B825B4004ABF86 /* YKFKeyRawCommandServiceTests.m */,
+				9529CBB9214903FA0041D2F8 /* YKFAccessorySessionConfigurationTests.m */,
+				9529CBC0214927D80041D2F8 /* YKFKeyU2FServiceTests.m */,
+				956884C020AAD98500E0F72C /* YKFNFCOTPServiceTests.m */,
+				95DD659021664B6800BA85C9 /* YKFOATHCredentialTests.m */,
+				95D61A03216F9159001E7AC8 /* YKFOATHCredentialValidatorTests.m */,
+				9564333320A5B99F007621BD /* YKFOTPTextParserTests.m */,
+				9564333520A5C03C007621BD /* YKFOTPTokenParserTests.m */,
+				95885B3020A31EFF00828D02 /* YKFOTPTokenValidatorTests.m */,
+				9564333120A58EDA007621BD /* YKFOTPURIParserTests.m */,
+				95D7364E21CA44EF0039141A /* YKFPCSCTests.m */,
+				95EF75C1213FEF0500059C79 /* YKFTestCase.h */,
+				95EF75C2213FEF0500059C79 /* YKFTestCase.m */,
+				950C70082298095F00E48458 /* YubiKitDeviceCapabilitiesTests.m */,
+			);
+			path = Tests;
+			sourceTree = "<group>";
+		};
+		956991F122C2248F00C5EB02 /* WebAuthN */ = {
+			isa = PBXGroup;
+			children = (
+				956991F222C224BC00C5EB02 /* YKFWebAuthnClientData.h */,
+				956991F322C224BC00C5EB02 /* YKFWebAuthnClientData.m */,
+			);
+			path = WebAuthN;
+			sourceTree = "<group>";
+		};
+		956DB6682063919D006B1738 /* QRReaderSession */ = {
+			isa = PBXGroup;
+			children = (
+				956DB6862068E2DD006B1738 /* YKFQRCodeScanError+Errors.h */,
+				956DB669206391C5006B1738 /* YKFQRCodeScanError.h */,
+				956DB66A206391C5006B1738 /* YKFQRCodeScanError.m */,
+				956DB67020639C7C006B1738 /* YKFQRCodeScanViewController.h */,
+				956DB67120639C7D006B1738 /* YKFQRCodeScanViewController.m */,
+				95885B0620A07ED800828D02 /* YKFQRReaderSession.h */,
+				95885B0720A07ED800828D02 /* YKFQRReaderSession.m */,
+				956DB66C20639C0F006B1738 /* Views */,
+			);
+			path = QRReaderSession;
+			sourceTree = "<group>";
+		};
+		956DB66C20639C0F006B1738 /* Views */ = {
+			isa = PBXGroup;
+			children = (
+				956DB66D20639C2B006B1738 /* YKFQRCodeScanOverlayView.h */,
+				956DB66E20639C2B006B1738 /* YKFQRCodeScanOverlayView.m */,
+			);
+			path = Views;
+			sourceTree = "<group>";
+		};
+		956DBB8621EDFE19004D6EE3 /* FIDO2 */ = {
+			isa = PBXGroup;
+			children = (
+				956DBB8721EDFE50004D6EE3 /* YKFSelectFIDO2ApplicationAPDU.h */,
+				956DBB8821EDFE50004D6EE3 /* YKFSelectFIDO2ApplicationAPDU.m */,
+				95BA204621F877BA00EED927 /* YKFFIDO2TouchPoolingAPDU.h */,
+				95BA204721F877BA00EED927 /* YKFFIDO2TouchPoolingAPDU.m */,
+				956DBB8B21EE0A0B004D6EE3 /* YKFFIDO2CommandAPDU.h */,
+				956DBB8C21EE0A0B004D6EE3 /* YKFFIDO2CommandAPDU.m */,
+				956DBB8E21EE1E5E004D6EE3 /* YKFFIDO2GetInfoAPDU.h */,
+				956DBB8F21EE1E5E004D6EE3 /* YKFFIDO2GetInfoAPDU.m */,
+				956DBB9221EE2B29004D6EE3 /* YKFFIDO2ResetAPDU.h */,
+				956DBB9321EE2B29004D6EE3 /* YKFFIDO2ResetAPDU.m */,
+				9578A61321F20BA400349DCF /* YKFFIDO2MakeCredentialAPDU.h */,
+				9578A61421F20BA400349DCF /* YKFFIDO2MakeCredentialAPDU.m */,
+				953A6FC021F733D8003B2477 /* YKFFIDO2GetAssertionAPDU.h */,
+				953A6FC121F733D8003B2477 /* YKFFIDO2GetAssertionAPDU.m */,
+				95A04D1C2253920B008E3036 /* YKFFIDO2GetNextAssertionAPDU.h */,
+				95A04D1D2253920B008E3036 /* YKFFIDO2GetNextAssertionAPDU.m */,
+				954E2C522211AA5600720D2B /* YKFFIDO2ClientPinAPDU.h */,
+				954E2C532211AA5600720D2B /* YKFFIDO2ClientPinAPDU.m */,
+			);
+			path = FIDO2;
+			sourceTree = "<group>";
+		};
+		956DBB8A21EE00EC004D6EE3 /* FIDO2 */ = {
+			isa = PBXGroup;
+			children = (
+				9578A60E21F1D79400349DCF /* YKFFIDO2Type.h */,
+				9578A60F21F1D79400349DCF /* YKFFIDO2Type.m */,
+				95C65A672201A2DE0004A991 /* YKFFIDO2Type+Private.h */,
+				956DBB9521EE32E5004D6EE3 /* YKFKeyFIDO2Request.h */,
+				956DBB9621EE32E5004D6EE3 /* YKFKeyFIDO2Request.m */,
+				95B0CAA721EF36D9009C6A34 /* YKFKeyFIDO2Request+Private.h */,
+				95B0CAAE21F098C6009C6A34 /* YKFKeyFIDO2GetInfoResponse.h */,
+				95B0CAAF21F098C6009C6A34 /* YKFKeyFIDO2GetInfoResponse.m */,
+				9578A60921F0ADBD00349DCF /* YKFKeyFIDO2GetInfoResponse+Private.h */,
+				9578A60B21F1D63000349DCF /* YKFKeyFIDO2MakeCredentialRequest.h */,
+				9578A60C21F1D63000349DCF /* YKFKeyFIDO2MakeCredentialRequest.m */,
+				9588490522156A5500559024 /* YKFKeyFIDO2MakeCredentialRequest+Private.h */,
+				957BDF4D21F5C3A700899B5B /* YKFKeyFIDO2MakeCredentialResponse.h */,
+				957BDF4E21F5C3A700899B5B /* YKFKeyFIDO2MakeCredentialResponse.m */,
+				957BDF5021F5D4A300899B5B /* YKFKeyFIDO2MakeCredentialResponse+Private.h */,
+				957BDF5221F7130900899B5B /* YKFKeyFIDO2GetAssertionRequest.h */,
+				957BDF5321F7130900899B5B /* YKFKeyFIDO2GetAssertionRequest.m */,
+				95884906221576BE00559024 /* YKFKeyFIDO2GetAssertionRequest+Private.h */,
+				95BA204321F7483100EED927 /* YKFKeyFIDO2GetAssertionResponse.h */,
+				95BA204421F7483100EED927 /* YKFKeyFIDO2GetAssertionResponse.m */,
+				955DF94921F8AE3700CED8F1 /* YKFKeyFIDO2GetAssertionResponse+Private.h */,
+				954E2C4F2211A34900720D2B /* YKFKeyFIDO2ClientPinRequest.h */,
+				954E2C502211A34900720D2B /* YKFKeyFIDO2ClientPinRequest.m */,
+				954E2C552211B53100720D2B /* YKFKeyFIDO2ClientPinResponse.h */,
+				954E2C562211B53100720D2B /* YKFKeyFIDO2ClientPinResponse.m */,
+				954E2C582211C64900720D2B /* YKFKeyFIDO2VerifyPinRequest.h */,
+				954E2C592211C64900720D2B /* YKFKeyFIDO2VerifyPinRequest.m */,
+				95CCAE5D2241220600B567D2 /* YKFKeyFIDO2SetPinRequest.h */,
+				95CCAE5E2241220600B567D2 /* YKFKeyFIDO2SetPinRequest.m */,
+				95CCAE60224122FD00B567D2 /* YKFKeyFIDO2ChangePinRequest.h */,
+				95CCAE61224122FD00B567D2 /* YKFKeyFIDO2ChangePinRequest.m */,
+			);
+			path = FIDO2;
+			sourceTree = "<group>";
+		};
+		9581394E21590652008558F3 /* Services */ = {
+			isa = PBXGroup;
+			children = (
+				958D0B62215D106F00942CB9 /* YKFKeyService.h */,
+				958D0B63215D106F00942CB9 /* YKFKeyService.m */,
+				958D0B65215D10AD00942CB9 /* YKFKeyService+Private.h */,
+				95D9D3D921D510A700473888 /* FIDO2 */,
+				958139562159281D008558F3 /* OATH */,
+				95D5E2D6218711CB00AA1C11 /* RawCommand */,
+				958139552159280B008558F3 /* U2F */,
+			);
+			path = Services;
+			sourceTree = "<group>";
+		};
+		9581395021591D32008558F3 /* U2F */ = {
+			isa = PBXGroup;
+			children = (
+				95DD40862099A86900363FEE /* YKFSelectU2FApplicationAPDU.h */,
+				95DD40812099A86900363FEE /* YKFSelectU2FApplicationAPDU.m */,
+				95DD40832099A86900363FEE /* YKFU2FRegisterAPDU.h */,
+				95DD40852099A86900363FEE /* YKFU2FRegisterAPDU.m */,
+				95DD40842099A86900363FEE /* YKFU2FSignAPDU.h */,
+				95DD40802099A86900363FEE /* YKFU2FSignAPDU.m */,
+			);
+			path = U2F;
+			sourceTree = "<group>";
+		};
+		9581395121591D94008558F3 /* OATH */ = {
+			isa = PBXGroup;
+			children = (
+				9581395221591DE1008558F3 /* YKFSelectOATHApplicationAPDU.h */,
+				9581395321591DE1008558F3 /* YKFSelectOATHApplicationAPDU.m */,
+				95A456672177639C00AD5A94 /* YKFOATHCalculateAllAPDU.h */,
+				95A456682177639C00AD5A94 /* YKFOATHCalculateAllAPDU.m */,
+				95EEEF6A2167AB6A00BE7D7B /* YKFOATHCalculateAPDU.h */,
+				95EEEF6B2167AB6A00BE7D7B /* YKFOATHCalculateAPDU.m */,
+				95DD658C21663C3C00BA85C9 /* YKFOATHDeleteAPDU.h */,
+				95DD658D21663C3C00BA85C9 /* YKFOATHDeleteAPDU.m */,
+				958793E5216CE35F001A0406 /* YKFOATHListAPDU.h */,
+				958793E6216CE35F001A0406 /* YKFOATHListAPDU.m */,
+				955BCBF3215A33CA00C2EA2B /* YKFOATHPutAPDU.h */,
+				955BCBF4215A33CA00C2EA2B /* YKFOATHPutAPDU.m */,
+				952564C6216F699500BC6F64 /* YKFOATHResetAPDU.h */,
+				952564C7216F699500BC6F64 /* YKFOATHResetAPDU.m */,
+				95EEEF6421676B7F00BE7D7B /* YKFOATHSendRemainingAPDU.h */,
+				95EEEF6521676B7F00BE7D7B /* YKFOATHSendRemainingAPDU.m */,
+				95D61A0B2170E0C3001E7AC8 /* YKFOATHSetCodeAPDU.h */,
+				95D61A0C2170E0C3001E7AC8 /* YKFOATHSetCodeAPDU.m */,
+				95F75BAC2175D6D600C13DC5 /* YKFOATHValidateAPDU.h */,
+				95F75BAD2175D6D600C13DC5 /* YKFOATHValidateAPDU.m */,
+			);
+			path = OATH;
+			sourceTree = "<group>";
+		};
+		958139552159280B008558F3 /* U2F */ = {
+			isa = PBXGroup;
+			children = (
+				95DD40A22099A8A300363FEE /* YKFKeyU2FService.h */,
+				95DD40A12099A8A300363FEE /* YKFKeyU2FService.m */,
+				95DD40A42099A8A400363FEE /* YKFKeyU2FService+Private.h */,
+			);
+			path = U2F;
+			sourceTree = "<group>";
+		};
+		958139562159281D008558F3 /* OATH */ = {
+			isa = PBXGroup;
+			children = (
+				958139572159286F008558F3 /* YKFKeyOATHService.h */,
+				9581395821592870008558F3 /* YKFKeyOATHService.m */,
+				9581395A215A302B008558F3 /* YKFKeyOATHService+Private.h */,
+				955BCC08215A463E00C2EA2B /* YKFOATHCredential.h */,
+				955BCC09215A463E00C2EA2B /* YKFOATHCredential.m */,
+				958793EB216DEF96001A0406 /* YKFOATHCredential+Private.h */,
+				95DD6583216635CA00BA85C9 /* YKFOATHCredentialValidator.h */,
+				95DD6584216635CA00BA85C9 /* YKFOATHCredentialValidator.m */,
+			);
+			path = OATH;
+			sourceTree = "<group>";
+		};
+		95885B2620A3187700828D02 /* YubiKitTests */ = {
+			isa = PBXGroup;
+			children = (
+				95885B2920A3187700828D02 /* Info.plist */,
+				956884C620AADA7900E0F72C /* Tests */,
+				956884C220AADA1800E0F72C /* Fakes */,
+			);
+			path = YubiKitTests;
+			sourceTree = "<group>";
+		};
+		95A2AD77230EA12500A4A568 /* Shared */ = {
+			isa = PBXGroup;
+			children = (
+				951446A221874CC2002BB3C5 /* YKFKeyCommandConfiguration.h */,
+				951446A321874CC2002BB3C5 /* YKFKeyCommandConfiguration.m */,
+				951EA6F92315B95500E35C8C /* YKFKeyConnectionControllerProtocol.h */,
+				95DD407E2099A85700363FEE /* APDU */,
+				95DD408B2099A87600363FEE /* Errors */,
+				95DD408F2099A88A00363FEE /* Requests */,
+				9581394E21590652008558F3 /* Services */,
+			);
+			path = Shared;
+			sourceTree = "<group>";
+		};
+		95C29608206247210091318B = {
+			isa = PBXGroup;
+			children = (
+				95C29613206247210091318B /* YubiKit */,
+				95885B2620A3187700828D02 /* YubiKitTests */,
+				95C29612206247210091318B /* Products */,
+				95C2961D206247440091318B /* Frameworks */,
+			);
+			sourceTree = "<group>";
+		};
+		95C29612206247210091318B /* Products */ = {
+			isa = PBXGroup;
+			children = (
+				95C29611206247210091318B /* libYubiKit.a */,
+				95885B2520A3187700828D02 /* YubiKitTests.xctest */,
+			);
+			name = Products;
+			sourceTree = "<group>";
+		};
+		95C29613206247210091318B /* YubiKit */ = {
+			isa = PBXGroup;
+			children = (
+				95C29614206247210091318B /* YubiKit.h */,
+				95C296552062A46A0091318B /* YubiKitConfiguration.h */,
+				95C296562062A46B0091318B /* YubiKitConfiguration.m */,
+				95C2965220629FD10091318B /* YubiKitDeviceCapabilities.h */,
+				95C2965320629FD10091318B /* YubiKitDeviceCapabilities.m */,
+				956884CA20AAF83300E0F72C /* YubiKitDeviceCapabilities+Testing.h */,
+				95C2964E206295A00091318B /* YubiKitExternalLocalization.h */,
+				95C2964F206295A00091318B /* YubiKitExternalLocalization.m */,
+				95233E542330DEDF00C51F92 /* YubiKitLogger.h */,
+				95233E532330DEDF00C51F92 /* YubiKitLogger.m */,
+				95C296582062A5970091318B /* YubiKitManager.h */,
+				95C296592062A5970091318B /* YubiKitManager.m */,
+				953A507F213EECED00929ABB /* Sessions */,
+				951446A6218858F0002BB3C5 /* Layers */,
+				95C29620206247730091318B /* SharedModel */,
+				95C29629206250B30091318B /* Helpers */,
+				953A507E213EE92F00929ABB /* TestExtentions */,
+				955BCC0B215A999400C2EA2B /* ThirdParties */,
+			);
+			path = YubiKit;
+			sourceTree = "<group>";
+		};
+		95C2961D206247440091318B /* Frameworks */ = {
+			isa = PBXGroup;
+			children = (
+				956DB6732063B41E006B1738 /* AudioToolbox.framework */,
+				95C29636206259660091318B /* UIKit.framework */,
+				95C29627206250820091318B /* AVFoundation.framework */,
+				95C2961E206247450091318B /* CoreNFC.framework */,
+			);
+			name = Frameworks;
+			sourceTree = "<group>";
+		};
+		95C29620206247730091318B /* SharedModel */ = {
+			isa = PBXGroup;
+			children = (
+				95C29645206268C90091318B /* YKFOTPTextParser.h */,
+				95C29646206268C90091318B /* YKFOTPTextParser.m */,
+				956DB6882068EE64006B1738 /* YKFOTPTextParserProtocol.h */,
+				95C29621206247920091318B /* YKFOTPToken.h */,
+				95C29622206247920091318B /* YKFOTPToken.m */,
+				95C2963F2062653E0091318B /* YKFOTPTokenParser.h */,
+				95C296402062653E0091318B /* YKFOTPTokenParser.m */,
+				95C296242062497C0091318B /* YKFOTPTokenValidator.h */,
+				95C296252062497C0091318B /* YKFOTPTokenValidator.m */,
+				95C296422062656C0091318B /* YKFOTPURIParser.h */,
+				95C296432062656C0091318B /* YKFOTPURIParser.m */,
+				956DB6872068EE02006B1738 /* YKFOTPURIParserProtocol.h */,
+			);
+			path = SharedModel;
+			sourceTree = "<group>";
+		};
+		95C29629206250B30091318B /* Helpers */ = {
+			isa = PBXGroup;
+			children = (
+				953306872088CB8400A625C8 /* Additions */,
+				95C2962A206250D90091318B /* YKFPermissions.h */,
+				95C2962B206250D90091318B /* YKFPermissions.m */,
+				95C29630206256120091318B /* YKFLogger.h */,
+				95C29631206256120091318B /* YKFLogger.m */,
+				95C2963820625A180091318B /* YKFView.h */,
+				95C2963920625A180091318B /* YKFView.m */,
+				95C2963B2062634C0091318B /* YKFViewController.h */,
+				95C2963C2062634C0091318B /* YKFViewController.m */,
+				956DB6802063EF27006B1738 /* YKFBlockMacros.h */,
+				9547C9E3216CB648001E1F4A /* YKFAssert.h */,
+				95DD40792099A64C00363FEE /* YKFDispatch.h */,
+				95DD407A2099A64C00363FEE /* YKFDispatch.m */,
+				95E1B257219EE2D300E349E3 /* YKFKVOObservation.h */,
+				95E1B258219EE2D300E349E3 /* YKFKVOObservation.m */,
+			);
+			path = Helpers;
+			sourceTree = "<group>";
+		};
+		95C2963E206264FB0091318B /* NFCSession */ = {
+			isa = PBXGroup;
+			children = (
+				95E152A423151EAE00C4B7E7 /* Services */,
+				95DF11902317C60600CF0C39 /* YKFNFCConnectionController.h */,
+				95DF11912317C60600CF0C39 /* YKFNFCConnectionController.m */,
+				95C2964820627F2F0091318B /* YKFNFCError.h */,
+				95C2964920627F2F0091318B /* YKFNFCError.m */,
+				956DB691206A89A4006B1738 /* YKFNFCError+Errors.h */,
+				95E152A52315257600C4B7E7 /* YKFNFCSession.h */,
+				95E152A62315257600C4B7E7 /* YKFNFCSession.m */,
+				95E3934220C137770027E7B4 /* YKFURIIdentifierCode.h */,
+				95E3934320C137770027E7B4 /* YKFURIIdentifierCode.m */,
+				816C68492343126100209342 /* YKFNFCTagDescription.h */,
+				816C684A2343126100209342 /* YKFNFCTagDescription.m */,
+				816C684C234315CC00209342 /* YKFNFCTagDescription+Private.h */,
+			);
+			path = NFCSession;
+			sourceTree = "<group>";
+		};
+		95D5E2D6218711CB00AA1C11 /* RawCommand */ = {
+			isa = PBXGroup;
+			children = (
+				95D5E2D7218712BF00AA1C11 /* YKFKeyRawCommandService.h */,
+				95D5E2D8218712BF00AA1C11 /* YKFKeyRawCommandService.m */,
+				951446A121874013002BB3C5 /* YKFKeyRawCommandService+Private.h */,
+			);
+			path = RawCommand;
+			sourceTree = "<group>";
+		};
+		95D9D3D921D510A700473888 /* FIDO2 */ = {
+			isa = PBXGroup;
+			children = (
+				95D9D3DA21D510CB00473888 /* CBOR */,
+				952CB9E9220D82C4004A7624 /* Crypto */,
+				956991F122C2248F00C5EB02 /* WebAuthN */,
+				956DBB8221EDEA1D004D6EE3 /* YKFKeyFIDO2Service.h */,
+				956DBB8321EDEA1D004D6EE3 /* YKFKeyFIDO2Service.m */,
+				956DBB8521EDF841004D6EE3 /* YKFKeyFIDO2Service+Private.h */,
+			);
+			path = FIDO2;
+			sourceTree = "<group>";
+		};
+		95D9D3DA21D510CB00473888 /* CBOR */ = {
+			isa = PBXGroup;
+			children = (
+				95D9D3DB21D5110100473888 /* YKFCBOREncoder.h */,
+				95D9D3DC21D5110100473888 /* YKFCBOREncoder.m */,
+				95D9D3DE21D5111500473888 /* YKFCBORDecoder.h */,
+				95D9D3DF21D5111500473888 /* YKFCBORDecoder.m */,
+				95D9D3E121D67AAA00473888 /* YKFCBORType.h */,
+				95D9D3E221D67AAA00473888 /* YKFCBORType.m */,
+				95D9D3E421D6800D00473888 /* YKFCBORTag.h */,
+			);
+			path = CBOR;
+			sourceTree = "<group>";
+		};
+		95DD407D2099A84700363FEE /* AccessorySession */ = {
+			isa = PBXGroup;
+			children = (
+				95DD409F2099A8A300363FEE /* YKFAccessoryConnectionController.h */,
+				95DD40A52099A8A400363FEE /* YKFAccessoryConnectionController.m */,
+				953A5077213E9F4600929ABB /* YKFAccessoryDescription.h */,
+				953A5078213E9F4600929ABB /* YKFAccessoryDescription.m */,
+				953A507A213EA10F00929ABB /* YKFAccessoryDescription+Private.h */,
+				95DD40A02099A8A300363FEE /* YKFAccessorySession.h */,
+				95DD40A32099A8A300363FEE /* YKFAccessorySession.m */,
+				95B58B89229C03AE00199F8E /* YKFAccessorySession+Debugging.h */,
+				95B58B8A229C03AE00199F8E /* YKFAccessorySession+Debugging.m */,
+				95A6C92320ADAC09004F43CC /* YKFAccessorySession+Private.h */,
+				958491712130286900D7E2A3 /* YKFAccessorySessionConfiguration.h */,
+				958491722130286900D7E2A3 /* YKFAccessorySessionConfiguration.m */,
+			);
+			path = AccessorySession;
+			sourceTree = "<group>";
+		};
+		95DD407E2099A85700363FEE /* APDU */ = {
+			isa = PBXGroup;
+			children = (
+				95DD407F2099A86900363FEE /* YKFAPDU.h */,
+				95DD40822099A86900363FEE /* YKFAPDU.m */,
+				95A67D7C218729E5008C0B59 /* YKFAPDU+Private.h */,
+				9529CBC5214AA8A10041D2F8 /* YKFAPDUCommandInstruction.h */,
+				956DBB8621EDFE19004D6EE3 /* FIDO2 */,
+				9581395121591D94008558F3 /* OATH */,
+				9581395021591D32008558F3 /* U2F */,
+			);
+			path = APDU;
+			sourceTree = "<group>";
+		};
+		95DD408B2099A87600363FEE /* Errors */ = {
+			isa = PBXGroup;
+			children = (
+				95DD408C2099A88400363FEE /* YKFKeySessionError.h */,
+				95DD408D2099A88400363FEE /* YKFKeySessionError.m */,
+				95885B1B20A30DBD00828D02 /* YKFKeySessionError+Private.h */,
+				9551882E2265F4EE001A4191 /* YKFKeyAPDUError.h */,
+				9551882F2265F4EE001A4191 /* YKFKeyAPDUError.m */,
+				95B0CAAB21EF53E1009C6A34 /* YKFKeyFIDO2Error.h */,
+				95B0CAAC21EF53E1009C6A34 /* YKFKeyFIDO2Error.m */,
+				9551882A2265E8DE001A4191 /* YKFKeyOATHError.h */,
+				9551882B2265E8DE001A4191 /* YKFKeyOATHError.m */,
+				955188262265E4B9001A4191 /* YKFKeyU2FError.h */,
+				955188272265E4B9001A4191 /* YKFKeyU2FError.m */,
+			);
+			path = Errors;
+			sourceTree = "<group>";
+		};
+		95DD408F2099A88A00363FEE /* Requests */ = {
+			isa = PBXGroup;
+			children = (
+				95081DEC2214255B006CD08C /* YKFKeyRequest.h */,
+				95081DED2214255B006CD08C /* YKFKeyRequest.m */,
+				956DBB8A21EE00EC004D6EE3 /* FIDO2 */,
+				955BCBFE215A431200C2EA2B /* OATH */,
+				955BCBFD215A35C900C2EA2B /* U2F */,
+			);
+			path = Requests;
+			sourceTree = "<group>";
+		};
+		95E152A423151EAE00C4B7E7 /* Services */ = {
+			isa = PBXGroup;
+			children = (
+				95C2964B20628EBE0091318B /* YKFNFCOTPService.h */,
+				95C2964C20628EBE0091318B /* YKFNFCOTPService.m */,
+				956884BF20AAD0FB00E0F72C /* YKFNFCOTPService+Private.h */,
+			);
+			path = Services;
+			sourceTree = "<group>";
+		};
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+		95885B2420A3187700828D02 /* YubiKitTests */ = {
+			isa = PBXNativeTarget;
+			buildConfigurationList = 95885B2D20A3187700828D02 /* Build configuration list for PBXNativeTarget "YubiKitTests" */;
+			buildPhases = (
+				95885B2120A3187700828D02 /* Sources */,
+				95885B2220A3187700828D02 /* Frameworks */,
+				95885B2320A3187700828D02 /* Resources */,
+			);
+			buildRules = (
+			);
+			dependencies = (
+				95885B2C20A3187700828D02 /* PBXTargetDependency */,
+			);
+			name = YubiKitTests;
+			productName = YubiKitTests;
+			productReference = 95885B2520A3187700828D02 /* YubiKitTests.xctest */;
+			productType = "com.apple.product-type.bundle.unit-test";
+		};
+		95C29610206247210091318B /* YubiKit */ = {
+			isa = PBXNativeTarget;
+			buildConfigurationList = 95C2961A206247210091318B /* Build configuration list for PBXNativeTarget "YubiKit" */;
+			buildPhases = (
+				95C2960D206247210091318B /* Sources */,
+				95C2960E206247210091318B /* Frameworks */,
+				95C2960F206247210091318B /* CopyFiles */,
+			);
+			buildRules = (
+			);
+			dependencies = (
+			);
+			name = YubiKit;
+			productName = YubiKit;
+			productReference = 95C29611206247210091318B /* libYubiKit.a */;
+			productType = "com.apple.product-type.library.static";
+		};
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+		95C29609206247210091318B /* Project object */ = {
+			isa = PBXProject;
+			attributes = {
+				LastUpgradeCheck = 0930;
+				ORGANIZATIONNAME = Yubico;
+				TargetAttributes = {
+					95885B2420A3187700828D02 = {
+						CreatedOnToolsVersion = 9.3;
+						ProvisioningStyle = Automatic;
+					};
+					95C29610206247210091318B = {
+						CreatedOnToolsVersion = 9.2;
+						ProvisioningStyle = Automatic;
+					};
+				};
+			};
+			buildConfigurationList = 95C2960C206247210091318B /* Build configuration list for PBXProject "YubiKit" */;
+			compatibilityVersion = "Xcode 8.0";
+			developmentRegion = en;
+			hasScannedForEncodings = 0;
+			knownRegions = (
+				en,
+				Base,
+			);
+			mainGroup = 95C29608206247210091318B;
+			productRefGroup = 95C29612206247210091318B /* Products */;
+			projectDirPath = "";
+			projectRoot = "";
+			targets = (
+				95C29610206247210091318B /* YubiKit */,
+				95885B2420A3187700828D02 /* YubiKitTests */,
+			);
+		};
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+		95885B2320A3187700828D02 /* Resources */ = {
+			isa = PBXResourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+		95885B2120A3187700828D02 /* Sources */ = {
+			isa = PBXSourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				9564333420A5B99F007621BD /* YKFOTPTextParserTests.m in Sources */,
+				953A5085213FCDA100929ABB /* FakeEASession.m in Sources */,
+				950C700C22980CFE00E48458 /* FakeUIDevice.m in Sources */,
+				9564333620A5C03C007621BD /* YKFOTPTokenParserTests.m in Sources */,
+				956884C120AAD98500E0F72C /* YKFNFCOTPServiceTests.m in Sources */,
+				9529CBC1214927D80041D2F8 /* YKFKeyU2FServiceTests.m in Sources */,
+				957D869921B825B4004ABF86 /* YKFKeyRawCommandServiceTests.m in Sources */,
+				95EF75C3213FEF0500059C79 /* YKFTestCase.m in Sources */,
+				95EF75BE213FE9F200059C79 /* YKFAccessoryConnectionControllerTests.m in Sources */,
+				950C70092298095F00E48458 /* YubiKitDeviceCapabilitiesTests.m in Sources */,
+				9529CBBD214905770041D2F8 /* FakeEAAccessory.m in Sources */,
+				956884D020AAFE9300E0F72C /* FakeYKFOTPTextParser.m in Sources */,
+				956884C820AAE6F200E0F72C /* YKFNSDataAdditions.m in Sources */,
+				956884C520AADA5A00E0F72C /* FakeNFCNDEFReaderSession.m in Sources */,
+				95885B3120A31EFF00828D02 /* YKFOTPTokenValidatorTests.m in Sources */,
+				95D7364F21CA44EF0039141A /* YKFPCSCTests.m in Sources */,
+				951446A021873D43002BB3C5 /* YKFNSMutableDataAdditions.m in Sources */,
+				9529CBBA214903FA0041D2F8 /* YKFAccessorySessionConfigurationTests.m in Sources */,
+				95B8547E21E898F3000D6D7A /* YKFCBORDecoderTests.m in Sources */,
+				9564333220A58EDA007621BD /* YKFOTPURIParserTests.m in Sources */,
+				9529CBBF2149105F0041D2F8 /* YKFAccessoryDescriptionTests.m in Sources */,
+				95885B3220A31F1D00828D02 /* YKFOTPTokenValidator.m in Sources */,
+				95D7364D21C94BFD0039141A /* FakeYKFPCSCLayer.m in Sources */,
+				956884D320AB012200E0F72C /* FakeYKFOTPURIParser.m in Sources */,
+				956884CD20AAFB3F00E0F72C /* FakeYubiKitDeviceCapabilities.m in Sources */,
+				9529CBC421492A3E0041D2F8 /* FakeYKFKeyConnectionController.m in Sources */,
+				956884C920AAE70E00E0F72C /* UIDeviceAdditions.m in Sources */,
+				95EEEF6321664E4600BE7D7B /* MF_Base32Additions.m in Sources */,
+				95DD659121664B6800BA85C9 /* YKFOATHCredentialTests.m in Sources */,
+				95D61A04216F9159001E7AC8 /* YKFOATHCredentialValidatorTests.m in Sources */,
+				95B8547C21E628BE000D6D7A /* YKFCBOREncoderTests.m in Sources */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+		95C2960D206247210091318B /* Sources */ = {
+			isa = PBXSourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				95A04D1E2253920B008E3036 /* YKFFIDO2GetNextAssertionAPDU.m in Sources */,
+				95233E552330DEE000C51F92 /* YubiKitLogger.m in Sources */,
+				95F75BAB2175D63D00C13DC5 /* YKFKeyOATHValidateRequest.m in Sources */,
+				952564C8216F699500BC6F64 /* YKFOATHResetAPDU.m in Sources */,
+				95DD40872099A86A00363FEE /* YKFU2FSignAPDU.m in Sources */,
+				95D7364A21C9119D0039141A /* YKFPCSCErrorMap.m in Sources */,
+				954E2C542211AA5600720D2B /* YKFFIDO2ClientPinAPDU.m in Sources */,
+				95D61A072170B26F001E7AC8 /* YKFKeyOATHSelectApplicationResponse.m in Sources */,
+				95E1B259219EE2D300E349E3 /* YKFKVOObservation.m in Sources */,
+				9551882C2265E8DE001A4191 /* YKFKeyOATHError.m in Sources */,
+				95C296572062A46B0091318B /* YubiKitConfiguration.m in Sources */,
+				95A456692177639C00AD5A94 /* YKFOATHCalculateAllAPDU.m in Sources */,
+				95EEEF6621676B7F00BE7D7B /* YKFOATHSendRemainingAPDU.m in Sources */,
+				955BCC0F215A9A4D00C2EA2B /* MF_Base32Additions.m in Sources */,
+				95DD658E21663C3C00BA85C9 /* YKFOATHDeleteAPDU.m in Sources */,
+				95CCAE62224122FD00B567D2 /* YKFKeyFIDO2ChangePinRequest.m in Sources */,
+				95EEEF692167A93900BE7D7B /* YKFKeyOATHCalculateRequest.m in Sources */,
+				95C2965420629FD10091318B /* YubiKitDeviceCapabilities.m in Sources */,
+				955BCC01215A436100C2EA2B /* YKFKeyOATHPutRequest.m in Sources */,
+				95C2964D20628EBE0091318B /* YKFNFCOTPService.m in Sources */,
+				95C29635206259050091318B /* UIDeviceAdditions.m in Sources */,
+				955188302265F4EE001A4191 /* YKFKeyAPDUError.m in Sources */,
+				9581395421591DE1008558F3 /* YKFSelectOATHApplicationAPDU.m in Sources */,
+				95BA204521F7483100EED927 /* YKFKeyFIDO2GetAssertionResponse.m in Sources */,
+				95081DEE2214255B006CD08C /* YKFKeyRequest.m in Sources */,
+				956991F422C224BC00C5EB02 /* YKFWebAuthnClientData.m in Sources */,
+				956DBB8921EDFE50004D6EE3 /* YKFSelectFIDO2ApplicationAPDU.m in Sources */,
+				95C296262062497C0091318B /* YKFOTPTokenValidator.m in Sources */,
+				95885B1A20A2F96100828D02 /* YKFKeyU2FRequest.m in Sources */,
+				956DB66B206391C5006B1738 /* YKFQRCodeScanError.m in Sources */,
+				956DBB9421EE2B29004D6EE3 /* YKFFIDO2ResetAPDU.m in Sources */,
+				954E2C512211A34900720D2B /* YKFKeyFIDO2ClientPinRequest.m in Sources */,
+				95C29632206256120091318B /* YKFLogger.m in Sources */,
+				95DD409E2099A89600363FEE /* YKFKeyU2FSignRequest.m in Sources */,
+				955BCC0A215A463E00C2EA2B /* YKFOATHCredential.m in Sources */,
+				95C296442062656C0091318B /* YKFOTPURIParser.m in Sources */,
+				95D9D3E021D5111500473888 /* YKFCBORDecoder.m in Sources */,
+				95885B1820A2F94700828D02 /* YKFAccessoryConnectionController.m in Sources */,
+				95DD409C2099A89600363FEE /* YKFKeyU2FSignResponse.m in Sources */,
+				95DD407B2099A64C00363FEE /* YKFDispatch.m in Sources */,
+				95E3934420C137770027E7B4 /* YKFURIIdentifierCode.m in Sources */,
+				9578A60D21F1D63000349DCF /* YKFKeyFIDO2MakeCredentialRequest.m in Sources */,
+				955188282265E4B9001A4191 /* YKFKeyU2FError.m in Sources */,
+				95D61A0D2170E0C3001E7AC8 /* YKFOATHSetCodeAPDU.m in Sources */,
+				95E152A72315257600C4B7E7 /* YKFNFCSession.m in Sources */,
+				95F75BAE2175D6D600C13DC5 /* YKFOATHValidateAPDU.m in Sources */,
+				95C2965A2062A5970091318B /* YubiKitManager.m in Sources */,
+				958491732130286900D7E2A3 /* YKFAccessorySessionConfiguration.m in Sources */,
+				95C2962C206250D90091318B /* YKFPermissions.m in Sources */,
+				956DBB8421EDEA1D004D6EE3 /* YKFKeyFIDO2Service.m in Sources */,
+				958793E7216CE35F001A0406 /* YKFOATHListAPDU.m in Sources */,
+				95D5E2DC2187159700AA1C11 /* YKFNSMutableDataAdditions.m in Sources */,
+				955BCBF5215A33CA00C2EA2B /* YKFOATHPutAPDU.m in Sources */,
+				95EA81E72178805D0020595D /* YKFNSStringAdditions.m in Sources */,
+				9581395921592870008558F3 /* YKFKeyOATHService.m in Sources */,
+				95C29623206247920091318B /* YKFOTPToken.m in Sources */,
+				95B67185216F6A6300FA20E6 /* YKFKeyOATHResetRequest.m in Sources */,
+				956DBB9721EE32E5004D6EE3 /* YKFKeyFIDO2Request.m in Sources */,
+				95D9D3E321D67AAA00473888 /* YKFCBORType.m in Sources */,
+				9535F0122175FFB600A6D617 /* YKFKeyOATHValidateResponse.m in Sources */,
+				958D0B64215D106F00942CB9 /* YKFKeyService.m in Sources */,
+				954E2C572211B53100720D2B /* YKFKeyFIDO2ClientPinResponse.m in Sources */,
+				956DBB8D21EE0A0B004D6EE3 /* YKFFIDO2CommandAPDU.m in Sources */,
+				95C2964A20627F2F0091318B /* YKFNFCError.m in Sources */,
+				95C29647206268C90091318B /* YKFOTPTextParser.m in Sources */,
+				95885B0820A07ED800828D02 /* YKFQRReaderSession.m in Sources */,
+				951446AD21885962002BB3C5 /* YKFPCSC.m in Sources */,
+				816C684B2343126100209342 /* YKFNFCTagDescription.m in Sources */,
+				95BA204821F877BA00EED927 /* YKFFIDO2TouchPoolingAPDU.m in Sources */,
+				9578A61021F1D79400349DCF /* YKFFIDO2Type.m in Sources */,
+				95A4567121777D0E00AD5A94 /* YKFKeyOATHCalculateAllRequest.m in Sources */,
+				957BDF4F21F5C3A700899B5B /* YKFKeyFIDO2MakeCredentialResponse.m in Sources */,
+				956DB67220639C7D006B1738 /* YKFQRCodeScanViewController.m in Sources */,
+				95DD6585216635CA00BA85C9 /* YKFOATHCredentialValidator.m in Sources */,
+				95B0CAB021F098C6009C6A34 /* YKFKeyFIDO2GetInfoResponse.m in Sources */,
+				95C2963A20625A180091318B /* YKFView.m in Sources */,
+				95D61A0A2170DEEF001E7AC8 /* YKFKeyOATHSetCodeRequest.m in Sources */,
+				958793EE216E0DB2001A0406 /* YKFKeyOATHListResponse.m in Sources */,
+				95DD408E2099A88500363FEE /* YKFKeySessionError.m in Sources */,
+				954E2C5A2211C64900720D2B /* YKFKeyFIDO2VerifyPinRequest.m in Sources */,
+				957BDF5421F7130900899B5B /* YKFKeyFIDO2GetAssertionRequest.m in Sources */,
+				951446AA2188592C002BB3C5 /* YKFPCSCLayer.m in Sources */,
+				95B58B8B229C03AE00199F8E /* YKFAccessorySession+Debugging.m in Sources */,
+				95B0CAAD21EF53E1009C6A34 /* YKFKeyFIDO2Error.m in Sources */,
+				95CCAE5F2241220600B567D2 /* YKFKeyFIDO2SetPinRequest.m in Sources */,
+				95DD40A72099A8A400363FEE /* YKFAccessorySession.m in Sources */,
+				95DD409D2099A89600363FEE /* YKFKeyU2FRegisterResponse.m in Sources */,
+				95A4566C2177663800AD5A94 /* YKFKeyOATHCalculateAllResponse.m in Sources */,
+				953A5079213E9F4600929ABB /* YKFAccessoryDescription.m in Sources */,
+				956DBB9021EE1E5E004D6EE3 /* YKFFIDO2GetInfoAPDU.m in Sources */,
+				95D9D3DD21D5110100473888 /* YKFCBOREncoder.m in Sources */,
+				95885B1920A2F95800828D02 /* YKFKeyU2FRegisterRequest.m in Sources */,
+				95885B1720A2F92D00828D02 /* YKFSelectU2FApplicationAPDU.m in Sources */,
+				95DD40892099A86A00363FEE /* YKFAPDU.m in Sources */,
+				95DD658B21663B0400BA85C9 /* YKFKeyOATHDeleteRequest.m in Sources */,
+				95EEEF6C2167AB6A00BE7D7B /* YKFOATHCalculateAPDU.m in Sources */,
+				9578A61521F20BA400349DCF /* YKFFIDO2MakeCredentialAPDU.m in Sources */,
+				9547C9DD216B59E2001E1F4A /* YKFKeyOATHCalculateResponse.m in Sources */,
+				95DD40782099A4EB00363FEE /* YKFNSDataAdditions.m in Sources */,
+				95C2963D2062634C0091318B /* YKFViewController.m in Sources */,
+				956DB66F20639C2B006B1738 /* YKFQRCodeScanOverlayView.m in Sources */,
+				952CB9EC220D835F004A7624 /* YKFFIDO2PinAuthKey.m in Sources */,
+				95DD40A62099A8A400363FEE /* YKFKeyU2FService.m in Sources */,
+				95C296412062653E0091318B /* YKFOTPTokenParser.m in Sources */,
+				95C29650206295A00091318B /* YubiKitExternalLocalization.m in Sources */,
+				9533068A2088CB9F00A625C8 /* UIWindowAdditions.m in Sources */,
+				955BCC04215A43A000C2EA2B /* YKFKeyOATHRequest.m in Sources */,
+				95D5E2D9218712BF00AA1C11 /* YKFKeyRawCommandService.m in Sources */,
+				958793EA216CE6B9001A0406 /* YKFKeyOATHListRequest.m in Sources */,
+				95DF11922317C60600CF0C39 /* YKFNFCConnectionController.m in Sources */,
+				951446A421874CC2002BB3C5 /* YKFKeyCommandConfiguration.m in Sources */,
+				953A6FC221F733D8003B2477 /* YKFFIDO2GetAssertionAPDU.m in Sources */,
+				95DD408A2099A86A00363FEE /* YKFU2FRegisterAPDU.m in Sources */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXSourcesBuildPhase section */
+
+/* Begin PBXTargetDependency section */
+		95885B2C20A3187700828D02 /* PBXTargetDependency */ = {
+			isa = PBXTargetDependency;
+			target = 95C29610206247210091318B /* YubiKit */;
+			targetProxy = 95885B2B20A3187700828D02 /* PBXContainerItemProxy */;
+		};
+/* End PBXTargetDependency section */
+
+/* Begin XCBuildConfiguration section */
+		95885B2E20A3187700828D02 /* Debug */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				CLANG_ENABLE_OBJC_WEAK = YES;
+				CODE_SIGN_STYLE = Automatic;
+				DEVELOPMENT_TEAM = LQA3CS5MM7;
+				INFOPLIST_FILE = YubiKitTests/Info.plist;
+				IPHONEOS_DEPLOYMENT_TARGET = 11.0;
+				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
+				OTHER_LDFLAGS = "-ObjcC";
+				PRODUCT_BUNDLE_IDENTIFIER = com.yubico.YubiKitTests;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				TARGETED_DEVICE_FAMILY = "1,2";
+			};
+			name = Debug;
+		};
+		95885B2F20A3187700828D02 /* Release */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				CLANG_ENABLE_OBJC_WEAK = YES;
+				CODE_SIGN_STYLE = Automatic;
+				DEVELOPMENT_TEAM = LQA3CS5MM7;
+				INFOPLIST_FILE = YubiKitTests/Info.plist;
+				IPHONEOS_DEPLOYMENT_TARGET = 11.0;
+				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
+				OTHER_LDFLAGS = "-ObjcC";
+				PRODUCT_BUNDLE_IDENTIFIER = com.yubico.YubiKitTests;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				TARGETED_DEVICE_FAMILY = "1,2";
+			};
+			name = Release;
+		};
+		95C29618206247210091318B /* Debug */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				ALWAYS_SEARCH_USER_PATHS = NO;
+				CLANG_ANALYZER_NONNULL = YES;
+				CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+				CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
+				CLANG_CXX_LIBRARY = "libc++";
+				CLANG_ENABLE_MODULES = YES;
+				CLANG_ENABLE_OBJC_ARC = YES;
+				CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+				CLANG_WARN_BOOL_CONVERSION = YES;
+				CLANG_WARN_COMMA = YES;
+				CLANG_WARN_CONSTANT_CONVERSION = YES;
+				CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+				CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+				CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+				CLANG_WARN_EMPTY_BODY = YES;
+				CLANG_WARN_ENUM_CONVERSION = YES;
+				CLANG_WARN_INFINITE_RECURSION = YES;
+				CLANG_WARN_INT_CONVERSION = YES;
+				CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+				CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+				CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+				CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+				CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+				CLANG_WARN_STRICT_PROTOTYPES = YES;
+				CLANG_WARN_SUSPICIOUS_MOVE = YES;
+				CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+				CLANG_WARN_UNREACHABLE_CODE = YES;
+				CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+				CODE_SIGN_IDENTITY = "iPhone Developer";
+				COPY_PHASE_STRIP = NO;
+				DEBUG_INFORMATION_FORMAT = dwarf;
+				ENABLE_STRICT_OBJC_MSGSEND = YES;
+				ENABLE_TESTABILITY = YES;
+				GCC_C_LANGUAGE_STANDARD = gnu11;
+				GCC_DYNAMIC_NO_PIC = NO;
+				GCC_NO_COMMON_BLOCKS = YES;
+				GCC_OPTIMIZATION_LEVEL = 0;
+				GCC_PREPROCESSOR_DEFINITIONS = (
+					"DEBUG=1",
+					"$(inherited)",
+				);
+				GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+				GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+				GCC_WARN_UNDECLARED_SELECTOR = YES;
+				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+				GCC_WARN_UNUSED_FUNCTION = YES;
+				GCC_WARN_UNUSED_VARIABLE = YES;
+				IPHONEOS_DEPLOYMENT_TARGET = 10.0;
+				MTL_ENABLE_DEBUG_INFO = YES;
+				ONLY_ACTIVE_ARCH = YES;
+				SDKROOT = iphoneos;
+			};
+			name = Debug;
+		};
+		95C29619206247210091318B /* Release */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				ALWAYS_SEARCH_USER_PATHS = NO;
+				CLANG_ANALYZER_NONNULL = YES;
+				CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+				CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
+				CLANG_CXX_LIBRARY = "libc++";
+				CLANG_ENABLE_CODE_COVERAGE = NO;
+				CLANG_ENABLE_MODULES = YES;
+				CLANG_ENABLE_OBJC_ARC = YES;
+				CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+				CLANG_WARN_BOOL_CONVERSION = YES;
+				CLANG_WARN_COMMA = YES;
+				CLANG_WARN_CONSTANT_CONVERSION = YES;
+				CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+				CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+				CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+				CLANG_WARN_EMPTY_BODY = YES;
+				CLANG_WARN_ENUM_CONVERSION = YES;
+				CLANG_WARN_INFINITE_RECURSION = YES;
+				CLANG_WARN_INT_CONVERSION = YES;
+				CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+				CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+				CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+				CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+				CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+				CLANG_WARN_STRICT_PROTOTYPES = YES;
+				CLANG_WARN_SUSPICIOUS_MOVE = YES;
+				CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+				CLANG_WARN_UNREACHABLE_CODE = YES;
+				CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+				CODE_SIGN_IDENTITY = "iPhone Developer";
+				COPY_PHASE_STRIP = NO;
+				DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+				ENABLE_NS_ASSERTIONS = NO;
+				ENABLE_STRICT_OBJC_MSGSEND = YES;
+				GCC_C_LANGUAGE_STANDARD = gnu11;
+				GCC_NO_COMMON_BLOCKS = YES;
+				GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+				GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+				GCC_WARN_UNDECLARED_SELECTOR = YES;
+				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+				GCC_WARN_UNUSED_FUNCTION = YES;
+				GCC_WARN_UNUSED_VARIABLE = YES;
+				IPHONEOS_DEPLOYMENT_TARGET = 10.0;
+				MTL_ENABLE_DEBUG_INFO = NO;
+				SDKROOT = iphoneos;
+				VALIDATE_PRODUCT = YES;
+			};
+			name = Release;
+		};
+		95C2961B206247210091318B /* Debug */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				CODE_SIGN_STYLE = Automatic;
+				DEVELOPMENT_TEAM = LQA3CS5MM7;
+				OTHER_LDFLAGS = "-ObjC";
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				SKIP_INSTALL = YES;
+				TARGETED_DEVICE_FAMILY = "1,2";
+			};
+			name = Debug;
+		};
+		95C2961C206247210091318B /* Release */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				CODE_SIGN_STYLE = Automatic;
+				DEVELOPMENT_TEAM = LQA3CS5MM7;
+				OTHER_LDFLAGS = "-ObjC";
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				SKIP_INSTALL = YES;
+				TARGETED_DEVICE_FAMILY = "1,2";
+			};
+			name = Release;
+		};
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+		95885B2D20A3187700828D02 /* Build configuration list for PBXNativeTarget "YubiKitTests" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				95885B2E20A3187700828D02 /* Debug */,
+				95885B2F20A3187700828D02 /* Release */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
+		95C2960C206247210091318B /* Build configuration list for PBXProject "YubiKit" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				95C29618206247210091318B /* Debug */,
+				95C29619206247210091318B /* Release */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
+		95C2961A206247210091318B /* Build configuration list for PBXNativeTarget "YubiKit" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				95C2961B206247210091318B /* Debug */,
+				95C2961C206247210091318B /* Release */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
+/* End XCConfigurationList section */
+	};
+	rootObject = 95C29609206247210091318B /* Project object */;
+}
diff --git a/YubiKit/YubiKit.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/YubiKit/YubiKit.xcodeproj/project.xcworkspace/contents.xcworkspacedata
new file mode 100755
index 000000000..0a01a0f75
--- /dev/null
+++ b/YubiKit/YubiKit.xcodeproj/project.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Workspace
+   version = "1.0">
+   <FileRef
+      location = "self:YubiKit.xcodeproj">
+   </FileRef>
+</Workspace>
diff --git a/YubiKit/YubiKit.xcodeproj/xcshareddata/xcschemes/YubiKit.xcscheme b/YubiKit/YubiKit.xcodeproj/xcshareddata/xcschemes/YubiKit.xcscheme
new file mode 100755
index 000000000..92181c907
--- /dev/null
+++ b/YubiKit/YubiKit.xcodeproj/xcshareddata/xcschemes/YubiKit.xcscheme
@@ -0,0 +1,109 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+   LastUpgradeVersion = "0930"
+   version = "1.3">
+   <BuildAction
+      parallelizeBuildables = "YES"
+      buildImplicitDependencies = "YES">
+      <BuildActionEntries>
+         <BuildActionEntry
+            buildForTesting = "YES"
+            buildForRunning = "YES"
+            buildForProfiling = "YES"
+            buildForArchiving = "YES"
+            buildForAnalyzing = "YES">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "95C29610206247210091318B"
+               BuildableName = "libYubiKit.a"
+               BlueprintName = "YubiKit"
+               ReferencedContainer = "container:YubiKit.xcodeproj">
+            </BuildableReference>
+         </BuildActionEntry>
+      </BuildActionEntries>
+   </BuildAction>
+   <TestAction
+      buildConfiguration = "Debug"
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      codeCoverageEnabled = "YES"
+      shouldUseLaunchSchemeArgsEnv = "YES">
+      <CodeCoverageTargets>
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "95C29610206247210091318B"
+            BuildableName = "libYubiKit.a"
+            BlueprintName = "YubiKit"
+            ReferencedContainer = "container:YubiKit.xcodeproj">
+         </BuildableReference>
+      </CodeCoverageTargets>
+      <Testables>
+         <TestableReference
+            skipped = "NO">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "95885B2420A3187700828D02"
+               BuildableName = "YubiKitTests.xctest"
+               BlueprintName = "YubiKitTests"
+               ReferencedContainer = "container:YubiKit.xcodeproj">
+            </BuildableReference>
+         </TestableReference>
+      </Testables>
+      <MacroExpansion>
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "95C29610206247210091318B"
+            BuildableName = "libYubiKit.a"
+            BlueprintName = "YubiKit"
+            ReferencedContainer = "container:YubiKit.xcodeproj">
+         </BuildableReference>
+      </MacroExpansion>
+      <AdditionalOptions>
+      </AdditionalOptions>
+   </TestAction>
+   <LaunchAction
+      buildConfiguration = "Debug"
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      launchStyle = "0"
+      useCustomWorkingDirectory = "NO"
+      ignoresPersistentStateOnLaunch = "NO"
+      debugDocumentVersioning = "YES"
+      debugServiceExtension = "internal"
+      allowLocationSimulation = "YES">
+      <MacroExpansion>
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "95C29610206247210091318B"
+            BuildableName = "libYubiKit.a"
+            BlueprintName = "YubiKit"
+            ReferencedContainer = "container:YubiKit.xcodeproj">
+         </BuildableReference>
+      </MacroExpansion>
+      <AdditionalOptions>
+      </AdditionalOptions>
+   </LaunchAction>
+   <ProfileAction
+      buildConfiguration = "Release"
+      shouldUseLaunchSchemeArgsEnv = "YES"
+      savedToolIdentifier = ""
+      useCustomWorkingDirectory = "NO"
+      debugDocumentVersioning = "YES">
+      <MacroExpansion>
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "95C29610206247210091318B"
+            BuildableName = "libYubiKit.a"
+            BlueprintName = "YubiKit"
+            ReferencedContainer = "container:YubiKit.xcodeproj">
+         </BuildableReference>
+      </MacroExpansion>
+   </ProfileAction>
+   <AnalyzeAction
+      buildConfiguration = "Debug">
+   </AnalyzeAction>
+   <ArchiveAction
+      buildConfiguration = "Release"
+      revealArchiveInOrganizer = "YES">
+   </ArchiveAction>
+</Scheme>
diff --git a/YubiKit/YubiKit/Helpers/Additions/UIDeviceAdditions.h b/YubiKit/YubiKit/Helpers/Additions/UIDeviceAdditions.h
new file mode 100755
index 000000000..c0285d05d
--- /dev/null
+++ b/YubiKit/YubiKit/Helpers/Additions/UIDeviceAdditions.h
@@ -0,0 +1,80 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+#import <UIKit/UIKit.h>
+
+typedef NS_ENUM(NSUInteger, YKFDeviceModel) {
+    
+    YKFDeviceModelUnknown,
+    YKFDeviceModelSimulator,
+    
+    // iPhone models
+    
+    YKFDeviceModelIPhone4,
+    YKFDeviceModelIPhone4S,
+    YKFDeviceModelIPhone5,
+    YKFDeviceModelIPhone5C,
+    YKFDeviceModelIPhone5S,
+    YKFDeviceModelIPhone6,
+    YKFDeviceModelIPhone6Plus,
+    YKFDeviceModelIPhone6S,
+    YKFDeviceModelIPhone6SPlus,
+    YKFDeviceModelIPhoneSE,
+    YKFDeviceModelIPhone7,
+    YKFDeviceModelIPhone7Plus,
+    YKFDeviceModelIPhone8,
+    YKFDeviceModelIPhone8Plus,
+    YKFDeviceModelIPhoneX,
+    YKFDeviceModelIPhoneXS,
+    YKFDeviceModelIPhoneXSMax,
+    YKFDeviceModelIPhoneXR,
+    
+    // iPad models
+    
+    YKFDeviceModelIPad1,
+    YKFDeviceModelIPad2,
+    YKFDeviceModelIPadMini,
+    YKFDeviceModelIPad3,
+    YKFDeviceModelIPad4,
+    YKFDeviceModelIPadAir,
+    YKFDeviceModelIPadMini2,
+    YKFDeviceModelIPadAir2,
+    YKFDeviceModelIPadMini3,
+    YKFDeviceModelIPadMini4,
+    YKFDeviceModelIPadPro,
+    YKFDeviceModelIPad2017,
+    YKFDeviceModelIPadPro2,
+    YKFDeviceModelIPad6,
+    YKFDeviceModelIPadPro3,
+    
+    // iPod models
+    
+    YKFDeviceModelIPodTouch1,
+    YKFDeviceModelIPodTouch2,
+    YKFDeviceModelIPodTouch3,
+    YKFDeviceModelIPodTouch4,
+    YKFDeviceModelIPodTouch5,
+    YKFDeviceModelIPodTouch6
+};
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface UIDevice(YKFDeviceType)
+
+@property (nonatomic, assign, readonly) YKFDeviceModel ykf_deviceModel;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/YubiKit/YubiKit/Helpers/Additions/UIDeviceAdditions.m b/YubiKit/YubiKit/Helpers/Additions/UIDeviceAdditions.m
new file mode 100755
index 000000000..6508a4073
--- /dev/null
+++ b/YubiKit/YubiKit/Helpers/Additions/UIDeviceAdditions.m
@@ -0,0 +1,176 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <sys/utsname.h>
+#import "UIDeviceAdditions.h"
+
+@implementation UIDevice(YKFDeviceAdditions)
+
+static YKFDeviceModel ykf_deviceModelInternal = YKFDeviceModelUnknown;
+
+- (YKFDeviceModel)ykf_setupDeviceModel {
+#if TARGET_IPHONE_SIMULATOR
+    return YKFDeviceModelSimulator;
+#else
+    struct utsname systemInfo;
+    uname(&systemInfo);
+    NSString *deviceName = [NSString stringWithCString:systemInfo.machine encoding:NSUTF8StringEncoding];
+    
+    // iPhone models
+    
+    if ([self ykf_deviceName:deviceName isInList:@[@"iPhone3,1", @"iPhone3,2", @"iPhone3,3"]]) {
+        return YKFDeviceModelIPhone4;
+    }
+    if ([self ykf_deviceName:deviceName isInList:@[@"iPhone4,1", @"iPhone4,2", @"iPhone4,3"]]) {
+        return YKFDeviceModelIPhone4S;
+    }
+    if ([self ykf_deviceName:deviceName isInList:@[@"iPhone5,1", @"iPhone5,2"]]) {
+        return YKFDeviceModelIPhone5;
+    }
+    if ([self ykf_deviceName:deviceName isInList:@[@"iPhone5,3", @"iPhone5,4"]]) {
+        return YKFDeviceModelIPhone5C;
+    }
+    if ([self ykf_deviceName:deviceName isInList:@[@"iPhone6,1", @"iPhone6,2"]]) {
+        return YKFDeviceModelIPhone5S;
+    }
+    if ([self ykf_deviceName:deviceName isInList:@[@"iPhone7,2"]]) {
+        return YKFDeviceModelIPhone6;
+    }
+    if ([self ykf_deviceName:deviceName isInList:@[@"iPhone7,1"]]) {
+        return YKFDeviceModelIPhone6Plus;
+    }
+    if ([self ykf_deviceName:deviceName isInList:@[@"iPhone8,1"]]) {
+        return YKFDeviceModelIPhone6S;
+    }
+    if ([self ykf_deviceName:deviceName isInList:@[@"iPhone8,2"]]) {
+        return YKFDeviceModelIPhone6SPlus;
+    }
+    if ([self ykf_deviceName:deviceName isInList:@[@"iPhone8,4"]]) {
+        return YKFDeviceModelIPhoneSE;
+    }
+    if ([self ykf_deviceName:deviceName isInList:@[@"iPhone9,1", @"iPhone9,3"]]) {
+        return YKFDeviceModelIPhone7;
+    }
+    if ([self ykf_deviceName:deviceName isInList:@[@"iPhone9,2", @"iPhone9,4"]]) {
+        return YKFDeviceModelIPhone7Plus;
+    }
+    if ([self ykf_deviceName:deviceName isInList:@[@"iPhone10,1", @"iPhone10,4"]]) {
+        return YKFDeviceModelIPhone8;
+    }
+    if ([self ykf_deviceName:deviceName isInList:@[@"iPhone10,2", @"iPhone10,5"]]) {
+        return YKFDeviceModelIPhone8Plus;
+    }
+    if ([self ykf_deviceName:deviceName isInList:@[@"iPhone10,3", @"iPhone10,6"]]) {
+        return YKFDeviceModelIPhoneX;
+    }
+    if ([self ykf_deviceName:deviceName isInList:@[@"iPhone11,2"]]) {
+        return YKFDeviceModelIPhoneXS;
+    }
+    if ([self ykf_deviceName:deviceName isInList:@[@"iPhone11,4", @"iPhone11,6"]]) {
+        return YKFDeviceModelIPhoneXSMax;
+    }
+    if ([self ykf_deviceName:deviceName isInList:@[@"iPhone11,8"]]) {
+        return YKFDeviceModelIPhoneXR;
+    }
+
+    // iPad models
+
+    if ([self ykf_deviceName:deviceName isInList:@[@"iPad1,1"]]) {
+        return YKFDeviceModelIPad1;
+    }
+    if ([self ykf_deviceName:deviceName isInList:@[@"iPad2,1", @"iPad2,2", @"iPad2,3", @"iPad2,4"]]) {
+        return YKFDeviceModelIPad2;
+    }
+    if ([self ykf_deviceName:deviceName isInList:@[@"iPad3,1", @"iPad3,2", @"iPad3,3"]]) {
+        return YKFDeviceModelIPad3;
+    }
+    if ([self ykf_deviceName:deviceName isInList:@[@"iPad3,4", @"iPad3,5", @"iPad3,6"]]) {
+        return YKFDeviceModelIPad4;
+    }
+    if ([self ykf_deviceName:deviceName isInList:@[@"iPad4,1", @"iPad4,2", @"iPad4,3"]]) {
+        return YKFDeviceModelIPadAir;
+    }
+    if ([self ykf_deviceName:deviceName isInList:@[@"iPad5,3", @"iPad5,4"]]) {
+        return YKFDeviceModelIPadAir2;
+    }
+    if ([self ykf_deviceName:deviceName isInList:@[@"iPad2,5", @"iPad2,6", @"iPad2,7"]]) {
+        return YKFDeviceModelIPadMini;
+    }
+    if ([self ykf_deviceName:deviceName isInList:@[@"iPad4,4", @"iPad4,5", @"iPad4,6"]]) {
+        return YKFDeviceModelIPadMini2;
+    }
+    if ([self ykf_deviceName:deviceName isInList:@[@"iPad4,7", @"iPad4,8", @"iPad4,9"]]) {
+        return YKFDeviceModelIPadMini3;
+    }
+    if ([self ykf_deviceName:deviceName isInList:@[@"iPad5,1", @"iPad5,2"]]) {
+        return YKFDeviceModelIPadMini4;
+    }
+    if ([self ykf_deviceName:deviceName isInList:@[@"iPad6,3", @"iPad6,4", @"iPad6,7", @"iPad6,8"]]) {
+        return YKFDeviceModelIPadPro;
+    }
+    if ([self ykf_deviceName:deviceName isInList:@[@"iPad6,11", @"iPad6,12"]]) {
+        return YKFDeviceModelIPad2017;
+    }
+    if ([self ykf_deviceName:deviceName isInList:@[@"iPad7,1", @"iPad7,2", @"iPad7,3", @"iPad7,4"]]) {
+        return YKFDeviceModelIPadPro2;
+    }
+    if ([self ykf_deviceName:deviceName isInList:@[@"iPad7,5", @"iPad7,6"]]) {
+        return YKFDeviceModelIPad6;
+    }
+    if ([self ykf_deviceName:deviceName isInList:@[@"iPad8,1", @"iPad8,2", @"iPad8,3", @"iPad8,4", @"iPad8,5", @"iPad8,6", @"iPad8,7", @"iPad8,8"]]) {
+        return YKFDeviceModelIPadPro3;
+    }
+    
+    // iPod models
+
+    if ([self ykf_deviceName:deviceName isInList:@[@"iPod1,1"]]) {
+        return YKFDeviceModelIPodTouch1;
+    }
+    if ([self ykf_deviceName:deviceName isInList:@[@"iPod2,1"]]) {
+        return YKFDeviceModelIPodTouch2;
+    }
+    if ([self ykf_deviceName:deviceName isInList:@[@"iPod3,1"]]) {
+        return YKFDeviceModelIPodTouch3;
+    }
+    if ([self ykf_deviceName:deviceName isInList:@[@"iPod4,1"]]) {
+        return YKFDeviceModelIPodTouch4;
+    }
+    if ([self ykf_deviceName:deviceName isInList:@[@"iPod5,1"]]) {
+        return YKFDeviceModelIPodTouch5;
+    }
+    if ([self ykf_deviceName:deviceName isInList:@[@"iPod7,1"]]) {
+        return YKFDeviceModelIPodTouch6;
+    }
+
+    // Unknown device
+    
+    return YKFDeviceModelUnknown;
+#endif
+}
+
+- (BOOL)ykf_deviceName:(NSString *)deviceName isInList:(NSArray *)deviceList {
+    return [deviceList containsObject:deviceName];
+}
+
+- (YKFDeviceModel)ykf_deviceModel {
+    // The device type does not change at runtime so set it up once.
+    static dispatch_once_t onceToken;
+    dispatch_once(&onceToken, ^{
+        ykf_deviceModelInternal = [self ykf_setupDeviceModel];
+    });
+    return ykf_deviceModelInternal;
+}
+
+@end
+
diff --git a/YubiKit/YubiKit/Helpers/Additions/UIWindowAdditions.h b/YubiKit/YubiKit/Helpers/Additions/UIWindowAdditions.h
new file mode 100755
index 000000000..85da2de70
--- /dev/null
+++ b/YubiKit/YubiKit/Helpers/Additions/UIWindowAdditions.h
@@ -0,0 +1,25 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <UIKit/UIKit.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface UIWindow(YKFSafeReagion)
+
+@property (nonatomic, readonly) UIEdgeInsets ykf_safeAreaInsets;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/YubiKit/YubiKit/Helpers/Additions/UIWindowAdditions.m b/YubiKit/YubiKit/Helpers/Additions/UIWindowAdditions.m
new file mode 100755
index 000000000..93b48172a
--- /dev/null
+++ b/YubiKit/YubiKit/Helpers/Additions/UIWindowAdditions.m
@@ -0,0 +1,28 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "UIWindowAdditions.h"
+
+@implementation UIWindow(YKFSafeReagion)
+
+- (UIEdgeInsets)ykf_safeAreaInsets {
+    if (@available(iOS 11, *)) {
+        UIWindow *window = UIApplication.sharedApplication.keyWindow;
+        return window.safeAreaInsets;
+    }
+    return UIEdgeInsetsZero;
+}
+
+@end
+
diff --git a/YubiKit/YubiKit/Helpers/Additions/YKFNSDataAdditions+Private.h b/YubiKit/YubiKit/Helpers/Additions/YKFNSDataAdditions+Private.h
new file mode 100755
index 000000000..7f29fc70c
--- /dev/null
+++ b/YubiKit/YubiKit/Helpers/Additions/YKFNSDataAdditions+Private.h
@@ -0,0 +1,53 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+#import <CommonCrypto/CommonCrypto.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface NSData(NSData_Marshalling)
+
+- (NSUInteger)ykf_getBigEndianIntegerInRange:(NSRange)range;
+
+@end
+
+@interface NSData (NSDATA_OATHAdditions)
+
+- (nullable NSData *)ykf_deriveOATHKeyWithSalt:(NSData *)salt;
+- (nullable NSData *)ykf_oathHMACWithKey:(NSData *)key;
+- (nullable NSString *)ykf_parseOATHOTPFromIndex:(NSUInteger)index digits:(UInt8)digits;
+
+@end
+
+@interface NSData (NSDATA_FIDO2Additions)
+
+- (nullable NSData *)ykf_fido2HMACWithKey:(NSData *)key;
+
+- (nullable NSData *)ykf_aes256EncryptedDataWithKey:(NSData *)key;
+- (nullable NSData *)ykf_aes256DecryptedDataWithKey:(NSData *)key;
+- (nullable NSData *)ykf_aes256Operation:(CCOperation)operation withKey:(NSData *)key;
+
+- (nullable NSData *)ykf_fido2PaddedPinData;
+
+@end
+
+@interface NSData(NSDATA_SizeCheckAdditions)
+
+- (BOOL)ykf_containsIndex:(NSUInteger) index;
+- (BOOL)ykf_containsRange:(NSRange) range;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/YubiKit/YubiKit/Helpers/Additions/YKFNSDataAdditions.h b/YubiKit/YubiKit/Helpers/Additions/YKFNSDataAdditions.h
new file mode 100755
index 000000000..699cb0d24
--- /dev/null
+++ b/YubiKit/YubiKit/Helpers/Additions/YKFNSDataAdditions.h
@@ -0,0 +1,102 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * ---------------------------------------------------------------------------------------------------------------------
+ * @name NSData(NSData_WebSafeBase64)
+ * ---------------------------------------------------------------------------------------------------------------------
+ */
+
+@interface NSData(NSData_WebSafeBase64)
+
+/*!
+ @method ykf_initWithWebsafeBase64EncodedString:dataLength:
+ 
+ @return
+    A new data object created with a websafeBase64/base64url encoded string.
+ 
+ @param websafeBase64EncodedData
+    The websafe Base64 encoded string.
+ @param dataLen
+    The expected data length of the output. If the output is smaller the data will be padded to match the length.
+ */
+- (nullable instancetype)ykf_initWithWebsafeBase64EncodedString:(nonnull NSString *)websafeBase64EncodedData dataLength:(NSUInteger)dataLen;
+
+/*!
+ @method ykf_websafeBase64EncodedString
+ 
+ @return
+    The websafeBase64/base64url encoded string of the data.
+ */
+- (nullable NSString *)ykf_websafeBase64EncodedString;
+
+@end
+
+/**
+ * ---------------------------------------------------------------------------------------------------------------------
+ * @name NSData(NSData_SHAAdditions)
+ * ---------------------------------------------------------------------------------------------------------------------
+ */
+
+@interface NSData(NSData_SHAAdditions)
+
+/*!
+ @method ykf_SHA1
+ 
+ @return
+    The SHA1 hash of the data.
+ */
+- (NSData *)ykf_SHA1;
+
+/*!
+ @method ykf_SHA256
+ 
+ @return
+    The SHA256 hash of the data.
+ */
+- (NSData *)ykf_SHA256;
+
+/*!
+ @method ykf_SHA512
+ 
+ @return
+    The SHA512 hash of the data.
+ */
+- (NSData *)ykf_SHA512;
+
+@end
+
+/**
+ * ---------------------------------------------------------------------------------------------------------------------
+ * @name NSData(NSData_Base32Additions)
+ * ---------------------------------------------------------------------------------------------------------------------
+ */
+
+@interface NSData(NSData_Base32Additions)
+
+/*!
+ @method ykf_dataWithBase32String:
+ 
+ @return
+    A data object from a Base32 encoded string or nil if the string could not be parsed.
+ */
++ (nullable NSData *)ykf_dataWithBase32String:(NSString *)base32String;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/YubiKit/YubiKit/Helpers/Additions/YKFNSDataAdditions.m b/YubiKit/YubiKit/Helpers/Additions/YKFNSDataAdditions.m
new file mode 100755
index 000000000..63b57c8fd
--- /dev/null
+++ b/YubiKit/YubiKit/Helpers/Additions/YKFNSDataAdditions.m
@@ -0,0 +1,254 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+#import "YKFNSDataAdditions.h"
+#import "YKFNSDataAdditions+Private.h"
+#import "MF_Base32Additions.h"
+
+#pragma mark - SHA
+
+@implementation NSData(NSData_SHAAdditions)
+
+- (NSData *)ykf_SHA1 {
+    UInt8 digest[CC_SHA1_DIGEST_LENGTH];
+    CC_SHA1((const void *)[self bytes], (CC_LONG)[self length], digest);
+    return [[NSData alloc] initWithBytes:(const void *)digest length:CC_SHA1_DIGEST_LENGTH];
+}
+
+- (NSData *)ykf_SHA256 {
+	UInt8 digest[CC_SHA256_DIGEST_LENGTH];
+	CC_SHA256((const void *)[self bytes], (CC_LONG)[self length], digest);
+	return [[NSData alloc] initWithBytes:(const void *)digest length:CC_SHA256_DIGEST_LENGTH];
+}
+
+- (NSData *)ykf_SHA512 {
+    UInt8 digest[CC_SHA512_DIGEST_LENGTH];
+    CC_SHA512((const void *)[self bytes], (CC_LONG)[self length], digest);
+    return [[NSData alloc] initWithBytes:(const void *)digest length:CC_SHA512_DIGEST_LENGTH];
+}
+
+@end
+
+#pragma mark - OATH
+
+@implementation NSData(NSData_OATHAdditions)
+
+- (NSData *)ykf_deriveOATHKeyWithSalt:(NSData *)salt {
+    if (!salt.length) {
+        return nil;
+    }
+    
+    UInt8 keyLength = 16; // use only 16 bytes
+    UInt8 key[keyLength];
+    CCKeyDerivationPBKDF(kCCPBKDF2, self.bytes, self.length, salt.bytes, salt.length, kCCPRFHmacAlgSHA1, 1000, key, keyLength);
+    return [NSData dataWithBytes:key length:keyLength];
+}
+
+- (NSData *)ykf_oathHMACWithKey:(NSData *)key {
+    if (!key.length) {
+        return nil;
+    }
+    
+    UInt8 *keyBytes = (UInt8 *)key.bytes;
+    UInt8 *dataBytes = (UInt8 *)self.bytes;
+    
+    UInt8 result[CC_SHA1_DIGEST_LENGTH];
+    
+    CCHmac(kCCHmacAlgSHA1, keyBytes, key.length, dataBytes, self.length, result);
+    
+    return [[NSData alloc] initWithBytes:result length:CC_SHA1_DIGEST_LENGTH];
+}
+
+- (NSString *)ykf_parseOATHOTPFromIndex:(NSUInteger)index digits:(UInt8)digits {
+    if (index + sizeof(UInt32) > self.length) {
+        return nil;
+    }
+    
+    UInt32 otpResponseValue = CFSwapInt32BigToHost(*((UInt32 *)&self.bytes[index]));
+    otpResponseValue &= 0x7FFFFFFF; // remove first bit (sign bit)
+    
+    UInt32 modMask = pow(10, digits); // get last [digits] only
+    otpResponseValue = otpResponseValue % modMask;
+    
+    NSString *otp = nil;
+    
+    // Format with 0 paddigs up to [digits] number
+    if (digits == 6) {
+        otp = [NSString stringWithFormat:@"%06d", (unsigned int)otpResponseValue];
+    } else if (digits == 7){
+        otp = [NSString stringWithFormat:@"%07d", (unsigned int)otpResponseValue];
+    } else if (digits == 8){
+        otp = [NSString stringWithFormat:@"%08d", (unsigned int)otpResponseValue];
+    } else {
+        return nil;
+    }
+    
+    return otp;
+}
+
+@end
+
+#pragma mark - FIDO2
+
+@implementation NSData (NSDATA_FIDO2Additions)
+
+- (NSData *)ykf_fido2HMACWithKey:(NSData *)key {
+    if (!key.length) {
+        return nil;
+    }
+    
+    UInt8 *keyBytes = (UInt8 *)key.bytes;
+    UInt8 *dataBytes = (UInt8 *)self.bytes;
+    
+    UInt8 result[CC_SHA256_DIGEST_LENGTH];
+    
+    CCHmac(kCCHmacAlgSHA256, keyBytes, key.length, dataBytes, self.length, result);
+    
+    return [[NSData alloc] initWithBytes:result length:CC_SHA256_DIGEST_LENGTH];
+}
+
+- (NSData *)ykf_aes256EncryptedDataWithKey:(NSData *)key {
+    return [self ykf_aes256Operation:kCCEncrypt withKey:key];
+}
+
+- (NSData *)ykf_aes256DecryptedDataWithKey:(NSData *)key {
+    return [self ykf_aes256Operation:kCCDecrypt withKey:key];
+}
+
+- (NSData *)ykf_aes256Operation:(CCOperation)operation withKey:(NSData *)key {
+    if (!key.length) {
+        return nil;
+    }
+    
+    size_t outLength;
+    NSMutableData *outData = [NSMutableData dataWithLength:self.length + kCCBlockSizeAES128];
+    
+    CCCryptorRef ccRef = NULL;
+    CCCryptorCreate(operation, kCCAlgorithmAES, 0, key.bytes, kCCKeySizeAES256, NULL, &ccRef);
+    if (!ccRef) {
+        return nil;
+    }
+    CCCryptorStatus cryptStatus = CCCryptorUpdate(ccRef, self.bytes, self.length, outData.mutableBytes, outData.length, &outLength);
+    CCCryptorRelease(ccRef);
+    
+    if(cryptStatus == kCCSuccess) {
+        outData.length = outLength;
+        return outData;
+    }
+    return nil;
+}
+
+- (NSData *)ykf_fido2PaddedPinData {
+    if (!self.length) {
+        return nil;
+    }
+    if (self.length == 64) {
+        return self;
+    }
+    if ((self.length > 64) && (self.length % 16 == 0)) {
+        return self;
+    }
+    
+    NSMutableData *mutableData = [[NSMutableData alloc] initWithData:self];
+    NSUInteger lengthToIncrease = 0;
+    if (self.length < 64) {
+        lengthToIncrease = 64 - self.length;
+    } else {
+        lengthToIncrease = 16 - self.length % 16;
+    }
+    
+    if (lengthToIncrease) {
+        [mutableData increaseLengthBy:lengthToIncrease];
+    }
+    
+    return [mutableData copy];
+}
+
+@end
+
+#pragma mark - Marshalling
+
+@implementation NSData(NSData_Marshalling)
+
+- (NSUInteger)ykf_getBigEndianIntegerInRange:(NSRange)range {
+    NSInteger numberOfBytes = range.length;
+    if (numberOfBytes>sizeof(NSUInteger)) {
+        numberOfBytes = sizeof(NSUInteger);
+    }
+    Byte buffer[numberOfBytes];
+    [self getBytes:buffer range:NSMakeRange(range.location, numberOfBytes)];
+    NSUInteger value = 0;
+    for(NSInteger i = 0; i < numberOfBytes; ++i){
+        value = (value<<8) | buffer[i];
+    }
+    return value;
+}
+
+@end
+
+#pragma mark - WebSafe Base64
+
+@implementation NSData(NSData_WebSafeBase64)
+
+- (instancetype)ykf_initWithWebsafeBase64EncodedString:(NSString *)websafeBase64EncodedData dataLength:(NSUInteger)dataLen {
+    if (!websafeBase64EncodedData) {
+        return nil;
+    }
+    NSMutableString *base64EncodedString = [[NSMutableString alloc] initWithString:websafeBase64EncodedData];
+    [base64EncodedString replaceOccurrencesOfString:@"-" withString:@"+" options:0 range:NSMakeRange(0, [base64EncodedString length])];
+    [base64EncodedString replaceOccurrencesOfString:@"_" withString:@"/" options:0 range:NSMakeRange(0, [base64EncodedString length])];
+    if ((dataLen % 3) == 1){
+        [base64EncodedString appendString:@"=="];
+    }
+    else if ((dataLen % 3) == 2) {
+        [base64EncodedString appendString:@"="];
+    }
+    return [self initWithBase64EncodedString:base64EncodedString options:0];
+}
+
+- (NSString *)ykf_websafeBase64EncodedString {
+    NSMutableString *base64 = [[NSMutableString alloc] initWithString:[self base64EncodedStringWithOptions:0]];
+    [base64 replaceOccurrencesOfString:@"+" withString:@"-" options:0 range:NSMakeRange(0, [base64 length])];
+    [base64 replaceOccurrencesOfString:@"/" withString:@"_" options:0 range:NSMakeRange(0, [base64 length])];
+    [base64 replaceOccurrencesOfString:@"=" withString:@"" options:0 range:NSMakeRange(0, [base64 length])];
+    
+    return [NSString stringWithString:base64];
+}
+
+@end
+
+#pragma mark - Size Check
+
+@implementation NSData(NSDATA_SizeCheckAdditions)
+
+- (BOOL)ykf_containsIndex:(NSUInteger) index {
+    return index < self.length;
+}
+
+- (BOOL)ykf_containsRange:(NSRange) range {
+    return range.location + range.length <= self.length;
+}
+
+@end
+
+#pragma mark - Base32
+
+@implementation NSData(NSData_Base32Additions)
+
++ (NSData *)ykf_dataWithBase32String:(NSString *)base32String {
+    return [self dataWithBase32String:base32String];
+}
+
+@end
diff --git a/YubiKit/YubiKit/Helpers/Additions/YKFNSMutableDataAdditions.h b/YubiKit/YubiKit/Helpers/Additions/YKFNSMutableDataAdditions.h
new file mode 100755
index 000000000..b60952f6a
--- /dev/null
+++ b/YubiKit/YubiKit/Helpers/Additions/YKFNSMutableDataAdditions.h
@@ -0,0 +1,51 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+/*
+ Helper category for building and parsing APDU related binary data.
+ */
+@interface NSMutableData(NSMutableData_APDU)
+
+/*
+ Appends one byte to the mutable data buffer.
+ */
+- (void)ykf_appendByte:(UInt8)byte;
+
+/*
+ Appends [tag] + [data length] + [data] to the mutable data buffer.
+ */
+- (void)ykf_appendEntryWithTag:(UInt8)tag data:(NSData *)data;
+
+/*
+ Appends a tag with the data = [header bytes] + [data];
+ */
+- (void)ykf_appendEntryWithTag:(UInt8)tag headerBytes:(NSArray *)headerBytes data:(NSData *)data;
+
+/*
+ Appends the UInt32 value to the mutable data after converting it to a big endian represetation (key representation).
+ */
+- (void)ykf_appendUInt32EntryWithTag:(UInt8)tag value:(UInt32)value;
+
+/*
+ Appends the UInt64 value to the mutable data after converting it to a big endian represetation (key representation).
+ */
+- (void)ykf_appendUInt64EntryWithTag:(UInt8)tag value:(UInt64)value;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/YubiKit/YubiKit/Helpers/Additions/YKFNSMutableDataAdditions.m b/YubiKit/YubiKit/Helpers/Additions/YKFNSMutableDataAdditions.m
new file mode 100755
index 000000000..c762e27d9
--- /dev/null
+++ b/YubiKit/YubiKit/Helpers/Additions/YKFNSMutableDataAdditions.m
@@ -0,0 +1,70 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "YKFNSMutableDataAdditions.h"
+#import "YKFAssert.h"
+
+@implementation NSMutableData(NSMutableData_APDU)
+
+- (void)ykf_appendByte:(UInt8)byte {
+    [self appendBytes:&byte length:1];
+}
+
+- (void)ykf_appendEntryWithTag:(UInt8)tag data:(NSData *)data {    
+    YKFParameterAssertReturn(tag > 0);
+    YKFParameterAssertReturn(data.length > 0);
+    YKFParameterAssertReturn(data.length <= UINT8_MAX);
+    
+    [self ykf_appendByte:tag];
+    [self ykf_appendByte:data.length];
+    [self appendData:data];
+}
+
+- (void)ykf_appendEntryWithTag:(UInt8)tag headerBytes:(NSArray *)headerBytes data:(NSData *)data {
+    YKFParameterAssertReturn(tag > 0);
+    YKFParameterAssertReturn(headerBytes.count > 0);
+    YKFParameterAssertReturn(data.length > 0);
+    YKFParameterAssertReturn(headerBytes.count + data.length <= UINT8_MAX);
+    
+    NSMutableData *buffer = [[NSMutableData alloc] initWithCapacity:headerBytes.count + data.length];
+    for (NSNumber *byte in headerBytes) {
+        UInt8 byteValue = [byte unsignedCharValue];
+        [buffer ykf_appendByte:byteValue];
+    }
+    [buffer appendData:data];
+    
+    [self ykf_appendEntryWithTag:tag data:buffer];
+}
+
+- (void)ykf_appendUInt32EntryWithTag:(UInt8)tag value:(UInt32)value {
+    YKFParameterAssertReturn(tag > 0);
+    
+    UInt32 bigEndianValue = CFSwapInt32HostToBig(value);
+    
+    [self ykf_appendByte:tag];
+    [self ykf_appendByte:sizeof(UInt32)];
+    [self appendBytes:&bigEndianValue length:sizeof(UInt32)];
+}
+
+- (void)ykf_appendUInt64EntryWithTag:(UInt8)tag value:(UInt64)value {
+    YKFParameterAssertReturn(tag > 0);
+    
+    UInt64 bigEndianValue = CFSwapInt64HostToBig(value);
+    
+    [self ykf_appendByte:tag];
+    [self ykf_appendByte:sizeof(UInt64)];
+    [self appendBytes:&bigEndianValue length:sizeof(UInt64)];
+}
+
+@end
diff --git a/YubiKit/YubiKit/Helpers/Additions/YKFNSStringAdditions.h b/YubiKit/YubiKit/Helpers/Additions/YKFNSStringAdditions.h
new file mode 100755
index 000000000..6b999d4ce
--- /dev/null
+++ b/YubiKit/YubiKit/Helpers/Additions/YKFNSStringAdditions.h
@@ -0,0 +1,21 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+
+@interface NSString(NSString_OATH)
+
+- (void)ykf_OATHKeyExtractPeriod:(NSUInteger *)period issuer:(NSString **)issuer account:(NSString **)account label:(NSString **)label;
+
+@end
diff --git a/YubiKit/YubiKit/Helpers/Additions/YKFNSStringAdditions.m b/YubiKit/YubiKit/Helpers/Additions/YKFNSStringAdditions.m
new file mode 100755
index 000000000..8b493cf1a
--- /dev/null
+++ b/YubiKit/YubiKit/Helpers/Additions/YKFNSStringAdditions.m
@@ -0,0 +1,46 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "YKFNSStringAdditions.h"
+
+@implementation NSString(NSString_OATH)
+
+- (void)ykf_OATHKeyExtractPeriod:(NSUInteger *)period issuer:(NSString **)issuer account:(NSString **)account label:(NSString **)label {
+    NSString *key = self;
+    
+    // TOTP key with format [period]/[label]
+    if ([self containsString:@"/"]) {
+        NSArray *stringComponents = [self componentsSeparatedByString:@"/"];
+        if (stringComponents.count == 2) {
+            NSUInteger interval = [stringComponents[0] intValue];
+            if (interval) {
+                *period = interval;
+            }
+            key = stringComponents[1];
+        }
+    }
+    
+    *label = key;
+
+    // Parse the label as [issuer]:[account]
+    NSArray *labelComponents = [key componentsSeparatedByString:@":"];
+    if (labelComponents.count == 2) {
+        *issuer = labelComponents[0];
+        *account = labelComponents[1];
+    } else {
+        *account = key;
+    }
+}
+
+@end
diff --git a/YubiKit/YubiKit/Helpers/YKFAssert.h b/YubiKit/YubiKit/Helpers/YKFAssert.h
new file mode 100755
index 000000000..060125229
--- /dev/null
+++ b/YubiKit/YubiKit/Helpers/YKFAssert.h
@@ -0,0 +1,79 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+/*
+ Checks for condition to be true. If the condition is false it will nullify self and return nil from the initializer.
+ This macro must be used in initializers only!
+ */
+#define YKFAbortInitWhen(condition) if (condition) { self = nil; return nil; }
+
+/*
+ This macro is an extension of ykf_abort_init_when(condition). It has a different behaviour between release and debug builds:
+ - In release builds it will behave like ykf_abort_init_when(condition) because assertions are disabled.
+ - In debug builds it will assert for state to be true and stop the execution.
+ This macro must be used in initializers only!
+ */
+#define YKFAssertAbortInit(state) NSAssert(state, @"Did not satisfy initializer requirements."); YKFAbortInitWhen(!(state))
+
+/*
+ This macro has a different behaviour between release and debug builds:
+ - In release builds it will return because assertions are disabled.
+ - In debug builds it will assert for state to be true and stop the execution.
+ */
+#define YKFParameterAssertReturn(state) NSAssert(state, @"Did not satisfy parameter requirements."); if (!(state)) { return; }
+
+/*
+ This macro has a different behaviour between release and debug builds:
+ - In release builds it will return the specified value because assertions are disabled.
+ - In debug builds it will assert for state to be true and stop the execution.
+ */
+#define YKFParameterAssertReturnValue(state, value) NSParameterAssert(state); if (!(state)) { return value; }
+
+/*
+ This macro has a different behaviour between release and debug builds:
+ - In release builds it will return because assertions are disabled.
+ - In debug builds it will assert for state to be true and stop the execution.
+ */
+#define YKFAssertReturn(state, message) NSAssert(state, message); if (!(state)) { return; }
+
+/*
+ This macro has a different behaviour between release and debug builds:
+ - In release builds it will return the specified value because assertions are disabled.
+ - In debug builds it will assert for state to be true and stop the execution.
+ */
+#define YKFAssertReturnValue(state, message, value) NSAssert(state, message); if (!(state)) { return value; }
+
+//
+// Thread Execution Assertions
+//
+
+/*
+ Asserts that the execution happens off the main thread.
+ */
+#define YKFAssertOffMainThread() NSAssert(![NSThread isMainThread], @"Execution not allowed on the main thread.")
+
+/*
+ Asserts that the execution happens off the main thread (C Version).
+ */
+#define YKFCAssertOffMainThread() NSCAssert(![NSThread isMainThread], @"Execution not allowed on the main thread.")
+
+/*
+ Asserts that the execution happens on the main thread.
+ */
+#define YKFAssertOnMainThread() NSAssert([NSThread isMainThread], @"Execution not allowed off the main thread.")
+
+/*
+ Asserts that the execution happens on the main thread (C Version).
+ */
+#define YKFCAssertOnMainThread() NSCAssert([NSThread isMainThread], @"Execution not allowed off the main thread.")
diff --git a/YubiKit/YubiKit/Helpers/YKFBlockMacros.h b/YubiKit/YubiKit/Helpers/YKFBlockMacros.h
new file mode 100755
index 000000000..003634269
--- /dev/null
+++ b/YubiKit/YubiKit/Helpers/YKFBlockMacros.h
@@ -0,0 +1,20 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+
+#define ykf_weak_self() __weak typeof(self) weakSelf = self
+
+#define ykf_strong_self() __strong typeof(self) strongSelf = weakSelf
+#define ykf_safe_strong_self() __strong typeof(self) strongSelf = weakSelf; if (!strongSelf) { return; }
diff --git a/YubiKit/YubiKit/Helpers/YKFDispatch.h b/YubiKit/YubiKit/Helpers/YKFDispatch.h
new file mode 100755
index 000000000..6a7c844fc
--- /dev/null
+++ b/YubiKit/YubiKit/Helpers/YKFDispatch.h
@@ -0,0 +1,21 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+
+void ykf_dispatch_thread_async(NSThread* thread, dispatch_block_t block);
+void ykf_dispatch_thread_sync(NSThread* thread, dispatch_block_t block);
+
+void ykf_dispatch_block_main(dispatch_block_t block);
+void ykf_dispatch_block_sync_main(dispatch_block_t block);
diff --git a/YubiKit/YubiKit/Helpers/YKFDispatch.m b/YubiKit/YubiKit/Helpers/YKFDispatch.m
new file mode 100755
index 000000000..5ceefa725
--- /dev/null
+++ b/YubiKit/YubiKit/Helpers/YKFDispatch.m
@@ -0,0 +1,48 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "YKFDispatch.h"
+
+void ykf_dispatch_thread_async(NSThread* thread, dispatch_block_t block) {
+    if ([NSThread currentThread] == thread) {
+        block();
+    } else {
+        block = [block copy];
+        [(id)block performSelector: @selector(invoke) onThread: thread withObject: nil waitUntilDone: NO];
+    }
+}
+
+void ykf_dispatch_thread_sync(NSThread* thread, dispatch_block_t block) {
+    if ([NSThread currentThread] == thread) {
+        block();
+    } else {
+        [(id)block performSelector: @selector(invoke) onThread: thread withObject: nil waitUntilDone: YES];
+    }
+}
+
+void ykf_dispatch_block_main(dispatch_block_t block) {
+    if ([NSThread isMainThread]) {
+        block();
+    } else {
+        dispatch_async(dispatch_get_main_queue(), block);
+    }
+}
+
+void ykf_dispatch_block_sync_main(dispatch_block_t block) {
+    if ([NSThread isMainThread]) {
+        block();
+    } else {
+        dispatch_sync(dispatch_get_main_queue(), block);
+    }
+}
diff --git a/YubiKit/YubiKit/Helpers/YKFKVOObservation.h b/YubiKit/YubiKit/Helpers/YKFKVOObservation.h
new file mode 100755
index 000000000..2944d6c8b
--- /dev/null
+++ b/YubiKit/YubiKit/Helpers/YKFKVOObservation.h
@@ -0,0 +1,24 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+
+typedef void (^YKFKVOObservationBlock) (id _Nonnull oldValue, id _Nonnull newValue);
+
+@interface YKFKVOObservation: NSObject
+
+- (nullable instancetype)initWithTarget:(nonnull id)target keyPath:(nonnull NSString *)keyPath callback:(nonnull YKFKVOObservationBlock)callback NS_DESIGNATED_INITIALIZER;
+- (nonnull instancetype)init NS_UNAVAILABLE;
+
+@end
diff --git a/YubiKit/YubiKit/Helpers/YKFKVOObservation.m b/YubiKit/YubiKit/Helpers/YKFKVOObservation.m
new file mode 100755
index 000000000..8faf33f94
--- /dev/null
+++ b/YubiKit/YubiKit/Helpers/YKFKVOObservation.m
@@ -0,0 +1,78 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "YKFKVOObservation.h"
+#import "YKFAssert.h"
+
+static const int YKFKVOObservationContext = 0;
+
+@interface YKFKVOObservation()
+
+@property (nonatomic, weak) id target; // The target is not retained.
+@property (nonatomic) NSString *keyPath;
+
+@property (nonatomic, copy) YKFKVOObservationBlock callback;
+
+@end
+
+@implementation YKFKVOObservation
+
+- (instancetype)initWithTarget:(id)target keyPath:(NSString *)keyPath callback:(YKFKVOObservationBlock)callback {
+    YKFAssertAbortInit(target);
+    YKFAssertAbortInit(keyPath);
+    YKFAssertAbortInit(callback)
+    
+    self = [super init];
+    if (self) {
+        self.target = target;
+        self.keyPath = keyPath;
+        self.callback = callback;
+        [self addObservation];
+    }
+    return self;
+}
+
+- (void)dealloc {
+    YKFAssertReturn(self.target, @"The observation target was deallocated before removing the observation.");
+    [self removeObservation];
+}
+
+#pragma mark - KVO
+
+- (void)addObservation {
+    [self.target addObserver:self forKeyPath:self.keyPath
+                     options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
+                     context:(void *)&YKFKVOObservationContext];
+}
+
+- (void)removeObservation {
+    [self.target removeObserver:self forKeyPath:self.keyPath
+                        context:(void *)&YKFKVOObservationContext];
+}
+
+- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
+    if (context != &YKFKVOObservationContext) {
+        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
+        return;
+    }
+    
+    YKFAssertReturn([keyPath isEqualToString:self.keyPath], @"Invalid KVO update from unknown keyPath.");
+    
+    id oldValue = change[NSKeyValueChangeOldKey];
+    id newValue = change[NSKeyValueChangeNewKey];
+    
+    self.callback(oldValue, newValue);
+}
+
+@end
diff --git a/YubiKit/YubiKit/Helpers/YKFLogger.h b/YubiKit/YubiKit/Helpers/YKFLogger.h
new file mode 100755
index 000000000..50d2537a9
--- /dev/null
+++ b/YubiKit/YubiKit/Helpers/YKFLogger.h
@@ -0,0 +1,28 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+
+void YKFLogInfo(NSString* _Nonnull format, ...) NS_FORMAT_FUNCTION(1, 2);
+
+void YKFLogError(NSString* _Nonnull format, ...) NS_FORMAT_FUNCTION(1, 2);
+
+void YKFLogNSError(NSError* _Nonnull error);
+
+void YKFLogAssertion(NSString* _Nonnull format, ...) NS_FORMAT_FUNCTION(1, 2);
+
+// This should be disabled for release builds.
+// #define YKF_ENABLE_VERBOSE_LOGGING
+
+void YKFLogVerbose(NSString* _Nonnull format, ...) NS_FORMAT_FUNCTION(1, 2);
diff --git a/YubiKit/YubiKit/Helpers/YKFLogger.m b/YubiKit/YubiKit/Helpers/YKFLogger.m
new file mode 100755
index 000000000..d84ad5e85
--- /dev/null
+++ b/YubiKit/YubiKit/Helpers/YKFLogger.m
@@ -0,0 +1,88 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "YKFLogger.h"
+#import "YubiKitLogger.h"
+
+// Prefix for showing Info logs.
+static NSString* const YKFLogPrefixInfo = @"►►[I]► YubiKit:";
+
+// Prefix for showing Verbose logs.
+static NSString* const YKFLogPrefixVerbose = @"►►[V]► YubiKit:";
+
+// Prefix for showing Error logs.
+static NSString* const YKFLogPrefixError = @"►►[E]► YubiKit:";
+
+// Prefix for showing Assertions logs (helpful in Automation).
+static NSString* const YKFLogPrefixAssertion = @"►►[A]► YubiKit:";
+
+void YKFLog(NSString *format, va_list args) {
+#ifdef DEBUG
+    NSLogv(format, args);
+#endif
+    if (YubiKitLogger.customLogger) {
+        NSString *message = [[NSString alloc] initWithFormat:format arguments: args];
+        [YubiKitLogger.customLogger log:message];
+    }
+}
+
+void YKFLogInfo(NSString* _Nonnull format, ...) {
+    va_list args;
+    va_start(args, format);
+    
+    NSString *ykFormat = [NSString stringWithFormat:@"%@ %@", YKFLogPrefixInfo, format];
+    YKFLog(ykFormat, args);
+    
+    va_end(args);
+}
+
+void YKFLogError(NSString* _Nonnull format, ...) {
+    va_list args;
+    va_start(args, format);
+    
+    NSString *ykFormat = [NSString stringWithFormat:@"%@ %@", YKFLogPrefixError, format];
+    YKFLog(ykFormat, args);
+    
+    va_end(args);
+}
+
+void YKFLogNSError(NSError *error) {
+    NSInteger errorCode = error.code;
+    NSString *errorType = NSStringFromClass(error.class);
+    NSString *errorMessage = error.localizedDescription;
+    
+    YKFLogError(@"%@(%ld) - %@", errorType, (long)errorCode, errorMessage);
+}
+
+void YKFLogAssertion(NSString* _Nonnull format, ...) {
+    va_list args;
+    va_start(args, format);
+    
+    NSString *ykFormat = [NSString stringWithFormat:@"%@ %@", YKFLogPrefixAssertion, format];
+    YKFLog(ykFormat, args);
+    
+    va_end(args);
+}
+
+void YKFLogVerbose(NSString* _Nonnull format, ...) {
+#ifdef YKF_ENABLE_VERBOSE_LOGGING
+    va_list args;
+    va_start(args, format);
+    
+    NSString *ykFormat = [NSString stringWithFormat:@"%@ %@", YKFLogPrefixVerbose, format];
+    YKFLog(ykFormat, args);
+    
+    va_end(args);
+#endif
+}
diff --git a/YubiKit/YubiKit/Helpers/YKFPermissions.h b/YubiKit/YubiKit/Helpers/YKFPermissions.h
new file mode 100755
index 000000000..408a9cecf
--- /dev/null
+++ b/YubiKit/YubiKit/Helpers/YKFPermissions.h
@@ -0,0 +1,41 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+typedef NS_ENUM(NSUInteger, YKFPermissionType) {
+    YKFPermissionTypeVideoCapture
+};
+
+typedef NS_ENUM(NSUInteger, YKFPermissionAuthorizationStatus) {
+    YKFPermissionAuthorizationStatusNotDetermined,
+    YKFPermissionAuthorizationStatusDenied,
+    YKFPermissionAuthorizationStatusRestricted,
+    YKFPermissionAuthorizationStatusAuthorized
+};
+
+@protocol YKFPermissionsProtocol<NSObject>
+
+@property (nonatomic, assign, readonly) YKFPermissionAuthorizationStatus videoCaptureAuthorizationStatus;
+
+- (void)requestVideoCaptureAuthorization:(void (^)(BOOL))completion;
+
+@end
+
+@interface YKFPermissions : NSObject<YKFPermissionsProtocol>
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/YubiKit/YubiKit/Helpers/YKFPermissions.m b/YubiKit/YubiKit/Helpers/YKFPermissions.m
new file mode 100755
index 000000000..09ec2985d
--- /dev/null
+++ b/YubiKit/YubiKit/Helpers/YKFPermissions.m
@@ -0,0 +1,49 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <AVFoundation/AVFoundation.h>
+#import "YKFPermissions.h"
+
+@implementation YKFPermissions
+
+@synthesize videoCaptureAuthorizationStatus;
+
+- (YKFPermissionAuthorizationStatus)videoCaptureAuthorizationStatus {
+    AVAuthorizationStatus status = [AVCaptureDevice authorizationStatusForMediaType: AVMediaTypeVideo];
+    switch (status) {
+        case AVAuthorizationStatusNotDetermined:
+            return YKFPermissionAuthorizationStatusNotDetermined;
+        case AVAuthorizationStatusDenied:
+            return YKFPermissionAuthorizationStatusDenied;
+        case AVAuthorizationStatusRestricted:
+            return YKFPermissionAuthorizationStatusRestricted;
+        case AVAuthorizationStatusAuthorized:
+            return YKFPermissionAuthorizationStatusAuthorized;
+    }
+}
+
+- (void)requestVideoCaptureAuthorization:(void (^_Nonnull)(BOOL))completion {
+    YKFPermissionAuthorizationStatus status = self.videoCaptureAuthorizationStatus;
+    if (status == YKFPermissionAuthorizationStatusAuthorized) {
+        completion(YES);
+        return;
+    }
+    if (status != YKFPermissionAuthorizationStatusNotDetermined) {
+        completion(NO);
+        return;
+    }
+    [AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo completionHandler:completion];
+}
+
+@end
diff --git a/YubiKit/YubiKit/Helpers/YKFView.h b/YubiKit/YubiKit/Helpers/YKFView.h
new file mode 100755
index 000000000..816693ca9
--- /dev/null
+++ b/YubiKit/YubiKit/Helpers/YKFView.h
@@ -0,0 +1,29 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+#import <UIKit/UIKit.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+@protocol YKFLocalizable<NSObject>
+
+- (void)setupLocalization;
+
+@end
+
+@interface YKFView : UIView<YKFLocalizable>
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/YubiKit/YubiKit/Helpers/YKFView.m b/YubiKit/YubiKit/Helpers/YKFView.m
new file mode 100755
index 000000000..c6882678f
--- /dev/null
+++ b/YubiKit/YubiKit/Helpers/YKFView.m
@@ -0,0 +1,31 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "YKFView.h"
+#import "YKFAssert.h"
+
+@implementation YKFView
+
+- (void)didMoveToSuperview {
+    [super didMoveToSuperview];
+    if (self.superview != nil) {
+        [self setupLocalization];
+    }
+}
+
+- (void)setupLocalization {
+    YKFAssertReturn(NO, @"Type does not override setupLocalization().");
+}
+
+@end
diff --git a/YubiKit/YubiKit/Helpers/YKFViewController.h b/YubiKit/YubiKit/Helpers/YKFViewController.h
new file mode 100755
index 000000000..c2de994e7
--- /dev/null
+++ b/YubiKit/YubiKit/Helpers/YKFViewController.h
@@ -0,0 +1,27 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+#import <UIKit/UIKit.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface YKFViewController : UIViewController
+
+- (void)pinViewToEdges:(UIView*)view insets:(UIEdgeInsets)insets;
+- (void)pinViewToSafeAreaEdges:(UIView*)view;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/YubiKit/YubiKit/Helpers/YKFViewController.m b/YubiKit/YubiKit/Helpers/YKFViewController.m
new file mode 100755
index 000000000..ab3c0f535
--- /dev/null
+++ b/YubiKit/YubiKit/Helpers/YKFViewController.m
@@ -0,0 +1,48 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "YKFViewController.h"
+#import "UIWindowAdditions.h"
+
+@implementation YKFViewController
+
+- (void)pinViewToEdges:(UIView*)view insets:(UIEdgeInsets)insets {
+    view.translatesAutoresizingMaskIntoConstraints = NO;
+    
+    [self.view addSubview:view];
+    
+    NSLayoutConstraint *left = [NSLayoutConstraint constraintWithItem:view attribute:NSLayoutAttributeLeading
+                                                            relatedBy:NSLayoutRelationEqual toItem:self.view
+                                                            attribute:NSLayoutAttributeLeading multiplier:1 constant:insets.left];
+    NSLayoutConstraint *right = [NSLayoutConstraint constraintWithItem:view attribute:NSLayoutAttributeTrailing
+                                                             relatedBy:NSLayoutRelationEqual toItem:self.view
+                                                             attribute:NSLayoutAttributeTrailing multiplier:1 constant:-insets.right];
+    NSLayoutConstraint *top = [NSLayoutConstraint constraintWithItem:view attribute:NSLayoutAttributeTop
+                                                           relatedBy:NSLayoutRelationEqual toItem:self.view
+                                                           attribute:NSLayoutAttributeTop multiplier:1 constant:insets.top];
+    
+    NSLayoutConstraint *bottom = [NSLayoutConstraint constraintWithItem:view attribute:NSLayoutAttributeBottom
+                                                              relatedBy:NSLayoutRelationEqual toItem:self.view
+                                                              attribute:NSLayoutAttributeBottom multiplier:1 constant:-insets.bottom];
+    
+    NSArray *constraints = [[NSArray alloc] initWithObjects:left, right, top, bottom, nil];
+    [self.view addConstraints:constraints];
+}
+
+- (void)pinViewToSafeAreaEdges:(UIView*)view  {    
+    UIEdgeInsets safeAreaInsets = UIApplication.sharedApplication.keyWindow.ykf_safeAreaInsets;
+    [self pinViewToEdges:view insets:safeAreaInsets];
+}
+
+@end
diff --git a/YubiKit/YubiKit/Layers/PCSC/YKFPCSC.h b/YubiKit/YubiKit/Layers/PCSC/YKFPCSC.h
new file mode 100755
index 000000000..6b370bf6e
--- /dev/null
+++ b/YubiKit/YubiKit/Layers/PCSC/YKFPCSC.h
@@ -0,0 +1,171 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+//  PC/SC like interface for YubiKit.
+//  PC/SC Lite API reference: https://pcsclite.apdu.fr/api/group__API.html
+//
+//  Note:
+//    In the iOS SDK has no native concept of PC/SC. This interface is just an adaptation of
+//    the PC/SC interface, specific to YubiKit.
+
+#import <Foundation/Foundation.h>
+#import "YKFPCSCErrors.h"
+#import "YKFPCSCTypes.h"
+
+/*!
+ @abstract
+    Adapted version of SCardEstablishContext for YubiKit.
+    For more details of the original API check SCardEstablishContext on https://pcsclite.apdu.fr/api/group__API.html
+ */
+SInt64 YKFSCardEstablishContext(UInt32 scope,
+                                const void *reserved1,
+                                const void *reserved2,
+                                SInt32 *context);
+/*!
+ @abstract
+    Adapted version of SCardReleaseContext for YubiKit.
+    For more details of the original API check SCardReleaseContext on https://pcsclite.apdu.fr/api/group__API.html
+ */
+SInt64 YKFSCardReleaseContext(SInt32 context);
+
+/*!
+ @abstract
+    Adapted version of SCardConnect for YubiKit.
+    For more details of the original API check SCardConnect on https://pcsclite.apdu.fr/api/group__API.html
+ 
+ @discussion
+    In YubiKit this API will try to start the communication session with the key if the key is
+    connected to the device.
+ */
+SInt64 YKFSCardConnect(SInt32 context,
+                       const char *reader,
+                       UInt32 shareMode,
+                       UInt32 preferredProtocols,
+                       SInt32 *card,
+                       UInt32 *activeProtocol);
+
+/*!
+ @abstract
+    Adapted version of SCardReconnect for YubiKit.
+    For more details of the original API check SCardReconnect on https://pcsclite.apdu.fr/api/group__API.html
+ 
+ @discussion
+    In YubiKit this API will try to stop and start the communication session with the key if the key is
+    connected to the device.
+ */
+SInt64 YKFSCardReconnect(SInt32 card,
+                         UInt32 shareMode,
+                         UInt32 preferredProtocols,
+                         UInt32 initialization,
+                         UInt32 *activeProtocol);
+
+/*!
+ @abstract
+    Adapted version of SCardDisconnect for YubiKit.
+    For more details of the original API check SCardDisconnect on https://pcsclite.apdu.fr/api/group__API.html
+ 
+ @discussion
+    In YubiKit this API will try to stop the communication session with the key if the key is
+    connected to the device.
+ */
+SInt64 YKFSCardDisconnect(SInt32 card,
+                          UInt32 disposition);
+
+/*!
+ @abstract
+    Adapted version of SCardBeginTransaction for YubiKit.
+    For more details of the original API check SCardBeginTransaction on https://pcsclite.apdu.fr/api/group__API.html
+ */
+SInt64 YKFSCardBeginTransaction(SInt32 card);
+
+/*!
+ @abstract
+    Adapted version of SCardEndTransaction for YubiKit.
+    For more details of the original API check SCardEndTransaction on https://pcsclite.apdu.fr/api/group__API.html
+ */
+SInt64 YKFSCardEndTransaction(SInt32 card,
+                              UInt32 disposition);
+
+/*!
+ @abstract
+    Adapted version of SCardStatus for YubiKit.
+    For more details of the original API check SCardStatus on https://pcsclite.apdu.fr/api/group__API.html
+ */
+SInt64 YKFSCardStatus(SInt32 card,
+                      char *readerNames,
+                      UInt32 *readerLen,
+                      UInt32 *state,
+                      UInt32 *protocol,
+                      unsigned char *atr,
+                      UInt32 *atrLength);
+
+/*!
+ @abstract
+    Adapted version of SCardGetStatusChange for YubiKit.
+    For more details of the original API check SCardGetStatusChange on https://pcsclite.apdu.fr/api/group__API.html
+ 
+ @note
+    Supports only YKF_SCARD_STATE_UNAWARE and returns immediately.
+ */
+SInt64 YKFSCardGetStatusChange(SInt32 context,
+                               UInt32 timeout,
+                               YKF_SCARD_READERSTATE *readerStates,
+                               UInt32 readers);
+
+/*!
+ @abstract
+    Adapted version of SCardTransmit for YubiKit.
+    For more details of the original API check SCardTransmit on https://pcsclite.apdu.fr/api/group__API.html
+ */
+SInt64 YKFSCardTransmit(SInt32 card,
+                        YKF_SCARD_IO_REQUEST *sendPci,
+                        const unsigned char *sendBuffer,
+                        UInt32 sendLength,
+                        YKF_SCARD_IO_REQUEST *recvPci,
+                        unsigned char *recvBuffer,
+                        UInt32 *recvLength);
+
+/*!
+ @abstract
+ Adapted version of SCardListReaders for YubiKit.
+ For more details of the original API check SCardListReaders on https://pcsclite.apdu.fr/api/group__API.html
+ */
+SInt64 YKFSCardListReaders(SInt32 context,
+                           const char *groups,
+                           char *readers,
+                           UInt32 *readersLength);
+
+
+/*!
+ @abstract
+    Adapted version of SCardCancel for YubiKit.
+    For more details of the original API check SCardCancel on https://pcsclite.apdu.fr/api/group__API.html
+ */
+SInt64 YKFSCardCancel(SInt32 context);
+
+/*!
+ @abstract
+    Adapted version of SCardGetAttrib for YubiKit.
+    For more details of the original API check SCardGetAttrib on https://pcsclite.apdu.fr/api/group__API.html
+ */
+SInt64 YKFSCardGetAttrib(SInt32 card,
+                         UInt32 attrId,
+                         UInt8 *attr,
+                         UInt32 *attrLength);
+
+/*!
+ @abstract
+    Return a description of the PC/SC error code.
+ */
+const char* YKFPCSCStringifyError(const SInt64 pcscError);
diff --git a/YubiKit/YubiKit/Layers/PCSC/YKFPCSC.m b/YubiKit/YubiKit/Layers/PCSC/YKFPCSC.m
new file mode 100755
index 000000000..411d02965
--- /dev/null
+++ b/YubiKit/YubiKit/Layers/PCSC/YKFPCSC.m
@@ -0,0 +1,375 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "YKFPCSC.h"
+#import "YKFPCSCLayer.h"
+#import "YubiKitManager.h"
+#import "YKFAssert.h"
+
+/*
+ Assigns a random context value and creates the PC/SC communication layer.
+ */
+SInt64 YKFSCardEstablishContext(UInt32 scope, const void *reserved1, const void *reserved2, SInt32 *context) {
+    YKFCAssertOffMainThread();
+    
+    if (!context) {
+        return YKF_SCARD_E_INVALID_PARAMETER;
+    }
+    
+    SInt32 newContext = arc4random();
+    newContext = abs(newContext);
+    
+    if (![YKFPCSCLayer.shared addContext:newContext]) {
+        return YKF_SCARD_E_NO_MEMORY;
+    }
+    
+    *context = newContext;
+    return YKF_SCARD_S_SUCCESS;
+}
+
+/*
+ Releases the the PC/SC communication layer.
+ */
+SInt64 YKFSCardReleaseContext(SInt32 context) {
+    YKFCAssertOffMainThread();
+
+    if (![YKFPCSCLayer.shared removeContext:context]) {
+        return YKF_SCARD_E_INVALID_HANDLE;
+    }
+    return YKF_SCARD_S_SUCCESS;
+}
+
+/*
+ Starts the Key Session if not started and selects the PIV application.
+ */
+SInt64 YKFSCardConnect(SInt32 context, const char *reader, UInt32 shareMode, UInt32 preferredProtocols, SInt32 *card, UInt32 *activeProtocol) {
+    YKFCAssertOffMainThread();
+    
+    if (!card || !activeProtocol) {
+        return YKF_SCARD_E_INVALID_PARAMETER;
+    }
+    
+    if (![YKFPCSCLayer.shared contextIsValid:context]) {
+        return YKF_SCARD_E_INVALID_HANDLE;
+    }
+    
+    SInt32 newCard = arc4random();
+    newCard = abs(newCard);
+    
+    if (![YKFPCSCLayer.shared addCard:newCard toContext:context]) {
+        return YKF_SCARD_E_NO_MEMORY;
+    }
+
+    SInt64 connectCardResponse = [YKFPCSCLayer.shared connectCard];
+    if (connectCardResponse == YKF_SCARD_S_SUCCESS) {
+        *card = newCard;
+        *activeProtocol = YKF_SCARD_PROTOCOL_T1;
+    }
+    
+    return connectCardResponse;
+}
+
+/*
+ Stops and then starts the Key Session and selects the PIV application.
+ */
+SInt64 YKFSCardReconnect(SInt32 card, UInt32 shareMode, UInt32 preferredProtocols, UInt32 initialization, UInt32 *activeProtocol) {
+    YKFCAssertOffMainThread();
+    
+    if (![YKFPCSCLayer.shared cardIsValid:card]) {
+        return YKF_SCARD_E_INVALID_HANDLE;
+    }
+    
+    *activeProtocol = YKF_SCARD_PROTOCOL_T1;
+    return [YKFPCSCLayer.shared reconnectCard];
+}
+
+/*
+ Stops the Key Session.
+ */
+SInt64 YKFSCardDisconnect(SInt32 card, UInt32 disposition) {
+    YKFCAssertOffMainThread();
+    
+    if (![YKFPCSCLayer.shared cardIsValid:card]) {
+        return YKF_SCARD_E_INVALID_HANDLE;
+    }
+    if (![YKFPCSCLayer.shared removeCard:card]) {
+        return YKF_SCARD_F_COMM_ERROR;
+    }
+    
+    return [YKFPCSCLayer.shared disconnectCard];
+}
+
+/*
+ Does nothing. This will always succeed because only one application can talk to the key.
+ */
+SInt64 YKFSCardBeginTransaction(SInt32 card) {
+    YKFCAssertOffMainThread();
+    
+    if (![YKFPCSCLayer.shared cardIsValid:card]) {
+        return YKF_SCARD_E_INVALID_HANDLE;
+    }
+    
+    return YKF_SCARD_S_SUCCESS;
+}
+
+/*
+ Does nothing. This will always succeed because only one application can talk to the key.
+ */
+SInt64 YKFSCardEndTransaction(SInt32 card, UInt32 disposition) {
+    YKFCAssertOffMainThread();
+    
+    if (![YKFPCSCLayer.shared cardIsValid:card]) {
+        return YKF_SCARD_E_INVALID_HANDLE;
+    }
+    
+    return YKF_SCARD_S_SUCCESS;
+}
+
+/*
+ Returns the status of the Key Session and the serial of the key when connected.
+ */
+SInt64 YKFSCardStatus(SInt32 card, char *readerNames, UInt32 *readerLen, UInt32 *state, UInt32 *protocol, unsigned char *atr, UInt32 *atrLength) {
+    YKFCAssertOffMainThread();
+    
+    if (![YKFPCSCLayer.shared cardIsValid:card]) {
+        return YKF_SCARD_E_INVALID_HANDLE;
+    }
+    
+    // Readers
+    
+    SInt32 context = [YKFPCSCLayer.shared contextForCard:card];
+    if (!context) {
+        return YKF_SCARD_E_INVALID_HANDLE;
+    }
+    
+    SInt64 result = YKFSCardListReaders(context, nil, readerNames, readerLen);
+    if (result != YKF_SCARD_S_SUCCESS) {
+        return result;
+    }
+
+    // State
+    
+    if (state) {
+        *state = YKFPCSCLayer.shared.cardState;
+    }
+    
+    // Protocol
+    
+    if (protocol) {
+        *protocol = YKF_SCARD_PROTOCOL_T1;
+    }
+    
+    // ATR
+    
+    NSData *keyAtr = YKFPCSCLayer.shared.cardAtr;
+    if (keyAtr.length) {
+        UInt8 *keyAtrBytes = (UInt8 *)keyAtr.bytes;
+        
+        UInt32 inAtrLength = atrLength ? *atrLength : 0;
+        UInt32 outAtrLen = (UInt32)keyAtr.length;
+        
+        if (atrLength) {
+            *atrLength = outAtrLen;
+        }
+        
+        if (atr && inAtrLength && inAtrLength < outAtrLen) {
+            return YKF_SCARD_E_INSUFFICIENT_BUFFER;
+        }
+        
+        if (atr) {
+            memcpy(atr, keyAtrBytes, outAtrLen);
+        }
+    }
+    
+    return YKF_SCARD_S_SUCCESS;
+}
+
+SInt64 YKFSCardGetStatusChange(SInt32 context, UInt32 timeout, YKF_SCARD_READERSTATE *readerStates, UInt32 readers) {
+    YKFCAssertOffMainThread();
+    
+    if (![YKFPCSCLayer.shared contextIsValid:context]) {
+        return YKF_SCARD_E_INVALID_HANDLE;
+    }
+    if (!readerStates && readers) {
+        return YKF_SCARD_E_INVALID_PARAMETER;
+    }
+    
+    // 1. Get the key connection status.
+    UInt8 status = YKFPCSCLayer.shared.statusChange;
+    
+    // 2. Get the ATR
+    NSData *keyAtr = YKFPCSCLayer.shared.cardAtr;
+    NSCAssert(keyAtr.length <= YKF_MAX_ATR_SIZE, @"ATR value too long.");
+    
+    UInt8 *atrValue = (UInt8 *)keyAtr.bytes;
+    UInt8 atrLength = keyAtr.length;
+    
+    // 3. Populate the list or readers.
+    for (int i = 0; i < readers; ++i) {
+        readerStates[i].eventState = status;
+        
+        readerStates[i].atr = atrLength;
+        memset(readerStates[i].rgbAtr, 0, YKF_MAX_ATR_SIZE);
+        memcpy(readerStates[i].rgbAtr, atrValue, atrLength);
+    }
+    
+    return YKF_SCARD_S_SUCCESS;
+}
+
+/*
+ Sends the APDU through the Raw Command Service to the PIV Application.
+ */
+SInt64 YKFSCardTransmit(SInt32 card, YKF_SCARD_IO_REQUEST *sendPci, const unsigned char *sendBuffer, UInt32 sendLength,
+                        YKF_SCARD_IO_REQUEST *recvPci, unsigned char *recvBuffer, UInt32 *recvLength) {
+    YKFCAssertOffMainThread();
+
+    if (![YKFPCSCLayer.shared cardIsValid:card]) {
+        return YKF_SCARD_E_INVALID_HANDLE;
+    }
+    if (!sendBuffer || !sendLength || !recvBuffer || !*recvLength) {
+        // If send or receive buffers are empty/nil
+        return YKF_SCARD_E_INVALID_PARAMETER;
+    }
+    
+    NSData *commandData = [NSData dataWithBytes:sendBuffer length:sendLength];
+    NSData *responseData = nil;
+    
+    SInt64 responseCode = [YKFPCSCLayer.shared transmit:commandData response:&responseData];
+    
+    if (responseCode == YKF_SCARD_S_SUCCESS) {
+        UInt32 inRecvLength = recvLength ? *recvLength : 0;
+        UInt32 outRecvLength = (UInt32)responseData.length;
+        
+        if (recvLength) {
+            *recvLength = outRecvLength;
+        }
+        
+        if (recvBuffer && inRecvLength < outRecvLength) {
+            return YKF_SCARD_E_INSUFFICIENT_BUFFER;
+        }
+        
+        [responseData getBytes:recvBuffer length:responseData.length];
+    }
+    return responseCode;
+}
+
+/*
+ If the key is connected it will return the name of the key.
+ */
+SInt64 YKFSCardListReaders(SInt32 context, const char *groups, char *readers, UInt32 *readersLength) {
+    YKFCAssertOffMainThread();
+    
+    if (![YKFPCSCLayer.shared contextIsValid:context]) {
+        return YKF_SCARD_E_INVALID_HANDLE;
+    }
+    
+    NSString *readerName = nil;
+    SInt64 responseCode = [YKFPCSCLayer.shared listReaders:&readerName];
+    
+    if (responseCode == YKF_SCARD_S_SUCCESS && readerName) {
+        const char *ykReader = [readerName cStringUsingEncoding:NSUTF8StringEncoding];
+        if (!ykReader) {
+            return YKF_SCARD_F_INTERNAL_ERROR;
+        }
+        
+        unsigned long readerLength = strlen(ykReader);
+        unsigned long outReadersLength = readerLength + 2; // double null terminated multistring.
+        
+        UInt32 inReadersLength = readersLength ? *readersLength : 0; // aux
+        
+        if (readersLength) {
+            *readersLength = (UInt32)outReadersLength;
+        }
+        
+        if (readers && inReadersLength < outReadersLength) {
+            return YKF_SCARD_E_INSUFFICIENT_BUFFER;
+        }
+        
+        if (readers) { // copy the value in the provided buffer.
+            memset(readers, 0, outReadersLength);
+            strcpy(readers, ykReader);
+        }
+    }
+    return responseCode;
+}
+
+SInt64 YKFSCardCancel(SInt32 context) {
+    YKFCAssertOffMainThread();
+    
+    if (![YKFPCSCLayer.shared contextIsValid:context]) {
+        return YKF_SCARD_E_INVALID_HANDLE;
+    }
+    return YKF_SCARD_S_SUCCESS;
+}
+
+/*
+ Returns some attributes specific to the YubiKey.
+ */
+SInt64 YKFSCardGetAttrib(SInt32 card, UInt32 attrId, UInt8 *attr, UInt32 *attrLength) {
+    YKFCAssertOffMainThread();
+    
+    if (![YKFPCSCLayer.shared cardIsValid:card]) {
+        return YKF_SCARD_E_INVALID_HANDLE;
+    }
+    
+    const char *attributeValue = nil;
+    switch (attrId) {
+        case YKF_SCARD_ATTR_DEVICE_FRIENDLY_NAME:
+            attributeValue = YKFPCSCLayer.shared.deviceFriendlyName.UTF8String;
+            break;
+
+        case YKF_SCARD_ATTR_VENDOR_IFD_SERIAL_NO: {
+                NSString *serial = YKFPCSCLayer.shared.cardSerial;
+                if (!serial.length) {
+                    return YKF_SCARD_S_SUCCESS;
+                }
+                attributeValue = [serial UTF8String];
+            }
+            break;
+
+        case YKF_SCARD_ATTR_VENDOR_IFD_TYPE:
+            attributeValue = YKFPCSCLayer.shared.deviceModelName.UTF8String;
+            break;
+
+        case YKF_SCARD_ATTR_VENDOR_NAME:
+            attributeValue = YKFPCSCLayer.shared.deviceVendorName.UTF8String;
+            break;
+            
+        default:
+            return YKF_SCARD_E_UNSUPPORTED_FEATURE;
+            break;
+    }
+    
+    UInt32 inAttrLength = attrLength ? *attrLength : 0;
+    UInt32 outAttrLength = (UInt32)strlen(attributeValue) + 1;
+    
+    if (attrLength) {
+        *attrLength = outAttrLength;
+    }
+    
+    if (attr && inAttrLength < outAttrLength) {
+        return YKF_SCARD_E_INSUFFICIENT_BUFFER;
+    }
+    
+    if (attr) {
+        memcpy(attr, attributeValue, outAttrLength);
+    }
+    
+    return YKF_SCARD_S_SUCCESS;
+}
+
+const char* YKFPCSCStringifyError(const SInt64 pcscError) {
+    NSString *description = [YKFPCSCLayer.shared stringifyError:pcscError];
+    return description ? [description UTF8String] : "";
+}
diff --git a/YubiKit/YubiKit/Layers/PCSC/YKFPCSCErrorMap.h b/YubiKit/YubiKit/Layers/PCSC/YKFPCSCErrorMap.h
new file mode 100755
index 000000000..0de9a8a44
--- /dev/null
+++ b/YubiKit/YubiKit/Layers/PCSC/YKFPCSCErrorMap.h
@@ -0,0 +1,21 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+
+@interface YKFPCSCErrorMap: NSObject
+
+- (nullable NSString *)errorForCode:(SInt64)code;
+
+@end
diff --git a/YubiKit/YubiKit/Layers/PCSC/YKFPCSCErrorMap.m b/YubiKit/YubiKit/Layers/PCSC/YKFPCSCErrorMap.m
new file mode 100755
index 000000000..27beac9e1
--- /dev/null
+++ b/YubiKit/YubiKit/Layers/PCSC/YKFPCSCErrorMap.m
@@ -0,0 +1,108 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "YKFPCSCErrors.h"
+#import "YKFPCSCErrorMap.h"
+
+@interface YKFPCSCErrorMap()
+
+@property (nonatomic) NSDictionary<NSNumber*, NSString*> *errorMap;
+
+@end
+
+@implementation YKFPCSCErrorMap
+
+- (instancetype)init {
+    self = [super init];
+    if (self) {
+        [self setupErrorMap];
+    }
+    return self;
+}
+
+- (NSString *)errorForCode:(SInt64)code {
+    return self.errorMap[@(code)];
+}
+
+#pragma mark - Helpers
+
+- (void)setupErrorMap {
+    NSMutableDictionary<NSNumber*, NSString*> *descriptions = [[NSMutableDictionary alloc] init];
+    
+    descriptions[@(YKF_SCARD_S_SUCCESS)] = @"No error.";
+    descriptions[@(YKF_SCARD_F_INTERNAL_ERROR)] = @"An internal consistency check failed.";
+    descriptions[@(YKF_SCARD_E_CANCELLED)] = @"The action was cancelled by an SCardCancel request.";
+    descriptions[@(YKF_SCARD_E_INVALID_HANDLE)] = @"The supplied handle was invalid.";
+    descriptions[@(YKF_SCARD_E_INVALID_PARAMETER)] = @"One or more of the supplied parameters could not be properly interpreted.";
+    descriptions[@(YKF_SCARD_E_INVALID_TARGET)] = @"Registry startup information is missing or invalid.";
+    descriptions[@(YKF_SCARD_E_NO_MEMORY)] = @"Not enough memory available to complete this command.";
+    descriptions[@(YKF_SCARD_F_WAITED_TOO_LONG)] = @"An internal consistency timer has expired.";
+    descriptions[@(YKF_SCARD_E_INSUFFICIENT_BUFFER)] = @"The data buffer to receive returned data is too small for the returned data.";
+    descriptions[@(YKF_SCARD_E_UNKNOWN_READER)] = @"The specified reader name is not recognized.";
+    descriptions[@(YKF_SCARD_E_TIMEOUT)] = @"The user-specified timeout value has expired.";
+    descriptions[@(YKF_SCARD_E_SHARING_VIOLATION)] = @"The smart card cannot be accessed because of other connections outstanding.";
+    descriptions[@(YKF_SCARD_E_NO_SMARTCARD)] = @"The operation requires a Smart Card, but no Smart Card is currently in the device.";
+    descriptions[@(YKF_SCARD_E_UNKNOWN_CARD)] = @"The specified smart card name is not recognized.";
+    descriptions[@(YKF_SCARD_E_CANT_DISPOSE)] = @"The system could not dispose of the media in the requested manner.";
+    descriptions[@(YKF_SCARD_E_PROTO_MISMATCH)] = @"The requested protocols are incompatible with the protocol currently in use with the smart card.";
+    descriptions[@(YKF_SCARD_E_NOT_READY)] = @"The reader or smart card is not ready to accept commands.";
+    descriptions[@(YKF_SCARD_E_INVALID_VALUE)] = @"One or more of the supplied parameters values could not be properly interpreted.";
+    descriptions[@(YKF_SCARD_E_SYSTEM_CANCELLED)] = @"The action was cancelled by the system, presumably to log off or shut down.";
+    descriptions[@(YKF_SCARD_F_COMM_ERROR)] = @"An internal communications error has been detected.";
+    descriptions[@(YKF_SCARD_F_UNKNOWN_ERROR)] = @"An internal error has been detected, but the source is unknown.";
+    descriptions[@(YKF_SCARD_E_INVALID_ATR)] = @"An ATR obtained from the registry is not a valid ATR string.";
+    descriptions[@(YKF_SCARD_E_NOT_TRANSACTED)] = @"An attempt was made to end a non-existent transaction.";
+    descriptions[@(YKF_SCARD_E_READER_UNAVAILABLE)] = @"The specified reader is not currently available for use.";
+    descriptions[@(YKF_SCARD_P_SHUTDOWN)] = @"The operation has been aborted to allow the server application to exit.";
+    descriptions[@(YKF_SCARD_E_PCI_TOO_SMALL)] = @"The PCI Receive buffer was too small.";
+    descriptions[@(YKF_SCARD_E_READER_UNSUPPORTED)] = @"The reader driver does not meet minimal requirements for support.";
+    descriptions[@(YKF_SCARD_E_DUPLICATE_READER)] = @"The reader driver did not produce a unique reader name.";
+    descriptions[@(YKF_SCARD_E_CARD_UNSUPPORTED)] = @"The smart card does not meet minimal requirements for support.";
+    descriptions[@(YKF_SCARD_E_NO_SERVICE)] = @"The Smart card resource manager is not running.";
+    descriptions[@(YKF_SCARD_E_SERVICE_STOPPED)] = @"The Smart card resource manager has shut down.";
+    descriptions[@(YKF_SCARD_E_UNEXPECTED)] = @"An unexpected card error has occurred.";
+    descriptions[@(YKF_SCARD_E_ICC_INSTALLATION)] = @"No primary provider can be found for the smart card.";
+    descriptions[@(YKF_SCARD_E_ICC_CREATEORDER)] = @"The requested order of object creation is not supported.";
+    descriptions[@(YKF_SCARD_E_DIR_NOT_FOUND)] = @"The identified directory does not exist in the smart card.";
+    descriptions[@(YKF_SCARD_E_FILE_NOT_FOUND)] = @"The identified file does not exist in the smart card.";
+    descriptions[@(YKF_SCARD_E_NO_DIR)] = @"The supplied path does not represent a smart card directory.";
+    descriptions[@(YKF_SCARD_E_NO_FILE)] = @"The supplied path does not represent a smart card file.";
+    descriptions[@(YKF_SCARD_E_NO_ACCESS)] = @"Access is denied to this file.";
+    descriptions[@(YKF_SCARD_E_WRITE_TOO_MANY)] = @"The smart card does not have enough memory to store the information.";
+    descriptions[@(YKF_SCARD_E_BAD_SEEK)] = @"There was an error trying to set the smart card file object pointer.";
+    descriptions[@(YKF_SCARD_E_INVALID_CHV)] = @"The supplied PIN is incorrect.";
+    descriptions[@(YKF_SCARD_E_UNKNOWN_RES_MNG)] = @"An unrecognized error code was returned from a layered component.";
+    descriptions[@(YKF_SCARD_E_NO_SUCH_CERTIFICATE)] = @"The requested certificate does not exist.";
+    descriptions[@(YKF_SCARD_E_CERTIFICATE_UNAVAILABLE)] = @"The requested certificate could not be obtained.";
+    descriptions[@(YKF_SCARD_E_NO_READERS_AVAILABLE)] = @"Cannot find a smart card reader.";
+    descriptions[@(YKF_SCARD_E_COMM_DATA_LOST)] = @"A communications error with the smart card has been detected. Retry the operation.";
+    descriptions[@(YKF_SCARD_E_NO_KEY_CONTAINER)] = @"The requested key container does not exist on the smart card.";
+    descriptions[@(YKF_SCARD_E_SERVER_TOO_BUSY)] = @"The Smart Card Resource Manager is too busy to complete this operation.";
+    descriptions[@(YKF_SCARD_W_UNSUPPORTED_CARD)] = @"The reader cannot communicate with the card, due to ATR string configuration conflicts.";
+    descriptions[@(YKF_SCARD_E_UNSUPPORTED_FEATURE)] = @"This smart card does not support the requested feature.";
+    descriptions[@(YKF_SCARD_W_UNRESPONSIVE_CARD)] = @"The smart card is not responding to a reset.";
+    descriptions[@(YKF_SCARD_W_UNPOWERED_CARD)] = @"Power has been removed from the smart card, so that further communication is not possible.";
+    descriptions[@(YKF_SCARD_W_RESET_CARD)] = @"The smart card has been reset, so any shared state information is invalid.";
+    descriptions[@(YKF_SCARD_W_REMOVED_CARD)] = @"The smart card has been removed, so further communication is not possible.";
+    descriptions[@(YKF_SCARD_W_SECURITY_VIOLATION)] = @"Access was denied because of a security violation.";
+    descriptions[@(YKF_SCARD_W_WRONG_CHV)] = @"The card cannot be accessed because the wrong PIN was presented.";
+    descriptions[@(YKF_SCARD_W_CHV_BLOCKED)] = @"The card cannot be accessed because the maximum number of PIN entry attempts has been reached.";
+    descriptions[@(YKF_SCARD_W_EOF)] = @"The end of the smart card file has been reached.";
+    descriptions[@(YKF_SCARD_W_CANCELLED_BY_USER)] = @"The user pressed 'Cancel' on a Smart Card Selection Dialog.";
+    descriptions[@(YKF_SCARD_W_CARD_NOT_AUTHENTICATED)] = @"No PIN was presented to the smart card.";
+    
+    self.errorMap = descriptions;
+}
+
+@end
diff --git a/YubiKit/YubiKit/Layers/PCSC/YKFPCSCErrors.h b/YubiKit/YubiKit/Layers/PCSC/YKFPCSCErrors.h
new file mode 100755
index 000000000..8fe65b758
--- /dev/null
+++ b/YubiKit/YubiKit/Layers/PCSC/YKFPCSCErrors.h
@@ -0,0 +1,322 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+//  Adapted version of PC/SC interface for YubiKit.
+//  Error Codes as defined in https://docs.microsoft.com/en-us/windows/desktop/SecAuthN/authentication-return-values
+//
+
+/**
+ No error was encountered.
+ */
+#define YKF_SCARD_S_SUCCESS                 0x00000000
+
+/**
+ An internal consistency check failed.
+ */
+#define YKF_SCARD_F_INTERNAL_ERROR          0x80100001
+
+/**
+ The action was cancelled by an SCardCancel request.
+ */
+#define YKF_SCARD_E_CANCELLED               0x80100002
+
+/**
+ The supplied handle was invalid.
+ */
+#define YKF_SCARD_E_INVALID_HANDLE          0x80100003
+
+/**
+ One or more of the supplied parameters could not be properly interpreted.
+ */
+#define YKF_SCARD_E_INVALID_PARAMETER       0x80100004
+
+/**
+ Registry startup information is missing or invalid.
+ */
+#define YKF_SCARD_E_INVALID_TARGET          0x80100005
+
+/**
+ Not enough memory available to complete this command.
+ */
+#define YKF_SCARD_E_NO_MEMORY               0x80100006
+
+/**
+ An internal consistency timer has expired.
+ */
+#define YKF_SCARD_F_WAITED_TOO_LONG         0x80100007
+
+/**
+ The data buffer to receive returned data is too small for the returned data.
+ */
+#define YKF_SCARD_E_INSUFFICIENT_BUFFER     0x80100008
+
+/**
+ The specified reader name is not recognized.
+ */
+#define YKF_SCARD_E_UNKNOWN_READER          0x80100009
+
+/**
+ The user-specified timeout value has expired.
+ */
+#define YKF_SCARD_E_TIMEOUT                 0x8010000A
+
+/**
+ The smart card cannot be accessed because of other connections outstanding.
+ */
+#define YKF_SCARD_E_SHARING_VIOLATION       0x8010000B
+
+/**
+ The operation requires a Smart Card, but no Smart Card is currently in the device.
+ */
+#define YKF_SCARD_E_NO_SMARTCARD            0x8010000C
+
+/**
+ The specified smart card name is not recognized.
+ */
+#define YKF_SCARD_E_UNKNOWN_CARD            0x8010000D
+
+/**
+ The system could not dispose of the media in the requested manner.
+ */
+#define YKF_SCARD_E_CANT_DISPOSE            0x8010000E
+
+/**
+ The requested protocols are incompatible with the protocol currently in use with the smart card.
+ */
+#define YKF_SCARD_E_PROTO_MISMATCH          0x8010000F
+
+/**
+ The reader or smart card is not ready to accept commands.
+ */
+#define YKF_SCARD_E_NOT_READY               0x80100010
+
+/**
+ One or more of the supplied parameters values could not be properly interpreted.
+ */
+#define YKF_SCARD_E_INVALID_VALUE           0x80100011
+
+/**
+ The action was cancelled by the system, presumably to log off or shut down.
+ */
+#define YKF_SCARD_E_SYSTEM_CANCELLED        0x80100012
+
+/**
+ An internal communications error has been detected.
+ */
+#define YKF_SCARD_F_COMM_ERROR              0x80100013
+
+/**
+ An internal error has been detected, but the source is unknown.
+ */
+#define YKF_SCARD_F_UNKNOWN_ERROR           0x80100014
+
+/**
+ An ATR obtained from the registry is not a valid ATR string.
+ */
+#define YKF_SCARD_E_INVALID_ATR             0x80100015
+
+/**
+ An attempt was made to end a non-existent transaction.
+ */
+#define YKF_SCARD_E_NOT_TRANSACTED          0x80100016
+
+/**
+ The specified reader is not currently available for use.
+ */
+#define YKF_SCARD_E_READER_UNAVAILABLE      0x80100017
+
+/**
+ The operation has been aborted to allow the server application to exit.
+ */
+#define YKF_SCARD_P_SHUTDOWN                0x80100018
+
+/**
+ The PCI Receive buffer was too small.
+ */
+#define YKF_SCARD_E_PCI_TOO_SMALL           0x80100019
+
+/**
+ The reader driver does not meet minimal requirements for support.
+ */
+#define YKF_SCARD_E_READER_UNSUPPORTED      0x8010001A
+
+/**
+ The reader driver did not produce a unique reader name.
+ */
+#define YKF_SCARD_E_DUPLICATE_READER        0x8010001B
+
+/**
+ The smart card does not meet minimal requirements for support.
+ */
+#define YKF_SCARD_E_CARD_UNSUPPORTED        0x8010001C
+
+/**
+ The Smart card resource manager is not running.
+ */
+#define YKF_SCARD_E_NO_SERVICE              0x8010001D
+
+/**
+ The Smart card resource manager has shut down.
+ */
+#define YKF_SCARD_E_SERVICE_STOPPED         0x8010001E
+
+/**
+ An unexpected card error has occurred.
+ */
+#define YKF_SCARD_E_UNEXPECTED              0x8010001F
+
+/**
+ No primary provider can be found for the smart card.
+ */
+#define YKF_SCARD_E_ICC_INSTALLATION        0x80100020
+
+/**
+ The requested order of object creation is not supported.
+ */
+#define YKF_SCARD_E_ICC_CREATEORDER         0x80100021
+
+/**
+ The identified directory does not exist in the smart card.
+ */
+#define YKF_SCARD_E_DIR_NOT_FOUND           0x80100023
+
+/**
+ The identified file does not exist in the smart card.
+ */
+#define YKF_SCARD_E_FILE_NOT_FOUND          0x80100024
+
+/**
+ The supplied path does not represent a smart card directory.
+ */
+#define YKF_SCARD_E_NO_DIR                  0x80100025
+
+/**
+ The supplied path does not represent a smart card file.
+ */
+#define YKF_SCARD_E_NO_FILE                 0x80100026
+
+/**
+ Access is denied to this file.
+ */
+#define YKF_SCARD_E_NO_ACCESS               0x80100027
+
+/**
+ The smart card does not have enough memory to store the information.
+ */
+#define YKF_SCARD_E_WRITE_TOO_MANY          0x80100028
+
+/**
+ There was an error trying to set the smart card file object pointer.
+ */
+#define YKF_SCARD_E_BAD_SEEK                0x80100029
+
+/**
+ The supplied PIN is incorrect.
+ */
+#define YKF_SCARD_E_INVALID_CHV             0x8010002A
+
+/**
+ An unrecognized error code was returned from a layered component.
+ */
+#define YKF_SCARD_E_UNKNOWN_RES_MNG         0x8010002B
+
+/**
+ The requested certificate does not exist.
+ */
+#define YKF_SCARD_E_NO_SUCH_CERTIFICATE     0x8010002C
+
+/**
+ The requested certificate could not be obtained.
+ */
+#define YKF_SCARD_E_CERTIFICATE_UNAVAILABLE 0x8010002D
+
+/**
+ Cannot find a smart card reader.
+ */
+#define YKF_SCARD_E_NO_READERS_AVAILABLE    0x8010002E
+
+/**
+ A communications error with the smart card has been detected. Retry the operation.
+ */
+#define YKF_SCARD_E_COMM_DATA_LOST          0x8010002F
+
+/**
+ The requested key container does not exist on the smart card.
+ */
+#define YKF_SCARD_E_NO_KEY_CONTAINER        0x80100030
+
+/**
+ The Smart Card Resource Manager is too busy to complete this operation.
+ */
+#define YKF_SCARD_E_SERVER_TOO_BUSY         0x80100031
+
+/**
+ The reader cannot communicate with the card, due to ATR string configuration conflicts.
+ */
+#define YKF_SCARD_W_UNSUPPORTED_CARD        0x80100065
+
+/**
+ This smart card does not support the requested feature.
+ */
+#define YKF_SCARD_E_UNSUPPORTED_FEATURE     0x8010001F
+
+/**
+ The smart card is not responding to a reset.
+ */
+#define YKF_SCARD_W_UNRESPONSIVE_CARD       0x80100066
+
+/**
+ Power has been removed from the smart card, so that further communication is not possible.
+ */
+#define YKF_SCARD_W_UNPOWERED_CARD          0x80100067
+
+/**
+ The smart card has been reset, so any shared state information is invalid.
+ */
+#define YKF_SCARD_W_RESET_CARD              0x80100068
+
+/**
+ The smart card has been removed, so further communication is not possible.
+ */
+#define YKF_SCARD_W_REMOVED_CARD            0x80100069
+
+/**
+ Access was denied because of a security violation.
+ */
+#define YKF_SCARD_W_SECURITY_VIOLATION      0x8010006A
+
+/**
+ The card cannot be accessed because the wrong PIN was presented.
+ */
+#define YKF_SCARD_W_WRONG_CHV               0x8010006B
+
+/**
+ The card cannot be accessed because the maximum number of PIN entry attempts has been reached.
+ */
+#define YKF_SCARD_W_CHV_BLOCKED             0x8010006C
+
+/**
+ The end of the smart card file has been reached.
+ */
+#define YKF_SCARD_W_EOF                     0x8010006D
+
+/**
+ The user pressed "Cancel" on a Smart Card Selection Dialog.
+ */
+#define YKF_SCARD_W_CANCELLED_BY_USER       0x8010006E
+
+/**
+ No PIN was presented to the smart card.
+ */
+#define YKF_SCARD_W_CARD_NOT_AUTHENTICATED  0x8010006F
diff --git a/YubiKit/YubiKit/Layers/PCSC/YKFPCSCLayer.h b/YubiKit/YubiKit/Layers/PCSC/YKFPCSCLayer.h
new file mode 100755
index 000000000..7ff12ad3f
--- /dev/null
+++ b/YubiKit/YubiKit/Layers/PCSC/YKFPCSCLayer.h
@@ -0,0 +1,167 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+#import "YKFAccessorySession.h"
+
+/**
+ * ---------------------------------------------------------------------------------------------------------------------
+ * @name YKFPCSCLayerProtocol
+ * ---------------------------------------------------------------------------------------------------------------------
+ */
+
+@protocol YKFPCSCLayerProtocol<NSObject>
+
+@property (nonatomic, readonly) SInt32 cardState;
+@property (nonatomic, readonly, nullable) NSString *cardSerial;
+@property (nonatomic, readonly, nonnull) NSData *cardAtr;
+
+@property (nonatomic, readonly) SInt64 statusChange;
+
+@property (nonatomic, readonly, nullable) NSString *deviceFriendlyName;
+@property (nonatomic, readonly, nullable) NSString *deviceModelName;
+@property (nonatomic, readonly, nullable) NSString *deviceVendorName;
+
+/*!
+ Connects to the card. In the YubiKit context this opens the session with the key.
+ */
+- (SInt64)connectCard;
+
+/*!
+  Reconnects to the card. In the YubiKit context this reopens the session with the key.
+ */
+- (SInt64)reconnectCard;
+
+/*!
+ Disconnects the card. In the YubiKit context this closes the session with the key.
+ */
+- (SInt64)disconnectCard;
+
+/*!
+ Sends some data the card. In the YubiKit context this sends the data to the key.
+ */
+- (SInt64)transmit:(nonnull NSData *)commandData response:(NSData *_Nonnull*_Nullable)response;
+
+/*!
+ Returns the list of available readers. In the YubiKit context this returns the key name as a reader.
+ */
+- (SInt64)listReaders:(NSString *_Nonnull*_Nullable)yubikeyReaderName;
+
+/*!
+ Used by YKFPCSCStringifyError to create a human readable error from a defined code.
+ */
+- (nullable NSString *)stringifyError:(SInt64)errorCode;
+
+/*
+ Context and Card Tracking
+ */
+
+/*!
+ @abstract
+    Adds a new context to the layer. This happens when a new context is created from the PC/SC interface.
+ @returns
+    YES if the layer can store more contexts or no if the limit was exeeded (max 10).
+ */
+- (BOOL)addContext:(SInt32)context;
+
+/*!
+ @abstract
+    Removes an existing context from the layer. This happens when a context is released from the PC/SC interface.
+ @return
+    YES if the context was removed.
+ */
+- (BOOL)removeContext:(SInt32)context;
+
+/*!
+ @abstract
+    Adds a card which is associated with a context.
+ @return
+    YES if success.
+ */
+- (BOOL)addCard:(SInt32)card toContext:(SInt32)context;
+
+/*!
+ @abstract
+    Removes a card from its associated context.
+ @return
+    YES if success.
+ */
+- (BOOL)removeCard:(SInt32)card;
+
+/*!
+ @return
+    YES if the context is known by the layer, i.e. it was added using [addContext:].
+ */
+- (BOOL)contextIsValid:(SInt32)context;
+
+/*!
+ @return
+    YES if the card is known by the layer, i.e. it was added using [addCard:toContext:].
+ */
+- (BOOL)cardIsValid:(SInt32)card;
+
+/*!
+ @return
+    The context associated with the card if any. If no context is found returns 0.
+ */
+- (SInt32)contextForCard:(SInt32)card;
+
+@end
+
+/**
+ * ---------------------------------------------------------------------------------------------------------------------
+ * @name YKFPCSCLayer
+ * ---------------------------------------------------------------------------------------------------------------------
+ */
+
+@interface YKFPCSCLayer: NSObject<YKFPCSCLayerProtocol>
+
+/*!
+ Returns the shared instance of the layer.
+ */
+@property (class, nonatomic, readonly, nonnull) id<YKFPCSCLayerProtocol> shared;
+
+/*!
+ @abstract
+    Designated intialiser which will use the RawCommandService from the supplied session to
+    communicate with the key.
+ 
+ @param session
+    The session to be used by the layer when communicating with the key.
+ */
+- (nullable instancetype)initWithAccessorySession:(nonnull id<YKFAccessorySessionProtocol>)session NS_DESIGNATED_INITIALIZER;
+
+/*
+ Not available: use [initWithAccessorySession:]
+ */
+- (nonnull instancetype)init NS_UNAVAILABLE;
+
+@end
+
+/**
+ * ---------------------------------------------------------------------------------------------------------------------
+ * @name YKFPCSCLayer Testing Additions
+ * ---------------------------------------------------------------------------------------------------------------------
+ */
+
+#ifdef DEBUG
+
+@interface YKFPCSCLayer(/* Testing */)
+
+// Injected singleton by a unit test.
+@property (class, nonatomic, nullable) id<YKFPCSCLayerProtocol> fakePCSCLayer;
+
+@end
+
+#endif
diff --git a/YubiKit/YubiKit/Layers/PCSC/YKFPCSCLayer.m b/YubiKit/YubiKit/Layers/PCSC/YKFPCSCLayer.m
new file mode 100755
index 000000000..7307fc74a
--- /dev/null
+++ b/YubiKit/YubiKit/Layers/PCSC/YKFPCSCLayer.m
@@ -0,0 +1,320 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "YKFAccessoryConnectionController.h"
+#import "YubiKitManager.h"
+#import "YKFPCSCLayer.h"
+#import "YKFPCSCErrors.h"
+#import "YKFPCSCTypes.h"
+#import "YKFAssert.h"
+#import "YKFBlockMacros.h"
+#import "YKFPCSCErrorMap.h"
+#import "YKFLogger.h"
+#import "YKFAccessorySession+Private.h"
+#import "YKFNSDataAdditions+Private.h"
+
+static NSString* const YKFPCSCLayerReaderName = @"YubiKey";
+
+// YK5 ATR
+static const UInt8 YKFPCSCAtrSize = 23;
+static const UInt8 YKFPCSCAtr[] = {0x3b, 0xfd, 0x13, 0x00, 0x00, 0x81, 0x31, 0xfe, 0x15, 0x80, 0x73, 0xc0, 0x21, 0xc0, 0x57, 0x59, 0x75, 0x62, 0x69, 0x4b, 0x65, 0x79, 0x40};
+
+// Some constants to avoid too many unnecessary contexts in one app. Ideally the host app should
+// use a singleton to access the key, even when using PC/SC instead of replicating the same execution
+// code on multiple threads.
+static const NSUInteger YKFPCSCLayerContextLimit = 10;
+static const NSUInteger YKFPCSCLayerCardLimitPerContext = 10;
+
+
+@interface YKFPCSCLayer()
+
+@property (nonatomic) id<YKFAccessorySessionProtocol> accessorySession;
+@property (nonatomic) YKFPCSCErrorMap *errorMap;
+
+// Maps a context value to a list of card values
+@property (nonatomic) NSMutableDictionary<NSNumber*, NSMutableArray<NSNumber*>*> *contextMap;
+
+// Reverse lookup map between a card and a context.
+@property (nonatomic) NSMutableDictionary<NSNumber*, NSNumber*> *cardMap;
+
+@end
+
+
+@implementation YKFPCSCLayer
+
+@synthesize cardState;
+@synthesize cardSerial;
+@synthesize cardAtr;
+@synthesize statusChange;
+@synthesize deviceFriendlyName;
+@synthesize deviceModelName;
+@synthesize deviceVendorName;
+
+#pragma mark - Lifecycle
+
+static id<YKFPCSCLayerProtocol> sharedInstance;
+
++ (id<YKFPCSCLayerProtocol>)shared {
+#ifdef DEBUG
+    if (staticFakePCSCLayer) {
+        return staticFakePCSCLayer;
+    }
+#endif
+    static dispatch_once_t onceToken;
+    dispatch_once(&onceToken, ^{
+        sharedInstance = [[YKFPCSCLayer alloc] initWithAccessorySession:YubiKitManager.shared.accessorySession];
+    });
+    return sharedInstance;
+}
+
+- (instancetype)initWithAccessorySession:(id<YKFAccessorySessionProtocol>)session {
+    YKFAssertAbortInit(session);
+    
+    self = [super init];
+    if (self) {
+        self.accessorySession = session;        
+        self.contextMap = [[NSMutableDictionary alloc] init];
+        self.cardMap = [[NSMutableDictionary alloc] init];
+        self.errorMap = [[YKFPCSCErrorMap alloc] init];
+    }
+    return self;
+}
+
+#pragma mark - Property Overrides
+
+- (SInt32)cardState {
+    if (self.accessorySession.isKeyConnected) {
+        if (self.accessorySession.sessionState == YKFAccessorySessionStateOpen) {
+            return YKF_SCARD_SPECIFICMODE;
+        }
+        return YKF_SCARD_SWALLOWED;
+    }
+    return YKF_SCARD_ABSENT;
+}
+
+- (NSString *)cardSerial {
+    if (self.accessorySession.isKeyConnected) {
+        return self.accessorySession.accessoryDescription.serialNumber;
+    }
+    return nil;
+}
+
+- (NSData *)cardAtr {
+    return [NSData dataWithBytes:YKFPCSCAtr length:YKFPCSCAtrSize];
+}
+
+- (SInt64)statusChange {
+    if (self.accessorySession.isKeyConnected) {
+        return YKF_SCARD_STATE_PRESENT | YKF_SCARD_STATE_CHANGED;
+    }
+    return YKF_SCARD_STATE_EMPTY | YKF_SCARD_STATE_CHANGED;
+}
+
+- (NSString *)deviceFriendlyName {
+    if (self.accessorySession.isKeyConnected) {
+        return self.accessorySession.accessoryDescription.name;
+    }
+    return nil;
+}
+
+- (NSString *)deviceModelName {
+    if (self.accessorySession.isKeyConnected) {
+        return self.accessorySession.accessoryDescription.name;
+    }
+    return nil;
+}
+
+- (NSString *)deviceVendorName {
+    if (self.accessorySession.isKeyConnected) {
+        return self.accessorySession.accessoryDescription.manufacturer;
+    }
+    return nil;
+}
+
+#pragma mark - PC/SC
+
+- (SInt64)connectCard {
+    if (!self.accessorySession.isKeyConnected) {
+        return YKF_SCARD_E_NO_SMARTCARD;
+    }
+    
+    BOOL sessionOpened = [self.accessorySession startSessionSync];
+    return sessionOpened ? YKF_SCARD_S_SUCCESS : YKF_SCARD_F_WAITED_TOO_LONG;
+}
+
+- (SInt64)disconnectCard {
+    if (!self.accessorySession.isKeyConnected) {
+        return YKF_SCARD_E_NO_SMARTCARD;
+    }
+    
+    BOOL sessionClosed = [self.accessorySession stopSessionSync];
+    return sessionClosed ? YKF_SCARD_S_SUCCESS : YKF_SCARD_F_WAITED_TOO_LONG;
+}
+
+- (SInt64)reconnectCard {
+    SInt64 disconnectResult = [self disconnectCard];
+    if (disconnectResult != YKF_SCARD_S_SUCCESS) {
+        return disconnectResult;
+    }
+    return [self connectCard];
+}
+
+- (SInt64)transmit:(NSData *)commandData response:(NSData **)response {
+    YKFAssertReturnValue(self.accessorySession.sessionState == YKFAccessorySessionStateOpen, @"Session is closed. Cannot send command.", YKF_SCARD_E_READER_UNAVAILABLE);
+    YKFAssertReturnValue(commandData.length, @"The command data is empty.", YKF_SCARD_E_INVALID_PARAMETER);
+    
+    YKFAPDU *command = [[YKFAPDU alloc] initWithData:commandData];
+    YKFAssertReturnValue(command, @"Could not create APDU with data.", YKF_SCARD_E_INVALID_PARAMETER);
+
+    __block NSData *responseData = nil;
+    
+    [self.accessorySession.rawCommandService executeSyncCommand:command completion:^(NSData *resp, NSError *error) {
+        if (!error && resp) {
+            responseData = resp;
+        }
+    }];
+    
+    if (responseData) {
+        *response = responseData;
+        return YKF_SCARD_S_SUCCESS;
+    }
+    
+    return YKF_SCARD_F_WAITED_TOO_LONG;
+}
+
+- (SInt64)listReaders:(NSString **)yubikeyReaderName {
+    if (self.accessorySession.isKeyConnected) {
+        *yubikeyReaderName = YKFPCSCLayerReaderName;
+        return YKF_SCARD_S_SUCCESS;
+    }
+    return YKF_SCARD_E_NO_READERS_AVAILABLE;
+}
+
+#pragma mark - Context and Card tracking helpers
+
+- (BOOL)addContext:(SInt32)context {
+    @synchronized (self.contextMap) {
+        if (self.contextMap.allKeys.count >= YKFPCSCLayerContextLimit) {
+            YKFLogError(@"PC/SC - Could not establish context %d. Too many contexts started by the application.", (int)context);
+            return NO;
+        }
+        NSMutableArray<NSNumber*> *contextCards = [[NSMutableArray alloc] init];
+        self.contextMap[@(context)] = contextCards;
+        
+        YKFLogInfo(@"PC/SC - Context %d established.", (int)context);
+        return YES;
+    }
+}
+
+- (BOOL)removeContext:(SInt32)context {
+    @synchronized (self.contextMap) {
+        if (!self.contextMap[@(context)]) {
+            YKFLogError(@"PC/SC - Could not release context %d. Unknown context.", (int)context);
+            return NO;
+        }
+        
+        NSMutableArray<NSNumber*> *associatedCards = self.contextMap[@(context)];
+        [self.contextMap removeObjectForKey:@(context)];
+        
+        @synchronized (self.cardMap) {
+            ykf_weak_self();
+            [associatedCards enumerateObjectsUsingBlock:^(NSNumber *obj, NSUInteger idx, BOOL *stop) {
+                [weakSelf.cardMap removeObjectForKey:obj];
+            }];
+            
+            YKFLogInfo(@"PC/SC - Context %d released.", (int)context);
+            return YES;
+        }
+    }
+}
+
+- (BOOL)addCard:(SInt32)card toContext:(SInt32)context {
+    if (![self contextIsValid:context]) {
+        // YKFLogError(@"PC/SC - Could not use context %d. Unknown context.", context);
+        return NO;
+    }
+    
+    @synchronized (self.contextMap) {
+        if (self.contextMap[@(context)].count >= YKFPCSCLayerCardLimitPerContext) {
+            // YKFLogError(@"PC/SC - Could not connect to card %d in context %d. Too many cards per context.", card, context);
+            return NO;
+        }
+        [self.contextMap[@(context)] addObject:@(card)];
+    }
+    @synchronized (self.cardMap) {
+        self.cardMap[@(card)] = @(context);
+    }
+    
+    // YKFLogInfo(@"PC/SC - Connected to card %d in context %d.", card, context);
+    return YES;
+}
+
+- (BOOL)removeCard:(SInt32)card {    
+    if (![self cardIsValid:card]) {
+        // YKFLogError(@"PC/SC - Could not disconnect from card %d. Unknown card.", card);
+        return NO;
+    }
+
+    @synchronized (self.cardMap) {
+        NSNumber *context = self.cardMap[@(card)];
+        [self.cardMap removeObjectForKey:@(card)];
+        
+        @synchronized (self.contextMap) {
+            [self.contextMap[context] removeObject:@(card)];
+        }
+        
+        // YKFLogInfo(@"PC/SC - Disconnected from card %d.", card);
+        return YES;
+    }
+}
+
+- (BOOL)contextIsValid:(SInt32)context {
+    @synchronized (self.contextMap) {
+        return self.contextMap[@(context)] != nil;
+    }
+}
+
+- (BOOL)cardIsValid:(SInt32)card {
+    @synchronized (self.cardMap) {
+        return self.cardMap[@(card)] != nil;
+    }
+}
+
+- (SInt32)contextForCard:(SInt32)card {
+    @synchronized (self.cardMap) {
+        return self.cardMap[@(card)] != nil ? self.cardMap[@(card)].intValue : 0;
+    }
+}
+
+- (NSString *)stringifyError:(SInt64)errorCode {
+    return [self.errorMap errorForCode:errorCode];
+}
+
+#pragma mark - Testing additions
+
+#ifdef DEBUG
+
+static id<YKFPCSCLayerProtocol> staticFakePCSCLayer;
+
++ (void)setFakePCSCLayer:(id<YKFPCSCLayerProtocol>)fakePCSCLayer {
+    staticFakePCSCLayer = fakePCSCLayer;
+}
+
++ (id<YKFPCSCLayerProtocol>)fakePCSCLayer {
+    return staticFakePCSCLayer;
+}
+
+#endif
+
+@end
diff --git a/YubiKit/YubiKit/Layers/PCSC/YKFPCSCTypes.h b/YubiKit/YubiKit/Layers/PCSC/YKFPCSCTypes.h
new file mode 100755
index 000000000..f78492aec
--- /dev/null
+++ b/YubiKit/YubiKit/Layers/PCSC/YKFPCSCTypes.h
@@ -0,0 +1,120 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+
+static const UInt8 YKF_MAX_ATR_SIZE = 33;
+
+/*!
+ @abstract
+    Adapted version of SCARD_READERSTATE for YubiKit.
+    For more details of the original API check https://pcsclite.apdu.fr/api/structSCARD__READERSTATE.html
+ */
+typedef struct {
+    const char *reader;
+    void *userData;
+    UInt32 currentState;
+    UInt32 eventState;
+    UInt32 atr;
+    unsigned char rgbAtr[YKF_MAX_ATR_SIZE];
+}
+YKF_SCARD_READERSTATE;
+
+/*!
+ @abstract
+    Adapted version of SCARD_IO_REQUEST for YubiKit.
+    For more details of the original API check https://pcsclite.apdu.fr/api/structSCARD__IO__REQUEST.html
+ */
+typedef struct {
+    UInt32 protocol;
+    UInt32 pciLength;
+}
+YKF_SCARD_IO_REQUEST;
+
+/*!
+ Scope is in user space.
+ */
+static const UInt32 YKF_SCARD_SCOPE_USER = 0x0000;
+
+/*!
+ T=1 active protocol.
+ */
+static const UInt32 YKF_SCARD_PROTOCOL_T1 = 0x0002;
+
+/*!
+ Exclusive mode only.
+ */
+static const UInt32 YKF_SCARD_SHARE_EXCLUSIVE = 0x0001;
+
+/*!
+ The application wants the state of the card.
+ */
+static const UInt32 YKF_SCARD_STATE_UNAWARE  = 0x0000;
+
+/*!
+ The state of the card has changed.
+ */
+static const UInt32 YKF_SCARD_STATE_CHANGED = 0x0002;
+
+/*!
+ The card is not available.
+ In the YubiKit context this means that the key is not connected.
+ */
+static const UInt32 YKF_SCARD_STATE_EMPTY = 0x0010;
+
+/*!
+ The card is available.
+ In the YubiKit context this means that the key is connected.
+ */
+static const UInt32 YKF_SCARD_STATE_PRESENT = 0x0020;
+
+/*!
+ Do nothing on close.
+ */
+static const UInt32 YKF_SCARD_LEAVE_CARD = 0x0000;
+
+/*!
+ There is no card in the reader.
+ */
+static const UInt32 YKF_SCARD_ABSENT = 0x0001;
+
+/*!
+ There is a card in the reader in position for use. The card is not powered.
+ */
+static const UInt32 YKF_SCARD_SWALLOWED = 0x0003;
+
+/*!
+ The card has been reset and specific communication protocols have been established.
+ */
+static const UInt32 YKF_SCARD_SPECIFICMODE = 0x0006;
+
+/*!
+ Reader's display name.
+ */
+static const UInt32 YKF_SCARD_ATTR_DEVICE_FRIENDLY_NAME = 0x7FFF0003;
+
+/*!
+ Vendor-supplied interface device serial number.
+ */
+static const UInt32 YKF_SCARD_ATTR_VENDOR_IFD_SERIAL_NO = 0x00010103;
+
+/*!
+ Vendor-supplied interface device type (model designation of reader).
+ */
+static const UInt32 YKF_SCARD_ATTR_VENDOR_IFD_TYPE = 0x00010101;
+
+/*!
+ Vendor name.
+ */
+static const UInt32 YKF_SCARD_ATTR_VENDOR_NAME = 0x00010100;
diff --git a/YubiKit/YubiKit/Sessions/AccessorySession/YKFAccessoryConnectionController.h b/YubiKit/YubiKit/Sessions/AccessorySession/YKFAccessoryConnectionController.h
new file mode 100755
index 000000000..b3026c4ef
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/AccessorySession/YKFAccessoryConnectionController.h
@@ -0,0 +1,32 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+#import <ExternalAccessory/ExternalAccessory.h>
+
+#import "YKFKeyCommandConfiguration.h"
+#import "YKFKeyConnectionControllerProtocol.h"
+#import "YKFAPDU.h"
+#import "EASession+Testing.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface YKFAccessoryConnectionController : NSObject<YKFKeyConnectionControllerProtocol>
+
+- (nullable instancetype)initWithSession:(id<YKFEASessionProtocol>)session operationQueue:(NSOperationQueue *)operationQueue NS_DESIGNATED_INITIALIZER;
+- (instancetype)init NS_UNAVAILABLE;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/YubiKit/YubiKit/Sessions/AccessorySession/YKFAccessoryConnectionController.m b/YubiKit/YubiKit/Sessions/AccessorySession/YKFAccessoryConnectionController.m
new file mode 100755
index 000000000..750120519
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/AccessorySession/YKFAccessoryConnectionController.m
@@ -0,0 +1,410 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "YKFAccessoryConnectionController.h"
+#import "YKFKeyAPDUError.h"
+#import "YKFLogger.h"
+#import "YKFDispatch.h"
+#import "YKFBlockMacros.h"
+#import "YKFAssert.h"
+
+#import "YKFNSDataAdditions+Private.h"
+#import "YKFKeySessionError+Private.h"
+#import "YKFAPDU+Private.h"
+
+typedef void (^YKFKeyConnectionControllerCommunicationQueueBlock)(NSOperation *operation);
+
+@interface YKFAccessoryConnectionController()
+
+@property (nonatomic) NSOperationQueue *communicationQueue;
+@property (nonatomic) NSMutableDictionary *delayedDispatches;
+
+@property (nonatomic) NSInputStream *inputStream;
+@property (nonatomic) NSOutputStream *outputStream;
+@property (nonatomic) NSThread *streamsThread;
+
+@end
+
+@implementation YKFAccessoryConnectionController
+
+static NSUInteger const YubiKeyConnectionControllerReadBufferSize = 512; // bytes
+
+- (instancetype)initWithSession:(id<YKFEASessionProtocol>)session operationQueue:(NSOperationQueue *)operationQueue {
+    YKFAssertAbortInit(session);
+    YKFAssertAbortInit(operationQueue);
+    
+    self = [super init];
+    if (self) {
+        self.communicationQueue = operationQueue;
+        self.inputStream = session.inputStream;
+        self.outputStream = session.outputStream;
+        
+        YKFAssertAbortInit(self.inputStream);
+        YKFAssertAbortInit(self.outputStream);
+        
+        self.delayedDispatches = [[NSMutableDictionary alloc] init];
+        
+        self.streamsThread = [[NSThread alloc] initWithTarget: self selector:@selector(streamsThreadExecution) object:nil];
+        [self.streamsThread start];
+        
+        ykf_weak_self();
+        [self dispatchBlockOnCommunicationQueue:^(NSOperation *operation){
+            ykf_safe_strong_self();
+            [strongSelf setupConnectionInputStream:strongSelf.inputStream outputStream:strongSelf.outputStream];
+        }];
+    }
+    return self;
+}
+
+- (void)closeConnectionWithCompletion:(YKFKeyConnectionControllerCompletionBlock)completionBlock {
+    YKFParameterAssertReturn(completionBlock);
+    
+    [self cancelAllCommands];
+    
+    ykf_weak_self();
+    [self dispatchBlockOnCommunicationQueue:^(NSOperation *operation){
+        ykf_safe_strong_self();
+        [strongSelf closeConnectionInputStream:strongSelf.inputStream outputStream:strongSelf.outputStream];
+        completionBlock();
+    }];
+}
+
+#pragma mark - Dispatching
+
+- (void)dispatchBlockOnCommunicationQueue:(YKFKeyConnectionControllerCommunicationQueueBlock)block {
+    YKFParameterAssertReturn(block);
+    
+    NSBlockOperation *operation = [[NSBlockOperation alloc] init];
+    __weak NSBlockOperation *weakOperation = operation;
+    
+    [operation addExecutionBlock:^{
+        __strong NSBlockOperation *strongOperation = weakOperation;
+        if (!strongOperation || strongOperation.isCancelled) {
+            return;
+        }
+        block(strongOperation); // Execute the operation if it's still alive and not canceled.
+    }];
+    
+    [self.communicationQueue addOperation:operation];
+}
+
+- (void)streamsThreadExecution {
+    YKFAssertOffMainThread();
+    
+    // This adds a dummy port to keep the run look alive so the accessory input/output
+    // streams will be able to be attached to the existing run loop and receive events.
+    NSPort* keepAlivePort = [NSPort port];
+    [keepAlivePort scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
+    CFRunLoopRun();
+    [keepAlivePort removeFromRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
+}
+
+#pragma mark - Stream setup
+
+- (void)setupConnectionInputStream:(NSInputStream *)inputStream outputStream:(NSOutputStream *)outputStream {
+    YKFAssertOffMainThread();
+    YKFAssertReturn(self.streamsThread, @"Cannot start communication streams. Thread not available.");
+    
+    // Streams are opened on a dedicated background thread with its own runloop to avoid
+    // the delays from the main thread when the UI work is intensive on low end devices.
+    ykf_dispatch_thread_async(self.streamsThread, ^{
+        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
+        
+        [inputStream scheduleInRunLoop:runLoop forMode:NSDefaultRunLoopMode];
+        [inputStream open];
+        
+        [outputStream scheduleInRunLoop:runLoop forMode:NSDefaultRunLoopMode];
+        [outputStream open];
+        
+        YKFLogInfo(@"YubiKey communication streams opened.");
+    });
+}
+
+- (void)closeConnectionInputStream:(NSInputStream *)inputStream outputStream:(NSOutputStream *)outputStream {
+    YKFAssertOffMainThread();
+    
+    // The streams must be closed on the same runloop as the one they started on.
+    ykf_dispatch_thread_async(self.streamsThread, ^{
+        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
+        
+        if (inputStream.streamStatus != NSStreamStatusClosed) {
+            [inputStream close];
+        }
+        [inputStream removeFromRunLoop:runLoop forMode:NSDefaultRunLoopMode];
+        
+        if (outputStream.streamStatus != NSStreamStatusClosed) {
+            [outputStream close];
+        }
+        [outputStream removeFromRunLoop:runLoop forMode:NSDefaultRunLoopMode];
+        
+        CFRunLoopStop(CFRunLoopGetCurrent());
+        
+        YKFLogInfo(@"YubiKey communication streams closed.");
+    });
+}
+
+#pragma mark - Stream IO
+
+- (BOOL)writeData:(NSData *)data configuration:(YKFKeyCommandConfiguration *)configuration parentOperation:(NSOperation *)operation {
+    YKFAssertOffMainThread();
+    
+    YKFParameterAssertReturnValue(data, NO);
+    YKFParameterAssertReturnValue(self.outputStream, NO);
+    
+    NSMutableData *writeData = [data mutableCopy];
+    NSTimeInterval totalSleepTime = 0;
+    
+    while (writeData.length > 0 && !operation.isCancelled) {
+        while (self.outputStream.hasSpaceAvailable && writeData.length > 0 && !operation.isCancelled) {
+            NSInteger bytesWritten = [self.outputStream write:writeData.bytes maxLength:writeData.length];
+            if (bytesWritten > 0) {
+                [writeData replaceBytesInRange:NSMakeRange(0, bytesWritten) withBytes:NULL length:0];
+            } else if (bytesWritten == -1) { // Write error.
+                return NO;
+            }
+        }
+        
+        [NSThread sleepForTimeInterval: configuration.commandProbeTime];
+        totalSleepTime += configuration.commandProbeTime;
+        if (totalSleepTime >= configuration.commandTimeout) {
+            return NO;
+        }
+    }
+    
+    if (operation.isCancelled) {
+        return  NO;
+    }
+    
+    return YES;
+}
+
+- (BOOL)readData:(NSData**)readData configuration:(YKFKeyCommandConfiguration *)configuration parentOperation:(NSOperation *)operation {
+    YKFAssertOffMainThread();
+    YKFParameterAssertReturnValue(self.inputStream, NO);
+    
+    NSMutableData *buffer = [[NSMutableData alloc] init];
+    UInt8 readBuffer[YubiKeyConnectionControllerReadBufferSize];
+    
+    NSTimeInterval totalSleepTime = 0;
+    while (!self.inputStream.hasBytesAvailable && !operation.isCancelled) {
+        [NSThread sleepForTimeInterval: configuration.commandProbeTime];
+        totalSleepTime += configuration.commandProbeTime;
+        if (totalSleepTime >= configuration.commandTimeout) {
+            return NO;
+        }
+    }
+    
+    if (operation.isCancelled) {
+        return NO;
+    }
+    
+    // Read the data while available.
+    while (self.inputStream.hasBytesAvailable) {
+        NSInteger bytesRead = [self.inputStream read:readBuffer maxLength:YubiKeyConnectionControllerReadBufferSize];
+        if (bytesRead > 0) {
+            [buffer appendBytes:readBuffer length:bytesRead];
+        } else if (bytesRead == -1) { // Read error.
+            return NO;
+        }
+    }
+    
+    *readData = [buffer copy];
+    
+    return YES;
+}
+
+#pragma mark - Commands
+
+- (void)execute:(YKFAPDU *)command completion:(YKFKeyConnectionControllerCommandResponseBlock)completion {
+    [self execute:command configuration:[YKFKeyCommandConfiguration defaultCommandCofiguration] completion:completion];
+}
+
+- (void)execute:(YKFAPDU *)command configuration:(YKFKeyCommandConfiguration *)configuration completion:(YKFKeyConnectionControllerCommandResponseBlock)completion {
+    YKFParameterAssertReturn(command);
+    YKFParameterAssertReturn(configuration);
+    YKFParameterAssertReturn(completion);
+    
+    YKFLogVerbose(@"AccessoryConnectionController - Execute command...");
+    
+    ykf_weak_self();
+    [self dispatchBlockOnCommunicationQueue:^(NSOperation *operation) {
+        ykf_safe_strong_self();
+        NSDate *commandStartDate = [NSDate date];
+        
+        // 1. Send the command to the key.
+        BOOL success = [strongSelf writeData:command.ylpApduData configuration:configuration parentOperation:operation];
+        
+        if (!success && !operation.isCancelled) {
+            NSError *error = nil;
+            if (strongSelf.outputStream.streamError) {
+                error = [strongSelf.outputStream.streamError copy];
+            } else {
+                error = [YKFKeySessionError errorWithCode:YKFKeySessionErrorWriteTimeoutCode];
+            }
+            
+            NSTimeInterval executionTime = [[NSDate date] timeIntervalSinceDate: commandStartDate];
+            completion(nil, error, executionTime);
+            return;
+        }
+
+        // Do not wait for the command to process if the operation was canceled.
+        if (operation.isCancelled) {
+            return;
+        }
+
+        BOOL keyIsBusyProcesssing = YES;
+        NSData *commandResult = nil;
+
+        while (keyIsBusyProcesssing) {
+            // 2. Wait for the key to process the command.
+            if (configuration.commandTime > 0) {
+                [NSThread sleepForTimeInterval: configuration.commandTime];
+            }
+            
+            // 3. Read the command result.
+            success = [strongSelf readData:&commandResult configuration:configuration parentOperation:operation];
+            
+            if ((!success || commandResult.length == 0) && !operation.isCancelled) {
+                NSError *error = nil;
+                if (strongSelf.inputStream.streamError) {
+                    error = [strongSelf.inputStream.streamError copy];
+                } else {
+                    error = [YKFKeySessionError errorWithCode:YKFKeySessionErrorReadTimeoutCode];
+                }
+                
+                NSTimeInterval executionTime = [[NSDate date] timeIntervalSinceDate: commandStartDate];
+                completion(nil, error, executionTime);
+                return;
+            }
+            
+            // Do not notify if the operation was canceled.
+            if (operation.isCancelled) {
+                return;
+            }
+            
+            keyIsBusyProcesssing = [strongSelf isKeyBusyProcessingResult:commandResult];
+            if (keyIsBusyProcesssing) {
+                YKFLogVerbose(@"The key is busy, processing the request. Waiting for response...");
+            }
+        }
+        
+        NSTimeInterval executionTime = [[NSDate date] timeIntervalSinceDate: commandStartDate];
+        commandResult = [self dataAndStatusFromKeyResponse:commandResult];
+        
+        completion(commandResult, nil, executionTime);
+        
+        YKFLogVerbose(@"Command execution time: %lf seconds", executionTime);
+    }];
+}
+
+- (void)dispatchOnSequentialQueue:(YKFKeyConnectionControllerCompletionBlock)block delay:(NSTimeInterval)delay {
+    dispatch_queue_t sharedDispatchQueue = self.communicationQueue.underlyingQueue;
+    
+    YKFParameterAssertReturn(sharedDispatchQueue);
+    YKFParameterAssertReturn(block);
+
+    block = [block copy]; // heap block
+    
+    if (delay == 0) {
+        dispatch_async(sharedDispatchQueue, block);
+    } else {
+        NSString *blockId = [NSUUID UUID].UUIDString;
+        
+        ykf_weak_self();
+        dispatch_block_t delayedBlock = dispatch_block_create(0, ^{
+            ykf_safe_strong_self();
+            dispatch_block_t blockReference = strongSelf.delayedDispatches[blockId];
+            strongSelf.delayedDispatches[blockId] = nil;
+            
+            // In case the block started already to run.
+            if (blockReference && dispatch_block_testcancel(blockReference)) {
+                return;
+            }
+            
+            block();
+        });
+        
+        self.delayedDispatches[blockId] = delayedBlock;
+        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delay * NSEC_PER_SEC)), sharedDispatchQueue, delayedBlock);
+    }
+}
+
+- (void)dispatchOnSequentialQueue:(YKFKeyConnectionControllerCompletionBlock)block {
+    YKFParameterAssertReturn(block);
+    [self dispatchOnSequentialQueue:block delay:0];
+}
+
+- (void)cancelAllCommands {
+    self.communicationQueue.suspended = YES;
+    dispatch_suspend(self.communicationQueue.underlyingQueue);
+    
+    [self.communicationQueue cancelAllOperations];
+    
+    NSArray *keys = self.delayedDispatches.allKeys;
+    for (NSString *key in keys) {
+        dispatch_block_t block = self.delayedDispatches[key];
+        dispatch_block_cancel(block);
+    };
+    [self.delayedDispatches removeAllObjects];
+    
+    dispatch_resume(self.communicationQueue.underlyingQueue);
+    self.communicationQueue.suspended = NO;
+}
+
+#pragma mark - Helpers
+
+/*
+ Returns YES if the key returned a status code with the header 0x01 (key is busy processing the request).
+ This status code is usually returned by CCID operations which require time to
+ complete like a certificate generation. When the key will finish to process the request it will send
+ again a new response, 0x9000 if the processig was successful or error code.
+ 
+ These responses should be received periodically (~500ms) while the key is processing.
+ */
+- (BOOL)isKeyBusyProcessingResult:(NSData *)result {
+    YKFParameterAssertReturnValue(result, NO);
+    YKFParameterAssertReturnValue(result.length >= 3, NO);
+    
+    UInt8 headerByte;
+    [result getBytes:&headerByte length:1];
+    
+    // BUG #62 - Workaround for WTX == 0x01 while status is 0x9000 (success).
+    BOOL statusIsSuccess = [result ykf_getBigEndianIntegerInRange:NSMakeRange(result.length - 2, 2)] == YKFKeyAPDUErrorCodeNoError;
+    // ~
+    
+    return headerByte == 0x01 && !statusIsSuccess;
+}
+
+- (NSData *)dataAndStatusFromKeyResponse:(NSData *)response {
+    YKFParameterAssertReturnValue(response, [NSData data]);
+    YKFAssertReturnValue(response.length >= 3, @"Key response data is too short.", [NSData data]);
+    
+    UInt8 *bytes = (UInt8 *)response.bytes;
+    YKFParameterAssertReturnValue(bytes[0] == 0x00 || bytes[0] == 0x01, [NSData data]);
+    
+    if (bytes[0] == 0x00) {
+        // Remove the first byte (the YLP key protocol header)
+        NSRange range = {1, response.length - 1};
+        return [response subdataWithRange:range];
+    }
+    else if (bytes[0] == 0x01) {
+        // Remove the first byte (the YLP key protocol header) and the WTX
+        NSRange range = {4, response.length - 4};
+        return [response subdataWithRange:range];
+    }
+    
+    return [NSData data];
+}
+
+@end
diff --git a/YubiKit/YubiKit/Sessions/AccessorySession/YKFAccessoryDescription+Private.h b/YubiKit/YubiKit/Sessions/AccessorySession/YKFAccessoryDescription+Private.h
new file mode 100755
index 000000000..b7c5cafed
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/AccessorySession/YKFAccessoryDescription+Private.h
@@ -0,0 +1,24 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <ExternalAccessory/ExternalAccessory.h>
+#import <Foundation/Foundation.h>
+#import "YKFAccessoryDescription.h"
+#import "EAAccessory+Testing.h"
+
+@interface YKFAccessoryDescription()
+
+- (nullable instancetype)initWithAccessory:(nonnull id<YKFEAAccessoryProtocol>)accessory;
+
+@end
diff --git a/YubiKit/YubiKit/Sessions/AccessorySession/YKFAccessoryDescription.h b/YubiKit/YubiKit/Sessions/AccessorySession/YKFAccessoryDescription.h
new file mode 100755
index 000000000..45b197315
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/AccessorySession/YKFAccessoryDescription.h
@@ -0,0 +1,83 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+/*!
+ @class YKFAccessoryDescription
+ 
+ @abstract
+    Provides a list of properties describing the connected key.
+ */
+@interface YKFAccessoryDescription : NSObject
+
+/*!
+ @property manufacturer
+ 
+ @abstract
+    The manufacturer of the key. YubiKit is designed to connect only to Yubico keys. The value of this property
+    should be always Yubico.
+ */
+@property(nonatomic, readonly) NSString *manufacturer;
+
+/*!
+ @property name
+ 
+ @abstract
+    The name of the key (e.g. YubiKey 5Ci, etc.).
+ */
+@property(nonatomic, readonly) NSString *name;
+
+/*!
+ @property modelNumber
+ 
+ @abstract
+    The model of the key. This property gives more details about the specific subtype of key (e.g. Neo, etc.).
+ */
+@property(nonatomic, readonly) NSString *modelNumber;
+
+/*!
+ @property serialNumber
+ 
+ @abstract
+    The unique serial number of the key.
+ */
+@property(nonatomic, readonly) NSString *serialNumber;
+
+/*!
+ @property firmwareRevision
+ 
+ @abstract
+    The firmware version of the key (e.g. 1.0.0, etc.)
+ */
+@property(nonatomic, readonly) NSString *firmwareRevision;
+
+/*!
+ @property hardwareRevision
+ 
+ @abstract
+    The hardware revision of the key (e.g. r1, r2, etc.)
+ */
+@property(nonatomic, readonly) NSString *hardwareRevision;
+
+/*
+ Not available: access the instance provided by YKFAccessorySession.
+ */
+- (instancetype)init NS_UNAVAILABLE;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/YubiKit/YubiKit/Sessions/AccessorySession/YKFAccessoryDescription.m b/YubiKit/YubiKit/Sessions/AccessorySession/YKFAccessoryDescription.m
new file mode 100755
index 000000000..ad0d60093
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/AccessorySession/YKFAccessoryDescription.m
@@ -0,0 +1,73 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "YKFAccessoryDescription.h"
+#import "YKFAssert.h"
+#import "YKFAccessoryDescription+Private.h"
+
+@interface YKFAccessoryDescription()
+
+@property(nonatomic, readwrite) NSString *manufacturer;
+@property(nonatomic, readwrite) NSString *name;
+@property(nonatomic, readwrite) NSString *modelNumber;
+@property(nonatomic, readwrite) NSString *serialNumber;
+@property(nonatomic, readwrite) NSString *firmwareRevision;
+@property(nonatomic, readwrite) NSString *hardwareRevision;
+
+@end
+
+@implementation YKFAccessoryDescription
+
+- (instancetype)initWithAccessory:(id<YKFEAAccessoryProtocol>)accessory {
+    YKFAssertAbortInit(accessory);
+    
+    self = [super init];
+    if (self) {
+        NSAssert(accessory.manufacturer, @"Manufacturer not provided by the accessory.");
+        self.manufacturer = accessory.manufacturer;
+        YKFAssertAbortInit(self.manufacturer);
+        
+        NSAssert(accessory.name, @"Name not provided by the accessory.");
+        self.name = accessory.name;
+        YKFAssertAbortInit(self.name);
+        
+        NSAssert(accessory.modelNumber, @"Model number not provided by the accessory.");
+        self.modelNumber = accessory.modelNumber;
+        YKFAssertAbortInit(self.modelNumber);
+        
+        NSAssert(accessory.firmwareRevision, @"Firmware revision not provided by the accessory.");
+        self.firmwareRevision = accessory.firmwareRevision;
+        YKFAssertAbortInit(self.firmwareRevision);
+        
+        // The production firmware should always have only 3 number chars.
+        if (self.firmwareRevision.length == 3 && [self.firmwareRevision integerValue]) {
+            NSMutableString *updatedFirmware = [[NSMutableString alloc] initWithString:self.firmwareRevision];
+            [updatedFirmware insertString:@"." atIndex:2];
+            [updatedFirmware insertString:@"." atIndex:1];
+            self.firmwareRevision = [updatedFirmware copy];
+        }
+        
+        NSAssert(accessory.hardwareRevision, @"Hardware revision not provided by the accessory.");
+        self.hardwareRevision = accessory.hardwareRevision;
+        YKFAssertAbortInit(self.hardwareRevision);
+        
+        NSAssert(accessory.serialNumber, @"Serial number not provided by the accessory.");
+        self.serialNumber = accessory.serialNumber;
+        YKFAssertAbortInit(self.serialNumber);
+    }
+    
+    return self;
+}
+
+@end
diff --git a/YubiKit/YubiKit/Sessions/AccessorySession/YKFAccessorySession+Debugging.h b/YubiKit/YubiKit/Sessions/AccessorySession/YKFAccessorySession+Debugging.h
new file mode 100755
index 000000000..dbe092d6f
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/AccessorySession/YKFAccessorySession+Debugging.h
@@ -0,0 +1,30 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+#import "YKFAccessorySession.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+#ifdef DEBUG
+
+@interface YKFAccessorySession(Debugging)
+
+- (void)checkApplicationConfiguration;
+
+@end
+
+#endif
+
+NS_ASSUME_NONNULL_END
diff --git a/YubiKit/YubiKit/Sessions/AccessorySession/YKFAccessorySession+Debugging.m b/YubiKit/YubiKit/Sessions/AccessorySession/YKFAccessorySession+Debugging.m
new file mode 100755
index 000000000..8dba9b663
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/AccessorySession/YKFAccessorySession+Debugging.m
@@ -0,0 +1,47 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "YKFAccessorySession+Debugging.h"
+#import "YKFLogger.h"
+
+#ifdef DEBUG
+
+@implementation YKFAccessorySession(Debugging)
+
+- (void)checkApplicationConfiguration {
+    // Search for the app bundle instead of using mainBundle to retrieve it when the library may be used inside frameworks.
+    NSArray *bundlesArray = NSBundle.allBundles;
+    NSBundle *applicationBundle = nil;
+    for (NSBundle *bundle in bundlesArray) {
+        NSString *bundlePath = bundle.bundlePath;
+        if ([bundlePath hasSuffix:@".app"]) {
+            applicationBundle = bundle;
+            break;
+        }
+    }
+    if (!applicationBundle) {
+        YKFLogError(@"Could not locate the application bundle.");
+    } else {
+        NSArray *plistAccessoryProtocols = applicationBundle.infoDictionary[@"UISupportedExternalAccessoryProtocols"];
+        if (plistAccessoryProtocols) {
+            YKFLogInfo(@"The application defines protocols in Info.plist:\n%@", plistAccessoryProtocols.description);
+        } else {
+            YKFLogError(@"The application must define the UISupportedExternalAccessoryProtocols in the Info plist.");
+        }
+    }
+}
+
+@end
+
+#endif
diff --git a/YubiKit/YubiKit/Sessions/AccessorySession/YKFAccessorySession+Private.h b/YubiKit/YubiKit/Sessions/AccessorySession/YKFAccessorySession+Private.h
new file mode 100755
index 000000000..bb9ef67c2
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/AccessorySession/YKFAccessorySession+Private.h
@@ -0,0 +1,30 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+
+#import "YKFAccessorySessionConfiguration.h"
+#import "EAAccessoryManager+Testing.h"
+
+typedef void (^YKFAccessorySessionStateChangeBlock)(YKFAccessorySessionState, YKFAccessorySessionState);
+
+@interface YKFAccessorySession()
+
+/*
+ Hidden initializer to avoid the creation of multiple instances outside YubiKit.
+ */
+- (nullable instancetype)initWithAccessoryManager:(nonnull id<YKFEAAccessoryManagerProtocol>)accessoryManager
+                                    configuration:(nonnull YKFAccessorySessionConfiguration *)configuration;
+
+@end
diff --git a/YubiKit/YubiKit/Sessions/AccessorySession/YKFAccessorySession.h b/YubiKit/YubiKit/Sessions/AccessorySession/YKFAccessorySession.h
new file mode 100755
index 000000000..3e52a6328
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/AccessorySession/YKFAccessorySession.h
@@ -0,0 +1,253 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+#import "YKFAccessoryDescription.h"
+
+#import "YKFKeyU2FService.h"
+#import "YKFKeyFIDO2Service.h"
+#import "YKFKeyOATHService.h"
+#import "YKFKeyRawCommandService.h"
+
+/**
+ * ---------------------------------------------------------------------------------------------------------------------
+ * @name YKFAccessorySession Types
+ * ---------------------------------------------------------------------------------------------------------------------
+ */
+
+/*!
+ Defines the states of the YKFAccessorySession.
+ */
+typedef NS_ENUM(NSUInteger, YKFAccessorySessionState) {
+    
+    /// The session is closed. No commands can be sent to the key.
+    YKFAccessorySessionStateClosed,
+    
+    /// The session is opened and ready to use. The application can send immediately commands to the key.
+    YKFAccessorySessionStateOpen,
+    
+    /// The session is in an intermediary state between opened and closed. The application should not send commands
+    /// to the key when the session is in this state.
+    YKFAccessorySessionStateClosing,
+    
+    /// The session is in an intermediary state between closed and opened. The application should not send commands
+    /// to the key when the session is in this state.
+    YKFAccessorySessionStateOpening
+};
+
+/**
+ * ---------------------------------------------------------------------------------------------------------------------
+ * @name YKFAccessorySessionProtocol
+ * ---------------------------------------------------------------------------------------------------------------------
+ */
+
+/*!
+ Defines the interface for YKFAccessorySession.
+ */
+@protocol YKFAccessorySessionProtocol<NSObject>
+
+/*!
+ @property sessionState
+ 
+ @abstract
+    This property allows to check and observe the state of the connection with the YubiKey.
+ 
+ NOTE:
+    This is a KVO compliant property. Observe it to get updates when the key is connected.
+ */
+@property (nonatomic, assign, readonly) YKFAccessorySessionState sessionState;
+
+/*!
+ @property accessoryDescription
+ 
+ @returns
+    The description of the connected key.
+ 
+ NOTE:
+    This property becomes available when the key is connected and is nil when the key is disconnected.
+ */
+@property (nonatomic, readonly, nullable) YKFAccessoryDescription *accessoryDescription;
+
+/*!
+ @property isKeyConnected
+ 
+ @returns
+    YES if the key is connected to the device.
+ */
+@property (nonatomic, assign, readonly, getter=isKeyConnected) BOOL keyConnected;
+
+/*!
+ @property u2fService
+ 
+ @abstract
+    The shared object to interact with the U2F application from the YubiKey.
+    This property becomes available when the key is connected and the session opened and is nil when
+    the session is closed. This property should be accessed based on the session state.
+ */
+@property (nonatomic, readonly, nullable) id<YKFKeyU2FServiceProtocol> u2fService;
+
+/*!
+ @property fido2Service
+ 
+ @abstract
+    The shared object to interact with the FIDO2 application from the YubiKey.
+    This property becomes available when the key is connected and the session opened and is nil when
+    the session is closed. This property should be accessed based on the session state.
+ */
+@property (nonatomic, readonly, nullable) id<YKFKeyFIDO2ServiceProtocol> fido2Service;
+
+/*!
+ @property oathService
+ 
+ @abstract
+    The shared object to interact with the OATH application from the YubiKey.
+    This property becomes available when the key is connected and the session opened and is nil when
+    the session is closed. This property should be accessed based on the session state.
+ */
+@property (nonatomic, readonly, nullable) id<YKFKeyOATHServiceProtocol> oathService;
+
+/*!
+ @property rawCommandService
+ 
+ @abstract
+    The shared object which provides an interface to send raw commands to the YubiKey.
+    This property becomes available when the key is connected and the session opened and is nil when
+    the session is closed. This property should be accessed based on the session state.
+ */
+@property (nonatomic, readonly, nullable) id<YKFKeyRawCommandServiceProtocol> rawCommandService;
+
+/*!
+ @method startSession
+ 
+ @abstract
+    To allow the session to connect and interract with the YubiKey, the session needs to be started. Calling this
+    method will enable the session to receive events when the key is connected or disconnected and tries to connect to
+    the key if it is already plugged in.
+ 
+ @discussion
+    The session is not started automatically to allow a more granular approch on when the app should listen and interract
+    with the key. When the app is requesting the user to use the key, the session needs to be started. When the app no longer
+    requires the user to use the key, the session should be stopped. After calling this method the session will be opened
+    asynchronously and the application can monitor the state by observing the sessionState property.
+ */
+- (void)startSession;
+
+/*!
+ @method startSessionSync
+ 
+ @abstract
+    Starts the session and blocks the execution of the calling thread until the session is started or the operation times out.
+ 
+ @discussion
+    This method should be used only when the application communicates with the key over the Raw Command service and
+    a certain operation requires to bulk multiple key requests over a temporary opened connection with the key.
+
+ @warning
+    This method should never be called from the main thread, to not block it. In debug configurations, if it's called
+    from the main thread, it will fire an assertion.
+ 
+ @returns
+    YES if the session was started, otherwise NO.
+ */
+- (BOOL)startSessionSync;
+
+/*!
+ @method stopSession
+ 
+ @abstract
+    Closes the communication with the key and disables the key connection events. After calling this method the session will
+    be closed asynchronously and the application will receive events on the sessionState when the session is closed. After the
+    session is closed the u2fService will become unavaliable.
+ */
+- (void)stopSession;
+
+/*!
+ @method stopSessionSync
+ 
+ @abstract
+    Stops the session and blocks the execution of the calling thread until the session is stopped or the operation times out.
+ 
+ @discussion
+    This method should be used only when the application communicates with the key over the Raw Command service and
+    a certain operation requires to bulk multiple key requests over a temporary opened connection with the key.
+ 
+ @warning
+    This method should never be called from the main thread, to not block it. In debug configurations, if it's called
+    from the main thread, it will fire an assertion.
+ 
+ @returns
+    YES if the session was stopped, otherwise NO.
+ */
+- (BOOL)stopSessionSync;
+
+/*!
+ @method cancelCommands
+ 
+ @abstract:
+    Cancels all issued commands to the key, which are still in the processing queue but not yet started. This method
+    would be usually called when the user wants to cancel an operation in the UI and the application also cancels the
+    requests to the key.
+ */
+- (void)cancelCommands;
+
+@end
+
+/**
+ * ---------------------------------------------------------------------------------------------------------------------
+ * @name YKFAccessorySession
+ * ---------------------------------------------------------------------------------------------------------------------
+ */
+
+NS_ASSUME_NONNULL_BEGIN
+
+/*!
+ @class YKFAccessorySession
+ 
+ @abstract
+    Provides a list of services for interacting with the YubiKey.
+ */
+@interface YKFAccessorySession : NSObject<YKFAccessorySessionProtocol>
+
+/*
+ Not available: use the shared single instance from YubiKitManager.
+ */
+- (instancetype)init NS_UNAVAILABLE;
+
+@end
+
+/*!
+ @constant YKFAccessorySessionStatePropertyKey
+ 
+ @abstract
+    Helper property name to setup KVO paths in ObjC. For Swift there is a better built-in language support for composing keypaths.
+ */
+extern NSString* const YKFAccessorySessionStatePropertyKey;
+
+/*!
+ @constant YKFAccessorySessionU2FServicePropertyKey
+ 
+ @abstract
+    Helper property name to setup KVO paths in ObjC. For Swift there is a better built-in language support for composing keypaths.
+ */
+extern NSString* const YKFAccessorySessionU2FServicePropertyKey;
+
+/*!
+ @constant YKFAccessorySessionFIDO2ServicePropertyKey
+ 
+ @abstract
+    Helper property name to setup KVO paths in ObjC. For Swift there is a better built-in language support for composing keypaths.
+ */
+extern NSString* const YKFAccessorySessionFIDO2ServicePropertyKey;
+
+NS_ASSUME_NONNULL_END
diff --git a/YubiKit/YubiKit/Sessions/AccessorySession/YKFAccessorySession.m b/YubiKit/YubiKit/Sessions/AccessorySession/YKFAccessorySession.m
new file mode 100755
index 000000000..e50a27219
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/AccessorySession/YKFAccessorySession.m
@@ -0,0 +1,539 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <ExternalAccessory/ExternalAccessory.h>
+#import <UIKit/UIKit.h>
+
+#import "YKFAccessorySession.h"
+#import "YKFAccessorySession+Private.h"
+#import "YKFAccessorySession+Debugging.h"
+
+#import "YubiKitDeviceCapabilities.h"
+#import "YKFAccessoryConnectionController.h"
+#import "YKFAccessorySessionConfiguration.h"
+#import "YKFKeyCommandConfiguration.h"
+#import "YKFAccessoryDescription.h"
+#import "YKFKVOObservation.h"
+#import "YKFBlockMacros.h"
+#import "YKFLogger.h"
+#import "YKFDispatch.h"
+#import "YKFAssert.h"
+
+#import "YKFKeyRawCommandService+Private.h"
+#import "YKFKeyOATHService+Private.h"
+#import "YKFKeyU2FService+Private.h"
+#import "YKFKeyFIDO2Service+Private.h"
+#import "YKFKeyService+Private.h"
+#import "YKFAccessoryDescription+Private.h"
+
+#import "EAAccessory+Testing.h"
+#import "EASession+Testing.h"
+
+#pragma mark - Private Block Types
+
+typedef void (^YKFAccessorySessionDispatchBlock)(void);
+
+#pragma mark - Constants
+
+NSString* const YKFAccessorySessionStatePropertyKey = @"sessionState";
+NSString* const YKFAccessorySessionU2FServicePropertyKey = @"u2fService";
+NSString* const YKFAccessorySessionFIDO2ServicePropertyKey = @"fido2Service";
+
+static NSTimeInterval const YubiAccessorySessionStartDelay = 0.05; // seconds
+static NSTimeInterval const YubiAccessorySessionStreamOpenDelay = 0.2; // seconds
+
+#pragma mark - YKFAccessorySession
+
+@interface YKFAccessorySession()<NSStreamDelegate, YKFKeyServiceDelegate>
+
+// Dispatching
+
+@property (nonatomic) NSOperationQueue *communicationQueue;
+@property (nonatomic) dispatch_queue_t sharedDispatchQueue;
+
+// Accessory
+
+@property (nonatomic, readwrite) YKFAccessoryDescription *accessoryDescription;
+
+@property (nonatomic) id<YKFEAAccessoryManagerProtocol> accessoryManager;
+@property (nonatomic) id<YKFEAAccessoryProtocol> accessory;
+@property (nonatomic) id<YKFEASessionProtocol> session;
+
+@property (nonatomic) id<YKFKeyConnectionControllerProtocol> connectionController;
+
+// Services
+
+@property (nonatomic, assign, readwrite) YKFAccessorySessionState sessionState;
+
+@property (nonatomic, readwrite) id<YKFKeyU2FServiceProtocol, YKFKeyServiceDelegate> u2fService;
+@property (nonatomic, readwrite) id<YKFKeyFIDO2ServiceProtocol, YKFKeyServiceDelegate> fido2Service;
+@property (nonatomic, readwrite) id<YKFKeyOATHServiceProtocol, YKFKeyServiceDelegate> oathService;
+@property (nonatomic, readwrite) id<YKFKeyRawCommandServiceProtocol, YKFKeyServiceDelegate> rawCommandService;
+
+// Observation
+
+@property (nonatomic, assign) BOOL observeAccessoryConnection;
+@property (nonatomic, assign) BOOL observeApplicationState;
+
+// Behaviour
+
+@property (nonatomic) id<YKFAccessorySessionConfigurationProtocol> configuration;
+@property (nonatomic) NSString *currentKeyProtocol; // The protocol used to create a communication session with the key.
+
+// Flags
+
+@property (nonatomic, assign) BOOL reconnectOnApplicationActive;
+
+@end
+
+@implementation YKFAccessorySession
+
+- (instancetype)initWithAccessoryManager:(id<YKFEAAccessoryManagerProtocol>)accessoryManager configuration:(YKFAccessorySessionConfiguration *)configuration {
+    YKFAssertAbortInit(accessoryManager);
+    YKFAssertAbortInit(configuration);
+    
+    self = [super init];
+    if (self) {
+        self.configuration = configuration;
+        self.accessoryManager = accessoryManager;
+        
+        [self setupCommunicationQueue];
+    }
+    return self;
+}
+
+- (void)dealloc {
+    self.observeAccessoryConnection = NO;
+    self.observeApplicationState = NO;
+}
+
+#pragma mark - Private properties
+
+- (BOOL)isKeyConnected {
+    for (EAAccessory *connectedAccessory in self.accessoryManager.connectedAccessories) {
+        if ([self shouldAcceptAccessory:connectedAccessory]) {
+            return YES;
+        }
+    }
+    return NO;
+}
+
+#pragma mark - Session start/stop
+
+- (void)startSession {
+    YKFAssertReturn(YubiKitDeviceCapabilities.supportsMFIAccessoryKey, @"Cannot start the key session on an unsupported device.");
+    YKFLogInfo(@"Accessory session start requested.");
+    
+#ifdef DEBUG
+    [self checkApplicationConfiguration];
+#endif
+    
+    if (self.sessionState != YKFAccessorySessionStateClosed) {
+        YKFLogInfo(@"Accessory session start ignored. The session is already started.");
+        return;
+    }
+    
+    self.observeAccessoryConnection = YES;
+    self.observeApplicationState = YES;
+
+    [self connectToExistingKey]; // If a key is already plugged, connect to it.
+}
+
+- (BOOL)startSessionSync {
+    YKFAssertOffMainThread();
+    
+    YKFAssertReturnValue(YubiKitDeviceCapabilities.supportsMFIAccessoryKey, @"Cannot start the accessory session on an unsupported device.", NO);
+    YKFAssertReturnValue(self.isKeyConnected, @"Cannot start the session if the key is not connected.", NO);
+    
+    if (self.sessionState == YKFAccessorySessionStateOpen) {
+        return YES;
+    }
+    
+    dispatch_semaphore_t openSemaphore = dispatch_semaphore_create(0);
+    
+    YKFKVOObservation *observation = [[YKFKVOObservation alloc] initWithTarget:self keyPath:YKFAccessorySessionStatePropertyKey callback:^(id oldValue, id newValue) {
+        YKFAccessorySessionState newState = ((NSNumber *)newValue).unsignedLongValue;
+        if (newState == YKFAccessorySessionStateOpen) {
+            dispatch_semaphore_signal(openSemaphore);
+        }
+    }];
+    YKFAssertReturnValue(observation, @"Could not observe the session state.", NO);
+    
+    [self startSession];
+    
+    YKFKeyCommandConfiguration *configuration = [YKFKeyCommandConfiguration defaultCommandCofiguration];
+    dispatch_semaphore_wait(openSemaphore, dispatch_time(DISPATCH_TIME_NOW, (int64_t)(configuration.commandTimeout * NSEC_PER_SEC)));
+    
+    observation = nil;
+    
+    // There was an error when opening the session
+    if (self.sessionState != YKFAccessorySessionStateOpen) {
+        return NO;
+    }
+    
+    return YES;
+}
+
+- (void)stopSession {
+    YKFLogInfo(@"Accessory session stop requested.");
+    
+    if (self.sessionState != YKFAccessorySessionStateOpen) {
+        YKFLogInfo(@"Accessory session stop ignored. The session is already stopped.");
+        return;
+    }
+
+    self.observeAccessoryConnection = NO;
+    self.observeApplicationState = NO;
+
+    [self closeSession];
+}
+
+- (BOOL)stopSessionSync {
+    YKFAssertOffMainThread();
+    YKFAssertReturnValue(self.isKeyConnected, @"Cannot stop the session if the key is not connected.", NO);
+    
+    if (self.sessionState == YKFAccessorySessionStateClosed) {
+        return YES;
+    }
+        
+    dispatch_semaphore_t closeSemaphore = dispatch_semaphore_create(0);
+    
+    YKFKVOObservation *observation = [[YKFKVOObservation alloc] initWithTarget:self keyPath:YKFAccessorySessionStatePropertyKey callback:^(id oldValue, id newValue) {
+        YKFAccessorySessionState newState = ((NSNumber *)newValue).unsignedLongValue;
+        if (newState == YKFAccessorySessionStateClosed) {
+            dispatch_semaphore_signal(closeSemaphore);
+        }
+    }];
+    YKFAssertReturnValue(observation, @"Could not observe the session state.", NO);
+    
+    [self stopSession];
+    
+    YKFKeyCommandConfiguration *configuration = [YKFKeyCommandConfiguration defaultCommandCofiguration];
+    dispatch_semaphore_wait(closeSemaphore, dispatch_time(DISPATCH_TIME_NOW, (int64_t)(configuration.commandTimeout * NSEC_PER_SEC)));
+    
+    observation = nil;
+    
+    // There was an error when closing the session
+    if (self.sessionState != YKFAccessorySessionStateClosed) {
+        return NO;
+    }
+    
+    return YES;
+}
+
+#pragma mark - Notification subscription
+
+- (void)setObserveApplicationState:(BOOL)observeApplicationState {
+    if (_observeApplicationState == observeApplicationState) {
+        return;
+    }
+    _observeApplicationState = observeApplicationState;
+    if (_observeApplicationState) {
+        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationDidEnterBackground:)
+                                                     name:UIApplicationDidEnterBackgroundNotification object:nil];
+        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationWillTerminate:)
+                                                     name:UIApplicationWillTerminateNotification object:nil];
+        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationDidBecomeActive:)
+                                                     name:UIApplicationDidBecomeActiveNotification object:nil];
+    } else {
+        [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidEnterBackgroundNotification object:nil];
+        [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationWillTerminateNotification object:nil];
+        [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidBecomeActiveNotification object:nil];
+    }
+}
+
+- (void)setObserveAccessoryConnection:(BOOL)observeAccessoryConnection {
+    if (_observeAccessoryConnection == observeAccessoryConnection) {
+        return;
+    }
+    _observeAccessoryConnection = observeAccessoryConnection;
+    if (_observeAccessoryConnection) {
+        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(accessoryDidConnect:)
+                                                     name:EAAccessoryDidConnectNotification object:nil];
+        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(accessoryDidDisconnect:)
+                                                     name:EAAccessoryDidDisconnectNotification object:nil];
+        
+        [[EAAccessoryManager sharedAccessoryManager] registerForLocalNotifications];
+    } else {
+        [[NSNotificationCenter defaultCenter] removeObserver:self name:EAAccessoryDidConnectNotification object:nil];
+        [[NSNotificationCenter defaultCenter] removeObserver:self name:EAAccessoryDidDisconnectNotification object:nil];
+        
+        [[EAAccessoryManager sharedAccessoryManager] unregisterForLocalNotifications];
+    }
+}
+
+- (void)connectToExistingKey {
+    for (EAAccessory *connectedAccessory in self.accessoryManager.connectedAccessories) {
+        if (![self shouldAcceptAccessory:connectedAccessory]) {
+            continue;
+        }
+        
+        NSDictionary *userInfo = @{EAAccessoryKey: connectedAccessory};
+        NSNotification *notification = [[NSNotification alloc] initWithName:EAAccessoryDidConnectNotification object:self userInfo:userInfo];
+        [self accessoryDidConnect:notification];
+        break;
+    }
+}
+
+#pragma mark - Session state
+
+- (void)setSessionState:(YKFAccessorySessionState)sessionState {
+    // Avoid updating the state if the same to not trigger unnecessary KVO notifications.
+    if (sessionState == _sessionState) {
+        return;
+    }
+    _sessionState = sessionState;
+}
+
+#pragma mark - Shared communication queue
+
+- (void)setupCommunicationQueue {
+    // Create a sequential queue because the YubiKey accepts sequential commands.
+    
+    self.communicationQueue = [[NSOperationQueue alloc] init];
+    self.communicationQueue.maxConcurrentOperationCount = 1;
+    
+    dispatch_queue_attr_t dispatchQueueAttributes = dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, DISPATCH_QUEUE_PRIORITY_HIGH, -1);
+    self.sharedDispatchQueue = dispatch_queue_create("com.yubico.YKCOMACC", dispatchQueueAttributes);
+    
+    self.communicationQueue.underlyingQueue = self.sharedDispatchQueue;
+}
+
+- (void)dispatchOnSharedQueueBlock:(YKFAccessorySessionDispatchBlock)block delay:(NSTimeInterval)delay {
+    YKFParameterAssertReturn(block);
+    YKFParameterAssertReturn(self.sharedDispatchQueue);
+    
+    if (delay > 0) {
+        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delay * NSEC_PER_SEC)), self.sharedDispatchQueue, block);
+    } else {
+        dispatch_async(self.sharedDispatchQueue, block);
+    }
+}
+
+- (void)dispatchOnSharedQueueBlock:(YKFAccessorySessionDispatchBlock)block {
+    [self dispatchOnSharedQueueBlock:block delay:0];
+}
+
+#pragma mark - Accessory connection
+
+- (void)accessoryDidConnect:(NSNotification *)notification {
+    EAAccessory *accessory = [[notification userInfo] objectForKey:EAAccessoryKey];
+    if (![self shouldAcceptAccessory:accessory]) {
+        return;
+    }
+    
+    self.currentKeyProtocol = [self.configuration keyProtocolForAccessory:accessory];
+    YKFAssertReturn(self.currentKeyProtocol != nil, @"Could not find a valid protocol for the accessory.");
+    
+    YKFLogInfo(@"The YubiKey is connected to the iOS device.");
+    
+    self.accessory = accessory;
+    self.accessoryDescription = [[YKFAccessoryDescription alloc] initWithAccessory:self.accessory];
+    if (!self.accessoryDescription) {
+        // If a key description could not be fetched, do not start the session.
+        return;
+    }
+    
+    self.sessionState = YKFAccessorySessionStateOpening;
+    
+    ykf_weak_self();
+    [self dispatchOnSharedQueueBlock:^{
+        ykf_safe_strong_self();
+        BOOL success = [strongSelf openSession];
+        if (!success) {
+            strongSelf.sessionState = YKFAccessorySessionStateClosed;
+            return;
+        }
+        
+        [strongSelf dispatchOnSharedQueueBlock:^{
+            strongSelf.sessionState = YKFAccessorySessionStateOpen;
+        } delay:YubiAccessorySessionStreamOpenDelay]; // Add a small delay to allow the streams to open.
+    }
+    delay:YubiAccessorySessionStartDelay]; // Add a small delay to allow the Key to initialize after connected.
+}
+
+- (void)accessoryDidDisconnect:(id)notification {
+    if (!self.accessory) { return; }
+    
+    // Framework bug workaround
+    EAAccessory *accessory = [notification isKindOfClass:[EAAccessory class]] ? (EAAccessory*)notification : [[notification userInfo] objectForKey:EAAccessoryKey];
+    
+    if (accessory.connectionID != self.accessory.connectionID) {
+        return;
+    }
+    
+    YKFLogInfo(@"The YubiKey is disconnected from the iOS device.");
+    
+    self.accessory = nil;
+    self.accessoryDescription = nil;
+    
+    // Close session will dispatch the cleanup of streams on the dispatch queue.
+    [self closeSession];
+}
+
+#pragma mark - Application Notifications
+
+- (void)applicationWillTerminate:(NSNotification *)notification {
+    [self closeSession];
+}
+
+- (void)applicationDidEnterBackground:(NSNotification *)notification {
+    if (self.sessionState == YKFAccessorySessionStateClosed) {
+        return;
+    }
+    
+    UIApplication *application = [UIApplication sharedApplication];
+    __block UIBackgroundTaskIdentifier bgTask = [application beginBackgroundTaskWithName:@"CloseSessionTask" expirationHandler:^{
+        [application endBackgroundTask:bgTask];
+        bgTask = UIBackgroundTaskInvalid;
+        YKFLogVerbose(@"Background task expired.");
+    }];
+    
+    if (self.sessionState == YKFAccessorySessionStateOpen || self.sessionState == YKFAccessorySessionStateOpening) {
+        self.reconnectOnApplicationActive = YES;
+        [self closeSession];
+    }
+    
+    // Dispatch a subsequent operation which will wait for closing.
+    dispatch_async(self.sharedDispatchQueue, ^{
+        [application endBackgroundTask:bgTask];
+        bgTask = UIBackgroundTaskInvalid;
+        YKFLogVerbose(@"Background task ended.");
+    });
+}
+
+- (void)applicationDidBecomeActive:(NSNotification *)notification {
+    if (self.reconnectOnApplicationActive) {
+        [self connectToExistingKey];
+    }
+}
+
+#pragma mark - Session
+
+- (BOOL)openSession {
+    YKFAssertOffMainThread();
+    YKFAssertReturnValue(self.currentKeyProtocol != nil, @"No known protocol to connect to the key.", NO);
+
+    self.session = [[EASession alloc] initWithAccessory:self.accessory forProtocol:self.currentKeyProtocol];
+    
+    if (self.session) {
+        self.reconnectOnApplicationActive = NO;
+        self.connectionController = [[YKFAccessoryConnectionController alloc] initWithSession:self.session operationQueue:self.communicationQueue];
+        self.session.outputStream.delegate = self;
+        
+        /*
+         Setup services after the connection is created
+         */
+        
+        YKFKeyU2FService *u2fService = [[YKFKeyU2FService alloc] initWithConnectionController:self.connectionController];
+        u2fService.delegate = self;
+        self.u2fService = u2fService;
+        
+        YKFKeyFIDO2Service *fido2Service = [[YKFKeyFIDO2Service alloc] initWithConnectionController:self.connectionController];
+        fido2Service.delegate = self;
+        self.fido2Service = fido2Service;
+        
+        YKFKeyOATHService *oathService = [[YKFKeyOATHService alloc] initWithConnectionController:self.connectionController];
+        oathService.delegate = self;
+        self.oathService = oathService;
+        
+        YKFKeyRawCommandService *rawCommandService = [[YKFKeyRawCommandService alloc] initWithConnectionController:self.connectionController];
+        rawCommandService.delegate = self;
+        self.rawCommandService = rawCommandService;
+        
+        YKFLogInfo(@"Session opened.");
+    } else {
+        YKFLogInfo(@"Session opening failed.");
+    }
+    return self.session != nil;
+}
+
+- (void)closeSession {
+    if (!self.session) {
+        return;
+    }
+    if (self.sessionState == YKFAccessorySessionStateClosed || self.sessionState == YKFAccessorySessionStateClosing) {
+        return;
+    }
+    
+    self.sessionState = YKFAccessorySessionStateClosing;
+        
+    ykf_weak_self();
+    [self.connectionController closeConnectionWithCompletion:^{
+        ykf_safe_strong_self();
+        
+        // Clean services first
+        strongSelf.u2fService = nil;
+        strongSelf.fido2Service = nil;
+        strongSelf.oathService = nil;
+        strongSelf.rawCommandService = nil;
+        
+        strongSelf.connectionController = nil;
+        strongSelf.session = nil;
+        
+        strongSelf.sessionState = YKFAccessorySessionStateClosed;
+        YKFLogInfo(@"Session closed.");
+    }];
+}
+
+#pragma mark - Commands
+
+- (void)cancelCommands {
+    [self.connectionController cancelAllCommands];
+}
+
+#pragma mark - NSStreamDelegate
+
+- (void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode {
+    if (eventCode != NSStreamEventErrorOccurred && eventCode != NSStreamEventEndEncountered) {
+        return;
+    }
+    
+    // Stream was closed as a part of a normal session shutdown
+    if (self.sessionState != YKFAccessorySessionStateOpen) {
+        return;
+    }
+    
+    // Stream  was dropped or externally closed -> close the session to avoid lingering
+    YKFLogInfo(@"The communication with the key was closed by the system.");
+    [self closeSession];
+    
+    __block UIApplicationState applicationState = UIApplicationStateActive;
+    ykf_dispatch_block_sync_main(^{
+        applicationState = [UIApplication sharedApplication].applicationState;
+    });
+    
+    // If the connection was lost in inactive or backgroud states -> mark it for reconnecting again when the application becomes active.
+    if (applicationState != UIApplicationStateActive) {
+        self.reconnectOnApplicationActive = YES;
+    }
+}
+
+#pragma mark - YKFKeyServiceDelegate
+
+- (void)keyService:(YKFKeyService *)service willExecuteRequest:(YKFKeyRequest *)request {
+    [self.u2fService keyService:service willExecuteRequest:request];
+    [self.fido2Service keyService:service willExecuteRequest:request];
+    [self.oathService keyService:service willExecuteRequest:request];
+    [self.rawCommandService keyService:service willExecuteRequest:request];
+}
+
+#pragma mark - Helpers
+
+- (BOOL)shouldAcceptAccessory:(EAAccessory*)accessory {
+    YKFParameterAssertReturnValue(accessory, NO);
+    return [self.configuration allowsAccessory:accessory];
+}
+
+@end
diff --git a/YubiKit/YubiKit/Sessions/AccessorySession/YKFAccessorySessionConfiguration.h b/YubiKit/YubiKit/Sessions/AccessorySession/YKFAccessorySessionConfiguration.h
new file mode 100755
index 000000000..0e03f36bb
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/AccessorySession/YKFAccessorySessionConfiguration.h
@@ -0,0 +1,34 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+#import <ExternalAccessory/ExternalAccessory.h>
+#import "EAAccessory+Testing.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@protocol YKFAccessorySessionConfigurationProtocol <NSObject>
+
+/// Returns YES if the accessory is a YubiKey.
+- (BOOL)allowsAccessory:(nonnull id<YKFEAAccessoryProtocol>)accessory;
+
+/// Returns the known session protocol found in the protocols array received from an accessory.
+- (nullable NSString *)keyProtocolForAccessory:(nonnull id<YKFEAAccessoryProtocol>)accessory;
+
+@end
+
+@interface YKFAccessorySessionConfiguration : NSObject<YKFAccessorySessionConfigurationProtocol>
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/YubiKit/YubiKit/Sessions/AccessorySession/YKFAccessorySessionConfiguration.m b/YubiKit/YubiKit/Sessions/AccessorySession/YKFAccessorySessionConfiguration.m
new file mode 100755
index 000000000..0a369ec8b
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/AccessorySession/YKFAccessorySessionConfiguration.m
@@ -0,0 +1,83 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "YKFAccessorySessionConfiguration.h"
+#import "YKFAssert.h"
+
+// Protocols
+static NSString* const YKFAccessorySessionConfigurationYLPProtocolName = @"com.yubico.ylp";
+
+// Manufactures
+static NSString* const YKFAccessorySessionConfigurationYubicoManufacturesName = @"Yubico";
+
+@interface YKFAccessorySessionConfiguration()
+
+@property (nonatomic) NSArray *allowedProtocols;
+@property (nonatomic) NSArray *allowedManufactures;
+
+@end
+
+@implementation YKFAccessorySessionConfiguration
+
+- (instancetype)init {
+    self = [super init];
+    if (self) {
+        self.allowedProtocols = @[YKFAccessorySessionConfigurationYLPProtocolName];
+        self.allowedManufactures = @[YKFAccessorySessionConfigurationYubicoManufacturesName];
+    }
+    return self;
+}
+
+#pragma mark - YKFAccessorySessionConfigurationDelegate
+
+- (BOOL)allowsAccessory:(id<YKFEAAccessoryProtocol>)accessory {
+    YKFParameterAssertReturnValue(accessory, NO);
+    return  [self allowsProtocols:accessory.protocolStrings] && [self allowsManufacturer:accessory.manufacturer];
+}
+
+- (NSString *)keyProtocolForAccessory:(id<YKFEAAccessoryProtocol>)accessory {
+    YKFParameterAssertReturnValue(accessory, nil);
+    
+    NSArray *protocols = accessory.protocolStrings;
+    
+    if (![self allowsProtocols:protocols]) {
+        return nil;
+    }
+    
+    // Return the first known protocol because the key allows only one session.
+    for (NSString *protocol in protocols) {
+        if ([self.allowedProtocols containsObject:protocol]) {
+            return protocol;
+        }
+    }
+    
+    return nil;
+}
+
+#pragma mark - Helpers
+
+- (BOOL)allowsProtocols:(NSArray *)protocols {
+    for (NSString *protocol in protocols) {
+        if ([self.allowedProtocols containsObject:protocol]) {
+            return YES;
+        }
+    }
+    return NO;
+}
+
+- (BOOL)allowsManufacturer:(NSString *)manufacturer {
+    return [self.allowedManufactures containsObject:manufacturer];
+}
+
+@end
diff --git a/YubiKit/YubiKit/Sessions/NFCSession/Services/YKFNFCOTPService+Private.h b/YubiKit/YubiKit/Sessions/NFCSession/Services/YKFNFCOTPService+Private.h
new file mode 100755
index 000000000..152efe380
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/NFCSession/Services/YKFNFCOTPService+Private.h
@@ -0,0 +1,28 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "YKFNFCOTPService.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@protocol YKFOTPTokenParserProtocol;
+@protocol YKFNFCNDEFReaderSessionProtocol;
+
+@interface YKFNFCOTPService()
+
+- (instancetype)initWithTokenParser:(nullable id<YKFOTPTokenParserProtocol>)tokenParser session:(nullable id<YKFNFCNDEFReaderSessionProtocol>)session;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/YubiKit/YubiKit/Sessions/NFCSession/Services/YKFNFCOTPService.h b/YubiKit/YubiKit/Sessions/NFCSession/Services/YKFNFCOTPService.h
new file mode 100755
index 000000000..b8ad01049
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/NFCSession/Services/YKFNFCOTPService.h
@@ -0,0 +1,59 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+
+@protocol YKFOTPTokenProtocol;
+
+/*!
+ @abstract
+    Response block used by requestOTPToken: to provide the results of a OTP NFC scan.
+ */
+typedef void (^YKFOTPResponseBlock)(id<YKFOTPTokenProtocol> _Nullable, NSError* _Nullable);
+
+
+API_AVAILABLE(ios(11.0))
+@protocol YKFNFCOTPServiceProtocol<NSObject>
+
+/*!
+ @method requestOTPToken:
+ 
+ @param completion
+    The completion block which returns the response from the OTP token request using NFC.
+ 
+ @abstract
+    Async call for requesting a OTP token from a YubiKey supporting NFC. After caling
+    this method YubiKit will prompt the user to scan the YubiKey using the native NFC scanning UI.
+    To provide an additional message to guide the user or to provide a reason for the scan, YubiKit
+    allows to customize the default message shown in the system UI. This can be done by setting
+    a localized value in YubiKitExternalLocalization.
+ 
+ NOTE:
+    Before using this method make sure the project is properly configured to allow NFC reading. Using the NFC reader
+    requires iOS 11 and iPhone 7 or above. For more details check CoreNFC requirements. YubiKit provides the ability
+    to check if the device supports NFC reading by using the shared instance deviceCapabilities from YubiKitManager.
+ */
+- (void)requestOTPToken:(nonnull YKFOTPResponseBlock)completion;
+
+@end
+
+API_AVAILABLE(ios(11.0))
+@interface YKFNFCOTPService : NSObject<YKFNFCOTPServiceProtocol>
+
+/*
+ Not available: use the shared single instance from YubiKitManager.
+ */
+- (nonnull instancetype)init NS_UNAVAILABLE;
+
+@end
diff --git a/YubiKit/YubiKit/Sessions/NFCSession/Services/YKFNFCOTPService.m b/YubiKit/YubiKit/Sessions/NFCSession/Services/YKFNFCOTPService.m
new file mode 100755
index 000000000..372a320de
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/NFCSession/Services/YKFNFCOTPService.m
@@ -0,0 +1,108 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <CoreNFC/CoreNFC.h>
+
+#import "YKFNFCOTPService.h"
+#import "YKFNFCOTPService+Private.h"
+
+#import "YKFNFCError.h"
+#import "YKFNFCError+Errors.h"
+
+#import "YubiKitExternalLocalization.h"
+#import "YKFOTPTokenParser.h"
+#import "YKFLogger.h"
+#import "YKFDispatch.h"
+#import "YubiKitDeviceCapabilities.h"
+#import "YKFBlockMacros.h"
+#import "YKFAssert.h"
+
+#import "NFCNDEFReaderSession+Testing.h"
+
+@interface YKFNFCOTPService()<NFCNDEFReaderSessionDelegate>
+
+@property (nonatomic, strong) id<YKFNFCNDEFReaderSessionProtocol> nfcSession;
+@property (nonatomic, strong) id<YKFOTPTokenParserProtocol> otpTokenParser;
+
+@property (nonatomic, copy) YKFOTPResponseBlock nfcOTPResponseBlock;
+
+@end
+
+@implementation YKFNFCOTPService
+
+- (instancetype)initWithTokenParser:(id<YKFOTPTokenParserProtocol>)tokenParser session:(id<YKFNFCNDEFReaderSessionProtocol>)session {
+    self = [super init];
+    if (self) {
+        self.otpTokenParser = tokenParser ? tokenParser : [[YKFOTPTokenParser alloc] init];
+        self.nfcSession = session;
+    }
+    return self;
+}
+
+#pragma mark - YKFNFCReaderManagerProtocol
+
+- (void)requestOTPToken:(YKFOTPResponseBlock)completion {
+    YKFAssertReturn(YubiKitDeviceCapabilities.supportsNFCScanning, @"Device does not support NFC scanning.");
+    
+    self.nfcOTPResponseBlock = completion;
+    [self start];
+}
+
+- (void)start {
+    if (!self.nfcSession) {
+        // The updates are dispatched on the main queue.
+        dispatch_queue_t mainQueue = dispatch_get_main_queue();
+        self.nfcSession = [[NFCNDEFReaderSession alloc] initWithDelegate:self queue:mainQueue invalidateAfterFirstRead:YES];
+        self.nfcSession.alertMessage = YubiKitExternalLocalization.nfcScanAlertMessage;
+    }
+    [self.nfcSession beginSession];
+}
+
+#pragma mark - NFCNDEFReaderSessionDelegate
+
+- (void)readerSession:(NFCNDEFReaderSession *)session didInvalidateWithError:(NSError *)error {
+    YKFAssertOnMainThread();
+    
+    self.nfcSession = nil;
+    if ([self shouldIgnoreError: error]) {
+        return;
+    }
+    
+    YKFLogNSError(error);
+    
+    self.nfcOTPResponseBlock(nil, error);
+    self.nfcOTPResponseBlock = nil;
+}
+
+- (void)readerSession:(NFCNDEFReaderSession *)session didDetectNDEFs:(NSArray<NFCNDEFMessage *> *)messages {
+    YKFAssertOnMainThread();
+    
+    id<YKFOTPTokenProtocol> otpToken = [self.otpTokenParser otpTokenFromNfcMessages:messages];
+    if (otpToken) {
+        self.nfcOTPResponseBlock(otpToken, nil);
+        self.nfcOTPResponseBlock = nil;
+    } else {
+        YKFNFCError *error = [YKFNFCError noTokenAfterScanError];
+        self.nfcOTPResponseBlock(nil, error);
+        self.nfcOTPResponseBlock = nil;
+    }
+}
+
+#pragma mark - Helpers
+
+- (BOOL)shouldIgnoreError:(NSError *)error {
+    return error.code == NFCReaderSessionInvalidationErrorFirstNDEFTagRead;
+}
+
+@end
diff --git a/YubiKit/YubiKit/Sessions/NFCSession/YKFNFCConnectionController.h b/YubiKit/YubiKit/Sessions/NFCSession/YKFNFCConnectionController.h
new file mode 100755
index 000000000..64de82df0
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/NFCSession/YKFNFCConnectionController.h
@@ -0,0 +1,28 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+#import <CoreNFC/CoreNFC.h>
+#import "YKFKeyConnectionControllerProtocol.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+API_AVAILABLE(ios(13.0))
+@interface YKFNFCConnectionController: NSObject<YKFKeyConnectionControllerProtocol>
+
+- (instancetype)initWithNFCTag:(id<NFCISO7816Tag>)tag operationQueue:(NSOperationQueue *)operationQueue;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/YubiKit/YubiKit/Sessions/NFCSession/YKFNFCConnectionController.m b/YubiKit/YubiKit/Sessions/NFCSession/YKFNFCConnectionController.m
new file mode 100755
index 000000000..606d1bbc2
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/NFCSession/YKFNFCConnectionController.m
@@ -0,0 +1,193 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "YKFNFCConnectionController.h"
+#import "YKFNSMutableDataAdditions.h"
+#import "YKFBlockMacros.h"
+#import "YKFLogger.h"
+#import "YKFAssert.h"
+#import "YKFAPDU+Private.h"
+
+typedef void (^YKFKeyConnectionControllerCommunicationQueueBlock)(NSOperation *operation);
+
+@interface YKFNFCConnectionController()
+
+@property (nonatomic) NSOperationQueue *communicationQueue;
+@property (nonatomic) NSMutableDictionary *delayedDispatches;
+
+@property (nonatomic) id<NFCISO7816Tag> tag;
+
+@end
+
+@implementation YKFNFCConnectionController
+
+- (instancetype)initWithNFCTag:(id<NFCISO7816Tag>)tag operationQueue:(NSOperationQueue *)operationQueue {
+    self = [super init];
+    if (self) {
+        self.tag = tag;
+        self.communicationQueue = operationQueue;        
+        self.delayedDispatches = [[NSMutableDictionary alloc] init];
+    }
+    return self;
+}
+
+#pragma mark - Commands
+
+- (void)execute:(nonnull YKFAPDU *)command completion:(nonnull YKFKeyConnectionControllerCommandResponseBlock)completion {
+    [self execute:command configuration:[YKFKeyCommandConfiguration defaultCommandCofiguration] completion:completion];
+}
+
+- (void)execute:(nonnull YKFAPDU *)command configuration:(nonnull YKFKeyCommandConfiguration *)configuration completion:(nonnull YKFKeyConnectionControllerCommandResponseBlock)completion {
+    YKFParameterAssertReturn(command);
+    YKFParameterAssertReturn(configuration);
+    YKFParameterAssertReturn(completion);
+    
+    YKFLogVerbose(@"NFCConnectionController - Execute command...");
+    
+    ykf_weak_self();
+    [self dispatchBlockOnCommunicationQueue:^(NSOperation *operation) {
+        ykf_safe_strong_self();
+      
+        // Do not wait for the command to process if the operation was canceled.
+        if (operation.isCancelled) {
+            return;
+        }
+        
+        // Check availability before executing. If the command is queued, the tag may become unavailable at execution time.
+        if (!strongSelf.tag.isAvailable) {
+            return;
+        }
+                
+        NFCISO7816APDU *cnApdu = [[NFCISO7816APDU alloc] initWithData:command.apduData];
+        YKFAssertReturn(cnApdu, @"Could not create a Core NFC APDU object from the command data.");
+
+        __block NSError *executionError = nil;
+        __block NSData *executionResult = nil;
+        NSDate *commandStartDate = [NSDate date];
+        dispatch_semaphore_t executionSemaphore = dispatch_semaphore_create(0);
+        
+        [strongSelf.tag sendCommandAPDU:cnApdu completionHandler:^(NSData *responseData, uint8_t sw1, uint8_t sw2, NSError *error) {
+            if (error) {
+                executionError = error;
+                dispatch_semaphore_signal(executionSemaphore);
+                return;
+            }
+            
+            NSMutableData *fullResponse = [[NSMutableData alloc] initWithData:responseData];
+            [fullResponse ykf_appendByte:sw1];
+            [fullResponse ykf_appendByte:sw2];
+            executionResult = [fullResponse copy];
+            
+            dispatch_semaphore_signal(executionSemaphore);
+        }];
+        
+        // Lock the async call to enforce the sequential execution using the library dispatch queue.
+        dispatch_semaphore_wait(executionSemaphore, dispatch_time(DISPATCH_TIME_NOW, (int64_t)(configuration.commandTimeout * NSEC_PER_SEC)));
+        
+        // Do not notify if the operation was canceled.
+        if (operation.isCancelled) {
+            return;
+        }
+
+        NSTimeInterval executionTime = [[NSDate date] timeIntervalSinceDate: commandStartDate];
+        if (executionError) {
+            completion(nil, executionError, executionTime);
+        } else {
+            YKFAssertReturn(executionResult, @"The command did not return any response data when error was not nil.");
+            completion(executionResult, nil, executionTime);
+        }
+        
+        YKFLogVerbose(@"Command execution time: %lf seconds", executionTime);
+    }];
+}
+
+- (void)closeConnectionWithCompletion:(nonnull YKFKeyConnectionControllerCompletionBlock)completion {
+    // Does nothing: The NFCISO7816Tag doesn't expose a stream for communication.
+    completion();
+}
+
+- (void)dispatchOnSequentialQueue:(YKFKeyConnectionControllerCompletionBlock)block delay:(NSTimeInterval)delay {
+    dispatch_queue_t sharedDispatchQueue = self.communicationQueue.underlyingQueue;
+    
+    YKFParameterAssertReturn(sharedDispatchQueue);
+    YKFParameterAssertReturn(block);
+
+    block = [block copy]; // heap block
+    
+    if (delay == 0) {
+        dispatch_async(sharedDispatchQueue, block);
+    } else {
+        NSString *blockId = [NSUUID UUID].UUIDString;
+        
+        ykf_weak_self();
+        dispatch_block_t delayedBlock = dispatch_block_create(0, ^{
+            ykf_safe_strong_self();
+            dispatch_block_t blockReference = strongSelf.delayedDispatches[blockId];
+            strongSelf.delayedDispatches[blockId] = nil;
+            
+            // In case the block started already to run.
+            if (blockReference && dispatch_block_testcancel(blockReference)) {
+                return;
+            }
+            
+            block();
+        });
+        
+        self.delayedDispatches[blockId] = delayedBlock;
+        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delay * NSEC_PER_SEC)), sharedDispatchQueue, delayedBlock);
+    }
+}
+
+- (void)dispatchOnSequentialQueue:(YKFKeyConnectionControllerCompletionBlock)block {
+    YKFParameterAssertReturn(block);
+    [self dispatchOnSequentialQueue:block delay:0];
+}
+
+- (void)cancelAllCommands {
+    self.communicationQueue.suspended = YES;
+    dispatch_suspend(self.communicationQueue.underlyingQueue);
+    
+    [self.communicationQueue cancelAllOperations];
+    
+    NSArray *keys = self.delayedDispatches.allKeys;
+    for (NSString *key in keys) {
+        dispatch_block_t block = self.delayedDispatches[key];
+        dispatch_block_cancel(block);
+    };
+    [self.delayedDispatches removeAllObjects];
+    
+    dispatch_resume(self.communicationQueue.underlyingQueue);
+    self.communicationQueue.suspended = NO;
+}
+
+#pragma mark - Helpers
+
+- (void)dispatchBlockOnCommunicationQueue:(YKFKeyConnectionControllerCommunicationQueueBlock)block {
+    YKFParameterAssertReturn(block);
+    
+    NSBlockOperation *operation = [[NSBlockOperation alloc] init];
+    __weak NSBlockOperation *weakOperation = operation;
+    
+    [operation addExecutionBlock:^{
+        __strong NSBlockOperation *strongOperation = weakOperation;
+        if (!strongOperation || strongOperation.isCancelled) {
+            return;
+        }
+        block(strongOperation); // Execute the operation if it's still alive and not canceled.
+    }];
+    
+    [self.communicationQueue addOperation:operation];
+}
+
+@end
diff --git a/YubiKit/YubiKit/Sessions/NFCSession/YKFNFCError+Errors.h b/YubiKit/YubiKit/Sessions/NFCSession/YKFNFCError+Errors.h
new file mode 100755
index 000000000..e860963d8
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/NFCSession/YKFNFCError+Errors.h
@@ -0,0 +1,25 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agrbeed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "YKFNFCError.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface YKFNFCError()
+
++ (YKFNFCError *)noTokenAfterScanError;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/YubiKit/YubiKit/Sessions/NFCSession/YKFNFCError.h b/YubiKit/YubiKit/Sessions/NFCSession/YKFNFCError.h
new file mode 100755
index 000000000..cee2fec2f
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/NFCSession/YKFNFCError.h
@@ -0,0 +1,47 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+/*!
+ @constant
+    YKFNFCErrorDomain
+ @abstract
+    Domain for errors from NFC.
+ */
+extern NSString* const YKFNFCErrorDomain;
+
+/*!
+ @class
+    YKFNFCReadError
+ @abstract
+    Error from NFC.
+ */
+@interface YKFNFCError : NSError
+
+- (instancetype)init NS_UNAVAILABLE;
+
+@end
+
+/*!
+ @constant
+    YKFNFCReadErrorNoTokenAfterScanCode
+ @abstract
+    When the library was not able to retreive a OTP token from the NDEF payload.
+ */
+extern int const YKFNFCReadErrorNoTokenAfterScanCode;
+
+NS_ASSUME_NONNULL_END
diff --git a/YubiKit/YubiKit/Sessions/NFCSession/YKFNFCError.m b/YubiKit/YubiKit/Sessions/NFCSession/YKFNFCError.m
new file mode 100755
index 000000000..f5ec9165b
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/NFCSession/YKFNFCError.m
@@ -0,0 +1,34 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "YKFNFCError.h"
+#import "YKFNFCError+Errors.h"
+
+NSString* const YKFNFCErrorDomain = @"YKFNFCError";
+
+int const YKFNFCReadErrorNoTokenAfterScanCode = 1;
+NSString* const YKFNFCReadErrorNoTokenAfterScanDescription = @"NFC scan succeeded but no OTP tokens could be detected. Please try again with a compatible key.";
+
+@implementation YKFNFCError
+
++ (YKFNFCError *)noTokenAfterScanError {
+    return [[YKFNFCError alloc] initWithCode:YKFNFCReadErrorNoTokenAfterScanCode
+                                     description:YKFNFCReadErrorNoTokenAfterScanDescription];
+}
+
+- (instancetype)initWithCode:(int)code description:(NSString *)description {
+    return [super initWithDomain:YKFNFCErrorDomain code:code userInfo:@{NSLocalizedDescriptionKey: description}];
+}
+
+@end
diff --git a/YubiKit/YubiKit/Sessions/NFCSession/YKFNFCSession.h b/YubiKit/YubiKit/Sessions/NFCSession/YKFNFCSession.h
new file mode 100755
index 000000000..224461ee4
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/NFCSession/YKFNFCSession.h
@@ -0,0 +1,137 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+#import "YKFNFCTagDescription.h"
+#import "YKFNFCOTPService.h"
+#import "YKFKeyU2FService.h"
+#import "YKFKeyFIDO2Service.h"
+#import "YKFKeyOATHService.h"
+#import "YKFKeyRawCommandService.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+typedef NS_ENUM(NSUInteger, YKFNFCISO7816SessionState) {
+    YKFNFCISO7816SessionStateClosed,
+    YKFNFCISO7816SessionStatePooling,
+    YKFNFCISO7816SessionStateOpening,
+    YKFNFCISO7816SessionStateOpen
+};
+
+@protocol YKFNFCSessionProtocol<NSObject>
+
+/*!
+ @property sessionState
+ 
+ @abstract
+    This property allows to check and observe the state of the connection with the YubiKey.
+ 
+ NOTE:
+    This is a KVO compliant property. Observe it to get updates when the key is connected.
+ */
+@property (nonatomic, assign, readonly) YKFNFCISO7816SessionState iso7816SessionState;
+
+/*!
+ @property tagDescription
+ 
+ @returns
+    The description of the discovered tag.
+ 
+ NOTE:
+    This property becomes available when the tag is discovered and is nil when the tas is lost.
+ */
+@property (nonatomic, readonly, nullable) YKFNFCTagDescription *tagDescription API_AVAILABLE(ios(13.0));
+
+
+/*!
+ @property otpService
+ 
+ @abstract
+    The shared object to interact with the OTP application from the YubiKey. This property is
+    always available and handles its own NFC NDEF session.
+ */
+@property (nonatomic, readonly) id<YKFNFCOTPServiceProtocol> otpService API_AVAILABLE(ios(11.0));
+
+/*!
+ @property u2fService
+ 
+ @abstract
+    The shared object to interact with the U2F application from the YubiKey.
+    This property becomes available when the key is connected and the session opened and is nil when
+    the session is closed. This property should be accessed based on the session state.
+ */
+@property (nonatomic, readonly, nullable) id<YKFKeyU2FServiceProtocol> u2fService API_AVAILABLE(ios(13.0));
+
+/*!
+ @property fido2Service
+ 
+ @abstract
+    The shared object to interact with the FIDO2 application from the YubiKey.
+    This property becomes available when the key is connected and the session opened and is nil when
+    the session is closed. This property should be accessed based on the session state.
+ */
+@property (nonatomic, readonly, nullable) id<YKFKeyFIDO2ServiceProtocol> fido2Service API_AVAILABLE(ios(13.0));
+
+/*!
+ @property oathService
+ 
+ @abstract
+    The shared object to interact with the OATH application from the YubiKey.
+    This property becomes available when the key is connected and the session opened and is nil when
+    the session is closed. This property should be accessed based on the session state.
+ */
+@property (nonatomic, readonly, nullable) id<YKFKeyOATHServiceProtocol> oathService API_AVAILABLE(ios(13.0));
+
+/*!
+ @property rawCommandService
+ 
+ @abstract
+    The shared object which provides an interface to send raw commands to the YubiKey.
+    This property becomes available when the key is connected and the session opened and is nil when
+    the session is closed. This property should be accessed based on the session state.
+ */
+@property (nonatomic, readonly, nullable) id<YKFKeyRawCommandServiceProtocol> rawCommandService API_AVAILABLE(ios(13.0));
+
+/*!
+ @method startIso7816Session
+ 
+ @abstract
+    To allow the session to connect and interract with the YubiKey, the session needs to be started.
+ */
+- (void)startIso7816Session API_AVAILABLE(ios(13.0));
+
+/*!
+ @method stopIso7816Session
+ 
+ @abstract
+    Closes the communication with the key and disables the key connection events.
+ */
+- (void)stopIso7816Session API_AVAILABLE(ios(13.0));
+
+/*!
+ @method cancelCommands
+ 
+ @abstract:
+    Cancels all issued commands to the key, which are still in the processing queue but not yet started. This method
+    would be usually called when the user wants to cancel an operation in the UI and the application also cancels the
+    requests to the key.
+ */
+- (void)cancelCommands API_AVAILABLE(ios(13.0));
+
+@end
+
+@interface YKFNFCSession: NSObject<YKFNFCSessionProtocol>
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/YubiKit/YubiKit/Sessions/NFCSession/YKFNFCSession.m b/YubiKit/YubiKit/Sessions/NFCSession/YKFNFCSession.m
new file mode 100755
index 000000000..080bba234
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/NFCSession/YKFNFCSession.m
@@ -0,0 +1,244 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <CoreNFC/CoreNFC.h>
+
+#import "YubiKitDeviceCapabilities.h"
+#import "YubiKitExternalLocalization.h"
+
+#import "YKFNFCConnectionController.h"
+#import "YKFNFCSession.h"
+#import "YKFBlockMacros.h"
+#import "YKFLogger.h"
+#import "YKFAssert.h"
+
+#import "YKFNFCOTPService+Private.h"
+#import "YKFKeyU2FService+Private.h"
+#import "YKFKeyFIDO2Service+Private.h"
+#import "YKFKeyOATHService+Private.h"
+#import "YKFKeyRawCommandService+Private.h"
+#import "YKFNFCTagDescription+Private.h"
+
+@interface YKFNFCSession()<NFCTagReaderSessionDelegate>
+
+@property (nonatomic, readwrite) YKFNFCISO7816SessionState iso7816SessionState;
+
+@property (nonatomic, readwrite) YKFNFCTagDescription *tagDescription API_AVAILABLE(ios(13.0));
+
+@property (nonatomic, readwrite) YKFNFCOTPService *otpService API_AVAILABLE(ios(11.0));
+@property (nonatomic, readwrite) YKFKeyU2FService *u2fService API_AVAILABLE(ios(13.0));
+@property (nonatomic, readwrite) YKFKeyFIDO2Service *fido2Service API_AVAILABLE(ios(13.0));
+@property (nonatomic, readwrite) YKFKeyOATHService *oathService API_AVAILABLE(ios(13.0));
+@property (nonatomic, readwrite) YKFKeyRawCommandService *rawCommandService API_AVAILABLE(ios(13.0));
+
+@property (nonatomic) id<YKFKeyConnectionControllerProtocol> connectionController;
+
+@property (nonatomic) NSOperationQueue *communicationQueue;
+@property (nonatomic) dispatch_queue_t sharedDispatchQueue;
+
+@property (nonatomic) NFCTagReaderSession *nfcTagReaderSession API_AVAILABLE(ios(13.0));
+@property (nonatomic) id<NFCISO7816Tag> iso7816NfcTag API_AVAILABLE(ios(13.0));
+
+@property (nonatomic) NSTimer *iso7816NfcTagAvailabilityTimer;
+
+@end
+
+@implementation YKFNFCSession
+
+- (instancetype)init {
+    self = [super init];
+    if (self) {
+        if (@available(iOS 11, *)) {
+            // Init with defaults
+            self.otpService = [[YKFNFCOTPService alloc] initWithTokenParser:nil session:nil];
+        }
+        [self setupCommunicationQueue];
+    }
+    return self;
+}
+
+- (void)dealloc {
+    if (@available(iOS 13.0, *)) {
+        [self unobserveIso7816TagAvailability];
+    }
+}
+
+#pragma mark - Property updates
+
+- (void)updateIso7816SessionSate:(YKFNFCISO7816SessionState)state {
+    if (self.iso7816SessionState == state) {
+        return;
+    }
+    self.iso7816SessionState = state;
+}
+
+#pragma mark - Session lifecycle
+
+- (void)startIso7816Session API_AVAILABLE(ios(13.0)) {
+    YKFAssertReturn(YubiKitDeviceCapabilities.supportsISO7816NFCTags, @"Cannot start the NFC session on an unsupported device.");
+    
+    if (self.nfcTagReaderSession) {
+        YKFLogInfo(@"NFC session already started. Ignoring start request.");
+        return;
+    }
+    
+    self.nfcTagReaderSession = [[NFCTagReaderSession alloc] initWithPollingOption:NFCPollingISO14443 delegate:self queue:nil];
+    self.nfcTagReaderSession.alertMessage = YubiKitExternalLocalization.nfcScanAlertMessage;
+    [self.nfcTagReaderSession beginSession];
+}
+
+- (void)stopIso7816Session API_AVAILABLE(ios(13.0)) {
+    if (!self.nfcTagReaderSession) {
+        YKFLogInfo(@"NFC session already stopped. Ignoring stop request.");
+        return;
+    }
+    
+    [self.nfcTagReaderSession invalidateSession];
+    self.nfcTagReaderSession = nil;
+}
+
+- (void)cancelCommands API_AVAILABLE(ios(13.0)) {
+    [self.connectionController cancelAllCommands];
+}
+
+#pragma mark - Shared communication queue
+
+- (void)setupCommunicationQueue {
+    // Create a sequential queue because the YubiKey accepts sequential commands.
+    
+    self.communicationQueue = [[NSOperationQueue alloc] init];
+    self.communicationQueue.maxConcurrentOperationCount = 1;
+    
+    dispatch_queue_attr_t dispatchQueueAttributes = dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, DISPATCH_QUEUE_PRIORITY_HIGH, -1);
+    self.sharedDispatchQueue = dispatch_queue_create("com.yubico.YKCOMNFC", dispatchQueueAttributes);
+    
+    self.communicationQueue.underlyingQueue = self.sharedDispatchQueue;
+}
+
+#pragma mark - NFCTagReaderSessionDelegate
+
+- (void)tagReaderSession:(NFCTagReaderSession *)session didInvalidateWithError:(NSError *)error API_AVAILABLE(ios(13.0)) {
+    YKFLogNSError(error);
+    [self updateServicesForTag:nil state:YKFNFCISO7816SessionStateClosed];
+}
+
+- (void)tagReaderSessionDidBecomeActive:(NFCTagReaderSession *)session API_AVAILABLE(ios(13.0)) {
+    YKFLogInfo(@"NFC session did become active.");
+    [self updateIso7816SessionSate:YKFNFCISO7816SessionStatePooling];
+}
+
+- (void)tagReaderSession:(NFCTagReaderSession *)session didDetectTags:(NSArray<__kindof id<NFCTag>> *)tags API_AVAILABLE(ios(13.0)) {
+    YKFLogInfo(@"NFC session did detect tags.");
+    
+    if (!tags.count) {
+        return;
+    }
+    id<NFCISO7816Tag> activeTag = nil;
+    for (id<NFCTag> tag in tags) {
+        if (tag.type == NFCTagTypeISO7816Compatible) {
+            activeTag = [tag asNFCISO7816Tag];
+            break;
+        }
+    }
+    if (!activeTag) {
+        return;
+    }
+    
+    [self updateServicesForTag:activeTag state:YKFNFCISO7816SessionStateOpening];
+
+    ykf_weak_self();
+    [self.nfcTagReaderSession connectToTag:activeTag completionHandler:^(NSError *error) {
+        ykf_safe_strong_self();
+        if (error) {
+            [strongSelf updateServicesForTag:activeTag state:YKFNFCISO7816SessionStateClosed];
+            YKFLogNSError(error);
+            self.nfcTagReaderSession = nil;
+            return;
+        }
+        
+        YKFLogInfo(@"NFC session did connect to tag.");
+        [strongSelf updateServicesForTag:activeTag state:YKFNFCISO7816SessionStateOpen];
+    }];
+}
+
+#pragma mark - Helpers
+
+- (void)updateServicesForTag:(id<NFCISO7816Tag>)tag state:(YKFNFCISO7816SessionState)state API_AVAILABLE(ios(13.0)) {
+    if (self.iso7816SessionState == state) {
+        return;
+    }
+    switch (state) {
+        case YKFNFCISO7816SessionStateClosed:
+            self.u2fService = nil;
+            self.fido2Service = nil;
+            self.rawCommandService = nil;
+            self.oathService = nil;
+            self.connectionController = nil;
+            
+            [self unobserveIso7816TagAvailability];
+            self.iso7816NfcTag = nil;
+            
+            [self.nfcTagReaderSession invalidateSession];
+            self.nfcTagReaderSession = nil;
+            self.tagDescription = nil;
+            break;
+        
+        case YKFNFCISO7816SessionStatePooling:
+            break;
+            
+        case YKFNFCISO7816SessionStateOpening:
+            break;
+
+        case YKFNFCISO7816SessionStateOpen:
+            self.iso7816NfcTag = tag;
+            [self observeIso7816TagAvailability];
+            
+            self.connectionController = [[YKFNFCConnectionController alloc] initWithNFCTag:tag operationQueue:self.communicationQueue];
+            self.u2fService = [[YKFKeyU2FService alloc] initWithConnectionController:self.connectionController];
+            self.fido2Service = [[YKFKeyFIDO2Service alloc] initWithConnectionController:self.connectionController];
+            self.oathService = [[YKFKeyOATHService alloc] initWithConnectionController:self.connectionController];
+            self.rawCommandService = [[YKFKeyRawCommandService alloc] initWithConnectionController:self.connectionController];
+            self.tagDescription = [[YKFNFCTagDescription alloc] initWithTag: tag];
+            break;
+    }
+    [self updateIso7816SessionSate:state];
+}
+
+#pragma mark - Tag availability observation
+
+- (void)observeIso7816TagAvailability API_AVAILABLE(ios(13.0)) {
+    // Note: A timer is used because the "available" property is not KVO observable and the tag has no delegate.
+    // This solution is suboptimal but in line with some examples from Apple using a dispatch queue.
+    ykf_weak_self();
+    self.iso7816NfcTagAvailabilityTimer = [[NSTimer alloc] initWithFireDate:[NSDate date] interval:0.5 repeats:YES block:^(NSTimer *timer) {
+        ykf_safe_strong_self();
+        BOOL available = strongSelf.iso7816NfcTag.available;
+        if (available) {
+            YKFLogVerbose(@"NFC tag is available.");
+        } else {
+            YKFLogInfo(@"NFC tag is no longer available.");
+            [strongSelf updateServicesForTag:nil state:YKFNFCISO7816SessionStateClosed];
+        }
+    }];
+    [[NSRunLoop mainRunLoop] addTimer:self.iso7816NfcTagAvailabilityTimer forMode:NSDefaultRunLoopMode];
+}
+
+- (void)unobserveIso7816TagAvailability API_AVAILABLE(ios(13.0)) {
+    // Note: A timer is used because the "available" property is not KVO observable and the tag has no delegate.
+    // This solution is suboptimal but in line with some examples from Apple using a dispatch queue.
+    [self.iso7816NfcTagAvailabilityTimer invalidate];
+    self.iso7816NfcTagAvailabilityTimer = nil;
+}
+
+@end
diff --git a/YubiKit/YubiKit/Sessions/NFCSession/YKFNFCTagDescription+Private.h b/YubiKit/YubiKit/Sessions/NFCSession/YKFNFCTagDescription+Private.h
new file mode 100755
index 000000000..99168354f
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/NFCSession/YKFNFCTagDescription+Private.h
@@ -0,0 +1,19 @@
+//
+//  YKFNFCTagDescription+Private.h
+//  YubiKit
+//
+//  Created by Irina Makhalova on 9/30/19.
+//  Copyright © 2019 Yubico. All rights reserved.
+//
+
+#ifndef YKFNFCTagDescription_Private_h
+#define YKFNFCTagDescription_Private_h
+
+@interface YKFNFCTagDescription()
+
+- (nullable instancetype)initWithTag:(nullable id<NFCISO7816Tag>)tag;
+
+@end
+
+
+#endif /* YKFNFCTagDescription_Private_h */
diff --git a/YubiKit/YubiKit/Sessions/NFCSession/YKFNFCTagDescription.h b/YubiKit/YubiKit/Sessions/NFCSession/YKFNFCTagDescription.h
new file mode 100755
index 000000000..95926765b
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/NFCSession/YKFNFCTagDescription.h
@@ -0,0 +1,48 @@
+//
+//  YKFNFCTagDescription.h
+//  YubiKit
+//
+//  Created by Irina Makhalova on 9/30/19.
+//  Copyright © 2019 Yubico. All rights reserved.
+//
+
+#import <Foundation/Foundation.h>
+#import <CoreNFC/CoreNFC.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+API_AVAILABLE(ios(13.0))
+
+/*!
+@class YKFNFCTagDescription
+
+@abstract
+   Provides a list of properties describing the connected key.
+*/
+@interface YKFNFCTagDescription : NSObject
+
+/*!
+ @property identifier
+ 
+ @abstract
+    The hardware UID of the tag.
+ */
+@property(nonatomic, readonly) NSData *identifier;
+
+/*!
+ @property historicalBytes
+ 
+ @abstract
+    The historical bytes extracted from the Type A Answer To Select response.
+ */
+@property(nonatomic, readonly) NSData *historicalBytes;
+
+
+/*
+ Not available: access the instance provided by YKFNFCSession.
+ */
+- (instancetype)init NS_UNAVAILABLE;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/YubiKit/YubiKit/Sessions/NFCSession/YKFNFCTagDescription.m b/YubiKit/YubiKit/Sessions/NFCSession/YKFNFCTagDescription.m
new file mode 100755
index 000000000..18fe42b80
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/NFCSession/YKFNFCTagDescription.m
@@ -0,0 +1,38 @@
+//
+//  YKFNFCTagDescription.m
+//  YubiKit
+//
+//  Created by Irina Makhalova on 9/30/19.
+//  Copyright © 2019 Yubico. All rights reserved.
+//
+
+#import "YKFNFCTagDescription.h"
+#import "YKFNFCTagDescription+Private.h"
+#import "YKFAssert.h"
+
+@interface YKFNFCTagDescription()
+
+@property(nonatomic, readwrite) NSData *identifier;
+@property(nonatomic, readwrite) NSData *historicalBytes;
+
+@end
+
+@implementation YKFNFCTagDescription
+
+- (instancetype)initWithTag:(id<NFCISO7816Tag>)tag  {
+    YKFAssertAbortInit(tag);
+
+    self = [super init];
+    if (self) {
+        NSAssert(tag.identifier, @"Identifier is not provided by the tag.");
+        self.identifier = tag.identifier;
+        YKFAssertAbortInit(self.identifier);
+
+        NSAssert(tag.historicalBytes, @"Historical bytes are not provided by the tag.");
+        self.historicalBytes = tag.historicalBytes;
+        YKFAssertAbortInit(self.historicalBytes);
+    }
+
+    return self;
+}
+@end
diff --git a/YubiKit/YubiKit/Sessions/NFCSession/YKFURIIdentifierCode.h b/YubiKit/YubiKit/Sessions/NFCSession/YKFURIIdentifierCode.h
new file mode 100755
index 000000000..3731d1e22
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/NFCSession/YKFURIIdentifierCode.h
@@ -0,0 +1,21 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+
+@interface YKFURIIdentifierCode : NSObject
+
+- (nullable NSString *)prependingStringForCode:(UInt8)code;
+
+@end
diff --git a/YubiKit/YubiKit/Sessions/NFCSession/YKFURIIdentifierCode.m b/YubiKit/YubiKit/Sessions/NFCSession/YKFURIIdentifierCode.m
new file mode 100755
index 000000000..2e9e8e937
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/NFCSession/YKFURIIdentifierCode.m
@@ -0,0 +1,78 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "YKFURIIdentifierCode.h"
+
+@implementation YKFURIIdentifierCode
+
+// Contains the most common URI Identifier codes used to shorten the URI in a NDEF URI type payload.
+static NSDictionary *codesDictionary;
+
+- (instancetype)init {
+    self = [super init];
+    if (self) {
+        static dispatch_once_t onceToken;
+        dispatch_once(&onceToken, ^{
+            [self setupIdentifierCodes];
+        });
+    }
+    return self;
+}
+
+- (void)setupIdentifierCodes {
+    codesDictionary = @{
+      @(0x00): @"", // No prepending is done.
+      @(0x01): @"http://www.",
+      @(0x02): @"https://www.",
+      @(0x03): @"http://",
+      @(0x04): @"https://",
+      @(0x05): @"tel:",
+      @(0x06): @"mailto:",
+      @(0x07): @"ftp://anonymous:anonymous@",
+      @(0x08): @"ftp://ftp.",
+      @(0x09): @"ftps://",
+      @(0x0A): @"sftp://",
+      @(0x0B): @"smb://",
+      @(0x0C): @"nfs://",
+      @(0x0D): @"ftp://",
+      @(0x0E): @"dav://",
+      @(0x0F): @"news:",
+      @(0x10): @"telnet://",
+      @(0x11): @"imap:",
+      @(0x12): @"rtsp://",
+      @(0x13): @"urn:",
+      @(0x14): @"pop:",
+      @(0x15): @"sip:",
+      @(0x16): @"sips:",
+      @(0x17): @"tftp:",
+      @(0x18): @"btspp://",
+      @(0x19): @"btl2cap://",
+      @(0x1A): @"btgoep://",
+      @(0x1B): @"tcpobex://",
+      @(0x1C): @"irdaobex://",
+      @(0x1D): @"file://",
+      @(0x1E): @"urn:epc:id:",
+      @(0x1F): @"urn:epc:tag:",
+      @(0x20): @"urn:epc:pat:",
+      @(0x21): @"urn:epc:raw:",
+      @(0x22): @"urn:epc:",
+      @(0x23): @"urn:nfc:"
+    };
+}
+
+- (NSString *)prependingStringForCode:(UInt8)code {
+    return codesDictionary[@(code)];
+}
+
+@end
diff --git a/YubiKit/YubiKit/Sessions/QRReaderSession/Views/YKFQRCodeScanOverlayView.h b/YubiKit/YubiKit/Sessions/QRReaderSession/Views/YKFQRCodeScanOverlayView.h
new file mode 100755
index 000000000..a9c9f302f
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/QRReaderSession/Views/YKFQRCodeScanOverlayView.h
@@ -0,0 +1,33 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+#import <UIKit/UIKit.h>
+
+#import "YKFView.h"
+
+@class YKFQRCodeScanOverlayView;
+@protocol YKFQRCodeScanOverlayViewDelegate<NSObject>
+
+- (void)qrCodeScanControlsOverlayViewDidDismiss:(YKFQRCodeScanOverlayView *)view;
+
+@end
+
+@interface YKFQRCodeScanOverlayView : YKFView
+
+@property (nonatomic, weak) id<YKFQRCodeScanOverlayViewDelegate> delegate;
+
+- (void)showCameraPermissionsNotGranted;
+
+@end
diff --git a/YubiKit/YubiKit/Sessions/QRReaderSession/Views/YKFQRCodeScanOverlayView.m b/YubiKit/YubiKit/Sessions/QRReaderSession/Views/YKFQRCodeScanOverlayView.m
new file mode 100755
index 000000000..f9c5bbeeb
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/QRReaderSession/Views/YKFQRCodeScanOverlayView.m
@@ -0,0 +1,173 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "YKFQRCodeScanOverlayView.h"
+#import "YubiKitExternalLocalization.h"
+#import "YubiKitManager.h"
+#import "UIWindowAdditions.h"
+
+@interface YKFQRCodeScanOverlayView()
+
+@property (nonatomic) UIButton *dismissButton;
+@property (nonatomic) UILabel *scanHintLabel;
+@property (nonatomic) UILabel *cameraPermissionLabel;
+
+@end
+
+@implementation YKFQRCodeScanOverlayView
+
+- (instancetype)initWithFrame:(CGRect)frame {
+    self = [super initWithFrame:frame];
+    if (self) {
+        [self setupControls];
+    }
+    return self;
+}
+
+- (void)showCameraPermissionsNotGranted {
+    self.cameraPermissionLabel.hidden = NO;
+    self.scanHintLabel.hidden = YES;
+}
+
+#pragma mark - Actions
+
+- (void)dismissButtonDidPress:(id)sender {
+    [self.delegate qrCodeScanControlsOverlayViewDidDismiss:self];
+}
+
+#pragma mark - Localization
+
+- (void)setupLocalization {
+    [self.dismissButton setTitle:YubiKitExternalLocalization.qrCodeScanDismissButtonTitle forState:UIControlStateNormal];
+    self.scanHintLabel.text = YubiKitExternalLocalization.qrCodeScanHintMessage;
+    self.cameraPermissionLabel.text = YubiKitExternalLocalization.qrCodeScanCameraNotAvailableMessage;
+}
+
+#pragma mark - UI setup
+
+- (void)setupControls {
+    self.backgroundColor = [UIColor clearColor];
+    [self setupCameraPermissionLabel];
+    [self setupDismissButton];
+    [self setupScanHintLabel];
+}
+
+- (void)setupDismissButton {
+    self.dismissButton = [[UIButton alloc] initWithFrame:CGRectZero];
+    [self.dismissButton addTarget:self action:@selector(dismissButtonDidPress:) forControlEvents:UIControlEventTouchUpInside];
+    self.dismissButton.translatesAutoresizingMaskIntoConstraints = NO;
+    self.dismissButton.backgroundColor = [UIColor blackColor];
+    self.dismissButton.alpha = 0.8;
+    self.dismissButton.layer.cornerRadius = 18;
+    
+    [self addSubview: self.dismissButton];
+    
+    UIEdgeInsets safeAreaInsets = UIApplication.sharedApplication.keyWindow.ykf_safeAreaInsets;
+    
+    NSLayoutConstraint *width = [NSLayoutConstraint constraintWithItem:self.dismissButton attribute:NSLayoutAttributeWidth
+                                                             relatedBy:NSLayoutRelationEqual toItem:nil
+                                                             attribute:NSLayoutAttributeWidth multiplier:1 constant:200];
+    NSLayoutConstraint *height = [NSLayoutConstraint constraintWithItem:self.dismissButton attribute:NSLayoutAttributeHeight
+                                                              relatedBy:NSLayoutRelationEqual toItem:nil
+                                                              attribute:NSLayoutAttributeHeight multiplier:1 constant:36];
+    NSLayoutConstraint *horizontalAlign = [NSLayoutConstraint constraintWithItem:self.dismissButton attribute:NSLayoutAttributeCenterX
+                                                                       relatedBy:NSLayoutRelationEqual toItem:self
+                                                                       attribute:NSLayoutAttributeCenterX multiplier:1 constant:0];
+    NSLayoutConstraint *bottom = [NSLayoutConstraint constraintWithItem:self.dismissButton attribute:NSLayoutAttributeBottom
+                                                              relatedBy:NSLayoutRelationEqual toItem:self
+                                                              attribute:NSLayoutAttributeBottom multiplier:1 constant: -(30 + safeAreaInsets.bottom)];
+    
+    NSArray *constraints = [[NSArray alloc] initWithObjects:width, height, horizontalAlign, bottom, nil];
+    [self addConstraints:constraints];
+}
+
+- (void)setupScanHintLabel {
+    UIEdgeInsets safeAreaInsets = UIApplication.sharedApplication.keyWindow.ykf_safeAreaInsets;
+    
+    UIView *hintBackgroundView = [[UIView alloc] initWithFrame:CGRectZero];
+    hintBackgroundView.translatesAutoresizingMaskIntoConstraints = NO;
+    hintBackgroundView.backgroundColor = [UIColor blackColor];
+    hintBackgroundView.alpha = 0.5;
+    
+    [self addSubview:hintBackgroundView];
+    
+    NSLayoutConstraint *height = [NSLayoutConstraint constraintWithItem:hintBackgroundView attribute:NSLayoutAttributeHeight
+                                                              relatedBy:NSLayoutRelationEqual toItem:nil
+                                                              attribute:NSLayoutAttributeHeight multiplier:1 constant:120 + safeAreaInsets.top];
+    NSLayoutConstraint *top = [NSLayoutConstraint constraintWithItem:hintBackgroundView attribute:NSLayoutAttributeTop
+                                                           relatedBy:NSLayoutRelationEqual toItem:self
+                                                           attribute:NSLayoutAttributeTop multiplier:1 constant:0];
+    NSLayoutConstraint *left = [NSLayoutConstraint constraintWithItem:hintBackgroundView attribute:NSLayoutAttributeLeading
+                                                            relatedBy:NSLayoutRelationEqual toItem:self
+                                                            attribute:NSLayoutAttributeLeading multiplier:1 constant:0];
+    NSLayoutConstraint *right = [NSLayoutConstraint constraintWithItem:hintBackgroundView attribute:NSLayoutAttributeTrailing
+                                                            relatedBy:NSLayoutRelationEqual toItem:self
+                                                            attribute:NSLayoutAttributeTrailing multiplier:1 constant:0];
+    
+    NSArray *constraints = [[NSArray alloc] initWithObjects:height, top, left, right, nil];
+    [self addConstraints:constraints];
+    
+    self.scanHintLabel = [[UILabel alloc] initWithFrame:CGRectZero];
+    self.scanHintLabel.translatesAutoresizingMaskIntoConstraints = NO;
+    self.scanHintLabel.textColor = [UIColor whiteColor];
+    self.scanHintLabel.textAlignment = NSTextAlignmentCenter;
+    self.scanHintLabel.numberOfLines = 3;
+    
+    [self addSubview:self.scanHintLabel];
+    
+    height = [NSLayoutConstraint constraintWithItem:self.scanHintLabel attribute:NSLayoutAttributeHeight
+                                          relatedBy:NSLayoutRelationEqual toItem:nil
+                                          attribute:NSLayoutAttributeHeight multiplier:1 constant:120];
+    top = [NSLayoutConstraint constraintWithItem:self.scanHintLabel attribute:NSLayoutAttributeTop
+                                       relatedBy:NSLayoutRelationEqual toItem:self
+                                       attribute:NSLayoutAttributeTop multiplier:1 constant:safeAreaInsets.top];
+    left = [NSLayoutConstraint constraintWithItem:self.scanHintLabel attribute:NSLayoutAttributeLeading
+                                        relatedBy:NSLayoutRelationEqual toItem:self
+                                        attribute:NSLayoutAttributeLeading multiplier:1 constant:0];
+    right = [NSLayoutConstraint constraintWithItem:self.scanHintLabel attribute:NSLayoutAttributeTrailing
+                                         relatedBy:NSLayoutRelationEqual toItem:self
+                                         attribute:NSLayoutAttributeTrailing multiplier:1 constant:0];
+    
+    constraints = [[NSArray alloc] initWithObjects:height, top, left, right, nil];
+    [self addConstraints:constraints];
+}
+
+- (void)setupCameraPermissionLabel {
+    self.cameraPermissionLabel = [[UILabel alloc] initWithFrame:CGRectZero];
+    self.cameraPermissionLabel.translatesAutoresizingMaskIntoConstraints = NO;
+    self.cameraPermissionLabel.textColor = [UIColor whiteColor];
+    self.cameraPermissionLabel.textAlignment = NSTextAlignmentCenter;
+    self.cameraPermissionLabel.numberOfLines = 5;
+    self.cameraPermissionLabel.hidden = YES;
+    
+    [self addSubview:self.cameraPermissionLabel];
+    
+    NSLayoutConstraint *top = [NSLayoutConstraint constraintWithItem:self.cameraPermissionLabel attribute:NSLayoutAttributeTop
+                                                           relatedBy:NSLayoutRelationEqual toItem:self
+                                                           attribute:NSLayoutAttributeTop multiplier:1 constant:0];
+    NSLayoutConstraint *bottom = [NSLayoutConstraint constraintWithItem:self.cameraPermissionLabel attribute:NSLayoutAttributeBottom
+                                                              relatedBy:NSLayoutRelationEqual toItem:self
+                                                              attribute:NSLayoutAttributeBottom multiplier:1 constant:-20];
+    NSLayoutConstraint *left = [NSLayoutConstraint constraintWithItem:self.cameraPermissionLabel attribute:NSLayoutAttributeLeading
+                                                            relatedBy:NSLayoutRelationEqual toItem:self
+                                                            attribute:NSLayoutAttributeLeading multiplier:1 constant:30];
+    NSLayoutConstraint *right = [NSLayoutConstraint constraintWithItem:self.cameraPermissionLabel attribute:NSLayoutAttributeTrailing
+                                                             relatedBy:NSLayoutRelationEqual toItem:self
+                                                             attribute:NSLayoutAttributeTrailing multiplier:1 constant:-30];
+    
+    NSArray *constraints = [[NSArray alloc] initWithObjects:top, bottom, left, right, nil];
+    [self addConstraints:constraints];
+}
+
+@end
diff --git a/YubiKit/YubiKit/Sessions/QRReaderSession/YKFQRCodeScanError+Errors.h b/YubiKit/YubiKit/Sessions/QRReaderSession/YKFQRCodeScanError+Errors.h
new file mode 100755
index 000000000..d302cc789
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/QRReaderSession/YKFQRCodeScanError+Errors.h
@@ -0,0 +1,29 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "YKFQRCodeScanError.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface YKFQRCodeScanError(Errors)
+
++ (YKFQRCodeScanError *)noCameraAvailableError;
++ (YKFQRCodeScanError *)unableToCreateCaptureDeviceInputError;
++ (YKFQRCodeScanError *)unableToAddDeviceInputError;
++ (YKFQRCodeScanError *)unableToAddQrDetectorError;
++ (YKFQRCodeScanError *)noDataAvailableError;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/YubiKit/YubiKit/Sessions/QRReaderSession/YKFQRCodeScanError.h b/YubiKit/YubiKit/Sessions/QRReaderSession/YKFQRCodeScanError.h
new file mode 100755
index 000000000..efc221a88
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/QRReaderSession/YKFQRCodeScanError.h
@@ -0,0 +1,76 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+
+
+/*!
+ @constant
+    YKFQRCodeScanErrorDomain
+ @abstract
+    Domain for errors from the QR code scanning.
+ */
+extern NSString* _Nonnull const YKFQRCodeScanErrorDomain;
+
+/*!
+ @class
+    YKFQRCodeScanError
+ @abstract
+    Error from QR Code scanning.
+ */
+@interface YKFQRCodeScanError : NSError
+
+- (nonnull instancetype)init NS_UNAVAILABLE;
+
+@end
+
+/*!
+ @constant
+    YKFQRCodeScanErrorNoCameraAvailableCode
+ @abstract
+    When the camera device is not available.
+ */
+extern int const YKFQRCodeScanErrorNoCameraAvailableCode;
+
+/*!
+ @constant
+    YKFQRCodeScanErrorUnableToCreateCaptureDeviceInputCode
+ @abstract
+    When the library is not able to create a device input to attach to the capture device (camera).
+ */
+extern int const YKFQRCodeScanErrorUnableToCreateCaptureDeviceInputCode;
+
+/*!
+ @constant
+    YKFQRCodeScanErrorUnableToAddDeviceInputCode
+ @abstract
+    When the library is not able to attach the capture input to the capture device (camera).
+ */
+extern int const YKFQRCodeScanErrorUnableToAddDeviceInputCode;
+
+/*!
+ @constant
+    YKFQRCodeScanErrorUnableToAddQrDetectorCode
+ @abstract
+    When the library is not able to detect QR Codes.
+ */
+extern int const YKFQRCodeScanErrorUnableToAddQrDetectorCode;
+
+/*!
+ @constant
+    YKFQRCodeScanErrorNoDataAvailableCode
+ @abstract
+    When the library did read a QR Code but data could not be extracted from it.
+ */
+extern int const YKFQRCodeScanErrorNoDataAvailableCode;
diff --git a/YubiKit/YubiKit/Sessions/QRReaderSession/YKFQRCodeScanError.m b/YubiKit/YubiKit/Sessions/QRReaderSession/YKFQRCodeScanError.m
new file mode 100755
index 000000000..f28917692
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/QRReaderSession/YKFQRCodeScanError.m
@@ -0,0 +1,69 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "YKFQRCodeScanError.h"
+#import "YKFQRCodeScanError+Errors.h"
+
+NSString* const YKFQRCodeScanErrorDomain = @"YKQRCodeScanError";
+
+int const YKFQRCodeScanErrorNoCameraAvailableCode = 1;
+NSString* const YKFQRCodeScanErrorNoCameraAvailableDescription = @"No capture device available.";
+
+int const YKFQRCodeScanErrorUnableToCreateCaptureDeviceInputCode = 2;
+NSString* const YKFQRCodeScanErrorUnableToCreateCaptureDeviceInputDescription = @"Unable to create capture device input.";
+
+int const YKFQRCodeScanErrorUnableToAddDeviceInputCode = 3;
+NSString* const YKFQRCodeScanErrorUnableToAddDeviceInputDescription = @"Unable to add capture input to capture device.";
+
+int const YKFQRCodeScanErrorUnableToAddQrDetectorCode = 4;
+NSString* const YKFQRCodeScanErrorUnableToAddQrDetectorDescription = @"Unable to add QR metadata detector output.";
+
+int const YKFQRCodeScanErrorNoDataAvailableCode = 5;
+NSString* const YKFQRCodeScanErrorNoDataAvailableDescription = @"No data after QR code scan.";
+
+
+@implementation YKFQRCodeScanError
+
++ (YKFQRCodeScanError *)noCameraAvailableError {
+    return [[YKFQRCodeScanError alloc] initWithCode:YKFQRCodeScanErrorNoCameraAvailableCode
+                                     description:YKFQRCodeScanErrorNoCameraAvailableDescription];
+}
+
++ (YKFQRCodeScanError *)unableToCreateCaptureDeviceInputError {
+    return [[YKFQRCodeScanError alloc] initWithCode:YKFQRCodeScanErrorUnableToCreateCaptureDeviceInputCode
+                                        description:YKFQRCodeScanErrorUnableToCreateCaptureDeviceInputDescription];
+}
+
++ (YKFQRCodeScanError *)unableToAddDeviceInputError {
+    return [[YKFQRCodeScanError alloc] initWithCode:YKFQRCodeScanErrorUnableToAddDeviceInputCode
+                                        description:YKFQRCodeScanErrorUnableToAddDeviceInputDescription];
+}
+
++ (YKFQRCodeScanError *)unableToAddQrDetectorError {
+    return [[YKFQRCodeScanError alloc] initWithCode:YKFQRCodeScanErrorUnableToAddQrDetectorCode
+                                        description:YKFQRCodeScanErrorUnableToAddQrDetectorDescription];
+}
+
++ (YKFQRCodeScanError *)noDataAvailableError {
+    return [[YKFQRCodeScanError alloc] initWithCode:YKFQRCodeScanErrorNoDataAvailableCode
+                                        description:YKFQRCodeScanErrorNoDataAvailableDescription];
+}
+
+#pragma mark - Creation
+
+- (instancetype)initWithCode:(int)code description:(NSString *)description {
+    return [super initWithDomain:YKFQRCodeScanErrorDomain code:code userInfo:@{NSLocalizedDescriptionKey: description}];
+}
+
+@end
diff --git a/YubiKit/YubiKit/Sessions/QRReaderSession/YKFQRCodeScanViewController.h b/YubiKit/YubiKit/Sessions/QRReaderSession/YKFQRCodeScanViewController.h
new file mode 100755
index 000000000..1de8e2c0f
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/QRReaderSession/YKFQRCodeScanViewController.h
@@ -0,0 +1,32 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+#import <UIKit/UIKit.h>
+#import "YKFViewController.h"
+
+@class YKFQRCodeScanViewController;
+@protocol YKFQRCodeScanViewControllerDelegate<NSObject>
+
+- (void)qrCodeScanViewController:(nonnull YKFQRCodeScanViewController *)viewController didScanPayload:(nonnull NSString *)payload;
+- (void)qrCodeScanViewController:(nonnull YKFQRCodeScanViewController *)viewController didFailWithError:(nonnull NSError *)error;
+- (void)qrCodeScanViewControllerDidCancel:(nonnull YKFQRCodeScanViewController *)viewController;
+
+@end
+
+@interface YKFQRCodeScanViewController : YKFViewController
+
+@property (nonatomic, weak, nullable) id<YKFQRCodeScanViewControllerDelegate> delegate;
+
+@end
diff --git a/YubiKit/YubiKit/Sessions/QRReaderSession/YKFQRCodeScanViewController.m b/YubiKit/YubiKit/Sessions/QRReaderSession/YKFQRCodeScanViewController.m
new file mode 100755
index 000000000..2f1eed84b
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/QRReaderSession/YKFQRCodeScanViewController.m
@@ -0,0 +1,284 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <AVFoundation/AVFoundation.h>
+
+#import "YKFQRCodeScanViewController.h"
+#import "YKFQRCodeScanOverlayView.h"
+#import "YKFPermissions.h"
+#import "YKFQRCodeScanError.h"
+#import "YKFLogger.h"
+#import "YKFBlockMacros.h"
+#import "YKFDispatch.h"
+
+#import "YKFQRCodeScanError+Errors.h"
+
+@interface YKFQRCodeScanViewController()<AVCaptureMetadataOutputObjectsDelegate, YKFQRCodeScanOverlayViewDelegate>
+
+@property (nonatomic) AVCaptureSession *captureSession;
+@property (nonatomic) AVCaptureVideoPreviewLayer *previewLayer;
+
+@property (nonatomic) id<YKFPermissionsProtocol> permissions;
+
+@property (nonatomic) YKFQRCodeScanOverlayView *controlsOverlayView;
+
+@end
+
+@implementation YKFQRCodeScanViewController
+
+- (instancetype)init {
+    self = [super init];
+    if (self) {
+        self.permissions = [[YKFPermissions alloc] init];
+    }
+    return self;
+}
+
+#pragma mark - View lifecycle
+
+- (void)viewDidLoad {
+    [super viewDidLoad];
+    [self setupViewAppearance];
+    [self setupCaptureAndOverlay];
+}
+
+- (void)viewDidAppear:(BOOL)animated {
+    [super viewDidAppear:animated];
+    
+    // Dispatch the capture session running on a next runloop to avoid animation hick-ups
+    ykf_weak_self();
+    ykf_dispatch_block_main(^{
+        [weakSelf startCaptureSession];
+    });
+}
+
+- (void)viewWillDisappear:(BOOL)animated {
+    [super viewWillDisappear:animated];
+    [self stopCaptureSession];
+}
+
+#pragma mark - Status bar
+
+- (UIStatusBarStyle)preferredStatusBarStyle {
+    return UIStatusBarStyleLightContent;
+}
+
+#pragma mark - Orientation
+
+- (UIInterfaceOrientationMask)supportedInterfaceOrientations {
+    return UIInterfaceOrientationMaskPortrait | UIInterfaceOrientationMaskLandscape;
+}
+
+- (BOOL)shouldAutorotate {
+    return YES;
+}
+
+- (void)viewDidLayoutSubviews {
+    [super viewDidLayoutSubviews];
+    
+    BOOL frameChanged = !CGRectEqualToRect(self.previewLayer.frame, self.view.layer.bounds);
+    if (!frameChanged) {
+        return;
+    }
+    
+    self.previewLayer.frame = self.view.layer.bounds;
+    [self updateVideoCaptureOrientation];
+}
+
+- (void)updateVideoCaptureOrientation {
+    AVCaptureConnection *captureConnection = self.previewLayer.connection;
+    
+    if (!captureConnection || !captureConnection.supportsVideoOrientation) {
+        return;
+    }
+    
+    UIDeviceOrientation deviceOrientation = UIDevice.currentDevice.orientation;
+    switch (deviceOrientation) {
+        case UIDeviceOrientationPortrait:
+            captureConnection.videoOrientation = AVCaptureVideoOrientationPortrait;
+            break;
+            
+        case UIDeviceOrientationPortraitUpsideDown:
+            captureConnection.videoOrientation = AVCaptureVideoOrientationPortraitUpsideDown;
+            break;
+            
+        case UIDeviceOrientationLandscapeLeft:
+            captureConnection.videoOrientation = AVCaptureVideoOrientationLandscapeRight;
+            break;
+            
+        case UIDeviceOrientationLandscapeRight:
+            captureConnection.videoOrientation = AVCaptureVideoOrientationLandscapeLeft;
+            break;
+            
+        case UIDeviceOrientationFaceUp:
+        case UIDeviceOrientationFaceDown:
+        case UIDeviceOrientationUnknown:
+            // Do nothing
+            break;
+    }
+}
+
+#pragma mark - Capture session handling
+
+- (void)startCaptureSession {
+    BOOL authorized = self.permissions.videoCaptureAuthorizationStatus == YKFPermissionAuthorizationStatusAuthorized;
+    if (authorized && !self.captureSession.isRunning) {
+        [self updateVideoCaptureOrientation];
+        [self.captureSession startRunning];
+    }
+}
+
+- (void)stopCaptureSession {
+    BOOL authorized = self.permissions.videoCaptureAuthorizationStatus == YKFPermissionAuthorizationStatusAuthorized;
+    if (authorized && self.captureSession.isRunning) {
+        [self.captureSession stopRunning];
+    }
+}
+
+#pragma mark - AVCaptureMetadataOutputObjectsDelegate
+
+- (void)captureOutput:(AVCaptureOutput *)output didOutputMetadataObjects:(NSArray<__kindof AVMetadataObject *> *)metadataObjects fromConnection:(AVCaptureConnection *)connection {
+    [self stopCaptureSession];
+    
+    AVMetadataObject *metadataObject = metadataObjects.firstObject;
+    if (!metadataObject) {
+        NSError *error = [YKFQRCodeScanError noDataAvailableError];
+        [self.delegate qrCodeScanViewController:self didFailWithError:error];
+        return;
+    }
+    
+    AVMetadataMachineReadableCodeObject *readableObject = (AVMetadataMachineReadableCodeObject *)metadataObject;
+    NSString *payload = readableObject.stringValue;
+    if (!payload) {
+        NSError *error = [YKFQRCodeScanError noDataAvailableError];
+        [self.delegate qrCodeScanViewController:self didFailWithError:error];
+        return;
+    }
+    
+    AudioServicesPlaySystemSound(kSystemSoundID_Vibrate);
+    [self.delegate qrCodeScanViewController:self didScanPayload:payload];
+    
+    YKFLogInfo(@"QR code scanned with value: %@", payload);
+}
+
+#pragma mark - UI Setup
+
+- (void)setupViewAppearance {
+    self.view.backgroundColor = [UIColor blackColor];
+}
+
+- (void)setupCaptureAndOverlay {
+    switch (self.permissions.videoCaptureAuthorizationStatus) {
+            
+        case YKFPermissionAuthorizationStatusAuthorized:
+            [self setupQRCodeDetection];
+            [self setupControlsOverlay];
+        break;
+            
+        case YKFPermissionAuthorizationStatusNotDetermined: {
+            ykf_weak_self();
+            [self.permissions requestVideoCaptureAuthorization:^(BOOL granted) {
+                ykf_safe_strong_self();
+                if (!granted) {
+                    ykf_dispatch_block_main(^{
+                        [strongSelf setupControlsOverlay];
+                        [strongSelf.controlsOverlayView showCameraPermissionsNotGranted];
+                    });
+                    return;
+                }                
+                ykf_dispatch_block_main(^{
+                    [strongSelf setupQRCodeDetection];
+                    [strongSelf setupControlsOverlay];
+                    [strongSelf startCaptureSession];
+                });
+            }];
+        }
+        break;
+            
+        case YKFPermissionAuthorizationStatusRestricted:
+        case YKFPermissionAuthorizationStatusDenied:
+            [self setupControlsOverlay];
+            [self.controlsOverlayView showCameraPermissionsNotGranted];
+    }
+}
+
+- (void)setupQRCodeDetection {
+    NSError *error = [self setupCaptureSession];
+    if (error) {
+        [self.delegate qrCodeScanViewController:self didFailWithError:error];
+        return;
+    }
+    
+    error = [self setupQRCodeDetector];
+    if (error) {
+        [self.delegate qrCodeScanViewController:self didFailWithError:error];
+        return;
+    }
+    
+    [self setupPreviewLayer];
+}
+
+- (NSError *)setupCaptureSession {
+    self.captureSession = [[AVCaptureSession alloc] init];
+    
+    AVCaptureDevice *captureDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
+    if (!captureDevice) {
+        return [YKFQRCodeScanError noCameraAvailableError];
+    }
+    
+    NSError *videoInputError;
+    AVCaptureDeviceInput *videoInput = [[AVCaptureDeviceInput alloc] initWithDevice:captureDevice error:&videoInputError];
+    if (videoInputError) {
+        return [YKFQRCodeScanError unableToCreateCaptureDeviceInputError];
+    }
+    
+    if ([self.captureSession canAddInput:videoInput]) {
+        [self.captureSession addInput:videoInput];
+        return nil;
+    }
+    
+    return [YKFQRCodeScanError unableToAddDeviceInputError];
+}
+
+- (NSError *)setupQRCodeDetector {
+    AVCaptureMetadataOutput *metadataOutput = [[AVCaptureMetadataOutput alloc] init];
+    if ([self.captureSession canAddOutput:metadataOutput]) {
+        [self.captureSession addOutput:metadataOutput];
+        [metadataOutput setMetadataObjectsDelegate:self queue:dispatch_get_main_queue()];
+        metadataOutput.metadataObjectTypes = @[AVMetadataObjectTypeQRCode];
+        return nil;
+    }
+    return [YKFQRCodeScanError unableToAddQrDetectorError];
+}
+
+- (void)setupPreviewLayer {
+    self.previewLayer = [[AVCaptureVideoPreviewLayer alloc] initWithSession:self.captureSession];
+    self.previewLayer.frame = self.view.layer.bounds;
+    self.previewLayer.videoGravity = AVLayerVideoGravityResizeAspectFill;
+    [self.view.layer addSublayer: self.previewLayer];
+}
+
+- (void)setupControlsOverlay {
+    self.controlsOverlayView = [[YKFQRCodeScanOverlayView alloc] initWithFrame:self.view.bounds];
+    self.controlsOverlayView.delegate = self;
+    [self pinViewToEdges:self.controlsOverlayView insets:UIEdgeInsetsZero];
+}
+
+#pragma mark - YKFQRCodeScanOverlayViewDelegate
+
+- (void)qrCodeScanControlsOverlayViewDidDismiss:(YKFQRCodeScanOverlayView *)view {
+    [self.delegate qrCodeScanViewControllerDidCancel:self];
+}
+
+@end
diff --git a/YubiKit/YubiKit/Sessions/QRReaderSession/YKFQRReaderSession.h b/YubiKit/YubiKit/Sessions/QRReaderSession/YKFQRReaderSession.h
new file mode 100755
index 000000000..c1a36fe1c
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/QRReaderSession/YKFQRReaderSession.h
@@ -0,0 +1,64 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+#import <UIKit/UIKit.h>
+
+/*!
+ @abstract
+    Response block used by scanQrCodeWithPresenter:completion: to provide the results of a QR Code scan.
+ */
+typedef void (^YKFQRCodeResponseBlock)(NSString* _Nullable, NSError* _Nullable);
+
+@protocol YKFQRReaderSessionProtocol<NSObject>
+
+/*!
+ @method scanQrCodeWithPresenter:completion:
+ 
+ @param viewController
+    The view controller which will be used to present modally the QR Code scanning UI.
+ 
+ @param completion
+    The completion block which returns the scanned QR Code value or an error in case of failure.
+ 
+ @abstract
+    Async request for scanning a QR Code. After caling this method YubiKit will prompt the user to scan
+    the QR Code using a built-in UI presented modally on top of the specified presenter. YubiKit allows to
+    customize the default messages shown in the library UI. This can be done by setting localized values
+    in YubiKitExternalLocalization.
+ 
+ NOTE:
+    Before using this method make sure the project is properly configured to allow the usage of camera. YubiKit provides
+    the ability to check if the device supports QR Code scanning by using the shared instance deviceCapabilities
+    from YubiKitManager.
+ */
+- (void)scanQrCodeWithPresenter:(nonnull UIViewController*)viewController completion:(nonnull YKFQRCodeResponseBlock)completion;
+
+/*!
+ @method scanQrCodeWithPresenter:completion:
+ 
+ @abstract
+    Dismisses the presented QR Code scanning UI, which was presented using scanQrCodeWithPresenter:completion:
+ 
+ NOTE:
+    YubiKit will take care of dismissing the UI in case the user pressed on Dismiss button without scanning a QR Code or
+    after the scanner detected a QR Code or failed for any reason to do so. This method should be used when the UI needs
+    to reset for any reason without user interaction.
+ */
+- (void)dismissQRCodeScanViewController;
+
+@end
+
+@interface YKFQRReaderSession : NSObject<YKFQRReaderSessionProtocol>
+@end
diff --git a/YubiKit/YubiKit/Sessions/QRReaderSession/YKFQRReaderSession.m b/YubiKit/YubiKit/Sessions/QRReaderSession/YKFQRReaderSession.m
new file mode 100755
index 000000000..3b9c6cded
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/QRReaderSession/YKFQRReaderSession.m
@@ -0,0 +1,69 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "YKFQRReaderSession.h"
+#import "YKFQRCodeScanViewController.h"
+#import "YubiKitDeviceCapabilities.h"
+#import "YKFAssert.h"
+
+@interface YKFQRReaderSession()<YKFQRCodeScanViewControllerDelegate>
+
+@property (nonatomic) YKFQRCodeScanViewController *scanViewController;
+@property (nonatomic, copy) YKFQRCodeResponseBlock qrCodeScanResponseBlock;
+
+@end
+
+@implementation YKFQRReaderSession
+
+#pragma mark - YubiKitManagerProtocol
+
+- (void)scanQrCodeWithPresenter:(UIViewController *)viewController completion:(YKFQRCodeResponseBlock)completion {
+    YKFAssertReturn(YubiKitDeviceCapabilities.supportsQRCodeScanning, @"Device does not support QR code scanning.");
+    
+    self.qrCodeScanResponseBlock = completion;
+    self.scanViewController = [[YKFQRCodeScanViewController alloc] init];
+    self.scanViewController.delegate = self;
+    
+    [viewController presentViewController:self.scanViewController animated:YES completion:nil];
+}
+
+- (void)dismissQRCodeScanViewController {
+    [self.scanViewController dismissViewControllerAnimated:YES completion:nil];
+    self.scanViewController = nil;
+}
+
+#pragma mark - YKFQRCodeScanViewControllerDelegate
+
+- (void)qrCodeScanViewController:(YKFQRCodeScanViewController *)viewController didFailWithError:(NSError *)error {
+    [self dismissQRCodeScanViewController];
+    if (self.qrCodeScanResponseBlock) {
+        self.qrCodeScanResponseBlock(nil, error);
+        self.qrCodeScanResponseBlock = nil;
+    }
+}
+
+- (void)qrCodeScanViewController:(YKFQRCodeScanViewController *)viewController didScanPayload:(NSString *)payload {
+    [self dismissQRCodeScanViewController];
+    if (self.qrCodeScanResponseBlock) {
+        self.qrCodeScanResponseBlock(payload, nil);
+        self.qrCodeScanResponseBlock = nil;
+    }
+}
+
+- (void)qrCodeScanViewControllerDidCancel:(YKFQRCodeScanViewController *)viewController {
+    [self dismissQRCodeScanViewController];
+    self.qrCodeScanResponseBlock = nil;
+}
+
+@end
diff --git a/YubiKit/YubiKit/Sessions/Shared/APDU/FIDO2/YKFFIDO2ClientPinAPDU.h b/YubiKit/YubiKit/Sessions/Shared/APDU/FIDO2/YKFFIDO2ClientPinAPDU.h
new file mode 100755
index 000000000..5ffe34980
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/APDU/FIDO2/YKFFIDO2ClientPinAPDU.h
@@ -0,0 +1,29 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+#import "YKFFIDO2CommandAPDU.h"
+
+@class YKFKeyFIDO2ClientPinRequest;
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface YKFFIDO2ClientPinAPDU: YKFFIDO2CommandAPDU
+
+- (nullable instancetype)initWithRequest:(YKFKeyFIDO2ClientPinRequest *)request NS_DESIGNATED_INITIALIZER;
+- (instancetype)init NS_UNAVAILABLE;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/YubiKit/YubiKit/Sessions/Shared/APDU/FIDO2/YKFFIDO2ClientPinAPDU.m b/YubiKit/YubiKit/Sessions/Shared/APDU/FIDO2/YKFFIDO2ClientPinAPDU.m
new file mode 100755
index 000000000..d18f1040f
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/APDU/FIDO2/YKFFIDO2ClientPinAPDU.m
@@ -0,0 +1,66 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "YKFFIDO2ClientPinAPDU.h"
+#import "YKFKeyFIDO2ClientPinRequest.h"
+#import "YKFCBOREncoder.h"
+#import "YKFCBORType.h"
+#import "YKFAssert.h"
+
+typedef NS_ENUM(NSUInteger, YKFFIDO2ClientPinAPDUKey) {
+    YKFFIDO2ClientPinAPDUKeyPinProtocol     = 0x01,
+    YKFFIDO2ClientPinAPDUKeySubCommand      = 0x02,
+    YKFFIDO2ClientPinAPDUKeyKeyAgreement    = 0x03,
+    YKFFIDO2ClientPinAPDUKeyPinAuth         = 0x04,
+    YKFFIDO2ClientPinAPDUKeyPinEnc          = 0x05,
+    YKFFIDO2ClientPinAPDUKeyPinHashEnc      = 0x06
+};
+
+@implementation YKFFIDO2ClientPinAPDU
+
+- (instancetype)initWithRequest:(YKFKeyFIDO2ClientPinRequest *)request {
+    YKFAssertAbortInit(request);
+    YKFAssertAbortInit(request.subCommand >= 0x01 && request.subCommand <= 0x05)
+    
+    if (request.subCommand == YKFKeyFIDO2ClientPinRequestSubCommandGetKeyAgreement) {
+        YKFAssertAbortInit(request.keyAgreement);
+    } else if (request.subCommand == YKFKeyFIDO2ClientPinRequestSubCommandGetPINToken) {        
+        YKFAssertAbortInit(request.pinHashEnc);
+    }
+    
+    NSMutableDictionary *requestDictionary = [[NSMutableDictionary alloc] init];
+    
+    requestDictionary[YKFCBORInteger(YKFFIDO2ClientPinAPDUKeyPinProtocol)] = YKFCBORInteger(request.pinProtocol);
+    requestDictionary[YKFCBORInteger(YKFFIDO2ClientPinAPDUKeySubCommand)] = YKFCBORInteger(request.subCommand);
+    
+    if (request.keyAgreement) {
+        requestDictionary[YKFCBORInteger(YKFFIDO2ClientPinAPDUKeyKeyAgreement)] = request.keyAgreement;
+    }
+    if (request.pinAuth) {
+        requestDictionary[YKFCBORInteger(YKFFIDO2ClientPinAPDUKeyPinAuth)] = YKFCBORByteString(request.pinAuth);
+    }
+    if (request.pinEnc) {
+        requestDictionary[YKFCBORInteger(YKFFIDO2ClientPinAPDUKeyPinEnc)] = YKFCBORByteString(request.pinEnc);
+    }
+    if (request.pinHashEnc) {
+        requestDictionary[YKFCBORInteger(YKFFIDO2ClientPinAPDUKeyPinHashEnc)] = YKFCBORByteString(request.pinHashEnc);
+    }
+    
+    NSData *cborData = [YKFCBOREncoder encodeMap:YKFCBORMap(requestDictionary)];
+    YKFAssertAbortInit(cborData);
+    
+    return [super initWithCommand:YKFFIDO2CommandClientPIN data:cborData];
+}
+
+@end
diff --git a/YubiKit/YubiKit/Sessions/Shared/APDU/FIDO2/YKFFIDO2CommandAPDU.h b/YubiKit/YubiKit/Sessions/Shared/APDU/FIDO2/YKFFIDO2CommandAPDU.h
new file mode 100755
index 000000000..29ce6f261
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/APDU/FIDO2/YKFFIDO2CommandAPDU.h
@@ -0,0 +1,38 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+#import "YKFAPDU.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+typedef NS_ENUM(NSUInteger, YKFFIDO2Command) {
+    YKFFIDO2CommandMakeCredential      = 0x01,
+    YKFFIDO2CommandGetAssertion        = 0x02,
+    YKFFIDO2CommandCancel              = 0x03,
+    YKFFIDO2CommandGetInfo             = 0x04,
+    YKFFIDO2CommandClientPIN           = 0x06,
+    YKFFIDO2CommandReset               = 0x07,
+    YKFFIDO2CommandGetNextAssertion    = 0x08,
+    YKFFIDO2CommandVendorFirst         = 0x40,
+    YKFFIDO2CommandVendorLast          = 0xBF
+};
+
+@interface YKFFIDO2CommandAPDU: YKFAPDU
+
+- (instancetype)initWithCommand:(YKFFIDO2Command)command data:(nullable NSData *)data;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/YubiKit/YubiKit/Sessions/Shared/APDU/FIDO2/YKFFIDO2CommandAPDU.m b/YubiKit/YubiKit/Sessions/Shared/APDU/FIDO2/YKFFIDO2CommandAPDU.m
new file mode 100755
index 000000000..127d41a45
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/APDU/FIDO2/YKFFIDO2CommandAPDU.m
@@ -0,0 +1,40 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "YKFFIDO2CommandAPDU.h"
+#import "YKFAPDUCommandInstruction.h"
+#import "YKFNSMutableDataAdditions.h"
+#import "YKFAssert.h"
+
+@implementation YKFFIDO2CommandAPDU
+
+- (instancetype)initWithCommand:(YKFFIDO2Command)command data:(NSData *)data {
+    BOOL isFido2Command = command >= 0x01 && command <= 0x08 && command != 0x05;
+    BOOL isVendorCommand = command >= 0x40 && command <= 0xBF;
+    YKFAssertAbortInit(isFido2Command || isVendorCommand);
+    
+    NSMutableData *commandData = [[NSMutableData alloc] initWithCapacity:data.length + 1];
+    [commandData ykf_appendByte:command];
+    
+    if (data.length) {
+        [commandData appendData:data];
+        return [super initWithCla:0x80 ins:YKFAPDUCommandInstructionFIDO2Msg
+                               p1:0x80 p2:0x00 data:commandData type:YKFAPDUTypeExtended];
+    }
+    
+    return [super initWithCla:0x80 ins:YKFAPDUCommandInstructionFIDO2Msg
+                           p1:0x80 p2:0x00 data:commandData type:YKFAPDUTypeShort];
+}
+
+@end
diff --git a/YubiKit/YubiKit/Sessions/Shared/APDU/FIDO2/YKFFIDO2GetAssertionAPDU.h b/YubiKit/YubiKit/Sessions/Shared/APDU/FIDO2/YKFFIDO2GetAssertionAPDU.h
new file mode 100755
index 000000000..560d65c03
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/APDU/FIDO2/YKFFIDO2GetAssertionAPDU.h
@@ -0,0 +1,29 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+#import "YKFFIDO2CommandAPDU.h"
+
+@class YKFKeyFIDO2GetAssertionRequest;
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface YKFFIDO2GetAssertionAPDU: YKFFIDO2CommandAPDU
+
+- (nullable instancetype)initWithRequest:(YKFKeyFIDO2GetAssertionRequest *)request NS_DESIGNATED_INITIALIZER;
+- (instancetype)init NS_UNAVAILABLE;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/YubiKit/YubiKit/Sessions/Shared/APDU/FIDO2/YKFFIDO2GetAssertionAPDU.m b/YubiKit/YubiKit/Sessions/Shared/APDU/FIDO2/YKFFIDO2GetAssertionAPDU.m
new file mode 100755
index 000000000..6530e2210
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/APDU/FIDO2/YKFFIDO2GetAssertionAPDU.m
@@ -0,0 +1,87 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "YKFFIDO2GetAssertionAPDU.h"
+#import "YKFCBORType.h"
+#import "YKFCBOREncoder.h"
+#import "YKFAssert.h"
+
+#import "YKFKeyFIDO2GetAssertionRequest.h"
+#import "YKFKeyFIDO2GetAssertionRequest+Private.h"
+
+#import "YKFFIDO2Type.h"
+#import "YKFFIDO2Type+Private.h"
+
+typedef NS_ENUM(NSUInteger, YKFFIDO2GetAssertionAPDUKey) {
+    YKFFIDO2GetAssertionAPDUKeyRp               = 0x01,
+    YKFFIDO2GetAssertionAPDUKeyClientDataHash   = 0x02,
+    YKFFIDO2GetAssertionAPDUKeyAllowList        = 0x03,
+    YKFFIDO2GetAssertionAPDUKeyExtensions       = 0x04,
+    YKFFIDO2GetAssertionAPDUKeyOptions          = 0x05,
+    YKFFIDO2GetAssertionAPDUKeyPinAuth          = 0x06,
+    YKFFIDO2GetAssertionAPDUKeyPinProtocol      = 0x07
+};
+
+@implementation YKFFIDO2GetAssertionAPDU
+
+- (instancetype)initWithRequest:(YKFKeyFIDO2GetAssertionRequest *)request {
+    YKFAssertAbortInit(request);
+    YKFAssertAbortInit(request.rpId);
+    YKFAssertAbortInit(request.clientDataHash);
+    
+    NSMutableDictionary *requestDictionary = [[NSMutableDictionary alloc] init];
+    
+    // RP
+    requestDictionary[YKFCBORInteger(YKFFIDO2GetAssertionAPDUKeyRp)] = YKFCBORTextString(request.rpId);
+    
+    // Client Data Hash
+    requestDictionary[YKFCBORInteger(YKFFIDO2GetAssertionAPDUKeyClientDataHash)] = YKFCBORByteString(request.clientDataHash);
+    
+    // Allow List
+    if (request.allowList) {
+        NSMutableArray *allowList = [[NSMutableArray alloc] initWithCapacity:request.allowList.count];
+        for (YKFFIDO2PublicKeyCredentialDescriptor *credentialDescriptor in request.allowList) {
+            [allowList addObject:[credentialDescriptor cborTypeObject]];
+        }
+        requestDictionary[YKFCBORInteger(YKFFIDO2GetAssertionAPDUKeyAllowList)] = YKFCBORArray(allowList);
+    }
+    
+    // Options
+    if (request.options) {
+        NSMutableDictionary *options = [[NSMutableDictionary alloc] initWithCapacity:request.options.count];
+        NSArray *optionsKeys = request.options.allKeys;
+        for (NSString *optionKey in optionsKeys) {
+            NSNumber *value = request.options[optionKey];
+            options[YKFCBORTextString(optionKey)] = YKFCBORBool(value.boolValue);
+        }
+        requestDictionary[YKFCBORInteger(YKFFIDO2GetAssertionAPDUKeyOptions)] = YKFCBORMap(options);
+    }
+
+    // Pin Auth
+    if (request.pinAuth) {
+        requestDictionary[YKFCBORInteger(YKFFIDO2GetAssertionAPDUKeyPinAuth)] = YKFCBORByteString(request.pinAuth);
+    }
+
+    // Pin Protocol
+    if (request.pinProtocol) {
+        requestDictionary[YKFCBORInteger(YKFFIDO2GetAssertionAPDUKeyPinProtocol)] = YKFCBORInteger(request.pinProtocol);
+    }
+    
+    NSData *cborData = [YKFCBOREncoder encodeMap:YKFCBORMap(requestDictionary)];
+    YKFAssertAbortInit(cborData);
+    
+    return [super initWithCommand:YKFFIDO2CommandGetAssertion data:cborData];
+}
+
+@end
diff --git a/YubiKit/YubiKit/Sessions/Shared/APDU/FIDO2/YKFFIDO2GetInfoAPDU.h b/YubiKit/YubiKit/Sessions/Shared/APDU/FIDO2/YKFFIDO2GetInfoAPDU.h
new file mode 100755
index 000000000..b1c9c241c
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/APDU/FIDO2/YKFFIDO2GetInfoAPDU.h
@@ -0,0 +1,23 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+#import "YKFFIDO2CommandAPDU.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface YKFFIDO2GetInfoAPDU: YKFFIDO2CommandAPDU
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/YubiKit/YubiKit/Sessions/Shared/APDU/FIDO2/YKFFIDO2GetInfoAPDU.m b/YubiKit/YubiKit/Sessions/Shared/APDU/FIDO2/YKFFIDO2GetInfoAPDU.m
new file mode 100755
index 000000000..157d533f3
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/APDU/FIDO2/YKFFIDO2GetInfoAPDU.m
@@ -0,0 +1,23 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "YKFFIDO2GetInfoAPDU.h"
+
+@implementation YKFFIDO2GetInfoAPDU
+
+- (instancetype)init {
+    return [super initWithCommand:YKFFIDO2CommandGetInfo data:nil];
+}
+
+@end
diff --git a/YubiKit/YubiKit/Sessions/Shared/APDU/FIDO2/YKFFIDO2GetNextAssertionAPDU.h b/YubiKit/YubiKit/Sessions/Shared/APDU/FIDO2/YKFFIDO2GetNextAssertionAPDU.h
new file mode 100755
index 000000000..d513ba465
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/APDU/FIDO2/YKFFIDO2GetNextAssertionAPDU.h
@@ -0,0 +1,23 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+#import "YKFFIDO2CommandAPDU.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface YKFFIDO2GetNextAssertionAPDU: YKFFIDO2CommandAPDU
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/YubiKit/YubiKit/Sessions/Shared/APDU/FIDO2/YKFFIDO2GetNextAssertionAPDU.m b/YubiKit/YubiKit/Sessions/Shared/APDU/FIDO2/YKFFIDO2GetNextAssertionAPDU.m
new file mode 100755
index 000000000..686206d9f
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/APDU/FIDO2/YKFFIDO2GetNextAssertionAPDU.m
@@ -0,0 +1,24 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "YKFFIDO2GetNextAssertionAPDU.h"
+
+@implementation YKFFIDO2GetNextAssertionAPDU
+
+- (instancetype)init {
+    return [super initWithCommand:YKFFIDO2CommandGetNextAssertion data:nil];
+}
+
+@end
+
diff --git a/YubiKit/YubiKit/Sessions/Shared/APDU/FIDO2/YKFFIDO2MakeCredentialAPDU.h b/YubiKit/YubiKit/Sessions/Shared/APDU/FIDO2/YKFFIDO2MakeCredentialAPDU.h
new file mode 100755
index 000000000..3c6618501
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/APDU/FIDO2/YKFFIDO2MakeCredentialAPDU.h
@@ -0,0 +1,29 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+#import "YKFFIDO2CommandAPDU.h"
+
+@class YKFKeyFIDO2MakeCredentialRequest;
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface YKFFIDO2MakeCredentialAPDU: YKFFIDO2CommandAPDU
+
+- (nullable instancetype)initWithRequest:(YKFKeyFIDO2MakeCredentialRequest *)request NS_DESIGNATED_INITIALIZER;
+- (instancetype)init NS_UNAVAILABLE;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/YubiKit/YubiKit/Sessions/Shared/APDU/FIDO2/YKFFIDO2MakeCredentialAPDU.m b/YubiKit/YubiKit/Sessions/Shared/APDU/FIDO2/YKFFIDO2MakeCredentialAPDU.m
new file mode 100755
index 000000000..478859d85
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/APDU/FIDO2/YKFFIDO2MakeCredentialAPDU.m
@@ -0,0 +1,100 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "YKFFIDO2MakeCredentialAPDU.h"
+#import "YKFCBOREncoder.h"
+#import "YKFAssert.h"
+
+#import "YKFKeyFIDO2MakeCredentialRequest.h"
+#import "YKFKeyFIDO2MakeCredentialRequest+Private.h"
+
+#import "YKFFIDO2Type.h"
+#import "YKFFIDO2Type+Private.h"
+
+typedef NS_ENUM(NSUInteger, YKFFIDO2MakeCredentialAPDUKey) {
+    YKFFIDO2MakeCredentialAPDUKeyClientDataHash     = 0x01,
+    YKFFIDO2MakeCredentialAPDUKeyRp                 = 0x02,
+    YKFFIDO2MakeCredentialAPDUKeyUser               = 0x03,
+    YKFFIDO2MakeCredentialAPDUKeyPubKeyCredParams   = 0x04,
+    YKFFIDO2MakeCredentialAPDUKeyExcludeList        = 0x05,
+    YKFFIDO2MakeCredentialAPDUKeyExtensions         = 0x06,
+    YKFFIDO2MakeCredentialAPDUKeyOptions            = 0x07,
+    YKFFIDO2MakeCredentialAPDUKeyPinAuth            = 0x08,
+    YKFFIDO2MakeCredentialAPDUKeyPinProtocol        = 0x09,
+};
+
+@implementation YKFFIDO2MakeCredentialAPDU
+
+- (instancetype)initWithRequest:(YKFKeyFIDO2MakeCredentialRequest *)request {
+    YKFAssertAbortInit(request)
+    YKFAssertAbortInit(request.clientDataHash)
+    YKFAssertAbortInit(request.rp)
+    YKFAssertAbortInit(request.user)
+    YKFAssertAbortInit(request.pubKeyCredParams)
+    
+    NSMutableDictionary *requestDictionary = [[NSMutableDictionary alloc] init];
+    
+    // Client Data Hash
+    requestDictionary[YKFCBORInteger(YKFFIDO2MakeCredentialAPDUKeyClientDataHash)] = YKFCBORByteString(request.clientDataHash);
+    
+    // RP
+    requestDictionary[YKFCBORInteger(YKFFIDO2MakeCredentialAPDUKeyRp)] = [request.rp cborTypeObject];
+    
+    // User
+    requestDictionary[YKFCBORInteger(YKFFIDO2MakeCredentialAPDUKeyUser)] = [request.user cborTypeObject];
+    
+    // PubKeyCredParams
+    NSMutableArray *pubKeyCredParams = [[NSMutableArray alloc] initWithCapacity:request.pubKeyCredParams.count];
+    for (YKFFIDO2PublicKeyCredentialType *credentialType in request.pubKeyCredParams) {
+        [pubKeyCredParams addObject:[credentialType cborTypeObject]];
+    }
+    requestDictionary[YKFCBORInteger(YKFFIDO2MakeCredentialAPDUKeyPubKeyCredParams)] = YKFCBORArray(pubKeyCredParams);
+    
+    // ExcludeList
+    if (request.excludeList) {
+        NSMutableArray *excludeList = [[NSMutableArray alloc] initWithCapacity:request.excludeList.count];
+        for (YKFFIDO2PublicKeyCredentialDescriptor *descriptor in request.excludeList) {
+            [excludeList addObject:[descriptor cborTypeObject]];
+        }
+        requestDictionary[YKFCBORInteger(YKFFIDO2MakeCredentialAPDUKeyExcludeList)] = YKFCBORArray(excludeList);
+    }
+    
+    // Options
+    if (request.options) {
+        NSMutableDictionary *options = [[NSMutableDictionary alloc] initWithCapacity:request.options.count];
+        NSArray *optionsKeys = request.options.allKeys;
+        for (NSString *optionKey in optionsKeys) {
+            NSNumber *value = request.options[optionKey];
+            options[YKFCBORTextString(optionKey)] = YKFCBORBool(value.boolValue);
+        }
+        requestDictionary[YKFCBORInteger(YKFFIDO2MakeCredentialAPDUKeyOptions)] = YKFCBORMap(options);
+    }
+    
+    // Pin Auth
+    if (request.pinAuth) {
+        requestDictionary[YKFCBORInteger(YKFFIDO2MakeCredentialAPDUKeyPinAuth)] = YKFCBORByteString(request.pinAuth);
+    }
+    
+    // Pin Protocol
+    if (request.pinProtocol) {
+        requestDictionary[YKFCBORInteger(YKFFIDO2MakeCredentialAPDUKeyPinProtocol)] = YKFCBORInteger(request.pinProtocol);
+    }
+
+    NSData *cborData = [YKFCBOREncoder encodeMap:YKFCBORMap(requestDictionary)];
+    YKFAssertAbortInit(cborData);
+    
+    return [super initWithCommand:YKFFIDO2CommandMakeCredential data:cborData];
+}
+
+@end
diff --git a/YubiKit/YubiKit/Sessions/Shared/APDU/FIDO2/YKFFIDO2ResetAPDU.h b/YubiKit/YubiKit/Sessions/Shared/APDU/FIDO2/YKFFIDO2ResetAPDU.h
new file mode 100755
index 000000000..c9aa3e317
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/APDU/FIDO2/YKFFIDO2ResetAPDU.h
@@ -0,0 +1,23 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+#import "YKFFIDO2CommandAPDU.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface YKFFIDO2ResetAPDU: YKFFIDO2CommandAPDU
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/YubiKit/YubiKit/Sessions/Shared/APDU/FIDO2/YKFFIDO2ResetAPDU.m b/YubiKit/YubiKit/Sessions/Shared/APDU/FIDO2/YKFFIDO2ResetAPDU.m
new file mode 100755
index 000000000..a3a513ca4
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/APDU/FIDO2/YKFFIDO2ResetAPDU.m
@@ -0,0 +1,23 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "YKFFIDO2ResetAPDU.h"
+
+@implementation YKFFIDO2ResetAPDU
+
+- (instancetype)init {
+    return [super initWithCommand:YKFFIDO2CommandReset data:nil];
+}
+
+@end
diff --git a/YubiKit/YubiKit/Sessions/Shared/APDU/FIDO2/YKFFIDO2TouchPoolingAPDU.h b/YubiKit/YubiKit/Sessions/Shared/APDU/FIDO2/YKFFIDO2TouchPoolingAPDU.h
new file mode 100755
index 000000000..28a77b837
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/APDU/FIDO2/YKFFIDO2TouchPoolingAPDU.h
@@ -0,0 +1,23 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+#import "YKFAPDU.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface YKFFIDO2TouchPoolingAPDU: YKFAPDU
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/YubiKit/YubiKit/Sessions/Shared/APDU/FIDO2/YKFFIDO2TouchPoolingAPDU.m b/YubiKit/YubiKit/Sessions/Shared/APDU/FIDO2/YKFFIDO2TouchPoolingAPDU.m
new file mode 100755
index 000000000..b40d689f4
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/APDU/FIDO2/YKFFIDO2TouchPoolingAPDU.m
@@ -0,0 +1,25 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "YKFFIDO2TouchPoolingAPDU.h"
+#import "YKFAPDUCommandInstruction.h"
+
+@implementation YKFFIDO2TouchPoolingAPDU
+
+- (instancetype)init {
+    return [super initWithCla:0x80 ins:YKFAPDUCommandInstructionFIDO2GetResponse
+                           p1:0x00 p2:0x00 data:[NSData data] type:YKFAPDUTypeExtended];
+}
+
+@end
diff --git a/YubiKit/YubiKit/Sessions/Shared/APDU/FIDO2/YKFSelectFIDO2ApplicationAPDU.h b/YubiKit/YubiKit/Sessions/Shared/APDU/FIDO2/YKFSelectFIDO2ApplicationAPDU.h
new file mode 100755
index 000000000..fc6117779
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/APDU/FIDO2/YKFSelectFIDO2ApplicationAPDU.h
@@ -0,0 +1,23 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+#import "YKFAPDU.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface YKFSelectFIDO2ApplicationAPDU: YKFAPDU
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/YubiKit/YubiKit/Sessions/Shared/APDU/FIDO2/YKFSelectFIDO2ApplicationAPDU.m b/YubiKit/YubiKit/Sessions/Shared/APDU/FIDO2/YKFSelectFIDO2ApplicationAPDU.m
new file mode 100755
index 000000000..914fa2227
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/APDU/FIDO2/YKFSelectFIDO2ApplicationAPDU.m
@@ -0,0 +1,28 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "YKFSelectFIDO2ApplicationAPDU.h"
+#import "YKFAPDUCommandInstruction.h"
+
+static const NSUInteger YKFFido2AIDSize = 8;
+static const UInt8 YKFFido2AID[YKFFido2AIDSize] = {0xA0, 0x00, 0x00, 0x06, 0x47, 0x2F, 0x00, 0x01};
+
+@implementation YKFSelectFIDO2ApplicationAPDU
+
+- (instancetype)init {
+    NSData *data = [NSData dataWithBytes:YKFFido2AID length:YKFFido2AIDSize];
+    return [super initWithCla:0x00 ins:YKFAPDUCommandInstructionSelectApplication p1:0x04 p2:0x00 data:data type:YKFAPDUTypeShort];
+}
+
+@end
diff --git a/YubiKit/YubiKit/Sessions/Shared/APDU/OATH/YKFOATHCalculateAPDU.h b/YubiKit/YubiKit/Sessions/Shared/APDU/OATH/YKFOATHCalculateAPDU.h
new file mode 100755
index 000000000..2b579afbf
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/APDU/OATH/YKFOATHCalculateAPDU.h
@@ -0,0 +1,32 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+#import "YKFAPDU.h"
+
+@class YKFKeyOATHCalculateRequest;
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface YKFOATHCalculateAPDU: YKFAPDU
+
+/*
+ Note: Timestamp is passed to make sure the same exact timestamp is shared between the request and the response.
+ */
+- (nullable instancetype)initWithRequest:(YKFKeyOATHCalculateRequest *)request timestamp:(NSDate *)timestamp NS_DESIGNATED_INITIALIZER;
+- (instancetype)init NS_UNAVAILABLE;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/YubiKit/YubiKit/Sessions/Shared/APDU/OATH/YKFOATHCalculateAPDU.m b/YubiKit/YubiKit/Sessions/Shared/APDU/OATH/YKFOATHCalculateAPDU.m
new file mode 100755
index 000000000..eca4d7873
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/APDU/OATH/YKFOATHCalculateAPDU.m
@@ -0,0 +1,56 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "YKFOATHCalculateAPDU.h"
+#import "YKFKeyOATHCalculateRequest.h"
+#import "YKFAPDUCommandInstruction.h"
+#import "YKFAssert.h"
+#import "YKFNSMutableDataAdditions.h"
+#import "YKFOATHCredential+Private.h"
+
+static const UInt8 YKFOATHCalculateAPDUNameTag = 0x71;
+static const UInt8 YKFOATHCalculateAPDUChallengeTag = 0x74;
+
+@implementation YKFOATHCalculateAPDU
+
+- (nullable instancetype)initWithRequest:(nonnull YKFKeyOATHCalculateRequest *)request timestamp:(NSDate *)timestamp {
+    YKFAssertAbortInit(request);
+    YKFAssertAbortInit(timestamp);
+    
+    NSMutableData *rawRequest = [[NSMutableData alloc] init];
+    
+    // Name
+    NSString *name = request.credential.key;
+    NSData *nameData = [name dataUsingEncoding:NSUTF8StringEncoding];
+    
+    [rawRequest ykf_appendEntryWithTag:YKFOATHCalculateAPDUNameTag data:nameData];
+    
+    // Challenge
+    
+    if (request.credential.type == YKFOATHCredentialTypeTOTP) {
+        time_t time = (time_t)[timestamp timeIntervalSince1970];
+        time_t challengeTime = time / request.credential.period;
+        
+        [rawRequest ykf_appendUInt64EntryWithTag:YKFOATHCalculateAPDUChallengeTag value:challengeTime];
+    } else {
+        // For HOTP the challenge is 0
+        [rawRequest ykf_appendByte:YKFOATHCalculateAPDUChallengeTag];
+        [rawRequest ykf_appendByte:0];
+    }
+    
+    // P2 is 0x01 for truncated response only
+    return [super initWithCla:0 ins:YKFAPDUCommandInstructionOATHCalculate p1:0 p2:0x01 data:rawRequest type:YKFAPDUTypeShort];
+}
+
+@end
diff --git a/YubiKit/YubiKit/Sessions/Shared/APDU/OATH/YKFOATHCalculateAllAPDU.h b/YubiKit/YubiKit/Sessions/Shared/APDU/OATH/YKFOATHCalculateAllAPDU.h
new file mode 100755
index 000000000..46462da23
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/APDU/OATH/YKFOATHCalculateAllAPDU.h
@@ -0,0 +1,30 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+#import "YKFAPDU.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface YKFOATHCalculateAllAPDU: YKFAPDU
+
+/*
+ Note: Timestamp is passed to make sure the same exact timestamp is shared between the request and the response.
+ */
+- (nullable instancetype)initWithTimestamp:(nonnull NSDate *)timestamp NS_DESIGNATED_INITIALIZER;
+- (instancetype)init NS_UNAVAILABLE;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/YubiKit/YubiKit/Sessions/Shared/APDU/OATH/YKFOATHCalculateAllAPDU.m b/YubiKit/YubiKit/Sessions/Shared/APDU/OATH/YKFOATHCalculateAllAPDU.m
new file mode 100755
index 000000000..f753aefa6
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/APDU/OATH/YKFOATHCalculateAllAPDU.m
@@ -0,0 +1,40 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "YKFOATHCalculateAllAPDU.h"
+#import "YKFAPDUCommandInstruction.h"
+#import "YKFNSMutableDataAdditions.h"
+#import "YKFAssert.h"
+
+static const UInt8 YKFOATHCalculateAllAPDUChallengeTag = 0x74;
+
+@implementation YKFOATHCalculateAllAPDU
+
+- (instancetype)initWithTimestamp:(NSDate *)timestamp {
+    YKFAssertAbortInit(timestamp)
+    
+    NSMutableData *rawRequest = [[NSMutableData alloc] init];
+    
+    // Challenge
+    
+    time_t time = (time_t)[timestamp timeIntervalSince1970];
+    time_t challengeTime = time / 30; // Calculate all assumes only 30s TOTPs
+    
+    [rawRequest ykf_appendUInt64EntryWithTag:YKFOATHCalculateAllAPDUChallengeTag value:challengeTime];
+    
+    // P2 is 0x01 for truncated response only
+    return [super initWithCla:0x00 ins:YKFAPDUCommandInstructionOATHCalculateAll p1:0x00 p2:0x01 data:rawRequest type:YKFAPDUTypeShort];
+}
+
+@end
diff --git a/YubiKit/YubiKit/Sessions/Shared/APDU/OATH/YKFOATHDeleteAPDU.h b/YubiKit/YubiKit/Sessions/Shared/APDU/OATH/YKFOATHDeleteAPDU.h
new file mode 100755
index 000000000..634258924
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/APDU/OATH/YKFOATHDeleteAPDU.h
@@ -0,0 +1,29 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+#import "YKFAPDU.h"
+
+@class YKFKeyOATHDeleteRequest;
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface YKFOATHDeleteAPDU: YKFAPDU
+
+- (nullable instancetype)initWithRequest:(YKFKeyOATHDeleteRequest *)request NS_DESIGNATED_INITIALIZER;
+- (instancetype)init NS_UNAVAILABLE;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/YubiKit/YubiKit/Sessions/Shared/APDU/OATH/YKFOATHDeleteAPDU.m b/YubiKit/YubiKit/Sessions/Shared/APDU/OATH/YKFOATHDeleteAPDU.m
new file mode 100755
index 000000000..60d96af46
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/APDU/OATH/YKFOATHDeleteAPDU.m
@@ -0,0 +1,41 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "YKFOATHDeleteAPDU.h"
+#import "YKFKeyOATHDeleteRequest.h"
+#import "YKFAPDUCommandInstruction.h"
+#import "YKFAssert.h"
+#import "YKFNSMutableDataAdditions.h"
+#import "YKFOATHCredential+Private.h"
+
+static const UInt8 YKFOATHDeleteAPDUNameTag = 0x71;
+
+@implementation YKFOATHDeleteAPDU
+
+- (instancetype)initWithRequest:(nonnull YKFKeyOATHDeleteRequest *)request {
+    YKFAssertAbortInit(request);
+    
+    NSMutableData *rawRequest = [[NSMutableData alloc] init];
+    
+    // Name
+    
+    NSString *name = request.credential.key;
+    NSData *nameData = [name dataUsingEncoding:NSUTF8StringEncoding];
+    
+    [rawRequest ykf_appendEntryWithTag:YKFOATHDeleteAPDUNameTag data:nameData];
+    
+    return [super initWithCla:0 ins:YKFAPDUCommandInstructionOATHDelete p1:0 p2:0 data:rawRequest type:YKFAPDUTypeShort];
+}
+
+@end
diff --git a/YubiKit/YubiKit/Sessions/Shared/APDU/OATH/YKFOATHListAPDU.h b/YubiKit/YubiKit/Sessions/Shared/APDU/OATH/YKFOATHListAPDU.h
new file mode 100755
index 000000000..e4cc7b20b
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/APDU/OATH/YKFOATHListAPDU.h
@@ -0,0 +1,23 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+#import "YKFAPDU.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface YKFOATHListAPDU: YKFAPDU
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/YubiKit/YubiKit/Sessions/Shared/APDU/OATH/YKFOATHListAPDU.m b/YubiKit/YubiKit/Sessions/Shared/APDU/OATH/YKFOATHListAPDU.m
new file mode 100755
index 000000000..af478de38
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/APDU/OATH/YKFOATHListAPDU.m
@@ -0,0 +1,24 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "YKFOATHListAPDU.h"
+#import "YKFAPDUCommandInstruction.h"
+
+@implementation YKFOATHListAPDU
+
+- (instancetype)init {
+    return [super initWithCla:0x00 ins:YKFAPDUCommandInstructionOATHList p1:0x00 p2:0x00 data:[NSData data] type:YKFAPDUTypeShort];
+}
+
+@end
diff --git a/YubiKit/YubiKit/Sessions/Shared/APDU/OATH/YKFOATHPutAPDU.h b/YubiKit/YubiKit/Sessions/Shared/APDU/OATH/YKFOATHPutAPDU.h
new file mode 100755
index 000000000..1d7084799
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/APDU/OATH/YKFOATHPutAPDU.h
@@ -0,0 +1,29 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+#import "YKFAPDU.h"
+
+@class YKFKeyOATHPutRequest;
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface YKFOATHPutAPDU: YKFAPDU
+
+- (nullable instancetype)initWithRequest:(YKFKeyOATHPutRequest *)request NS_DESIGNATED_INITIALIZER;
+- (instancetype)init NS_UNAVAILABLE;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/YubiKit/YubiKit/Sessions/Shared/APDU/OATH/YKFOATHPutAPDU.m b/YubiKit/YubiKit/Sessions/Shared/APDU/OATH/YKFOATHPutAPDU.m
new file mode 100755
index 000000000..39496e0a7
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/APDU/OATH/YKFOATHPutAPDU.m
@@ -0,0 +1,68 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "YKFOATHPutAPDU.h"
+#import "YKFKeyOATHPutRequest.h"
+#import "YKFAPDUCommandInstruction.h"
+#import "YKFAssert.h"
+#import "YKFNSMutableDataAdditions.h"
+#import "YKFOATHCredential+Private.h"
+
+typedef NS_ENUM(NSUInteger, YKFOATHPutCredentialAPDUTag) {
+    YKFOATHPutCredentialAPDUTagName = 0x71,
+    YKFOATHPutCredentialAPDUTagKey = 0x73,
+    YKFOATHPutCredentialAPDUTagProperty = 0x78,
+    YKFOATHPutCredentialAPDUTagCounter = 0x7A // Only HOTP
+};
+
+typedef NS_ENUM(NSUInteger, YKFOATHPutCredentialAPDUProperty) {
+    YKFOATHPutCredentialAPDUPropertyTouch = 0x02
+};
+
+@implementation YKFOATHPutAPDU
+
+- (instancetype)initWithRequest:(YKFKeyOATHPutRequest *)request {
+    YKFAssertAbortInit(request);
+    
+    NSMutableData *rawRequest = [[NSMutableData alloc] init];
+    
+    // Name - max 64 bytes
+    
+    NSString *name = request.credential.key;
+    NSData *nameData = [name dataUsingEncoding:NSUTF8StringEncoding];
+    [rawRequest ykf_appendEntryWithTag:YKFOATHPutCredentialAPDUTagName data:nameData];
+    
+    // Key
+    
+    NSData *secret = request.credential.secret;
+    UInt8 keyAlgorithm = request.credential.algorithm | request.credential.type;
+    UInt8 keyDigits = request.credential.digits;
+    
+    [rawRequest ykf_appendEntryWithTag:YKFOATHPutCredentialAPDUTagKey headerBytes:@[@(keyAlgorithm), @(keyDigits)] data:secret];
+    
+    // Touch
+    if (request.credential.requiresTouch) {
+        [rawRequest ykf_appendByte:YKFOATHPutCredentialAPDUTagProperty];
+        [rawRequest ykf_appendByte:YKFOATHPutCredentialAPDUPropertyTouch];
+    }
+    
+    // Counter if HOTP
+    if (request.credential.type == YKFOATHCredentialTypeHOTP) {
+        [rawRequest ykf_appendUInt32EntryWithTag:YKFOATHPutCredentialAPDUTagCounter value:request.credential.counter];
+    }
+    
+    return [super initWithCla:0 ins:YKFAPDUCommandInstructionOATHPut p1:0 p2:0 data:rawRequest type:YKFAPDUTypeShort];
+}
+
+@end
diff --git a/YubiKit/YubiKit/Sessions/Shared/APDU/OATH/YKFOATHResetAPDU.h b/YubiKit/YubiKit/Sessions/Shared/APDU/OATH/YKFOATHResetAPDU.h
new file mode 100755
index 000000000..6c52feed4
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/APDU/OATH/YKFOATHResetAPDU.h
@@ -0,0 +1,23 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+#import "YKFAPDU.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface YKFOATHResetAPDU: YKFAPDU
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/YubiKit/YubiKit/Sessions/Shared/APDU/OATH/YKFOATHResetAPDU.m b/YubiKit/YubiKit/Sessions/Shared/APDU/OATH/YKFOATHResetAPDU.m
new file mode 100755
index 000000000..af13dc3b0
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/APDU/OATH/YKFOATHResetAPDU.m
@@ -0,0 +1,24 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "YKFOATHResetAPDU.h"
+#import "YKFAPDUCommandInstruction.h"
+
+@implementation YKFOATHResetAPDU
+
+- (instancetype)init {
+    return [super initWithCla:0x00 ins:YKFAPDUCommandInstructionOATHReset p1:0xDE p2:0xAD data:[NSData data] type:YKFAPDUTypeShort];
+}
+
+@end
diff --git a/YubiKit/YubiKit/Sessions/Shared/APDU/OATH/YKFOATHSendRemainingAPDU.h b/YubiKit/YubiKit/Sessions/Shared/APDU/OATH/YKFOATHSendRemainingAPDU.h
new file mode 100755
index 000000000..e1b9e145a
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/APDU/OATH/YKFOATHSendRemainingAPDU.h
@@ -0,0 +1,23 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+#import "YKFAPDU.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface YKFOATHSendRemainingAPDU: YKFAPDU
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/YubiKit/YubiKit/Sessions/Shared/APDU/OATH/YKFOATHSendRemainingAPDU.m b/YubiKit/YubiKit/Sessions/Shared/APDU/OATH/YKFOATHSendRemainingAPDU.m
new file mode 100755
index 000000000..657e7edfe
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/APDU/OATH/YKFOATHSendRemainingAPDU.m
@@ -0,0 +1,24 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "YKFOATHSendRemainingAPDU.h"
+#import "YKFAPDUCommandInstruction.h"
+
+@implementation YKFOATHSendRemainingAPDU
+
+- (instancetype)init {
+    return [super initWithCla:0x00 ins:YKFAPDUCommandInstructionOATHSendRemaining p1:0x04 p2:0x00 data:[NSData data] type:YKFAPDUTypeShort];
+}
+
+@end
diff --git a/YubiKit/YubiKit/Sessions/Shared/APDU/OATH/YKFOATHSetCodeAPDU.h b/YubiKit/YubiKit/Sessions/Shared/APDU/OATH/YKFOATHSetCodeAPDU.h
new file mode 100755
index 000000000..7ed7b2d4f
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/APDU/OATH/YKFOATHSetCodeAPDU.h
@@ -0,0 +1,29 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+#import "YKFAPDU.h"
+
+@class YKFKeyOATHSetCodeRequest;
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface YKFOATHSetCodeAPDU: YKFAPDU
+
+- (nullable instancetype)initWithRequest:(YKFKeyOATHSetCodeRequest *)request salt:(NSData *)salt NS_DESIGNATED_INITIALIZER;
+- (instancetype)init NS_UNAVAILABLE;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/YubiKit/YubiKit/Sessions/Shared/APDU/OATH/YKFOATHSetCodeAPDU.m b/YubiKit/YubiKit/Sessions/Shared/APDU/OATH/YKFOATHSetCodeAPDU.m
new file mode 100755
index 000000000..ad20d73c2
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/APDU/OATH/YKFOATHSetCodeAPDU.m
@@ -0,0 +1,62 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "YKFOATHSetCodeAPDU.h"
+#import "YKFKeyOATHSetCodeRequest.h"
+#import "YKFAPDUCommandInstruction.h"
+#import "YKFOATHCredential.h"
+#import "YKFAssert.h"
+#import "YKFNSMutableDataAdditions.h"
+#import "YKFNSDataAdditions+Private.h"
+
+static const UInt8 YKFKeyOATHSetCodeAPDUKeyTag = 0x73;
+static const UInt8 YKFKeyOATHSetCodeAPDUChallengeTag = 0x74;
+static const UInt8 YKFKeyOATHSetCodeAPDUResponseTag = 0x75;
+
+@implementation YKFOATHSetCodeAPDU
+
+- (instancetype)initWithRequest:(YKFKeyOATHSetCodeRequest *)request salt:(NSData *)salt {
+    YKFAssertAbortInit(request);
+    YKFAssertAbortInit(salt.length);
+    
+    NSMutableData *rawRequest = [[NSMutableData alloc] init];
+    
+    // Password available - set authentication
+    if (request.password.length) {
+        NSData *keyData = [[request.password dataUsingEncoding:NSUTF8StringEncoding] ykf_deriveOATHKeyWithSalt:salt];
+        UInt8 algorithm = YKFOATHCredentialTypeTOTP | YKFOATHCredentialAlgorithmSHA1;
+        
+        [rawRequest ykf_appendEntryWithTag:YKFKeyOATHSetCodeAPDUKeyTag headerBytes:@[@(algorithm)] data:keyData];
+        
+        // Challenge
+        
+        UInt8 challengeBuffer[8];
+        arc4random_buf(challengeBuffer, 8);
+        NSData *challenge = [NSData dataWithBytes:challengeBuffer length:8];
+        [rawRequest ykf_appendEntryWithTag:YKFKeyOATHSetCodeAPDUChallengeTag data:challenge];
+        
+        // Response
+        
+        NSData *response = [challenge ykf_oathHMACWithKey:keyData];
+        [rawRequest ykf_appendEntryWithTag:YKFKeyOATHSetCodeAPDUResponseTag data:response];
+    } else {
+        // Password empty - remove authentication
+        [rawRequest ykf_appendByte:YKFKeyOATHSetCodeAPDUKeyTag];
+        [rawRequest ykf_appendByte:0x00];
+    }
+        
+    return [super initWithCla:0 ins:YKFAPDUCommandInstructionOATHSet p1:0 p2:0 data:rawRequest type:YKFAPDUTypeShort];
+}
+
+@end
diff --git a/YubiKit/YubiKit/Sessions/Shared/APDU/OATH/YKFOATHValidateAPDU.h b/YubiKit/YubiKit/Sessions/Shared/APDU/OATH/YKFOATHValidateAPDU.h
new file mode 100755
index 000000000..422fdfc21
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/APDU/OATH/YKFOATHValidateAPDU.h
@@ -0,0 +1,31 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+#import "YKFAPDU.h"
+
+@class YKFKeyOATHValidateRequest;
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface YKFOATHValidateAPDU: YKFAPDU
+
+@property (nonatomic, nullable) NSData *expectedChallengeData;
+
+- (nullable instancetype)initWithRequest:(YKFKeyOATHValidateRequest *)request challenge:(NSData *)challenge salt:(NSData *)salt NS_DESIGNATED_INITIALIZER;
+- (instancetype)init NS_UNAVAILABLE;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/YubiKit/YubiKit/Sessions/Shared/APDU/OATH/YKFOATHValidateAPDU.m b/YubiKit/YubiKit/Sessions/Shared/APDU/OATH/YKFOATHValidateAPDU.m
new file mode 100755
index 000000000..3dd537bd1
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/APDU/OATH/YKFOATHValidateAPDU.m
@@ -0,0 +1,54 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "YKFOATHValidateAPDU.h"
+#import "YKFKeyOATHValidateRequest.h"
+#import "YKFAPDUCommandInstruction.h"
+#import "YKFOATHCredential.h"
+#import "YKFAssert.h"
+#import "YKFNSMutableDataAdditions.h"
+#import "YKFNSDataAdditions+Private.h"
+
+static const UInt8 YKFKeyOATHSetCodeAPDUChallengeTag = 0x74;
+static const UInt8 YKFKeyOATHSetCodeAPDUResponseTag = 0x75;
+
+@implementation YKFOATHValidateAPDU
+
+- (nullable instancetype)initWithRequest:(nonnull YKFKeyOATHValidateRequest *)request challenge:(NSData *)challenge salt:(NSData *)salt {
+    YKFAssertAbortInit(request);
+    YKFAssertAbortInit(challenge);
+    YKFAssertAbortInit(salt.length);
+    
+    NSMutableData *rawRequest = [[NSMutableData alloc] init];
+    
+    NSData *keyData = [[request.password dataUsingEncoding:NSUTF8StringEncoding] ykf_deriveOATHKeyWithSalt:salt];
+    
+    // Response (hmac of the select challenge)
+    
+    NSData *response = [challenge ykf_oathHMACWithKey:keyData];
+    [rawRequest ykf_appendEntryWithTag:YKFKeyOATHSetCodeAPDUResponseTag data:response];
+        
+    // Challenge (random bytes)
+    
+    UInt8 challengeBuffer[8];
+    arc4random_buf(challengeBuffer, 8);
+    NSData *randomChallenge = [NSData dataWithBytes:challengeBuffer length:8];
+    
+    self.expectedChallengeData = [randomChallenge ykf_oathHMACWithKey:keyData];
+    [rawRequest ykf_appendEntryWithTag:YKFKeyOATHSetCodeAPDUChallengeTag data:randomChallenge];
+        
+    return [super initWithCla:0 ins:YKFAPDUCommandInstructionOATHValidate p1:0 p2:0 data:rawRequest type:YKFAPDUTypeShort];
+}
+
+@end
diff --git a/YubiKit/YubiKit/Sessions/Shared/APDU/OATH/YKFSelectOATHApplicationAPDU.h b/YubiKit/YubiKit/Sessions/Shared/APDU/OATH/YKFSelectOATHApplicationAPDU.h
new file mode 100755
index 000000000..1287999b6
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/APDU/OATH/YKFSelectOATHApplicationAPDU.h
@@ -0,0 +1,23 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+#import "YKFAPDU.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface YKFSelectOATHApplicationAPDU : YKFAPDU
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/YubiKit/YubiKit/Sessions/Shared/APDU/OATH/YKFSelectOATHApplicationAPDU.m b/YubiKit/YubiKit/Sessions/Shared/APDU/OATH/YKFSelectOATHApplicationAPDU.m
new file mode 100755
index 000000000..605d3c999
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/APDU/OATH/YKFSelectOATHApplicationAPDU.m
@@ -0,0 +1,29 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "YKFSelectOATHApplicationAPDU.h"
+#import "YKFAPDUCommandInstruction.h"
+
+// SELECT APDU format: https://developers.yubico.com/OATH/YKOATH_Protocol.html
+static const NSUInteger YKFOathAIDSize = 7;
+static const UInt8 YKFOathAID[YKFOathAIDSize] = {0xA0, 0x00, 0x00, 0x05, 0x27, 0x21, 0x01};
+
+@implementation YKFSelectOATHApplicationAPDU
+
+- (instancetype)init {
+    NSData *data = [NSData dataWithBytes:YKFOathAID length:YKFOathAIDSize];
+    return [super initWithCla:0x00 ins:YKFAPDUCommandInstructionSelectApplication p1:0x04 p2:0x00 data:data type:YKFAPDUTypeShort];
+}
+
+@end
diff --git a/YubiKit/YubiKit/Sessions/Shared/APDU/U2F/YKFSelectU2FApplicationAPDU.h b/YubiKit/YubiKit/Sessions/Shared/APDU/U2F/YKFSelectU2FApplicationAPDU.h
new file mode 100755
index 000000000..2501b021c
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/APDU/U2F/YKFSelectU2FApplicationAPDU.h
@@ -0,0 +1,23 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+#import "YKFAPDU.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface YKFSelectU2FApplicationAPDU : YKFAPDU
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/YubiKit/YubiKit/Sessions/Shared/APDU/U2F/YKFSelectU2FApplicationAPDU.m b/YubiKit/YubiKit/Sessions/Shared/APDU/U2F/YKFSelectU2FApplicationAPDU.m
new file mode 100755
index 000000000..7d6bd0f71
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/APDU/U2F/YKFSelectU2FApplicationAPDU.m
@@ -0,0 +1,28 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "YKFSelectU2FApplicationAPDU.h"
+#import "YKFAPDUCommandInstruction.h"
+
+static const NSUInteger YKFU2fAIDSize = 8;
+static const UInt8 YKFU2fAID[YKFU2fAIDSize] = {0xA0, 0x00, 0x00, 0x06, 0x47, 0x2F, 0x00, 0x01};
+
+@implementation YKFSelectU2FApplicationAPDU
+
+- (instancetype)init {
+    NSData *data = [NSData dataWithBytes:YKFU2fAID length:YKFU2fAIDSize];
+    return [super initWithCla:0x00 ins:YKFAPDUCommandInstructionSelectApplication p1:0x04 p2:0x00 data:data type:YKFAPDUTypeShort];
+}
+
+@end
diff --git a/YubiKit/YubiKit/Sessions/Shared/APDU/U2F/YKFU2FRegisterAPDU.h b/YubiKit/YubiKit/Sessions/Shared/APDU/U2F/YKFU2FRegisterAPDU.h
new file mode 100755
index 000000000..f0a1894a2
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/APDU/U2F/YKFU2FRegisterAPDU.h
@@ -0,0 +1,29 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+#import "YKFAPDU.h"
+
+@class YKFKeyU2FRegisterRequest;
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface YKFU2FRegisterAPDU : YKFAPDU
+
+- (nullable instancetype)initWithU2FRegisterRequest:(YKFKeyU2FRegisterRequest *)request NS_DESIGNATED_INITIALIZER;
+- (instancetype)init NS_UNAVAILABLE;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/YubiKit/YubiKit/Sessions/Shared/APDU/U2F/YKFU2FRegisterAPDU.m b/YubiKit/YubiKit/Sessions/Shared/APDU/U2F/YKFU2FRegisterAPDU.m
new file mode 100755
index 000000000..2366b8269
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/APDU/U2F/YKFU2FRegisterAPDU.m
@@ -0,0 +1,51 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "YKFU2FRegisterAPDU.h"
+#import "YKFAPDUCommandInstruction.h"
+#import "YKFNSDataAdditions.h"
+#import "YKFKeyU2FRegisterRequest.h"
+#import "YKFAssert.h"
+
+#import "YKFKeyU2FRequest+Private.h"
+#import "YKFNSDataAdditions+Private.h"
+
+static const UInt8 YKFU2FRegisterAPDUEnforceUserPresenceAndSign = 0x03;
+
+@implementation YKFU2FRegisterAPDU
+
+- (instancetype)initWithU2FRegisterRequest:(YKFKeyU2FRegisterRequest *)request {
+    YKFAssertAbortInit(request);
+        
+    NSString *appId = request.appId;
+    YKFAssertAbortInit(appId);
+        
+    NSString *clientData = request.clientData;
+    YKFAssertAbortInit(clientData);
+    
+    NSData *challengeSHA256 = [[clientData dataUsingEncoding:NSUTF8StringEncoding] ykf_SHA256];
+    YKFAssertAbortInit(challengeSHA256);
+    
+    NSData *applicationSHA256 = [[appId dataUsingEncoding:NSUTF8StringEncoding] ykf_SHA256];
+    YKFAssertAbortInit(applicationSHA256);
+    
+    NSMutableData *rawU2fRequest = [[NSMutableData alloc] init];
+    
+    [rawU2fRequest appendData:challengeSHA256];
+    [rawU2fRequest appendData:applicationSHA256];
+    
+    return [super initWithCla:0 ins:YKFAPDUCommandInstructionU2FRegister p1:YKFU2FRegisterAPDUEnforceUserPresenceAndSign p2:0 data:rawU2fRequest type:YKFAPDUTypeExtended];
+}
+
+@end
diff --git a/YubiKit/YubiKit/Sessions/Shared/APDU/U2F/YKFU2FSignAPDU.h b/YubiKit/YubiKit/Sessions/Shared/APDU/U2F/YKFU2FSignAPDU.h
new file mode 100755
index 000000000..3da2f4abb
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/APDU/U2F/YKFU2FSignAPDU.h
@@ -0,0 +1,30 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+#import "YKFAPDU.h"
+#import "YKFKeyU2FSignRequest.h"
+
+@class YKFKeyU2FSignRequest;
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface YKFU2FSignAPDU : YKFAPDU
+
+- (nullable instancetype)initWithU2fSignRequest:(YKFKeyU2FSignRequest *)request NS_DESIGNATED_INITIALIZER;
+- (instancetype)init NS_UNAVAILABLE;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/YubiKit/YubiKit/Sessions/Shared/APDU/U2F/YKFU2FSignAPDU.m b/YubiKit/YubiKit/Sessions/Shared/APDU/U2F/YKFU2FSignAPDU.m
new file mode 100755
index 000000000..5e3087e46
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/APDU/U2F/YKFU2FSignAPDU.m
@@ -0,0 +1,62 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "YKFU2FSignAPDU.h"
+#import "YKFAPDUCommandInstruction.h"
+#import "YKFNSDataAdditions.h"
+#import "YKFAssert.h"
+
+#import "YKFNSDataAdditions+Private.h"
+#import "YKFKeyU2FRequest+Private.h"
+
+static const UInt8 YKFU2FSignAPDUKeyHandleSize = 64;
+static const UInt8 YKFU2FSignAPDUEnforceUserPresenceAndSign = 0x03;
+
+@implementation YKFU2FSignAPDU
+
+- (instancetype)initWithU2fSignRequest:(YKFKeyU2FSignRequest *)request {
+    YKFAssertAbortInit(request);
+        
+    NSString *keyHandle = request.keyHandle;
+    YKFAssertAbortInit(keyHandle);
+    
+    NSString *appId = request.appId;
+    YKFAssertAbortInit(appId);
+        
+    NSString *clientData = request.clientData;
+    YKFAssertAbortInit(clientData);
+    
+    NSData *challengeSHA256 = [[clientData dataUsingEncoding:NSUTF8StringEncoding] ykf_SHA256];
+    YKFAssertAbortInit(challengeSHA256);
+    
+    NSData *applicationSHA256 = [[appId dataUsingEncoding:NSUTF8StringEncoding] ykf_SHA256];
+    YKFAssertAbortInit(applicationSHA256);
+    
+    NSMutableData *rawU2FRequest = [NSMutableData data];
+    
+    [rawU2FRequest appendData:challengeSHA256];
+    [rawU2FRequest appendData:applicationSHA256];
+    
+    NSData *keyHandleData = [[NSData alloc] ykf_initWithWebsafeBase64EncodedString:keyHandle dataLength:YKFU2FSignAPDUKeyHandleSize];
+    UInt8 keyHandleLength = [keyHandleData length];
+    YKFAssertAbortInit(keyHandle);
+    YKFAssertAbortInit(keyHandleLength <= UINT8_MAX);
+    
+    [rawU2FRequest appendBytes:&keyHandleLength length:1];
+    [rawU2FRequest appendData:keyHandleData];
+    
+    return [super initWithCla:0 ins:YKFAPDUCommandInstructionU2FSign p1:YKFU2FSignAPDUEnforceUserPresenceAndSign p2:0 data:rawU2FRequest type:YKFAPDUTypeExtended];
+}
+
+@end
diff --git a/YubiKit/YubiKit/Sessions/Shared/APDU/YKFAPDU+Private.h b/YubiKit/YubiKit/Sessions/Shared/APDU/YKFAPDU+Private.h
new file mode 100755
index 000000000..40e29244c
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/APDU/YKFAPDU+Private.h
@@ -0,0 +1,27 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+@interface YKFAPDU()
+
+/*!
+ The APDU raw data which cotains the YubiKey iAP2 Protocol framing.
+ */
+@property (nonatomic, readonly) NSData *ylpApduData;
+
+/*!
+The APDU raw data.
+*/
+@property (nonatomic, readonly) NSData *apduData;
+
+@end
diff --git a/YubiKit/YubiKit/Sessions/Shared/APDU/YKFAPDU.h b/YubiKit/YubiKit/Sessions/Shared/APDU/YKFAPDU.h
new file mode 100755
index 000000000..0d30f4125
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/APDU/YKFAPDU.h
@@ -0,0 +1,92 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+
+/**
+ * ---------------------------------------------------------------------------------------------------------------------
+ * @name APDU Types
+ * ---------------------------------------------------------------------------------------------------------------------
+ */
+
+/*!
+ @abstract
+    Reffers to the encoding type of APDU as defined in ISO/IEC 7816-4 standard.
+ */
+typedef NS_ENUM(NSUInteger, YKFAPDUType) {
+    /*!
+     Data does not exceed 256 bytes in length. CCID commands usually are encoded with short APDUs.
+     */
+    YKFAPDUTypeShort,
+    
+    /*!
+     Data exceeds 256 bytes in length. Some YubiKey applications (like U2F) use extended APDUs.
+     */
+    YKFAPDUTypeExtended
+};
+
+/**
+ * ---------------------------------------------------------------------------------------------------------------------
+ * @name APDU
+ * ---------------------------------------------------------------------------------------------------------------------
+ */
+
+/*!
+ @class YKFAPDU
+ 
+ @abstract
+    Data model for encapsulating an APDU command, as defined by ISO/IEC 7816-4 standard.
+ */
+@interface YKFAPDU: NSObject
+
+/*!
+ @method initWithCla:ins:p1:p2:data:type:
+ 
+ @abstract
+    Creates a new APDU binary command from a list of parameters specified by the ISO/IEC 7816-4 standard.
+ 
+ @param cla
+    The instruction class.
+ @param ins
+    The instruction number.
+ @param p1
+    The first instruction paramater byte.
+ @param p2
+    The second instruction paramater byte.
+ @param data
+    The command data.
+ @param type
+    The type of the APDU, short or extended.
+ 
+ @returns
+    The newly initialized object or nil if the data param is empty or if the data length is too large for a short APDU.
+ */
+- (nullable instancetype)initWithCla:(UInt8)cla ins:(UInt8)ins p1:(UInt8)p1 p2:(UInt8)p2 data:(nonnull NSData *)data type:(YKFAPDUType)type;
+
+/*!
+ @method initWithData:
+ 
+ @abstract
+    Creates a new APDU with pre-built data.
+ @param data
+    The pre-built APDU data.
+ @note
+    This initializer does not check for the data integrity. It is recommended to use always [initWithCla:ins:p1:p2:data:type:] when possible.
+ 
+ @returns
+    The newly initialized object or nil if the data param is empty.
+ */
+- (nullable instancetype)initWithData:(nonnull NSData *)data;
+
+@end
diff --git a/YubiKit/YubiKit/Sessions/Shared/APDU/YKFAPDU.m b/YubiKit/YubiKit/Sessions/Shared/APDU/YKFAPDU.m
new file mode 100755
index 000000000..3a9a81c7f
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/APDU/YKFAPDU.m
@@ -0,0 +1,120 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "YKFAPDU.h"
+#import "YKFAccessoryConnectionController.h"
+#import "YKFNSMutableDataAdditions.h"
+#import "YKFAssert.h"
+
+@interface YKFAPDU()
+
+@property (nonatomic, readwrite) NSData *ylpApduData;
+@property (nonatomic, readwrite) NSData *apduData;
+
+@end
+
+@implementation YKFAPDU
+
+- (instancetype)initWithCla:(UInt8)cla ins:(UInt8)ins p1:(UInt8)p1 p2:(UInt8)p2 data:(NSData *)data type:(YKFAPDUType)type {
+    if (data.length) {
+        if (type == YKFAPDUTypeShort) {
+            YKFAssertAbortInit(data.length <= UINT8_MAX)
+        } else if (type == YKFAPDUTypeExtended) {
+            YKFAssertAbortInit(data.length <= UINT16_MAX)
+        }
+    }
+    
+    self = [super init];
+    if (self) {
+        switch (type) {
+            case YKFAPDUTypeShort:
+                [self setupApduWithCla:cla ins:ins p1:p1 p2:p2 data:data];
+                break;
+            case YKFAPDUTypeExtended:
+            default:
+                [self setupExtendedApduWithCla:cla ins:ins p1:p1 p2:p2 data:data];
+                break;
+        }
+    }
+    return self;
+}
+
+- (void)setupApduWithCla:(UInt8)cla ins:(UInt8)ins p1:(UInt8)p1 p2:(UInt8)p2 data:(NSData*)data {
+    NSMutableData *command = [[NSMutableData alloc] init];
+    
+    [command ykf_appendByte:cla];   // APDU CLA
+    [command ykf_appendByte:ins];   // APDU INS
+    [command ykf_appendByte:p1];    // APDU P1
+    [command ykf_appendByte:p2];    // APDU P2
+
+    if (data.length) {
+        UInt8 length = data.length;
+        [command ykf_appendByte:length];    // LenLc
+        [command appendData:data];          // Data
+    }
+    
+    self.apduData = [command copy];
+    
+    NSMutableData *ylpCommand = [[NSMutableData alloc] initWithCapacity:command.length + 1];
+    [ylpCommand ykf_appendByte:0x00]; // YLP iAP2 Signal
+    [ylpCommand appendData:command];
+    self.ylpApduData = [ylpCommand copy];
+    
+}
+
+- (void)setupExtendedApduWithCla:(UInt8)cla ins:(UInt8)ins p1:(UInt8)p1 p2:(UInt8)p2 data:(NSData *)data {
+    NSMutableData *command = [[NSMutableData alloc] init];
+    
+    [command ykf_appendByte:cla];   // APDU CLA
+    [command ykf_appendByte:ins];   // APDU INS
+    [command ykf_appendByte:p1];    // APDU P1
+    [command ykf_appendByte:p2];    // APDU P2
+    
+    if (data.length) {
+        UInt8 lengthHigh = data.length / 256;
+        UInt8 lengthLow = data.length % 256;
+        [command ykf_appendByte:0x00];           // APDU Zero
+        [command ykf_appendByte:lengthHigh];     // LenH
+        [command ykf_appendByte:lengthLow];      // LenL
+        [command appendData:data];               // Data
+    } else {
+        [command ykf_appendByte:0x00];           // APDU Zero
+        [command ykf_appendByte:0x00];           // LenH
+        [command ykf_appendByte:0x00];           // LenL
+    }
+    
+    self.apduData = [command copy];
+    
+    NSMutableData *ylpCommand = [[NSMutableData alloc] initWithCapacity:command.length + 1];
+    [ylpCommand ykf_appendByte:0x00]; // YLP iAP2 Signal
+    [ylpCommand appendData:command];
+    self.ylpApduData = [ylpCommand copy];
+}
+
+- (nullable instancetype)initWithData:(nonnull NSData *)data {
+    YKFAssertAbortInit(data.length);
+    self = [super init];
+    if (self) {
+        self.apduData = [data copy];
+        
+        // Append the YLP iAP2 Signal for the ylpApduData.
+        NSMutableData *tempBuffer = [[NSMutableData alloc] initWithCapacity:data.length + 1];
+        [tempBuffer ykf_appendByte:0x00];
+        [tempBuffer appendData:data];
+        self.ylpApduData = [tempBuffer copy];
+    }
+    return self;
+}
+
+@end
diff --git a/YubiKit/YubiKit/Sessions/Shared/APDU/YKFAPDUCommandInstruction.h b/YubiKit/YubiKit/Sessions/Shared/APDU/YKFAPDUCommandInstruction.h
new file mode 100755
index 000000000..979b7725d
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/APDU/YKFAPDUCommandInstruction.h
@@ -0,0 +1,44 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+
+typedef NS_ENUM(NSUInteger, YKFAPDUCommandInstruction) {
+    YKFAPDUCommandInstructionNone = 0x00,
+    
+    /* U2F instructions */
+    YKFAPDUCommandInstructionU2FRegister = 0x01,
+    YKFAPDUCommandInstructionU2FSign = 0x02,
+    YKFAPDUCommandInstructionU2FVersion = 0x03,
+    YKFAPDUCommandInstructionU2FPing = 0x40,
+    YKFAPDUCommandInstructionU2FCustom = 0xFF,
+    
+    /* FIDO2 instructions */
+    YKFAPDUCommandInstructionFIDO2Msg = 0x10,
+    YKFAPDUCommandInstructionFIDO2GetResponse = 0x11,
+    
+    /* OATH instructions */
+    YKFAPDUCommandInstructionOATHPut = 0x01,
+    YKFAPDUCommandInstructionOATHDelete = 0x02,
+    YKFAPDUCommandInstructionOATHSet = 0x03,
+    YKFAPDUCommandInstructionOATHReset = 0x04,
+    YKFAPDUCommandInstructionOATHList = 0xA1,
+    YKFAPDUCommandInstructionOATHCalculate = 0xA2,
+    YKFAPDUCommandInstructionOATHValidate = 0xA3,
+    YKFAPDUCommandInstructionOATHCalculateAll = 0xA4,
+    YKFAPDUCommandInstructionOATHSendRemaining = 0xA5,
+    
+    /* Application selection */
+    YKFAPDUCommandInstructionSelectApplication = 0xA4
+};
diff --git a/YubiKit/YubiKit/Sessions/Shared/Errors/YKFKeyAPDUError.h b/YubiKit/YubiKit/Sessions/Shared/Errors/YKFKeyAPDUError.h
new file mode 100755
index 000000000..a4e6907aa
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Errors/YKFKeyAPDUError.h
@@ -0,0 +1,41 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "YKFKeySessionError.h"
+
+typedef NS_ENUM(NSUInteger, YKFKeyAPDUErrorCode) {
+    YKFKeyAPDUErrorCodeNoError                  = 0x9000,
+    YKFKeyAPDUErrorCodeFIDO2TouchRequired       = 0x9100,
+    YKFKeyAPDUErrorCodeConditionNotSatisfied    = 0x6985,
+    
+    YKFKeyAPDUErrorCodeAuthenticationRequired   = 0x6982,
+    YKFKeyAPDUErrorCodeDataInvalid              = 0x6984,
+    YKFKeyAPDUErrorCodeWrongLength              = 0x6700,
+    YKFKeyAPDUErrorCodeWrongData                = 0x6A80,
+    YKFKeyAPDUErrorCodeInsNotSupported          = 0x6D00,
+    YKFKeyAPDUErrorCodeCLANotSupported          = 0x6E00,
+    YKFKeyAPDUErrorCodeUnknown                  = 0x6F00,
+    YKFKeyAPDUErrorCodeMissingFile              = 0x6A82,
+    
+    // Application/Applet short codes
+    
+    YKFKeyAPDUErrorCodeMoreData                 = 0x61 // 0x61XX
+};
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface YKFKeyAPDUError: YKFKeySessionError
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/YubiKit/YubiKit/Sessions/Shared/Errors/YKFKeyAPDUError.m b/YubiKit/YubiKit/Sessions/Shared/Errors/YKFKeyAPDUError.m
new file mode 100755
index 000000000..89fc632d4
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Errors/YKFKeyAPDUError.m
@@ -0,0 +1,18 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "YKFKeyAPDUError.h"
+
+@implementation YKFKeyAPDUError
+@end
diff --git a/YubiKit/YubiKit/Sessions/Shared/Errors/YKFKeyFIDO2Error.h b/YubiKit/YubiKit/Sessions/Shared/Errors/YKFKeyFIDO2Error.h
new file mode 100755
index 000000000..3e67b370d
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Errors/YKFKeyFIDO2Error.h
@@ -0,0 +1,223 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "YKFKeySessionError.h"
+
+typedef NS_ENUM(NSUInteger, YKFKeyFIDO2ErrorCode) {
+    
+    /*! Indicates successful response.
+     */
+    YKFKeyFIDO2ErrorCodeSUCCESS = 0x00,
+    
+    /*! The command is not a valid CTAP command.
+     */
+    YKFKeyFIDO2ErrorCodeINVALID_COMMAND = 0x01,
+    
+    /*! The command included an invalid parameter.
+     */
+    YKFKeyFIDO2ErrorCodeINVALID_PARAMETER = 0x02,
+    
+    /*! Invalid message or item length.
+     */
+    YKFKeyFIDO2ErrorCodeINVALID_LENGTH = 0x03,
+    
+    /*! Invalid message sequencing.
+     */
+    YKFKeyFIDO2ErrorCodeINVALID_SEQ = 0x04,
+    
+    /*! Message timed out.
+     */
+    YKFKeyFIDO2ErrorCodeTIMEOUT = 0x05,
+    
+    /*! Channel busy.
+     */
+    YKFKeyFIDO2ErrorCodeCHANNEL_BUSY = 0x06,
+    
+    /*! Command requires channel lock.
+     */
+    YKFKeyFIDO2ErrorCodeLOCK_REQUIRED = 0x0A,
+    
+    /*! Command not allowed on this cid.
+     */
+    YKFKeyFIDO2ErrorCodeINVALID_CHANNEL = 0x0B,
+    
+    /*! Other unspecified error.
+     */
+    YKFKeyFIDO2ErrorCodeOTHER = 0x7F,
+    
+    /*! Invalid/unexpected CBOR error.
+     */
+    YKFKeyFIDO2ErrorCodeCBOR_UNEXPECTED_TYPE = 0x11,
+    
+    /*! Error when parsing CBOR.
+     */
+    YKFKeyFIDO2ErrorCodeINVALID_CBOR = 0x12,
+    
+    /*! Missing non-optional parameter.
+     */
+    YKFKeyFIDO2ErrorCodeMISSING_PARAMETER = 0x14,
+    
+    /*! Limit for number of items exceeded.
+     */
+    YKFKeyFIDO2ErrorCodeLIMIT_EXCEEDED = 0x15,
+    
+    /*! Unsupported extension.
+     */
+    YKFKeyFIDO2ErrorCodeUNSUPPORTED_EXTENSION = 0x16,
+    
+    /*! Valid credential found in the exclude list.
+     */
+    YKFKeyFIDO2ErrorCodeCREDENTIAL_EXCLUDED = 0x19,
+    
+    /*! Lengthy operation is in progress.
+     */
+    YKFKeyFIDO2ErrorCodePROCESSING = 0x21,
+    
+    /*! Credential not valid for the authenticator.
+     */
+    YKFKeyFIDO2ErrorCodeINVALID_CREDENTIAL = 0x22,
+    
+    /*! Authentication is waiting for user interaction.
+     */
+    YKFKeyFIDO2ErrorCodeUSER_ACTION_PENDING = 0x23,
+    
+    /*! Processing, lengthy operation is in progress.
+     */
+    YKFKeyFIDO2ErrorCodeOPERATION_PENDING = 0x24,
+    
+    /*! No request is pending.
+     */
+    YKFKeyFIDO2ErrorCodeNO_OPERATIONS = 0x25,
+    
+    /*! Authenticator does not support requested algorithm.
+     */
+    YKFKeyFIDO2ErrorCodeUNSUPPORTED_ALGORITHM = 0x26,
+    
+    /*! Not authorized for requested operation.
+     */
+    YKFKeyFIDO2ErrorCodeOPERATION_DENIED = 0x27,
+    
+    /*! Internal key storage is full.
+     */
+    YKFKeyFIDO2ErrorCodeKEY_STORE_FULL = 0x28,
+    
+    /*! Authenticator cannot cancel as it is not busy.
+     */
+    YKFKeyFIDO2ErrorCodeNOT_BUSY = 0x29,
+    
+    /*! No outstanding operations.
+     */
+    YKFKeyFIDO2ErrorCodeNO_OPERATION_PENDING = 0x2A,
+    
+    /*! Unsupported option.
+     */
+    YKFKeyFIDO2ErrorCodeUNSUPPORTED_OPTION = 0x2B,
+    
+    /*! Not a valid option for current operation.
+     */
+    YKFKeyFIDO2ErrorCodeINVALID_OPTION = 0x2C,
+    
+    /*! Pending keep alive was cancelled.
+     */
+    YKFKeyFIDO2ErrorCodeKEEPALIVE_CANCEL = 0x2D,
+    
+    /*! No valid credentials provided.
+     */
+    YKFKeyFIDO2ErrorCodeNO_CREDENTIALS = 0x2E,
+    
+    /*! Timeout waiting for user interaction.
+     */
+    YKFKeyFIDO2ErrorCodeUSER_ACTION_TIMEOUT = 0x2F,
+    
+    /*! Continuation command, such as, authenticatorGetNextAssertion not allowed.
+     */
+    YKFKeyFIDO2ErrorCodeNOT_ALLOWED = 0x30,
+    
+    /*! PIN Invalid.
+     */
+    YKFKeyFIDO2ErrorCodePIN_INVALID = 0x31,
+    
+    /*! PIN Blocked.
+     */
+    YKFKeyFIDO2ErrorCodePIN_BLOCKED = 0x32,
+    
+    /*! PIN authentication,pinAuth, verification failed.
+     */
+    YKFKeyFIDO2ErrorCodePIN_AUTH_INVALID = 0x33,
+    
+    /*! PIN authentication,pinAuth, blocked. Requires power recycle to reset.
+     */
+    YKFKeyFIDO2ErrorCodePIN_AUTH_BLOCKED = 0x34,
+    
+    /*! No PIN has been set.
+     */
+    YKFKeyFIDO2ErrorCodePIN_NOT_SET = 0x35,
+    
+    /*! PIN is required for the selected operation.
+     */
+    YKFKeyFIDO2ErrorCodePIN_REQUIRED = 0x36,
+    
+    /*! PIN policy violation. Currently only enforces minimum length.
+     */
+    YKFKeyFIDO2ErrorCodePIN_POLICY_VIOLATION = 0x37,
+    
+    /*! pinToken expired on authenticator.
+     */
+    YKFKeyFIDO2ErrorCodePIN_TOKEN_EXPIRED = 0x38,
+    
+    /*! Authenticator cannot handle this request due to memory constraints.
+     */
+    YKFKeyFIDO2ErrorCodeREQUEST_TOO_LARGE = 0x39,
+    
+    /*! The current operation has timed out.
+     */
+    YKFKeyFIDO2ErrorCodeACTION_TIMEOUT = 0x3A,
+    
+    /*! User presence is required for the requested operation.
+     */
+    YKFKeyFIDO2ErrorCodeUP_REQUIRED = 0x3B,
+    
+    /*! CTAP 2 spec last error.
+     */
+    YKFKeyFIDO2ErrorCodeSPEC_LAST = 0xDF,
+    
+    /*! Extension specific error.
+     */
+    YKFKeyFIDO2ErrorCodeEXTENSION_FIRST = 0xE0,
+    
+    /*! Extension specific error.
+     */
+    YKFKeyFIDO2ErrorCodeEXTENSION_LAST = 0xEF,
+    
+    /*! Vendor specific error.
+     */
+    YKFKeyFIDO2ErrorCodeVENDOR_FIRST = 0xF0,
+    
+    /*! Vendor specific error.
+     */
+    YKFKeyFIDO2ErrorCodeVENDOR_LAST = 0xFF,
+};
+
+NS_ASSUME_NONNULL_BEGIN
+
+/*!
+ @class
+    YKFKeyFIDO2Error
+ @abstract
+    Error type returned by the YKFKeyFIDO2Service.
+ */
+@interface YKFKeyFIDO2Error: YKFKeySessionError
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/YubiKit/YubiKit/Sessions/Shared/Errors/YKFKeyFIDO2Error.m b/YubiKit/YubiKit/Sessions/Shared/Errors/YKFKeyFIDO2Error.m
new file mode 100755
index 000000000..bd43ebcd8
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Errors/YKFKeyFIDO2Error.m
@@ -0,0 +1,131 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "YKFKeyFIDO2Error.h"
+#import "YKFKeySessionError+Private.h"
+
+#pragma mark - Error Descriptions
+
+static NSString* const YKFKeyFIDO2ErrorSUCCESS = @"Successful response";
+static NSString* const YKFKeyFIDO2ErrorINVALID_COMMAND = @"The command is not a valid CTAP command.";
+static NSString* const YKFKeyFIDO2ErrorINVALID_PARAMETER = @"The command included an invalid parameter.";
+static NSString* const YKFKeyFIDO2ErrorINVALID_LENGTH = @"Invalid message or item length.";
+static NSString* const YKFKeyFIDO2ErrorINVALID_SEQ = @"Invalid message sequencing.";
+static NSString* const YKFKeyFIDO2ErrorTIMEOUT = @"Message timed out.";
+static NSString* const YKFKeyFIDO2ErrorCHANNEL_BUSY = @"Channel busy.";
+static NSString* const YKFKeyFIDO2ErrorLOCK_REQUIRED = @"Command requires channel lock.";
+static NSString* const YKFKeyFIDO2ErrorINVALID_CHANNEL = @"Command not allowed on this cid.";
+static NSString* const YKFKeyFIDO2ErrorOTHER = @"Other unspecified error.";
+static NSString* const YKFKeyFIDO2ErrorCBOR_UNEXPECTED_TYPE = @"Invalid/unexpected CBOR error.";
+static NSString* const YKFKeyFIDO2ErrorINVALID_CBOR = @"Error when parsing CBOR.";
+static NSString* const YKFKeyFIDO2ErrorMISSING_PARAMETER = @"Missing non-optional parameter.";
+static NSString* const YKFKeyFIDO2ErrorLIMIT_EXCEEDED = @"Limit for number of items exceeded.";
+static NSString* const YKFKeyFIDO2ErrorUNSUPPORTED_EXTENSION = @"Unsupported extension.";
+static NSString* const YKFKeyFIDO2ErrorCREDENTIAL_EXCLUDED = @"Valid credential found in the exclude list.";
+static NSString* const YKFKeyFIDO2ErrorPROCESSING = @"Lengthy operation is in progress.";
+static NSString* const YKFKeyFIDO2ErrorINVALID_CREDENTIAL = @"Credential not valid for the authenticator.";
+static NSString* const YKFKeyFIDO2ErrorUSER_ACTION_PENDING = @"Authentication is waiting for user interaction.";
+static NSString* const YKFKeyFIDO2ErrorOPERATION_PENDING = @"Processing, lengthy operation is in progress.";
+static NSString* const YKFKeyFIDO2ErrorNO_OPERATIONS = @"No request is pending.";
+static NSString* const YKFKeyFIDO2ErrorUNSUPPORTED_ALGORITHM = @"Authenticator does not support requested algorithm.";
+static NSString* const YKFKeyFIDO2ErrorOPERATION_DENIED = @"Not authorized for requested operation.";
+static NSString* const YKFKeyFIDO2ErrorKEY_STORE_FULL = @"Internal key storage is full.";
+static NSString* const YKFKeyFIDO2ErrorNOT_BUSY = @"Authenticator cannot cancel as it is not busy.";
+static NSString* const YKFKeyFIDO2ErrorNO_OPERATION_PENDING = @"No outstanding operations.";
+static NSString* const YKFKeyFIDO2ErrorUNSUPPORTED_OPTION = @"Unsupported option.";
+static NSString* const YKFKeyFIDO2ErrorINVALID_OPTION = @"Not a valid option for current operation.";
+static NSString* const YKFKeyFIDO2ErrorKEEPALIVE_CANCEL = @"Pending keep alive was cancelled.";
+static NSString* const YKFKeyFIDO2ErrorNO_CREDENTIALS = @"No valid credentials provided.";
+static NSString* const YKFKeyFIDO2ErrorUSER_ACTION_TIMEOUT = @"Timeout waiting for user interaction.";
+static NSString* const YKFKeyFIDO2ErrorNOT_ALLOWED = @"Continuation command, such as, authenticatorGetNextAssertion not allowed.";
+static NSString* const YKFKeyFIDO2ErrorPIN_INVALID = @"PIN Invalid.";
+static NSString* const YKFKeyFIDO2ErrorPIN_BLOCKED = @"PIN Blocked.";
+static NSString* const YKFKeyFIDO2ErrorPIN_AUTH_INVALID = @"PIN authentication,pinAuth, verification failed.";
+static NSString* const YKFKeyFIDO2ErrorPIN_AUTH_BLOCKED = @"PIN authentication,pinAuth, blocked. Requires power recycle to reset.";
+static NSString* const YKFKeyFIDO2ErrorPIN_NOT_SET = @"No PIN has been set.";
+static NSString* const YKFKeyFIDO2ErrorPIN_REQUIRED = @"PIN is required for the selected operation.";
+static NSString* const YKFKeyFIDO2ErrorPIN_POLICY_VIOLATION = @"PIN policy violation. Currently only enforces minimum length.";
+static NSString* const YKFKeyFIDO2ErrorPIN_TOKEN_EXPIRED = @"pinToken expired on authenticator.";
+static NSString* const YKFKeyFIDO2ErrorREQUEST_TOO_LARGE = @"Authenticator cannot handle this request due to memory constraints.";
+static NSString* const YKFKeyFIDO2ErrorACTION_TIMEOUT = @"The current operation has timed out.";
+static NSString* const YKFKeyFIDO2ErrorUP_REQUIRED = @"User presence is required for the requested operation.";
+
+#pragma mark - YKFKeyFIDO2Error
+
+@implementation YKFKeyFIDO2Error
+
+static NSDictionary *errorMap = nil;
+
++ (YKFKeySessionError *)errorWithCode:(NSUInteger)code {
+    static dispatch_once_t onceToken;
+    dispatch_once(&onceToken, ^{
+        [YKFKeyFIDO2Error buildErrorMap];
+    });
+    
+    NSString *errorDescription = errorMap[@(code)];
+    if (!errorDescription) {
+        return [super errorWithCode:code];
+    }
+    return [[YKFKeySessionError alloc] initWithCode:code message:errorDescription];
+}
+
++ (void)buildErrorMap {
+    errorMap =
+    @{@(YKFKeyFIDO2ErrorCodeSUCCESS): YKFKeyFIDO2ErrorSUCCESS,
+      @(YKFKeyFIDO2ErrorCodeINVALID_COMMAND): YKFKeyFIDO2ErrorINVALID_COMMAND,
+      @(YKFKeyFIDO2ErrorCodeINVALID_PARAMETER): YKFKeyFIDO2ErrorINVALID_PARAMETER,
+      @(YKFKeyFIDO2ErrorCodeINVALID_LENGTH): YKFKeyFIDO2ErrorINVALID_LENGTH,
+      @(YKFKeyFIDO2ErrorCodeINVALID_SEQ): YKFKeyFIDO2ErrorINVALID_SEQ,
+      @(YKFKeyFIDO2ErrorCodeTIMEOUT): YKFKeyFIDO2ErrorTIMEOUT,
+      @(YKFKeyFIDO2ErrorCodeCHANNEL_BUSY): YKFKeyFIDO2ErrorCHANNEL_BUSY,
+      @(YKFKeyFIDO2ErrorCodeLOCK_REQUIRED): YKFKeyFIDO2ErrorLOCK_REQUIRED,
+      @(YKFKeyFIDO2ErrorCodeINVALID_CHANNEL): YKFKeyFIDO2ErrorINVALID_CHANNEL,
+      @(YKFKeyFIDO2ErrorCodeOTHER): YKFKeyFIDO2ErrorOTHER,
+      @(YKFKeyFIDO2ErrorCodeCBOR_UNEXPECTED_TYPE): YKFKeyFIDO2ErrorCBOR_UNEXPECTED_TYPE,
+      @(YKFKeyFIDO2ErrorCodeINVALID_CBOR): YKFKeyFIDO2ErrorINVALID_CBOR,
+      @(YKFKeyFIDO2ErrorCodeMISSING_PARAMETER): YKFKeyFIDO2ErrorMISSING_PARAMETER,
+      @(YKFKeyFIDO2ErrorCodeLIMIT_EXCEEDED): YKFKeyFIDO2ErrorLIMIT_EXCEEDED,
+      @(YKFKeyFIDO2ErrorCodeUNSUPPORTED_EXTENSION): YKFKeyFIDO2ErrorUNSUPPORTED_EXTENSION,
+      @(YKFKeyFIDO2ErrorCodeCREDENTIAL_EXCLUDED): YKFKeyFIDO2ErrorCREDENTIAL_EXCLUDED,
+      @(YKFKeyFIDO2ErrorCodePROCESSING): YKFKeyFIDO2ErrorPROCESSING,
+      @(YKFKeyFIDO2ErrorCodeINVALID_CREDENTIAL): YKFKeyFIDO2ErrorINVALID_CREDENTIAL,
+      @(YKFKeyFIDO2ErrorCodeUSER_ACTION_PENDING): YKFKeyFIDO2ErrorUSER_ACTION_PENDING,
+      @(YKFKeyFIDO2ErrorCodeOPERATION_PENDING): YKFKeyFIDO2ErrorOPERATION_PENDING,
+      @(YKFKeyFIDO2ErrorCodeNO_OPERATIONS): YKFKeyFIDO2ErrorNO_OPERATIONS,
+      @(YKFKeyFIDO2ErrorCodeUNSUPPORTED_ALGORITHM): YKFKeyFIDO2ErrorUNSUPPORTED_ALGORITHM,
+      @(YKFKeyFIDO2ErrorCodeOPERATION_DENIED): YKFKeyFIDO2ErrorOPERATION_DENIED,
+      @(YKFKeyFIDO2ErrorCodeKEY_STORE_FULL): YKFKeyFIDO2ErrorKEY_STORE_FULL,
+      @(YKFKeyFIDO2ErrorCodeNOT_BUSY): YKFKeyFIDO2ErrorNOT_BUSY,
+      @(YKFKeyFIDO2ErrorCodeNO_OPERATION_PENDING): YKFKeyFIDO2ErrorNO_OPERATION_PENDING,
+      @(YKFKeyFIDO2ErrorCodeUNSUPPORTED_OPTION): YKFKeyFIDO2ErrorUNSUPPORTED_OPTION,
+      @(YKFKeyFIDO2ErrorCodeINVALID_OPTION): YKFKeyFIDO2ErrorINVALID_OPTION,
+      @(YKFKeyFIDO2ErrorCodeKEEPALIVE_CANCEL): YKFKeyFIDO2ErrorKEEPALIVE_CANCEL,
+      @(YKFKeyFIDO2ErrorCodeNO_CREDENTIALS): YKFKeyFIDO2ErrorNO_CREDENTIALS,
+      @(YKFKeyFIDO2ErrorCodeUSER_ACTION_TIMEOUT): YKFKeyFIDO2ErrorUSER_ACTION_TIMEOUT,
+      @(YKFKeyFIDO2ErrorCodeNOT_ALLOWED): YKFKeyFIDO2ErrorNOT_ALLOWED,
+      @(YKFKeyFIDO2ErrorCodePIN_INVALID): YKFKeyFIDO2ErrorPIN_INVALID,
+      @(YKFKeyFIDO2ErrorCodePIN_BLOCKED): YKFKeyFIDO2ErrorPIN_BLOCKED,
+      @(YKFKeyFIDO2ErrorCodePIN_AUTH_INVALID): YKFKeyFIDO2ErrorPIN_AUTH_INVALID,
+      @(YKFKeyFIDO2ErrorCodePIN_AUTH_BLOCKED): YKFKeyFIDO2ErrorPIN_AUTH_BLOCKED,
+      @(YKFKeyFIDO2ErrorCodePIN_NOT_SET): YKFKeyFIDO2ErrorPIN_NOT_SET,
+      @(YKFKeyFIDO2ErrorCodePIN_REQUIRED): YKFKeyFIDO2ErrorPIN_REQUIRED,
+      @(YKFKeyFIDO2ErrorCodePIN_POLICY_VIOLATION): YKFKeyFIDO2ErrorPIN_POLICY_VIOLATION,
+      @(YKFKeyFIDO2ErrorCodePIN_TOKEN_EXPIRED): YKFKeyFIDO2ErrorPIN_TOKEN_EXPIRED,
+      @(YKFKeyFIDO2ErrorCodeREQUEST_TOO_LARGE): YKFKeyFIDO2ErrorREQUEST_TOO_LARGE,
+      @(YKFKeyFIDO2ErrorCodeACTION_TIMEOUT): YKFKeyFIDO2ErrorACTION_TIMEOUT,
+      @(YKFKeyFIDO2ErrorCodeUP_REQUIRED): YKFKeyFIDO2ErrorUP_REQUIRED
+      };
+}
+
+@end
diff --git a/YubiKit/YubiKit/Sessions/Shared/Errors/YKFKeyOATHError.h b/YubiKit/YubiKit/Sessions/Shared/Errors/YKFKeyOATHError.h
new file mode 100755
index 000000000..339da59fc
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Errors/YKFKeyOATHError.h
@@ -0,0 +1,74 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "YKFKeySessionError.h"
+
+typedef NS_ENUM(NSUInteger, YKFKeyOATHErrorCode) {
+    
+    /*! The host application tried to perform an OATH put credential operation with a credential which has a name longer then the
+     maximum allowed length by the key.
+     */
+    YKFKeyOATHErrorCodeNameTooLong = 0x000100,
+    
+    /*! The host application tried to perform an OATH put credential operation with a credential which has a secret longer then the
+     maximum allowed length of the SHA block size. The size of the secret should be less or equal to the size of the hash algorithm.
+     */
+    YKFKeyOATHErrorCodeSecretTooLong = 0x000101,
+    
+    /*! The key did not return correct data to a calculate request.
+     */
+    YKFKeyOATHErrorCodeBadCalculationResponse = 0x000102,
+    
+    /*! The key did not return correct data for a list request.
+     */
+    YKFKeyOATHErrorCodeBadListResponse = 0x000103,
+    
+    /*! The key did not return correct data when selecting the key OATH application.
+     */
+    YKFKeyOATHErrorCodeBadApplicationSelectionResponse = 0x000104,
+    
+    /*! The OATH application requires a validate call to unlock the application, when a password/code is set.
+     */
+    YKFKeyOATHErrorCodeAuthenticationRequired = 0x000105,
+    
+    /*! The key did not return correct data when validating a code set on the OATH application.
+     */
+    YKFKeyOATHErrorCodeBadValidationResponse = 0x000106,
+    
+    /*! The key did not return correct data when calculating all credentials.
+     */
+    YKFKeyOATHErrorCodeBadCalculateAllResponse = 0x000107,
+    
+    /*! The key did time out, waiting for the user to touch the key when calculating a credential which requires touch.
+     */
+    YKFKeyOATHErrorCodeTouchTimeout = 0x000108,
+    
+    /*! Wrong password used for authentication.
+     */
+    YKFKeyOATHErrorCodeWrongPassword = 0x000109
+};
+
+
+NS_ASSUME_NONNULL_BEGIN
+
+/*!
+ @class
+    YKFKeyOATHError
+ @abstract
+    Error type returned by the YKFKeyOATHService.
+ */
+@interface YKFKeyOATHError: YKFKeySessionError
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/YubiKit/YubiKit/Sessions/Shared/Errors/YKFKeyOATHError.m b/YubiKit/YubiKit/Sessions/Shared/Errors/YKFKeyOATHError.m
new file mode 100755
index 000000000..4ffdbc65c
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Errors/YKFKeyOATHError.m
@@ -0,0 +1,61 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "YKFKeyOATHError.h"
+#import "YKFKeySessionError+Private.h"
+
+static NSString* const YKFKeyOATHErrorNameTooLongDescription = @"The credential has a name longer then the maximum allowed size by the key (64 bytes).";
+static NSString* const YKFKeyOATHErrorSecretTooLongDescription = @"The credential has a secret longer then the size of the hash algorithm size.";
+static NSString* const YKFKeyOATHErrorBadCalculationResponseDescription = @"The key returned a malformed response to the calculate request.";
+static NSString* const YKFKeyOATHErrorBadListResponseDescription = @"The key returned a malformed response to the list request.";
+static NSString* const YKFKeyOATHErrorBadApplicationSelectionResponseDescription = @"The key returned a malformed response when selecting OATH.";
+static NSString* const YKFKeyOATHErrorAuthenticationRequiredDescription = @"Authentication required.";
+static NSString* const YKFKeyOATHErrorMalformedValidationResponseDescription = @"The key returned a malformed response when validating.";
+static NSString* const YKFKeyOATHErrorBadCalculateAllResponseDescription = @"The key returned a malformed response when calculating all credentials.";
+static NSString* const YKFKeyOATHErrorCodeTouchTimeoutDescription = @"The key did time out, waiting for touch.";
+static NSString* const YKFKeyOATHErrorCodeWrongPasswordDescription = @"Wrong password.";
+
+@implementation YKFKeyOATHError
+
+static NSDictionary *errorMap = nil;
+
++ (YKFKeySessionError *)errorWithCode:(NSUInteger)code {
+    static dispatch_once_t onceToken;
+    dispatch_once(&onceToken, ^{
+        [YKFKeyOATHError buildErrorMap];
+    });
+    
+    NSString *errorDescription = errorMap[@(code)];
+    if (!errorDescription) {
+        return [super errorWithCode:code];
+    }
+    return [[YKFKeySessionError alloc] initWithCode:code message:errorDescription];
+}
+
++ (void)buildErrorMap {
+    errorMap =
+    @{@(YKFKeyOATHErrorCodeNameTooLong): YKFKeyOATHErrorNameTooLongDescription,
+      @(YKFKeyOATHErrorCodeSecretTooLong): YKFKeyOATHErrorSecretTooLongDescription,
+      @(YKFKeyOATHErrorCodeBadCalculationResponse): YKFKeyOATHErrorBadCalculationResponseDescription,
+      @(YKFKeyOATHErrorCodeBadListResponse): YKFKeyOATHErrorBadListResponseDescription,
+      @(YKFKeyOATHErrorCodeBadApplicationSelectionResponse): YKFKeyOATHErrorBadApplicationSelectionResponseDescription,
+      @(YKFKeyOATHErrorCodeAuthenticationRequired): YKFKeyOATHErrorAuthenticationRequiredDescription,
+      @(YKFKeyOATHErrorCodeBadValidationResponse): YKFKeyOATHErrorMalformedValidationResponseDescription,
+      @(YKFKeyOATHErrorCodeBadCalculateAllResponse): YKFKeyOATHErrorBadCalculateAllResponseDescription,
+      @(YKFKeyOATHErrorCodeTouchTimeout): YKFKeyOATHErrorCodeTouchTimeoutDescription,
+      @(YKFKeyOATHErrorCodeWrongPassword): YKFKeyOATHErrorCodeWrongPasswordDescription
+      };
+}
+
+@end
diff --git a/YubiKit/YubiKit/Sessions/Shared/Errors/YKFKeySessionError+Private.h b/YubiKit/YubiKit/Sessions/Shared/Errors/YKFKeySessionError+Private.h
new file mode 100755
index 000000000..b9d824607
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Errors/YKFKeySessionError+Private.h
@@ -0,0 +1,26 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface YKFKeySessionError()
+
++ (YKFKeySessionError *)errorWithCode:(NSUInteger)code;
+- (instancetype)initWithCode:(NSInteger)code message:(NSString *)message NS_DESIGNATED_INITIALIZER;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/YubiKit/YubiKit/Sessions/Shared/Errors/YKFKeySessionError.h b/YubiKit/YubiKit/Sessions/Shared/Errors/YKFKeySessionError.h
new file mode 100755
index 000000000..a64819442
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Errors/YKFKeySessionError.h
@@ -0,0 +1,72 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+/*!
+ @constant
+    YKFKeySessionErrorDomain
+ @abstract
+    Domain for errors generated by a communication session with a key.
+ */
+extern NSString* const YKFKeySessionErrorDomain;
+
+typedef NS_ENUM(NSUInteger, YKFKeySessionErrorCode) {
+    
+    /*! When the key didn't repond to an issued command. In such a scenario the key may not be properly connected
+     to the device or the communication with the key is somehow broken along the SDK-IAP2-Firmware path. In such
+     a scenario replugging or checking if the key is properly connected may solve the issue.
+     */
+    YKFKeySessionErrorReadTimeoutCode = 0x000001,
+    
+    /*! When the library cannot write/send a command to the key. In such a scenario the key may not be properly connected
+     to the device or the communication with the key is somehow broken along the SDK-IAP2-Firmware path. In such
+     a scenario replugging or checking if the key is properly connected may solve the issue.
+     */
+    YKFKeySessionErrorWriteTimeoutCode = 0x000002,
+    
+    /*! When the key expects the user to confirm the presence by touching the key. The user didn't touch the key
+     for 15 seconds so the operation was canceled.
+     */
+    YKFKeySessionErrorTouchTimeoutCode = 0x000003,
+    
+    /*! A request to the key cannot be performed because the key is performing another operation.
+     @discussion
+        This should not be an issue when using only YubiKit because YubiKit will execute the requests sequentially. This issue
+        may happen when the key is performing an operation on behalf of another application or if the user is generating an OTP
+        which is independent of YubiKit. The key operations are usually fast so a recovery solution is to try again in a few seconds.
+     */
+    YKFKeySessionErrorKeyBusyCode = 0x000004,
+
+    /*! A certain key application is missing or it was disabled using a configuration tool like YubiKey Manager. In such a scenario
+     the functionality of the key should be enabled before trying again the request.
+     */
+    YKFKeySessionErrorMissingApplicationCode = 0x000005
+};
+
+/*!
+ @class
+    YKFKeySessionError
+ @abstract
+    Error type returned by the YKFAccessorySession.
+ */
+@interface YKFKeySessionError : NSError
+
+- (instancetype)init NS_UNAVAILABLE;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/YubiKit/YubiKit/Sessions/Shared/Errors/YKFKeySessionError.m b/YubiKit/YubiKit/Sessions/Shared/Errors/YKFKeySessionError.m
new file mode 100755
index 000000000..01819dc27
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Errors/YKFKeySessionError.m
@@ -0,0 +1,65 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "YKFKeySessionError.h"
+
+NSString* const YKFKeySessionErrorDomain = @"YubiKeySessionError";
+
+#pragma mark - Error Descriptions
+
+static NSString* const YKFKeySessionErrorStatusErrorDescription = @"Status error returned by the key.";
+
+static NSString* const YKFKeySessionErrorReadTimeoutDescription = @"Unable to read from key. Operation timeout.";
+static NSString* const YKFKeySessionErrorWriteTimeoutDescription = @"Unable to write to the key. Operation timeout.";
+static NSString* const YKFKeySessionErrorTouchTimeoutDescription = @"Operation ended. User didn't touch the key.";
+static NSString* const YKFKeySessionErrorKeyBusyDescription = @"The key is busy performing another operation";
+static NSString* const YKFKeySessionErrorMissingApplicationDescription = @"The requested functionality is missing or disabled in the key configuration.";
+
+#pragma mark - YKFKeySessionError
+
+@implementation YKFKeySessionError
+
+static NSDictionary *errorMap = nil;
+
++ (YKFKeySessionError *)errorWithCode:(NSUInteger)code {
+    static dispatch_once_t onceToken;
+    dispatch_once(&onceToken, ^{
+        [YKFKeySessionError buildErrorMap];
+    });
+    
+    NSString *errorDescription = errorMap[@(code)];
+    if (!errorDescription) {
+        errorDescription = YKFKeySessionErrorStatusErrorDescription;
+    }
+    
+    return [[YKFKeySessionError alloc] initWithCode:code message:errorDescription];
+}
+
++ (void)buildErrorMap {
+    errorMap =
+    @{@(YKFKeySessionErrorReadTimeoutCode):         YKFKeySessionErrorReadTimeoutDescription,
+      @(YKFKeySessionErrorWriteTimeoutCode):        YKFKeySessionErrorWriteTimeoutDescription,
+      @(YKFKeySessionErrorTouchTimeoutCode):        YKFKeySessionErrorTouchTimeoutDescription,
+      @(YKFKeySessionErrorKeyBusyCode):             YKFKeySessionErrorKeyBusyDescription,
+      @(YKFKeySessionErrorMissingApplicationCode):  YKFKeySessionErrorMissingApplicationDescription,            
+      };
+}
+
+#pragma mark - Initializers
+
+- (instancetype)initWithCode:(NSInteger)code message:(NSString *)message {
+    return [super initWithDomain:YKFKeySessionErrorDomain code:code userInfo:@{NSLocalizedDescriptionKey: message}];
+}
+
+@end
diff --git a/YubiKit/YubiKit/Sessions/Shared/Errors/YKFKeyU2FError.h b/YubiKit/YubiKit/Sessions/Shared/Errors/YKFKeyU2FError.h
new file mode 100755
index 000000000..699d12310
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Errors/YKFKeyU2FError.h
@@ -0,0 +1,38 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "YKFKeySessionError.h"
+
+typedef NS_ENUM(NSUInteger, YKFKeyU2FErrorCode) {
+    
+    /*! The host application tried to perform an U2F sign operation with a key handle which was not generated with the current key.
+     @discussion
+     This error happens when the application tries to authenticate without registering first. Ideally the authentication flow of
+     the application should not allow this to happen.
+     */
+    YKFKeyU2FErrorCodeU2FSigningUnavailable = 0x000010
+};
+
+NS_ASSUME_NONNULL_BEGIN
+
+/*!
+ @class
+    YKFKeyU2FError
+ @abstract
+    Error type returned by the YKFKeyU2FService.
+ */
+@interface YKFKeyU2FError: YKFKeySessionError
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/YubiKit/YubiKit/Sessions/Shared/Errors/YKFKeyU2FError.m b/YubiKit/YubiKit/Sessions/Shared/Errors/YKFKeyU2FError.m
new file mode 100755
index 000000000..b65837437
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Errors/YKFKeyU2FError.m
@@ -0,0 +1,42 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "YKFKeyU2FError.h"
+#import "YKFKeySessionError+Private.h"
+
+static NSString* const YKFKeyU2FErrorU2FSigningUnavailableDescription = @"A sign operation was performed without registration first."
+                                                                         "Register the device before authenticating with it.";
+
+@implementation YKFKeyU2FError
+
+static NSDictionary *errorMap = nil;
+
++ (YKFKeySessionError *)errorWithCode:(NSUInteger)code {
+    static dispatch_once_t onceToken;
+    dispatch_once(&onceToken, ^{
+        [YKFKeyU2FError buildErrorMap];
+    });
+    
+    NSString *errorDescription = errorMap[@(code)];
+    if (!errorDescription) {
+        return [super errorWithCode:code];
+    }
+    return [[YKFKeySessionError alloc] initWithCode:code message:errorDescription];
+}
+
++ (void)buildErrorMap {
+    errorMap = @{@(YKFKeyU2FErrorCodeU2FSigningUnavailable): YKFKeyU2FErrorU2FSigningUnavailableDescription };
+}
+
+@end
diff --git a/YubiKit/YubiKit/Sessions/Shared/Requests/FIDO2/YKFFIDO2Type+Private.h b/YubiKit/YubiKit/Sessions/Shared/Requests/FIDO2/YKFFIDO2Type+Private.h
new file mode 100755
index 000000000..bca89a03f
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Requests/FIDO2/YKFFIDO2Type+Private.h
@@ -0,0 +1,37 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+@protocol YKFFIDO2TypeProtocol<NSObject>
+
+- (id)cborTypeObject;
+
+@end
+
+@interface YKFFIDO2PublicKeyCredentialRpEntity()<YKFFIDO2TypeProtocol>
+@end
+
+@interface YKFFIDO2PublicKeyCredentialUserEntity()<YKFFIDO2TypeProtocol>
+@end
+
+@interface YKFFIDO2PublicKeyCredentialType()<YKFFIDO2TypeProtocol>
+@end
+
+@interface YKFFIDO2PublicKeyCredentialParam()<YKFFIDO2TypeProtocol>
+@end
+
+@interface YKFFIDO2AuthenticatorTransport()<YKFFIDO2TypeProtocol>
+@end
+
+@interface YKFFIDO2PublicKeyCredentialDescriptor()<YKFFIDO2TypeProtocol>
+@end
diff --git a/YubiKit/YubiKit/Sessions/Shared/Requests/FIDO2/YKFFIDO2Type.h b/YubiKit/YubiKit/Sessions/Shared/Requests/FIDO2/YKFFIDO2Type.h
new file mode 100755
index 000000000..a038cee26
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Requests/FIDO2/YKFFIDO2Type.h
@@ -0,0 +1,192 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * ---------------------------------------------------------------------------------------------------------------------
+ * @name YKFFIDO2PublicKeyCredentialRpEntity
+ * ---------------------------------------------------------------------------------------------------------------------
+ */
+
+/*!
+ @class YKFFIDO2PublicKeyCredentialRpEntity
+ 
+ @abstract
+    Data structure to represent a Relying Party in FIDO2/CTAP2, used in authenticator requests like Make Credential.
+ 
+ @discussion
+    To optimize the communication with the authenticator, it's recommended to use short values for names and icon URLs.
+ */
+@interface YKFFIDO2PublicKeyCredentialRpEntity: NSObject
+
+/// A required unique ID of the RP, usually a domain name (e.g. yubico.com).
+@property (nonatomic) NSString *rpId;
+
+/// An optional human readable name for the RP (e.g. Yubico).
+@property (nonatomic, nullable) NSString *rpName;
+
+/// An optional URI to the RP icon (e.g. https://www.yubico.com/rpicon.png).
+@property (nonatomic, nullable) NSString *rpIcon;
+
+@end
+
+/**
+ * ---------------------------------------------------------------------------------------------------------------------
+ * @name YKFFIDO2PublicKeyCredentialUserEntity
+ * ---------------------------------------------------------------------------------------------------------------------
+ */
+
+/*!
+ @class YKFFIDO2PublicKeyCredentialUserEntity
+ 
+ @abstract
+    Data structure to represent a User in FIDO2/CTAP2, used in authenticator requests like Make Credential.
+ 
+ @discussion
+    To optimize the communication with the authenticator, it's recommended to use short values for names and icon URLs.
+ */
+@interface YKFFIDO2PublicKeyCredentialUserEntity: NSObject
+
+/// A required unique User ID, used by the RP to indentify the user in the RP systems.
+@property (nonatomic) NSData *userId;
+
+/// An optional username (usually an account name) used by the RP to identify the user (e.g. john.smith@yubico.com).
+@property (nonatomic, nullable) NSString *userName;
+
+/// An optional display name of the user (e.g. John Smith).
+@property (nonatomic, nullable) NSString *userDisplayName;
+
+/// An optional URI to the User Icon (e.g. https://www.yubico.com/johnsmith.png).
+@property (nonatomic, nullable) NSString *userIcon;
+
+@end
+
+/**
+ * ---------------------------------------------------------------------------------------------------------------------
+ * @name YKFFIDO2PublicKeyCredentialType
+ * ---------------------------------------------------------------------------------------------------------------------
+ */
+
+/*!
+ @class YKFFIDO2PublicKeyCredentialType
+ 
+ @abstract
+    Data structure to represent a public credential type in FIDO2/CTAP2, used in authenticator requests like Get Assertion.
+ */
+@interface YKFFIDO2PublicKeyCredentialType: NSObject
+
+/// In FIDO2/CTAP2 the public credential is a public key, so the value of this name should be "public-key".
+@property (nonatomic) NSString *name;
+
+@end
+
+/**
+ * ---------------------------------------------------------------------------------------------------------------------
+ * @name YKFFIDO2PublicKeyCredentialParam
+ * ---------------------------------------------------------------------------------------------------------------------
+ */
+
+/*!
+ @abstract
+    ECDSA (Elliptic Curve Digital Signature Algorithm) using P-256 and SHA-256
+ 
+ @discussion
+    The authenticator will generate an ECC-256 (curve P-256) key pair for the credential and use the private key to
+    sign a SHA256 (32 bytes of data) when requesting signatures from the authenticator.
+ */
+static const NSInteger YKFFIDO2PublicKeyAlgorithmES256 = -7;
+
+/*!
+ @abstract
+    EdDSA (Edwards-curve Digital Signature Algorithm), using the Ed25519 curve.
+ 
+ @discussion
+    The authenticator will generate an Ed25519 key pair for the credential and use the private key to sign a SHA256 (32 bytes
+    of data) when requesting signatures from the authenticator.
+ */
+static const NSInteger YKFFIDO2PublicKeyAlgorithmEdDSA = -8;
+
+/*!
+ @class YKFFIDO2PublicKeyCredentialParam
+ 
+ @abstract
+    Data structure to represent the desired public key parameters when creating a FIDO2 credential.
+ */
+@interface YKFFIDO2PublicKeyCredentialParam: NSObject
+
+/// The type of algorithm to use for the credential (YKFFIDO2PublicKeyAlgorithmES256 or YKFFIDO2PublicKeyAlgorithmEdDSA).
+@property (nonatomic) NSInteger alg;
+
+@end
+
+/**
+ * ---------------------------------------------------------------------------------------------------------------------
+ * @name YKFFIDO2AuthenticatorTransport
+ * ---------------------------------------------------------------------------------------------------------------------
+ */
+
+/// Constant for USB transport name.
+extern NSString* const YKFFIDO2AuthenticatorTransportUSB;
+
+/// Constant for NFC transport name.
+extern NSString* const YKFFIDO2AuthenticatorTransportNFC;
+
+/// Constant for BLE transport name.
+extern NSString* const YKFFIDO2AuthenticatorTransportBLE;
+
+/*!
+ @class YKFFIDO2AuthenticatorTransport
+ 
+ @abstract
+    Data structure to represent how the application communicates with the authenticator (over USB, NFC or BLE).
+ */
+@interface YKFFIDO2AuthenticatorTransport: NSObject
+
+/*!
+ The name of the transport:
+ YKFFIDO2AuthenticatorTransportUSB, YKFFIDO2AuthenticatorTransportNFC or YKFFIDO2AuthenticatorTransportBLE
+ */
+@property (nonatomic) NSString *name;
+
+@end
+
+/**
+ * ---------------------------------------------------------------------------------------------------------------------
+ * @name YKFFIDO2PublicKeyCredentialDescriptor
+ * ---------------------------------------------------------------------------------------------------------------------
+ */
+
+/*
+ @class YKFFIDO2PublicKeyCredentialDescriptor
+ 
+ @abstract
+    Data structure to represent a FIDO2 credential, used in requests like Make Credential or Get Assertion.
+ */
+@interface YKFFIDO2PublicKeyCredentialDescriptor: NSObject
+
+/// The unique ID of the credential, usually generated by the authenticator when a credential is created.
+@property (nonatomic) NSData *credentialId;
+
+/// The type of the credential.
+@property (nonatomic) YKFFIDO2PublicKeyCredentialType *credentialType;
+
+/// An optional list of YKFFIDO2AuthenticatorTransport objects.
+@property (nonatomic, nullable) NSArray *credentialTransports;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/YubiKit/YubiKit/Sessions/Shared/Requests/FIDO2/YKFFIDO2Type.m b/YubiKit/YubiKit/Sessions/Shared/Requests/FIDO2/YKFFIDO2Type.m
new file mode 100755
index 000000000..5999335ca
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Requests/FIDO2/YKFFIDO2Type.m
@@ -0,0 +1,137 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "YKFFIDO2Type.h"
+#import "YKFCBORType.h"
+#import "YKFFIDO2Type+Private.h"
+
+#pragma mark - YKFFIDO2PublicKeyCredentialRpEntity
+
+@implementation YKFFIDO2PublicKeyCredentialRpEntity
+
+- (id)cborTypeObject {
+    NSMutableDictionary *dictionary = [[NSMutableDictionary alloc] init];
+    
+    YKFCBORTextString *idKey = YKFCBORTextString(@"id");
+    dictionary[idKey] = YKFCBORTextString(self.rpId);
+    
+    if (self.rpName) {
+        YKFCBORTextString *nameKey = YKFCBORTextString(@"name");
+        dictionary[nameKey] = YKFCBORTextString(self.rpName);
+    }
+    if (self.rpIcon) {
+        YKFCBORTextString *iconKey = YKFCBORTextString(@"icon");
+        dictionary[iconKey] = YKFCBORTextString(self.rpIcon);
+    }
+    
+    return YKFCBORMap([dictionary copy]);
+}
+
+@end
+
+
+#pragma mark - YKFFIDO2PublicKeyCredentialUserEntity
+
+@implementation YKFFIDO2PublicKeyCredentialUserEntity
+
+- (id)cborTypeObject {
+    NSMutableDictionary *dictionary = [[NSMutableDictionary alloc] init];
+    
+    YKFCBORTextString *idKey = YKFCBORTextString(@"id");
+    dictionary[idKey] = YKFCBORByteString(self.userId);
+    
+    if (self.userName) {
+        YKFCBORTextString *nameKey = YKFCBORTextString(@"name");
+        dictionary[nameKey] = YKFCBORTextString(self.userName);
+    }
+    if (self.userDisplayName) {
+        YKFCBORTextString *userDisplayNameKey = YKFCBORTextString(@"displayName");
+        dictionary[userDisplayNameKey] = YKFCBORTextString(self.userDisplayName);
+    }
+    if (self.userIcon) {
+        YKFCBORTextString *userIconKey = YKFCBORTextString(@"icon");
+        dictionary[userIconKey] = YKFCBORTextString(self.userIcon);
+    }
+    
+    return YKFCBORMap([dictionary copy]);
+}
+
+@end
+
+
+#pragma mark - YKFFIDO2PublicKeyCredentialType
+
+@implementation YKFFIDO2PublicKeyCredentialType
+
+- (id)cborTypeObject {
+    return YKFCBORTextString(self.name);
+}
+
+@end
+
+
+#pragma mark - YKFFIDO2PublicKeyCredentialParam
+
+@implementation YKFFIDO2PublicKeyCredentialParam
+
+- (id)cborTypeObject {
+    NSDictionary *dictionary = @{YKFCBORTextString(@"alg"): YKFCBORInteger(self.alg),
+                                 YKFCBORTextString(@"type"): YKFCBORTextString(@"public-key")};
+    return YKFCBORMap(dictionary);
+}
+
+@end
+
+
+#pragma mark - YKFFIDO2AuthenticatorTransport
+
+NSString* const YKFFIDO2AuthenticatorTransportUSB = @"usb";
+NSString* const YKFFIDO2AuthenticatorTransportNFC = @"nfc";
+NSString* const YKFFIDO2AuthenticatorTransportBLE = @"ble";
+
+@implementation YKFFIDO2AuthenticatorTransport
+
+- (id)cborTypeObject {
+    return YKFCBORTextString(self.name);
+}
+
+@end
+
+
+#pragma mark - YKFFIDO2PublicKeyCredentialDescriptor
+
+@implementation YKFFIDO2PublicKeyCredentialDescriptor
+
+- (id)cborTypeObject {
+    NSMutableDictionary *dictionary = [[NSMutableDictionary alloc] init];
+    
+    YKFCBORTextString *idKey = YKFCBORTextString(@"id");
+    dictionary[idKey] = YKFCBORByteString(self.credentialId);
+    
+    YKFCBORTextString *typeKey = YKFCBORTextString(@"type");
+    dictionary[typeKey] = [self.credentialType cborTypeObject];
+
+    if (self.credentialTransports) {
+        YKFCBORTextString *transportsKey = YKFCBORTextString(@"transports");
+        NSMutableArray *transportsArray = [[NSMutableArray alloc] initWithCapacity:self.credentialTransports.count];
+        for (YKFFIDO2AuthenticatorTransport *transport in self.credentialTransports) {
+            [transportsArray addObject:[transport cborTypeObject]];
+        }
+        dictionary[transportsKey] = YKFCBORArray([transportsArray copy]);
+    }
+    
+    return YKFCBORMap([dictionary copy]);
+}
+
+@end
diff --git a/YubiKit/YubiKit/Sessions/Shared/Requests/FIDO2/YKFKeyFIDO2ChangePinRequest.h b/YubiKit/YubiKit/Sessions/Shared/Requests/FIDO2/YKFKeyFIDO2ChangePinRequest.h
new file mode 100755
index 000000000..98433226b
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Requests/FIDO2/YKFKeyFIDO2ChangePinRequest.h
@@ -0,0 +1,75 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+#import "YKFKeyFIDO2Request.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/*!
+ @class YKFKeyFIDO2ChangePinRequest
+ 
+ @abstract
+    Request for changing the PIN on the FIDO2 key application. This request maps to the
+    authenticatorClientPin command from CTAP2 protocol:
+    https://fidoalliance.org/specs/fido-v2.0-rd-20180702/fido-client-to-authenticator-protocol-v2.0-rd-20180702.pdf
+ 
+ @discussion
+    The key FIDO2 application should have a PIN when executing this request. If the application
+    doesn't have a PIN, this request will return an error and a set PIN request should be executed
+    instead. After changing the PIN, a verify PIN request must be executed to re-authenticate
+    the session.
+ */
+@interface YKFKeyFIDO2ChangePinRequest: YKFKeyFIDO2Request
+
+/*!
+ @abstract
+    The old PIN which was set on the FIDO2 key application.
+ 
+ @discussion
+    The minimum PIN length accepted by the key is 4. If the PIN is shorter then 4 or longer then 255
+    UTF8 encoded characters, the library will return an error.
+ */
+@property (nonatomic, readonly) NSString *pinOld;
+
+/*!
+ @abstract
+    The new PIN to set on the FIDO2 key application.
+ 
+ @discussion
+    The minimum PIN length accepted by the key is 4. If the PIN is shorter then 4 or longer then 255
+    UTF8 encoded characters, the library will return an error.
+ */
+@property (nonatomic, readonly) NSString *pinNew;
+
+/*!
+ @abstract
+    Creates a new instance with the new PIN to set and the old PIN for verification.
+ 
+ @param newPin
+    The new PIN to set on the FIDO2 key application.
+ 
+ @param oldPin
+    The old PIN of the FIDO2 key application.
+ */
+- (nullable instancetype)initWithNewPin:(NSString *)newPin oldPin:(NSString *)oldPin NS_DESIGNATED_INITIALIZER;
+
+/*
+ Not available: use [initWithPin:] instead.
+ */
+- (instancetype)init NS_UNAVAILABLE;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/YubiKit/YubiKit/Sessions/Shared/Requests/FIDO2/YKFKeyFIDO2ChangePinRequest.m b/YubiKit/YubiKit/Sessions/Shared/Requests/FIDO2/YKFKeyFIDO2ChangePinRequest.m
new file mode 100755
index 000000000..bfde9af29
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Requests/FIDO2/YKFKeyFIDO2ChangePinRequest.m
@@ -0,0 +1,39 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "YKFKeyFIDO2ChangePinRequest.h"
+#import "YKFAssert.h"
+
+@interface YKFKeyFIDO2ChangePinRequest()
+
+@property (nonatomic, readwrite) NSString *pinOld;
+@property (nonatomic, readwrite) NSString *pinNew;
+
+@end
+
+@implementation YKFKeyFIDO2ChangePinRequest
+
+- (nullable instancetype)initWithNewPin:(NSString *)newPin oldPin:(NSString *)oldPin {
+    self = [super init];
+    if (self) {
+        YKFAssertAbortInit(newPin);
+        YKFAssertAbortInit(oldPin);
+        
+        self.pinOld = oldPin;
+        self.pinNew = newPin;
+    }
+    return self;
+}
+
+@end
diff --git a/YubiKit/YubiKit/Sessions/Shared/Requests/FIDO2/YKFKeyFIDO2ClientPinRequest.h b/YubiKit/YubiKit/Sessions/Shared/Requests/FIDO2/YKFKeyFIDO2ClientPinRequest.h
new file mode 100755
index 000000000..0dc10fcf1
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Requests/FIDO2/YKFKeyFIDO2ClientPinRequest.h
@@ -0,0 +1,44 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+#import "YKFKeyFIDO2Request.h"
+#import "YKFCBORType.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+typedef NS_ENUM(NSUInteger, YKFKeyFIDO2ClientPinRequestSubCommand) {
+    YKFKeyFIDO2ClientPinRequestSubCommandGetRetries         = 0x01,
+    YKFKeyFIDO2ClientPinRequestSubCommandGetKeyAgreement    = 0x02,
+    YKFKeyFIDO2ClientPinRequestSubCommandSetPIN             = 0x03,
+    YKFKeyFIDO2ClientPinRequestSubCommandChangePIN          = 0x04,
+    YKFKeyFIDO2ClientPinRequestSubCommandGetPINToken        = 0x05
+};
+
+@interface YKFKeyFIDO2ClientPinRequest: YKFKeyFIDO2Request
+
+@property (nonatomic) NSUInteger pinProtocol;
+@property (nonatomic) YKFKeyFIDO2ClientPinRequestSubCommand subCommand;
+
+/*!
+ A COSE encoded EC public key used to generate a shared secret with the authenticator to send the PIN encripted.
+ */
+@property (nonatomic, nullable) YKFCBORMap *keyAgreement;
+@property (nonatomic, nullable) NSData *pinAuth;
+@property (nonatomic, nullable) NSData *pinEnc;
+@property (nonatomic, nullable) NSData *pinHashEnc;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/YubiKit/YubiKit/Sessions/Shared/Requests/FIDO2/YKFKeyFIDO2ClientPinRequest.m b/YubiKit/YubiKit/Sessions/Shared/Requests/FIDO2/YKFKeyFIDO2ClientPinRequest.m
new file mode 100755
index 000000000..de5e4463d
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Requests/FIDO2/YKFKeyFIDO2ClientPinRequest.m
@@ -0,0 +1,18 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "YKFKeyFIDO2ClientPinRequest.h"
+
+@implementation YKFKeyFIDO2ClientPinRequest
+@end
diff --git a/YubiKit/YubiKit/Sessions/Shared/Requests/FIDO2/YKFKeyFIDO2ClientPinResponse.h b/YubiKit/YubiKit/Sessions/Shared/Requests/FIDO2/YKFKeyFIDO2ClientPinResponse.h
new file mode 100755
index 000000000..99209528b
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Requests/FIDO2/YKFKeyFIDO2ClientPinResponse.h
@@ -0,0 +1,34 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface YKFKeyFIDO2ClientPinResponse: NSObject
+
+/*!
+ A cose encoded EC public key used to generate a shared secret to communicate PIN data with the library.
+ */
+@property (nonatomic, nullable, readonly) NSDictionary *keyAgreement;
+
+@property (nonatomic, nullable, readonly) NSData *pinToken;
+@property (nonatomic, readonly) NSUInteger retries;
+
+- (nullable instancetype)initWithCBORData:(NSData *)cborData NS_DESIGNATED_INITIALIZER;
+- (instancetype)init NS_UNAVAILABLE;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/YubiKit/YubiKit/Sessions/Shared/Requests/FIDO2/YKFKeyFIDO2ClientPinResponse.m b/YubiKit/YubiKit/Sessions/Shared/Requests/FIDO2/YKFKeyFIDO2ClientPinResponse.m
new file mode 100755
index 000000000..b2c10334e
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Requests/FIDO2/YKFKeyFIDO2ClientPinResponse.m
@@ -0,0 +1,71 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "YKFKeyFIDO2ClientPinResponse.h"
+#import "YKFCBORDEcoder.h"
+#import "YKFAssert.h"
+
+typedef NS_ENUM(NSUInteger, YKFKeyFIDO2ClientPinResponseKey) {
+    YKFKeyFIDO2ClientPinResponseKeyKeyAgreement = 0x01,
+    YKFKeyFIDO2ClientPinResponsePinToken        = 0x02,
+    YKFKeyFIDO2ClientPinResponseKeyRetries      = 0x03
+};
+
+@interface YKFKeyFIDO2ClientPinResponse()
+
+@property (nonatomic, readwrite) NSDictionary *keyAgreement;
+@property (nonatomic, readwrite) NSData *pinToken;
+@property (nonatomic, readwrite) NSUInteger retries;
+
+@end
+
+@implementation YKFKeyFIDO2ClientPinResponse
+
+- (nullable instancetype)initWithCBORData:(NSData *)cborData {
+    self = [super init];
+    if (self) {
+        YKFCBORMap *responseMap = nil;
+        
+        NSInputStream *decoderInputStream = [[NSInputStream alloc] initWithData:cborData];
+        [decoderInputStream open];
+        responseMap = [YKFCBORDecoder decodeObjectFrom:decoderInputStream];
+        [decoderInputStream close];
+        
+        YKFAssertAbortInit(responseMap);
+        
+        BOOL success = [self parseResponseMap: responseMap];
+        YKFAssertAbortInit(success);
+    }
+    return self;
+}
+
+- (BOOL)parseResponseMap:(YKFCBORMap *)map {
+    id convertedObject = [YKFCBORDecoder convertCBORObjectToFoundationType:map];
+    if (!convertedObject || ![convertedObject isKindOfClass:NSDictionary.class]) {
+        return NO;
+    }
+    NSDictionary *response = (NSDictionary *)convertedObject;
+    
+    self.keyAgreement = response[@(YKFKeyFIDO2ClientPinResponseKeyKeyAgreement)];
+    self.pinToken = response[@(YKFKeyFIDO2ClientPinResponsePinToken)];
+    
+    NSNumber *retries = response[@(YKFKeyFIDO2ClientPinResponseKeyRetries)];
+    if (retries != nil) {
+        self.retries = retries.integerValue;
+    }
+    
+    return YES;
+}
+
+@end
diff --git a/YubiKit/YubiKit/Sessions/Shared/Requests/FIDO2/YKFKeyFIDO2GetAssertionRequest+Private.h b/YubiKit/YubiKit/Sessions/Shared/Requests/FIDO2/YKFKeyFIDO2GetAssertionRequest+Private.h
new file mode 100755
index 000000000..025b6d47b
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Requests/FIDO2/YKFKeyFIDO2GetAssertionRequest+Private.h
@@ -0,0 +1,40 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "YKFKeyFIDO2GetAssertionRequest.h"
+
+
+@interface YKFKeyFIDO2GetAssertionRequest()
+
+/*!
+ @abstract
+    First 16 bytes of HMAC-SHA-256 of clientDataHash using pinToken which platform got from the
+    authenticator: HMAC-SHA256(pinToken, clientDataHash).
+ 
+ @discussion
+    This parameter is optional.
+ */
+@property (nonatomic, nullable) NSData *pinAuth;
+
+/*!
+ @abstract
+    PIN protocol version selected by client.
+ 
+ @discussion
+    This parameter is optional. Currently the key supports only pin protocol version 1. If not
+    specified the key will use version 1 as default.
+ */
+@property (nonatomic) NSUInteger pinProtocol;
+
+@end
diff --git a/YubiKit/YubiKit/Sessions/Shared/Requests/FIDO2/YKFKeyFIDO2GetAssertionRequest.h b/YubiKit/YubiKit/Sessions/Shared/Requests/FIDO2/YKFKeyFIDO2GetAssertionRequest.h
new file mode 100755
index 000000000..9832ab78a
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Requests/FIDO2/YKFKeyFIDO2GetAssertionRequest.h
@@ -0,0 +1,115 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+#import "YKFKeyFIDO2Request.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * ---------------------------------------------------------------------------------------------------------------------
+ * @name Options Keys
+ * ---------------------------------------------------------------------------------------------------------------------
+ */
+
+/*!
+ @abstract
+    User Presence option key to set in the request options dictionary.
+ 
+ @discussion
+    Instructs the authenticator to require user consent to complete the operation. Set this key in the options
+    dictionary of the request when necessary.
+ */
+extern NSString* const YKFKeyFIDO2GetAssertionRequestOptionUP;
+
+/*!
+ @abstract
+    User Verification option key to set in the request options dictionary.
+ 
+ @discussion
+    Instructs the authenticator to require a gesture that verifies the user to complete the request. Examples of such
+    gestures are fingerprint scan or a PIN. Set this key in the options dictionary of the request when necessary.
+ */
+extern NSString* const YKFKeyFIDO2GetAssertionRequestOptionUV;
+
+/**
+ * ---------------------------------------------------------------------------------------------------------------------
+ * @name YKFKeyFIDO2GetAssertionRequest
+ * ---------------------------------------------------------------------------------------------------------------------
+ */
+
+/*!
+ @class YKFKeyFIDO2GetAssertionRequest
+ 
+ @abstract
+    Request for retrieving a signature from the authenticator. This request maps to the
+    authenticatorGetAssertion command from CTAP2 protocol:
+    https://fidoalliance.org/specs/fido-v2.0-rd-20180702/fido-client-to-authenticator-protocol-v2.0-rd-20180702.pdf
+ */
+@interface YKFKeyFIDO2GetAssertionRequest: YKFKeyFIDO2Request
+
+/*!
+ @abstract
+    Relying party identifier as defined in WebAuthN.
+ 
+ @discussion
+    This property is required by the key to fulfil the request. If missing, the FIDO2 Service will return an error
+    when trying to execute the request.
+ */
+@property (nonatomic) NSString *rpId;
+
+/*!
+ @abstract
+    Hash of the serialized client data collected by the host as defined in WebAuthN.
+ 
+ @discussion
+    This property is required by the key to fulfil the request. The value should be a SHA256 of the received
+    Client Data from the WebAuthN server. If missing, the FIDO2 Service will return an error when trying to
+    execute the request.
+ */
+@property (nonatomic) NSData *clientDataHash;
+
+/*!
+ @abstract
+    A sequence of YKFFIDO2PublicKeyCredentialDescriptor objects, each denoting a credential. The authenticator
+    will generate a signature for each credential.
+ 
+ @discussion
+    This property is optional but it's recommended to always specify a credential in the list and not make
+    assumtions on the number of credentials generated by the key.
+ */
+@property (nonatomic, nullable) NSArray *allowList;
+
+/*!
+ @discussion
+    The options provide a list of properties to influence authenticator operation when signing, as specified
+    in in the table below. This parameter is optional.
+ 
+    @code
+    Key           | Default value      | Definition
+    ----------------------------------------------------------------------------------------
+    uv            | false              | user verification: Instructs the authenticator to
+                                         require a gesture that verifies the user to complete
+                                         the request. Examples of such gestures are fingerprint
+                                         scan or a PIN.
+    ----------------------------------------------------------------------------------------
+    up            | true               | user presence: Instructs the authenticator to require
+                                         user consent to complete the operation.
+    @endcode
+ */
+@property (nonatomic, nullable) NSDictionary *options;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/YubiKit/YubiKit/Sessions/Shared/Requests/FIDO2/YKFKeyFIDO2GetAssertionRequest.m b/YubiKit/YubiKit/Sessions/Shared/Requests/FIDO2/YKFKeyFIDO2GetAssertionRequest.m
new file mode 100755
index 000000000..7499b8151
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Requests/FIDO2/YKFKeyFIDO2GetAssertionRequest.m
@@ -0,0 +1,22 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "YKFKeyFIDO2GetAssertionRequest.h"
+#import "YKFKeyFIDO2GetAssertionRequest+Private.h"
+
+NSString* const YKFKeyFIDO2GetAssertionRequestOptionUP = @"up";
+NSString* const YKFKeyFIDO2GetAssertionRequestOptionUV = @"uv";
+
+@implementation YKFKeyFIDO2GetAssertionRequest
+@end
diff --git a/YubiKit/YubiKit/Sessions/Shared/Requests/FIDO2/YKFKeyFIDO2GetAssertionResponse+Private.h b/YubiKit/YubiKit/Sessions/Shared/Requests/FIDO2/YKFKeyFIDO2GetAssertionResponse+Private.h
new file mode 100755
index 000000000..19ee3c61f
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Requests/FIDO2/YKFKeyFIDO2GetAssertionResponse+Private.h
@@ -0,0 +1,26 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+#import "YKFKeyFIDO2GetAssertionResponse.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface YKFKeyFIDO2GetAssertionResponse()
+
+- (nullable instancetype)initWithCBORData:(NSData *)cborData NS_DESIGNATED_INITIALIZER;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/YubiKit/YubiKit/Sessions/Shared/Requests/FIDO2/YKFKeyFIDO2GetAssertionResponse.h b/YubiKit/YubiKit/Sessions/Shared/Requests/FIDO2/YKFKeyFIDO2GetAssertionResponse.h
new file mode 100755
index 000000000..1e36c74ab
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Requests/FIDO2/YKFKeyFIDO2GetAssertionResponse.h
@@ -0,0 +1,94 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+#import "YKFFIDO2Type.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/*!
+ @abstract
+    The response to a FIDO2 Get Assertion request. The result contains the data structures
+    defined in the CTAP2 authenticatorGetAssertion command response.
+ */
+@interface YKFKeyFIDO2GetAssertionResponse: NSObject
+
+/*!
+ @abstract
+    Contains the credential identifier whose private key was used to generate the assertion.
+ 
+ @discussion
+    This property is optional. It may be omitted by the key if the allowList from the request has
+    exactly one credential.
+ */
+@property (nonatomic, readonly, nullable) YKFFIDO2PublicKeyCredentialDescriptor *credential;
+
+/*!
+ @abstract
+    The signed-over contextual bindings made by the authenticator, as specified in WebAuthN.
+ 
+ @discussion
+    This property is not optional. The key must reply with the authData when the Get Assertion
+    request was successful.
+ */
+@property (nonatomic, readonly) NSData *authData;
+
+/*!
+ @abstract
+    The assertion signature produced by the authenticator, as specified in WebAuthN.
+ 
+ @discussion
+    This property is not optional. The key must reply with a signature when the Get Assertion
+    request was successful.
+ */
+@property (nonatomic, readonly) NSData *signature;
+
+/*!
+ @abstract
+    Contains the user account information.
+ 
+ @discussion
+    This property is optional. The key may not provide the user data structure
+    in the response. See CTAP2 specifications for more details.
+ */
+@property (nonatomic, readonly, nullable) YKFFIDO2PublicKeyCredentialUserEntity *user;
+
+/*!
+ @abstract
+    The total number of account credentials for the RP.
+ 
+ @discussion
+    This property is optional. The key may not provide the number of credentials
+    in some cases. See CTAP2 specifications for more details.
+ */
+@property (nonatomic, readonly) NSInteger numberOfCredentials;
+
+/*!
+ @abstract
+    The raw, unparsed CBOR response of the request.
+ 
+ @discussion
+    This value is useful when the server implementation will handle the parsing of the response and requires
+    the unparsed payload received from the key.
+ */
+@property (nonatomic, readonly) NSData *rawResponse;
+
+/*
+ Not available: the response will be created by the library.
+ */
+- (instancetype)init NS_UNAVAILABLE;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/YubiKit/YubiKit/Sessions/Shared/Requests/FIDO2/YKFKeyFIDO2GetAssertionResponse.m b/YubiKit/YubiKit/Sessions/Shared/Requests/FIDO2/YKFKeyFIDO2GetAssertionResponse.m
new file mode 100755
index 000000000..b69ba9290
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Requests/FIDO2/YKFKeyFIDO2GetAssertionResponse.m
@@ -0,0 +1,125 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "YKFKeyFIDO2GetAssertionResponse.h"
+#import "YKFKeyFIDO2GetAssertionResponse+Private.h"
+#import "YKFCBORDEcoder.h"
+#import "YKFFIDO2Type.h"
+#import "YKFAssert.h"
+
+typedef NS_ENUM(NSUInteger, YKFKeyFIDO2GetAssertionResponseKey) {
+    YKFKeyFIDO2GetAssertionResponseKeyCredential            = 0x01,
+    YKFKeyFIDO2GetAssertionResponseKeyAuthData              = 0x02,
+    YKFKeyFIDO2GetAssertionResponseKeySignature             = 0x03,
+    YKFKeyFIDO2GetAssertionResponseKeyUser                  = 0x04,
+    YKFKeyFIDO2GetAssertionResponseKeyNumberOfCredentials   = 0x05
+};
+
+@interface YKFKeyFIDO2GetAssertionResponse()
+
+@property (nonatomic, readwrite) YKFFIDO2PublicKeyCredentialDescriptor *credential;
+@property (nonatomic, readwrite) NSData *authData;
+@property (nonatomic, readwrite) NSData *signature;
+@property (nonatomic, readwrite) YKFFIDO2PublicKeyCredentialUserEntity *user;
+@property (nonatomic, readwrite) NSInteger numberOfCredentials;
+
+@property (nonatomic, readwrite) NSData *rawResponse;
+
+@end
+
+@implementation YKFKeyFIDO2GetAssertionResponse
+
+- (instancetype)initWithCBORData:(NSData *)cborData {
+    self = [super init];
+    if (self) {
+        YKFAssertAbortInit(cborData);
+        self.rawResponse = cborData;
+        
+        YKFCBORMap *responseMap = nil;
+        
+        NSInputStream *decoderInputStream = [[NSInputStream alloc] initWithData:cborData];
+        [decoderInputStream open];
+        responseMap = [YKFCBORDecoder decodeObjectFrom:decoderInputStream];
+        [decoderInputStream close];
+        
+        YKFAssertAbortInit(responseMap);
+        
+        BOOL success = [self parseResponseMap: responseMap];
+        YKFAssertAbortInit(success);
+    }
+    return self;
+}
+
+#pragma mark - Private
+
+- (BOOL)parseResponseMap:(YKFCBORMap *)map {
+    id convertedObject = [YKFCBORDecoder convertCBORObjectToFoundationType:map];
+    if (!convertedObject || ![convertedObject isKindOfClass:NSDictionary.class]) {
+        return NO;
+    }
+    NSDictionary *response = (NSDictionary *)convertedObject;
+    
+    // Credential    
+    NSDictionary *responseCredential = response[@(YKFKeyFIDO2GetAssertionResponseKeyCredential)];
+    if (responseCredential) {
+        YKFFIDO2PublicKeyCredentialDescriptor *credentialDescriptor = [[YKFFIDO2PublicKeyCredentialDescriptor alloc] init];
+        credentialDescriptor.credentialId = responseCredential[@"id"];
+        
+        YKFFIDO2PublicKeyCredentialType *credentialType = [[YKFFIDO2PublicKeyCredentialType alloc] init];
+        credentialType.name = responseCredential[@"type"];
+        credentialDescriptor.credentialType = credentialType;
+        
+        NSArray *responseTransports = responseCredential[@"transports"];
+        NSMutableArray *transports = [[NSMutableArray alloc] initWithCapacity:responseTransports.count];
+        for (NSString *responseTransport in responseTransports) {
+            YKFFIDO2AuthenticatorTransport *transport = [[YKFFIDO2AuthenticatorTransport alloc] init];
+            transport.name = responseTransport;
+            [transports addObject: transport];
+        }
+        credentialDescriptor.credentialTransports = transports;
+        
+        self.credential = credentialDescriptor;
+    }
+
+    // Auth Data
+    NSData *authData = response[@(YKFKeyFIDO2GetAssertionResponseKeyAuthData)];    
+    YKFAssertReturnValue(authData, @"authenticatorGetAssertion authData is required.", NO);
+    self.authData = authData;
+    
+    // Signature
+    NSData *signature = response[@(YKFKeyFIDO2GetAssertionResponseKeySignature)];
+    YKFAssertReturnValue(signature, @"authenticatorGetAssertion signature is required.", NO);
+    self.signature = signature;
+    
+    // User
+    NSDictionary *responseUser = response[@(YKFKeyFIDO2GetAssertionResponseKeyUser)];
+    if (responseUser) {
+        YKFFIDO2PublicKeyCredentialUserEntity *user = [[YKFFIDO2PublicKeyCredentialUserEntity alloc] init];
+        user.userId = responseUser[@"id"];
+        user.userName = responseUser[@"name"];
+        user.userDisplayName = responseUser[@"displayName"];
+        user.userIcon = responseUser[@"icon"];
+        self.user = user;
+    }
+    
+    // Number Of Credentials
+    NSNumber *numberOfCredentials = response[@(YKFKeyFIDO2GetAssertionResponseKeyNumberOfCredentials)];
+    if (numberOfCredentials != nil) {
+        self.numberOfCredentials = numberOfCredentials.integerValue;
+    }
+    
+    return YES;
+}
+
+@end
diff --git a/YubiKit/YubiKit/Sessions/Shared/Requests/FIDO2/YKFKeyFIDO2GetInfoResponse+Private.h b/YubiKit/YubiKit/Sessions/Shared/Requests/FIDO2/YKFKeyFIDO2GetInfoResponse+Private.h
new file mode 100755
index 000000000..7e2c0659f
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Requests/FIDO2/YKFKeyFIDO2GetInfoResponse+Private.h
@@ -0,0 +1,25 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface YKFKeyFIDO2GetInfoResponse()
+
+- (nullable instancetype)initWithCBORData:(NSData *)cborData NS_DESIGNATED_INITIALIZER;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/YubiKit/YubiKit/Sessions/Shared/Requests/FIDO2/YKFKeyFIDO2GetInfoResponse.h b/YubiKit/YubiKit/Sessions/Shared/Requests/FIDO2/YKFKeyFIDO2GetInfoResponse.h
new file mode 100755
index 000000000..f25c4b203
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Requests/FIDO2/YKFKeyFIDO2GetInfoResponse.h
@@ -0,0 +1,154 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * ---------------------------------------------------------------------------------------------------------------------
+ * @name Options Keys
+ * ---------------------------------------------------------------------------------------------------------------------
+ */
+
+/*!
+ @abstract
+    Key to fetch clientPin value from YKFKeyFIDO2GetInfoResponse.options
+ 
+ @discussion
+    If present and set to true, it indicates that the device is capable of accepting a PIN from the client and
+    PIN has been set.
+    If present and set to false, it indicates that the device is capable of accepting a PIN from the client and
+    PIN has not been set yet.
+    If absent, it indicates that the device is not capable of accepting a PIN from the client.
+ */
+extern NSString* const YKFKeyFIDO2GetInfoResponseOptionClientPin;
+
+/*!
+ @abstract
+    Key to fetch plat value from YKFKeyFIDO2GetInfoResponse.options
+ 
+ @discussion
+    Indicates that the authenticator is attached to the client and therefore can’t be removed and used on another
+    client. The value returned by the key will always be false because the key can be removed.
+ */
+extern NSString* const YKFKeyFIDO2GetInfoResponseOptionPlatformDevice;
+
+/*!
+ @abstract
+    Key to fetch rk value from YKFKeyFIDO2GetInfoResponse.options
+ 
+ @discussion
+    Indicates that the authenticator is capable of storing keys on the device itself and therefore can satisfy the
+    Get Assertion request with allowList parameter not specified or empty.
+ */
+extern NSString* const YKFKeyFIDO2GetInfoResponseOptionResidentKey;
+
+/*!
+ @abstract
+    Key to fetch up value from YKFKeyFIDO2GetInfoResponse.options
+ @discussion
+    Indicates that the device is capable of testing user presence.
+ */
+extern NSString* const YKFKeyFIDO2GetInfoResponseOptionUserPresence;
+
+/*!
+ @abstract
+    Key to fetch uv value from YKFKeyFIDO2GetInfoResponse.options
+ @discussion
+    Indicates that the device is capable of verifying the user within itself.
+ */
+extern NSString* const YKFKeyFIDO2GetInfoResponseOptionUserVerification;
+
+/**
+ * ---------------------------------------------------------------------------------------------------------------------
+ * @name YKFKeyFIDO2GetInfoResponse
+ * ---------------------------------------------------------------------------------------------------------------------
+ */
+
+/*!
+ @abstract
+    The response to a FIDO2 Get Info request. The result contains the data structures defined in the CTAP2
+    authenticatorGetInfo command response.
+ */
+@interface YKFKeyFIDO2GetInfoResponse: NSObject
+
+/*!
+ @abstract
+    The list of supported FIDO protocol versions by the authenticator.
+ 
+ @discussion
+    This property contains a list of strings as defined by the CTAP2 specifications. Because the key supports
+    both FIDO U2F and FIDO2 protocols it will return ["FIDO_2_X", "U2F_V2"] as the result.
+ */
+@property (nonatomic, readonly) NSArray *versions;
+
+/*!
+ @abstract
+    The list of supported extensions.
+ 
+ @discussion
+    This property contains a list of strings as defined by the CTAP2 specifications. Currently the key supports only
+    the "hmac-secret" extension.
+ */
+@property (nonatomic, readonly, nullable) NSArray *extensions;
+
+/*!
+ @abstract
+    The claimed AAGUID by the authenticator.
+ 
+ @discussion
+    The value should always have 16 bytes in length and encoded the same as MakeCredential AuthenticatorData, as
+    specified in WebAuthN.
+ */
+@property (nonatomic, readonly) NSData *aaguid;
+
+/*!
+ @abstract
+    The list of supported options by the authenticator.
+ 
+ @discussion
+    This dictionary contains the list of keys in the Options Keys section. Based on this information the client can
+    deduce what options will be available when requesting new credentials or assertions.
+ */
+@property (nonatomic, readonly, nullable) NSDictionary *options;
+
+/*!
+ @abstract
+    Maximum message size supported by the authenticator, in bytes.
+ 
+ @discussion
+    The value is 0 when not returned by the authenticator. In CTAP2 this value is optional but the YubiKey will always
+    return a value for this property.
+ */
+@property (nonatomic, readonly) NSUInteger maxMsgSize;
+
+/*!
+ @abstract
+    The list of PIN Protocol versions supported by the authenticator.
+ 
+ @discussion
+    CTAP2 defines only one protocol version, version 1. Because there is only one version for the protocol the
+    value of the protocol can be omitted when requesting a new credential or an assertion from the key.
+ */
+@property (nonatomic, readonly, nullable) NSArray *pinProtocols;
+
+/*
+ Not available: the response will be created by the library.
+ */
+- (instancetype)init NS_UNAVAILABLE;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/YubiKit/YubiKit/Sessions/Shared/Requests/FIDO2/YKFKeyFIDO2GetInfoResponse.m b/YubiKit/YubiKit/Sessions/Shared/Requests/FIDO2/YKFKeyFIDO2GetInfoResponse.m
new file mode 100755
index 000000000..057ec0899
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Requests/FIDO2/YKFKeyFIDO2GetInfoResponse.m
@@ -0,0 +1,104 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "YKFKeyFIDO2GetInfoResponse.h"
+#import "YKFKeyFIDO2GetInfoResponse+Private.h"
+#import "YKFCBORDecoder.h"
+#import "YKFAssert.h"
+
+NSString* const YKFKeyFIDO2GetInfoResponseOptionClientPin = @"clientPin";
+NSString* const YKFKeyFIDO2GetInfoResponseOptionPlatformDevice = @"plat";
+NSString* const YKFKeyFIDO2GetInfoResponseOptionResidentKey = @"rk";
+NSString* const YKFKeyFIDO2GetInfoResponseOptionUserPresence = @"up";
+NSString* const YKFKeyFIDO2GetInfoResponseOptionUserVerification = @"uv";
+
+typedef NS_ENUM(NSUInteger, YKFKeyFIDO2GetInfoResponseKey) {
+    YKFKeyFIDO2GetInfoResponseKeyVersions       = 0x01,
+    YKFKeyFIDO2GetInfoResponseKeyExtensions     = 0x02,
+    YKFKeyFIDO2GetInfoResponseKeyAAGUID         = 0x03,
+    YKFKeyFIDO2GetInfoResponseKeyOptions        = 0x04,
+    YKFKeyFIDO2GetInfoResponseKeyMaxMsgSize     = 0x05,
+    YKFKeyFIDO2GetInfoResponseKeyPinProtocols   = 0x06
+};
+
+@interface YKFKeyFIDO2GetInfoResponse()
+
+@property (nonatomic, readwrite) NSArray *versions;
+@property (nonatomic, readwrite) NSArray *extensions;
+@property (nonatomic, readwrite) NSData *aaguid;
+@property (nonatomic, readwrite) NSDictionary *options;
+@property (nonatomic, assign, readwrite) NSUInteger maxMsgSize;
+@property (nonatomic, readwrite) NSArray *pinProtocols;
+
+@end
+
+@implementation YKFKeyFIDO2GetInfoResponse
+
+- (instancetype)initWithCBORData:(NSData *)cborData {
+    self = [super init];
+    if (self) {
+        YKFCBORMap *getInfoMap = nil;
+        
+        NSInputStream *decoderInputStream = [[NSInputStream alloc] initWithData:cborData];
+        [decoderInputStream open];
+        getInfoMap = [YKFCBORDecoder decodeObjectFrom:decoderInputStream];
+        [decoderInputStream close];
+        
+        YKFAssertAbortInit(getInfoMap);
+        
+        BOOL success = [self parseResponseMap:getInfoMap];
+        YKFAssertAbortInit(success);
+    }
+    return self;
+}
+
+#pragma mark - Private
+
+- (BOOL)parseResponseMap:(YKFCBORMap *)map {
+    id convertedObject = [YKFCBORDecoder convertCBORObjectToFoundationType:map];
+    if (!convertedObject || ![convertedObject isKindOfClass:NSDictionary.class]) {
+        return NO;
+    }
+    NSDictionary *response = (NSDictionary *)convertedObject;
+    
+    // versions
+    NSArray *versions = response[@(YKFKeyFIDO2GetInfoResponseKeyVersions)];
+    YKFAssertReturnValue(versions, @"authenticatorGetInfo versions is required.", NO);
+    self.versions = versions;
+    
+    // extensions
+    self.extensions = response[@(YKFKeyFIDO2GetInfoResponseKeyExtensions)];
+    
+    // aaguid
+    NSData *aaguid = response[@(YKFKeyFIDO2GetInfoResponseKeyAAGUID)];
+    YKFAssertReturnValue(aaguid, @"authenticatorGetInfo aaguid is required.", NO);
+    YKFAssertReturnValue(aaguid.length == 16, @"authenticatorGetInfo aaguid has the wrong value.", NO);
+    self.aaguid = aaguid;
+    
+    // options
+    self.options = response[@(YKFKeyFIDO2GetInfoResponseKeyOptions)];
+    
+    // maxMsgSize
+    NSNumber *maxMsgSize = response[@(YKFKeyFIDO2GetInfoResponseKeyMaxMsgSize)];
+    if (maxMsgSize != nil) {
+        self.maxMsgSize = maxMsgSize.integerValue;
+    }
+    
+    // pin protocols
+    self.pinProtocols = response[@(YKFKeyFIDO2GetInfoResponseKeyPinProtocols)];
+        
+    return YES;
+}
+
+@end
diff --git a/YubiKit/YubiKit/Sessions/Shared/Requests/FIDO2/YKFKeyFIDO2MakeCredentialRequest+Private.h b/YubiKit/YubiKit/Sessions/Shared/Requests/FIDO2/YKFKeyFIDO2MakeCredentialRequest+Private.h
new file mode 100755
index 000000000..3f85d22a3
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Requests/FIDO2/YKFKeyFIDO2MakeCredentialRequest+Private.h
@@ -0,0 +1,38 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "YKFKeyFIDO2MakeCredentialRequest.h"
+
+@interface YKFKeyFIDO2MakeCredentialRequest()
+
+/*!
+ @abstract
+    First 16 bytes of HMAC-SHA256 of clientDataHash using pinToken which platform got from the
+    authenticator: HMACSHA-256(pinToken, clientDataHash).
+ 
+ @discussion
+    This parameter is optional.
+ */
+@property (nonatomic, nullable) NSData *pinAuth;
+
+/*!
+ @abstract
+    PIN protocol version chosen by the client.
+ 
+ @discussion
+    This parameter is optional.
+ */
+@property (nonatomic) NSUInteger pinProtocol;
+
+@end
diff --git a/YubiKit/YubiKit/Sessions/Shared/Requests/FIDO2/YKFKeyFIDO2MakeCredentialRequest.h b/YubiKit/YubiKit/Sessions/Shared/Requests/FIDO2/YKFKeyFIDO2MakeCredentialRequest.h
new file mode 100755
index 000000000..07e4c2d5a
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Requests/FIDO2/YKFKeyFIDO2MakeCredentialRequest.h
@@ -0,0 +1,142 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+#import "YKFKeyFIDO2Request.h"
+#import "YKFFIDO2Type.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * ---------------------------------------------------------------------------------------------------------------------
+ * @name Options Keys
+ * ---------------------------------------------------------------------------------------------------------------------
+ */
+
+/*!
+ @abstract
+    Resident Key option key to set in the request options dictionary.
+ 
+ @discusion
+    Instructs the authenticator to store the key material on the device. Set this key in the options dictionary of
+    the request when necessary.
+ */
+extern NSString* const YKFKeyFIDO2MakeCredentialRequestOptionRK;
+
+/*!
+ @abstract
+    User Verification option key to set in the request options dictionary.
+ 
+ @discussion
+    Instructs the authenticator to require a gesture that verifies the user to complete the request. Examples of such
+    gestures are fingerprint scan or a PIN. Set this key in the options dictionary of the request when necessary.
+ */
+extern NSString* const YKFKeyFIDO2MakeCredentialRequestOptionUV;
+
+/**
+ * ---------------------------------------------------------------------------------------------------------------------
+ * @name YKFKeyFIDO2MakeCredentialRequest
+ * ---------------------------------------------------------------------------------------------------------------------
+ */
+
+/*!
+ @class YKFKeyFIDO2MakeCredentialRequest
+ 
+ @abstract
+    Request for creating/updating a FIDO2 credential on the key. This request maps to the
+    authenticatorMakeCredential command from CTAP2 protocol:
+    https://fidoalliance.org/specs/fido-v2.0-rd-20180702/fido-client-to-authenticator-protocol-v2.0-rd-20180702.pdf
+ */
+@interface YKFKeyFIDO2MakeCredentialRequest: YKFKeyFIDO2Request
+
+/*!
+ @abstract
+    Hash of the ClientData contextual binding specified by host.
+ 
+ @discussion
+    This property is required by the key to fulfil the request. The value should be a SHA256 of the received
+    Client Data from the WebAuthN server. If missing, the FIDO2 Service will return an error when trying to
+    execute the request.
+ */
+@property (nonatomic) NSData *clientDataHash;
+
+/*!
+ @abstract
+    This property describes a Relying Party with which the new public key credential will be associated.
+ 
+ @discussion
+    This property is required by the key to fulfil the request. If missing, the FIDO2 Service will return
+    an error when trying to execute the request.
+ */
+@property (nonatomic) YKFFIDO2PublicKeyCredentialRpEntity *rp;
+
+/*!
+ @abstract
+    This property describes the user account to which the new public key credential will be associated at the RP.
+ 
+ @discussion
+    This property is required by the key to fulfil the request. If missing, the FIDO2 Service will return
+    an error when trying to execute the request.
+ */
+@property (nonatomic) YKFFIDO2PublicKeyCredentialUserEntity *user;
+
+/*!
+ @abstract
+    A list of YKFFIDO2PublicKeyCredentialParam objects with algorithm identifiers which are values registered in
+    the IANA COSE Algorithms registry. This sequence is ordered from most preferred (by the RP) to least preferred.
+ 
+ @discussion
+    This property is required by the key to fulfil the request. If missing, the FIDO2 Service will return
+    an error when trying to execute the request.
+ */
+@property (nonatomic) NSArray *pubKeyCredParams;
+
+/*!
+ @abstract
+    A list of YKFFIDO2PublicKeyCredentialDescriptor to be excluded when creating a new credential.
+ 
+ @discussion
+    The authenticator returns an error if the authenticator already contains one of the credentials enumerated in
+    this sequence. This allows RPs to limit the creation of multiple credentials for the same account on a single
+    authenticator. This property is optional.
+ */
+@property (nonatomic, nullable) NSArray *excludeList;
+
+/*!
+ @discussion
+    Parameters to influence authenticator operation, as specified in in the table below.
+    This parameter is optional.
+ 
+    @code
+    Key           | Default value      | Definition
+    ----------------------------------------------------------------------------------------
+    rk            | false              | resident key: Instructs the authenticator to store
+                                         the key material on the device.
+    ----------------------------------------------------------------------------------------
+    uv            | false              | user verification: Instructs the authenticator to
+                                         require a gesture that verifies the user to complete
+                                         the request. Examples of such gestures are fingerprint
+                                         scan or a PIN.
+    ----------------------------------------------------------------------------------------
+    up            | INVALID            | user presence: The key will return an error if this
+                                         parameter is set when creating a credential.
+                                         UP cannot be configured when creating a credential
+                                         because it's implicitly set to true.
+    @endcode
+ */
+@property (nonatomic, nullable) NSDictionary *options;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/YubiKit/YubiKit/Sessions/Shared/Requests/FIDO2/YKFKeyFIDO2MakeCredentialRequest.m b/YubiKit/YubiKit/Sessions/Shared/Requests/FIDO2/YKFKeyFIDO2MakeCredentialRequest.m
new file mode 100755
index 000000000..39923534e
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Requests/FIDO2/YKFKeyFIDO2MakeCredentialRequest.m
@@ -0,0 +1,22 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "YKFKeyFIDO2MakeCredentialRequest.h"
+#import "YKFKeyFIDO2MakeCredentialRequest+Private.h"
+
+NSString* const YKFKeyFIDO2MakeCredentialRequestOptionRK = @"rk";
+NSString* const YKFKeyFIDO2MakeCredentialRequestOptionUV = @"uv";
+
+@implementation YKFKeyFIDO2MakeCredentialRequest
+@end
diff --git a/YubiKit/YubiKit/Sessions/Shared/Requests/FIDO2/YKFKeyFIDO2MakeCredentialResponse+Private.h b/YubiKit/YubiKit/Sessions/Shared/Requests/FIDO2/YKFKeyFIDO2MakeCredentialResponse+Private.h
new file mode 100755
index 000000000..9ae4fab7f
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Requests/FIDO2/YKFKeyFIDO2MakeCredentialResponse+Private.h
@@ -0,0 +1,26 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+#import "YKFKeyFIDO2MakeCredentialResponse.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface YKFKeyFIDO2MakeCredentialResponse()
+
+- (nullable instancetype)initWithCBORData:(NSData *)cborData NS_DESIGNATED_INITIALIZER;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/YubiKit/YubiKit/Sessions/Shared/Requests/FIDO2/YKFKeyFIDO2MakeCredentialResponse.h b/YubiKit/YubiKit/Sessions/Shared/Requests/FIDO2/YKFKeyFIDO2MakeCredentialResponse.h
new file mode 100755
index 000000000..9dad05ce8
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Requests/FIDO2/YKFKeyFIDO2MakeCredentialResponse.h
@@ -0,0 +1,162 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+
+@class YKFFIDO2AuthenticatorData;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * ---------------------------------------------------------------------------------------------------------------------
+ * @name YKFKeyFIDO2MakeCredentialResponse
+ * ---------------------------------------------------------------------------------------------------------------------
+ */
+
+/*!
+ @abstract
+    The response to a FIDO2 Make Credential request. The result contains the data structures defined in
+    the CTAP2 authenticatorMakeCredential command response.
+ 
+ @discussion
+    For convenience, there are some derived properties which can be used on the client to check some parsed
+    parameters of the response, but they should not be necessary in the client-server communication.
+ */
+@interface YKFKeyFIDO2MakeCredentialResponse: NSObject
+
+/*!
+ The authenticator data object.
+ */
+@property (nonatomic, readonly) NSData *authData;
+
+/*!
+ The attestation statement format identifier.
+ */
+@property (nonatomic, readonly) NSString *fmt;
+
+/*!
+ The attestation statement, whose format is identified by the "fmt" object member. The client should treat it
+ as an opaque object.
+ */
+@property (nonatomic, readonly) NSData *attStmt;
+
+/*!
+ @abstract
+    The raw, unparsed CBOR response of the request.
+ 
+ @discussion
+    This value is useful when the server implementation will handle the parsing of the response and requires
+    the unparsed payload received from the key.
+ */
+@property (nonatomic, readonly) NSData *rawResponse;
+
+/*
+ * Derived Properties
+ */
+
+/*!
+ @abstract
+    Returns the attestation object as defined in the CTAP2 specifictions.
+ 
+ @discussion
+    Currently this property provides the same CBOR map as the rawResponse. In this map the attestation
+    object is using numeric keys for fmt, authData and attStmt, as defined by the CTAP2 specification:
+    https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#responses
+ */
+@property (nonatomic, readonly) NSData *ctapAttestationObject;
+
+/*!
+ @abstract
+    Returns the CTAP2 attestation object in WebAuthN format.
+ 
+ @discussion
+    This property returns a CBOR map with the content identical to the ctapAttestationObject, except
+    for the keys, which are text, as defined by the WebAuthN specifications:
+    https://developer.mozilla.org/en-US/docs/Web/API/AuthenticatorAttestationResponse/attestationObject
+ */
+@property (nonatomic, readonly) NSData *webauthnAttestationObject;
+
+/*!
+ @abstract
+    The authenticatorData is a derived property which will be lazy created by parsing the authData property value.
+ 
+ @discussion
+    The information provided by this property should not be required by the server. The client should treat the
+    Make Credential result as an opaque object and send it to the authentication server to be processed. However,
+    the information provided by this object can be used by the client to inspect some authenticator and response
+    properties.
+ */
+@property (nonatomic, readonly, nullable) YKFFIDO2AuthenticatorData *authenticatorData;
+
+/*
+ Not available: the response will be created by the library.
+ */
+- (instancetype)init NS_UNAVAILABLE;
+
+@end
+
+/**
+ * ---------------------------------------------------------------------------------------------------------------------
+ * @name YKFFIDO2AuthenticatorData
+ * ---------------------------------------------------------------------------------------------------------------------
+ */
+
+/*!
+ @class YKFFIDO2AuthenticatorData
+ 
+ @abstract
+    Provides a list of authenticator parameters as defined in WebAuthN authenticator data structure:
+    https://www.w3.org/TR/webauthn/#authenticator-data
+ */
+@interface YKFFIDO2AuthenticatorData: NSObject
+
+/*!
+ SHA-256 hash of the RP ID the credential is scoped to.
+ */
+@property (nonatomic, readonly) NSData *rpIdHash;
+
+/*!
+ A bit field of flags which indicate some of the make credential result parameters like user presence,
+ user verification, presence of attestation data, etc.
+ */
+@property (nonatomic, readonly) UInt8 flags;
+
+/*!
+ The signature counter.
+ */
+@property (nonatomic, readonly) UInt32 signCount;
+
+/*!
+ The AAGUID of the authenticator. This is a 16 bytes identifier.
+ */
+@property (nonatomic, readonly, nullable) NSData *aaguid;
+
+/*!
+ The credential ID of the newly created credential.
+ */
+@property (nonatomic, readonly, nullable) NSData *credentialId;
+
+/*!
+ The credential public key, encoded in COSE key format (RFC 8152).
+ */
+@property (nonatomic, readonly, nullable) NSData *coseEncodedCredentialPublicKey;
+
+/*
+ Not available: instances should be created only by the library.
+ */
+- (instancetype)init NS_UNAVAILABLE;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/YubiKit/YubiKit/Sessions/Shared/Requests/FIDO2/YKFKeyFIDO2MakeCredentialResponse.m b/YubiKit/YubiKit/Sessions/Shared/Requests/FIDO2/YKFKeyFIDO2MakeCredentialResponse.m
new file mode 100755
index 000000000..50fddb5a4
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Requests/FIDO2/YKFKeyFIDO2MakeCredentialResponse.m
@@ -0,0 +1,189 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "YKFKeyFIDO2MakeCredentialResponse.h"
+#import "YKFKeyFIDO2MakeCredentialResponse+Private.h"
+#import "YKFCBORDecoder.h"
+#import "YKFCBOREncoder.h"
+#import "YKFAssert.h"
+
+typedef NS_ENUM(NSUInteger, YKFKeyFIDO2MakeCredentialResponseKey) {
+    YKFKeyFIDO2GetInfoResponseKeyFmt        = 0x01,
+    YKFKeyFIDO2GetInfoResponseKeyAuthData   = 0x02,
+    YKFKeyFIDO2GetInfoResponseKeyAttStmt    = 0x03
+};
+
+typedef NS_ENUM(NSUInteger, YKFFIDO2AuthenticatorDataFlag) {
+    YKFFIDO2AuthenticatorDataFlagUserPresent    = 0x01,
+    YKFFIDO2AuthenticatorDataFlagUserVerified   = 0x04,
+    YKFFIDO2AuthenticatorDataFlagAttested       = 0x40,
+    YKFFIDO2AuthenticatorDataFlagExtensionData  = 0x80
+};
+
+static NSString* const YKFKeyFIDO2MakeCredentialResponsePackedAttStmtFmt = @"packed";
+
+@interface YKFFIDO2AuthenticatorData()
+
+@property (nonatomic, readwrite) NSData *rpIdHash;
+@property (nonatomic, readwrite) UInt8 flags;
+@property (nonatomic, readwrite) UInt32 signCount;
+@property (nonatomic, readwrite) NSData *aaguid;
+@property (nonatomic, readwrite) NSData *credentialId;
+@property (nonatomic, readwrite) NSData *coseEncodedCredentialPublicKey;
+
+- (instancetype)initWithData:(NSData *)data NS_DESIGNATED_INITIALIZER;
+
+@end
+
+@interface YKFKeyFIDO2MakeCredentialResponse()
+
+@property (nonatomic, readwrite) NSData *authData;
+@property (nonatomic, readwrite) NSString *fmt;
+@property (nonatomic, readwrite) NSData *attStmt;
+
+@property (nonatomic, readwrite) NSData *rawResponse;
+
+@property (nonatomic, readwrite) NSData *ctapAttestationObject;
+@property (nonatomic, readwrite) NSData *webauthnAttestationObject;
+
+@end
+
+@implementation YKFKeyFIDO2MakeCredentialResponse
+
+- (instancetype)initWithCBORData:(NSData *)cborData {
+    self = [super init];
+    if (self) {
+        YKFAssertAbortInit(cborData);
+        
+        self.rawResponse = cborData;
+        self.ctapAttestationObject = cborData;
+        
+        YKFCBORMap *attestationMap = nil;
+        
+        NSInputStream *decoderInputStream = [[NSInputStream alloc] initWithData:cborData];
+        [decoderInputStream open];
+        attestationMap = [YKFCBORDecoder decodeObjectFrom:decoderInputStream];
+        [decoderInputStream close];
+        
+        YKFAssertAbortInit(attestationMap);
+        
+        BOOL success = [self parseAttestationMap: attestationMap];
+        YKFAssertAbortInit(success);
+        
+        success = [self buildWebAuthnAttestationObjectFromMap:attestationMap];
+        YKFAssertAbortInit(success);
+    }
+    return self;
+}
+
+- (BOOL)parseAttestationMap:(YKFCBORMap *)map {
+    id convertedObject = [YKFCBORDecoder convertCBORObjectToFoundationType:map];
+    if (!convertedObject || ![convertedObject isKindOfClass:NSDictionary.class]) {
+        return NO;
+    }
+    NSDictionary *response = (NSDictionary *)convertedObject;
+    
+    // Auth Data
+    NSData *authData = response[@(YKFKeyFIDO2GetInfoResponseKeyAuthData)];
+    YKFAssertReturnValue(authData, @"authenticatorMakeCredential authData is required.", NO);
+    self.authData = authData;
+
+    // Fmt
+    NSString *fmt = response[@(YKFKeyFIDO2GetInfoResponseKeyFmt)];
+    YKFAssertReturnValue(fmt, @"authenticatorMakeCredential fmt is required.", NO);
+    self.fmt = fmt;
+
+    // AttStmt
+    if ([fmt isEqualToString:YKFKeyFIDO2MakeCredentialResponsePackedAttStmtFmt]) {
+        YKFCBORMap *attStmtMap = map.value[YKFCBORInteger(YKFKeyFIDO2GetInfoResponseKeyAttStmt)];
+        self.attStmt = [YKFCBOREncoder encodeMap:attStmtMap];
+    } else {
+        self.attStmt = response[@(YKFKeyFIDO2GetInfoResponseKeyAttStmt)];
+    }
+    YKFAssertReturnValue(self.attStmt, @"authenticatorGetInfo attStmt is required.", NO);
+
+    return YES;
+}
+
+- (BOOL)buildWebAuthnAttestationObjectFromMap:(YKFCBORMap *)map {
+    id authData = map.value[YKFCBORInteger(YKFKeyFIDO2GetInfoResponseKeyAuthData)];
+    YKFAssertReturnValue(authData, @"authenticatorGetInfo authData is required.", NO);
+    
+    id fmt = map.value[YKFCBORInteger(YKFKeyFIDO2GetInfoResponseKeyFmt)];
+    YKFAssertReturnValue(fmt, @"authenticatorGetInfo fmt is required.", NO);
+    
+    id attStmt = map.value[YKFCBORInteger(YKFKeyFIDO2GetInfoResponseKeyAttStmt)];
+    YKFAssertReturnValue(attStmt, @"authenticatorGetInfo attStmt is required.", NO);
+    
+    NSDictionary *attestationDictionary = @{YKFCBORTextString(@"authData"): authData,
+                                            YKFCBORTextString(@"fmt"): fmt,
+                                            YKFCBORTextString(@"attStmt"): attStmt};
+    YKFCBORMap *attestationMap = YKFCBORMap(attestationDictionary);
+    NSData *cborEncodedAttestationMap = [YKFCBOREncoder encodeMap:attestationMap];
+    self.webauthnAttestationObject = cborEncodedAttestationMap;
+    
+    return cborEncodedAttestationMap != nil;
+}
+
+#pragma mark - Derived Properties
+
+- (YKFFIDO2AuthenticatorData *)authenticatorData {
+    return [[YKFFIDO2AuthenticatorData alloc] initWithData:self.authData];
+}
+
+@end
+
+@implementation YKFFIDO2AuthenticatorData
+
+- (instancetype)initWithData:(NSData *)data {
+    YKFAssertAbortInit(data.length >= 37) // SHA(32) + Flags(1) + Counter(4)
+    
+    self = [super init];
+    if (self) {
+        self.rpIdHash = [data subdataWithRange:NSMakeRange(0, 32)];
+        
+        UInt8 *dataBytes = (UInt8 *)data.bytes;
+        self.flags = dataBytes[32];
+        
+        UInt32 bigEndianSignCount = *((UInt32 *)(&dataBytes[33]));
+        self.signCount = CFSwapInt32BigToHost(bigEndianSignCount);
+        
+        if (self.flags & YKFFIDO2AuthenticatorDataFlagAttested) {
+            NSUInteger attestedCredentialDataOffset = 37;
+            
+            NSData *attestedCredentialData = [data subdataWithRange:NSMakeRange(attestedCredentialDataOffset, data.length - attestedCredentialDataOffset)];
+            YKFAssertAbortInit(attestedCredentialData.length >= 18); // AAGUID(16) + CredentialIdLength(2)
+            
+            self.aaguid = [attestedCredentialData subdataWithRange:NSMakeRange(0, 16)];
+            
+            UInt8 *attestedCredentialDataBytes = (UInt8 *)attestedCredentialData.bytes;
+            UInt16 bigEndianCredentialIdLength = *((UInt16 *)(&attestedCredentialDataBytes[16]));
+            UInt16 credentialIdLength = CFSwapInt16BigToHost(bigEndianCredentialIdLength);
+            
+            if (credentialIdLength > 0) {
+                NSUInteger coseKeyOffset = 18 + credentialIdLength;
+                
+                YKFAssertAbortInit(attestedCredentialData.length > coseKeyOffset);
+                self.credentialId = [attestedCredentialData subdataWithRange:NSMakeRange(18, credentialIdLength)];
+                
+                NSRange coseKeyRange = NSMakeRange(coseKeyOffset, attestedCredentialData.length - coseKeyOffset);
+                self.coseEncodedCredentialPublicKey = [attestedCredentialData subdataWithRange: coseKeyRange];
+                YKFAssertAbortInit(self.coseEncodedCredentialPublicKey.length > 0);
+            }
+        }
+    }
+    return self;
+}
+
+@end
diff --git a/YubiKit/YubiKit/Sessions/Shared/Requests/FIDO2/YKFKeyFIDO2Request+Private.h b/YubiKit/YubiKit/Sessions/Shared/Requests/FIDO2/YKFKeyFIDO2Request+Private.h
new file mode 100755
index 000000000..c9bcebe8e
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Requests/FIDO2/YKFKeyFIDO2Request+Private.h
@@ -0,0 +1,31 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+#import "YKFKeyFIDO2Request.h"
+#import "YKFAPDU.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface YKFKeyFIDO2Request()
+
+@property (nonatomic, assign) int retries;
+@property (nonatomic, assign, readonly) BOOL shouldRetry;
+@property (nonatomic, assign, readonly) NSTimeInterval retryTimeInterval;
+
+@property (nonatomic) YKFAPDU *apdu;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/YubiKit/YubiKit/Sessions/Shared/Requests/FIDO2/YKFKeyFIDO2Request.h b/YubiKit/YubiKit/Sessions/Shared/Requests/FIDO2/YKFKeyFIDO2Request.h
new file mode 100755
index 000000000..84c8ae5e2
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Requests/FIDO2/YKFKeyFIDO2Request.h
@@ -0,0 +1,30 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+#import "YKFKeyRequest.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/*!
+ @class YKFKeyFIDO2Request
+ 
+ @abstract
+    Base clase for all FIDO2 requests. Use the subclasses of this type  for sending specific
+    FIDO2 requests to the key.
+ */
+@interface YKFKeyFIDO2Request: YKFKeyRequest
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/YubiKit/YubiKit/Sessions/Shared/Requests/FIDO2/YKFKeyFIDO2Request.m b/YubiKit/YubiKit/Sessions/Shared/Requests/FIDO2/YKFKeyFIDO2Request.m
new file mode 100755
index 000000000..fcc499831
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Requests/FIDO2/YKFKeyFIDO2Request.m
@@ -0,0 +1,32 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "YKFKeyFIDO2Request.h"
+#import "YKFKeyFIDO2Request+Private.h"
+#import "YKFAPDU.h"
+
+static const int YKFKeyFIDO2RequestMaxRetries = 30; // times
+static const NSTimeInterval YKFKeyFIDO2RequestRetryTimeInterval = 0.5; // seconds
+
+@implementation YKFKeyFIDO2Request
+
+- (BOOL)shouldRetry {
+    return self.retries <= YKFKeyFIDO2RequestMaxRetries;
+}
+
+- (NSTimeInterval)retryTimeInterval {
+    return YKFKeyFIDO2RequestRetryTimeInterval;
+}
+
+@end
diff --git a/YubiKit/YubiKit/Sessions/Shared/Requests/FIDO2/YKFKeyFIDO2SetPinRequest.h b/YubiKit/YubiKit/Sessions/Shared/Requests/FIDO2/YKFKeyFIDO2SetPinRequest.h
new file mode 100755
index 000000000..ebe035674
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Requests/FIDO2/YKFKeyFIDO2SetPinRequest.h
@@ -0,0 +1,62 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+#import "YKFKeyFIDO2Request.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/*!
+ @class YKFKeyFIDO2SetPinRequest
+ 
+ @abstract
+    Request for settings the PIN on the FIDO2 key application. This request maps to the
+    authenticatorClientPin command from CTAP2 protocol:
+    https://fidoalliance.org/specs/fido-v2.0-rd-20180702/fido-client-to-authenticator-protocol-v2.0-rd-20180702.pdf
+ 
+ @discussion
+    The key FIDO2 application should not have a PIN when executing this request. If the
+    application has already a PIN, this request will return an error and a change PIN
+    request should be executed instead. After setting the PIN, a verify PIN request must be
+    executed to authenticate the session.
+ */
+@interface YKFKeyFIDO2SetPinRequest: YKFKeyFIDO2Request
+
+/*!
+ @abstract
+    The PIN to set on the FIDO2 key application.
+ 
+ @discussion
+    The minimum PIN length accepted by the key is 4. If the PIN is shorter then 4 or longer then 255
+    UTF8 encoded characters, the library will return an error.
+ */
+@property (nonatomic, readonly) NSString *pin;
+
+/*!
+ @abstract
+    Creates a new instance with the PIN to set.
+ 
+ @param pin
+    The PIN to set on the FIDO2 key application.
+ */
+- (nullable instancetype)initWithPin:(NSString *)pin NS_DESIGNATED_INITIALIZER;
+
+/*
+ Not available: use [initWithPin:] instead.
+ */
+- (instancetype)init NS_UNAVAILABLE;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/YubiKit/YubiKit/Sessions/Shared/Requests/FIDO2/YKFKeyFIDO2SetPinRequest.m b/YubiKit/YubiKit/Sessions/Shared/Requests/FIDO2/YKFKeyFIDO2SetPinRequest.m
new file mode 100755
index 000000000..093527254
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Requests/FIDO2/YKFKeyFIDO2SetPinRequest.m
@@ -0,0 +1,35 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "YKFKeyFIDO2SetPinRequest.h"
+#import "YKFAssert.h"
+
+@interface YKFKeyFIDO2SetPinRequest()
+
+@property (nonatomic, readwrite) NSString *pin;
+
+@end
+
+@implementation YKFKeyFIDO2SetPinRequest
+
+- (instancetype)initWithPin:(NSString *)pin {
+    self = [super init];
+    if (self) {
+        YKFAssertAbortInit(pin);
+        self.pin = pin;
+    }
+    return self;
+}
+
+@end
diff --git a/YubiKit/YubiKit/Sessions/Shared/Requests/FIDO2/YKFKeyFIDO2VerifyPinRequest.h b/YubiKit/YubiKit/Sessions/Shared/Requests/FIDO2/YKFKeyFIDO2VerifyPinRequest.h
new file mode 100755
index 000000000..7c19cde9b
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Requests/FIDO2/YKFKeyFIDO2VerifyPinRequest.h
@@ -0,0 +1,63 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+#import "YKFKeyFIDO2Request.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/*!
+ @class YKFKeyFIDO2VerifyPinRequest
+ 
+ @abstract
+    Request for verifying the PIN on the FIDO2 key application. This request maps to the
+    authenticatorClientPin command from CTAP2 protocol:
+    https://fidoalliance.org/specs/fido-v2.0-rd-20180702/fido-client-to-authenticator-protocol-v2.0-rd-20180702.pdf
+ 
+ @discussion
+    If the FIDO2 application on the key has a PIN, the client will have to execute this
+    request before creating a credential or getting an assertion from the key. Once the PIN
+    is successfully verified, any subsequent requests, which require PIN verification, in the
+    same session can be executed. If the session is reopened (by replugging the key or reopened
+    by the application) the PIN verification is required again.
+ */
+@interface YKFKeyFIDO2VerifyPinRequest: YKFKeyFIDO2Request
+
+/*!
+ @abstract
+    The PIN to verify on the FIDO2 key application.
+ 
+ @discussion
+    The minimum PIN length accepted by the key is 4. If the PIN is shorter then 4 or longer then 255
+    UTF8 encoded characters, the library will return an error.
+ */
+@property (nonatomic, readonly) NSString *pin;
+
+/*!
+ @abstract
+    Creates a new instance with the PIN to verify.
+ 
+ @param pin
+    The PIN to verify on the FIDO2 key application.
+ */
+- (nullable instancetype)initWithPin:(NSString *)pin NS_DESIGNATED_INITIALIZER;
+
+/*
+ Not available: use [initWithPin:] instead.
+ */
+- (instancetype)init NS_UNAVAILABLE;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/YubiKit/YubiKit/Sessions/Shared/Requests/FIDO2/YKFKeyFIDO2VerifyPinRequest.m b/YubiKit/YubiKit/Sessions/Shared/Requests/FIDO2/YKFKeyFIDO2VerifyPinRequest.m
new file mode 100755
index 000000000..2ad10e087
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Requests/FIDO2/YKFKeyFIDO2VerifyPinRequest.m
@@ -0,0 +1,35 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "YKFKeyFIDO2VerifyPinRequest.h"
+#import "YKFAssert.h"
+
+@interface YKFKeyFIDO2VerifyPinRequest()
+
+@property (nonatomic, readwrite) NSString *pin;
+
+@end
+
+@implementation YKFKeyFIDO2VerifyPinRequest
+
+- (instancetype)initWithPin:(NSString *)pin {
+    self = [super init];
+    if (self) {
+        YKFAssertAbortInit(pin);
+        self.pin = pin;
+    }
+    return self;
+}
+
+@end
diff --git a/YubiKit/YubiKit/Sessions/Shared/Requests/OATH/YKFKeyOATHCalculateAllRequest+Private.h b/YubiKit/YubiKit/Sessions/Shared/Requests/OATH/YKFKeyOATHCalculateAllRequest+Private.h
new file mode 100755
index 000000000..a3eca5f0c
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Requests/OATH/YKFKeyOATHCalculateAllRequest+Private.h
@@ -0,0 +1,24 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "YKFKeyOATHCalculateAllRequest.h"
+
+@interface YKFKeyOATHCalculateAllRequest()
+
+/*
+ The timestamp which was used to initiate the key command.
+ */
+@property (nonatomic, nonnull) NSDate *timestamp;
+
+@end
diff --git a/YubiKit/YubiKit/Sessions/Shared/Requests/OATH/YKFKeyOATHCalculateAllRequest.h b/YubiKit/YubiKit/Sessions/Shared/Requests/OATH/YKFKeyOATHCalculateAllRequest.h
new file mode 100755
index 000000000..89486d699
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Requests/OATH/YKFKeyOATHCalculateAllRequest.h
@@ -0,0 +1,40 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+#import "YKFKeyOATHRequest.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/*!
+ @class YKFKeyOATHCalculateAllRequest
+ 
+ @abstract
+    Request for calculating all OATH credentials saved on the key. This request maps to the
+    CALCULATE ALL command from YOATH protocol:
+    https://developers.yubico.com/OATH/YKOATH_Protocol.html
+ 
+ @discussion
+    This calculates only TOTP credentials and returns the labels for HOTP credentials to avoid
+    overloading the HOTP counters.
+ 
+    Calculate All assumes that all TOTP credentials have a period of 30 seconds (default period)
+    because the key command receives only one time challenge. For TOTP credentials which have a
+    different period an explicit calculate request with the right period challenge needs to be
+    performed afterwards.
+ */
+@interface YKFKeyOATHCalculateAllRequest: YKFKeyOATHRequest
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/YubiKit/YubiKit/Sessions/Shared/Requests/OATH/YKFKeyOATHCalculateAllRequest.m b/YubiKit/YubiKit/Sessions/Shared/Requests/OATH/YKFKeyOATHCalculateAllRequest.m
new file mode 100755
index 000000000..986a00a9c
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Requests/OATH/YKFKeyOATHCalculateAllRequest.m
@@ -0,0 +1,31 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "YKFKeyOATHCalculateAllRequest.h"
+#import "YKFKeyOATHCalculateAllRequest+Private.h"
+#import "YKFOATHCalculateAllAPDU.h"
+#import "YKFKeyOATHRequest+Private.h"
+
+@implementation YKFKeyOATHCalculateAllRequest
+
+- (instancetype)init {
+    self = [super init];
+    if (self) {
+        self.timestamp = [NSDate date];
+        self.apdu = [[YKFOATHCalculateAllAPDU alloc] initWithTimestamp:self.timestamp];
+    }
+    return self;
+}
+
+@end
diff --git a/YubiKit/YubiKit/Sessions/Shared/Requests/OATH/YKFKeyOATHCalculateAllResponse+Private.h b/YubiKit/YubiKit/Sessions/Shared/Requests/OATH/YKFKeyOATHCalculateAllResponse+Private.h
new file mode 100755
index 000000000..9a18ebab0
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Requests/OATH/YKFKeyOATHCalculateAllResponse+Private.h
@@ -0,0 +1,29 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "YKFKeyOATHCalculateAllResponse.h"
+
+@interface YKFOATHCredentialCalculateResult ()
+
+@property (nonatomic, nonnull) NSString *key;
+
+@end
+
+@interface YKFKeyOATHCalculateAllResponse()
+
+- (nullable instancetype)initWithKeyResponseData:(nonnull NSData *)responseData requestTimetamp:(nonnull NSDate *)timestamp NS_DESIGNATED_INITIALIZER;
+
+@end
+
+
diff --git a/YubiKit/YubiKit/Sessions/Shared/Requests/OATH/YKFKeyOATHCalculateAllResponse.h b/YubiKit/YubiKit/Sessions/Shared/Requests/OATH/YKFKeyOATHCalculateAllResponse.h
new file mode 100755
index 000000000..6a13a8abc
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Requests/OATH/YKFKeyOATHCalculateAllResponse.h
@@ -0,0 +1,91 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+#import "YKFOATHCredential.h"
+
+/*!
+ @class YKFOATHCredentialCalculateResult
+ 
+ @abstract
+    The response for a credential from Calculate All request. The result contains
+    both some credential information and the calculation for a TOTP credential.
+ */
+@interface YKFOATHCredentialCalculateResult: NSObject
+
+/*!
+ The type of the credential (TOTP or HOTP).
+ */
+@property (nonatomic, assign, readonly) YKFOATHCredentialType type;
+
+/*!
+ The account name associated with the credential. This value was set when the credential
+ was added to the key.
+ */
+@property (nonatomic, readonly, nonnull) NSString *account;
+
+/*!
+ The issuer associated with the credential. This value was set when the credential was
+ added to the key.
+ */
+@property (nonatomic, readonly, nullable) NSString *issuer;
+
+/*!
+ The validity period for the credential, when TOTP. For HOTP this property has the value 0.
+ */
+@property (nonatomic, assign, readonly) NSUInteger period;
+
+/*!
+ The validity date interval for the credential, when TOTP. For HOTP this property is the
+ interval [<time of request>, <date distant future>] because an HOTP credential does not have
+ an expiration date.
+ */
+@property (nonatomic, readonly, nonnull) NSDateInterval *validity;
+
+/*!
+ The OTP value of the credential. Calculate All does not calculate HOTP credentials to not overload
+ the counters. When the credential is HOTP, the value of this property is nil. To calculate HOTP
+ credentials an explicit calculate request with the credential needs to pe performed.
+ */
+@property (nonatomic, readonly, nullable) NSString *otp;
+
+/*!
+ The credential requires the user to touch the key to generate it. The property returns YES for
+ HOTP credentials and for TOTP credentials created with Touch Required option.
+ */
+@property (nonatomic, readonly) BOOL requiresTouch;
+
+@end
+
+/*!
+ @class YKFKeyOATHCalculateAllResponse
+ 
+ @abstract
+    Response from Calculate All request for calculating all OATH credentials saved on the key.
+ */
+@interface YKFKeyOATHCalculateAllResponse : NSObject
+
+/*!
+ The list of credentials (YKFOATHCredentialCalculateResult type) with the calculated OTPs.
+ If the key does not contain any OATH credentials, this property returns an empty array.
+ */
+@property (nonatomic, readonly, nonnull) NSArray *credentials;
+
+/*
+ Not available: the library will create a response as the result of the Calculate All request.
+ */
+- (nonnull instancetype)init NS_UNAVAILABLE;
+
+@end
+
diff --git a/YubiKit/YubiKit/Sessions/Shared/Requests/OATH/YKFKeyOATHCalculateAllResponse.m b/YubiKit/YubiKit/Sessions/Shared/Requests/OATH/YKFKeyOATHCalculateAllResponse.m
new file mode 100755
index 000000000..a9dea3d39
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Requests/OATH/YKFKeyOATHCalculateAllResponse.m
@@ -0,0 +1,175 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "YKFKeyOATHCalculateAllResponse.h"
+#import "YKFKeyOATHCalculateAllResponse+Private.h"
+#import "YKFAssert.h"
+#import "YKFNSStringAdditions.h"
+#import "YKFNSDataAdditions+Private.h"
+
+static const UInt8 YKFKeyOATHCalculateAllNameTag = 0x71;
+static const UInt8 YKFKeyOATHCalculateAllResponseHOTPTag = 0x77;
+static const UInt8 YKFKeyOATHCalculateAllResponseFullResponseTag = 0x75;
+static const UInt8 YKFKeyOATHCalculateAllResponseTruncatedResponseTag = 0x76;
+static const UInt8 YKFKeyOATHCalculateAllResponseTouchTag = 0x7C;
+
+static NSUInteger const YKFOATHCredentialCalculateResultDefaultPeriod = 30; // seconds
+
+@interface YKFOATHCredentialCalculateResult()
+
+@property (nonatomic, assign, readwrite) YKFOATHCredentialType type;
+@property (nonatomic, readwrite) NSString *account;
+@property (nonatomic, readwrite) NSString *issuer;
+@property (nonatomic, assign, readwrite) NSUInteger period;
+@property (nonatomic, readwrite, nonnull) NSDateInterval *validity;
+@property (nonatomic, readwrite) NSString *otp;
+@property (nonatomic, readwrite) BOOL requiresTouch;
+
+@end
+
+@implementation YKFOATHCredentialCalculateResult
+
+- (NSUInteger)period {
+    if (_period) {
+        return _period;
+    }
+    return self.type == YKFOATHCredentialTypeTOTP ? YKFOATHCredentialCalculateResultDefaultPeriod : 0;
+}
+
+@end
+
+
+@interface YKFKeyOATHCalculateAllResponse()
+
+@property (nonatomic, readwrite) NSArray *credentials;
+
+@end
+
+@implementation YKFKeyOATHCalculateAllResponse
+
+- (instancetype)initWithKeyResponseData:(NSData *)responseData requestTimetamp:(NSDate *)timestamp {
+    YKFAssertAbortInit(responseData);
+    
+    self = [super init];
+    if (self) {
+        NSMutableArray *responseCredentials = [[NSMutableArray alloc] init];
+        UInt8 *responseBytes = (UInt8 *)responseData.bytes;
+        NSUInteger readIndex = 0;
+        
+        while (readIndex < responseData.length && responseBytes[readIndex] == YKFKeyOATHCalculateAllNameTag) {
+            YKFOATHCredentialCalculateResult *credentialResult = [[YKFOATHCredentialCalculateResult alloc] init];
+            
+            ++readIndex;
+            YKFAssertAbortInit([responseData ykf_containsIndex:readIndex]);
+            
+            UInt8 nameLength = responseBytes[readIndex];
+            YKFAssertAbortInit(nameLength > 0);
+            
+            ++readIndex;
+            NSRange nameRange = NSMakeRange(readIndex, nameLength);
+            YKFAssertAbortInit([responseData ykf_containsRange:nameRange]);
+            
+            NSData *nameData = [responseData subdataWithRange:nameRange];
+            credentialResult.key = [[NSString alloc] initWithData:nameData encoding:NSUTF8StringEncoding];
+            
+            readIndex += nameLength;
+            YKFAssertAbortInit([responseData ykf_containsIndex:readIndex]);
+            
+            UInt8 responseTag = responseBytes[readIndex];
+            switch (responseTag) {
+                case YKFKeyOATHCalculateAllResponseHOTPTag:
+                    credentialResult.type = YKFOATHCredentialTypeHOTP;
+                    break;
+                    
+                case YKFKeyOATHCalculateAllResponseFullResponseTag:
+                case YKFKeyOATHCalculateAllResponseTruncatedResponseTag:
+                case YKFKeyOATHCalculateAllResponseTouchTag:
+                    credentialResult.type = YKFOATHCredentialTypeTOTP;
+                    break;
+                
+                default:
+                    credentialResult.type = YKFOATHCredentialTypeUnknown;
+            }
+            YKFAssertAbortInit(credentialResult.type != YKFOATHCredentialTypeUnknown);
+            
+            ++readIndex;
+            YKFAssertAbortInit([responseData ykf_containsIndex:readIndex]);
+            
+            UInt8 responseLength = responseBytes[readIndex];
+            YKFAssertAbortInit(responseLength > 0);
+            
+            ++readIndex;
+            YKFAssertAbortInit([responseData ykf_containsIndex:readIndex]);
+            
+            UInt8 digits = responseBytes[readIndex];
+            YKFAssertAbortInit(digits == 6 || digits == 7 || digits == 8);
+            
+            // Parse the period, account and issuer from the key.
+            
+            NSString *credentialKey = credentialResult.key;
+            NSUInteger period = 0;
+            NSString *issuer = nil;
+            NSString *account = nil;
+            NSString *label = nil;
+            
+            [credentialKey ykf_OATHKeyExtractPeriod:&period issuer:&issuer account:&account label:&label];
+            
+            credentialResult.issuer = issuer;
+            credentialResult.account = account;
+            if (credentialResult.type == YKFOATHCredentialTypeTOTP) {
+                credentialResult.period = period ? period : YKFOATHCredentialCalculateResultDefaultPeriod;
+            }
+            
+            // Parse the OTP value when TOTP and touch is not required.
+            
+            if (credentialResult.type == YKFOATHCredentialTypeTOTP && responseTag != YKFKeyOATHCalculateAllResponseTouchTag) {
+                ++readIndex;
+                YKFAssertAbortInit([responseData ykf_containsIndex:readIndex]);
+                
+                UInt8 otpBytesLength = responseLength - 1;
+                YKFAssertAbortInit(otpBytesLength == 4);
+                
+                NSString *otp = [responseData ykf_parseOATHOTPFromIndex:readIndex digits:digits];
+                YKFAssertAbortInit(otp.length == digits);
+                
+                credentialResult.otp = otp;
+                
+                readIndex += otpBytesLength; // Jump to the next extry.
+            } else {
+                // No result for TOTP with touch or HOTP
+                if (credentialResult.type == YKFOATHCredentialTypeTOTP) {
+                    credentialResult.requiresTouch = YES;
+                }
+                ++readIndex;
+            }
+            
+            // Calculate validity
+            
+            if (credentialResult.type == YKFOATHCredentialTypeTOTP && responseTag != YKFKeyOATHCalculateAllResponseTouchTag) {
+                NSUInteger timestampTimeInterval = [timestamp timeIntervalSince1970]; // truncate to seconds
+                
+                NSDate *startDate = [NSDate dateWithTimeIntervalSince1970:timestampTimeInterval - timestampTimeInterval % credentialResult.period];
+                credentialResult.validity = [[NSDateInterval alloc] initWithStartDate:startDate duration:credentialResult.period];
+            } else {
+                credentialResult.validity = [[NSDateInterval alloc] initWithStartDate:timestamp endDate:[NSDate distantFuture]];
+            }                        
+            
+            [responseCredentials addObject:credentialResult];
+        }
+        self.credentials = [responseCredentials copy];
+    }
+    return self;
+}
+
+@end
diff --git a/YubiKit/YubiKit/Sessions/Shared/Requests/OATH/YKFKeyOATHCalculateRequest+Private.h b/YubiKit/YubiKit/Sessions/Shared/Requests/OATH/YKFKeyOATHCalculateRequest+Private.h
new file mode 100755
index 000000000..fc5690931
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Requests/OATH/YKFKeyOATHCalculateRequest+Private.h
@@ -0,0 +1,24 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "YKFKeyOATHCalculateRequest.h"
+
+@interface YKFKeyOATHCalculateRequest()
+
+/*
+ The timestamp which was used to initiate the key command.
+ */
+@property (nonatomic, nonnull) NSDate *timestamp;
+
+@end
diff --git a/YubiKit/YubiKit/Sessions/Shared/Requests/OATH/YKFKeyOATHCalculateRequest.h b/YubiKit/YubiKit/Sessions/Shared/Requests/OATH/YKFKeyOATHCalculateRequest.h
new file mode 100755
index 000000000..690c62ac8
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Requests/OATH/YKFKeyOATHCalculateRequest.h
@@ -0,0 +1,67 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+#import "YKFKeyOATHRequest.h"
+#import "YKFOATHCredential.h"
+
+/*!
+ @class YKFKeyOATHCalculateRequest
+ 
+ @abstract
+    Request for calculating an OATH credential saved on the key. This request maps to the CALCULATE
+    command from YOATH protocol:
+    https://developers.yubico.com/OATH/YKOATH_Protocol.html
+ */
+@interface YKFKeyOATHCalculateRequest: YKFKeyOATHRequest
+
+/*!
+ The credential for the request. The credential must provide at least the label and
+ period (TOTP) properties for the request to succeed. This value is set at initialization.
+ */
+@property (nonatomic, readonly, nonnull) YKFOATHCredential *credential;
+
+/*!
+ @method initWithCredential:
+ 
+ @abstract
+    The designated initializer for this type of request. The credential parameter is required.
+ 
+ @param credential
+    The credential for the request. The credential must be already added to the key when calling
+    this request.
+ */
+- (nullable instancetype)initWithCredential:(nonnull YKFOATHCredential*)credential;
+
+/*!
+ @method initWithCredential:
+ 
+ @abstract
+    The designated initializer for this type of request. The credential parameter is required.
+ 
+ @param credential
+    The credential for the request. The credential must be already added to the key when calling
+    this request.
+
+ @param timestamp
+    The challenge used for the TOTP calculation. This can be used to provide a custom point in time for the TOTP generation.
+ */
+- (nullable instancetype)initWithCredential:(nonnull YKFOATHCredential*)credential timestamp:(nonnull NSDate*)timestamp NS_DESIGNATED_INITIALIZER;
+
+/*
+ Not available: use [initWithCredential:].
+ */
+- (nonnull instancetype)init NS_UNAVAILABLE;
+
+@end
diff --git a/YubiKit/YubiKit/Sessions/Shared/Requests/OATH/YKFKeyOATHCalculateRequest.m b/YubiKit/YubiKit/Sessions/Shared/Requests/OATH/YKFKeyOATHCalculateRequest.m
new file mode 100755
index 000000000..8afe2ed72
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Requests/OATH/YKFKeyOATHCalculateRequest.m
@@ -0,0 +1,49 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "YKFKeyOATHCalculateRequest.h"
+#import "YKFKeyOATHCalculateRequest+Private.h"
+#import "YKFOATHCalculateAPDU.h"
+#import "YKFAssert.h"
+#import "YKFKeyOATHRequest+Private.h"
+
+@interface YKFKeyOATHCalculateRequest()
+
+@property (nonatomic, readwrite) YKFOATHCredential *credential;
+
+@end
+
+@implementation YKFKeyOATHCalculateRequest
+
+- (instancetype)initWithCredential:(nonnull YKFOATHCredential*)credential {
+    return [self initWithCredential:credential timestamp:[NSDate date]];
+}
+
+- (instancetype)initWithCredential:(nonnull YKFOATHCredential*)credential timestamp: (NSDate*) timestamp {
+    YKFAssertAbortInit(credential);
+    
+    if (credential.type == YKFOATHCredentialTypeTOTP) {
+        YKFAssertAbortInit(credential.period > 0);
+    }
+    
+    self = [super init];
+    if (self) {
+        self.credential = credential;
+        self.timestamp = timestamp;
+        self.apdu = [[YKFOATHCalculateAPDU alloc] initWithRequest:self timestamp:self.timestamp];
+    }
+    return self;
+}
+
+@end
diff --git a/YubiKit/YubiKit/Sessions/Shared/Requests/OATH/YKFKeyOATHCalculateResponse+Private.h b/YubiKit/YubiKit/Sessions/Shared/Requests/OATH/YKFKeyOATHCalculateResponse+Private.h
new file mode 100755
index 000000000..2ea870d0a
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Requests/OATH/YKFKeyOATHCalculateResponse+Private.h
@@ -0,0 +1,22 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+#import "YKFKeyOATHCalculateResponse.h"
+
+@interface YKFKeyOATHCalculateResponse()
+
+- (nullable instancetype)initWithKeyResponseData:(nonnull NSData *)responseData requestTimetamp:(nonnull NSDate *)timestamp requestPeriod:(NSUInteger)period NS_DESIGNATED_INITIALIZER;
+
+@end
diff --git a/YubiKit/YubiKit/Sessions/Shared/Requests/OATH/YKFKeyOATHCalculateResponse.h b/YubiKit/YubiKit/Sessions/Shared/Requests/OATH/YKFKeyOATHCalculateResponse.h
new file mode 100755
index 000000000..7b4fef265
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Requests/OATH/YKFKeyOATHCalculateResponse.h
@@ -0,0 +1,47 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+/*!
+ @class YKFKeyOATHCalculateResponse
+ 
+ @abstract
+    Response from Calculate OATH credential request.
+ */
+@interface YKFKeyOATHCalculateResponse: NSObject
+
+/*!
+ The OTP value for the credential. The value of this string is numeric and may have
+ only 6 or 8 characters.
+ */
+@property (nonatomic, readonly) NSString *otp;
+
+/*!
+ The validity of the OTP when the credential is TOTP. For HOTP this property is the
+ interval [<time of request>, <date distant future>] because an HOTP credential does
+ not have an expiration date.
+ */
+@property (nonatomic, readonly) NSDateInterval *validity;
+
+/*
+ Not available: the library will create a response as the result of the Calculate request.
+ */
+- (instancetype)init NS_UNAVAILABLE;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/YubiKit/YubiKit/Sessions/Shared/Requests/OATH/YKFKeyOATHCalculateResponse.m b/YubiKit/YubiKit/Sessions/Shared/Requests/OATH/YKFKeyOATHCalculateResponse.m
new file mode 100755
index 000000000..0afe4d7d3
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Requests/OATH/YKFKeyOATHCalculateResponse.m
@@ -0,0 +1,71 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "YKFKeyOATHCalculateResponse.h"
+#import "YKFKeyOATHCalculateResponse+Private.h"
+#import "YKFAssert.h"
+#import "YKFNSDataAdditions+Private.h"
+
+typedef NS_ENUM(NSUInteger, YKFKeyOATHCalculateResponseType) {
+    YKFKeyOATHCalculateResponseTypeFull = 0x75,
+    YKFKeyOATHCalculateResponseTypeTruncated = 0x76
+};
+
+@interface YKFKeyOATHCalculateResponse()
+
+@property (nonatomic, readwrite) NSString *otp;
+@property (nonatomic, readwrite) NSDateInterval *validity;
+
+@end
+
+@implementation YKFKeyOATHCalculateResponse
+
+- (nullable instancetype)initWithKeyResponseData:(nonnull NSData *)responseData requestTimetamp:(NSDate *)timestamp requestPeriod:(NSUInteger)period {
+    YKFAssertAbortInit(responseData.length);
+    YKFAssertAbortInit(timestamp);
+    
+    self = [super init];
+    if (self) {
+        UInt8 *bytes = (UInt8 *)responseData.bytes;
+        
+        UInt8 responseType = bytes[0];
+        YKFAssertAbortInit(responseType == YKFKeyOATHCalculateResponseTypeTruncated);
+        
+        YKFAssertAbortInit([responseData ykf_containsRange:NSMakeRange(1, 2)]);
+        UInt8 responseLength = bytes[1];
+        UInt8 digits = bytes[2];
+        YKFAssertAbortInit(digits == 6 || digits == 7 || digits == 8);
+        
+        UInt8 otpBytesLength = responseLength - 1;
+        YKFAssertAbortInit(otpBytesLength == 4);
+        
+        self.otp = [responseData ykf_parseOATHOTPFromIndex:3 digits:digits];
+        YKFAssertAbortInit(self.otp);
+        
+        if (period > 0) {
+            // TOTP
+            NSUInteger timestampTimeInterval = [timestamp timeIntervalSince1970]; // truncate to seconds
+            
+            NSDate *startDate = [NSDate dateWithTimeIntervalSince1970:timestampTimeInterval - timestampTimeInterval % period];
+            NSDate *endDate = [startDate dateByAddingTimeInterval:period];
+            self.validity = [[NSDateInterval alloc] initWithStartDate:startDate endDate:endDate];
+        } else {
+            // HOTP
+            self.validity = [[NSDateInterval alloc] initWithStartDate:timestamp endDate:[NSDate distantFuture]];
+        }        
+    }
+    return self;
+}
+
+@end
diff --git a/YubiKit/YubiKit/Sessions/Shared/Requests/OATH/YKFKeyOATHDeleteRequest.h b/YubiKit/YubiKit/Sessions/Shared/Requests/OATH/YKFKeyOATHDeleteRequest.h
new file mode 100755
index 000000000..798be8e49
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Requests/OATH/YKFKeyOATHDeleteRequest.h
@@ -0,0 +1,51 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+#import "YKFKeyOATHRequest.h"
+#import "YKFOATHCredential.h"
+
+/*!
+ @class YKFKeyOATHDeleteRequest
+ 
+ @abstract
+    Request for deleting an OATH credential saved on the key. This request maps to the DELETE
+    command from YOATH protocol:
+    https://developers.yubico.com/OATH/YKOATH_Protocol.html
+ */
+@interface YKFKeyOATHDeleteRequest: YKFKeyOATHRequest
+
+/*!
+ The credential for the request. The credential must provide at least the label and
+ period (TOTP) properties for the request to succeed. This value is set at initialization.
+ */
+@property (nonatomic, readonly, nonnull) YKFOATHCredential *credential;
+
+/*!
+ @method initWithCredential:
+ 
+ @abstract
+    The designated initializer for this type of request. The credential parameter is required.
+ 
+ @param credential
+    The credential for the request. The credential must be already added to the key when calling this request.
+ */
+- (nullable instancetype)initWithCredential:(nonnull YKFOATHCredential*)credential NS_DESIGNATED_INITIALIZER;
+
+/*
+ Not available: use [initWithCredential:].
+ */
+- (nonnull instancetype)init NS_UNAVAILABLE;
+
+@end
diff --git a/YubiKit/YubiKit/Sessions/Shared/Requests/OATH/YKFKeyOATHDeleteRequest.m b/YubiKit/YubiKit/Sessions/Shared/Requests/OATH/YKFKeyOATHDeleteRequest.m
new file mode 100755
index 000000000..943b1b5a9
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Requests/OATH/YKFKeyOATHDeleteRequest.m
@@ -0,0 +1,39 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "YKFKeyOATHDeleteRequest.h"
+#import "YKFOATHDeleteAPDU.h"
+#import "YKFAssert.h"
+#import "YKFKeyOATHRequest+Private.h"
+
+@interface YKFKeyOATHDeleteRequest()
+
+@property (nonatomic, readwrite) YKFOATHCredential *credential;
+
+@end
+
+@implementation YKFKeyOATHDeleteRequest
+
+- (nullable instancetype)initWithCredential:(nonnull YKFOATHCredential*)credential {
+    YKFAssertAbortInit(credential);
+    
+    self = [super init];
+    if (self) {
+        self.credential = credential;
+        self.apdu = [[YKFOATHDeleteAPDU alloc] initWithRequest:self];
+    }
+    return self;
+}
+
+@end
diff --git a/YubiKit/YubiKit/Sessions/Shared/Requests/OATH/YKFKeyOATHListRequest.h b/YubiKit/YubiKit/Sessions/Shared/Requests/OATH/YKFKeyOATHListRequest.h
new file mode 100755
index 000000000..a10ec585e
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Requests/OATH/YKFKeyOATHListRequest.h
@@ -0,0 +1,34 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+#import "YKFKeyOATHRequest.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/*!
+ @class YKFKeyOATHListRequest
+ 
+ @abstract
+    Request for listing all OATH credentials saved on the key. This request maps to the LIST command
+    from YOATH protocol: https://developers.yubico.com/OATH/YKOATH_Protocol.html
+ 
+ @note
+    This request does not calculate the credentials. To calculate credentials use either
+    Calculate or Calculate All requests.
+ */
+@interface YKFKeyOATHListRequest: YKFKeyOATHRequest
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/YubiKit/YubiKit/Sessions/Shared/Requests/OATH/YKFKeyOATHListRequest.m b/YubiKit/YubiKit/Sessions/Shared/Requests/OATH/YKFKeyOATHListRequest.m
new file mode 100755
index 000000000..4435b0ee2
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Requests/OATH/YKFKeyOATHListRequest.m
@@ -0,0 +1,29 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "YKFKeyOATHListRequest.h"
+#import "YKFKeyOATHRequest+Private.h"
+#import "YKFOATHListAPDU.h"
+
+@implementation YKFKeyOATHListRequest
+
+- (instancetype)init {
+    self = [super init];
+    if (self) {
+        self.apdu = [[YKFOATHListAPDU alloc] init];
+    }
+    return self;
+}
+
+@end
diff --git a/YubiKit/YubiKit/Sessions/Shared/Requests/OATH/YKFKeyOATHListResponse+Private.h b/YubiKit/YubiKit/Sessions/Shared/Requests/OATH/YKFKeyOATHListResponse+Private.h
new file mode 100755
index 000000000..4ce51f178
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Requests/OATH/YKFKeyOATHListResponse+Private.h
@@ -0,0 +1,21 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "YKFKeyOATHListResponse.h"
+
+@interface YKFKeyOATHListResponse()
+
+- (nullable instancetype)initWithKeyResponseData:(nonnull NSData *)responseData NS_DESIGNATED_INITIALIZER;
+
+@end
diff --git a/YubiKit/YubiKit/Sessions/Shared/Requests/OATH/YKFKeyOATHListResponse.h b/YubiKit/YubiKit/Sessions/Shared/Requests/OATH/YKFKeyOATHListResponse.h
new file mode 100755
index 000000000..8ab033fc7
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Requests/OATH/YKFKeyOATHListResponse.h
@@ -0,0 +1,39 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+/*!
+ @class YKFKeyOATHListResponse
+ 
+ @abstract
+    Response from List OATH credentials request.
+ */
+@interface YKFKeyOATHListResponse : NSObject
+
+/*!
+ The list of stored credentials (YKFOATHCredential type) on the key.
+ */
+@property (nonatomic, readonly, nonnull) NSArray *credentials;
+
+/*
+ Not available: the library will create a response as the result of the List request.
+ */
+- (instancetype)init NS_UNAVAILABLE;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/YubiKit/YubiKit/Sessions/Shared/Requests/OATH/YKFKeyOATHListResponse.m b/YubiKit/YubiKit/Sessions/Shared/Requests/OATH/YKFKeyOATHListResponse.m
new file mode 100755
index 000000000..ffcdfa9f1
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Requests/OATH/YKFKeyOATHListResponse.m
@@ -0,0 +1,130 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "YKFKeyOATHListResponse.h"
+#import "YKFKeyOATHListResponse+Private.h"
+#import "YKFOATHCredential.h"
+#import "YKFOATHCredential+Private.h"
+#import "YKFAssert.h"
+#import "YKFNSStringAdditions.h"
+#import "YKFNSDataAdditions+Private.h"
+
+static const int YKFKeyOATHListResponseNameTag = 0x72;
+
+@interface YKFKeyOATHListResponse()
+
+@property (nonatomic, readwrite) NSArray *credentials;
+
+@end
+
+@implementation YKFKeyOATHListResponse
+
+- (nullable instancetype)initWithKeyResponseData:(NSData *)responseData {
+    YKFAssertAbortInit(responseData);
+    
+    self = [super init];
+    if (self) {
+        BOOL success = [self readCredentialsFromData:responseData];        
+        YKFAbortInitWhen(!success)
+    }
+    return self;
+}
+
+- (BOOL)readCredentialsFromData:(NSData *)data {
+    if (!data.length) {
+        self.credentials = [[NSArray alloc] init];
+        return YES;
+    }
+    
+    NSUInteger readIndex = 0;
+    UInt8 *bytes = (UInt8 *)data.bytes;
+    NSMutableArray *parsedCredentials = [[NSMutableArray alloc] init];
+
+    while (readIndex < data.length && bytes[readIndex] == YKFKeyOATHListResponseNameTag) {
+        YKFOATHCredential *credential = [[YKFOATHCredential alloc] init];
+        
+        ++readIndex;
+        if (![data ykf_containsIndex:readIndex]) {
+            return NO;
+        }
+        
+        UInt8 nameLength = bytes[readIndex];
+        if (nameLength < 1) {
+            return NO; // Malformed response length
+        }
+        
+        ++readIndex;
+        if (![data ykf_containsIndex:readIndex]) {
+            return NO;
+        }
+        
+        UInt8 type = bytes[readIndex];
+        
+        if (type & YKFOATHCredentialTypeHOTP) {
+            credential.type = YKFOATHCredentialTypeHOTP;
+        } else if (type & YKFOATHCredentialTypeTOTP) {
+            credential.type = YKFOATHCredentialTypeTOTP;
+        } else {
+            return NO; // Malformed response otp type
+        }
+        
+        if (type & YKFOATHCredentialAlgorithmSHA1) {
+            credential.algorithm = YKFOATHCredentialAlgorithmSHA1;
+        } else if (type & YKFOATHCredentialAlgorithmSHA256) {
+            credential.algorithm = YKFOATHCredentialAlgorithmSHA256;
+        } else if (type & YKFOATHCredentialAlgorithmSHA512) {
+            credential.algorithm = YKFOATHCredentialAlgorithmSHA512;
+        } else {
+            return NO; // Malformed response algorithm
+        }
+        
+        ++readIndex;
+        if (![data ykf_containsIndex:readIndex]) {
+            return NO;
+        }
+        
+        UInt8 keyLength = nameLength - 1;
+        NSRange keyRange = NSMakeRange(readIndex, keyLength);
+        if (![data ykf_containsRange:keyRange]) {
+            return NO;
+        }
+        
+        NSData *key = [data subdataWithRange:keyRange];
+        NSString *keyString = [[NSString alloc] initWithData:key encoding:NSUTF8StringEncoding];
+        credential.key = keyString;
+        
+        // Parse the period, account and issuer from the key.
+        
+        NSUInteger period = 0;
+        NSString *issuer = nil;
+        NSString *account = nil;
+        NSString *label = nil;
+        
+        [keyString ykf_OATHKeyExtractPeriod:&period issuer:&issuer account:&account label:&label];
+        credential.period = period;
+        credential.issuer = issuer;
+        credential.account = account;
+        credential.label = label;
+        
+        [parsedCredentials addObject:credential];
+        
+        readIndex += keyLength;
+    }
+    
+    self.credentials = [parsedCredentials copy];
+    
+    return YES;
+}
+
+@end
diff --git a/YubiKit/YubiKit/Sessions/Shared/Requests/OATH/YKFKeyOATHPutRequest.h b/YubiKit/YubiKit/Sessions/Shared/Requests/OATH/YKFKeyOATHPutRequest.h
new file mode 100755
index 000000000..0d6d2cf3e
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Requests/OATH/YKFKeyOATHPutRequest.h
@@ -0,0 +1,51 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+#import "YKFOATHCredential.h"
+#import "YKFKeyOATHRequest.h"
+
+/*!
+ @class YKFKeyOATHPutRequest
+ 
+ @abstract
+    Request for adding a new OATH credential to the key or to override an existig credential
+    from the key. This request maps to the PUT command from YOATH protocol:
+    https://developers.yubico.com/OATH/YKOATH_Protocol.html
+ */
+@interface YKFKeyOATHPutRequest: YKFKeyOATHRequest
+
+/*!
+ The credential to be added to the key. This property is set at initialization.
+ */
+@property (nonatomic, readonly, nonnull) YKFOATHCredential *credential;
+
+/*!
+ @method initWithCredential:
+ 
+ @abstract
+    The designated initializer for this type of request. The credential parameter is required.
+ 
+ @param credential
+    The credential to be added to the key. If a credential with the same type, account and issuer
+    exists on the key, the old credential is overriden.
+ */
+- (nullable instancetype)initWithCredential:(nonnull YKFOATHCredential *)credential NS_DESIGNATED_INITIALIZER;
+
+/*
+ Not available: use [initWithCredential:].
+ */
+- (nonnull instancetype)init NS_UNAVAILABLE;
+
+@end
diff --git a/YubiKit/YubiKit/Sessions/Shared/Requests/OATH/YKFKeyOATHPutRequest.m b/YubiKit/YubiKit/Sessions/Shared/Requests/OATH/YKFKeyOATHPutRequest.m
new file mode 100755
index 000000000..218a79f99
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Requests/OATH/YKFKeyOATHPutRequest.m
@@ -0,0 +1,44 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "YKFKeyOATHPutRequest.h"
+#import "YKFOATHPutAPDU.h"
+#import "YKFAssert.h"
+#import "YKFKeyOATHRequest+Private.h"
+
+@interface YKFKeyOATHPutRequest()
+
+@property (nonatomic, readwrite) YKFOATHCredential *credential;
+
+@end
+
+@implementation YKFKeyOATHPutRequest
+
+- (instancetype)initWithCredential:(YKFOATHCredential *)credential {
+    YKFAssertAbortInit(credential);
+    
+    self = [super init];
+    if (self) {
+        YKFAssertAbortInit(credential.label.length);
+        YKFAssertAbortInit(credential.secret.length);
+        YKFAssertAbortInit(credential.type != YKFOATHCredentialTypeUnknown);
+        YKFAssertAbortInit(credential.algorithm != YKFOATHCredentialAlgorithmUnknown);
+                
+        self.credential = credential;
+        self.apdu = [[YKFOATHPutAPDU alloc] initWithRequest:self];
+    }
+    return self;
+}
+
+@end
diff --git a/YubiKit/YubiKit/Sessions/Shared/Requests/OATH/YKFKeyOATHRequest+Private.h b/YubiKit/YubiKit/Sessions/Shared/Requests/OATH/YKFKeyOATHRequest+Private.h
new file mode 100755
index 000000000..6376d5940
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Requests/OATH/YKFKeyOATHRequest+Private.h
@@ -0,0 +1,27 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+#import "YKFKeyOATHRequest.h"
+#import "YKFAPDU.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface YKFKeyOATHRequest()
+
+@property (nonatomic) YKFAPDU *apdu;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/YubiKit/YubiKit/Sessions/Shared/Requests/OATH/YKFKeyOATHRequest.h b/YubiKit/YubiKit/Sessions/Shared/Requests/OATH/YKFKeyOATHRequest.h
new file mode 100755
index 000000000..1778d94aa
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Requests/OATH/YKFKeyOATHRequest.h
@@ -0,0 +1,30 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+#import "YKFKeyRequest.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/*!
+ @class YKFKeyOATHRequest
+ 
+ @abstract
+    Base clase for all OATH requests. Use the subclasses of this type  for sending specific
+    OATH requests to the key.
+ */
+@interface YKFKeyOATHRequest: YKFKeyRequest
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/YubiKit/YubiKit/Sessions/Shared/Requests/OATH/YKFKeyOATHRequest.m b/YubiKit/YubiKit/Sessions/Shared/Requests/OATH/YKFKeyOATHRequest.m
new file mode 100755
index 000000000..09c1dfa72
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Requests/OATH/YKFKeyOATHRequest.m
@@ -0,0 +1,19 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "YKFKeyOATHRequest.h"
+#import "YKFKeyOATHRequest+Private.h"
+
+@implementation YKFKeyOATHRequest
+@end
diff --git a/YubiKit/YubiKit/Sessions/Shared/Requests/OATH/YKFKeyOATHResetRequest.h b/YubiKit/YubiKit/Sessions/Shared/Requests/OATH/YKFKeyOATHResetRequest.h
new file mode 100755
index 000000000..f98f2d15b
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Requests/OATH/YKFKeyOATHResetRequest.h
@@ -0,0 +1,36 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+#import "YKFKeyOATHRequest.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/*!
+ @class YKFKeyOATHResetRequest
+ 
+ @abstract
+    Request for resetting the OATH appllication on the key. This request maps to the RESET command
+    from YOATH protocol: https://developers.yubico.com/OATH/YKOATH_Protocol.html
+ 
+ @discussion
+    This request is destructive and removes all stored credentials from the key. The reset
+    command does not require validation if a PIN was set on the OATH application
+    from the key. If the OATH application is password protected, the reset command will remove
+    the authentication as well.
+ */
+@interface YKFKeyOATHResetRequest: YKFKeyOATHRequest
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/YubiKit/YubiKit/Sessions/Shared/Requests/OATH/YKFKeyOATHResetRequest.m b/YubiKit/YubiKit/Sessions/Shared/Requests/OATH/YKFKeyOATHResetRequest.m
new file mode 100755
index 000000000..c9daaed9d
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Requests/OATH/YKFKeyOATHResetRequest.m
@@ -0,0 +1,29 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "YKFKeyOATHResetRequest.h"
+#import "YKFKeyOATHRequest+Private.h"
+#import "YKFOATHResetAPDU.h"
+
+@implementation YKFKeyOATHResetRequest
+
+- (instancetype)init {
+    self = [super init];
+    if (self) {
+        self.apdu = [[YKFOATHResetAPDU alloc] init];
+    }
+    return self;
+}
+
+@end
diff --git a/YubiKit/YubiKit/Sessions/Shared/Requests/OATH/YKFKeyOATHSelectApplicationResponse.h b/YubiKit/YubiKit/Sessions/Shared/Requests/OATH/YKFKeyOATHSelectApplicationResponse.h
new file mode 100755
index 000000000..9b59271cb
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Requests/OATH/YKFKeyOATHSelectApplicationResponse.h
@@ -0,0 +1,31 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+#import "YKFOATHCredential.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface YKFKeyOATHSelectApplicationResponse : NSObject
+
+@property (nonatomic, readonly) NSData *selectID;
+@property (nonatomic, readonly, nullable) NSData *challenge;
+@property (nonatomic, assign, readonly) YKFOATHCredentialAlgorithm algorithm;
+
+- (nullable instancetype)initWithResponseData:(NSData *)responseData NS_DESIGNATED_INITIALIZER;
+- (instancetype)init NS_UNAVAILABLE;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/YubiKit/YubiKit/Sessions/Shared/Requests/OATH/YKFKeyOATHSelectApplicationResponse.m b/YubiKit/YubiKit/Sessions/Shared/Requests/OATH/YKFKeyOATHSelectApplicationResponse.m
new file mode 100755
index 000000000..e8f029b99
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Requests/OATH/YKFKeyOATHSelectApplicationResponse.m
@@ -0,0 +1,102 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "YKFKeyOATHSelectApplicationResponse.h"
+#import "YKFAssert.h"
+#import "YKFNSDataAdditions+Private.h"
+
+typedef NS_ENUM(NSUInteger, YKFKeyOATHSelectApplicationResponseTag) {
+    YKFKeyOATHSelectApplicationResponseTagName = 0x71,
+    YKFKeyOATHSelectApplicationResponseTagVersion = 0x79,
+    YKFKeyOATHSelectApplicationResponseTagChallenge = 0x74,
+    YKFKeyOATHSelectApplicationResponseTagAlgorithm = 0x7B
+};
+
+@interface YKFKeyOATHSelectApplicationResponse()
+
+@property (nonatomic, readwrite) NSData *selectID;
+@property (nonatomic, readwrite) NSData *challenge;
+@property (nonatomic, assign, readwrite) YKFOATHCredentialAlgorithm algorithm;
+
+@end
+
+@implementation YKFKeyOATHSelectApplicationResponse
+
+- (nullable instancetype)initWithResponseData:(NSData *)responseData {
+    YKFAssertAbortInit(responseData.length);
+    
+    self = [super init];
+    if (self) {
+        UInt8 *bytes = (UInt8 *)responseData.bytes;
+        NSUInteger readIndex = 0;
+        
+        UInt8 versionTag = bytes[readIndex];
+        YKFAssertAbortInit(versionTag == YKFKeyOATHSelectApplicationResponseTagVersion);
+        
+        ++readIndex;
+        YKFAssertAbortInit([responseData ykf_containsIndex:readIndex]);
+        
+        UInt8 lengthOfVersion = bytes[readIndex];
+        YKFAssertAbortInit(lengthOfVersion > 0);
+        
+        readIndex += lengthOfVersion + 1;
+        YKFAssertAbortInit([responseData ykf_containsIndex:readIndex]);
+        
+        UInt8 nameTag = bytes[readIndex];
+        YKFAssertAbortInit(nameTag == YKFKeyOATHSelectApplicationResponseTagName);
+        
+        ++readIndex;
+        YKFAssertAbortInit([responseData ykf_containsIndex:readIndex]);
+        
+        UInt8 lengthOfName = bytes[readIndex];
+        YKFAssertAbortInit(lengthOfName > 0);
+        
+        ++readIndex;
+        NSRange nameRange = NSMakeRange(readIndex, lengthOfName);
+        YKFAssertAbortInit([responseData ykf_containsRange:nameRange]);
+        
+        NSData *nameData = [responseData subdataWithRange:nameRange];
+        self.selectID = nameData;
+        
+        readIndex += lengthOfName;
+        
+        // Challenge is present
+        if (readIndex < responseData.length) {
+            UInt8 challengeTag = bytes[readIndex];
+            YKFAssertAbortInit(challengeTag == YKFKeyOATHSelectApplicationResponseTagChallenge);
+            
+            ++readIndex;
+            YKFAssertAbortInit([responseData ykf_containsIndex:readIndex]);
+            UInt8 challengeLength = bytes[readIndex];
+            YKFAssertAbortInit(challengeLength > 0);
+            
+            ++readIndex;
+            NSRange challengeRange = NSMakeRange(readIndex, challengeLength);
+            YKFAssertAbortInit([responseData ykf_containsRange:challengeRange]);
+            self.challenge = [responseData subdataWithRange:NSMakeRange(readIndex, challengeLength)];
+            
+            readIndex += challengeLength;
+            YKFAssertAbortInit([responseData ykf_containsIndex:readIndex]);
+            UInt8 algorithmTag = bytes[readIndex];
+            YKFAssertAbortInit(algorithmTag == YKFKeyOATHSelectApplicationResponseTagAlgorithm);
+            
+            readIndex += 2; // 1 byte is length which is always 1
+            YKFAssertAbortInit([responseData ykf_containsIndex:readIndex]);
+            self.algorithm = bytes[readIndex];
+        }
+    }
+    return self;
+}
+
+@end
diff --git a/YubiKit/YubiKit/Sessions/Shared/Requests/OATH/YKFKeyOATHSetCodeRequest.h b/YubiKit/YubiKit/Sessions/Shared/Requests/OATH/YKFKeyOATHSetCodeRequest.h
new file mode 100755
index 000000000..f55c76267
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Requests/OATH/YKFKeyOATHSetCodeRequest.h
@@ -0,0 +1,50 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+#import "YKFKeyOATHRequest.h"
+
+/*!
+ @class YKFKeyOATHSetCodeRequest
+ 
+ @abstract
+    Request for setting a PIN on the OATH application from the key. This request maps
+    to the SET CODE command from YOATH protocol:
+    https://developers.yubico.com/OATH/YKOATH_Protocol.html
+ */
+@interface YKFKeyOATHSetCodeRequest: YKFKeyOATHRequest
+
+/*!
+ The password to set on the OATH application. This property is set at initialization.
+ */
+@property (nonatomic, readonly, nonnull) NSString *password;
+
+/*!
+ @method initWithPassword:
+ 
+ @abstract
+    The designated initializer for this type of request. The password parameter is required.
+ 
+ @param password
+    The password to set on the OATH application. The password can be an empty string. If the
+    password is an empty string, the authentication will be removed.
+ */
+- (nullable instancetype)initWithPassword:(nonnull NSString *)password NS_DESIGNATED_INITIALIZER;
+
+/*
+ Not available: use [initWithPassword:].
+ */
+- (nonnull instancetype)init NS_UNAVAILABLE;
+
+@end
diff --git a/YubiKit/YubiKit/Sessions/Shared/Requests/OATH/YKFKeyOATHSetCodeRequest.m b/YubiKit/YubiKit/Sessions/Shared/Requests/OATH/YKFKeyOATHSetCodeRequest.m
new file mode 100755
index 000000000..fe866cd15
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Requests/OATH/YKFKeyOATHSetCodeRequest.m
@@ -0,0 +1,37 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "YKFKeyOATHSetCodeRequest.h"
+#import "YKFAssert.h"
+
+@interface YKFKeyOATHSetCodeRequest()
+
+@property (nonatomic, readwrite) NSString *password;
+
+@end
+
+@implementation YKFKeyOATHSetCodeRequest
+
+- (instancetype)initWithPassword:(nonnull NSString *)password {
+    YKFAssertAbortInit(password);
+    
+    self = [super init];
+    if (self) {
+        self.password = password;
+        // Note: APDU is not set yet. It will be set after the salt is received as a result of a select application request.
+    }
+    return self;
+}
+
+@end
diff --git a/YubiKit/YubiKit/Sessions/Shared/Requests/OATH/YKFKeyOATHValidateRequest.h b/YubiKit/YubiKit/Sessions/Shared/Requests/OATH/YKFKeyOATHValidateRequest.h
new file mode 100755
index 000000000..722d2b155
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Requests/OATH/YKFKeyOATHValidateRequest.h
@@ -0,0 +1,50 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+#import "YKFKeyOATHRequest.h"
+
+/*!
+ @class YKFKeyOATHValidateRequest
+ 
+ @abstract
+    Request for authehticating on the OATH application from the key. This request maps to the
+    VALIDATE command from YOATH protocol:
+    https://developers.yubico.com/OATH/YKOATH_Protocol.html
+ */
+@interface YKFKeyOATHValidateRequest: YKFKeyOATHRequest
+
+/*!
+ The password to validate against the OATH application. This property is set at initialization.
+ */
+@property (nonatomic, readonly, nonnull) NSString *password;
+
+/*!
+ @method initWithPassword:
+ 
+ @abstract
+    The designated initializer for this type of request. The password parameter is required.
+ 
+ @param password
+    The password to authenticate on the key OATH application. The password may not be an
+    empty string.
+ */
+- (nullable instancetype)initWithPassword:(nonnull NSString *)password NS_DESIGNATED_INITIALIZER;
+
+/*
+ Not available: use [initWithPassword:].
+ */
+- (nonnull instancetype)init NS_UNAVAILABLE;
+
+@end
diff --git a/YubiKit/YubiKit/Sessions/Shared/Requests/OATH/YKFKeyOATHValidateRequest.m b/YubiKit/YubiKit/Sessions/Shared/Requests/OATH/YKFKeyOATHValidateRequest.m
new file mode 100755
index 000000000..7fd71aeb0
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Requests/OATH/YKFKeyOATHValidateRequest.m
@@ -0,0 +1,37 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "YKFKeyOATHValidateRequest.h"
+#import "YKFAssert.h"
+
+@interface YKFKeyOATHValidateRequest()
+
+@property (nonatomic, readwrite) NSString *password;
+
+@end
+
+@implementation YKFKeyOATHValidateRequest
+
+- (instancetype)initWithPassword:(nonnull NSString *)password {
+    YKFAssertAbortInit(password.length);
+    
+    self = [super init];
+    if (self) {
+        self.password = password;
+        // Note: APDU is not set yet. It will be set after the salt is received as a result of a select application request.
+    }
+    return self;
+}
+
+@end
diff --git a/YubiKit/YubiKit/Sessions/Shared/Requests/OATH/YKFKeyOATHValidateResponse.h b/YubiKit/YubiKit/Sessions/Shared/Requests/OATH/YKFKeyOATHValidateResponse.h
new file mode 100755
index 000000000..b171bb5a0
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Requests/OATH/YKFKeyOATHValidateResponse.h
@@ -0,0 +1,24 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+
+@interface YKFKeyOATHValidateResponse: NSObject
+
+@property (nonatomic, readonly, nonnull) NSData *response;
+
+- (nullable instancetype)initWithResponseData:(nonnull NSData *)responseData NS_DESIGNATED_INITIALIZER;
+- (nonnull instancetype)init NS_UNAVAILABLE;
+
+@end
diff --git a/YubiKit/YubiKit/Sessions/Shared/Requests/OATH/YKFKeyOATHValidateResponse.m b/YubiKit/YubiKit/Sessions/Shared/Requests/OATH/YKFKeyOATHValidateResponse.m
new file mode 100755
index 000000000..5337652e8
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Requests/OATH/YKFKeyOATHValidateResponse.m
@@ -0,0 +1,54 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "YKFKeyOATHValidateResponse.h"
+#import "YKFAssert.h"
+#import "YKFNSDataAdditions+Private.h"
+
+static const UInt8 YKFKeyOATHValidateResponseTag = 0x75;
+
+@interface YKFKeyOATHValidateResponse()
+
+@property (nonatomic, readwrite, nonnull) NSData *response;
+
+@end
+
+@implementation YKFKeyOATHValidateResponse
+
+- (instancetype)initWithResponseData:(NSData *)responseData {
+    YKFAssertAbortInit(responseData.length);
+    
+    self = [super init];
+    if (self) {
+        UInt8 *bytes = (UInt8 *)responseData.bytes;
+        NSUInteger readIndex = 0;
+        
+        UInt8 responseTag = bytes[readIndex];
+        YKFAssertAbortInit(responseTag == YKFKeyOATHValidateResponseTag);
+        
+        ++readIndex;
+        YKFAssertAbortInit([responseData ykf_containsIndex:readIndex]);
+        
+        UInt8 responseLength = bytes[readIndex];
+        YKFAssertAbortInit(responseLength > 0);
+        
+        ++readIndex;
+        NSRange responseRange = NSMakeRange(readIndex, responseLength);
+        YKFAssertAbortInit([responseData ykf_containsRange:responseRange]);
+        self.response = [responseData subdataWithRange:responseRange];
+    }
+    return self;
+}
+
+@end
diff --git a/YubiKit/YubiKit/Sessions/Shared/Requests/U2F/YKFKeyU2FRegisterRequest.h b/YubiKit/YubiKit/Sessions/Shared/Requests/U2F/YKFKeyU2FRegisterRequest.h
new file mode 100755
index 000000000..da8e65a77
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Requests/U2F/YKFKeyU2FRegisterRequest.h
@@ -0,0 +1,70 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+#import "YKFKeyU2FRequest.h"
+
+/*!
+ @class YKFKeyU2FRegisterRequest
+ 
+ @abstract
+    Data model which contains the required information by the key to perform an U2F registration request.
+ */
+@interface YKFKeyU2FRegisterRequest: YKFKeyU2FRequest
+
+/*!
+ @property challenge
+ 
+ @abstract
+    The U2F registration challenge which is usually received from the authentication server.
+ @discussion
+    Registration challenge message format as defined by the FIDO Alliance specifications
+    ---
+    https://fidoalliance.org/specs/fido-u2f-v1.2-ps-20170411/fido-u2f-raw-message-formats-v1.2-ps-20170411.html#registration-messages
+ */
+@property (nonatomic, readonly, nonnull) NSString *challenge;
+
+/*!
+ @property appId
+ 
+ @abstract
+    The application ID (sometimes reffered as origin or facet ID) as described by the U2F standard.
+    This is usually a domain which belongs to the application.
+ @discussion
+    Documentation for the application ID format
+    ---
+    https://developers.yubico.com/U2F/App_ID.html
+ */
+@property (nonatomic, readonly, nonnull) NSString *appId;
+
+/*!
+ @method initWithChallenge:appId:
+ 
+ @abstract
+    The designated initializer for this type of request. Both challenge and appId parameters are required to properly
+    perfrom a registration request with the key.
+ 
+ @param challenge
+    See the challenge property documentation on YKFKeyU2FRegisterRequest.
+ @param appId
+    See the appId property documentation on YKFKeyU2FRegisterRequest.
+ */
+- (nullable instancetype)initWithChallenge:(nonnull NSString *)challenge appId:(nonnull NSString *)appId NS_DESIGNATED_INITIALIZER;
+
+/*
+ Not available: use the designated initializer.
+ */
+- (nonnull instancetype)init NS_UNAVAILABLE;
+
+@end
diff --git a/YubiKit/YubiKit/Sessions/Shared/Requests/U2F/YKFKeyU2FRegisterRequest.m b/YubiKit/YubiKit/Sessions/Shared/Requests/U2F/YKFKeyU2FRegisterRequest.m
new file mode 100755
index 000000000..0080c5406
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Requests/U2F/YKFKeyU2FRegisterRequest.m
@@ -0,0 +1,57 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "YKFKeyU2FRegisterRequest.h"
+#import "YKFU2FRegisterAPDU.h"
+#import "YKFAssert.h"
+#import "YKFKeyU2FRequest+Private.h"
+
+/*
+ DOMString typ as defined in FIDO U2F Raw Message Format
+ https://fidoalliance.org/specs/u2f-specs-1.0-bt-nfc-id-amendment/fido-u2f-raw-message-formats.html
+ */
+static NSString* const U2FClientDataTypeRegistration = @"navigator.id.finishEnrollment";
+
+/*
+ Client data as defined in FIDO U2F Raw Message Format
+ https://fidoalliance.org/specs/u2f-specs-1.0-bt-nfc-id-amendment/fido-u2f-raw-message-formats.html
+ Note: The "cid_pubkey" is missing in this case since the TLS stack on iOS does not support channel id.
+ */
+static NSString* const U2FClientDataTemplate = @"\{\"typ\":\"%@\",\"challenge\":\"%@\",\"origin\":\"%@\"}";
+
+@interface YKFKeyU2FRegisterRequest()
+
+@property (nonatomic, readwrite) NSString *challenge;
+@property (nonatomic, readwrite) NSString *appId;
+
+@end
+
+@implementation YKFKeyU2FRegisterRequest
+
+- (instancetype)initWithChallenge:(NSString *)challenge appId:(NSString *)appId {
+    YKFAssertAbortInit(challenge);
+    YKFAssertAbortInit(appId);
+    
+    self = [super init];
+    if (self) {
+        self.challenge = challenge;
+        self.appId = appId;
+        self.clientData = [[NSString alloc] initWithFormat:U2FClientDataTemplate, U2FClientDataTypeRegistration, self.challenge, self.appId];
+        
+        self.apdu = [[YKFU2FRegisterAPDU alloc] initWithU2FRegisterRequest:self];
+    }
+    return self;
+}
+
+@end
diff --git a/YubiKit/YubiKit/Sessions/Shared/Requests/U2F/YKFKeyU2FRegisterResponse+Private.h b/YubiKit/YubiKit/Sessions/Shared/Requests/U2F/YKFKeyU2FRegisterResponse+Private.h
new file mode 100755
index 000000000..446d7e753
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Requests/U2F/YKFKeyU2FRegisterResponse+Private.h
@@ -0,0 +1,25 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+#import "YKFKeyU2FRegisterResponse.h"
+
+@interface YKFKeyU2FRegisterResponse()
+
+/*
+ Hidden designated initializer for the response which is used only by YubiKit.
+ */
+- (nullable instancetype)initWithClientData:(nonnull NSString *)clientData registrationData:(nonnull NSData *)registrationResponse NS_DESIGNATED_INITIALIZER;
+
+@end
diff --git a/YubiKit/YubiKit/Sessions/Shared/Requests/U2F/YKFKeyU2FRegisterResponse.h b/YubiKit/YubiKit/Sessions/Shared/Requests/U2F/YKFKeyU2FRegisterResponse.h
new file mode 100755
index 000000000..38588bd6b
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Requests/U2F/YKFKeyU2FRegisterResponse.h
@@ -0,0 +1,61 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+/*!
+ @class YKFKeyU2FRegisterResponse
+ 
+ @abstract
+    Data model which contains the result of a registration request sent to the key.
+ @discussion
+    The application should not process the content of this response, like parsing it, unless there is a
+    good reason for it. The result is usually sent back to the authentication server for validation.
+ */
+@interface YKFKeyU2FRegisterResponse : NSObject
+
+/*!
+ @property clientData
+ 
+ @abstract
+    The clientData data structure as defined by the U2F standard.
+ @discussion
+    The full specification of the client data format as defined by the FIDO Alliance specifications
+    ---
+    https://fidoalliance.org/specs/fido-u2f-v1.2-ps-20170411/fido-u2f-raw-message-formats-v1.2-ps-20170411.html#client-data
+ */
+@property (nonatomic, readonly) NSString *clientData;
+
+/*!
+ @property registrationData
+ 
+ @abstract
+    The clientData data structure as defined by the U2F standard.
+ @discussion
+    The full specification of the registration data format as defined by the FIDO Alliance specifications
+    ---
+    https://fidoalliance.org/specs/fido-u2f-v1.2-ps-20170411/fido-u2f-raw-message-formats-v1.2-ps-20170411.html#registration-messages
+ */
+@property (nonatomic, readonly) NSData *registrationData;
+
+/*
+ Not available: this type of response should be created only by the library.
+ */
+- (instancetype)init NS_UNAVAILABLE;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/YubiKit/YubiKit/Sessions/Shared/Requests/U2F/YKFKeyU2FRegisterResponse.m b/YubiKit/YubiKit/Sessions/Shared/Requests/U2F/YKFKeyU2FRegisterResponse.m
new file mode 100755
index 000000000..55196b538
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Requests/U2F/YKFKeyU2FRegisterResponse.m
@@ -0,0 +1,39 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "YKFKeyU2FRegisterResponse.h"
+#import "YKFAssert.h"
+
+@interface YKFKeyU2FRegisterResponse()
+
+@property (nonatomic, readwrite) NSString *clientData;
+@property (nonatomic, readwrite) NSData *registrationData;
+
+@end
+
+@implementation YKFKeyU2FRegisterResponse
+
+- (instancetype)initWithClientData:(NSString *)clientData registrationData:(NSData *)registrationData {
+    YKFAssertAbortInit(clientData);
+    YKFAssertAbortInit(registrationData);
+    
+    self = [super init];
+    if (self) {
+        self.clientData = clientData;
+        self.registrationData = registrationData;
+    }
+    return self;
+}
+
+@end
diff --git a/YubiKit/YubiKit/Sessions/Shared/Requests/U2F/YKFKeyU2FRequest+Private.h b/YubiKit/YubiKit/Sessions/Shared/Requests/U2F/YKFKeyU2FRequest+Private.h
new file mode 100755
index 000000000..72e54e29f
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Requests/U2F/YKFKeyU2FRequest+Private.h
@@ -0,0 +1,33 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+#import "YKFKeyU2FRequest.h"
+#import "YKFAPDU.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface YKFKeyU2FRequest()
+
+@property (nonatomic, assign) int retries;
+@property (nonatomic, assign, readonly) BOOL shouldRetry;
+@property (nonatomic, assign, readonly) NSTimeInterval retryTimeInterval;
+
+@property (nonatomic) NSString *clientData;
+
+@property (nonatomic) YKFAPDU *apdu;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/YubiKit/YubiKit/Sessions/Shared/Requests/U2F/YKFKeyU2FRequest.h b/YubiKit/YubiKit/Sessions/Shared/Requests/U2F/YKFKeyU2FRequest.h
new file mode 100755
index 000000000..95cf39232
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Requests/U2F/YKFKeyU2FRequest.h
@@ -0,0 +1,30 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+#import "YKFKeyRequest.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/*!
+ @class YKFKeyU2FRequest
+ 
+ @abstract
+    Base clase for key U2F requests. Use YKFKeyU2FRegisterRequest and YKFKeyU2FSignRequest for sending
+    specific requests to the key.
+ */
+@interface YKFKeyU2FRequest: YKFKeyRequest
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/YubiKit/YubiKit/Sessions/Shared/Requests/U2F/YKFKeyU2FRequest.m b/YubiKit/YubiKit/Sessions/Shared/Requests/U2F/YKFKeyU2FRequest.m
new file mode 100755
index 000000000..9f3785b18
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Requests/U2F/YKFKeyU2FRequest.m
@@ -0,0 +1,31 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "YKFKeyU2FRequest.h"
+#import "YKFKeyU2FRequest+Private.h"
+
+static const int YKFKeyU2FRequestMaxRetries = 30; // times
+static const NSTimeInterval YKFKeyU2FRequestRetryTimeInterval = 0.5; // seconds
+
+@implementation YKFKeyU2FRequest
+
+- (BOOL)shouldRetry {
+    return self.retries <= YKFKeyU2FRequestMaxRetries;
+}
+
+- (NSTimeInterval)retryTimeInterval {
+    return YKFKeyU2FRequestRetryTimeInterval;
+}
+
+@end
diff --git a/YubiKit/YubiKit/Sessions/Shared/Requests/U2F/YKFKeyU2FSignRequest.h b/YubiKit/YubiKit/Sessions/Shared/Requests/U2F/YKFKeyU2FSignRequest.h
new file mode 100755
index 000000000..d997d0f1b
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Requests/U2F/YKFKeyU2FSignRequest.h
@@ -0,0 +1,84 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+#import "YKFKeyU2FRequest.h"
+
+/*!
+ @class YKFKeyU2FSignRequest
+ 
+ @abstract
+    Data model which contains the required information by the key to perform an U2F sign request.
+ */
+@interface YKFKeyU2FSignRequest : YKFKeyU2FRequest
+
+/*!
+ @property challenge
+ 
+ @abstract
+    The U2F authentication challenge which is usually received from the authentication server.
+ @discussion
+    Authentication challenge message format as defined by the FIDO Alliance specifications
+    ---
+    https://fidoalliance.org/specs/fido-u2f-v1.2-ps-20170411/fido-u2f-raw-message-formats-v1.2-ps-20170411.html#authentication-messages
+ */
+@property (nonatomic, readonly, nonnull) NSString *challenge;
+
+/*!
+ @property keyHandle
+ 
+ @abstract
+    The U2F authentication keyHandle which is usually received from the authentication server and used by
+    the hardware key to identify the required cryptographic key for signing.
+ @discussion
+    Format as defined by the FIDO Alliance specifications
+    ---
+    https://fidoalliance.org/specs/fido-u2f-v1.2-ps-20170411/fido-u2f-raw-message-formats-v1.2-ps-20170411.html#authentication-messages
+ */
+@property (nonatomic, readonly, nonnull) NSString *keyHandle;
+
+/*!
+ @property appId
+ 
+ @abstract
+    The application ID (sometimes reffered as origin or facet ID) as described by the U2F standard.
+    This is usually a domain which belongs to the application.
+ @discussion
+    Documentation for the application ID format
+    ---
+    https://developers.yubico.com/U2F/App_ID.html
+ */
+@property (nonatomic, readonly, nonnull) NSString *appId;
+
+/*!
+ @method initWithChallenge:keyHandle:appId:
+ 
+ @abstract
+    The designated initializer for this type of request. All parameters are required to properly perfrom a sign request with the key.
+ 
+ @param challenge
+    See the challenge property documentation on YKFKeyU2FSignRequest.
+ @param keyHandle
+    See the keyHandle property documentation on YKFKeyU2FSignRequest.
+ @param appId
+    See the appId property documentation on YKFKeyU2FSignRequest.
+ */
+- (nullable instancetype)initWithChallenge:(nonnull NSString *)challenge keyHandle:(nonnull NSString *)keyHandle appId:(nonnull NSString *)appId NS_DESIGNATED_INITIALIZER;
+
+/*
+ Not available: use the designated initializer.
+ */
+- (nonnull instancetype)init NS_UNAVAILABLE;
+
+@end
diff --git a/YubiKit/YubiKit/Sessions/Shared/Requests/U2F/YKFKeyU2FSignRequest.m b/YubiKit/YubiKit/Sessions/Shared/Requests/U2F/YKFKeyU2FSignRequest.m
new file mode 100755
index 000000000..1690e914a
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Requests/U2F/YKFKeyU2FSignRequest.m
@@ -0,0 +1,60 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "YKFKeyU2FSignRequest.h"
+#import "YKFU2FSignAPDU.h"
+#import "YKFAssert.h"
+#import "YKFKeyU2FRequest+Private.h"
+
+/*
+ DOMString typ as defined in FIDO U2F Raw Message Format
+ https://fidoalliance.org/specs/u2f-specs-1.0-bt-nfc-id-amendment/fido-u2f-raw-message-formats.html
+ */
+static NSString* const U2FClientDataTypeAuthentication = @"navigator.id.getAssertion";
+
+/*
+ Client data as defined in FIDO U2F Raw Message Format
+ https://fidoalliance.org/specs/u2f-specs-1.0-bt-nfc-id-amendment/fido-u2f-raw-message-formats.html
+ Note: The "cid_pubkey" is missing in this case since the TLS stack on iOS does not support channel id.
+ */
+static NSString* const U2FClientDataTypeTemplate = @"\{\"typ\":\"%@\",\"challenge\":\"%@\",\"origin\":\"%@\"}";
+
+@interface YKFKeyU2FSignRequest()
+
+@property (nonatomic, readwrite) NSString *challenge;
+@property (nonatomic, readwrite) NSString *keyHandle;
+@property (nonatomic, readwrite) NSString *appId;
+
+@end
+
+@implementation YKFKeyU2FSignRequest
+
+- (instancetype)initWithChallenge:(NSString *)challenge keyHandle:(NSString *)keyHandle appId:(NSString *)appId {
+    YKFAssertAbortInit(challenge);
+    YKFAssertAbortInit(keyHandle);
+    YKFAssertAbortInit(appId);
+    
+    self = [super init];
+    if (self) {
+        self.challenge = challenge;
+        self.keyHandle = keyHandle;        
+        self.appId = appId;        
+        self.clientData = [[NSString alloc] initWithFormat:U2FClientDataTypeTemplate, U2FClientDataTypeAuthentication, self.challenge, self.appId];
+        
+        self.apdu = [[YKFU2FSignAPDU alloc] initWithU2fSignRequest:self];
+    }
+    return self;
+}
+
+@end
diff --git a/YubiKit/YubiKit/Sessions/Shared/Requests/U2F/YKFKeyU2FSignResponse+Private.h b/YubiKit/YubiKit/Sessions/Shared/Requests/U2F/YKFKeyU2FSignResponse+Private.h
new file mode 100755
index 000000000..30383dd3e
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Requests/U2F/YKFKeyU2FSignResponse+Private.h
@@ -0,0 +1,24 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+
+@interface YKFKeyU2FSignResponse()
+
+/*
+ Hidden designated initializer for the response which is used only by YubiKit.
+ */
+- (nullable instancetype)initWithKeyHandle:(nonnull NSString *)keyHandle clientData:(nonnull NSString *)clientData signature:(nonnull NSData *)signature NS_DESIGNATED_INITIALIZER;
+
+@end
diff --git a/YubiKit/YubiKit/Sessions/Shared/Requests/U2F/YKFKeyU2FSignResponse.h b/YubiKit/YubiKit/Sessions/Shared/Requests/U2F/YKFKeyU2FSignResponse.h
new file mode 100755
index 000000000..52d73a51b
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Requests/U2F/YKFKeyU2FSignResponse.h
@@ -0,0 +1,73 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+/*!
+ @class YKFKeyU2FSignResponse
+ 
+ @abstract
+    Data model which contains the result of a sign request sent to the key.
+ @discussion
+    The application should not process the content of this response, like parsing it, unless there is a
+    good reason for it. The result is usually sent back to the authentication server for validation.
+ */
+@interface YKFKeyU2FSignResponse : NSObject
+
+/*!
+ @property keyHandle
+ 
+ @abstract
+    The keyHandle passed to the key when the sign operation was requested.
+ @discussion
+    Format as defined by the FIDO Alliance specifications
+    ---
+    https://fidoalliance.org/specs/fido-u2f-v1.2-ps-20170411/fido-u2f-raw-message-formats-v1.2-ps-20170411.html#authentication-messages
+ */
+@property (nonatomic, readonly) NSString *keyHandle;
+
+/*!
+ @property clientData
+ 
+ @abstract
+    The clientData data structure as defined by the U2F standard.
+ @discussion
+    The full specification of the client data format as defined by the FIDO Alliance specifications
+    ---
+    https://fidoalliance.org/specs/fido-u2f-v1.2-ps-20170411/fido-u2f-raw-message-formats-v1.2-ps-20170411.html#client-data
+ */
+@property (nonatomic, readonly) NSString *clientData;
+
+/*!
+ @property signature
+ 
+ @abstract
+    The signature produced by the key after the signing operation.
+ @discussion
+    The details for the signature format
+    ---
+    https://fidoalliance.org/specs/fido-u2f-v1.2-ps-20170411/fido-u2f-raw-message-formats-v1.2-ps-20170411.html#authentication-response-message-success
+ */
+@property (nonatomic, readonly) NSData *signature;
+
+/*
+ Not available: this type of response should be created only by the library.
+ */
+- (instancetype)init NS_UNAVAILABLE;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/YubiKit/YubiKit/Sessions/Shared/Requests/U2F/YKFKeyU2FSignResponse.m b/YubiKit/YubiKit/Sessions/Shared/Requests/U2F/YKFKeyU2FSignResponse.m
new file mode 100755
index 000000000..11200ceb7
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Requests/U2F/YKFKeyU2FSignResponse.m
@@ -0,0 +1,42 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "YKFKeyU2FSignResponse.h"
+#import "YKFAssert.h"
+
+@interface YKFKeyU2FSignResponse()
+
+@property (nonatomic, readwrite) NSString *keyHandle;
+@property (nonatomic, readwrite) NSString *clientData;
+@property (nonatomic, readwrite) NSData *signature;
+
+@end
+
+@implementation YKFKeyU2FSignResponse
+
+- (instancetype)initWithKeyHandle:(NSString *)keyHandle clientData:(NSString *)clientData signature:(NSData *)signature {
+    YKFAssertAbortInit(keyHandle);
+    YKFAssertAbortInit(clientData);
+    YKFAssertAbortInit(signature);
+    
+    self = [super init];
+    if (self) {
+        self.keyHandle = keyHandle;
+        self.clientData = clientData;
+        self.signature = signature;
+    }
+    return self;
+}
+
+@end
diff --git a/YubiKit/YubiKit/Sessions/Shared/Requests/YKFKeyRequest.h b/YubiKit/YubiKit/Sessions/Shared/Requests/YKFKeyRequest.h
new file mode 100755
index 000000000..442f4d00d
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Requests/YKFKeyRequest.h
@@ -0,0 +1,22 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface YKFKeyRequest: NSObject
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/YubiKit/YubiKit/Sessions/Shared/Requests/YKFKeyRequest.m b/YubiKit/YubiKit/Sessions/Shared/Requests/YKFKeyRequest.m
new file mode 100755
index 000000000..9c64e2853
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Requests/YKFKeyRequest.m
@@ -0,0 +1,18 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "YKFKeyRequest.h"
+
+@implementation YKFKeyRequest
+@end
diff --git a/YubiKit/YubiKit/Sessions/Shared/Services/FIDO2/CBOR/YKFCBORDecoder.h b/YubiKit/YubiKit/Sessions/Shared/Services/FIDO2/CBOR/YKFCBORDecoder.h
new file mode 100755
index 000000000..d6483fe07
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Services/FIDO2/CBOR/YKFCBORDecoder.h
@@ -0,0 +1,49 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+#import "YKFCBORType.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/*!
+ The interface for a CTAP2 CBOR decoder.
+ */
+@protocol YKFCBORDecoderProtocol<NSObject>
+
+/*!
+ @abstract
+    Decodes a CBOR type from an input stream.
+ @returns
+    The object or nil if the object could not be parsed.
+ */
++ (nullable id)decodeObjectFrom:(NSInputStream *)inputStream;
+
+/*!
+ @abstract
+    Converts a CBOR type to a foundation type object (e.g. YKFCBORArray -> NSArray).
+ @returns
+    The foudation type object (e.g. NSNumber, NSDictionary etc) or nil if the object could not be converted.
+ */
++ (nullable id)convertCBORObjectToFoundationType:(id)cborObject;
+
+@end
+
+/*!
+ CTAP2 CBOR decoder.
+ */
+@interface YKFCBORDecoder: NSObject<YKFCBORDecoderProtocol>
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/YubiKit/YubiKit/Sessions/Shared/Services/FIDO2/CBOR/YKFCBORDecoder.m b/YubiKit/YubiKit/Sessions/Shared/Services/FIDO2/CBOR/YKFCBORDecoder.m
new file mode 100755
index 000000000..636789c71
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Services/FIDO2/CBOR/YKFCBORDecoder.m
@@ -0,0 +1,329 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "YKFCBORDecoder.h"
+#import "YKFCBORTag.h"
+#import "YKFAssert.h"
+
+@interface NSInputStream(YKFCBORDecoder)
+
+- (UInt8)dequeueHead:(BOOL *)error;
+- (NSData *)dequeueBytes:(NSUInteger)numberOfBytes;
+
+@end
+
+@implementation YKFCBORDecoder
+
++ (nullable id)decodeObjectFrom:(NSInputStream *)inputStream {
+    YKFAssertReturnValue(inputStream, @"CBOR - Decoding input stream is nil.", nil);
+    YKFAssertReturnValue(inputStream.streamStatus == NSStreamStatusOpen, @"CBOR - Decoding input stream not opened.", nil);
+    YKFAssertReturnValue(inputStream.hasBytesAvailable, @"CBOR - Cannot decode from empty input stream.", nil);
+
+    BOOL headReadingError = NO;
+    UInt8 head = [inputStream dequeueHead:&headReadingError];
+    YKFAssertReturnValue(!headReadingError, @"CBOR - Decoding input stream error.", nil);
+
+    // MT 0,1: Integer (Positive || Negative)
+    if (head >= 0x00 && head <= 0x3B) {
+        return [self decodeIntegerFromInputStream:inputStream header:head];
+    }
+    // MT 2: Byte String
+    if (head >= 0x40 && head <= 0x5B) {
+        return [self decodeByteStringFromInputStream:inputStream header:head];
+    }
+    // MT 3: Text String
+    if (head >= 0x60 && head <= 0x7B) {
+        return [self decodeTextStringFromInputStream:inputStream header:head];
+    }
+    // MT 4: Array
+    if (head >= 0x80 && head <= 0x9B) {
+        return [self decodeArrayFromInputStream:inputStream header:head];
+    }
+    // MT 5: Map
+    if (head >= 0xA0 && head <= 0xBB) {
+        return [self decodeMapFromInputStream:inputStream header:head];
+    }
+    // Bool
+    if (head == 0xF4 || head == 0xF5) {
+        return [self decodeBoolFromInputStream:inputStream header:head];
+    }
+    
+    return nil;
+}
+
+#pragma mark - Helpers
+
++ (YKFCBORInteger *)decodeIntegerFromInputStream:(NSInputStream *)inputStream header:(UInt8)header {
+    NSData *integerData = nil;
+    
+    if ((header >= 0x00 && header <= 0x17) || (header >= 0x20 && header <= 0x37)) {
+        integerData = [self readObjectFromInputStream:inputStream header:header size:0];
+    }
+    else if (header == 0x18 || header == 0x38) {
+        integerData = [self readObjectFromInputStream:inputStream header:header size:1];
+    }
+    else if (header == 0x19 || header == 0x39) {
+        integerData = [self readObjectFromInputStream:inputStream header:header size:2];
+    }
+    else if (header == 0x1A || header == 0x3A) {
+        integerData = [self readObjectFromInputStream:inputStream header:header size:4];
+    }
+    else if (header == 0x1B || header == 0x3B) {
+        integerData = [self readObjectFromInputStream:inputStream header:header size:8];
+    }
+    YKFAssertReturnValue(integerData, @"CBOR - Decoding input stream error. Could not read Integer.", nil);
+    
+    return [self decodeInteger:integerData];
+}
+
++ (YKFCBORByteString *)decodeByteStringFromInputStream:(NSInputStream *)inputStream header:(UInt8)header {
+    UInt8 integerHeader = header & ~YKFCBORByteStringTagMask; // Remove MT mask from the number of elements.
+    YKFCBORInteger *numberOfBytes = [self decodeIntegerFromInputStream:inputStream header:integerHeader];
+    
+    NSData *byteStringData = [self readObjectFromInputStream:inputStream header:header size:numberOfBytes.value];
+    YKFAssertReturnValue(byteStringData, @"CBOR - Decoding input stream error. Could not read Byte String.", nil);
+    
+    return [self decodeByteString:byteStringData];
+}
+
++ (YKFCBORTextString *)decodeTextStringFromInputStream:(NSInputStream *)inputStream header:(UInt8)header {
+    UInt8 integerHeader = header & ~YKFCBORTextStringTagMask; // Remove MT mask from the number of elements.
+    YKFCBORInteger *numberOfBytes = [self decodeIntegerFromInputStream:inputStream header:integerHeader];
+    
+    NSData *textStringData = [self readObjectFromInputStream:inputStream header:header size:numberOfBytes.value];
+    YKFAssertReturnValue(textStringData, @"CBOR - Decoding input stream error. Could not read Text String.", nil);
+    
+    return [self decodeTextString:textStringData];
+}
+
++ (YKFCBORArray *)decodeArrayFromInputStream:(NSInputStream *)inputStream header:(UInt8)header {
+    UInt8 integerHeader = header & ~YKFCBORArrayTagMask; // Remove MT mask from the number of elements.
+    YKFCBORInteger *numberOfElements = [self decodeIntegerFromInputStream:inputStream header:integerHeader];
+    YKFAssertReturnValue(numberOfElements, @"CBOR - Decoding input stream error. Could not decode array count.", nil);
+    
+    NSMutableArray *array = [[NSMutableArray alloc] initWithCapacity:numberOfElements.value];
+    for (int i = 0; i < numberOfElements.value; ++i) {
+        id element = [self decodeObjectFrom:inputStream];
+        YKFAssertReturnValue(element, @"CBOR - Decoding input stream error. Could not decode array element.", nil);
+        [array addObject:element];
+    }
+
+    return YKFCBORArray([array copy]);
+}
+
++ (YKFCBORMap *)decodeMapFromInputStream:(NSInputStream *)inputStream header:(UInt8)header {
+    UInt8 integerHeader = header & ~YKFCBORMapTagMask; // Remove MT mask from the number of elements.
+    YKFCBORInteger *numberOfPairs = [self decodeIntegerFromInputStream:inputStream header:integerHeader];
+    YKFAssertReturnValue(numberOfPairs, @"CBOR - Decoding input stream error. Could not decode number of map pairs.", nil);
+    
+    NSMutableDictionary *dictionary = [[NSMutableDictionary alloc] initWithCapacity:numberOfPairs.value];
+    for (int i = 0; i < numberOfPairs.value; ++i) {
+        id key = [self decodeObjectFrom:inputStream];
+        YKFAssertReturnValue(key, @"CBOR - Decoding input stream error. Could not decode map key.", nil);
+
+        id value = [self decodeObjectFrom:inputStream];
+        YKFAssertReturnValue(value, @"CBOR - Decoding input stream error. Could not decode map value.", nil);
+        
+        // Security check: Verify if the key already exists in the decoded map. A map with duplicated keys is invalid.
+        YKFAssertReturnValue(!dictionary[key], @"CBOR - The key already exists in the map.", nil);
+        dictionary[key] = value;
+    }
+    
+    return YKFCBORMap([dictionary copy]);
+}
+
++ (YKFCBORBool *)decodeBoolFromInputStream:(NSInputStream *)inputStream header:(UInt8)header {
+    YKFAssertReturnValue(header == 0xF4 || header == 0xF5, @"CBOR - Decoding input stream error. Could not decode bool value.", nil);
+    return header == 0xF4 ? YKFCBORBool(NO) : YKFCBORBool(YES);
+}
+
+#pragma mark -
+
++ (NSData *)readObjectFromInputStream:(NSInputStream *)inputStream header:(UInt8)header size:(NSUInteger)size {
+    NSMutableData *objectData = [[NSMutableData alloc] initWithCapacity:size + 1];
+    [objectData appendBytes:&header length:1];
+    if (size) {
+        NSData *readData = [inputStream dequeueBytes:size];
+        if (!readData) {
+            return nil;
+        }
+        [objectData appendData:readData];
+    }
+    return objectData;
+}
+
+#pragma mark - Primitive Decoding
+
++ (YKFCBORInteger *)decodeInteger:(nonnull NSData *)data {
+    YKFAssertReturnValue(data.length, @"CBOR - Cannot decode from empty data.", nil);
+    
+    UInt8 *bytes = (UInt8 *)data.bytes;
+    
+    BOOL isNegative = bytes[0] & YKFCBORNegativeIntegerTagMask;
+    bytes[0] = bytes[0] & ~YKFCBORNegativeIntegerTagMask; // remove sign.
+    
+    NSInteger value = 0;
+    
+    switch (data.length) {
+        case 1:
+            value = bytes[0];
+            break;
+        case 2:
+            value = bytes[1];
+            break;
+        case 3:
+            value = CFSwapInt16BigToHost(*((UInt16 *)(&bytes[1])));
+            break;
+        case 5:
+            value = CFSwapInt32BigToHost(*((UInt32 *)(&bytes[1])));
+            break;
+            
+        case 9: {
+            UInt64 parsedValue = CFSwapInt64BigToHost(*((UInt64 *)(&bytes[1])));
+            
+            // Avoid overflow for values which cannot be represented on a NSInteger.
+            YKFAssertReturnValue(parsedValue <= INT64_MAX, @"CBOR - Cannot decode integer value. The value is too large.", nil);
+            value = (NSInteger)parsedValue;
+        }
+        break;
+            
+        default:
+            return nil;
+    }
+    
+    value = isNegative ? -(value + 1) : value;
+    return YKFCBORInteger(value);
+}
+
++ (YKFCBORByteString *)decodeByteString:(nonnull NSData *)data {
+    YKFAssertReturnValue(data.length, @"CBOR - Cannot decode from empty byte string data.", nil);
+    if (data.length == 1) {
+        return YKFCBORByteString([NSData data]);
+    }
+    return YKFCBORByteString([data subdataWithRange:NSMakeRange(1, data.length - 1)]);
+}
+
++ (YKFCBORTextString *)decodeTextString:(nonnull NSData *)data {
+    YKFAssertReturnValue(data.length, @"CBOR - Cannot decode from empty text string data.", nil);
+    
+    if (data.length == 1) {
+        return YKFCBORTextString(@"");
+    }
+    
+    NSData *textStringData = [data subdataWithRange:NSMakeRange(1, data.length -1)];
+    NSString *stringValue = [[NSString alloc] initWithData:textStringData encoding:NSUTF8StringEncoding];
+    YKFAssertReturnValue(stringValue, @"CBOR - Cannot decode UTF8 string data.", nil);
+    
+    return YKFCBORTextString(stringValue);
+}
+
+#pragma mark - CBOR to Foundation
+
++ (id)convertCBORObjectToFoundationType:(id)cborObject {
+    if ([cborObject isKindOfClass:YKFCBORInteger.class]) {
+        return @(((YKFCBORInteger *)cborObject).value);
+    }
+    
+    if ([cborObject isKindOfClass:YKFCBORByteString.class]) {
+        return [((YKFCBORByteString *)cborObject).value copy];
+    }
+    
+    if ([cborObject isKindOfClass:YKFCBORTextString.class]) {
+        return [((YKFCBORTextString *)cborObject).value copy];
+    }
+    
+    if ([cborObject isKindOfClass:YKFCBORBool.class]) {
+        return @(((YKFCBORBool *)cborObject).value);
+    }
+    
+    if ([cborObject isKindOfClass:YKFCBORArray.class]) {
+        YKFCBORArray *cborArray = (YKFCBORArray *)cborObject;
+        NSMutableArray *tempArray = [[NSMutableArray alloc] initWithCapacity:cborArray.value.count];
+        
+        for (id arrayObject in cborArray.value) {
+            id arrayElement = [self convertCBORObjectToFoundationType:arrayObject];
+            if (!arrayElement) {
+                return nil;
+            }
+            [tempArray addObject:arrayElement];
+        }
+        
+        return [tempArray copy];
+    }
+    
+    if ([cborObject isKindOfClass:YKFCBORMap.class]) {
+        YKFCBORMap *cborMap = (YKFCBORMap *)cborObject;
+        NSMutableDictionary *tempDictionary = [[NSMutableDictionary alloc] initWithCapacity:cborMap.value.count];
+        NSArray *dictionaryKeys = cborMap.value.allKeys;
+        
+        for (id key in dictionaryKeys) {
+            id value = cborMap.value[key];
+            id dictionaryKey = [self convertCBORObjectToFoundationType:key];
+            id dictionaryValue = [self convertCBORObjectToFoundationType:value];
+            if (!dictionaryKey || !dictionaryValue) {
+                return nil;
+            }
+            tempDictionary[dictionaryKey] = dictionaryValue;
+        }
+        
+        return [tempDictionary copy];
+    }
+    
+    return nil;
+}
+
+@end
+
+#pragma mark - NSInputStream(YKFCBORDecoder)
+
+@implementation NSInputStream(YKFCBORDecoder)
+
+- (UInt8)dequeueHead:(BOOL *)error {
+    NSData *headData = [self dequeueBytes:1];
+    if (!headData) {
+        *error = YES;
+        return 0;
+    }
+    return ((UInt8 *)(headData.bytes))[0];
+}
+
+- (NSData *)dequeueBytes:(NSUInteger)numberOfBytes {
+    YKFAssertReturnValue(self.streamStatus == NSStreamStatusOpen, @"CBOR - Input stream not opened.", nil);
+    YKFAssertReturnValue(self.hasBytesAvailable, @"CBOR - Cannot read from empty input stream.", nil);
+    YKFAssertReturnValue(numberOfBytes > 0 , @"CBOR - Cannot read 0 bytes.", nil);
+
+    UInt8 *buffer = malloc(numberOfBytes);
+    if (!buffer) {
+        return nil;
+    }
+    memset(buffer, 0, numberOfBytes);
+    
+    NSInteger bytesRead = [self read:buffer maxLength:numberOfBytes];
+    if (bytesRead != numberOfBytes) {
+        memset(buffer, 0, numberOfBytes);
+        free(buffer);
+        return nil;
+    }
+    
+    NSData *returnValue = [NSData dataWithBytes:buffer length:numberOfBytes];
+    
+    // Clear the buffer
+    memset(buffer, 0, numberOfBytes);
+    free(buffer);
+    
+    return returnValue;
+}
+
+@end
+
diff --git a/YubiKit/YubiKit/Sessions/Shared/Services/FIDO2/CBOR/YKFCBOREncoder.h b/YubiKit/YubiKit/Sessions/Shared/Services/FIDO2/CBOR/YKFCBOREncoder.h
new file mode 100755
index 000000000..ceb43febc
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Services/FIDO2/CBOR/YKFCBOREncoder.h
@@ -0,0 +1,45 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+#import "YKFCBORType.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/*!
+ The interface for a CTAP2 CBOR encoder.
+ */
+@protocol YKFCBOREncoderProtocol<NSObject>
+
++ (nullable NSData *)encodeInteger:(YKFCBORInteger *)cborInteger;
++ (nullable NSData *)encodeByteString:(YKFCBORByteString *)cborByteString;
++ (nullable NSData *)encodeTextString:(YKFCBORTextString *)cborTextString;
++ (nullable NSData *)encodeArray:(YKFCBORArray *)cborArray;
++ (nullable NSData *)encodeMap:(YKFCBORMap *)cborMap;
++ (nullable NSData *)encodeBool:(YKFCBORBool *)cborBool;
+
+/*!
+ Generic encoding method which will check the type.
+ */
++ (nullable NSData *)encodeObject:(id)object;
+
+@end
+
+/*!
+ CTAP2 CBOR encoder.
+ */
+@interface YKFCBOREncoder: NSObject<YKFCBOREncoderProtocol>
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/YubiKit/YubiKit/Sessions/Shared/Services/FIDO2/CBOR/YKFCBOREncoder.m b/YubiKit/YubiKit/Sessions/Shared/Services/FIDO2/CBOR/YKFCBOREncoder.m
new file mode 100755
index 000000000..7c2d8d578
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Services/FIDO2/CBOR/YKFCBOREncoder.m
@@ -0,0 +1,304 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "YKFCBOREncoder.h"
+#import "YKFCBORTag.h"
+#import "YKFAssert.h"
+
+@implementation YKFCBOREncoder
+
+#pragma mark - Integer (Major Types 0 and 1)
+
++ (NSData *)encodeInteger:(YKFCBORInteger *)cborInteger {
+    YKFAssertReturnValue(cborInteger, @"CBOR Encoding - Cannot encode empty CBOR integer.", nil);
+    
+    NSInteger value = cborInteger.value;
+    
+    if (labs(value) <= INT8_MAX) {
+        return [self encodeSInt8:(SInt8)value];
+    }
+    if (labs(value) <= UINT8_MAX) {
+        return value < 0 ? [self encodeSInt16:(SInt16)value] : [self encodeUInt8:(UInt8)value];
+    }
+    if (labs(value) <= INT16_MAX) {
+        return [self encodeSInt16:(SInt16)value];
+    }
+    if (labs(value) <= UINT16_MAX) {
+        return value < 0 ? [self encodeSInt32:(SInt32)value] : [self encodeUInt16:(UInt16)value];
+    }
+    if (labs(value) <= INT32_MAX) {
+        return [self encodeSInt32:(SInt32)value];
+    }
+    if (labs(value) <= UINT32_MAX) {
+        return value < 0 ? [self encodeSInt64:(SInt64)value] : [self encodeUInt32:(UInt32)value];
+    }
+
+    return [self encodeSInt64:(SInt64)value];
+}
+
+#pragma mark - Byte String (Major Type 2)
+
++ (NSData *)encodeByteString:(YKFCBORByteString *)cborByteString {
+    YKFAssertReturnValue(cborByteString, @"CBOR Encoding - Cannot encode nil CBOR byte string.", nil);
+    
+    NSData *byteString = cborByteString.value;
+    return [self encodeData:byteString tagMask:YKFCBORByteStringTagMask];
+}
+
+#pragma mark - Text String (Major Type 3)
+
++ (NSData *)encodeTextString:(YKFCBORTextString *)cborTextString {
+    YKFAssertReturnValue(cborTextString, @"CBOR Encoding - Cannot encode nil CBOR text string.", nil);
+    
+    NSString *string = cborTextString.value;
+    YKFAssertReturnValue(string, @"CBOR Encoding - Cannot encode nil string.", nil);
+    
+    NSData *utf8EncodedData = [string dataUsingEncoding:NSUTF8StringEncoding];    
+    return [self encodeData:utf8EncodedData tagMask:YKFCBORTextStringTagMask];
+}
+
+#pragma mark - Array (Major Type 4)
+
++ (NSData *)encodeArray:(YKFCBORArray *)cborArray {
+    YKFAssertReturnValue(cborArray, @"CBOR Encoding - Cannot encode empty CBOR array.", nil);
+    
+    NSArray *array = cborArray.value;
+    YKFAssertReturnValue(cborArray, @"CBOR Encoding - Cannot encode empty/nil array.", nil);
+    
+    YKFCBORInteger *arrayLength = YKFCBORInteger(array.count);
+    NSData *encodedLength = [self encodeInteger:arrayLength];
+    NSMutableData *encodedArray = [[NSMutableData alloc] initWithData:encodedLength];
+    
+    // Set major type 4.
+    UInt8 *encodedValueBytes = encodedArray.mutableBytes;
+    encodedValueBytes[0] = encodedValueBytes[0] | YKFCBORArrayTagMask;
+
+    // Append the elements.
+    for (id element in array) {
+        NSData *encodedElement = [self encodeObject:element];
+        NSAssert(encodedElement, @"Cannot encode all the elements in the array. Unknown type: %@",
+                 NSStringFromClass(((NSObject *)element).class));
+        if (!encodedElement) {
+            return nil;
+        }
+        [encodedArray appendData:encodedElement];
+    }
+    
+    return [encodedArray copy];
+}
+
+#pragma mark - Map (Major Type 5)
+
++ (NSData *)encodeMap:(YKFCBORMap *)cborMap {
+    YKFAssertReturnValue(cborMap, @"CBOR Encoding - Cannot encode nil CBOR map.", nil);
+    
+    NSDictionary *map = cborMap.value;
+    YKFAssertReturnValue(map, @"CBOR Encoding - Cannot encode nil dictionary.", nil);
+    
+    YKFCBORInteger *mapPairs = YKFCBORInteger(map.count);
+    NSData *encodedPairs = [self encodeInteger:mapPairs];
+    NSMutableData *encodedMap = [[NSMutableData alloc] initWithData:encodedPairs];
+    
+    // Set major type 5.
+    UInt8 *encodedValueBytes = encodedMap.mutableBytes;
+    encodedValueBytes[0] = encodedValueBytes[0] | YKFCBORMapTagMask;
+
+    // Append the pairs sorted by keys.
+    NSArray *keys = [map.allKeys sortedArrayUsingSelector:@selector(compare:)];
+    
+    for (id key in keys) {
+        id keyValue = map[key];
+        
+        NSData *encodedKey = [self encodeObject:key];
+        NSData *encodedKeyValue = [self encodeObject:keyValue];
+        
+        NSAssert(encodedKey, @"Cannot encode all the elements in the map. Unknown key type: %@",
+                 NSStringFromClass(((NSObject *)key).class));
+        NSAssert(encodedKeyValue, @"Cannot encode all the elements in the map. Unknown key value type: %@",
+                 NSStringFromClass(((NSObject *)keyValue).class));
+        
+        if (!encodedKey || !encodedKeyValue) {
+            return nil;
+        }
+        
+        [encodedMap appendData:encodedKey];
+        [encodedMap appendData:encodedKeyValue];
+    }
+    
+    return [encodedMap copy];
+}
+
+#pragma mark - Boolean (Appendix B.  Jump Table)
+
++ (NSData *)encodeBool:(YKFCBORBool *)cborBool {
+    YKFAssertReturnValue(cborBool, @"CBOR Encoding - Cannot encode empty CBOR bool.", nil);
+    
+    UInt8 encoded = cborBool.value ? 0xF5 : 0xF4;
+    return [NSData dataWithBytes:&encoded length:1];
+}
+
+#pragma mark - Helpers
+
++ (NSData *)encodeUInt8:(UInt8)value {
+    if (value < 24) {
+        return [NSData dataWithBytes:&value length:1];
+    }
+    const UInt8 bytes[] = {YKFCBORUInt8Tag, value};
+    return [NSData dataWithBytes:bytes length:2];
+}
+
++ (NSData *)encodeUInt16:(UInt16)value {
+    NSMutableData *buffer = [[NSMutableData alloc] init];
+    UInt16 bigUInt16 = CFSwapInt16HostToBig(value);
+    
+    [buffer appendBytes:&YKFCBORUInt16Tag length:1];
+    [buffer appendBytes:&bigUInt16 length:sizeof(UInt16)];
+    
+    return [buffer copy];
+}
+
++ (NSData *)encodeUInt32:(UInt32)value {
+    NSMutableData *buffer = [[NSMutableData alloc] init];
+    UInt32 bigUInt32 = CFSwapInt32HostToBig(value);
+    
+    [buffer appendBytes:&YKFCBORUInt32Tag length:1];
+    [buffer appendBytes:&bigUInt32 length:sizeof(UInt32)];
+    
+    return [buffer copy];
+}
+
++ (NSData *)encodeUInt64:(UInt64)value {
+    NSMutableData *buffer = [[NSMutableData alloc] init];
+    UInt64 bigUInt64 = CFSwapInt64HostToBig(value);
+    
+    [buffer appendBytes:&YKFCBORUInt64Tag length:1];
+    [buffer appendBytes:&bigUInt64 length:sizeof(UInt64)];
+    
+    return [buffer copy];
+}
+
++ (NSData *)encodeSInt8:(SInt8)value {
+    if (value >= 0) {
+        return [self encodeUInt8:value];
+    }
+    value = -1 - value;
+    
+    if (value < 24) {
+        value |= YKFCBORNegativeIntegerTagMask;
+        return [NSData dataWithBytes:&value length:1];
+    }
+    
+    const UInt8 bytes[] = {YKFCBORSInt8Tag, value};
+    return [NSData dataWithBytes:bytes length:2];
+}
+
++ (NSData *)encodeSInt16:(SInt16)value {
+    if (value >= 0) {
+        return [self encodeUInt16:value];
+    }
+    value = -1 - value;
+    
+    NSMutableData *buffer = [[NSMutableData alloc] init];
+    UInt16 bigUInt16 = CFSwapInt16HostToBig(value);
+    
+    [buffer appendBytes:&YKFCBORSInt16Tag length:1];
+    [buffer appendBytes:&bigUInt16 length:sizeof(UInt16)];
+    
+    return [buffer copy];
+}
+
++ (NSData *)encodeSInt32:(SInt32)value {
+    if (value >= 0) {
+        return [self encodeUInt32:value];
+    }
+    value = -1 - value;
+    
+    NSMutableData *buffer = [[NSMutableData alloc] init];
+    UInt32 bigUInt32 = CFSwapInt32HostToBig(value);
+    
+    [buffer appendBytes:&YKFCBORSInt32Tag length:1];
+    [buffer appendBytes:&bigUInt32 length:sizeof(UInt32)];
+    
+    return [buffer copy];
+}
+
++ (NSData *)encodeSInt64:(SInt64)value {
+    if (value >= 0) {
+        return [self encodeUInt64:value];
+    }
+    value = -1 - value;
+    
+    NSMutableData *buffer = [[NSMutableData alloc] init];
+    UInt64 bigUInt64 = CFSwapInt64HostToBig(value);
+    
+    [buffer appendBytes:&YKFCBORSInt64Tag length:1];
+    [buffer appendBytes:&bigUInt64 length:sizeof(UInt64)];
+    
+    return [buffer copy];
+}
+
++ (NSData *)encodeData:(NSData *)value tagMask:(UInt8)tagMask {
+    YKFAssertReturnValue(value, @"CBOR Encoding - Cannot encode nil data.", nil);
+    
+    YKFCBORInteger *dataLength = YKFCBORInteger(value.length);
+    NSMutableData *encodedValue = nil;
+    if (dataLength) {
+        NSData *encodedLength = [self encodeInteger:dataLength];
+        encodedValue = [[NSMutableData alloc] initWithData:encodedLength];
+        [encodedValue appendData:value];
+        
+        // Set the major type 2.
+        UInt8 *encodedValueBytes = encodedValue.mutableBytes;
+        encodedValueBytes[0] = encodedValueBytes[0] | tagMask;
+    } else {
+        // Set only the major type.
+        encodedValue = [[NSMutableData alloc] initWithCapacity:1];
+        [encodedValue appendBytes:&tagMask length:1];
+    }
+    
+    return [encodedValue copy];
+}
+
++ (NSData *)encodeObject:(id)object {
+    YKFAssertReturnValue(object, @"CBOR Encoding - Cannot encode a nil object.", nil);
+    
+    if ([object isKindOfClass:YKFCBORInteger.class]) {
+        YKFCBORInteger *integer = (YKFCBORInteger *)object;
+        return [self encodeInteger:integer];
+    }
+    if ([object isKindOfClass:YKFCBORByteString.class]) {
+        YKFCBORByteString *byteString = (YKFCBORByteString *)object;
+        return [self encodeByteString:byteString];
+    }
+    if ([object isKindOfClass:YKFCBORTextString.class]) {
+        YKFCBORTextString *textString = (YKFCBORTextString *)object;
+        return [self encodeTextString:textString];
+    }
+    if ([object isKindOfClass:YKFCBORArray.class]) {
+        YKFCBORArray *array = (YKFCBORArray *)object;
+        return [self encodeArray:array];
+    }
+    if ([object isKindOfClass:YKFCBORMap.class]) {
+        YKFCBORMap *map = (YKFCBORMap *)object;
+        return [self encodeMap:map];
+    }
+    if ([object isKindOfClass:YKFCBORBool.class]) {
+        YKFCBORBool *boolean = (YKFCBORBool *)object;
+        return [self encodeBool:boolean];
+    }
+    
+    return nil;
+}
+
+@end
diff --git a/YubiKit/YubiKit/Sessions/Shared/Services/FIDO2/CBOR/YKFCBORTag.h b/YubiKit/YubiKit/Sessions/Shared/Services/FIDO2/CBOR/YKFCBORTag.h
new file mode 100755
index 000000000..1e4a59236
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Services/FIDO2/CBOR/YKFCBORTag.h
@@ -0,0 +1,58 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+/*
+ Positive Integer Tags - Major type 0
+ First 3 bits of the major type are 0 (0b000_00000). No mask is required for major type 0.
+ */
+
+static const UInt8 YKFCBORUInt8Tag   = 0x18; // 24
+static const UInt8 YKFCBORUInt16Tag  = 0x19; // 25
+static const UInt8 YKFCBORUInt32Tag  = 0x1A; // 26
+static const UInt8 YKFCBORUInt64Tag  = 0x1B; // 27
+
+/*
+ Negative Integer Tags - Major type 1
+ First 3 bits of the major type are 1 (0b001_00000). Minor types are the same as positive integers.
+ */
+static const UInt8 YKFCBORNegativeIntegerTagMask = 0b00100000;
+
+static const UInt8 YKFCBORSInt8Tag   = YKFCBORUInt8Tag  | YKFCBORNegativeIntegerTagMask;
+static const UInt8 YKFCBORSInt16Tag  = YKFCBORUInt16Tag | YKFCBORNegativeIntegerTagMask;
+static const UInt8 YKFCBORSInt32Tag  = YKFCBORUInt32Tag | YKFCBORNegativeIntegerTagMask;
+static const UInt8 YKFCBORSInt64Tag  = YKFCBORUInt64Tag | YKFCBORNegativeIntegerTagMask;
+
+/*
+ Byte String Tags - Major type 2
+ First 3 bits of the major type are 2 (0b010_00000).
+ */
+static const UInt8 YKFCBORByteStringTagMask = 0b01000000;
+
+/*
+ Text String Tags - Major type 3
+ First 3 bits of the major type are 3 (0b011_00000).
+ */
+static const UInt8 YKFCBORTextStringTagMask = 0b01100000;
+
+/*
+ Array Tags - Major type 4
+ First 3 bits of the major type are 4 (0b100_00000).
+ */
+static const UInt8 YKFCBORArrayTagMask = 0b10000000;
+
+/*
+ Map - Major type 5
+ First 3 bits of the major type are 5 (0b101_00000).
+ */
+static const UInt8 YKFCBORMapTagMask = 0b10100000;
diff --git a/YubiKit/YubiKit/Sessions/Shared/Services/FIDO2/CBOR/YKFCBORType.h b/YubiKit/YubiKit/Sessions/Shared/Services/FIDO2/CBOR/YKFCBORType.h
new file mode 100755
index 000000000..1c14291c1
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Services/FIDO2/CBOR/YKFCBORType.h
@@ -0,0 +1,108 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+
+/*
+ YKFCBORTypeProtocol
+ */
+
+@protocol YKFCBORTypeProtocol<NSObject>
+
+@property (readonly) NSUInteger hash;
+
+- (BOOL)isEqual:(id)object;
+- (NSComparisonResult)compare:(id)other;
+
+- (NSString *)description;
+
+@end
+
+/*
+ YKFCBORInteger
+ */
+
+#define YKFCBORInteger(value) [YKFCBORInteger cborIntegerWithValue:value]
+
+@interface YKFCBORInteger: NSObject<NSCopying, YKFCBORTypeProtocol>
+
+@property (nonatomic) NSInteger value;
++ (YKFCBORInteger *)cborIntegerWithValue:(NSInteger)value;
+
+@end
+
+/*
+ YKFCBORByteString
+ */
+
+#define YKFCBORByteString(value) [YKFCBORByteString cborByteStringWithValue:value]
+
+@interface YKFCBORByteString: NSObject<NSCopying, YKFCBORTypeProtocol>
+
+@property (nonatomic) NSData *value;
++ (YKFCBORByteString *)cborByteStringWithValue:(NSData *)value;
+
+@end
+
+/*
+ YKFCBORTextString
+ */
+
+#define YKFCBORTextString(value) [YKFCBORTextString cborTextStringWithValue:value]
+
+@interface YKFCBORTextString: NSObject<NSCopying, YKFCBORTypeProtocol>
+
+@property (nonatomic) NSString *value;
++ (YKFCBORTextString *)cborTextStringWithValue:(NSString *)value;
+
+@end
+
+/*
+ YKFCBORArray
+ */
+
+#define YKFCBORArray(value) [YKFCBORArray cborArrayWithValue:value]
+
+@interface YKFCBORArray: NSObject<NSCopying, YKFCBORTypeProtocol>
+
+@property (nonatomic) NSArray *value;
++ (YKFCBORArray *)cborArrayWithValue:(NSArray *)value;
+
+@end
+
+/*
+ YKFCBORMap
+ */
+
+#define YKFCBORMap(value) [YKFCBORMap cborMapWithValue:value]
+
+@interface YKFCBORMap: NSObject<NSCopying, YKFCBORTypeProtocol>
+
+@property (nonatomic) NSDictionary *value;
++ (YKFCBORMap *)cborMapWithValue:(NSDictionary *)value;
+
+@end
+
+/*
+ YKFCBORBool
+ */
+
+#define YKFCBORBool(value) [YKFCBORBool cborBoolWithValue:value]
+
+@interface YKFCBORBool: NSObject<NSCopying, YKFCBORTypeProtocol>
+
+@property (nonatomic) BOOL value;
++ (YKFCBORBool *)cborBoolWithValue:(BOOL)value;
+
+@end
diff --git a/YubiKit/YubiKit/Sessions/Shared/Services/FIDO2/CBOR/YKFCBORType.m b/YubiKit/YubiKit/Sessions/Shared/Services/FIDO2/CBOR/YKFCBORType.m
new file mode 100755
index 000000000..3a9b895dd
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Services/FIDO2/CBOR/YKFCBORType.m
@@ -0,0 +1,252 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "YKFCBORType.h"
+
+#pragma mark - YKFCBORInteger
+
+@implementation YKFCBORInteger
+
++ (YKFCBORInteger *)cborIntegerWithValue:(NSInteger)value {
+    YKFCBORInteger *cborInteger = [[YKFCBORInteger alloc] init];
+    cborInteger.value = value;
+    return cborInteger;
+}
+
+- (NSUInteger)hash {
+    return self.value;
+}
+
+- (nonnull id)copyWithZone:(nullable NSZone *)zone {
+    return YKFCBORInteger(self.value);
+}
+
+- (NSComparisonResult)compare:(id)other {
+    NSParameterAssert([other isKindOfClass:self.class]);
+    YKFCBORInteger *otherInteger = (YKFCBORInteger *)other;
+    
+    if (self.value < otherInteger.value) { return NSOrderedAscending; }
+    if (self.value > otherInteger.value) { return NSOrderedDescending; }    
+    return NSOrderedSame;
+}
+
+- (BOOL)isEqual:(id)object {
+    NSParameterAssert([object isKindOfClass:self.class]);
+    YKFCBORInteger *otherInteger = (YKFCBORInteger *)object;
+    
+    return self.value == otherInteger.value;
+}
+
+- (NSString *)description {
+    NSString *className = NSStringFromClass(self.class);
+    return [NSString stringWithFormat:@"%@: %ld", className, (long)self.value];
+}
+
+@end
+
+#pragma mark - YKFCBORByteString
+
+@implementation YKFCBORByteString
+
++ (YKFCBORByteString *)cborByteStringWithValue:(NSData *)value {
+    YKFCBORByteString *byteString = [[YKFCBORByteString alloc] init];
+    byteString.value = value;
+    return byteString;
+}
+
+- (NSUInteger)hash {
+    return [self.value hash];
+}
+
+- (nonnull id)copyWithZone:(nullable NSZone *)zone {
+    return [YKFCBORByteString cborByteStringWithValue:self.value];
+}
+
+- (NSComparisonResult)compare:(id)other {
+    NSAssert(NO, @"Cannot compare NSData. NSData should not be used as a key.");
+    return NSOrderedSame;
+}
+
+- (BOOL)isEqual:(id)object {
+    NSParameterAssert([object isKindOfClass:self.class]);
+    YKFCBORByteString *otherByteString = (YKFCBORByteString *)object;
+
+    return [self.value isEqualToData:otherByteString.value];
+}
+
+- (NSString *)description {
+    NSString *className = NSStringFromClass(self.class);
+    return [NSString stringWithFormat:@"%@: %@", className, self.value.description];
+}
+
+@end
+
+#pragma mark - YKFCBORTextString
+
+@implementation YKFCBORTextString
+
++ (YKFCBORTextString *)cborTextStringWithValue:(NSString *)value {
+    YKFCBORTextString *textString = [[YKFCBORTextString alloc] init];
+    textString.value = value;
+    return textString;
+}
+
+- (NSUInteger)hash {
+    return [self.value hash];
+}
+
+- (nonnull id)copyWithZone:(nullable NSZone *)zone {
+    return [YKFCBORTextString cborTextStringWithValue:self.value];
+}
+
+- (NSComparisonResult)compare:(id)other {
+    NSParameterAssert([other isKindOfClass:self.class]);
+    YKFCBORTextString *otherTextString = (YKFCBORTextString *)other;
+    
+    if (self.value.length < otherTextString.value.length) {
+        return NSOrderedAscending;
+    } else if (self.value.length > otherTextString.value.length) {
+        return NSOrderedDescending;
+    } else {
+        return [self.value compare:otherTextString.value];
+    }
+}
+
+- (BOOL)isEqual:(id)object {
+    NSParameterAssert([object isKindOfClass:self.class]);
+    YKFCBORTextString *otherTextString = (YKFCBORTextString *)object;
+    
+    return [self.value isEqualToString:otherTextString.value];
+}
+
+- (NSString *)description {
+    NSString *className = NSStringFromClass(self.class);
+    return [NSString stringWithFormat:@"%@: %@", className, self.value];
+}
+
+@end
+
+#pragma mark - YKFCBORArray
+
+@implementation YKFCBORArray
+
++ (YKFCBORArray *)cborArrayWithValue:(NSArray *)value {
+    YKFCBORArray *array = [[YKFCBORArray alloc] init];
+    array.value = value;
+    return array;
+}
+
+- (NSUInteger)hash {
+    NSAssert(NO, @"Cannot hash NSArray. NSArrays should not be used as keys.");
+    return 0;
+}
+
+- (nonnull id)copyWithZone:(nullable NSZone *)zone {
+    return [YKFCBORArray cborArrayWithValue:self.value];
+}
+
+- (NSComparisonResult)compare:(id)otherArray {
+    NSAssert(NO, @"Cannot compare NSArray. NSArrays should not be used as keys.");
+    return NSOrderedSame;
+}
+
+- (BOOL)isEqual:(id)object {
+    NSParameterAssert([object isKindOfClass:self.class]);
+    YKFCBORArray *otherArray = (YKFCBORArray *)object;
+    
+    return [self.value isEqualToArray:otherArray.value];
+}
+
+- (NSString *)description {
+    NSString *className = NSStringFromClass(self.class);
+    return [NSString stringWithFormat:@"%@: %@", className, self.value.description];
+}
+
+@end
+
+#pragma mark - YKFCBORMap
+
+@implementation YKFCBORMap
+
++ (YKFCBORMap *)cborMapWithValue:(NSDictionary *)value {
+    YKFCBORMap *map = [[YKFCBORMap alloc] init];
+    map.value = value;
+    return map;
+}
+
+- (NSUInteger)hash {
+    NSAssert(NO, @"Cannot hash NSDictionary. NSDictionary should not be used as a key.");
+    return 0;
+}
+
+- (nonnull id)copyWithZone:(nullable NSZone *)zone {
+    return [YKFCBORMap cborMapWithValue:self.value];
+}
+
+- (NSComparisonResult)compare:(id)otherMap {
+    NSAssert(NO, @"Cannot compare NSDisctionary. NSDisctionary should not be used as a key.");
+    return NSOrderedSame;
+}
+
+- (BOOL)isEqual:(id)object {
+    NSParameterAssert([object isKindOfClass:self.class]);
+    YKFCBORMap *otherMap = (YKFCBORMap *)object;
+
+    return [self.value isEqualToDictionary:otherMap.value];
+}
+
+- (NSString *)description {
+    NSString *className = NSStringFromClass(self.class);
+    return [NSString stringWithFormat:@"%@: %@", className, self.value];
+}
+
+@end
+
+#pragma mark - YKFCBORBool
+
+@implementation YKFCBORBool
+
++ (YKFCBORBool *)cborBoolWithValue:(BOOL)value {
+    YKFCBORBool *cborBool = [[YKFCBORBool alloc] init];
+    cborBool.value = value;
+    return cborBool;
+}
+
+- (NSUInteger)hash {
+    NSAssert(NO, @"Cannot hash Bool. Bools should not be used as keys.");
+    return 0;
+}
+
+- (nonnull id)copyWithZone:(nullable NSZone *)zone {
+    return [YKFCBORBool cborBoolWithValue:self.value];
+}
+
+- (NSComparisonResult)compare:(id)otherBool {
+    NSAssert(NO, @"Cannot compare Bool. Bools should not be used as keys.");
+    return NSOrderedSame;
+}
+
+- (BOOL)isEqual:(id)object {
+    NSParameterAssert([object isKindOfClass:self.class]);
+    YKFCBORBool *otherBool = (YKFCBORBool *)object;
+
+    return self.value == otherBool.value;
+}
+
+- (NSString *)description {
+    NSString *className = NSStringFromClass(self.class);
+    return [NSString stringWithFormat:@"%@: %@", className, self.value ? @"true" : @"false"];
+}
+
+@end
diff --git a/YubiKit/YubiKit/Sessions/Shared/Services/FIDO2/Crypto/YKFFIDO2PinAuthKey.h b/YubiKit/YubiKit/Sessions/Shared/Services/FIDO2/Crypto/YKFFIDO2PinAuthKey.h
new file mode 100755
index 000000000..f7f3e8ee6
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Services/FIDO2/Crypto/YKFFIDO2PinAuthKey.h
@@ -0,0 +1,39 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+#import "YKFCBORType.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface YKFFIDO2PinAuthKey: NSObject
+
+@property (nonatomic, readonly, nullable) SecKeyRef publicKey;
+@property (nonatomic, readonly, nullable) SecKeyRef privateKey;
+
+/// Returns the COSE representation for the public key (ECC only).
+@property (nonatomic, readonly, nullable) YKFCBORMap *cosePublicKey;
+
+/// Generates an ECC key pair.
+- (nullable instancetype)init;
+
+/// Initializes the publicKey from a COSE public key representation (ECC only).
+- (nullable instancetype)initWithCosePublicKey:(NSDictionary *)cosePublicKey;
+
+/// Returns an ECDH shared secret: ECDH(self.privateKey, otherKey.publicKey).
+- (nullable NSData *)sharedSecretWithAuthKey:(YKFFIDO2PinAuthKey *)otherKey;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/YubiKit/YubiKit/Sessions/Shared/Services/FIDO2/Crypto/YKFFIDO2PinAuthKey.m b/YubiKit/YubiKit/Sessions/Shared/Services/FIDO2/Crypto/YKFFIDO2PinAuthKey.m
new file mode 100755
index 000000000..bd9ee4323
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Services/FIDO2/Crypto/YKFFIDO2PinAuthKey.m
@@ -0,0 +1,209 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Security/Security.h>
+
+#import "YKFFIDO2PinAuthKey.h"
+#import "YKFCBOREncoder.h"
+#import "YKFCBORDecoder.h"
+#import "YKFBlockMacros.h"
+#import "YKFAssert.h"
+
+/// The key type label.
+static const NSInteger YKFFIDO2PinAuthKeyCoseLabelKty = 1;
+
+/// The curve type label.
+static const NSInteger YKFFIDO2PinAuthKeyCoseLabelCrv = -1;
+
+/// The label for the X coordinate of an EC key.
+static const NSInteger YKFFIDO2PinAuthKeyCoseLabelEcX = -2;
+
+/// The label for the Y coordinate of an EC key.
+static const NSInteger YKFFIDO2PinAuthKeyCoseLabelEcY = -3;
+
+typedef NS_ENUM(NSUInteger, YKFFIDO2PinAuthKeyCoseKeyType) {
+    /// Elliptic Curve Keys with x and y coordinate.
+    YKFFIDO2PinAuthKeyCoseKeyTypeEc = 2
+};
+
+typedef NS_ENUM(NSUInteger, YKFFIDO2PinAuthKeyCoseCurve) {
+    /// NIST P-256 also known as secp256r1.
+    YKFFIDO2PinAuthKeyCoseCurveP256 = 1
+};
+
+@interface YKFFIDO2PinAuthKey()
+
+@property (nonatomic, readwrite) SecKeyRef publicKey;
+@property (nonatomic, readwrite) SecKeyRef privateKey;
+
+@end
+
+@implementation YKFFIDO2PinAuthKey
+
+- (instancetype)init {
+    self = [super init];
+    if (self) {
+        BOOL success = [self generateECKeyPair];
+        YKFAssertAbortInit(success);
+    }
+    return self;
+}
+
+- (instancetype)initWithCosePublicKey:(NSDictionary *)cosePublicKey {
+    YKFAssertAbortInit(cosePublicKey)
+    
+    self = [super init];
+    if (self) {
+        BOOL success = [self setupFromCosePublicKey:cosePublicKey];
+        YKFAssertAbortInit(success);
+    }
+    return self;
+}
+
+- (void)dealloc {
+    if (_publicKey != NULL) {
+        CFRelease(_publicKey);
+    }
+    if (_privateKey != NULL) {
+        CFRelease(_privateKey);
+    }
+}
+
+#pragma mark - COSE
+
+- (BOOL)setupFromCosePublicKey:(NSDictionary *)coseKey {
+    YKFAssertOffMainThread();
+    YKFParameterAssertReturnValue(coseKey, NO);
+        
+    NSDictionary *coseKeyDictionary = coseKey;
+    NSData *xCoordinate = coseKeyDictionary[@(YKFFIDO2PinAuthKeyCoseLabelEcX)];
+    NSData *yCoordinate = coseKeyDictionary[@(YKFFIDO2PinAuthKeyCoseLabelEcY)];
+    
+    YKFAssertReturnValue(xCoordinate.length && yCoordinate.length, @"Could not decode authKey COSE format.", NO);
+    
+#ifndef __clang_analyzer__ // Suppress the self.publicKey leak false-positive warning since it will be released in dealloc.
+    UInt8 uncompressedHeader = 0x04;
+    NSMutableData *rawKeyData = [[NSMutableData alloc] init];
+    [rawKeyData appendBytes:&uncompressedHeader length:1];
+    [rawKeyData appendData:xCoordinate];
+    [rawKeyData appendData:yCoordinate];
+    
+    NSDictionary *attributes = @{(id)kSecAttrKeyType: (id)kSecAttrKeyTypeECSECPrimeRandom,
+                                 (id)kSecAttrKeyClass: (id)kSecAttrKeyClassPublic,
+                                 (id)kSecAttrKeySizeInBits: @256};
+    CFErrorRef error = NULL;
+    self.publicKey = SecKeyCreateWithData((__bridge CFDataRef)rawKeyData, (__bridge CFDictionaryRef)attributes, &error);
+    
+    BOOL success = (error == NULL);
+    if (error) {
+        CFRelease(error);
+        if (self.publicKey) {
+            CFRelease(self.publicKey);
+        }
+    }
+    return success;
+#endif
+}
+
+- (YKFCBORMap *)cosePublicKey {
+    YKFAssertReturnValue(self.publicKey, @"The authKey does not contain a public key to encode.", nil);
+    
+    NSArray *keyCoordinates = [self getECKeyCoordinatesFromSecKey: self.publicKey];
+    YKFAssertReturnValue(keyCoordinates, @"Could not read authKey coordinates.", nil);
+    
+    NSDictionary *coseKeyDictionary = @{YKFCBORInteger(YKFFIDO2PinAuthKeyCoseLabelKty): YKFCBORInteger(YKFFIDO2PinAuthKeyCoseKeyTypeEc),
+                                        YKFCBORInteger(YKFFIDO2PinAuthKeyCoseLabelCrv): YKFCBORInteger(YKFFIDO2PinAuthKeyCoseCurveP256),
+                                        YKFCBORInteger(YKFFIDO2PinAuthKeyCoseLabelEcX): YKFCBORByteString(keyCoordinates[0]),
+                                        YKFCBORInteger(YKFFIDO2PinAuthKeyCoseLabelEcY): YKFCBORByteString(keyCoordinates[1])};
+    YKFCBORMap *coseKeyMap = YKFCBORMap(coseKeyDictionary);
+    return coseKeyMap;
+}
+
+- (NSData *)sharedSecretWithAuthKey:(YKFFIDO2PinAuthKey *)otherKey {
+    YKFAssertOffMainThread();
+    YKFAssertReturnValue(self.privateKey, @"Cannot generate ECDH shared secret (missing private key).", nil);
+    YKFAssertReturnValue(otherKey.publicKey, @"Cannot generate ECDH shared secret (missing public key)", nil);
+    
+    CFErrorRef error = NULL;
+    NSDictionary *parameters = [NSDictionary dictionary];
+    
+    // kSecKeyAlgorithmECDHKeyExchangeStandard - Returns the unwrapped X coordinate of the ECDH result (x, y).
+    CFDataRef sharedSecret = SecKeyCopyKeyExchangeResult(self.privateKey, kSecKeyAlgorithmECDHKeyExchangeStandard,
+                                                         otherKey.publicKey, (__bridge CFDictionaryRef)parameters, &error);
+    if (error) {
+        CFRelease(error);
+        if (sharedSecret) {
+            CFRelease(sharedSecret);
+        }
+        return nil;
+    }
+        
+    return (__bridge_transfer NSData*)sharedSecret;
+}
+
+#pragma mark - Key Generation
+
+- (BOOL)generateECKeyPair {
+    YKFAssertOffMainThread();
+    
+    NSMutableDictionary *privateKeyParams = [[NSMutableDictionary alloc] init];
+    NSMutableDictionary *publicKeyParams = [[NSMutableDictionary alloc] init];
+    NSMutableDictionary *keyPairParams = [[NSMutableDictionary alloc] init];
+    
+    // Public and private keys are not stored.
+    privateKeyParams[(id)kSecAttrIsPermanent] = @NO;
+    publicKeyParams[(id)kSecAttrIsPermanent] = @NO;
+    
+    // ECC P256
+    keyPairParams[(id)kSecPrivateKeyAttrs] = privateKeyParams;
+    keyPairParams[(id)kSecPublicKeyAttrs] = publicKeyParams;
+    keyPairParams[(id)kSecAttrKeyType] = (id)kSecAttrKeyTypeECSECPrimeRandom;
+    keyPairParams[(id)kSecAttrKeySizeInBits] = @256;
+    
+    SecKeyRef publicKey = NULL;
+    SecKeyRef privateKey = NULL;
+    OSStatus err = SecKeyGeneratePair((__bridge CFDictionaryRef)keyPairParams, &publicKey, &privateKey);
+
+    YKFAssertReturnValue(err == errSecSuccess, @"Could not generate an EC authKey", NO);
+    YKFAssertReturnValue(privateKey && publicKey, @"The authKey EC key pair was not generated.", NO);
+    
+    self.privateKey = privateKey;
+    self.publicKey = publicKey;
+    
+    return YES;
+}
+
+#pragma mark - Helpers
+
+- (NSArray *)getECKeyCoordinatesFromSecKey:(SecKeyRef)secKey {
+    CFDataRef externalRepresentation = SecKeyCopyExternalRepresentation(secKey, nil);
+    
+    // ANSI X9.63 standard using a byte string of 04 || X || Y.
+    NSData *keyData = (__bridge_transfer NSData*)externalRepresentation;
+    YKFAssertReturnValue(keyData.length, @"Could not copy the authKey external representation.", nil);
+    
+    UInt8 *keyDataBytes = (UInt8 *)keyData.bytes;
+    YKFAssertReturnValue(keyDataBytes[0] == 0x04, @"Invalid external representation of authKey.", nil);
+    
+    keyData = [keyData subdataWithRange:NSMakeRange(1, keyData.length - 1)];
+    YKFAssertReturnValue(keyData.length && (keyData.length % 2 == 0), @"Invalid external representation of authKey.", nil);
+
+    NSUInteger halfRange = keyData.length / 2;
+    NSData *xCoordinate = [keyData subdataWithRange:NSMakeRange(0, halfRange)];
+    NSData *yCoordinate = [keyData subdataWithRange:NSMakeRange(halfRange, halfRange)];
+    
+    return @[xCoordinate, yCoordinate];
+}
+
+@end
diff --git a/YubiKit/YubiKit/Sessions/Shared/Services/FIDO2/WebAuthN/YKFWebAuthnClientData.h b/YubiKit/YubiKit/Sessions/Shared/Services/FIDO2/WebAuthN/YKFWebAuthnClientData.h
new file mode 100755
index 000000000..cbd241ed2
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Services/FIDO2/WebAuthN/YKFWebAuthnClientData.h
@@ -0,0 +1,106 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * ---------------------------------------------------------------------------------------------------------------------
+ * @name YKFWebAuthnClientData Types
+ * ---------------------------------------------------------------------------------------------------------------------
+ */
+
+/*!
+ The list of supported WebAuthN operations.
+ */
+typedef NS_ENUM(NSUInteger, YKFWebAuthnClientDataType) {
+    
+    /// When the Client Data is used for creating a new credential.
+    YKFWebAuthnClientDataTypeCreate,
+    
+    /// When the Client Data is used to get an assertion from an existing credential.
+    YKFWebAuthnClientDataTypeGet
+};
+
+/**
+ * ---------------------------------------------------------------------------------------------------------------------
+ * @name YKFWebAuthnClientData
+ * ---------------------------------------------------------------------------------------------------------------------
+ */
+
+/*!
+ @class YKFWebAuthnClientData
+ 
+ @abstract
+    A representation of the WebAuthN Client Data used with the FIDO2 APIs.
+ */
+@interface YKFWebAuthnClientData: NSObject
+
+/*!
+ @abstract
+    The operation type the Client Data will be used for. This property has the value YKFWebAuthnClientDataTypeCreate
+    when creating new credentials and YKFWebAuthnClientDataTypeGet when getting an assertion from an existing credential.
+ */
+@property (nonatomic, readonly) YKFWebAuthnClientDataType type;
+
+/*!
+ @abstract
+    The challenge received from the WebAuthN Relying Party.
+ */
+@property (nonatomic, readonly) NSData *challenge;
+
+/*!
+ @abstract
+    This member contains the fully qualified origin of the requester, as provided to the authenticator by the client.
+ */
+@property (nonatomic, readonly) NSString *origin;
+
+/*!
+ @abstract
+    This is a derived property which returns the clientDataJson as defined by WebAuthN:
+    https://www.w3.org/TR/webauthn/#sec-client-data
+ */
+@property (nonatomic, nullable, readonly) NSData *jsonData;
+
+/*!
+ @abstract
+    This is a derived property which returns the SHA-256 of the clientDataJson.
+ */
+@property (nonatomic, nullable, readonly) NSData *clientDataHash;
+
+/*!
+ @method initWithType:challenge:origin:
+ 
+ @abstract
+    The designated initializer for this type. All the parameters are required to properly
+    initialise the Client Data.
+ 
+ @param type
+    The operation type.
+ @param challenge
+    The challenge to use for the operation.
+ @param origin
+    The origin of the Relying Party.
+ */
+- (nullable instancetype)initWithType:(YKFWebAuthnClientDataType)type challenge:(NSData *)challenge origin:(NSString *)origin NS_DESIGNATED_INITIALIZER;
+
+/*
+ Not available: use initWithType:challenge:origin:
+ */
+- (instancetype)init NS_UNAVAILABLE;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/YubiKit/YubiKit/Sessions/Shared/Services/FIDO2/WebAuthN/YKFWebAuthnClientData.m b/YubiKit/YubiKit/Sessions/Shared/Services/FIDO2/WebAuthN/YKFWebAuthnClientData.m
new file mode 100755
index 000000000..82b564b85
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Services/FIDO2/WebAuthN/YKFWebAuthnClientData.m
@@ -0,0 +1,73 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "YKFWebAuthnClientData.h"
+#import "YKFNSDataAdditions.h"
+#import "YKFAssert.h"
+
+@interface YKFWebAuthnClientData()
+
+@property (nonatomic, readwrite) YKFWebAuthnClientDataType type;
+@property (nonatomic, readwrite) NSData *challenge;
+@property (nonatomic, readwrite) NSString *origin;
+
+@end
+
+@implementation YKFWebAuthnClientData
+
+- (NSData *)jsonData {
+    NSString *websafeChallenge = self.challenge.ykf_websafeBase64EncodedString;
+    YKFAssertReturnValue(websafeChallenge, @"Could not websafeBase64 encode the challenge data.", nil);
+    
+    NSString *webauthnType = nil;
+    switch (self.type) {
+        case YKFWebAuthnClientDataTypeCreate:
+            webauthnType = @"webauthn.create";
+            break;
+        case YKFWebAuthnClientDataTypeGet:
+            webauthnType = @"webauthn.get";
+    }
+    YKFAssertReturnValue(webauthnType, @"Invalid WebAuthN method type.", nil);
+    
+    NSDictionary *jsonDictionary = @{@"type": webauthnType,
+                                     @"challenge": websafeChallenge,
+                                     @"origin": self.origin};
+    NSError *error = nil;
+    NSData *result = [NSJSONSerialization dataWithJSONObject:jsonDictionary options:0 error:&error];
+    YKFAssertReturnValue(!error && result, @"Could not serialize the clientDataJson.", nil);
+    
+    return result;
+}
+
+- (NSData *)clientDataHash {
+    NSData *clientData = self.jsonData;
+    YKFAssertReturnValue(clientData, @"Invalid WebAuthN client data JSON.", nil);
+    
+    return clientData.ykf_SHA256;
+}
+
+- (nullable instancetype)initWithType:(YKFWebAuthnClientDataType)type challenge:(NSData *)challenge origin:(NSString *)origin {
+    YKFAssertAbortInit(challenge);
+    YKFAssertAbortInit(origin);
+    
+    self = [super init];
+    if (self) {
+        self.type = type;
+        self.challenge = challenge;
+        self.origin = origin;
+    }
+    return self;
+}
+
+@end
diff --git a/YubiKit/YubiKit/Sessions/Shared/Services/FIDO2/YKFKeyFIDO2Service+Private.h b/YubiKit/YubiKit/Sessions/Shared/Services/FIDO2/YKFKeyFIDO2Service+Private.h
new file mode 100755
index 000000000..02a05fea0
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Services/FIDO2/YKFKeyFIDO2Service+Private.h
@@ -0,0 +1,27 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+
+@protocol YKFKeyConnectionControllerProtocol;
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface YKFKeyFIDO2Service()
+
+- (nullable instancetype)initWithConnectionController:(nonnull id<YKFKeyConnectionControllerProtocol>)connectionController NS_DESIGNATED_INITIALIZER;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/YubiKit/YubiKit/Sessions/Shared/Services/FIDO2/YKFKeyFIDO2Service.h b/YubiKit/YubiKit/Sessions/Shared/Services/FIDO2/YKFKeyFIDO2Service.h
new file mode 100755
index 000000000..e7947abf4
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Services/FIDO2/YKFKeyFIDO2Service.h
@@ -0,0 +1,356 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+#import "YKFKeyService.h"
+
+#import "YKFKeyFIDO2Request.h"
+#import "YKFKeyFIDO2MakeCredentialRequest.h"
+#import "YKFKeyFIDO2GetAssertionRequest.h"
+#import "YKFKeyFIDO2VerifyPinRequest.h"
+#import "YKFKeyFIDO2SetPinRequest.h"
+#import "YKFKeyFIDO2ChangePinRequest.h"
+
+#import "YKFKeyFIDO2GetInfoResponse.h"
+#import "YKFKeyFIDO2MakeCredentialResponse.h"
+#import "YKFKeyFIDO2GetAssertionResponse.h"
+
+/**
+ * ---------------------------------------------------------------------------------------------------------------------
+ * @name FIDO2 Service Response Blocks
+ * ---------------------------------------------------------------------------------------------------------------------
+ */
+
+/*!
+ @abstract
+    Response block used by FIDO2 requests which do not provide a result for the request.
+ 
+ @param error
+    In case of a failed request this parameter contains the error. If the request was successful
+    this parameter is nil.
+ */
+typedef void (^YKFKeyFIDO2ServiceCompletionBlock)
+    (NSError* _Nullable error);
+
+/*!
+ @abstract
+    Response block for [executeGetInfoRequestWithCompletion:] which provides the result for the execution
+    of the Get Info request.
+ 
+ @param response
+    The response of the request when it was successful. In case of error this parameter is nil.
+ 
+ @param error
+    In case of a failed request this parameter contains the error. If the request was successful this
+    parameter is nil.
+ */
+typedef void (^YKFKeyFIDO2ServiceGetInfoCompletionBlock)
+    (YKFKeyFIDO2GetInfoResponse* _Nullable response, NSError* _Nullable error);
+
+/*!
+ @abstract
+    Response block for [executeMakeCredentialRequest:completion:] which provides the result for the execution
+    of the Make Credential request.
+ 
+ @param response
+    The response of the request when it was successful. In case of error this parameter is nil.
+ 
+ @param error
+    In case of a failed request this parameter contains the error. If the request was successful this
+    parameter is nil.
+ */
+typedef void (^YKFKeyFIDO2ServiceMakeCredentialCompletionBlock)
+    (YKFKeyFIDO2MakeCredentialResponse* _Nullable response, NSError* _Nullable error);
+
+/*!
+ @abstract
+    Response block for [executeGetAssertionRequest:completion:] which provides the result for the execution
+    of the Get Assertion request.
+ 
+ @param response
+    The response of the request when it was successful. In case of error this parameter is nil.
+ 
+ @param error
+    In case of a failed request this parameter contains the error. If the request was successful this
+    parameter is nil.
+ */
+typedef void (^YKFKeyFIDO2ServiceGetAssertionCompletionBlock)
+    (YKFKeyFIDO2GetAssertionResponse* _Nullable response, NSError* _Nullable error);
+
+/*!
+ @abstract
+    Response block for [executeGetPinRetriesRequestWithCompletion:] which provides available number
+    of PIN retries.
+ 
+ @param retries
+    The number of PIN retries.
+ 
+ @param error
+    In case of a failed request this parameter contains the error. If the request was successful this
+    parameter is nil.
+ */
+typedef void (^YKFKeyFIDO2ServiceGetPinRetriesCompletionBlock)
+    (NSUInteger retries, NSError* _Nullable error);
+
+/**
+ * ---------------------------------------------------------------------------------------------------------------------
+ * @name FIDO2 Service Types
+ * ---------------------------------------------------------------------------------------------------------------------
+ */
+
+/*!
+ Enumerates the contextual states of the key when performing FIDO2 requests.
+ */
+typedef NS_ENUM(NSUInteger, YKFKeyFIDO2ServiceKeyState) {
+    
+    /// The key is not performing any FIDO2 operation.
+    YKFKeyFIDO2ServiceKeyStateIdle,
+    
+    /// The key is executing a FIDO2 request.
+    YKFKeyFIDO2ServiceKeyStateProcessingRequest,
+    
+    /// The user must touch the key to prove a human presence which allows the key to perform the current operation.
+    YKFKeyFIDO2ServiceKeyStateTouchKey
+};
+
+/**
+ * ---------------------------------------------------------------------------------------------------------------------
+ * @name YKFKeyFIDO2ServiceProtocol
+ * ---------------------------------------------------------------------------------------------------------------------
+ */
+
+NS_ASSUME_NONNULL_BEGIN
+
+/*!
+ @abstract
+    Defines the interface for YKFKeyFIDO2Service.
+ */
+@protocol YKFKeyFIDO2ServiceProtocol<NSObject>
+
+/*!
+ @abstract
+    This property provides the contextual state of the key when performing FIDO2 requests.
+ 
+ @discussion
+    This property is useful for checking the status of a FIDO2 request, when the default or specified
+    behaviour of the request requires UP. This property is KVO compliant and the application should
+    observe it to get asynchronous state updates.
+ */
+@property (nonatomic, assign, readonly) YKFKeyFIDO2ServiceKeyState keyState;
+
+/*!
+ @method executeGetInfoRequestWithCompletion:
+ 
+ @abstract
+    Sends to the key a FIDO2 Get Info request to retrieve the authenticator properties. The request
+    is performed asynchronously on a background execution queue.
+ 
+ @param completion
+    The response block which is executed after the request was processed by the key. The completion block
+    will be executed on a background thread. If the intention is to update the UI, dispatch the results
+    on the main thread to avoid an UIKit assertion.
+ 
+ @note
+    This method is thread safe and can be invoked from any thread (main or a background thread).
+ */
+- (void)executeGetInfoRequestWithCompletion:(YKFKeyFIDO2ServiceGetInfoCompletionBlock)completion;
+
+/*!
+ @method executeVerifyPinRequest:completion:
+ 
+ @abstract
+    Authenticates the session with the FIDO2 application from the key. This should be done once
+    per session lifetime (while the key is plugged in) or after the user verification was cleared
+    by calling [clearUserVerification].
+ 
+ @discussion
+    Once authenticated, the library will automatically attach the required PIN authentication parameters
+    to the subsequent requests against the key, when necessary.
+ 
+ @param completion
+    The response block which is executed after the request was processed by the key. The completion block
+    will be executed on a background thread. If the intention is to update the UI, dispatch the results
+    on the main thread to avoid an UIKit assertion.
+ 
+ @note
+    This method is thread safe and can be invoked from any thread (main or a background thread).
+ */
+- (void)executeVerifyPinRequest:(YKFKeyFIDO2VerifyPinRequest *)request completion:(YKFKeyFIDO2ServiceCompletionBlock)completion;
+
+/*!
+ @method clearUserVerification
+
+ @abstract
+    Clears the cached user verification if the user authenticated with [executeVerifyPinRequest:completion:].
+ */
+- (void)clearUserVerification;
+
+/*!
+ @method executeSetPinRequest:completion:
+ 
+ @abstract
+    Sets a PIN for the key FIDO2 application.
+ 
+ @discussion
+    If the key FIDO2 application has a PIN this method will return an error and change PIN should be used
+    instead. The PIN can be an alphanumeric string with the length in the range [4, 255].
+ 
+ @param completion
+    The response block which is executed after the request was processed by the key. The completion block
+    will be executed on a background thread. If the intention is to update the UI, dispatch the results
+    on the main thread to avoid an UIKit assertion.
+ 
+ @note
+    This method is thread safe and can be invoked from any thread (main or a background thread).
+ */
+- (void)executeSetPinRequest:(YKFKeyFIDO2SetPinRequest *)request completion:(YKFKeyFIDO2ServiceCompletionBlock)completion;
+
+/*!
+ @method executeChangePinRequest:completion:
+ 
+ @abstract
+    Changes the existing PIN for the key FIDO2 application.
+ 
+ @discussion
+    If the key FIDO2 application doesn't have a PIN, this method will return an error and set PIN should
+    be used instead. The PIN can be an alphanumeric string with the length in the range [4, 255].
+ 
+ @param completion
+    The response block which is executed after the request was processed by the key. The completion block
+    will be executed on a background thread. If the intention is to update the UI, dispatch the results
+    on the main thread to avoid an UIKit assertion.
+ 
+ @note
+    This method is thread safe and can be invoked from any thread (main or a background thread).
+ */
+- (void)executeChangePinRequest:(YKFKeyFIDO2ChangePinRequest *)request completion:(YKFKeyFIDO2ServiceCompletionBlock)completion;
+
+/*!
+ @method executeGetPinRetriesWithCompletion:
+ 
+ @abstract
+    Requests the number of PIN retries from the key FIDO2 application.
+ 
+ @param completion
+    The response block which is executed after the request was processed by the key. The completion block
+    will be executed on a background thread. If the intention is to update the UI, dispatch the results
+    on the main thread to avoid an UIKit assertion.
+ 
+ @note
+    This method is thread safe and can be invoked from any thread (main or a background thread).
+ */
+- (void)executeGetPinRetriesWithCompletion:(YKFKeyFIDO2ServiceGetPinRetriesCompletionBlock)completion;
+
+/*!
+ @method executeMakeCredentialRequest:completion:
+ 
+ @abstract
+    Sends to the key a FIDO2 Make Credential request to create/update a FIDO2 credential. The request
+    is performed asynchronously on a background execution queue.
+ 
+ @param completion
+    The response block which is executed after the request was processed by the key. The completion block
+    will be executed on a background thread. If the intention is to update the UI, dispatch the results
+    on the main thread to avoid an UIKit assertion.
+ 
+ @note
+    This method is thread safe and can be invoked from any thread (main or a background thread).
+ */
+- (void)executeMakeCredentialRequest:(YKFKeyFIDO2MakeCredentialRequest *)request completion:(YKFKeyFIDO2ServiceMakeCredentialCompletionBlock)completion;
+
+/*!
+ @method executeGetAssertionRequest:completion:
+ 
+ @abstract
+    Sends to the key a FIDO2 Get Assertion request to retrieve signatures for FIDO2 credentials. The request
+    is performed asynchronously on a background execution queue.
+ 
+ @param completion
+    The response block which is executed after the request was processed by the key. The completion block
+    will be executed on a background thread. If the intention is to update the UI, dispatch the results
+    on the main thread to avoid an UIKit assertion.
+ 
+ @note
+    This method is thread safe and can be invoked from any thread (main or a background thread).
+ */
+- (void)executeGetAssertionRequest:(YKFKeyFIDO2GetAssertionRequest *)request completion:(YKFKeyFIDO2ServiceGetAssertionCompletionBlock)completion;
+
+/*!
+ @method executeGetNextAssertionRequest:completion:
+ 
+ @abstract
+    Sends to the key a FIDO2 Get Next Assertion request to retrieve the next assertion from the list of
+    specified FIDO2 credentials in a previous Get Assertion request. The request is performed asynchronously on
+    a background execution queue.
+ 
+ @param completion
+    The response block which is executed after the request was processed by the key. The completion block
+    will be executed on a background thread. If the intention is to update the UI, dispatch the results
+    on the main thread to avoid an UIKit assertion.
+ 
+ @note
+    This method is thread safe and can be invoked from any thread (main or a background thread).
+ */
+- (void)executeGetNextAssertionWithCompletion:(YKFKeyFIDO2ServiceGetAssertionCompletionBlock)completion;
+
+/*!
+ @method executeResetRequestWithCompletion:
+ 
+ @abstract
+    Sends to the key a FIDO2 Reset to revert the key FIDO2 application to factory settings.
+ 
+ @discussion
+    The reset operation is destructive. It will delete all stored credentials, including the possibility to
+    compute the non-resident keys which were created with the authenticator before resetting it. To avoid an
+    accidental reset during the regular operation, the reset request must be executed within 5 seconds after
+    the key was powered up (plugged in) and it requires user presence (touch).
+ 
+ @param completion
+    The response block which is executed after the request was processed by the key. The completion block
+    will be executed on a background thread. If the intention is to update the UI, dispatch the results
+    on the main thread to avoid an UIKit assertion.
+ 
+ @note
+    This method is thread safe and can be invoked from any thread (main or a background thread).
+ */
+- (void)executeResetRequestWithCompletion:(YKFKeyFIDO2ServiceCompletionBlock)completion;
+
+@end
+
+/**
+ * ---------------------------------------------------------------------------------------------------------------------
+ * @name YKFKeyFIDO2Service
+ * ---------------------------------------------------------------------------------------------------------------------
+ */
+
+/*!
+ @class YKFKeyFIDO2Service
+ 
+ @abstract
+    Provides the interface for executing FIDO2/CTAP2 requests with the key.
+ @discussion
+    The FIDO2 service is mantained by the key session which controls its lifecycle. The application must not
+    create one. It has to use only the single shared instance from YKFAccessorySession and sync its usage with
+    the session state.
+ */
+@interface YKFKeyFIDO2Service: YKFKeyService<YKFKeyFIDO2ServiceProtocol>
+
+/*
+ Not available: use only the shared instance from the YKFAccessorySession.
+ */
+- (instancetype)init NS_UNAVAILABLE;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/YubiKit/YubiKit/Sessions/Shared/Services/FIDO2/YKFKeyFIDO2Service.m b/YubiKit/YubiKit/Sessions/Shared/Services/FIDO2/YKFKeyFIDO2Service.m
new file mode 100755
index 000000000..c6166ad66
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Services/FIDO2/YKFKeyFIDO2Service.m
@@ -0,0 +1,695 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "YKFKeyFIDO2Service.h"
+#import "YKFKeyFIDO2Service+Private.h"
+#import "YKFAccessoryConnectionController.h"
+#import "YKFKeyFIDO2Error.h"
+#import "YKFKeyAPDUError.h"
+#import "YKFKeyCommandConfiguration.h"
+#import "YKFLogger.h"
+#import "YKFBlockMacros.h"
+#import "YKFNSDataAdditions.h"
+#import "YKFAssert.h"
+
+#import "YKFFIDO2PinAuthKey.h"
+#import "YKFKeyFIDO2ClientPinRequest.h"
+#import "YKFKeyFIDO2ClientPinResponse.h"
+
+#import "YKFSelectFIDO2ApplicationAPDU.h"
+#import "YKFFIDO2MakeCredentialAPDU.h"
+#import "YKFFIDO2GetAssertionAPDU.h"
+#import "YKFFIDO2GetNextAssertionAPDU.h"
+#import "YKFFIDO2TouchPoolingAPDU.h"
+#import "YKFFIDO2ClientPinAPDU.h"
+#import "YKFFIDO2GetInfoAPDU.h"
+#import "YKFFIDO2ResetAPDU.h"
+
+#import "YKFKeyFIDO2GetInfoResponse+Private.h"
+#import "YKFKeyFIDO2MakeCredentialResponse+Private.h"
+#import "YKFKeyFIDO2GetAssertionResponse+Private.h"
+
+#import "YKFKeyFIDO2MakeCredentialRequest+Private.h"
+#import "YKFKeyFIDO2GetAssertionRequest+Private.h"
+
+#import "YKFNSDataAdditions+Private.h"
+#import "YKFKeySessionError+Private.h"
+#import "YKFKeyService+Private.h"
+#import "YKFKeyFIDO2Request+Private.h"
+#import "YKFAPDU+Private.h"
+
+#pragma mark - Private Response Blocks
+
+typedef void (^YKFKeyFIDO2ServiceResultCompletionBlock)
+    (NSData* _Nullable response, NSError* _Nullable error);
+
+typedef void (^YKFKeyFIDO2ServiceClientPinCompletionBlock)
+    (YKFKeyFIDO2ClientPinResponse* _Nullable response, NSError* _Nullable error);
+
+typedef void (^YKFKeyFIDO2ServiceClientPinSharedSecretCompletionBlock)
+    (NSData* _Nullable sharedSecret, YKFCBORMap* _Nullable cosePlatformPublicKey, NSError* _Nullable error);
+
+#pragma mark - YKFKeyFIDO2Service
+
+@interface YKFKeyFIDO2Service()
+
+@property (nonatomic, assign, readwrite) YKFKeyFIDO2ServiceKeyState keyState;
+@property (nonatomic) id<YKFKeyConnectionControllerProtocol> connectionController;
+
+// The cached authenticator pinToken, assigned after a successful validation.
+@property NSData *pinToken;
+// Keeps the state of the application selection to avoid reselecting the application.
+@property BOOL applicationSelected;
+
+@end
+
+@implementation YKFKeyFIDO2Service
+
+- (instancetype)initWithConnectionController:(id<YKFKeyConnectionControllerProtocol>)connectionController {
+    YKFAssertAbortInit(connectionController);
+    
+    self = [super init];
+    if (self) {
+        self.connectionController = connectionController;
+    }
+    return self;
+}
+
+#pragma mark - Key State
+
+- (void)updateKeyState:(YKFKeyFIDO2ServiceKeyState)keyState {
+    if (self.keyState == keyState) {
+        return;
+    }
+    self.keyState = keyState;
+}
+
+#pragma mark - Public Requests
+
+- (void)executeGetInfoRequestWithCompletion:(YKFKeyFIDO2ServiceGetInfoCompletionBlock)completion {
+    YKFParameterAssertReturn(completion);
+    
+    YKFKeyFIDO2Request *fido2Request = [[YKFKeyFIDO2Request alloc] init];
+    fido2Request.apdu = [[YKFFIDO2GetInfoAPDU alloc] init];
+    
+    ykf_weak_self();
+    [self executeFIDO2Request:fido2Request completion:^(NSData * response, NSError *error) {
+        ykf_safe_strong_self();
+        if (error) {
+            completion(nil, error);
+            return;
+        }
+        
+        NSData *cborData = [strongSelf cborFromKeyResponsePayloadData:response];
+        YKFKeyFIDO2GetInfoResponse *getInfoResponse = [[YKFKeyFIDO2GetInfoResponse alloc] initWithCBORData:cborData];
+        
+        if (getInfoResponse) {
+            completion(getInfoResponse, nil);
+        } else {
+            completion(nil, [YKFKeyFIDO2Error errorWithCode:YKFKeyFIDO2ErrorCodeINVALID_CBOR]);
+        }
+    }];
+}
+
+- (void)executeVerifyPinRequest:(YKFKeyFIDO2VerifyPinRequest *)request completion:(YKFKeyFIDO2ServiceCompletionBlock)completion {
+    YKFParameterAssertReturn(request);
+    YKFParameterAssertReturn(request.pin);
+    YKFParameterAssertReturn(completion);
+
+    [self clearUserVerification];
+    
+    ykf_weak_self();
+    [self executeGetSharedSecretWithCompletion:^(NSData *sharedSecret, YKFCBORMap *cosePlatformPublicKey, NSError *error) {
+        ykf_safe_strong_self();
+        if (error) {
+            completion(error);
+            return;
+        }        
+        YKFParameterAssertReturn(sharedSecret)
+        YKFParameterAssertReturn(cosePlatformPublicKey)
+        
+        // Get the authenticator pinToken
+        YKFKeyFIDO2ClientPinRequest *clientPinGetPinTokenRequest = [[YKFKeyFIDO2ClientPinRequest alloc] init];
+        clientPinGetPinTokenRequest.pinProtocol = 1;
+        clientPinGetPinTokenRequest.subCommand = YKFKeyFIDO2ClientPinRequestSubCommandGetPINToken;
+        clientPinGetPinTokenRequest.keyAgreement = cosePlatformPublicKey;
+        
+        NSData *pinData = [request.pin dataUsingEncoding:NSUTF8StringEncoding];
+        NSData *pinHash = [[pinData ykf_SHA256] subdataWithRange:NSMakeRange(0, 16)];
+        clientPinGetPinTokenRequest.pinHashEnc = [pinHash ykf_aes256EncryptedDataWithKey:sharedSecret];
+        
+        [strongSelf executeClientPinRequest:clientPinGetPinTokenRequest completion:^(YKFKeyFIDO2ClientPinResponse *response, NSError *error) {
+            if (error) {
+                completion(error);
+                return;
+            }
+            NSData *encryptedPinToken = response.pinToken;
+            if (!encryptedPinToken) {
+                completion([YKFKeyFIDO2Error errorWithCode:YKFKeyFIDO2ErrorCodeINVALID_CBOR]);
+                return;
+            }
+            
+            // Cache the pinToken
+            strongSelf.pinToken = [response.pinToken ykf_aes256DecryptedDataWithKey:sharedSecret];
+            
+            if (!strongSelf.pinToken) {
+                completion([YKFKeyFIDO2Error errorWithCode:YKFKeyFIDO2ErrorCodeINVALID_CBOR]);
+            } else {
+                completion(nil);
+            }
+        }];
+    }];
+}
+
+- (void)clearUserVerification {
+    if (!self.pinToken && !self.applicationSelected) {
+        return;
+    }
+    
+    YKFLogVerbose(@"Clearing FIDO2 Service user verification.");
+    
+    ykf_weak_self();
+    [self.connectionController dispatchOnSequentialQueue:^{
+        ykf_safe_strong_self();
+        strongSelf.pinToken = nil;
+        strongSelf.applicationSelected = NO; // Force also an application re-selection.
+    }];
+}
+
+- (void)executeChangePinRequest:(nonnull YKFKeyFIDO2ChangePinRequest *)request completion:(nonnull YKFKeyFIDO2ServiceCompletionBlock)completion {
+    YKFParameterAssertReturn(request);
+    YKFParameterAssertReturn(request.pinOld);
+    YKFParameterAssertReturn(request.pinNew);
+    YKFParameterAssertReturn(completion);
+
+    if (request.pinOld.length < 4 || request.pinNew.length < 4 ||
+        request.pinOld.length > 255 || request.pinNew.length > 255) {
+        completion([YKFKeyFIDO2Error errorWithCode:YKFKeyFIDO2ErrorCodePIN_POLICY_VIOLATION]);
+        return;
+    }
+    
+    ykf_weak_self();
+    [self executeGetSharedSecretWithCompletion:^(NSData *sharedSecret, YKFCBORMap *cosePlatformPublicKey, NSError *error) {
+        ykf_safe_strong_self();
+        if (error) {
+            completion(error);
+            return;
+        }
+        YKFParameterAssertReturn(sharedSecret)
+        YKFParameterAssertReturn(cosePlatformPublicKey)
+        
+        // Change the PIN
+        YKFKeyFIDO2ClientPinRequest *changePinRequest = [[YKFKeyFIDO2ClientPinRequest alloc] init];
+        NSData *oldPinData = [request.pinOld dataUsingEncoding:NSUTF8StringEncoding];
+        NSData *newPinData = [[request.pinNew dataUsingEncoding:NSUTF8StringEncoding] ykf_fido2PaddedPinData];
+
+        changePinRequest.pinProtocol = 1;
+        changePinRequest.subCommand = YKFKeyFIDO2ClientPinRequestSubCommandChangePIN;
+        changePinRequest.keyAgreement = cosePlatformPublicKey;
+
+        NSData *oldPinHash = [[oldPinData ykf_SHA256] subdataWithRange:NSMakeRange(0, 16)];
+        changePinRequest.pinHashEnc = [oldPinHash ykf_aes256EncryptedDataWithKey:sharedSecret];
+
+        changePinRequest.pinEnc = [newPinData ykf_aes256EncryptedDataWithKey:sharedSecret];
+        
+        NSMutableData *pinAuthData = [NSMutableData dataWithData:changePinRequest.pinEnc];
+        [pinAuthData appendData:changePinRequest.pinHashEnc];
+        changePinRequest.pinAuth = [[pinAuthData ykf_fido2HMACWithKey:sharedSecret] subdataWithRange:NSMakeRange(0, 16)];
+        
+        [strongSelf executeClientPinRequest:changePinRequest completion:^(YKFKeyFIDO2ClientPinResponse *response, NSError *error) {
+            if (error) {
+                completion(error);
+                return;
+            }
+            // clear the cached pin token.
+            strongSelf.pinToken = nil;
+            completion(nil);
+        }];
+    }];
+}
+
+- (void)executeSetPinRequest:(nonnull YKFKeyFIDO2SetPinRequest *)request completion:(nonnull YKFKeyFIDO2ServiceCompletionBlock)completion {
+    YKFParameterAssertReturn(request);
+    YKFParameterAssertReturn(request.pin);
+    YKFParameterAssertReturn(completion);
+
+    if (request.pin.length < 4 || request.pin.length > 255) {
+        completion([YKFKeyFIDO2Error errorWithCode:YKFKeyFIDO2ErrorCodePIN_POLICY_VIOLATION]);
+        return;
+    }
+    
+    ykf_weak_self();
+    [self executeGetSharedSecretWithCompletion:^(NSData *sharedSecret, YKFCBORMap *cosePlatformPublicKey, NSError *error) {
+        ykf_safe_strong_self();
+        if (error) {
+            completion(error);
+            return;
+        }
+        YKFParameterAssertReturn(sharedSecret)
+        YKFParameterAssertReturn(cosePlatformPublicKey)
+        
+        // Set the new PIN
+        YKFKeyFIDO2ClientPinRequest *setPinRequest = [[YKFKeyFIDO2ClientPinRequest alloc] init];
+        setPinRequest.pinProtocol = 1;
+        setPinRequest.subCommand = YKFKeyFIDO2ClientPinRequestSubCommandSetPIN;
+        setPinRequest.keyAgreement = cosePlatformPublicKey;
+        
+        NSData *pinData = [[request.pin dataUsingEncoding:NSUTF8StringEncoding] ykf_fido2PaddedPinData];
+        
+        setPinRequest.pinEnc = [pinData ykf_aes256EncryptedDataWithKey:sharedSecret];
+        setPinRequest.pinAuth = [[setPinRequest.pinEnc ykf_fido2HMACWithKey:sharedSecret] subdataWithRange:NSMakeRange(0, 16)];
+        
+        [strongSelf executeClientPinRequest:setPinRequest completion:^(YKFKeyFIDO2ClientPinResponse *response, NSError *error) {
+            if (error) {
+                completion(error);
+                return;
+            }
+            completion(nil);
+        }];
+    }];
+}
+
+- (void)executeGetPinRetriesWithCompletion:(YKFKeyFIDO2ServiceGetPinRetriesCompletionBlock)completion {
+    YKFParameterAssertReturn(completion);
+    
+    YKFKeyFIDO2ClientPinRequest *pinRetriesRequest = [[YKFKeyFIDO2ClientPinRequest alloc] init];
+    pinRetriesRequest.pinProtocol = 1;
+    pinRetriesRequest.subCommand = YKFKeyFIDO2ClientPinRequestSubCommandGetRetries;
+    
+    [self executeClientPinRequest:pinRetriesRequest completion:^(YKFKeyFIDO2ClientPinResponse *response, NSError *error) {
+        if (error) {
+            completion(0, error);
+            return;
+        }
+        completion(response.retries, nil);
+    }];
+}
+
+- (void)executeMakeCredentialRequest:(YKFKeyFIDO2MakeCredentialRequest *)request completion:(YKFKeyFIDO2ServiceMakeCredentialCompletionBlock)completion {
+    YKFParameterAssertReturn(request);
+    YKFParameterAssertReturn(completion);
+    
+    // Attach the PIN authentication if the pinToken is present.
+    if (self.pinToken) {
+        YKFParameterAssertReturn(request.clientDataHash);
+        request.pinProtocol = 1;
+        NSData *hmac = [request.clientDataHash ykf_fido2HMACWithKey:self.pinToken];
+        request.pinAuth = [hmac subdataWithRange:NSMakeRange(0, 16)];
+        if (!request.pinAuth) {
+            completion(nil, [YKFKeyFIDO2Error errorWithCode:YKFKeyFIDO2ErrorCodeOTHER]);
+        }
+    }
+    
+    YKFFIDO2MakeCredentialAPDU *apdu = [[YKFFIDO2MakeCredentialAPDU alloc] initWithRequest:request];
+    if (!apdu) {
+        YKFKeySessionError *error = [YKFKeyFIDO2Error errorWithCode:YKFKeyFIDO2ErrorCodeOTHER];
+        completion(nil, error);
+        return;
+    }
+    request.apdu = apdu;
+    
+    ykf_weak_self();
+    [self executeFIDO2Request:request completion:^(NSData *response, NSError *error) {
+        ykf_safe_strong_self();
+        if (error) {
+            completion(nil, error);
+            return;
+        }
+        
+        NSData *cborData = [strongSelf cborFromKeyResponsePayloadData:response];
+        YKFKeyFIDO2MakeCredentialResponse *makeCredentialResponse = [[YKFKeyFIDO2MakeCredentialResponse alloc] initWithCBORData:cborData];
+        
+        if (makeCredentialResponse) {
+            completion(makeCredentialResponse, nil);
+        } else {
+            completion(nil, [YKFKeyFIDO2Error errorWithCode:YKFKeyFIDO2ErrorCodeINVALID_CBOR]);
+        }
+    }];
+}
+
+- (void)executeGetAssertionRequest:(YKFKeyFIDO2GetAssertionRequest *)request completion:(YKFKeyFIDO2ServiceGetAssertionCompletionBlock)completion {
+    YKFParameterAssertReturn(request);
+    YKFParameterAssertReturn(completion);
+    
+    // Attach the PIN authentication if the pinToken is present.
+    if (self.pinToken) {
+        YKFParameterAssertReturn(request.clientDataHash);
+        request.pinProtocol = 1;
+        NSData *hmac = [request.clientDataHash ykf_fido2HMACWithKey:self.pinToken];
+        request.pinAuth = [hmac subdataWithRange:NSMakeRange(0, 16)];
+        if (!request.pinAuth) {
+            completion(nil, [YKFKeyFIDO2Error errorWithCode:YKFKeyFIDO2ErrorCodeOTHER]);
+        }
+    }
+    
+    YKFFIDO2GetAssertionAPDU *apdu = [[YKFFIDO2GetAssertionAPDU alloc] initWithRequest:request];
+    if (!apdu) {
+        YKFKeySessionError *error = [YKFKeyFIDO2Error errorWithCode:YKFKeyFIDO2ErrorCodeOTHER];
+        completion(nil, error);
+        return;
+    }
+    request.apdu = apdu;
+    
+    ykf_weak_self();
+    [self executeFIDO2Request:request completion:^(NSData *response, NSError *error) {
+        ykf_safe_strong_self();
+        if (error) {
+            completion(nil, error);
+            return;
+        }
+        
+        NSData *cborData = [strongSelf cborFromKeyResponsePayloadData:response];
+        YKFKeyFIDO2GetAssertionResponse *getAssertionResponse = [[YKFKeyFIDO2GetAssertionResponse alloc] initWithCBORData:cborData];
+        
+        if (getAssertionResponse) {
+            completion(getAssertionResponse, nil);
+        } else {
+            completion(nil, [YKFKeyFIDO2Error errorWithCode:YKFKeyFIDO2ErrorCodeINVALID_CBOR]);
+        }
+    }];
+}
+
+- (void)executeGetNextAssertionWithCompletion:(YKFKeyFIDO2ServiceGetAssertionCompletionBlock)completion {
+    YKFParameterAssertReturn(completion);
+    
+    YKFKeyFIDO2Request *fido2Request = [[YKFKeyFIDO2Request alloc] init];
+    fido2Request.apdu = [[YKFFIDO2GetNextAssertionAPDU alloc] init];
+    
+    ykf_weak_self();
+    [self executeFIDO2Request:fido2Request completion:^(NSData *response, NSError *error) {
+        ykf_safe_strong_self();
+        if (error) {
+            completion(nil, error);
+            return;
+        }
+        
+        NSData *cborData = [strongSelf cborFromKeyResponsePayloadData:response];
+        YKFKeyFIDO2GetAssertionResponse *getAssertionResponse = [[YKFKeyFIDO2GetAssertionResponse alloc] initWithCBORData:cborData];
+        
+        if (getAssertionResponse) {
+            completion(getAssertionResponse, nil);
+        } else {
+            completion(nil, [YKFKeyFIDO2Error errorWithCode:YKFKeyFIDO2ErrorCodeINVALID_CBOR]);
+        }
+    }];
+}
+
+- (void)executeResetRequestWithCompletion:(YKFKeyFIDO2ServiceCompletionBlock)completion {
+    YKFParameterAssertReturn(completion);
+    
+    YKFKeyFIDO2Request *fido2Request = [[YKFKeyFIDO2Request alloc] init];
+    fido2Request.apdu = [[YKFFIDO2ResetAPDU alloc] init];
+    
+    ykf_weak_self();
+    [self executeFIDO2Request:fido2Request completion:^(NSData *response, NSError *error) {
+        ykf_strong_self();
+        if (!error) {
+            [strongSelf clearUserVerification];
+        }
+        completion(error);
+    }];
+}
+
+#pragma mark - Private Requests
+
+- (void)executeClientPinRequest:(YKFKeyFIDO2ClientPinRequest *)request completion:(YKFKeyFIDO2ServiceClientPinCompletionBlock)completion {
+    YKFParameterAssertReturn(request);
+    YKFParameterAssertReturn(completion);
+
+    YKFFIDO2ClientPinAPDU *apdu = [[YKFFIDO2ClientPinAPDU alloc] initWithRequest:request];
+    if (!apdu) {
+        YKFKeySessionError *error = [YKFKeyFIDO2Error errorWithCode:YKFKeyFIDO2ErrorCodeOTHER];
+        completion(nil, error);
+        return;
+    }
+    request.apdu = apdu;
+    
+    ykf_weak_self();
+    [self executeFIDO2Request:request completion:^(NSData *response, NSError *error) {
+        ykf_safe_strong_self();
+        if (error) {
+            completion(nil, error);
+            return;
+        }
+        
+        NSData *cborData = [strongSelf cborFromKeyResponsePayloadData:response];
+        YKFKeyFIDO2ClientPinResponse *clientPinResponse = nil;
+        
+        // In case of Set/Change PIN no CBOR payload is returned.
+        if (cborData.length) {
+            clientPinResponse = [[YKFKeyFIDO2ClientPinResponse alloc] initWithCBORData:cborData];
+        }
+        
+        if (clientPinResponse) {
+            completion(clientPinResponse, nil);
+        } else {
+            if (cborData.length) {
+                completion(nil, [YKFKeyFIDO2Error errorWithCode:YKFKeyFIDO2ErrorCodeINVALID_CBOR]);
+            } else {
+                completion(nil, nil);
+            }
+        }
+    }];
+}
+
+- (void)executeGetSharedSecretWithCompletion:(YKFKeyFIDO2ServiceClientPinSharedSecretCompletionBlock)completion {
+    YKFParameterAssertReturn(completion);
+    
+    // If there is a cached user verification?
+    
+    ykf_weak_self();
+    [self.connectionController dispatchOnSequentialQueue:^{
+        ykf_safe_strong_self();
+        
+        // Generate the platform key.
+        YKFFIDO2PinAuthKey *platformKey = [[YKFFIDO2PinAuthKey alloc] init];
+        if (!platformKey) {
+            completion(nil, nil, [YKFKeyFIDO2Error errorWithCode:YKFKeyFIDO2ErrorCodeOTHER]);
+            return;
+        }
+        YKFCBORMap *cosePlatformPublicKey = platformKey.cosePublicKey;
+        if (!cosePlatformPublicKey) {
+            completion(nil, nil, [YKFKeyFIDO2Error errorWithCode:YKFKeyFIDO2ErrorCodeOTHER]);
+            return;
+        }
+        
+        // Get the authenticator public key.
+        YKFKeyFIDO2ClientPinRequest *clientPinKeyAgreementRequest = [[YKFKeyFIDO2ClientPinRequest alloc] init];
+        clientPinKeyAgreementRequest.pinProtocol = 1;
+        clientPinKeyAgreementRequest.subCommand = YKFKeyFIDO2ClientPinRequestSubCommandGetKeyAgreement;
+        clientPinKeyAgreementRequest.keyAgreement = cosePlatformPublicKey;
+        
+        [strongSelf executeClientPinRequest:clientPinKeyAgreementRequest completion:^(YKFKeyFIDO2ClientPinResponse *response, NSError *error) {
+            if (error) {
+                completion(nil, nil, error);
+                return;
+            }
+            NSDictionary *authenticatorKeyData = response.keyAgreement;
+            if (!authenticatorKeyData) {
+                completion(nil, nil, [YKFKeyFIDO2Error errorWithCode:YKFKeyFIDO2ErrorCodeINVALID_CBOR]);
+                return;
+            }
+            YKFFIDO2PinAuthKey *authenticatorKey = [[YKFFIDO2PinAuthKey alloc] initWithCosePublicKey:authenticatorKeyData];
+            if (!authenticatorKey) {
+                completion(nil, nil, [YKFKeyFIDO2Error errorWithCode:YKFKeyFIDO2ErrorCodeINVALID_CBOR]);
+                return;
+            }
+            
+            // Generate the shared secret.
+            NSData *sharedSecret = [platformKey sharedSecretWithAuthKey:authenticatorKey];
+            if (!sharedSecret) {
+                completion(nil, nil, [YKFKeyFIDO2Error errorWithCode:YKFKeyFIDO2ErrorCodeOTHER]);
+                return;
+            }
+            sharedSecret = [sharedSecret ykf_SHA256];
+            
+            // Success
+            completion(sharedSecret, cosePlatformPublicKey, nil);
+        }];
+    }];
+}
+
+#pragma mark - Application selection
+
+- (void)selectFIDO2ApplicationWithCompletion:(void (^)(NSError *))completion {
+    YKFParameterAssertReturn(completion);
+    
+    if (self.applicationSelected) {
+        completion(nil);
+        return;
+    }
+    
+    YKFAPDU *selectFIDO2ApplicationAPDU = [[YKFSelectFIDO2ApplicationAPDU alloc] init];
+    
+    ykf_weak_self();
+    [self.connectionController execute:selectFIDO2ApplicationAPDU
+                         configuration:[YKFKeyCommandConfiguration fastCommandCofiguration]
+                            completion:^(NSData *result, NSError *error, NSTimeInterval executionTime) {
+        ykf_safe_strong_self();
+        NSError *returnedError = nil;
+        
+        if (error) {
+            returnedError = error;
+        } else {
+            int statusCode = [strongSelf statusCodeFromKeyResponse: result];
+            switch (statusCode) {
+                case YKFKeyAPDUErrorCodeNoError:
+                    break;
+                    
+                case YKFKeyAPDUErrorCodeMissingFile:
+                    returnedError = [YKFKeySessionError errorWithCode:YKFKeySessionErrorMissingApplicationCode];
+                    break;
+                    
+                default:
+                    returnedError = [YKFKeySessionError errorWithCode:statusCode];
+            }
+        }
+
+        if (!returnedError) {
+            strongSelf.applicationSelected = YES;
+        }
+        completion(returnedError);
+    }];
+}
+
+#pragma mark - Request Execution
+
+- (void)executeFIDO2Request:(YKFKeyFIDO2Request *)request completion:(YKFKeyFIDO2ServiceResultCompletionBlock)completion {
+    YKFParameterAssertReturn(request);
+    YKFParameterAssertReturn(completion);
+    
+    [self.delegate keyService:self willExecuteRequest:request];
+    
+    [self updateKeyState:YKFKeyFIDO2ServiceKeyStateProcessingRequest];
+    
+    ykf_weak_self();
+    [self selectFIDO2ApplicationWithCompletion:^(NSError *error) {
+        ykf_safe_strong_self();
+        if (error) {
+            [strongSelf updateKeyState:YKFKeyFIDO2ServiceKeyStateIdle];
+            completion(nil, error);
+            return;
+        }
+        [strongSelf executeFIDO2RequestWithoutApplicationSelection:request completion:completion];
+    }];
+}
+
+- (void)executeFIDO2RequestWithoutApplicationSelection:(YKFKeyFIDO2Request *)request completion:(YKFKeyFIDO2ServiceResultCompletionBlock)completion {
+    YKFParameterAssertReturn(request);
+    YKFParameterAssertReturn(completion);
+    
+    ykf_weak_self();
+    [self.connectionController execute:request.apdu completion:^(NSData *result, NSError *error, NSTimeInterval executionTime) {
+        ykf_safe_strong_self();
+        
+        if (error) {
+            if (error.code == YKFKeyFIDO2ErrorCodeTIMEOUT) {
+                strongSelf.applicationSelected = NO;
+            }
+            
+            [strongSelf updateKeyState:YKFKeyFIDO2ServiceKeyStateIdle];
+            completion(nil, error);
+            return;
+        }
+        
+        UInt16 statusCode = [strongSelf statusCodeFromKeyResponse: result];
+        
+        switch (statusCode) {
+            case YKFKeyAPDUErrorCodeNoError: {
+                UInt8 fido2Error = [strongSelf errorCodeFromKeyResponsePayloadData:result];
+                
+                if (fido2Error != YKFKeyFIDO2ErrorCodeSUCCESS) {
+                    completion(nil, [YKFKeyFIDO2Error errorWithCode:fido2Error]);
+                } else {
+                    completion(result, nil);
+                }
+                [strongSelf updateKeyState:YKFKeyFIDO2ServiceKeyStateIdle];
+            }
+            break;
+                
+            case YKFKeyAPDUErrorCodeFIDO2TouchRequired: {
+                [strongSelf handleTouchRequired:request completion:completion];
+            }
+            break;
+                
+            case YKFKeyAPDUErrorCodeInsNotSupported: {
+                [strongSelf updateKeyState:YKFKeyFIDO2ServiceKeyStateIdle];
+                completion(nil, [YKFKeySessionError errorWithCode:YKFKeySessionErrorMissingApplicationCode]);
+            }
+            break;
+                
+            default: {
+                [strongSelf updateKeyState:YKFKeyFIDO2ServiceKeyStateIdle];
+                completion(nil, [YKFKeyFIDO2Error errorWithCode:statusCode]);
+            }
+        }
+    }];
+}
+
+#pragma mark - YKFKeyServiceProtocol
+
+- (void)keyService:(YKFKeyService *)service willExecuteRequest:(YKFKeyRequest *)request {
+    if (!service || (service == self)) {
+        return;
+    }
+    [self clearUserVerification];
+}
+
+#pragma mark - Helpers
+
+- (UInt8)errorCodeFromKeyResponsePayloadData:(NSData *)response {
+    NSData *responsePayload = [self dataFromKeyResponse:response];
+    YKFAssertReturnValue(responsePayload.length >= 1, @"Cannot extract FIDO2 error code from the key response.", YKFKeyFIDO2ErrorCodeOTHER);
+    
+    UInt8 *payloadBytes = (UInt8 *)responsePayload.bytes;
+    return payloadBytes[0];
+}
+
+- (NSData *)cborFromKeyResponsePayloadData:(NSData *)response {
+    NSData *responsePayload = [self dataFromKeyResponse:response];
+    YKFAssertReturnValue(responsePayload.length >= 1, @"Cannot extract FIDO2 cbor from the key response.", nil);
+    
+    // discard the error byte
+    return [responsePayload subdataWithRange:NSMakeRange(1, responsePayload.length - 1)];
+}
+
+- (void)handleTouchRequired:(YKFKeyFIDO2Request *)request completion:(YKFKeyFIDO2ServiceResultCompletionBlock)completion {
+    YKFParameterAssertReturn(request);
+    YKFParameterAssertReturn(completion);
+    
+    if (![request shouldRetry]) {
+        YKFKeySessionError *timeoutError = [YKFKeySessionError errorWithCode:YKFKeySessionErrorTouchTimeoutCode];
+        completion(nil, timeoutError);
+
+        [self updateKeyState:YKFKeyFIDO2ServiceKeyStateIdle];
+        return;
+    }
+    
+    [self updateKeyState:YKFKeyFIDO2ServiceKeyStateTouchKey];
+    request.retries += 1;
+
+    ykf_weak_self();
+    [self.connectionController dispatchOnSequentialQueue:^{
+        ykf_safe_strong_self();
+        
+        YKFKeyFIDO2Request *retryRequest = [[YKFKeyFIDO2Request alloc] init];
+        retryRequest.retries = request.retries;
+        retryRequest.apdu = [[YKFFIDO2TouchPoolingAPDU alloc] init];
+        
+        [strongSelf executeFIDO2RequestWithoutApplicationSelection:retryRequest completion:completion];
+    }
+    delay:request.retryTimeInterval];
+}
+
+@end
diff --git a/YubiKit/YubiKit/Sessions/Shared/Services/OATH/YKFKeyOATHService+Private.h b/YubiKit/YubiKit/Sessions/Shared/Services/OATH/YKFKeyOATHService+Private.h
new file mode 100755
index 000000000..56372f05e
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Services/OATH/YKFKeyOATHService+Private.h
@@ -0,0 +1,32 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+
+@protocol YKFKeyConnectionControllerProtocol;
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface YKFKeyOATHService()
+
+- (nullable instancetype)initWithConnectionController:(nonnull id<YKFKeyConnectionControllerProtocol>)connectionController NS_DESIGNATED_INITIALIZER;
+
+/*
+ Call this to force an applet selection on the next OATH operation.
+ */
+- (void)invalidateApplicationSelectionCache;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/YubiKit/YubiKit/Sessions/Shared/Services/OATH/YKFKeyOATHService.h b/YubiKit/YubiKit/Sessions/Shared/Services/OATH/YKFKeyOATHService.h
new file mode 100755
index 000000000..7b711fa76
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Services/OATH/YKFKeyOATHService.h
@@ -0,0 +1,293 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+#import "YKFKeyService.h"
+#import "YKFKeyOATHPutRequest.h"
+#import "YKFKeyOATHDeleteRequest.h"
+#import "YKFKeyOATHCalculateRequest.h"
+#import "YKFKeyOATHCalculateResponse.h"
+#import "YKFKeyOATHSetCodeRequest.h"
+#import "YKFKeyOATHValidateRequest.h"
+#import "YKFKeyOATHListResponse.h"
+#import "YKFKeyOATHCalculateAllResponse.h"
+
+/**
+ * ---------------------------------------------------------------------------------------------------------------------
+ * @name OATH Service Response Blocks
+ * ---------------------------------------------------------------------------------------------------------------------
+ */
+
+/*!
+ @abstract
+    Response block used by OATH requests which do not provide a result for the request.
+ 
+ @param error
+    In case of a failed request this parameter contains the error. If the request was successful
+    this parameter is nil.
+ */
+typedef void (^YKFKeyOATHServiceCompletionBlock)
+    (NSError* _Nullable error);
+
+/*!
+ @abstract
+    Response block for [executeCalculateRequest:completion:] which provides the result for the execution
+    of the Calculate request.
+ 
+ @param response
+    The response of the request when it was successful. In case of error this parameter is nil.
+
+ @param error
+    In case of a failed request this parameter contains the error. If the request was successful this
+    parameter is nil.
+ */
+typedef void (^YKFKeyOATHServiceCalculateCompletionBlock)
+    (YKFKeyOATHCalculateResponse* _Nullable response, NSError* _Nullable error);
+
+/*!
+ @abstract
+    Response block for [executeListRequest:completion:] which provides the result for the execution
+    of the List request.
+ 
+ @param response
+    The response of the request when it was successful. In case of error this parameter is nil.
+ 
+ @param error
+    In case of a failed request this parameter contains the error. If the request was successful this
+    parameter is nil.
+ */
+typedef void (^YKFKeyOATHServiceListCompletionBlock)
+    (YKFKeyOATHListResponse* _Nullable response, NSError* _Nullable error);
+
+/*!
+ @abstract
+    Response block for [executeCalculateAllRequest:completion:] which provides the result for the execution
+    of the Calculate All request.
+ 
+ @param response
+    The response of the request when it was successful. In case of error this parameter is nil.
+ 
+ @param error
+    In case of a failed request this parameter contains the error. If the request was successful this
+    parameter is nil.
+ */
+typedef void (^YKFKeyOATHServiceCalculateAllCompletionBlock)
+    (YKFKeyOATHCalculateAllResponse* _Nullable response, NSError* _Nullable error);
+
+/**
+ * ---------------------------------------------------------------------------------------------------------------------
+ * @name OATH Service Protocol
+ * ---------------------------------------------------------------------------------------------------------------------
+ */
+
+NS_ASSUME_NONNULL_BEGIN
+
+/*!
+ @abstract
+    Defines the interface for YKFKeyOATHService.
+ */
+@protocol YKFKeyOATHServiceProtocol<NSObject>
+
+/*!
+ @method executePutRequest:completion:
+ 
+ @abstract
+    Sends to the key an OATH Put request to add a new credential. The request is performed asynchronously
+    on a background execution queue.
+ 
+ @param request
+    The request which contains the required information to add a new credential.
+ 
+ @param completion
+    The response block which is executed after the request was processed by the key. The completion block
+    will be executed on a background thread. If the intention is to update the UI, dispatch the results
+    on the main thread to avoid an UIKit assertion.
+ 
+ @note:
+    This method is thread safe and can be invoked from any thread (main or a background thread).
+ */
+- (void)executePutRequest:(YKFKeyOATHPutRequest *)request
+               completion:(YKFKeyOATHServiceCompletionBlock)completion;
+
+/*!
+ @method executeDeleteRequest:completion:
+ 
+ @abstract
+    Sends to the key an OATH Delete request to remove an existing credential. The request is performed
+    asynchronously on a background execution queue.
+ 
+ @param request
+    The request which contains the required information to remove a credential.
+ 
+ @param completion
+    The response block which is executed after the request was processed by the key. The completion block
+    will be executed on a background thread. If the intention is to update the UI, dispatch the results
+    on the main thread to avoid an UIKit assertion.
+ 
+ @note
+    This method is thread safe and can be invoked from any thread (main or a background thread).
+ */
+- (void)executeDeleteRequest:(YKFKeyOATHDeleteRequest *)request
+                  completion:(YKFKeyOATHServiceCompletionBlock)completion;
+
+/*!
+ @method executeCalculateRequest:completion:
+ 
+ @abstract
+    Sends to the key an OATH Calculate request to calculate an existing credential. The request is performed
+    asynchronously on a background execution queue.
+ 
+ @param request
+    The request which contains the required information to calculate a credential.
+ 
+ @param completion
+    The response block which is executed after the request was processed by the key. The completion block
+    will be executed on a background thread. If the intention is to update the UI, dispatch the results
+    on the main thread to avoid an UIKit assertion.
+ 
+ @note
+    This method is thread safe and can be invoked from any thread (main or a background thread).
+ */
+- (void)executeCalculateRequest:(YKFKeyOATHCalculateRequest *)request
+                     completion:(YKFKeyOATHServiceCalculateCompletionBlock)completion;
+
+/*!
+ @method executeCalculateAllRequestWithCompletion:
+ 
+ @abstract
+    Sends to the key an OATH Calculate All request to calculate all stored credentials on the key.
+    The request is performed asynchronously on a background execution queue.
+ 
+ @param completion
+    The response block which is executed after the request was processed by the key. The completion block
+    will be executed on a background thread. If the intention is to update the UI, dispatch the results
+    on the main thread to avoid an UIKit assertion.
+ 
+ @note
+    This method is thread safe and can be invoked from any thread (main or a background thread).
+ */
+- (void)executeCalculateAllRequestWithCompletion:(YKFKeyOATHServiceCalculateAllCompletionBlock)completion;
+
+/*!
+ @method executeListRequestWithCompletion:
+ 
+ @abstract
+    Sends to the key an OATH List request to enumerate all stored credentials on the key.
+    The request is performed asynchronously on a background execution queue.
+ 
+ @param completion
+    The response block which is executed after the request was processed by the key. The completion block
+    will be executed on a background thread. If the intention is to update the UI, dispatch the results
+    on the main thread to avoid an UIKit assertion.
+ 
+ @note
+    This method is thread safe and can be invoked from any thread (main or a background thread).
+ */
+- (void)executeListRequestWithCompletion:(YKFKeyOATHServiceListCompletionBlock)completion;
+
+/*!
+ @method executeResetRequestWithCompletion:
+ 
+ @abstract
+    Sends to the key an OATH Reset request to reset the OATH application to its default state. This request
+    will remove all stored credentials and the authentication, if set. The request is performed asynchronously
+    on a background execution queue.
+ 
+ @param completion
+    The response block which is executed after the request was processed by the key. The completion block
+    will be executed on a background thread. If the intention is to update the UI, dispatch the results
+    on the main thread to avoid an UIKit assertion.
+ 
+ @note
+    This method is thread safe and can be invoked from any thread (main or a background thread).
+ */
+- (void)executeResetRequestWithCompletion:(YKFKeyOATHServiceCompletionBlock)completion;
+
+/*!
+ @method executeSetCodeRequest:completion:
+ 
+ @abstract
+    Sends to the key an OATH Set Code request to set a PIN on the key OATH application. The request
+    is performed asynchronously on a background execution queue.
+ 
+ @param request
+    The request which contains the required information to set a PIN on the key OATH application.
+ 
+ @param completion
+    The response block which is executed after the request was processed by the key. The completion block
+    will be executed on a background thread. If the intention is to update the UI, dispatch the results
+    on the main thread to avoid an UIKit assertion.
+ 
+ @note
+    This method is thread safe and can be invoked from any thread (main or a background thread).
+ */
+- (void)executeSetCodeRequest:(YKFKeyOATHSetCodeRequest *)request
+                   completion:(YKFKeyOATHServiceCompletionBlock)completion;
+
+/*!
+ @method executeValidateRequest:completion:
+ 
+ @abstract
+    Sends to the key an OATH Validate request to authentificate against the OATH application. After authentification
+    all subsequent requests can be performed until the key application is deselected, as the result of performing
+    another type of request (e.g. U2F) or by unplugging the key from the device. The request is performed
+    asynchronously on a background execution queue.
+ 
+ @param request
+    The request which contains the required information to validate a PIN on the key OATH application.
+ 
+ @param completion
+    The response block which is executed after the request was processed by the key. The completion block
+    will be executed on a background thread. If the intention is to update the UI, dispatch the results
+    on the main thread to avoid an UIKit assertion.
+ 
+ @note
+    This method is thread safe and can be invoked from any thread (main or a background thread).
+ */
+- (void)executeValidateRequest:(YKFKeyOATHValidateRequest *)request
+                    completion:(YKFKeyOATHServiceCompletionBlock)completion;
+
+@end
+
+NS_ASSUME_NONNULL_END
+
+/**
+ * ---------------------------------------------------------------------------------------------------------------------
+ * @name OATH Service
+ * ---------------------------------------------------------------------------------------------------------------------
+ */
+
+NS_ASSUME_NONNULL_BEGIN
+
+/*!
+ @class YKFKeyOATHService
+ 
+ @abstract
+    Provides the interface for executing OATH requests with the key.
+@discussion
+    The OATH service is mantained by the key session which controls its lifecycle. The application must not
+    create one. It has to use only the single shared instance from YKFAccessorySession and sync its usage with
+    the session state.
+ */
+@interface YKFKeyOATHService: YKFKeyService<YKFKeyOATHServiceProtocol>
+
+/*
+ Not available: use only the instance from the YKFAccessorySession.
+ */
+- (instancetype)init NS_UNAVAILABLE;
+
+@end
+
+NS_ASSUME_NONNULL_END
+
diff --git a/YubiKit/YubiKit/Sessions/Shared/Services/OATH/YKFKeyOATHService.m b/YubiKit/YubiKit/Sessions/Shared/Services/OATH/YKFKeyOATHService.m
new file mode 100755
index 000000000..c3c4efa2d
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Services/OATH/YKFKeyOATHService.m
@@ -0,0 +1,439 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "YKFKeyOATHService.h"
+#import "YKFKeyOATHService+Private.h"
+#import "YKFKeyService+Private.h"
+#import "YKFAccessoryConnectionController.h"
+#import "YKFKeyOATHError.h"
+#import "YKFKeyAPDUError.h"
+#import "YKFOATHCredentialValidator.h"
+#import "YKFLogger.h"
+#import "YKFKeyCommandConfiguration.h"
+#import "YKFBlockMacros.h"
+#import "YKFAssert.h"
+
+#import "YKFSelectOATHApplicationAPDU.h"
+#import "YKFOATHSendRemainingAPDU.h"
+#import "YKFOATHSetCodeAPDU.h"
+#import "YKFOATHValidateAPDU.h"
+
+#import "YKFKeySessionError+Private.h"
+#import "YKFKeyOATHRequest+Private.h"
+
+#import "YKFNSDataAdditions.h"
+#import "YKFNSDataAdditions+Private.h"
+
+#import "YKFKeyOATHListRequest.h"
+#import "YKFKeyOATHResetRequest.h"
+#import "YKFKeyOATHCalculateAllRequest.h"
+#import "YKFKeyOATHCalculateAllRequest+Private.h"
+#import "YKFKeyOATHSelectApplicationResponse.h"
+#import "YKFKeyOATHValidateResponse.h"
+#import "YKFKeyOATHCalculateAllResponse.h"
+
+#import "YKFKeyOATHCalculateResponse+Private.h"
+#import "YKFKeyOATHListResponse+Private.h"
+#import "YKFKeyOATHCalculateAllResponse+Private.h"
+#import "YKFKeyOATHCalculateRequest+Private.h"
+#import "YKFAPDU+Private.h"
+
+static const NSTimeInterval YKFKeyOATHServiceTimeoutThreshold = 10; // seconds
+
+typedef void (^YKFKeyOATHServiceResultCompletionBlock)(NSData* _Nullable  result, NSError* _Nullable error);
+
+@interface YKFKeyOATHService()
+
+@property (nonatomic) id<YKFKeyConnectionControllerProtocol> connectionController;
+
+/*
+ In case of OATH, the reselection of the application leads to the loss of authentication (if any). To avoid
+ this the select application response is cached to avoid reselecting the applet. If the request fails with
+ timeout the cache gets invalidated to allow again the following requests to select the application again.
+ */
+@property (nonatomic) YKFKeyOATHSelectApplicationResponse *cachedSelectApplicationResponse;
+
+@end
+
+@implementation YKFKeyOATHService
+
+- (instancetype)initWithConnectionController:(id<YKFKeyConnectionControllerProtocol>)connectionController {
+    YKFAssertAbortInit(connectionController);
+    
+    self = [super init];
+    if (self) {
+        self.connectionController = connectionController;
+    }
+    return self;
+}
+
+#pragma mark - Credential Add/Delete
+
+- (void)executePutRequest:(YKFKeyOATHPutRequest *)request completion:(YKFKeyOATHServiceCompletionBlock)completion {
+    YKFParameterAssertReturn(request);
+    YKFParameterAssertReturn(completion);
+    
+    YKFKeySessionError *credentialError = [YKFOATHCredentialValidator validateCredential:request.credential includeSecret:YES];
+    if (credentialError) {
+        completion(credentialError);
+    }
+    
+    [self executeOATHRequest:request completion:^(NSData * _Nullable result, NSError * _Nullable error) {
+        // No result except status code
+        completion(error);
+    }];
+}
+
+- (void)executeDeleteRequest:(YKFKeyOATHDeleteRequest *)request completion:(YKFKeyOATHServiceCompletionBlock)completion {
+    YKFParameterAssertReturn(request);
+    YKFParameterAssertReturn(completion);
+
+    YKFKeySessionError *credentialError = [YKFOATHCredentialValidator validateCredential:request.credential includeSecret:NO];
+    if (credentialError) {
+        completion(credentialError);
+    }
+    
+    [self executeOATHRequest:request completion:^(NSData * _Nullable result, NSError * _Nullable error) {
+        // No result except status code
+        completion(error);
+    }];
+}
+
+#pragma mark - Credential Calculation
+
+- (void)executeCalculateRequest:(YKFKeyOATHCalculateRequest *)request completion:(YKFKeyOATHServiceCalculateCompletionBlock)completion {
+    YKFParameterAssertReturn(request);
+    YKFParameterAssertReturn(completion);
+    
+    YKFKeySessionError *credentialError = [YKFOATHCredentialValidator validateCredential:request.credential includeSecret:NO];
+    if (credentialError) {
+        completion(nil, credentialError);
+    }
+
+    [self executeOATHRequest:request completion:^(NSData * _Nullable result, NSError * _Nullable error) {
+        if (error) {
+            completion(nil, error);
+            return;
+        }
+        YKFKeyOATHCalculateResponse *response = [[YKFKeyOATHCalculateResponse alloc] initWithKeyResponseData:result
+                                                                                             requestTimetamp:request.timestamp
+                                                                                               requestPeriod:request.credential.period];
+        if (!response) {
+            completion(nil, [YKFKeyOATHError errorWithCode:YKFKeyOATHErrorCodeBadCalculationResponse]);
+            return;
+        }
+        
+        completion(response, nil);
+    }];
+}
+
+- (void)executeCalculateAllRequestWithCompletion:(YKFKeyOATHServiceCalculateAllCompletionBlock)completion {
+    YKFParameterAssertReturn(completion);
+    
+    YKFKeyOATHCalculateAllRequest *request = [[YKFKeyOATHCalculateAllRequest alloc] init];
+    
+    [self executeOATHRequest:request completion:^(NSData * _Nullable result, NSError * _Nullable error) {
+        if (error) {
+            completion(nil, error);
+            return;
+        }        
+        YKFKeyOATHCalculateAllResponse *response = [[YKFKeyOATHCalculateAllResponse alloc] initWithKeyResponseData:result
+                                                                                                   requestTimetamp:request.timestamp];
+        if (!response) {
+            completion(nil, [YKFKeyOATHError errorWithCode:YKFKeyOATHErrorCodeBadCalculateAllResponse]);
+            return;
+        }
+        
+        completion(response, nil);
+    }];
+}
+
+#pragma mark - Credential Listing
+
+- (void)executeListRequestWithCompletion:(YKFKeyOATHServiceListCompletionBlock)completion {
+    YKFParameterAssertReturn(completion);
+    
+    YKFKeyOATHListRequest *request = [[YKFKeyOATHListRequest alloc] init];
+    
+    [self executeOATHRequest:request completion:^(NSData * _Nullable result, NSError * _Nullable error) {
+        if (error) {
+            completion(nil, error);
+            return;
+        }
+        
+        YKFKeyOATHListResponse *response = [[YKFKeyOATHListResponse alloc] initWithKeyResponseData:result];
+        if (!response) {
+            completion(nil, [YKFKeyOATHError errorWithCode:YKFKeyOATHErrorCodeBadListResponse]);
+            return;
+        }
+        
+        completion(response, nil);
+    }];
+}
+
+#pragma mark - Reset
+
+- (void)executeResetRequestWithCompletion:(YKFKeyOATHServiceCompletionBlock)completion {
+    YKFParameterAssertReturn(completion);
+    
+    YKFKeyOATHResetRequest *request = [[YKFKeyOATHResetRequest alloc] init];
+    
+    ykf_weak_self();
+    [self executeOATHRequest:request completion:^(NSData * _Nullable result, NSError * _Nullable error) {
+        ykf_safe_strong_self();
+        if (error) {
+            completion(error);
+            return;
+        }
+        strongSelf.cachedSelectApplicationResponse = nil;
+        completion(nil);
+    }];
+}
+
+#pragma mark - OATH Authentication
+
+- (void)executeSetCodeRequest:(YKFKeyOATHSetCodeRequest *)request completion:(YKFKeyOATHServiceCompletionBlock)completion {
+    YKFParameterAssertReturn(request);
+    YKFParameterAssertReturn(completion);
+
+    // This request does not reuse the select applet to get the salt for building the APDU and not ending in error.
+    ykf_weak_self();
+    [self selectOATHApplicationWithCompletion:^(YKFKeyOATHSelectApplicationResponse *response, NSError *error) {
+        ykf_safe_strong_self();
+        if (error) {
+            completion(error);
+            return;
+        }
+        
+        // Get the salt
+        if (!response.selectID) {
+            completion([YKFKeyOATHError errorWithCode:YKFKeyOATHErrorCodeBadApplicationSelectionResponse]);
+            return;
+        }
+        
+        // Build the request APDU with the select ID salt
+        request.apdu = [[YKFOATHSetCodeAPDU alloc] initWithRequest:request salt:response.selectID];
+        
+        [strongSelf executeOATHRequestWithoutApplicationSelection:request completion:^(NSData * _Nullable result, NSError * _Nullable error) {
+            if (error) {
+                completion(error);
+                return;
+            }
+            completion(nil);
+        }];
+    }];
+}
+
+- (void)executeValidateRequest:(YKFKeyOATHValidateRequest *)request completion:(YKFKeyOATHServiceCompletionBlock)completion {
+    YKFParameterAssertReturn(request);
+    YKFParameterAssertReturn(completion);
+
+    // This request does not reuse the select applet to get the salt for building the APDU and not ending in error.
+    ykf_weak_self();
+    [self selectOATHApplicationWithCompletion:^(YKFKeyOATHSelectApplicationResponse *response, NSError *error) {
+        ykf_safe_strong_self();
+        if (error) {
+            completion(error);
+            return;
+        }
+        
+        // Get the salt
+        if (!response.selectID || !response.challenge) {
+            completion([YKFKeyOATHError errorWithCode:YKFKeyOATHErrorCodeBadApplicationSelectionResponse]);
+            return;
+        }
+        
+        // Build the request APDU with the select ID salt
+        request.apdu = [[YKFOATHValidateAPDU alloc] initWithRequest:request challenge:response.challenge salt:response.selectID];
+        
+        [strongSelf executeOATHRequestWithoutApplicationSelection:request completion:^(NSData * _Nullable result, NSError * _Nullable error) {
+            if (error) {
+                if (error.code == YKFKeyAPDUErrorCodeWrongData) {
+                    completion([YKFKeyOATHError errorWithCode:YKFKeyOATHErrorCodeWrongPassword]);
+                } else {
+                    completion(error);
+                }
+                return;
+            }
+            
+            YKFKeyOATHValidateResponse *validateResponse = [[YKFKeyOATHValidateResponse alloc] initWithResponseData:result];
+            if (!validateResponse) {
+                completion([YKFKeyOATHError errorWithCode:YKFKeyOATHErrorCodeBadValidationResponse]);
+                return;
+            }
+            NSData *expectedApduData = ((YKFOATHValidateAPDU *)request.apdu).expectedChallengeData;
+            if (![validateResponse.response isEqualToData:expectedApduData]) {
+                completion([YKFKeyOATHError errorWithCode:YKFKeyOATHErrorCodeBadValidationResponse]);
+                return;
+            }
+            
+            completion(nil);
+        }];
+    }];
+}
+
+#pragma mark - Request Execution
+
+- (void)executeOATHRequest:(YKFKeyOATHRequest *)request completion:(YKFKeyOATHServiceResultCompletionBlock)completion {
+    YKFParameterAssertReturn(request);
+    YKFParameterAssertReturn(completion);
+
+    [self.delegate keyService:self willExecuteRequest:request];
+    
+    ykf_weak_self();
+    [self selectOATHApplicationWithCompletion:^(YKFKeyOATHSelectApplicationResponse *response, NSError *error) {
+        ykf_safe_strong_self();
+        if (error) {
+            completion(nil, error);
+            return;
+        }        
+        [strongSelf executeOATHRequestWithoutApplicationSelection:request completion:completion];
+    }];
+}
+
+- (void)executeOATHRequestWithoutApplicationSelection:(YKFKeyOATHRequest *)request completion:(YKFKeyOATHServiceResultCompletionBlock)completion {
+    YKFParameterAssertReturn(request);
+    YKFParameterAssertReturn(completion);
+
+    __block __weak YKFKeyConnectionControllerCommandResponseBlock weakBlock; // recursive block
+    YKFKeyConnectionControllerCommandResponseBlock block;
+    
+    // Used for chained responses
+    __block NSMutableData *responseDataBuffer = [[NSMutableData alloc] init];
+    
+    ykf_weak_self();
+    weakBlock = block = ^(NSData *result, NSError *error, NSTimeInterval executionTime) {
+        ykf_safe_strong_self();
+        
+        __strong YKFKeyConnectionControllerCommandResponseBlock strongBlock = weakBlock;
+        YKFAssertReturn(strongBlock, @"Recursive block failure.");
+        
+        if (error) {
+            // Clear the select cache when the resonse is timing out.
+            if (error.code == YKFKeySessionErrorReadTimeoutCode) {
+                strongSelf.cachedSelectApplicationResponse = nil;
+            }
+            completion(nil, error);
+            return;
+        }
+        YKFAssertReturn(result != nil, @"Invalid OATH request execution result value on success.");
+
+        NSData *responseData = [self dataFromKeyResponse:result];
+        [responseDataBuffer appendData:responseData];
+        
+        int statusCode = [strongSelf statusCodeFromKeyResponse: result];
+        int shortStatusCode = [strongSelf shortStatusCodeFromStatusCode:statusCode];
+        
+        if (shortStatusCode == YKFKeyAPDUErrorCodeMoreData) {
+            YKFLogInfo(@"Key has more data to send. Requesting for remaining data...");            
+            
+            // Queue a new request recursively
+            YKFOATHSendRemainingAPDU *sendRemainingDataAPDU = [[YKFOATHSendRemainingAPDU alloc] init];
+            [strongSelf.connectionController execute:sendRemainingDataAPDU completion:strongBlock];
+            return;
+        }
+        
+        switch (statusCode) {
+            case YKFKeyAPDUErrorCodeNoError:
+                completion(responseDataBuffer, nil);
+                break;
+            
+            case YKFKeyAPDUErrorCodeAuthenticationRequired:
+                if (executionTime < YKFKeyOATHServiceTimeoutThreshold) {
+                    strongSelf.cachedSelectApplicationResponse = nil; // Clear the cache to allow the application selection again.
+                    completion(nil, [YKFKeyOATHError errorWithCode:YKFKeyOATHErrorCodeAuthenticationRequired]);
+                } else {
+                    completion(nil, [YKFKeyOATHError errorWithCode:YKFKeyOATHErrorCodeTouchTimeout]);
+                }
+                break;
+                
+            // Errors - The status code is the error. The key doesn't send any other information.
+            default: {
+                YKFKeySessionError *connectionError = [YKFKeySessionError errorWithCode:statusCode];
+                completion(nil, connectionError);
+            }
+        }
+    };
+    [self.connectionController execute:request.apdu completion:block];
+}
+
+#pragma mark - Application Selection
+
+- (void)selectOATHApplicationWithCompletion:(void (^)(YKFKeyOATHSelectApplicationResponse* _Nullable, NSError* _Nullable))completion {
+    YKFAPDU *selectOATHApplicationAPDU = [[YKFSelectOATHApplicationAPDU alloc] init];
+    
+    // Return cached response if available.
+    if (self.cachedSelectApplicationResponse) {
+        completion(self.cachedSelectApplicationResponse, nil);
+        return;
+    }
+    
+    ykf_weak_self();
+    [self.connectionController execute:selectOATHApplicationAPDU
+                         configuration:[YKFKeyCommandConfiguration fastCommandCofiguration]
+                            completion:^(NSData *result, NSError *error, NSTimeInterval executionTime) {
+        ykf_safe_strong_self();
+        
+        if (error) {
+            completion(nil, error);
+            return;
+        }
+        
+        int statusCode = [strongSelf statusCodeFromKeyResponse: result];
+        switch (statusCode) {
+            case YKFKeyAPDUErrorCodeNoError: {
+                    NSData *responseData = [self dataFromKeyResponse:result];
+                    YKFKeyOATHSelectApplicationResponse *response = [[YKFKeyOATHSelectApplicationResponse alloc] initWithResponseData:responseData];
+                    if (response) {
+                        // Cache the response.
+                        strongSelf.cachedSelectApplicationResponse = response;
+                        completion(response, nil);
+                    } else {
+                        completion(nil, [YKFKeyOATHError errorWithCode:YKFKeyOATHErrorCodeBadApplicationSelectionResponse]);
+                    }
+                }
+                break;
+                
+            case YKFKeyAPDUErrorCodeMissingFile:
+                completion(nil, [YKFKeySessionError errorWithCode:YKFKeySessionErrorMissingApplicationCode]);
+                break;
+                
+            default:
+                completion(nil, [YKFKeySessionError errorWithCode:statusCode]);
+        }
+    }];
+}
+
+#pragma mark - YKFKeyServiceProtocol
+
+- (void)keyService:(YKFKeyService *)service willExecuteRequest:(YKFKeyRequest *)request {
+    if (!service || (service == self)) {
+        return;
+    }
+    if (!self.cachedSelectApplicationResponse) {
+        return;
+    }
+    
+    YKFLogVerbose(@"Clearing OATH Service application selection.");
+    
+    self.cachedSelectApplicationResponse = nil;
+}
+
+#pragma mark - Test Helpers
+
+- (void)invalidateApplicationSelectionCache {
+    self.cachedSelectApplicationResponse = nil;
+}
+
+@end
diff --git a/YubiKit/YubiKit/Sessions/Shared/Services/OATH/YKFOATHCredential+Private.h b/YubiKit/YubiKit/Sessions/Shared/Services/OATH/YKFOATHCredential+Private.h
new file mode 100755
index 000000000..d4048d123
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Services/OATH/YKFOATHCredential+Private.h
@@ -0,0 +1,28 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+@interface YKFOATHCredential()
+
+/*!
+ The name of the credential is precomputed when initialized with an URL like this
+ <period>/label if the credential is of TOTP type or label if the credential is HOTP.
+ The name of the credential is used by the key to identify which stored credential
+ to use for a compute operation when requested.
+ The name may not have more then 64 bytes (or 64 ASCI characters) which is the maximum size
+ accepted by the YubiKey. This property can be overidden if the name is larger and an application
+ mapping between the name and credential should be created.
+ */
+@property (nonatomic, nonnull) NSString *key;
+
+@end
diff --git a/YubiKit/YubiKit/Sessions/Shared/Services/OATH/YKFOATHCredential.h b/YubiKit/YubiKit/Sessions/Shared/Services/OATH/YKFOATHCredential.h
new file mode 100755
index 000000000..d4ddb5ec7
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Services/OATH/YKFOATHCredential.h
@@ -0,0 +1,133 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * ---------------------------------------------------------------------------------------------------------------------
+ * @name OATH Credential Types
+ * ---------------------------------------------------------------------------------------------------------------------
+ */
+
+/*!
+ The type of the credential as defined in https://developers.yubico.com/OATH/YKOATH_Protocol.html section TYPES
+ */
+typedef NS_ENUM(NSUInteger, YKFOATHCredentialType) {
+    YKFOATHCredentialTypeUnknown    = 0x00,
+    YKFOATHCredentialTypeHOTP       = 0x10,
+    YKFOATHCredentialTypeTOTP       = 0x20
+};
+
+/*!
+ The OATH algorithm as defined in https://developers.yubico.com/OATH/YKOATH_Protocol.html section ALGORITHMS
+ */
+typedef NS_ENUM(NSUInteger, YKFOATHCredentialAlgorithm) {
+    YKFOATHCredentialAlgorithmUnknown   = 0x00,
+    YKFOATHCredentialAlgorithmSHA1      = 0x01,
+    YKFOATHCredentialAlgorithmSHA256    = 0x02,
+    YKFOATHCredentialAlgorithmSHA512    = 0x03
+};
+
+/**
+ * ---------------------------------------------------------------------------------------------------------------------
+ * @name OATH Credential
+ * ---------------------------------------------------------------------------------------------------------------------
+ */
+
+/*!
+ @class YKFOATHCredential
+ 
+ @abstract
+    The YKFOATHCredential is a data model which contains a list of properties defining an OATH credential.
+ */
+@interface YKFOATHCredential: NSObject
+
+/*!
+ The credential type (HOTP or TOTP).
+ */
+@property (nonatomic, assign) YKFOATHCredentialType type;
+
+/*!
+ The hash algorithm to use for the OATH credential.
+ */
+@property (nonatomic, assign) YKFOATHCredentialAlgorithm algorithm;
+
+/*!
+ The Label of the credential as defined in the Key URI Format specifications:
+ https://github.com/google/google-authenticator/wiki/Key-Uri-Format
+ */
+@property (nonatomic, nullable) NSString *label;
+
+/*!
+ The Secret of the credential as defined in the Key URI Format specifications:
+ https://github.com/google/google-authenticator/wiki/Key-Uri-Format
+ */
+@property (nonatomic) NSData *secret;
+
+/*!
+ The Issuer of the credential as defined in the Key URI Format specifications:
+ https://github.com/google/google-authenticator/wiki/Key-Uri-Format
+ */
+@property (nonatomic, nullable) NSString *issuer;
+
+/*!
+ How long is the one-time passcode to display to the user. The value for this property can
+ only be 6, 7 or 8. The default value is 6.
+ */
+@property (nonatomic, assign) NSUInteger digits;
+
+/*!
+ The validity period for a TOTP code, in seconds. The default value for this property is 30.
+ If the credential is of HOTP type, this property returns 0.
+ */
+@property (nonatomic, assign) NSUInteger period;
+
+/*!
+ The counter parameter is required when the type is HOTP. It will set the initial counter value.
+ If the credential is of TOTP type, this property returns 0.
+ */
+@property (nonatomic, assign) UInt32 counter;
+
+/*!
+ The account name extracted from the label. If the label does not contain the issuer, the
+ name is the same as the label.
+ */
+@property (nonatomic) NSString *account;
+
+/*!
+ The credential requires the user to touch the key to generate it.
+ */
+@property (nonatomic) BOOL requiresTouch;
+
+/*!
+ @method initWithURL:
+ 
+ @abstract
+    Convenience initializer which creates a new credential from an URL which conforms to ther Key URI Format
+    specifications as defined in:
+    https://github.com/google/google-authenticator/wiki/Key-Uri-Format
+ 
+ @returns
+    The credential object created from the URL or nil if the URL is incorrect.
+ 
+ @param url
+    The URL containing the credential properties.
+ */
+- (nullable instancetype)initWithURL:(NSURL *)url;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/YubiKit/YubiKit/Sessions/Shared/Services/OATH/YKFOATHCredential.m b/YubiKit/YubiKit/Sessions/Shared/Services/OATH/YKFOATHCredential.m
new file mode 100755
index 000000000..642cd568f
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Services/OATH/YKFOATHCredential.m
@@ -0,0 +1,318 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <CommonCrypto/CommonCrypto.h>
+
+#import "YKFOATHCredential.h"
+#import "YKFOATHCredential+Private.h"
+#import "YKFAssert.h"
+#import "YKFLogger.h"
+#import "YKFNSDataAdditions.h"
+
+#import "MF_Base32Additions.h"
+
+static NSUInteger const YKFOATHCredentialDefaultDigits = 6;
+static NSUInteger const YKFOATHCredentialDefaultPeriod = 30; // seconds
+static NSUInteger const YKFOATHCredentialMinSecretLength = 14; // bytes
+
+static NSString* const YKFOATHCredentialScheme = @"otpauth";
+
+static NSString* const YKFOATHCredentialURLTypeHOTP = @"hotp";
+static NSString* const YKFOATHCredentialURLTypeTOTP = @"totp";
+
+static NSString* const YKFOATHCredentialURLParameterSecret = @"secret";
+static NSString* const YKFOATHCredentialURLParameterIssuer = @"issuer";
+static NSString* const YKFOATHCredentialURLParameterDigits = @"digits";
+static NSString* const YKFOATHCredentialURLParameterPeriod = @"period";
+static NSString* const YKFOATHCredentialURLParameterCounter = @"counter";
+static NSString* const YKFOATHCredentialURLParameterAlgorithm = @"algorithm";
+
+static NSString* const YKFOATHCredentialURLParameterValueSHA1 = @"SHA1";
+static NSString* const YKFOATHCredentialURLParameterValueSHA256 = @"SHA256";
+static NSString* const YKFOATHCredentialURLParameterValueSHA512 = @"SHA512";
+
+@implementation YKFOATHCredential
+
+- (instancetype)initWithURL:(NSURL *)url {
+    self = [super init];
+    if (self) {
+        if (![self parseUrl: url]) {
+            self = nil;
+        }
+    }
+    return self;
+}
+
+#pragma mark - Properties Overrides
+
+- (YKFOATHCredentialType)type{
+    if (_type) {
+        return _type;
+    }
+    return YKFOATHCredentialTypeTOTP;
+}
+
+- (YKFOATHCredentialAlgorithm)algorithm {
+    if (_algorithm) {
+        return _algorithm;
+    }
+    return YKFOATHCredentialAlgorithmSHA1;
+}
+
+- (NSUInteger)digits {
+    if (_digits) {
+        return _digits;
+    }
+    return YKFOATHCredentialDefaultDigits;
+}
+
+- (NSUInteger)period {
+    if (_period) {
+        return _period;
+    }
+    return self.type == YKFOATHCredentialTypeTOTP ? YKFOATHCredentialDefaultPeriod : 0;
+}
+
+- (NSString *)key {
+    if (!_key) {
+        NSString *keyLabel = self.label;
+        
+        if (![self.label containsString: @":"] && self.issuer) {
+            keyLabel = [NSString stringWithFormat:@"%@:%@", self.issuer, self.account];
+        }
+        
+        if (self.type == YKFOATHCredentialTypeTOTP) {
+            if (self.period != YKFOATHCredentialDefaultPeriod) {
+                return [NSString stringWithFormat:@"%ld/%@", (unsigned long)self.period, keyLabel];
+            }
+            else {
+                return keyLabel;
+            }
+        } else {
+            return keyLabel;
+        }
+    }
+    return _key;
+}
+
+- (NSString *)label {
+    if (_label) {
+        return _label;
+    }
+    YKFAssertReturnValue(self.account, @"Missing OATH credential account. Cannot build the credential label.", nil);
+    
+    if (self.issuer) {
+        return [NSString stringWithFormat:@"%@:%@", self.issuer, self.account];
+    } else {
+        return self.account;
+    }
+}
+
+- (void)setSecret:(NSData *)secret {
+    YKFAssertReturn(secret.length, @"Cannot set empty OATH secret.");
+    
+    if (secret.length < YKFOATHCredentialMinSecretLength) {
+        NSMutableData *paddedSecret = [[NSMutableData alloc] initWithData:secret];
+        [paddedSecret increaseLengthBy: YKFOATHCredentialMinSecretLength - secret.length];
+        _secret = [paddedSecret copy];
+        return;
+    }
+    
+    switch (self.algorithm) {
+        case YKFOATHCredentialAlgorithmSHA1:
+            if (secret.length > CC_SHA1_BLOCK_BYTES) {
+                _secret = [secret ykf_SHA1];
+            } else {
+                _secret = secret;
+            }
+            break;
+            
+        case YKFOATHCredentialAlgorithmSHA256:
+            if (secret.length > CC_SHA256_BLOCK_BYTES) {
+                _secret = [secret ykf_SHA256];
+            } else {
+                _secret = secret;
+            }
+            break;
+            
+        case YKFOATHCredentialAlgorithmSHA512:
+            if (secret.length > CC_SHA512_BLOCK_BYTES) {
+                _secret = [secret ykf_SHA512];
+            } else {
+                _secret = secret;
+            }
+            break;
+            
+        default:
+            YKFAssertReturn(NO, @"Unknown hash algorithm for credential.");
+            break;
+    }
+}
+
+#pragma mark - URL Parsing
+
+- (BOOL)parseUrl:(NSURL *)url {
+    YKFParameterAssertReturnValue(url, NO);
+    
+    NSURLComponents *urlComponents = [[NSURLComponents alloc] initWithURL:url resolvingAgainstBaseURL:NO];
+    
+    // Check scheme
+    if (![urlComponents.scheme isEqualToString:YKFOATHCredentialScheme]) {
+        return NO;
+    }
+    
+    if (![self parseTypeFromUrlComponents:urlComponents])       { return NO; }
+    if (![self parseLabelFromUrlComponents:urlComponents])      { return NO; }
+    if (![self parseIssuerFromUrlComponents:urlComponents])     { return NO; }
+    if (![self parseAlgorithmFromUrlComponents:urlComponents])  { return NO; }
+    if (![self parseDigitsFromUrlComponents:urlComponents])     { return NO; }
+    if (![self parseSecretFromUrlComponents:urlComponents])     { return NO; }
+
+    // Parse specific parameters
+    if (self.type == YKFOATHCredentialTypeHOTP) {
+        return [self parserHOTPParamsFromUrlComponents:urlComponents];
+    } else {
+        return [self parserTOTPParamsFromUrlComponents:urlComponents];
+    }
+}
+
+#pragma mark - Specific Parameters
+
+- (BOOL)parseDigitsFromUrlComponents:(NSURLComponents *)urlComponents {
+    YKFParameterAssertReturnValue(urlComponents, NO);
+    
+    NSString *digits = [self queryParameterValueForName:YKFOATHCredentialURLParameterDigits inUrlComponents:urlComponents];
+    if (digits) {
+        int value = [digits intValue];
+        if (value && (value == 6 || value == 7 || value == 8)) {
+            self.digits = value;
+        } else {
+            return NO; // Invalid digits number
+        }
+    } else {
+        self.digits = YKFOATHCredentialDefaultDigits;
+    }
+    return YES;
+}
+
+- (BOOL)parseAlgorithmFromUrlComponents:(NSURLComponents *)urlComponents {
+    YKFParameterAssertReturnValue(urlComponents, NO);
+    
+    NSString *algorithm = [self queryParameterValueForName:YKFOATHCredentialURLParameterAlgorithm inUrlComponents:urlComponents];
+    
+    if (!algorithm || [algorithm isEqualToString:YKFOATHCredentialURLParameterValueSHA1]) {
+        self.algorithm = YKFOATHCredentialAlgorithmSHA1;
+    } else if ([algorithm isEqualToString:YKFOATHCredentialURLParameterValueSHA256]) {
+        self.algorithm = YKFOATHCredentialAlgorithmSHA256;
+    } else if ([algorithm isEqualToString:YKFOATHCredentialURLParameterValueSHA512]) {
+        self.algorithm = YKFOATHCredentialAlgorithmSHA512;
+    } else {
+        return NO; // Unknown algorithm
+    }
+    
+    return YES;
+}
+
+- (BOOL)parseIssuerFromUrlComponents:(NSURLComponents *)urlComponents {
+    YKFParameterAssertReturnValue(urlComponents, NO);
+    
+    NSString *issuer = [self queryParameterValueForName:YKFOATHCredentialURLParameterIssuer inUrlComponents:urlComponents];
+    if (issuer && self.issuer) {
+        if (![issuer isEqualToString:self.issuer]) { // Malformed URI: issuers don't match
+            return NO;
+        }
+    } else if (issuer) {
+        self.issuer = issuer;
+    }
+    return YES;
+}
+
+- (BOOL)parseSecretFromUrlComponents:(NSURLComponents *)urlComponents {
+    YKFParameterAssertReturnValue(urlComponents, NO);
+    
+    NSString *base32EncodedSecret = [self queryParameterValueForName:YKFOATHCredentialURLParameterSecret inUrlComponents:urlComponents];
+    if (!base32EncodedSecret) {
+        return NO;
+    }
+    
+    self.secret = [NSData dataWithBase32String:base32EncodedSecret];
+    return self.secret.length > 0;
+}
+
+- (BOOL)parseLabelFromUrlComponents:(NSURLComponents *)urlComponents {
+    YKFParameterAssertReturnValue(urlComponents, NO);
+    
+    NSString *label = urlComponents.path;
+    label = [label stringByReplacingOccurrencesOfString:@"/" withString:@""];
+    if (!label.length) {
+        return NO;
+    }
+    self.label = label;
+    
+    if ([self.label containsString:@":"]) { // Issuer is present in the label
+        NSArray *labelComponents = [self.label componentsSeparatedByString:@":"];
+        self.issuer = labelComponents.firstObject; // It's fine if nil
+        self.account = labelComponents.lastObject;
+    } else {
+        self.account = label;
+    }
+    
+    return YES;
+}
+
+- (BOOL)parseTypeFromUrlComponents:(NSURLComponents *)urlComponents {
+    YKFParameterAssertReturnValue(urlComponents, NO);
+    
+    if ([urlComponents.host isEqualToString:YKFOATHCredentialURLTypeHOTP]) {
+        self.type = YKFOATHCredentialTypeHOTP;
+    } else if ([urlComponents.host isEqualToString:YKFOATHCredentialURLTypeTOTP]) {
+        self.type = YKFOATHCredentialTypeTOTP;
+    } else {
+        return NO;
+    }
+    return YES;
+}
+
+- (BOOL)parserHOTPParamsFromUrlComponents:(NSURLComponents *)urlComponents {
+    YKFParameterAssertReturnValue(urlComponents, NO);
+    
+    NSString *counter = [self queryParameterValueForName:YKFOATHCredentialURLParameterCounter inUrlComponents:urlComponents];
+    if (!counter) {
+        return NO;
+    }
+    self.counter = MAX(0, [counter intValue]);
+    return YES;
+}
+
+- (BOOL)parserTOTPParamsFromUrlComponents:(NSURLComponents *)urlComponents {
+    YKFParameterAssertReturnValue(urlComponents, NO);
+    
+    NSString *period = [self queryParameterValueForName:YKFOATHCredentialURLParameterPeriod inUrlComponents:urlComponents];
+    if (period) {
+        int periodValue = [period intValue];
+        self.period = periodValue > 0 ? periodValue : YKFOATHCredentialDefaultPeriod;
+    } else {
+        self.period = YKFOATHCredentialDefaultPeriod;
+    }    
+    return YES;
+}
+
+#pragma mark - Helpers
+
+- (NSString *)queryParameterValueForName:(NSString *)name inUrlComponents:(NSURLComponents *)urlComponents {
+    NSPredicate *predicate = [NSPredicate predicateWithFormat:@"name == %@", name];
+    return [urlComponents.queryItems filteredArrayUsingPredicate:predicate].firstObject.value;
+}
+
+@end
diff --git a/YubiKit/YubiKit/Sessions/Shared/Services/OATH/YKFOATHCredentialValidator.h b/YubiKit/YubiKit/Sessions/Shared/Services/OATH/YKFOATHCredentialValidator.h
new file mode 100755
index 000000000..725efd738
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Services/OATH/YKFOATHCredentialValidator.h
@@ -0,0 +1,27 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+#import "YKFOATHCredential.h"
+#import "YKFKeySessionError.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface YKFOATHCredentialValidator : NSObject
+
++ (nullable YKFKeySessionError *)validateCredential:(YKFOATHCredential *)credential includeSecret:(BOOL)secretIncluded;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/YubiKit/YubiKit/Sessions/Shared/Services/OATH/YKFOATHCredentialValidator.m b/YubiKit/YubiKit/Sessions/Shared/Services/OATH/YKFOATHCredentialValidator.m
new file mode 100755
index 000000000..e5e4aa595
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Services/OATH/YKFOATHCredentialValidator.m
@@ -0,0 +1,56 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <CommonCrypto/CommonCrypto.h>
+#import "YKFOATHCredentialValidator.h"
+#import "YKFKeyOATHError.h"
+#import "YKFAssert.h"
+
+#import "YKFKeySessionError+Private.h"
+#import "YKFOATHCredential+Private.h"
+
+static const int YKFOATHCredentialValidatorMaxNameSize = 64;
+
+@implementation YKFOATHCredentialValidator
+
++ (YKFKeySessionError *)validateCredential:(YKFOATHCredential *)credential includeSecret:(BOOL)secretIncluded {
+    YKFParameterAssertReturnValue(credential, nil);
+    
+    if (credential.key.length > YKFOATHCredentialValidatorMaxNameSize) {
+        return [YKFKeyOATHError errorWithCode:YKFKeyOATHErrorCodeNameTooLong];
+    }
+    if (secretIncluded) {
+        NSData *credentialSecret = credential.secret;
+        int shaAlgorithmBlockSize = 0;
+        switch (credential.algorithm) {
+            case YKFOATHCredentialAlgorithmSHA1:
+                shaAlgorithmBlockSize = CC_SHA1_BLOCK_BYTES;
+                break;
+            case YKFOATHCredentialAlgorithmSHA256:
+                shaAlgorithmBlockSize = CC_SHA256_BLOCK_BYTES;
+                break;
+            case YKFOATHCredentialAlgorithmSHA512:
+                shaAlgorithmBlockSize = CC_SHA512_BLOCK_BYTES;
+                break;
+            default:
+                YKFAssertReturnValue(NO, @"Invalid OATH algorithm.", nil);
+        }
+        if (credentialSecret.length > shaAlgorithmBlockSize) {
+            return [YKFKeyOATHError errorWithCode:YKFKeyOATHErrorCodeSecretTooLong];
+        }
+    }
+    return nil;
+}
+
+@end
diff --git a/YubiKit/YubiKit/Sessions/Shared/Services/RawCommand/YKFKeyRawCommandService+Private.h b/YubiKit/YubiKit/Sessions/Shared/Services/RawCommand/YKFKeyRawCommandService+Private.h
new file mode 100755
index 000000000..982053ebf
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Services/RawCommand/YKFKeyRawCommandService+Private.h
@@ -0,0 +1,27 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+
+@protocol YKFKeyConnectionControllerProtocol;
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface YKFKeyRawCommandService()
+
+- (nullable instancetype)initWithConnectionController:(nonnull id<YKFKeyConnectionControllerProtocol>)connectionController NS_DESIGNATED_INITIALIZER;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/YubiKit/YubiKit/Sessions/Shared/Services/RawCommand/YKFKeyRawCommandService.h b/YubiKit/YubiKit/Sessions/Shared/Services/RawCommand/YKFKeyRawCommandService.h
new file mode 100755
index 000000000..1c19d110e
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Services/RawCommand/YKFKeyRawCommandService.h
@@ -0,0 +1,131 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+#import "YKFKeyService.h"
+#import "YKFAPDU.h"
+
+/**
+ * ---------------------------------------------------------------------------------------------------------------------
+ * @name Raw Command Service Response Blocks
+ * ---------------------------------------------------------------------------------------------------------------------
+ */
+
+/*!
+ @abstract
+    Response block for [executeCommand:completion:] which provides the result for the execution
+    of the raw request.
+ 
+ @param response
+    The response of the request when it was successful. In case of error this parameter is nil.
+ 
+ @param error
+    In case of a failed request this parameter contains the error. If the request was successful
+    this parameter is nil.
+ */
+typedef void (^YKFKeyRawCommandServiceResponseBlock)
+    (NSData* _Nullable response, NSError* _Nullable error);
+
+/**
+ * ---------------------------------------------------------------------------------------------------------------------
+ * @name Raw Command Service Protocol
+ * ---------------------------------------------------------------------------------------------------------------------
+ */
+
+NS_ASSUME_NONNULL_BEGIN
+
+/*!
+ @abstract
+    Defines the interface for YKFKeyRawCommandService.
+ */
+@protocol YKFKeyRawCommandServiceProtocol<NSObject>
+
+/*!
+ @method executeCommand:completion:
+ 
+ @abstract
+    Sends to the key a raw APDU command to be executed by the key. The request is performed asynchronously
+    on a background execution queue.
+ 
+ @param apdu
+    The APDU command to be executed.
+ 
+ @param completion
+    The response block which is executed after the request was processed by the key. The completion block
+    will be executed on a background thread.
+ 
+ @note:
+    This method is thread safe and can be invoked from any thread (main or a background thread).
+ */
+- (void)executeCommand:(YKFAPDU *)apdu completion:(YKFKeyRawCommandServiceResponseBlock)completion;
+
+/*!
+ @method executeSyncCommand:completion:
+ 
+ @abstract
+    Sends synchronously to the key a raw APDU command to be executed. Calling this method will block the
+    execution of the calling thread until the request is fulfilled by the key or if it's timing out.
+ 
+ @discussion
+    This method should never be called from the main thread. If the application calls
+    it from the main thread, an assertion will be fired in debug configurations.
+ 
+ @param apdu
+    The APDU command to be executed.
+ 
+ @param completion
+    The response block which is executed after the request was processed by the key. The completion block
+    will be executed on a background thread. After the completion block is executed the calling thread will
+    resume its execution.
+ */
+- (void)executeSyncCommand:(YKFAPDU *)apdu completion:(YKFKeyRawCommandServiceResponseBlock)completion;
+
+@end
+
+NS_ASSUME_NONNULL_END
+
+/**
+ * ---------------------------------------------------------------------------------------------------------------------
+ * @name Raw Command Service
+ * ---------------------------------------------------------------------------------------------------------------------
+ */
+
+NS_ASSUME_NONNULL_BEGIN
+
+/*!
+ @class YKFKeyRawCommandService
+ 
+ @abstract
+    Provides a low level interface to communicate with the YubiKey.
+ 
+ @discussion
+    This service provides a low level interface to execute requests agaist the key when other specialized
+    services (e.g U2F, OATH etc.) may not be used. While this interface allows for low level interactions
+    with the key, it's recommended to use specialized services when available (e.g. if the intention
+    is to use U2F, it's better to use the U2F Service provided by the library).
+ 
+    The Raw Command service is mantained by the key session which controls its lifecycle. The application
+    must not create one. It has to use only the single shared instance from YKFAccessorySession and sync its
+    usage with the session state.
+ */
+@interface YKFKeyRawCommandService: YKFKeyService<YKFKeyRawCommandServiceProtocol>
+
+/*
+ Not available: use only the instance from the YKFAccessorySession.
+ */
+- (instancetype)init NS_UNAVAILABLE;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/YubiKit/YubiKit/Sessions/Shared/Services/RawCommand/YKFKeyRawCommandService.m b/YubiKit/YubiKit/Sessions/Shared/Services/RawCommand/YKFKeyRawCommandService.m
new file mode 100755
index 000000000..e9bb6f655
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Services/RawCommand/YKFKeyRawCommandService.m
@@ -0,0 +1,102 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "YKFKeyRawCommandService.h"
+#import "YKFKeyRawCommandService+Private.h"
+#import "YKFAccessoryConnectionController.h"
+#import "YKFKeyCommandConfiguration.h"
+#import "YKFKeySessionError.h"
+#import "YKFBlockMacros.h"
+#import "YKFAssert.h"
+
+#import "YKFAPDU+Private.h"
+#import "YKFKeyService+Private.h"
+#import "YKFKeySessionError+Private.h"
+
+// Make a long timeout. This should be double checked by WTX responses.
+static const NSTimeInterval YKFKeyRawCommandServiceCommandTimeout = 600;
+
+@interface YKFKeyRawCommandService()
+
+@property (nonatomic) id<YKFKeyConnectionControllerProtocol> connectionController;
+@property (nonatomic) YKFKeyCommandConfiguration *commandExecutionConfiguration;
+
+@end
+
+@implementation YKFKeyRawCommandService
+
+- (instancetype)initWithConnectionController:(id<YKFKeyConnectionControllerProtocol>)connectionController {
+    YKFAssertAbortInit(connectionController);
+    
+    self = [super init];
+    if (self) {
+        self.connectionController = connectionController;
+        self.commandExecutionConfiguration = [YKFKeyCommandConfiguration defaultCommandCofiguration];
+    }
+    return self;
+}
+
+#pragma mark - Command Execution
+
+- (void)executeCommand:(YKFAPDU *)apdu completion:(YKFKeyRawCommandServiceResponseBlock)completion {
+    YKFParameterAssertReturn(apdu);
+    YKFParameterAssertReturn(completion);
+    
+    [self.delegate keyService:self willExecuteRequest:nil];
+    
+    [self.connectionController execute:apdu
+                         configuration:self.commandExecutionConfiguration
+                            completion:^(NSData *response, NSError * error, NSTimeInterval executionTime) {
+        if (error) {
+            completion(nil, error);
+            return;
+        }
+        completion(response, nil);
+    }];
+}
+
+- (void)executeSyncCommand:(YKFAPDU *)apdu completion:(YKFKeyRawCommandServiceResponseBlock)completion {
+    YKFParameterAssertReturn(apdu);
+    YKFParameterAssertReturn(completion);
+    
+    YKFAssertOffMainThread();
+    
+    [self.delegate keyService:self willExecuteRequest:nil];
+    
+    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
+    
+    ykf_weak_self();
+    [self.connectionController execute:apdu
+                         configuration:self.commandExecutionConfiguration
+                            completion:^(NSData *response, NSError * error, NSTimeInterval executionTime) {
+        ykf_safe_strong_self();
+        if (error) {
+            completion(nil, error);
+            dispatch_semaphore_signal(semaphore);
+            return;
+        }
+        
+        completion(response, nil);
+        dispatch_semaphore_signal(semaphore);
+    }];
+    
+    dispatch_time_t timeout = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(YKFKeyRawCommandServiceCommandTimeout * NSEC_PER_SEC));
+    long requestDidTimeout = dispatch_semaphore_wait(semaphore, timeout);
+    
+    if (requestDidTimeout) {
+        completion(nil, [YKFKeySessionError errorWithCode:YKFKeySessionErrorReadTimeoutCode]);
+    }
+}
+
+@end
diff --git a/YubiKit/YubiKit/Sessions/Shared/Services/U2F/YKFKeyU2FService+Private.h b/YubiKit/YubiKit/Sessions/Shared/Services/U2F/YKFKeyU2FService+Private.h
new file mode 100755
index 000000000..029b7b74c
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Services/U2F/YKFKeyU2FService+Private.h
@@ -0,0 +1,27 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+
+@protocol YKFKeyConnectionControllerProtocol;
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface YKFKeyU2FService()
+
+- (nullable instancetype)initWithConnectionController:(nonnull id<YKFKeyConnectionControllerProtocol>)connectionController NS_DESIGNATED_INITIALIZER;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/YubiKit/YubiKit/Sessions/Shared/Services/U2F/YKFKeyU2FService.h b/YubiKit/YubiKit/Sessions/Shared/Services/U2F/YKFKeyU2FService.h
new file mode 100755
index 000000000..eb883d5bb
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Services/U2F/YKFKeyU2FService.h
@@ -0,0 +1,192 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+#import "YKFKeyService.h"
+#import "YKFKeyU2FSignRequest.h"
+#import "YKFKeyU2FSignResponse.h"
+#import "YKFKeyU2FRegisterRequest.h"
+#import "YKFKeyU2FRegisterResponse.h"
+
+/**
+ * ---------------------------------------------------------------------------------------------------------------------
+ * @name U2F Service Response Blocks
+ * ---------------------------------------------------------------------------------------------------------------------
+ */
+
+/*!
+ @abstract
+    Response block used by [executeSignRequest:completion:] to provide the result of a sign request.
+ 
+ @param response
+    The response of the request when it was successful. In case of error this parameter is nil.
+ 
+ @param error
+    In case of a failed request this parameter contains the error. If the request was successful
+    this parameter is nil.
+ */
+typedef void (^YKFKeyU2FServiceSignCompletionBlock)
+    (YKFKeyU2FSignResponse* _Nullable response, NSError* _Nullable error);
+
+/*!
+ @abstract
+    Response block used by [executeRegisterRequest:completion:] to provide the result of a register request.
+ 
+ @param response
+    The response of the request when it was successful. In case of error this parameter is nil.
+ 
+ @param error
+    In case of a failed request this parameter contains the error. If the request was successful this
+    parameter is nil.
+ */
+typedef void (^YKFKeyU2FServiceRegisterCompletionBlock)
+    (YKFKeyU2FRegisterResponse* _Nullable response, NSError* _Nullable error);
+
+/**
+ * ---------------------------------------------------------------------------------------------------------------------
+ * @name U2F Service Types
+ * ---------------------------------------------------------------------------------------------------------------------
+ */
+
+/*!
+ @abstract
+    Enumerates the contextual states of the key when performing U2F requests.
+ */
+typedef NS_ENUM(NSUInteger, YKFKeyU2FServiceKeyState) {
+    
+    /// The key is not performing any U2F operation.
+    YYKFKeyU2FServiceKeyStateIdle,
+    
+    /// The key is executing an U2F request.
+    YKFKeyU2FServiceKeyStateProcessingRequest,
+    
+    /// The user must touch the key to prove a human presence which allows the key to perform the current
+    /// U2F operation.
+    YKFKeyU2FServiceKeyStateTouchKey
+};
+
+/**
+ * ---------------------------------------------------------------------------------------------------------------------
+ * @name U2F Service Protocol
+ * ---------------------------------------------------------------------------------------------------------------------
+ */
+
+NS_ASSUME_NONNULL_BEGIN
+
+/*!
+ @abstract
+    Defines the interface for YKFKeyU2FService.
+ */
+@protocol YKFKeyU2FServiceProtocol<NSObject>
+
+/*!
+ @property keyState
+ 
+ @abstract
+    This property provides the contextual state of the key when performing U2F requests.
+ 
+ @discussion
+    This property is useful for checking the status of an U2F request, when the operation requires the
+    user presence. This property is KVO compliant and the application should observe it to ge asynchronous
+    state updates of the U2F request.
+ 
+ @note:
+    The default behaviour of YubiKit is to always ask for human presence when performing an U2F operation. To detect
+    asynchronously the touch state check for YKFKeyU2FServiceKeyStateTouchKey.
+ */
+@property (nonatomic, assign, readonly) YKFKeyU2FServiceKeyState keyState;
+
+/*!
+ @method executeRegisterRequest:completion:
+ 
+ @abstract
+    Sends to the key an U2F register request. The request is performed asynchronously on a background execution queue.
+ 
+ @param request
+    The request which packs all the required information to perform a registration.
+ 
+ @param completion
+    The response block which gets executed after the request was processed by the key. The completion block will be
+    executed on a background thread. If the intention is to update the UI, dispatch the results on the main thread
+    to avoid an UIKit assertion.
+ 
+ @note:
+    This method is thread safe and can be invoked from the main or a background thread.
+    The key can execute only one request at a time. If multiple requests are made against the service, they are
+    queued in the order they are received and executed sequentially.
+ */
+- (void)executeRegisterRequest:(YKFKeyU2FRegisterRequest *)request
+                    completion:(YKFKeyU2FServiceRegisterCompletionBlock)completion;
+
+/*!
+ @method executeRegisterRequest:completion:
+ 
+ @abstract
+    Sends to the key an U2F sign request. The request is performed asynchronously on a background execution queue.
+ 
+ @param request
+    The request which packs all the required information to perform a signing.
+ @param completion
+    The response block which gets executed after the request was processed by the key. The completion block will be
+    executed on a background thread. If the intention is to update the UI, dispatch the results on the main thread
+    to avoid an UIKit assertion.
+ 
+ NOTE:
+    This method is thread safe and can be invoked from the main or a background thread.
+    The key can execute only one request at a time. If multiple requests are made against the service, they are
+    queued in the order they are received and executed sequentially.
+ */
+- (void)executeSignRequest:(YKFKeyU2FSignRequest *)request
+                completion:(YKFKeyU2FServiceSignCompletionBlock)completion;
+
+@end
+
+NS_ASSUME_NONNULL_END
+
+/**
+ * ---------------------------------------------------------------------------------------------------------------------
+ * @name U2F Service
+ * ---------------------------------------------------------------------------------------------------------------------
+ */
+
+NS_ASSUME_NONNULL_BEGIN
+
+/*!
+ @class YKFKeyU2FService
+ 
+ @abstract
+    Provides the interface for sending U2F requests to the YubiKey.
+ @discussion
+    The U2F service is mantained by the key session which controls its lifecycle. The application must not create one.
+    It has to use only the single shared instance from YKFAccessorySession and sync its usage with the session state.
+ */
+@interface YKFKeyU2FService: YKFKeyService<YKFKeyU2FServiceProtocol>
+
+/*
+ Not available: use only the shared instance from the YKFAccessorySession.
+ */
+- (instancetype)init NS_UNAVAILABLE;
+
+@end
+
+/*!
+ @constant YKFKeyU2FServiceProtocolKeyStatePropertyKey
+ 
+ @abstract
+    Helper property name to setup KVO paths in ObjC. For Swift there is a better built-in language support for
+    composing keypaths.
+ */
+extern NSString* const YKFKeyU2FServiceProtocolKeyStatePropertyKey;
+
+NS_ASSUME_NONNULL_END
diff --git a/YubiKit/YubiKit/Sessions/Shared/Services/U2F/YKFKeyU2FService.m b/YubiKit/YubiKit/Sessions/Shared/Services/U2F/YKFKeyU2FService.m
new file mode 100755
index 000000000..f77d9ee72
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Services/U2F/YKFKeyU2FService.m
@@ -0,0 +1,242 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "YKFKeyU2FService.h"
+#import "YKFSelectU2FApplicationAPDU.h"
+#import "YKFAccessoryConnectionController.h"
+#import "YKFKeyU2FError.h"
+#import "YKFKeyAPDUError.h"
+#import "YKFKeyCommandConfiguration.h"
+#import "YKFBlockMacros.h"
+#import "YKFAssert.h"
+
+#import "YKFKeySessionError+Private.h"
+#import "YKFKeyU2FService+Private.h"
+#import "YKFKeyU2FRequest+Private.h"
+#import "YKFKeyU2FRegisterResponse+Private.h"
+#import "YKFKeyU2FSignResponse+Private.h"
+#import "YKFKeyService+Private.h"
+#import "YKFAPDU+Private.h"
+
+typedef void (^YKFKeyU2FServiceResultCompletionBlock)(NSData* _Nullable  result, NSError* _Nullable error);
+
+NSString* const YKFKeyU2FServiceProtocolKeyStatePropertyKey = @"keyState";
+
+@interface YKFKeyU2FService()
+
+@property (nonatomic, assign, readwrite) YKFKeyU2FServiceKeyState keyState;
+@property (nonatomic) id<YKFKeyConnectionControllerProtocol> connectionController;
+
+@end
+
+@implementation YKFKeyU2FService
+
+- (instancetype)initWithConnectionController:(id<YKFKeyConnectionControllerProtocol>)connectionController {
+    YKFAssertAbortInit(connectionController);
+    
+    self = [super init];
+    if (self) {
+        self.connectionController = connectionController;
+    }
+    return self;
+}
+
+#pragma mark - Key State
+
+- (void)updateKeyState:(YKFKeyU2FServiceKeyState)keyState {
+    if (self.keyState == keyState) {
+        return;
+    }
+    self.keyState = keyState;
+}
+
+#pragma mark - U2F Register
+
+- (void)executeRegisterRequest:(YKFKeyU2FRegisterRequest *)request completion:(YKFKeyU2FServiceRegisterCompletionBlock)completion {
+    YKFParameterAssertReturn(request);
+    YKFParameterAssertReturn(completion);
+    
+    ykf_weak_self();
+    [self executeU2FRequest:request completion:^(NSData *result, NSError *error) {
+        ykf_safe_strong_self();
+        if (error) {
+            completion(nil, error);
+            return;
+        }
+        YKFKeyU2FRegisterResponse *registerResponse = [strongSelf processRegisterData:result request:request];
+        completion(registerResponse, nil);
+    }];
+}
+
+#pragma mark - U2F Sign
+
+- (void)executeSignRequest:(YKFKeyU2FSignRequest *)request completion:(YKFKeyU2FServiceSignCompletionBlock)completion {
+    YKFParameterAssertReturn(request);
+    YKFParameterAssertReturn(completion);
+
+    ykf_weak_self();
+    [self executeU2FRequest:request completion:^(NSData *result, NSError *error) {
+        ykf_safe_strong_self();
+        if (error) {
+            completion(nil, error);
+            return;
+        }
+        YKFKeyU2FSignResponse *signResponse = [strongSelf processSignData:result request:request];
+        completion(signResponse, nil);
+    }];
+}
+
+#pragma mark - Application Selection
+
+- (void)selectU2FApplicationWithCompletion:(void (^)(NSError *))completion {
+    YKFAPDU *selectU2FApplicationAPDU = [[YKFSelectU2FApplicationAPDU alloc] init];
+    
+    ykf_weak_self();
+    [self.connectionController execute:selectU2FApplicationAPDU
+                         configuration:[YKFKeyCommandConfiguration fastCommandCofiguration]
+                            completion:^(NSData *result, NSError *error, NSTimeInterval executionTime) {
+        ykf_safe_strong_self();
+        NSError *returnedError = nil;
+        
+        if (error) {
+            returnedError = error;
+        } else {
+            int statusCode = [strongSelf statusCodeFromKeyResponse: result];            
+            switch (statusCode) {
+                case YKFKeyAPDUErrorCodeNoError:
+                    break;
+                    
+                case YKFKeyAPDUErrorCodeMissingFile:
+                    returnedError = [YKFKeySessionError errorWithCode:YKFKeySessionErrorMissingApplicationCode];
+                    break;
+                    
+                default:
+                    returnedError = [YKFKeySessionError errorWithCode:statusCode];
+            }
+        }
+        
+        completion(returnedError);
+    }];
+}
+
+#pragma mark - Request Execution
+
+- (void)executeU2FRequest:(YKFKeyU2FRequest *)request completion:(YKFKeyU2FServiceResultCompletionBlock)completion {
+    YKFParameterAssertReturn(request);
+    YKFParameterAssertReturn(completion);
+    
+    [self.delegate keyService:self willExecuteRequest:request];
+    
+    [self updateKeyState:YKFKeyU2FServiceKeyStateProcessingRequest];
+    
+    ykf_weak_self();
+    [self selectU2FApplicationWithCompletion:^(NSError *error) {
+        ykf_safe_strong_self();
+        if (error) {
+            [strongSelf updateKeyState:YYKFKeyU2FServiceKeyStateIdle];
+            completion(nil, error);
+            return;
+        }
+        [strongSelf executeU2FRequestWithoutApplicationSelection:request completion:completion];
+    }];
+}
+
+- (void)executeU2FRequestWithoutApplicationSelection:(YKFKeyU2FRequest *)request completion:(YKFKeyU2FServiceResultCompletionBlock)completion {
+    YKFParameterAssertReturn(request);
+    YKFParameterAssertReturn(completion);
+    
+    ykf_weak_self();
+    YKFKeyConnectionControllerCommandResponseBlock block = ^(NSData *result, NSError *error, NSTimeInterval executionTime) {
+        ykf_safe_strong_self();
+        if (error) {
+            [strongSelf updateKeyState:YYKFKeyU2FServiceKeyStateIdle];
+            completion(nil, error);
+            return;
+        }
+        int statusCode = [strongSelf statusCodeFromKeyResponse: result];
+        
+        switch (statusCode) {
+            case YKFKeyAPDUErrorCodeConditionNotSatisfied: {
+                [strongSelf handleTouchRequired:request completion:completion];
+            }
+            break;
+                
+            case YKFKeyAPDUErrorCodeNoError: {
+                [strongSelf updateKeyState:YYKFKeyU2FServiceKeyStateIdle];
+                completion(result, nil);
+            }
+            break;
+                
+            case YKFKeyAPDUErrorCodeWrongData: {
+                [strongSelf updateKeyState:YYKFKeyU2FServiceKeyStateIdle];
+                YKFKeySessionError *connectionError = [YKFKeyU2FError errorWithCode:YKFKeyU2FErrorCodeU2FSigningUnavailable];
+                completion(nil, connectionError);
+            }
+            break;
+                
+            case YKFKeyAPDUErrorCodeInsNotSupported: {
+                [strongSelf updateKeyState:YYKFKeyU2FServiceKeyStateIdle];
+                completion(nil, [YKFKeySessionError errorWithCode:YKFKeySessionErrorMissingApplicationCode]);
+            }
+            break;
+                
+            // Errors - The status code is the error. The key doesn't send any other information.
+            default: {
+                [strongSelf updateKeyState:YYKFKeyU2FServiceKeyStateIdle];
+                YKFKeySessionError *connectionError = [YKFKeySessionError errorWithCode:statusCode];
+                completion(nil, connectionError);
+            }
+            break;
+        }
+    };
+    [self.connectionController execute:request.apdu completion:block];
+}
+
+#pragma mark - Private
+
+- (void)handleTouchRequired:(YKFKeyU2FRequest *)request completion:(YKFKeyU2FServiceResultCompletionBlock)completion {
+    YKFParameterAssertReturn(completion);
+    
+    if (![request shouldRetry]) {
+        YKFKeySessionError *timeoutError = [YKFKeySessionError errorWithCode:YKFKeySessionErrorTouchTimeoutCode];
+        completion(nil, timeoutError);
+        
+        [self updateKeyState:YYKFKeyU2FServiceKeyStateIdle];
+        return;
+    }
+    
+    [self updateKeyState:YKFKeyU2FServiceKeyStateTouchKey];    
+    request.retries += 1;
+    
+    ykf_weak_self();
+    [self.connectionController dispatchOnSequentialQueue:^{
+        ykf_safe_strong_self();
+        [strongSelf executeU2FRequestWithoutApplicationSelection:request completion:completion];
+    }
+    delay:request.retryTimeInterval];
+}
+
+#pragma mark - Key responses
+
+- (YKFKeyU2FSignResponse *)processSignData:(NSData *)data request:(YKFKeyU2FSignRequest *)request {
+    NSData *signature = [self dataFromKeyResponse:data];
+    return [[YKFKeyU2FSignResponse alloc] initWithKeyHandle:request.keyHandle clientData:request.clientData signature:signature];
+}
+
+- (YKFKeyU2FRegisterResponse *)processRegisterData:(NSData *)data request:(YKFKeyU2FRegisterRequest *)request {
+    NSData *registrationData = [self dataFromKeyResponse:data];
+    return [[YKFKeyU2FRegisterResponse alloc] initWithClientData:request.clientData registrationData:registrationData];
+}
+
+@end
diff --git a/YubiKit/YubiKit/Sessions/Shared/Services/YKFKeyService+Private.h b/YubiKit/YubiKit/Sessions/Shared/Services/YKFKeyService+Private.h
new file mode 100755
index 000000000..a6ff2cefc
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Services/YKFKeyService+Private.h
@@ -0,0 +1,46 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+#import "YKFKeyService.h"
+#import "YKFKeyRequest.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/*!
+ Receives updates when the service performs a set of operations.
+ The service can be its own delegate or it can receive forwarded updates from a central delegate.
+ */
+@protocol YKFKeyServiceDelegate<NSObject>
+
+- (void)keyService:(YKFKeyService *)service willExecuteRequest:(nullable YKFKeyRequest *)request;
+
+@end
+
+@interface YKFKeyService()<YKFKeyServiceDelegate>
+
+@property (nonatomic, weak) id<YKFKeyServiceDelegate> delegate;
+
+/// Removes the YLP headers and status code from the response data received from a key command response.
+- (NSData *)dataFromKeyResponse:(NSData *)response;
+
+/// Returns the status code from a response received from a key command response.
+- (UInt16)statusCodeFromKeyResponse:(NSData *)response;
+
+/// Returns the first byte value of the status code.
+- (UInt8)shortStatusCodeFromStatusCode:(UInt16)statusCode;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/YubiKit/YubiKit/Sessions/Shared/Services/YKFKeyService.h b/YubiKit/YubiKit/Sessions/Shared/Services/YKFKeyService.h
new file mode 100755
index 000000000..0c437587f
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Services/YKFKeyService.h
@@ -0,0 +1,23 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+#import "YKFKeyRequest.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface YKFKeyService: NSObject
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/YubiKit/YubiKit/Sessions/Shared/Services/YKFKeyService.m b/YubiKit/YubiKit/Sessions/Shared/Services/YKFKeyService.m
new file mode 100755
index 000000000..2cb6d7169
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/Services/YKFKeyService.m
@@ -0,0 +1,58 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "YKFKeyService.h"
+#import "YKFKeyService+Private.h"
+#import "YKFAccessoryConnectionController.h"
+#import "YKFNSDataAdditions.h"
+#import "YKFNSDataAdditions+Private.h"
+#import "YKFKeyAPDUError.h"
+#import "YKFAssert.h"
+
+@implementation YKFKeyService
+
+#pragma mark - Key Response
+
+- (NSData *)dataFromKeyResponse:(NSData *)response {
+    YKFParameterAssertReturnValue(response, [NSData data]);
+    YKFAssertReturnValue(response.length >= 2, @"Key response data is too short.", [NSData data]);
+    
+    if (response.length == 2) {
+        return [NSData data];
+    } else {
+        NSRange range = {0, response.length - 2};
+        return [response subdataWithRange:range];
+    }
+}
+
+#pragma mark - Status Code
+
+- (UInt16)statusCodeFromKeyResponse:(NSData *)response {    
+    YKFParameterAssertReturnValue(response, YKFKeyAPDUErrorCodeWrongLength);
+    YKFAssertReturnValue(response.length >= 2, @"Key response data is too short.", YKFKeyAPDUErrorCodeWrongLength);
+    
+    return [response ykf_getBigEndianIntegerInRange:NSMakeRange([response length] - 2, 2)];
+}
+
+- (UInt8)shortStatusCodeFromStatusCode:(UInt16)statusCode {
+    return (UInt8)(statusCode >> 8);
+}
+
+#pragma mark - YKFKeyServiceDelegate
+
+- (void)keyService:(YKFKeyService *)service willExecuteRequest:(YKFKeyRequest *)request {
+    // Does nothing: override this in the service subclasses when necessary.
+}
+
+@end
diff --git a/YubiKit/YubiKit/Sessions/Shared/YKFKeyCommandConfiguration.h b/YubiKit/YubiKit/Sessions/Shared/YKFKeyCommandConfiguration.h
new file mode 100755
index 000000000..d1f102051
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/YKFKeyCommandConfiguration.h
@@ -0,0 +1,38 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface YKFKeyCommandConfiguration: NSObject
+
+/// The expected avarage execution time for the command.
+@property (nonatomic, assign) NSTimeInterval commandTime;
+
+/// The timeout for a command.
+@property (nonatomic, assign) NSTimeInterval commandTimeout;
+
+/// The time to wait between data availabe checks.
+@property (nonatomic, assign) NSTimeInterval commandProbeTime;
+
+// Factory methods
+
++ (YKFKeyCommandConfiguration *)fastCommandCofiguration;
++ (YKFKeyCommandConfiguration *)defaultCommandCofiguration;
++ (YKFKeyCommandConfiguration *)longCommandCofiguration;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/YubiKit/YubiKit/Sessions/Shared/YKFKeyCommandConfiguration.m b/YubiKit/YubiKit/Sessions/Shared/YKFKeyCommandConfiguration.m
new file mode 100755
index 000000000..eaadff91f
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/YKFKeyCommandConfiguration.m
@@ -0,0 +1,49 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "YKFKeyCommandConfiguration.h"
+
+@implementation YKFKeyCommandConfiguration
+
++ (YKFKeyCommandConfiguration *)fastCommandCofiguration {
+    YKFKeyCommandConfiguration *configuration = [[YKFKeyCommandConfiguration alloc] init];
+    
+    configuration.commandTime = 0.0;
+    configuration.commandTimeout = 5;
+    configuration.commandProbeTime = 0.05;
+    
+    return configuration;
+}
+
++ (YKFKeyCommandConfiguration *)defaultCommandCofiguration {
+    YKFKeyCommandConfiguration *configuration = [[YKFKeyCommandConfiguration alloc] init];
+
+    configuration.commandTime = 0.2;
+    configuration.commandTimeout = 10;
+    configuration.commandProbeTime = 0.05;
+    
+    return configuration;
+}
+
++ (YKFKeyCommandConfiguration *)longCommandCofiguration {
+    YKFKeyCommandConfiguration *configuration = [[YKFKeyCommandConfiguration alloc] init];
+    
+    configuration.commandTime = 2;
+    configuration.commandTimeout = 30;
+    configuration.commandProbeTime = 0.05;
+    
+    return configuration;
+}
+
+@end
diff --git a/YubiKit/YubiKit/Sessions/Shared/YKFKeyConnectionControllerProtocol.h b/YubiKit/YubiKit/Sessions/Shared/YKFKeyConnectionControllerProtocol.h
new file mode 100755
index 000000000..eeb1b8641
--- /dev/null
+++ b/YubiKit/YubiKit/Sessions/Shared/YKFKeyConnectionControllerProtocol.h
@@ -0,0 +1,36 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "YKFAPDU.h"
+#import "YKFKeyCommandConfiguration.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+typedef void (^YKFKeyConnectionControllerCommandResponseBlock)(NSData* _Nullable, NSError* _Nullable, NSTimeInterval);
+typedef void (^YKFKeyConnectionControllerCompletionBlock)(void);
+
+@protocol YKFKeyConnectionControllerProtocol
+
+- (void)execute:(YKFAPDU *)command completion:(YKFKeyConnectionControllerCommandResponseBlock)completion;
+- (void)execute:(YKFAPDU *)command configuration:(YKFKeyCommandConfiguration *)configuration completion:(YKFKeyConnectionControllerCommandResponseBlock)completion;
+
+- (void)dispatchOnSequentialQueue:(YKFKeyConnectionControllerCompletionBlock)block delay:(NSTimeInterval)delay;
+- (void)dispatchOnSequentialQueue:(YKFKeyConnectionControllerCompletionBlock)block;
+
+- (void)closeConnectionWithCompletion:(YKFKeyConnectionControllerCompletionBlock)completion;
+- (void)cancelAllCommands;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/YubiKit/YubiKit/SharedModel/YKFOTPTextParser.h b/YubiKit/YubiKit/SharedModel/YKFOTPTextParser.h
new file mode 100755
index 000000000..2c6a31715
--- /dev/null
+++ b/YubiKit/YubiKit/SharedModel/YKFOTPTextParser.h
@@ -0,0 +1,35 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+#import "YKFOTPTokenValidator.h"
+#import "YKFOTPTextParserProtocol.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/*!
+ @class YKFOTPTextParser
+ 
+ @abstract
+    The default implementation for parsing the OTP NDEF payload whnen the type is URI. This parser checks the format
+    en-US\<Text\>/\<token value\>. A usual Text payload looks like this: en-US/72403154.
+ */
+@interface YKFOTPTextParser : NSObject<YKFOTPTextParserProtocol>
+
+- (nullable instancetype)initWithValidator:(id<YKFOTPTokenValidatorProtocol>)validator NS_DESIGNATED_INITIALIZER;
+- (instancetype)init NS_UNAVAILABLE;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/YubiKit/YubiKit/SharedModel/YKFOTPTextParser.m b/YubiKit/YubiKit/SharedModel/YKFOTPTextParser.m
new file mode 100755
index 000000000..39bdbbbb9
--- /dev/null
+++ b/YubiKit/YubiKit/SharedModel/YKFOTPTextParser.m
@@ -0,0 +1,62 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "YKFOTPTextParser.h"
+#import "YKFAssert.h"
+
+static const UInt8 YKFOTPTextParserCustomPayloadType = 0x05;
+
+@interface YKFOTPTextParser()
+
+@property (nonatomic, nonnull) id<YKFOTPTokenValidatorProtocol> validator;
+
+@end
+
+@implementation YKFOTPTextParser
+
+- (instancetype)initWithValidator:(id<YKFOTPTokenValidatorProtocol>)validator {
+    YKFAssertAbortInit(validator)
+    
+    self = [super init];
+    if (self) {
+        self.validator = validator;
+    }
+    return self;
+}
+
+- (NSString *)tokenFromPayload:(NSString *)payload {
+    YKFParameterAssertReturnValue(payload, @"")
+    
+    NSArray *components = [payload componentsSeparatedByString:@"/"];
+    if (components.count >= 2) {
+        NSString *token = components.lastObject;
+        return [self.validator validateToken:token] ? token : @"";
+    }
+    
+    // Assume the payload is the token
+    return [self.validator validateToken:payload] ? payload : @"";
+}
+
+- (NSString *)textFromPayload:(NSString *)payload {
+    YKFParameterAssertReturnValue(payload, nil)
+        
+    if([payload characterAtIndex:0] == YKFOTPTextParserCustomPayloadType) {
+        // The payload contains a custom type identifier
+        return [payload substringFromIndex:1];
+    } else {
+        return payload;
+    }
+}
+
+@end
diff --git a/YubiKit/YubiKit/SharedModel/YKFOTPTextParserProtocol.h b/YubiKit/YubiKit/SharedModel/YKFOTPTextParserProtocol.h
new file mode 100755
index 000000000..bbdfa2e81
--- /dev/null
+++ b/YubiKit/YubiKit/SharedModel/YKFOTPTextParserProtocol.h
@@ -0,0 +1,45 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+NS_ASSUME_NONNULL_BEGIN
+
+/*!
+ @protocol YKFOTPTextParserProtocol
+ 
+ @abstract
+    Interface for a NFC OTP custom text parser. If the used YubiKeys use a custom way of formatting the text received from the YubiKey
+    a custom parser can be provided by the host application by implementing this interface and setting it on YubiKitConfiguration.
+ */
+@protocol YKFOTPTextParserProtocol<NSObject>
+
+/*!
+ @method tokenFromPayload:
+ 
+ @abstract
+    Implements the extraction of the token from the payload. This method should always return a token, since the token
+    is the minimal information all payloads should have. In case the token is not valid it should return a empty string.
+ */
+- (NSString *)tokenFromPayload:(NSString *)payload;
+
+/*!
+ @method textFromPayload:
+ 
+ @abstract
+    Implements the extraction of the text from the payload. This method can return nil if no text is available in the payload.
+ */
+- (nullable NSString *)textFromPayload:(NSString *)payload;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/YubiKit/YubiKit/SharedModel/YKFOTPToken.h b/YubiKit/YubiKit/SharedModel/YKFOTPToken.h
new file mode 100755
index 000000000..ea3b7090f
--- /dev/null
+++ b/YubiKit/YubiKit/SharedModel/YKFOTPToken.h
@@ -0,0 +1,137 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+
+/**
+ * ---------------------------------------------------------------------------------------------------------------------
+ * @name YKFOTPToken Types
+ * ---------------------------------------------------------------------------------------------------------------------
+ */
+
+typedef NS_ENUM(NSUInteger, YKFOTPTokenType) {
+    /*!
+     @constant  YKFOTPTokenTypeYubicoOTP
+                The token is of type YubicoOTP.
+     */
+    YKFOTPTokenTypeYubicoOTP,
+    
+    /*!
+     @constant  YKFOTPTokenTypeHOTP
+                The token is of type HOTP.
+     */
+    YKFOTPTokenTypeHOTP,
+    
+    /*!
+     @constant  YKFOTPTokenTypeUnknown
+                The token type was not properly initialized.
+     */
+    YKFOTPTokenTypeUnknown
+};
+
+typedef NS_ENUM(NSUInteger, YKFOTPMetadataType) {
+    /*!
+     @constant  YKFOTPMetadataTypeURI
+                The token was provided with URI metadata.
+     */
+    YKFOTPMetadataTypeURI,
+
+    /*!
+     @constant  YKFOTPMetadataTypeText
+                The token was provided with Text metadata.
+     */
+    YKFOTPMetadataTypeText,
+    
+    /*!
+     @constant  YKFOTPMetadataTypeUnknown
+                The token metadata type was not properly initialized.
+     */
+    YKFOTPMetadataTypeUnknown
+};
+
+/**
+ * ---------------------------------------------------------------------------------------------------------------------
+ * @name YKFOTPTokenProtocol
+ * ---------------------------------------------------------------------------------------------------------------------
+ */
+
+/*!
+ @protocol YKFOTPTokenProtocol
+ 
+ @abstract
+    Provides the the interface for an OTP token.
+ */
+@protocol YKFOTPTokenProtocol<NSObject>
+
+/*!
+ @property type
+ 
+ @abstract
+    The type of token received from the YubiKey.
+ */
+@property (nonatomic) YKFOTPTokenType type;
+
+/*!
+ @property metadataType
+ 
+ @abstract
+    The type of the metadata received with the OTP token from the YubiKey.
+ */
+@property (nonatomic) YKFOTPMetadataType metadataType;
+
+/*!
+ @property value
+ 
+ @abstract
+    The OTP extracted from the payload received from the YubiKey.
+ */
+@property (nonatomic, nonnull) NSString *value;
+
+/*!
+ @property uri
+ 
+ @abstract
+    If the YubiKey was configured to provide an URI when providing the OTP token,
+    this property contains the URI.
+ 
+ NOTE:
+    The URI is the default configuration for YubiKeys.
+ */
+@property (nonatomic, nullable) NSString *uri;
+
+/*!
+ @property text
+ 
+ @abstract
+    If the YubiKey was configured to provide a text when providing the OTP token,
+    this property contains the text.
+ */
+@property (nonatomic, nullable) NSString *text;
+
+@end
+
+/**
+ * ---------------------------------------------------------------------------------------------------------------------
+ * @name YKFOTPToken
+ * ---------------------------------------------------------------------------------------------------------------------
+ */
+
+/*!
+ @class YKFOTPToken
+ 
+ @abstract
+    Default implementation for YKFOTPTokenProtocol.
+ */
+@interface YKFOTPToken : NSObject<YKFOTPTokenProtocol>
+@end
diff --git a/YubiKit/YubiKit/SharedModel/YKFOTPToken.m b/YubiKit/YubiKit/SharedModel/YKFOTPToken.m
new file mode 100755
index 000000000..492de90e2
--- /dev/null
+++ b/YubiKit/YubiKit/SharedModel/YKFOTPToken.m
@@ -0,0 +1,25 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "YKFOTPToken.h"
+
+@implementation YKFOTPToken
+
+@synthesize uri;
+@synthesize text;
+@synthesize metadataType;
+@synthesize value;
+@synthesize type;
+
+@end
diff --git a/YubiKit/YubiKit/SharedModel/YKFOTPTokenParser.h b/YubiKit/YubiKit/SharedModel/YKFOTPTokenParser.h
new file mode 100755
index 000000000..d7035d708
--- /dev/null
+++ b/YubiKit/YubiKit/SharedModel/YKFOTPTokenParser.h
@@ -0,0 +1,33 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+#import <CoreNFC/CoreNFC.h>
+#import "YKFOTPToken.h"
+
+typedef NS_ENUM(NSUInteger, YKFNDEFWellKnownType) {
+    YKFNDEFWellKnownTypeURI  = 0x55,
+    YKFNDEFWellKnownTypeText = 0x54
+};
+
+API_AVAILABLE(ios(11.0))
+@protocol YKFOTPTokenParserProtocol
+
+- (nullable id<YKFOTPTokenProtocol>)otpTokenFromNfcMessages:(nonnull NSArray *)messages;
+
+@end
+
+API_AVAILABLE(ios(11.0))
+@interface YKFOTPTokenParser : NSObject<YKFOTPTokenParserProtocol>
+@end
diff --git a/YubiKit/YubiKit/SharedModel/YKFOTPTokenParser.m b/YubiKit/YubiKit/SharedModel/YKFOTPTokenParser.m
new file mode 100755
index 000000000..7d13897bd
--- /dev/null
+++ b/YubiKit/YubiKit/SharedModel/YKFOTPTokenParser.m
@@ -0,0 +1,152 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "YKFOTPTokenParser.h"
+#import "YKFOTPTokenValidator.h"
+#import "YKFOTPURIParser.h"
+#import "YKFOTPTextParser.h"
+#import "YKFLogger.h"
+#import "YubiKitConfiguration.h"
+
+@interface YKFOTPTokenParser()
+
+@property (nonatomic, strong) id<YKFOTPTextParserProtocol> textParser;
+@property (nonatomic, strong) id<YKFOTPURIParserProtocol> uriParser;
+
+@end
+
+@implementation YKFOTPTokenParser
+
+- (instancetype)init {
+    self = [super init];
+    if (self) {
+        YKFOTPTokenValidator *otpTokenValidator = [[YKFOTPTokenValidator alloc] init];
+        
+        if (YubiKitConfiguration.customOTPURIParser) {
+            self.uriParser = YubiKitConfiguration.customOTPURIParser;
+        } else {
+            self.uriParser = [[YKFOTPURIParser alloc] initWithValidator:otpTokenValidator];
+        }
+
+        if (YubiKitConfiguration.customOTPTextParser) {
+            self.textParser = YubiKitConfiguration.customOTPTextParser;
+        } else {
+            self.textParser = [[YKFOTPTextParser alloc] initWithValidator:otpTokenValidator];
+        }
+    }
+    return self;
+}
+
+#pragma mark - YKFOTPTokenParserProtocol
+
+- (id<YKFOTPTokenProtocol>)otpTokenFromNfcMessages:(NSArray *)messages {
+    for (NFCNDEFMessage *message in messages) {
+        id<YKFOTPTokenProtocol> token = [self extractTokenFromMessage:message];
+        if (token) {
+            return token; // Get the first one since the hardware key should generate only one OTP.
+        }
+    }
+    return nil;
+}
+
+#pragma mark - Helpers
+
+- (id<YKFOTPTokenProtocol>)extractTokenFromMessage:(NFCNDEFMessage *)message {
+    for (NFCNDEFPayload *record in message.records) {
+        // Filter to get only the TNF which is generated by the YubiKey
+        if (record.typeNameFormat != NFCTypeNameFormatNFCWellKnown) {
+            continue;
+        }
+        
+        // Filter to get only URIs and text records as encoded by the YubiKey
+        if (![self recordHasURI:record] && ![self recordHasText:record]) {
+            continue;
+        }
+        
+        NSString *payload = [self extractPayloadFromData:record.payload];
+        if (!payload) {
+            continue;
+        }
+        
+        YKFOTPToken *token = [[YKFOTPToken alloc] init];
+        
+        if ([self recordHasURI:record]) {
+            token.metadataType = YKFOTPMetadataTypeURI;
+            YKFLogInfo(@"Metdata is URI");
+            
+            token.value = [self.uriParser tokenFromPayload:payload];
+            YKFLogInfo(@"OTP value %@", token.value);
+            
+            token.uri = [self.uriParser uriFromPayload:payload];
+            YKFLogInfo(@"OTP uri %@", token.uri);
+        }
+        else if ([self recordHasText:record]) {
+            token.metadataType = YKFOTPMetadataTypeText;
+            YKFLogInfo(@"Metdata is Text");
+            
+            token.value = [self.textParser tokenFromPayload:payload];
+            YKFLogInfo(@"OTP value %@", token.value);
+            
+            token.text = [self.textParser textFromPayload:payload];
+            YKFLogInfo(@"OTP text %@", token.text);
+        }
+     
+        if (!token.value.length) { // Payload was extracted but it was not a valid or malformed URL/Text
+            return nil;
+        }
+        
+        id<YKFOTPTokenValidatorProtocol> validator = [[YKFOTPTokenValidator alloc] init];
+        if ([validator maybeYubicoOTP:token.value]) {
+            token.type = YKFOTPTokenTypeYubicoOTP;
+        } else if ([validator maybeHOTP:token.value]) {
+            token.type = YKFOTPTokenTypeHOTP;
+        } else {
+            token.type = YKFOTPTokenTypeUnknown;
+        }
+        
+        return token;
+    }
+    return nil;
+}
+
+- (BOOL)recordHasURI:(NFCNDEFPayload *)record {
+    if (!record.type.length) {
+        return NO;
+    }
+    
+    const UInt8* bytes = record.type.bytes;
+    UInt8 recordType = bytes[0];
+    
+    return recordType == YKFNDEFWellKnownTypeURI;
+}
+
+- (BOOL)recordHasText:(NFCNDEFPayload *)record {
+    if (!record.type.length) {
+        return NO;
+    }
+    
+    const UInt8* bytes = record.type.bytes;
+    UInt8 recordType = bytes[0];
+    
+    return recordType == YKFNDEFWellKnownTypeText;
+}
+
+- (NSString *)extractPayloadFromData:(NSData *)data {
+    if (!data.length) {
+        return nil;
+    }
+    return [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
+}
+
+@end
diff --git a/YubiKit/YubiKit/SharedModel/YKFOTPTokenValidator.h b/YubiKit/YubiKit/SharedModel/YKFOTPTokenValidator.h
new file mode 100755
index 000000000..49e8f982b
--- /dev/null
+++ b/YubiKit/YubiKit/SharedModel/YKFOTPTokenValidator.h
@@ -0,0 +1,31 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+@protocol YKFOTPTokenValidatorProtocol
+
+- (BOOL)validateToken:(NSString*)token;
+
+- (BOOL)maybeYubicoOTP:(NSString*)token;
+- (BOOL)maybeHOTP:(NSString*)token;
+
+@end
+
+@interface YKFOTPTokenValidator : NSObject<YKFOTPTokenValidatorProtocol>
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/YubiKit/YubiKit/SharedModel/YKFOTPTokenValidator.m b/YubiKit/YubiKit/SharedModel/YKFOTPTokenValidator.m
new file mode 100755
index 000000000..ea2309d18
--- /dev/null
+++ b/YubiKit/YubiKit/SharedModel/YKFOTPTokenValidator.m
@@ -0,0 +1,99 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "YKFOTPTokenValidator.h"
+
+typedef NS_ENUM(NSUInteger, YKFOTPTokenValidatorTokenLength) {
+    YKFOTPTokenValidatorTokenLengthDefaultYubicoOTP = 44,
+    YKFOTPTokenValidatorTokenMinLengthYubicoOTP = 32,
+    YKFOTPTokenValidatorTokenMaxLengthYubicoOTP = 64,
+    
+    YKFOTPTokenValidatorTokenHOTPShort = 6,
+    YKFOTPTokenValidatorTokenHOTPLong = 8
+};
+
+@interface YKFOTPTokenValidator()
+
+@property (nonatomic, strong) NSCharacterSet *notNumbersCharacterSet;
+@property (nonatomic, strong) NSCharacterSet *notModhexCharacterSet;
+
+@end
+
+@implementation YKFOTPTokenValidator
+
+- (instancetype)init {
+    self = [super init];
+    if (self) {
+        self.notNumbersCharacterSet = [[NSCharacterSet characterSetWithCharactersInString:@"0123456789."] invertedSet];
+        // Modhex mapping: https://developers.yubico.com/yubico-c/Manuals/modhex.1.html
+        self.notModhexCharacterSet = [[NSCharacterSet characterSetWithCharactersInString:@"cbdefghijklnrtuv"] invertedSet];
+    }
+    return self;
+}
+
+#pragma mark - Validation
+
+- (BOOL)maybeHOTP:(NSString *)token {
+    if (token.length < YKFOTPTokenValidatorTokenHOTPShort) {
+        return NO;
+    }
+    
+    NSString *actualToken = nil;
+    
+    // Try long
+    if (token.length >= YKFOTPTokenValidatorTokenHOTPLong) {
+        NSUInteger index = token.length - YKFOTPTokenValidatorTokenHOTPLong;
+        actualToken = [token substringFromIndex:index];
+        
+        // Try short
+        if (![self tokenIsDecimalNumber:actualToken]) {
+            NSUInteger index = token.length - YKFOTPTokenValidatorTokenHOTPShort;
+            actualToken = [token substringFromIndex:index];
+        }
+    // Try short
+    } else {
+        NSUInteger index = token.length - YKFOTPTokenValidatorTokenHOTPShort;
+        actualToken = [token substringFromIndex:index];
+    }
+    
+    return [self tokenIsDecimalNumber:actualToken];
+}
+
+- (BOOL)maybeYubicoOTP:(NSString *)token {
+    int minLength = YKFOTPTokenValidatorTokenMinLengthYubicoOTP;
+    int maxLength = YKFOTPTokenValidatorTokenMaxLengthYubicoOTP;
+    
+    BOOL tokenIsInSizeRange = token.length >= minLength && token.length <= maxLength;
+    BOOL tokenIsModhexEncoded = [token rangeOfCharacterFromSet:self.notModhexCharacterSet].location == NSNotFound;
+    
+    return tokenIsInSizeRange && tokenIsModhexEncoded;
+}
+
+- (BOOL)validateToken:(NSString *)token {
+    return [self maybeYubicoOTP:token] || [self maybeHOTP:token];
+}
+
+#pragma mark - Helpers
+
+- (BOOL)tokenIsDecimalNumber:(NSString *)token {
+    if (!token.length) {
+        return NO;
+    }
+    if ([token rangeOfCharacterFromSet:self.notNumbersCharacterSet].location != NSNotFound) {
+        return NO;
+    }
+    return [token intValue] != 0;
+}
+
+@end
diff --git a/YubiKit/YubiKit/SharedModel/YKFOTPURIParser.h b/YubiKit/YubiKit/SharedModel/YKFOTPURIParser.h
new file mode 100755
index 000000000..12d5ada48
--- /dev/null
+++ b/YubiKit/YubiKit/SharedModel/YKFOTPURIParser.h
@@ -0,0 +1,35 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+#import "YKFOTPTokenValidator.h"
+#import "YKFOTPURIParserProtocol.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/*!
+ @class YKFOTPURIParser
+ 
+ @abstract
+    The default implementation for parsing the OTP NDEF payload when the type is URI.
+    This parser checks the format \<URI\>/[#]<token value\> where the token value is the last path component of the URI.
+ */
+@interface YKFOTPURIParser : NSObject<YKFOTPURIParserProtocol>
+
+- (nullable instancetype)initWithValidator:(id<YKFOTPTokenValidatorProtocol>)validator NS_DESIGNATED_INITIALIZER;
+- (instancetype)init NS_UNAVAILABLE;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/YubiKit/YubiKit/SharedModel/YKFOTPURIParser.m b/YubiKit/YubiKit/SharedModel/YKFOTPURIParser.m
new file mode 100755
index 000000000..8aca47118
--- /dev/null
+++ b/YubiKit/YubiKit/SharedModel/YKFOTPURIParser.m
@@ -0,0 +1,90 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "YKFOTPURIParser.h"
+#import "YKFURIIdentifierCode.h"
+#import "YKFAssert.h"
+
+@interface YKFOTPURIParser()
+
+@property (nonatomic) id<YKFOTPTokenValidatorProtocol> validator;
+@property (nonatomic) YKFURIIdentifierCode *uriIdentifierCode;
+
+@end
+
+@implementation YKFOTPURIParser
+
+- (instancetype)initWithValidator:(id<YKFOTPTokenValidatorProtocol>)validator {
+    YKFAssertAbortInit(validator);
+    
+    self = [super init];
+    if (self) {
+        self.validator = validator;
+        self.uriIdentifierCode = [[YKFURIIdentifierCode alloc] init];
+    }
+    return self;
+}
+
+- (NSString *)tokenFromPayload:(NSString *)payload {
+    YKFParameterAssertReturnValue(payload, @"");
+    
+    NSString *composedURL = [self composedURIFromPayload:payload];
+    NSURL *url = [[NSURL alloc] initWithString:composedURL];
+    
+    NSString *token = nil;
+    
+    if (url.pathComponents.count > 1) {
+        token = [url.absoluteString lastPathComponent];
+    } else {
+        // Assume the payload is the token
+        token = payload;
+    }
+    
+    // Check for # on newer versions of the YubiKey. Remove it before checking for the token value.
+    if ([token hasPrefix:@"#"]) {
+        token = [token stringByReplacingOccurrencesOfString:@"#" withString:@""];
+    }
+    
+    return [self.validator validateToken:token] ? token : @"";
+}
+
+- (NSString *)uriFromPayload:(NSString *)payload {
+    YKFParameterAssertReturnValue(payload, nil);
+    
+    NSString *composedURL = [self composedURIFromPayload:payload];
+    NSURL *url = [[NSURL alloc] initWithString:composedURL];
+    
+    if (url.pathComponents.count > 1) {
+        return url.absoluteString;
+    }
+    return nil;
+}
+
+#pragma mark - Helpers
+
+- (NSString *)composedURIFromPayload:(NSString *)payload {
+    NSNumber *firstChar = [NSNumber numberWithChar:[payload characterAtIndex:0]];
+    NSString *identifierCode = [self.uriIdentifierCode prependingStringForCode:firstChar.unsignedCharValue];
+    
+    if (!identifierCode) {
+        return payload;
+    }
+    
+    NSMutableString *composedString = [NSMutableString stringWithString:identifierCode];
+    [composedString appendString:[payload substringFromIndex:1]]; // Append the payload without the identifier code
+    
+    return [composedString copy];
+}
+
+@end
diff --git a/YubiKit/YubiKit/SharedModel/YKFOTPURIParserProtocol.h b/YubiKit/YubiKit/SharedModel/YKFOTPURIParserProtocol.h
new file mode 100755
index 000000000..3fa510653
--- /dev/null
+++ b/YubiKit/YubiKit/SharedModel/YKFOTPURIParserProtocol.h
@@ -0,0 +1,45 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+NS_ASSUME_NONNULL_BEGIN
+
+/*!
+ @protocol YKFOTPURIParserProtocol
+ 
+ @abstract
+    Interface for a NFC OTP custom URI parser. If the used YubiKeys use a custom way of formatting the URI received from the YubiKey
+    a custom parser can be provided by the host application by implementing this interface and setting it on YubiKitConfiguration.
+ */
+@protocol YKFOTPURIParserProtocol<NSObject>
+
+/*!
+ @method tokenFromPayload:
+ 
+ @abstract:
+    Implements the extraction of the token from the payload. This method should always return a token, since the token is the minimal
+    information all payloads should have. In case the token is not valid it should return a empty string.
+ */
+- (NSString *)tokenFromPayload:(NSString *)payload;
+
+/*!
+ @method uriFromPayload:
+ 
+ @abstract:
+    Implements the extraction of the URI from the payload. This method can return nil if no URI is available in the payload.
+ */
+- (nullable NSString *)uriFromPayload:(NSString *)payload;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/YubiKit/YubiKit/TestExtentions/EAAccessory+Testing.h b/YubiKit/YubiKit/TestExtentions/EAAccessory+Testing.h
new file mode 100755
index 000000000..10750059f
--- /dev/null
+++ b/YubiKit/YubiKit/TestExtentions/EAAccessory+Testing.h
@@ -0,0 +1,42 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+#import <ExternalAccessory/ExternalAccessory.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+@protocol YKFEAAccessoryProtocol<NSObject>
+
+@property(nonatomic, readonly, getter=isConnected) BOOL connected;
+@property(nonatomic, readonly) NSUInteger connectionID;
+
+@property(nonatomic, readonly) NSString *manufacturer;
+@property(nonatomic, readonly) NSString *name;
+@property(nonatomic, readonly) NSString *modelNumber;
+@property(nonatomic, readonly) NSString *serialNumber;
+@property(nonatomic, readonly) NSString *firmwareRevision;
+@property(nonatomic, readonly) NSString *hardwareRevision;
+@property(nonatomic, readonly) NSString *dockType;
+
+@property(nonatomic, readonly) NSArray<NSString *> *protocolStrings;
+
+@property(nonatomic, assign, nullable) id<EAAccessoryDelegate> delegate;
+
+@end
+
+NS_ASSUME_NONNULL_END
+
+@interface EAAccessory()<YKFEAAccessoryProtocol>
+@end
diff --git a/YubiKit/YubiKit/TestExtentions/EAAccessoryManager+Testing.h b/YubiKit/YubiKit/TestExtentions/EAAccessoryManager+Testing.h
new file mode 100755
index 000000000..f1872fba8
--- /dev/null
+++ b/YubiKit/YubiKit/TestExtentions/EAAccessoryManager+Testing.h
@@ -0,0 +1,37 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+#import <ExternalAccessory/ExternalAccessory.h>
+#import "EAAccessory+Testing.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@protocol YKFEAAccessoryManagerProtocol<NSObject>
+
++ (id<YKFEAAccessoryManagerProtocol>)sharedAccessoryManager;
+
+- (void)showBluetoothAccessoryPickerWithNameFilter:(nullable NSPredicate *)predicate completion:(nullable EABluetoothAccessoryPickerCompletion)completion;
+
+- (void)registerForLocalNotifications;
+- (void)unregisterForLocalNotifications;
+
+@property (nonatomic, readonly) NSArray<id<YKFEAAccessoryProtocol>> *connectedAccessories;
+
+@end
+
+@interface EAAccessoryManager()<YKFEAAccessoryManagerProtocol>
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/YubiKit/YubiKit/TestExtentions/EASession+Testing.h b/YubiKit/YubiKit/TestExtentions/EASession+Testing.h
new file mode 100755
index 000000000..f472497b0
--- /dev/null
+++ b/YubiKit/YubiKit/TestExtentions/EASession+Testing.h
@@ -0,0 +1,32 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+#import <ExternalAccessory/ExternalAccessory.h>
+#import "EAAccessory+Testing.h"
+
+@protocol YKFEASessionProtocol<NSObject>
+
+@property (nonatomic, readonly, nullable) id<YKFEAAccessoryProtocol> accessory;
+@property (nonatomic, readonly, nullable) NSString *protocolString;
+@property (nonatomic, readonly, nullable) NSInputStream *inputStream;
+@property (nonatomic, readonly, nullable) NSOutputStream *outputStream;
+
+@end
+
+/*
+ Allows to define a EASession property as id<YKFEASessionProtocol> to facilitate dependecy injection.
+ */
+@interface EASession()<YKFEASessionProtocol>
+@end
diff --git a/YubiKit/YubiKit/TestExtentions/NFCNDEFReaderSession+Testing.h b/YubiKit/YubiKit/TestExtentions/NFCNDEFReaderSession+Testing.h
new file mode 100755
index 000000000..0c7b00db5
--- /dev/null
+++ b/YubiKit/YubiKit/TestExtentions/NFCNDEFReaderSession+Testing.h
@@ -0,0 +1,38 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <CoreNFC/CoreNFC.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+API_AVAILABLE(ios(11.0))
+@protocol YKFNFCNDEFReaderSessionProtocol
+
+@property (nonatomic) NSString *alertMessage;
+
+- (instancetype)initWithDelegate:(id<NFCNDEFReaderSessionDelegate>)delegate
+                           queue:(nullable dispatch_queue_t)queue
+        invalidateAfterFirstRead:(BOOL)invalidateAfterFirstRead;
+
+- (void)beginSession;
+
+@end
+
+/*
+ Allows to define a NFCNDEFReaderSession property as id<YKFNFCNDEFReaderSessionProtocol> to facilitate dependecy injection.
+ */
+@interface NFCNDEFReaderSession()<YKFNFCNDEFReaderSessionProtocol>
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/YubiKit/YubiKit/TestExtentions/UIDevice+Testing.h b/YubiKit/YubiKit/TestExtentions/UIDevice+Testing.h
new file mode 100755
index 000000000..4407fde99
--- /dev/null
+++ b/YubiKit/YubiKit/TestExtentions/UIDevice+Testing.h
@@ -0,0 +1,26 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <UIKit/UIKit.h>
+#import "UIDeviceAdditions.h"
+
+@protocol YKFUIDeviceProtocol <NSObject>
+
+@property (nonatomic) NSString  *systemVersion;
+@property (nonatomic) YKFDeviceModel ykf_deviceModel;
+
+@end
+
+@interface UIDevice()<YKFUIDeviceProtocol>
+@end
diff --git a/YubiKit/YubiKit/ThirdParties/MF_Base32Additions.h b/YubiKit/YubiKit/ThirdParties/MF_Base32Additions.h
new file mode 100755
index 000000000..333ecca13
--- /dev/null
+++ b/YubiKit/YubiKit/ThirdParties/MF_Base32Additions.h
@@ -0,0 +1,29 @@
+//
+//  MF_Base32Additions.h
+//  Base32 -- RFC 4648 compatible implementation
+//  see http://www.ietf.org/rfc/rfc4648.txt for more details
+//
+//  Designed to be compiled with Automatic Reference Counting
+//
+//  Created by Dave Poirier on 12-06-14.
+//  Public Domain
+//
+
+#import <Foundation/Foundation.h>
+
+#define NSBase32StringEncoding  0x4D467E32
+
+@interface NSString (Base32Addition)
++(NSString *)stringFromBase32String:(NSString *)base32String;
+-(NSString *)base32String;
+@end
+
+@interface NSData (Base32Addition)
++(NSData *)dataWithBase32String:(NSString *)base32String;
+-(NSString *)base32String;
+@end
+
+@interface MF_Base32Codec : NSObject
++(NSData *)dataFromBase32String:(NSString *)base32String;
++(NSString *)base32StringFromData:(NSData *)data;
+@end
diff --git a/YubiKit/YubiKit/ThirdParties/MF_Base32Additions.m b/YubiKit/YubiKit/ThirdParties/MF_Base32Additions.m
new file mode 100755
index 000000000..7d3ae35a0
--- /dev/null
+++ b/YubiKit/YubiKit/ThirdParties/MF_Base32Additions.m
@@ -0,0 +1,248 @@
+//
+//  MF_Base32Additions.m
+//  Base32 -- RFC 4648 compatible implementation
+//  see http://www.ietf.org/rfc/rfc4648.txt for more details
+//
+//  Designed to be compiled with Automatic Reference Counting
+//
+//  Created by Dave Poirier on 12-06-14.
+//  Public Domain
+//
+
+#import "MF_Base32Additions.h"
+
+@implementation MF_Base32Codec
++(NSData *)dataFromBase32String:(NSString *)encoding
+{
+    NSData *data = nil;
+    unsigned char *decodedBytes = NULL;
+    @try {
+#define __ 255
+        static char decodingTable[256] = {
+            __,__,__,__, __,__,__,__, __,__,__,__, __,__,__,__,  // 0x00 - 0x0F
+            __,__,__,__, __,__,__,__, __,__,__,__, __,__,__,__,  // 0x10 - 0x1F
+            __,__,__,__, __,__,__,__, __,__,__,__, __,__,__,__,  // 0x20 - 0x2F
+            __,__,26,27, 28,29,30,31, __,__,__,__, __, 0,__,__,  // 0x30 - 0x3F
+            __, 0, 1, 2,  3, 4, 5, 6,  7, 8, 9,10, 11,12,13,14,  // 0x40 - 0x4F
+            15,16,17,18, 19,20,21,22, 23,24,25,__, __,__,__,__,  // 0x50 - 0x5F
+            __, 0, 1, 2,  3, 4, 5, 6,  7, 8, 9,10, 11,12,13,14,  // 0x60 - 0x6F
+            15,16,17,18, 19,20,21,22, 23,24,25,__, __,__,__,__,  // 0x70 - 0x7F
+            __,__,__,__, __,__,__,__, __,__,__,__, __,__,__,__,  // 0x80 - 0x8F
+            __,__,__,__, __,__,__,__, __,__,__,__, __,__,__,__,  // 0x90 - 0x9F
+            __,__,__,__, __,__,__,__, __,__,__,__, __,__,__,__,  // 0xA0 - 0xAF
+            __,__,__,__, __,__,__,__, __,__,__,__, __,__,__,__,  // 0xB0 - 0xBF
+            __,__,__,__, __,__,__,__, __,__,__,__, __,__,__,__,  // 0xC0 - 0xCF
+            __,__,__,__, __,__,__,__, __,__,__,__, __,__,__,__,  // 0xD0 - 0xDF
+            __,__,__,__, __,__,__,__, __,__,__,__, __,__,__,__,  // 0xE0 - 0xEF
+            __,__,__,__, __,__,__,__, __,__,__,__, __,__,__,__,  // 0xF0 - 0xFF
+        };
+        static NSUInteger paddingAdjustment[8] = {0,1,1,1,2,3,3,4};
+        encoding = [encoding stringByReplacingOccurrencesOfString:@"=" withString:@""];
+        NSData *encodedData = [encoding dataUsingEncoding:NSASCIIStringEncoding];
+        unsigned char *encodedBytes = (unsigned char *)[encodedData bytes];
+
+        NSUInteger encodedLength = [encodedData length];
+        if( encodedLength >= (NSUIntegerMax - 7) ) return nil; // NSUInteger overflow check
+        NSUInteger encodedBlocks = (encodedLength + 7) >> 3;
+        NSUInteger expectedDataLength = encodedBlocks * 5;
+
+        decodedBytes = calloc(expectedDataLength, 1);
+        if( decodedBytes != NULL ) {
+
+            unsigned char encodedByte1, encodedByte2, encodedByte3, encodedByte4;
+            unsigned char encodedByte5, encodedByte6, encodedByte7, encodedByte8;
+            NSUInteger encodedBytesToProcess = encodedLength;
+            NSUInteger encodedBaseIndex = 0;
+            NSUInteger decodedBaseIndex = 0;
+            unsigned char encodedBlock[8] = {0,0,0,0,0,0,0,0};
+            NSUInteger encodedBlockIndex = 0;
+            unsigned char c;
+            while( encodedBytesToProcess-- >= 1 ) {
+                c = encodedBytes[encodedBaseIndex++];
+                if( c == '=' ) break; // padding...
+
+                c = decodingTable[c];
+                if( c == __ ) continue;
+
+                encodedBlock[encodedBlockIndex++] = c;
+                if( encodedBlockIndex == 8 ) {
+                    encodedByte1 = encodedBlock[0];
+                    encodedByte2 = encodedBlock[1];
+                    encodedByte3 = encodedBlock[2];
+                    encodedByte4 = encodedBlock[3];
+                    encodedByte5 = encodedBlock[4];
+                    encodedByte6 = encodedBlock[5];
+                    encodedByte7 = encodedBlock[6];
+                    encodedByte8 = encodedBlock[7];
+                    decodedBytes[decodedBaseIndex] = ((encodedByte1 << 3) & 0xF8) | ((encodedByte2 >> 2) & 0x07);
+                    decodedBytes[decodedBaseIndex+1] = ((encodedByte2 << 6) & 0xC0) | ((encodedByte3 << 1) & 0x3E) | ((encodedByte4 >> 4) & 0x01);
+                    decodedBytes[decodedBaseIndex+2] = ((encodedByte4 << 4) & 0xF0) | ((encodedByte5 >> 1) & 0x0F);
+                    decodedBytes[decodedBaseIndex+3] = ((encodedByte5 << 7) & 0x80) | ((encodedByte6 << 2) & 0x7C) | ((encodedByte7 >> 3) & 0x03);
+                    decodedBytes[decodedBaseIndex+4] = ((encodedByte7 << 5) & 0xE0) | (encodedByte8 & 0x1F);
+                    decodedBaseIndex += 5;
+                    encodedBlockIndex = 0;
+                }
+            }
+            encodedByte7 = 0;
+            encodedByte6 = 0;
+            encodedByte5 = 0;
+            encodedByte4 = 0;
+            encodedByte3 = 0;
+            encodedByte2 = 0;
+            switch (encodedBlockIndex) {
+                case 7:
+                    encodedByte7 = encodedBlock[6];
+                case 6:
+                    encodedByte6 = encodedBlock[5];
+                case 5:
+                    encodedByte5 = encodedBlock[4];
+                case 4:
+                    encodedByte4 = encodedBlock[3];
+                case 3:
+                    encodedByte3 = encodedBlock[2];
+                case 2:
+                    encodedByte2 = encodedBlock[1];
+                case 1:
+                    encodedByte1 = encodedBlock[0];
+                    decodedBytes[decodedBaseIndex] = ((encodedByte1 << 3) & 0xF8) | ((encodedByte2 >> 2) & 0x07);
+                    decodedBytes[decodedBaseIndex+1] = ((encodedByte2 << 6) & 0xC0) | ((encodedByte3 << 1) & 0x3E) | ((encodedByte4 >> 4) & 0x01);
+                    decodedBytes[decodedBaseIndex+2] = ((encodedByte4 << 4) & 0xF0) | ((encodedByte5 >> 1) & 0x0F);
+                    decodedBytes[decodedBaseIndex+3] = ((encodedByte5 << 7) & 0x80) | ((encodedByte6 << 2) & 0x7C) | ((encodedByte7 >> 3) & 0x03);
+                    decodedBytes[decodedBaseIndex+4] = ((encodedByte7 << 5) & 0xE0);
+            }
+            decodedBaseIndex += paddingAdjustment[encodedBlockIndex];
+            data = [[NSData alloc] initWithBytes:decodedBytes length:decodedBaseIndex];            
+        }
+    }
+    @catch (NSException *exception) {
+        data = nil;
+        NSLog(@"WARNING: error occured while decoding base 32 string: %@", exception);
+    }
+    @finally {
+        if( decodedBytes != NULL ) {
+            free( decodedBytes );
+        }
+    }
+    return data;
+}
++(NSString *)base32StringFromData:(NSData *)data
+{
+    NSString *encoding = nil;
+    unsigned char *encodingBytes = NULL;
+    @try {
+        static char encodingTable[32] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
+        static NSUInteger paddingTable[] = {0,6,4,3,1};
+        
+        //                     Table 3: The Base 32 Alphabet
+        //
+        // Value Encoding  Value Encoding  Value Encoding  Value Encoding
+        //     0 A             9 J            18 S            27 3
+        //     1 B            10 K            19 T            28 4
+        //     2 C            11 L            20 U            29 5
+        //     3 D            12 M            21 V            30 6
+        //     4 E            13 N            22 W            31 7
+        //     5 F            14 O            23 X
+        //     6 G            15 P            24 Y         (pad) =
+        //     7 H            16 Q            25 Z
+        //     8 I            17 R            26 2
+
+        NSUInteger dataLength = [data length];
+        NSUInteger encodedBlocks = dataLength / 5;
+        if( (encodedBlocks + 1) >= (NSUIntegerMax / 8) ) return nil; // NSUInteger overflow check
+        NSUInteger padding = paddingTable[dataLength % 5];
+        if( padding > 0 ) encodedBlocks++;
+        NSUInteger encodedLength = encodedBlocks * 8;
+
+        encodingBytes = calloc(encodedLength, 1);
+        if( encodingBytes != NULL ) {
+            NSUInteger rawBytesToProcess = dataLength;
+            NSUInteger rawBaseIndex = 0;
+            NSUInteger encodingBaseIndex = 0;
+            unsigned char *rawBytes = (unsigned char *)[data bytes];
+            unsigned char rawByte1, rawByte2, rawByte3, rawByte4, rawByte5;
+            while( rawBytesToProcess >= 5 ) {
+                rawByte1 = rawBytes[rawBaseIndex];
+                rawByte2 = rawBytes[rawBaseIndex+1];
+                rawByte3 = rawBytes[rawBaseIndex+2];
+                rawByte4 = rawBytes[rawBaseIndex+3];
+                rawByte5 = rawBytes[rawBaseIndex+4];
+                encodingBytes[encodingBaseIndex] = encodingTable[((rawByte1 >> 3) & 0x1F)];
+                encodingBytes[encodingBaseIndex+1] = encodingTable[((rawByte1 << 2) & 0x1C) | ((rawByte2 >> 6) & 0x03) ];
+                encodingBytes[encodingBaseIndex+2] = encodingTable[((rawByte2 >> 1) & 0x1F)];
+                encodingBytes[encodingBaseIndex+3] = encodingTable[((rawByte2 << 4) & 0x10) | ((rawByte3 >> 4) & 0x0F)];
+                encodingBytes[encodingBaseIndex+4] = encodingTable[((rawByte3 << 1) & 0x1E) | ((rawByte4 >> 7) & 0x01)];
+                encodingBytes[encodingBaseIndex+5] = encodingTable[((rawByte4 >> 2) & 0x1F)];
+                encodingBytes[encodingBaseIndex+6] = encodingTable[((rawByte4 << 3) & 0x18) | ((rawByte5 >> 5) & 0x07)];
+                encodingBytes[encodingBaseIndex+7] = encodingTable[rawByte5 & 0x1F];
+
+                rawBaseIndex += 5;
+                encodingBaseIndex += 8;
+                rawBytesToProcess -= 5;
+            }
+            rawByte4 = 0;
+            rawByte3 = 0;
+            rawByte2 = 0;
+            switch (dataLength-rawBaseIndex) {
+                case 4:
+                    rawByte4 = rawBytes[rawBaseIndex+3];
+                case 3:
+                    rawByte3 = rawBytes[rawBaseIndex+2];
+                case 2:
+                    rawByte2 = rawBytes[rawBaseIndex+1];
+                case 1:
+                    rawByte1 = rawBytes[rawBaseIndex];
+                    encodingBytes[encodingBaseIndex] = encodingTable[((rawByte1 >> 3) & 0x1F)];
+                    encodingBytes[encodingBaseIndex+1] = encodingTable[((rawByte1 << 2) & 0x1C) | ((rawByte2 >> 6) & 0x03) ];
+                    encodingBytes[encodingBaseIndex+2] = encodingTable[((rawByte2 >> 1) & 0x1F)];
+                    encodingBytes[encodingBaseIndex+3] = encodingTable[((rawByte2 << 4) & 0x10) | ((rawByte3 >> 4) & 0x0F)];
+                    encodingBytes[encodingBaseIndex+4] = encodingTable[((rawByte3 << 1) & 0x1E) | ((rawByte4 >> 7) & 0x01)];
+                    encodingBytes[encodingBaseIndex+5] = encodingTable[((rawByte4 >> 2) & 0x1F)];
+                    encodingBytes[encodingBaseIndex+6] = encodingTable[((rawByte4 << 3) & 0x18)]; 
+                    // we can skip rawByte5 since we have a partial block it would always be 0
+                    break;
+            }
+            // compute location from where to begin inserting padding, it may overwrite some bytes from the partial block encoding
+            // if their value was 0 (cases 1-3).
+            encodingBaseIndex = encodedLength - padding;
+            while( padding-- > 0 ) {
+                encodingBytes[encodingBaseIndex++] = '=';
+            }
+            encoding = [[NSString alloc] initWithBytes:encodingBytes length:encodedLength encoding:NSASCIIStringEncoding];
+        }
+    }
+    @catch (NSException *exception) {
+        encoding = nil;
+        NSLog(@"WARNING: error occured while tring to encode base 32 data: %@", exception);
+    }
+    @finally {
+        if( encodingBytes != NULL ) {
+            free( encodingBytes );
+        }
+    }
+    return encoding;
+}
+@end
+
+@implementation NSString (Base32Addition)
+-(NSString *)base32String
+{
+    NSData *utf8encoding = [self dataUsingEncoding:NSUTF8StringEncoding];
+    return [MF_Base32Codec base32StringFromData:utf8encoding];
+}
++(NSString *)stringFromBase32String:(NSString *)base32String
+{
+    NSData *utf8encoding = [MF_Base32Codec dataFromBase32String:base32String];
+    return [[NSString alloc] initWithData:utf8encoding encoding:NSUTF8StringEncoding];
+}
+@end
+
+@implementation NSData (Base32Addition)
++(NSData *)dataWithBase32String:(NSString *)base32String
+{
+    return [MF_Base32Codec dataFromBase32String:base32String];
+}
+-(NSString *)base32String
+{
+    return [MF_Base32Codec base32StringFromData:self];
+}
+@end
diff --git a/YubiKit/YubiKit/YubiKit.h b/YubiKit/YubiKit/YubiKit.h
new file mode 100755
index 000000000..79d00a3dd
--- /dev/null
+++ b/YubiKit/YubiKit/YubiKit.h
@@ -0,0 +1,56 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "YubiKitManager.h"
+#import "YubiKitLogger.h"
+#import "YubiKitConfiguration.h"
+#import "YubiKitExternalLocalization.h"
+#import "YubiKitDeviceCapabilities.h"
+
+#import "YKFOTPTextParserProtocol.h"
+#import "YKFOTPURIParserProtocol.h"
+#import "YKFOTPToken.h"
+
+#import "YKFQRReaderSession.h"
+#import "YKFQRCodeScanError.h"
+#import "YKFNFCSession.h"
+#import "YKFNFCOTPService.h"
+#import "YKFNFCError.h"
+#import "YKFNFCTagDescription.h"
+
+#import "YKFAccessorySession.h"
+#import "YKFAccessoryDescription.h"
+
+#import "YKFKeySessionError.h"
+#import "YKFKeyFIDO2Error.h"
+#import "YKFKeyU2FError.h"
+#import "YKFKeyOATHError.h"
+#import "YKFKeyAPDUError.h"
+
+#import "YKFKeyU2FService.h"
+#import "YKFKeyFIDO2Service.h"
+#import "YKFKeyOATHService.h"
+#import "YKFKeyRawCommandService.h"
+
+#import "YKFKeyFIDO2Request.h"
+#import "YKFKeyFIDO2MakeCredentialRequest.h"
+#import "YKFKeyFIDO2GetAssertionRequest.h"
+#import "YKFKeyFIDO2VerifyPinRequest.h"
+#import "YKFKeyFIDO2SetPinRequest.h"
+#import "YKFKeyFIDO2ChangePinRequest.h"
+
+#import "YKFPCSC.h"
+
+#import "YKFNSDataAdditions.h"
+#import "YKFWebAuthnClientData.h"
diff --git a/YubiKit/YubiKit/YubiKitConfiguration.h b/YubiKit/YubiKit/YubiKitConfiguration.h
new file mode 100755
index 000000000..34c3efd00
--- /dev/null
+++ b/YubiKit/YubiKit/YubiKitConfiguration.h
@@ -0,0 +1,62 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+
+#import "YKFOTPURIParserProtocol.h"
+#import "YKFOTPTextParserProtocol.h"
+
+
+/*!
+ @class YubiKitConfiguration
+ 
+ @abstract
+    YubiKitConfiguration allows the host application to configure YubiKit when the default behaviour of
+    the library is insufficient for the host application.
+ 
+ NOTE:
+    To configure YubiKit using YubiKitConfiguration, access the shared instance from YubiKitManager.
+ */
+@interface YubiKitConfiguration : NSObject
+
+/*!
+ @property customOTPURIParser
+ 
+ @abstract
+    Custom parser privided by the host application when the used YubiKeys have a custom
+    way of formatting the URI.
+ 
+ NOTE:
+    This parser instance is used when the NDEF is configured as URI by the configuration tool. The default
+    configuration for the YubiKey is URI. If no changes have been made to the NDEF payload format by configuring
+    the YubiKey, this should be left empty and rely on the default parser provided by YubiKit.
+ */
+@property (class, nonatomic, nullable) id<YKFOTPURIParserProtocol> customOTPURIParser;
+
+/*!
+ @property customOTPTextParser
+ 
+ @abstract
+    Custom parser privided by the host applicatiopn when the used YubiKeys have a custom way of formatting the Text.
+ 
+ NOTE:
+    This parser instance is used when the NDEF is configured as Text by the configuration tool. This is not
+    the default configuration for the YubiKey. If no changes have been made to the NDEF payload format by configuring
+    the YubiKey, this should be left empty and rely on the default parser provided by YubiKit.
+ */
+@property (class, nonatomic, nullable) id<YKFOTPTextParserProtocol> customOTPTextParser;
+
+- (nonnull instancetype)init NS_UNAVAILABLE;
+
+@end
diff --git a/YubiKit/YubiKit/YubiKitConfiguration.m b/YubiKit/YubiKit/YubiKitConfiguration.m
new file mode 100755
index 000000000..465496b25
--- /dev/null
+++ b/YubiKit/YubiKit/YubiKitConfiguration.m
@@ -0,0 +1,37 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "YubiKitConfiguration.h"
+
+@implementation YubiKitConfiguration
+
+static id<YKFOTPURIParserProtocol> internalCustomOTPURIParser = nil;
+
++ (id<YKFOTPURIParserProtocol>)customOTPURIParser {
+    return internalCustomOTPURIParser;
+}
++ (void)setCustomOTPURIParser:(id<YKFOTPURIParserProtocol>)parser {
+    internalCustomOTPURIParser = parser;
+}
+
+static id<YKFOTPTextParserProtocol> internalCustomOTPTextParser = nil;
+
++ (id<YKFOTPTextParserProtocol>)customOTPTextParser {
+    return internalCustomOTPTextParser;
+}
++ (void)setCustomOTPTextParser:(id<YKFOTPTextParserProtocol>)parser {
+    internalCustomOTPTextParser = parser;
+}
+
+@end
diff --git a/YubiKit/YubiKit/YubiKitDeviceCapabilities+Testing.h b/YubiKit/YubiKit/YubiKitDeviceCapabilities+Testing.h
new file mode 100755
index 000000000..c9ba99287
--- /dev/null
+++ b/YubiKit/YubiKit/YubiKitDeviceCapabilities+Testing.h
@@ -0,0 +1,30 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "YubiKitDeviceCapabilities.h"
+#import "UIDevice+Testing.h"
+
+#ifdef DEBUG
+
+@interface YubiKitDeviceCapabilities()
+
+// Injected singleton by UTs.
+@property (class, nonatomic) id<YubiKitDeviceCapabilitiesProtocol> fakeDeviceCapabilities;
+
+// Injected UIDevice by UTs.
+@property (class, nonatomic) id<YKFUIDeviceProtocol> fakeUIDevice;
+
+@end
+
+#endif
diff --git a/YubiKit/YubiKit/YubiKitDeviceCapabilities.h b/YubiKit/YubiKit/YubiKitDeviceCapabilities.h
new file mode 100755
index 000000000..8fb5e1801
--- /dev/null
+++ b/YubiKit/YubiKit/YubiKitDeviceCapabilities.h
@@ -0,0 +1,76 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+/*!
+ @protocol YubiKitDeviceCapabilitiesProtocol
+ 
+ @abstract
+    Interface for device capabilities required by YubiKit to run.
+ 
+ NOTE:
+    It is important for the host application to check these capabilities before setting up any UI or actions
+    which involve NFC and/or QR Code scanning.
+ */
+@protocol YubiKitDeviceCapabilitiesProtocol<NSObject>
+
+/*!
+ @property supportsQRCodeScanning
+ 
+ @abstract
+    Returns YES if the device can use a camera to scan a QR Code. Generally speaking this should not return
+    NO on any real device because all modern iOS devices have cameras and run recent versions of the OS.
+ */ 
+@property (class, nonatomic, assign, readonly) BOOL supportsQRCodeScanning;
+
+/*!
+ @property supportsNFCScanning
+ 
+ @abstract
+    Returns YES if the device can access the NFC device to scan NDEF tags.
+ */
+@property (class, nonatomic, assign, readonly) BOOL supportsNFCScanning;
+
+/*!
+ @property supportsISO7816NFCTags
+ 
+ @abstract
+    Returns YES if the device can communicate with ISO 7816 NFC compatible tags.
+ */
+@property (class, nonatomic, assign, readonly) BOOL supportsISO7816NFCTags;
+
+
+/*!
+ @property supportsMFIAccessoryKey
+ 
+ @abstract
+ Returns YES if the device and the OS version can interact with the MFI accessory YubiKeys.
+ */
+@property (class, nonatomic, assign, readonly) BOOL supportsMFIAccessoryKey;
+
+@end
+
+/*!
+ @class YubiKitDeviceCapabilities
+ 
+ @abstract
+    Default implementation for YKFDeviceCapabilitiesProtocol.
+*/
+@interface YubiKitDeviceCapabilities : NSObject<YubiKitDeviceCapabilitiesProtocol>
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/YubiKit/YubiKit/YubiKitDeviceCapabilities.m b/YubiKit/YubiKit/YubiKitDeviceCapabilities.m
new file mode 100755
index 000000000..f882e26e0
--- /dev/null
+++ b/YubiKit/YubiKit/YubiKitDeviceCapabilities.m
@@ -0,0 +1,197 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <UIKit/UIKit.h>
+#import <CoreNFC/CoreNFC.h>
+
+#import "UIDeviceAdditions.h"
+#import "UIDevice+Testing.h"
+
+#import "YubiKitDeviceCapabilities.h"
+#import "YubiKitDeviceCapabilities+Testing.h"
+
+@interface YubiKitDeviceCapabilities()
+
+@property (class, nonatomic, readonly) id<YKFUIDeviceProtocol> currentUIDevice;
+
+@end
+
+@implementation YubiKitDeviceCapabilities
+
++ (BOOL)supportsQRCodeScanning {
+#ifdef DEBUG
+    // When this is set by UTs.
+    if (self.fakeDeviceCapabilities) {
+        return [[self.fakeDeviceCapabilities class] supportsQRCodeScanning];
+    }
+#endif
+    
+    if (self.currentUIDevice.ykf_deviceModel == YKFDeviceModelSimulator) {
+        return NO;
+    }
+    
+    if (@available(iOS 8, *)) {
+        return YES;
+    }
+    return NO;
+}
+
++ (BOOL)supportsNFCScanning {
+#ifdef DEBUG
+    // When this is set by UTs.
+    if (self.fakeDeviceCapabilities) {
+        return [[self.fakeDeviceCapabilities class] supportsNFCScanning];
+    }
+#endif
+    
+    if (self.currentUIDevice.ykf_deviceModel == YKFDeviceModelSimulator) {
+        return NO;
+    }
+    if (@available(iOS 11, *)) {
+        // This check was introduced to avoid some random crashers caused by CoreNFC on devices which are not NFC enabled.
+        if ([self deviceIsNFCEnabled]) {
+            return NFCNDEFReaderSession.readingAvailable;
+        }
+        return NO;
+    }
+    return NO;
+}
+
++ (BOOL)supportsISO7816NFCTags {
+#ifdef DEBUG
+    // When this is set by UTs.
+    if (self.fakeDeviceCapabilities) {
+        return [[self.fakeDeviceCapabilities class] supportsISO7816NFCTags];
+    }
+#endif
+    
+    if (self.currentUIDevice.ykf_deviceModel == YKFDeviceModelSimulator) {
+        return NO;
+    }
+    if (@available(iOS 13, *)) {
+        // This check was introduced to avoid some random crashers caused by CoreNFC on devices which are not NFC enabled.
+        if ([self deviceIsNFCEnabled]) {
+            return NFCTagReaderSession.readingAvailable;
+        }
+        return NO;
+    }
+    return NO;
+}
+
++ (BOOL)supportsMFIAccessoryKey {
+#ifdef DEBUG
+    // When this is set by UTs.
+    if (self.fakeDeviceCapabilities) {
+        return [[self.fakeDeviceCapabilities class] supportsMFIAccessoryKey];
+    }
+#endif
+
+    // Simulator and USB-C type devices
+    if (self.currentUIDevice.ykf_deviceModel == YKFDeviceModelSimulator ||
+        self.currentUIDevice.ykf_deviceModel == YKFDeviceModelIPadPro3) {
+        return NO;
+    }
+    if (@available(iOS 10, *)) {
+        return [self systemSupportsMFIAccessoryKey];
+    }
+    return NO;
+}
+
+#pragma mark - Helpers
+
++ (id<YKFUIDeviceProtocol>)currentUIDevice {
+#ifdef DEBUG
+    return testFakeUIDevice ? testFakeUIDevice : UIDevice.currentDevice;
+#else
+    return UIDevice.currentDevice;
+#endif
+}
+
++ (BOOL)deviceIsNFCEnabled {
+    static BOOL ykfDeviceCapabilitiesDeviceIsNFCEnabled = YES;
+    static dispatch_once_t onceToken;
+    
+    dispatch_once(&onceToken, ^{
+        YKFDeviceModel deviceModel = self.currentUIDevice.ykf_deviceModel;
+        ykfDeviceCapabilitiesDeviceIsNFCEnabled =
+            deviceModel == YKFDeviceModelIPhone7 || deviceModel == YKFDeviceModelIPhone7Plus ||
+            deviceModel == YKFDeviceModelIPhone8 || deviceModel == YKFDeviceModelIPhone8Plus ||
+            deviceModel == YKFDeviceModelIPhoneX ||
+            deviceModel == YKFDeviceModelIPhoneXS || deviceModel == YKFDeviceModelIPhoneXSMax || deviceModel == YKFDeviceModelIPhoneXR ||
+            deviceModel == YKFDeviceModelUnknown; // A newer device which is not in the list yet
+    });
+    
+#ifdef DEBUG
+    if (testFakeUIDevice) {
+        // When the UTs run, reset to test different configurations.
+        onceToken = 0;
+    }
+#endif
+    
+    return ykfDeviceCapabilitiesDeviceIsNFCEnabled;
+}
+
++ (BOOL)systemSupportsMFIAccessoryKey {
+    static BOOL ykfDeviceCapabilitiesSystemSupportsMFIAccessoryKey = YES;
+    static dispatch_once_t onceToken;
+    
+    dispatch_once(&onceToken, ^{
+        // iOS 11.2 Versions
+        NSArray *excludedVersions = @[@"11.2", @"11.2.1", @"11.2.2", @"11.2.5"];
+        
+        NSString *systemVersion = self.currentUIDevice.systemVersion;
+        if ([excludedVersions containsObject:systemVersion]) {
+            ykfDeviceCapabilitiesSystemSupportsMFIAccessoryKey = NO;
+        } else {
+            ykfDeviceCapabilitiesSystemSupportsMFIAccessoryKey = YES;
+        }
+    });
+    
+#ifdef DEBUG
+    if (testFakeUIDevice) {
+        // When the UTs run, reset to test different configurations.
+        onceToken = 0;
+    }
+#endif
+    
+    return ykfDeviceCapabilitiesSystemSupportsMFIAccessoryKey;
+}
+
+#pragma mark - Testing additions
+
+#ifdef DEBUG
+
+static id<YKFUIDeviceProtocol> testFakeUIDevice;
+
++ (void)setFakeUIDevice:(id<YKFUIDeviceProtocol>)fakeUIDevice {
+    testFakeUIDevice = fakeUIDevice;
+}
+
++ (id<YKFUIDeviceProtocol>)fakeUIDevice {
+    return testFakeUIDevice;
+}
+
+static id<YubiKitDeviceCapabilitiesProtocol> testFakeDeviceCapabilities;
+
++ (void)setFakeDeviceCapabilities:(id<YubiKitDeviceCapabilitiesProtocol>)fakeDeviceCapabilities {
+    testFakeDeviceCapabilities = fakeDeviceCapabilities;
+}
+
++ (id<YubiKitDeviceCapabilitiesProtocol>)fakeDeviceCapabilities {
+    return testFakeDeviceCapabilities;
+}
+
+#endif
+
+@end
diff --git a/YubiKit/YubiKit/YubiKitExternalLocalization.h b/YubiKit/YubiKit/YubiKitExternalLocalization.h
new file mode 100755
index 000000000..080a32735
--- /dev/null
+++ b/YubiKit/YubiKit/YubiKitExternalLocalization.h
@@ -0,0 +1,75 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+
+/*!
+ @class YubiKitExternalLocalization
+ 
+ @abstract
+    YubiKitExternalLocalization allows the host application to provide localized strings for messages
+    shown to the user when YubiKit shows system UIs or build-in UI elements.
+ 
+ NOTE:
+    YubiKit already provides some generic non-localized values for the elements shown in the UI. While these are good
+    for a prototype phase, for production builds, it is recommended for the host application to provide localized
+    values specific for the type of application using the library.
+*/
+@interface YubiKitExternalLocalization : NSObject
+
+#pragma mark - NFC
+
+/*!
+ @property nfcScanAlertMessage
+ 
+ @abstract
+    The message shown in the system NFC scan UI. You can customize this by setting a localized value on this property.
+    Defaults to a non-localized english string.
+ */
+@property (class, nonatomic, nonnull) NSString *nfcScanAlertMessage;
+
+#pragma mark - QR code scan
+
+/*!
+ @property qrCodeScanHintMessage
+ 
+ @abstract
+    The message shown in the UI of the QR code scanner to guide the user to scan the QR Code. You can customize this by
+    setting a localized value on this property.
+    Defaults to a non-localized english string.
+ */
+@property (class, nonatomic, nonnull) NSString *qrCodeScanHintMessage;
+
+/*!
+ @property qrCodeScanCameraNotAvailableMessage
+ 
+ @abstract
+    The message shown in the UI of the QR code scanner when the camera permission was not granted. You can customize this by
+    setting a localized value on this property.
+    Defaults to a non-localized english string.
+ */
+@property (class, nonatomic, nonnull) NSString *qrCodeScanCameraNotAvailableMessage;
+
+/*!
+ @property qrCodeScanDismissButtonTitle
+ 
+ @abstract
+    The title of the Dismiss button in the QR code scan UI. You can customize this by setting a localized value on this property.
+    Defaults to a non-localized english string.
+ */
+@property (class, nonatomic, nonnull) NSString *qrCodeScanDismissButtonTitle;
+
+- (nonnull instancetype)init NS_UNAVAILABLE;
+
+@end
diff --git a/YubiKit/YubiKit/YubiKitExternalLocalization.m b/YubiKit/YubiKit/YubiKitExternalLocalization.m
new file mode 100755
index 000000000..0824015c3
--- /dev/null
+++ b/YubiKit/YubiKit/YubiKitExternalLocalization.m
@@ -0,0 +1,63 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "YubiKitExternalLocalization.h"
+
+@implementation YubiKitExternalLocalization
+
+static NSString *internalNfcScanAlertMessage = @"Scan your YubiKey";
+
++ (NSString *)nfcScanAlertMessage {
+    return internalNfcScanAlertMessage;
+}
+
++ (void)setNfcScanAlertMessage:(NSString *)nfcScanAlertMessage {
+    NSParameterAssert(nfcScanAlertMessage.length);
+    internalNfcScanAlertMessage = nfcScanAlertMessage;
+}
+
+static NSString *internalQrCodeScanHintMessage = @"Point the camera at the QR Code";
+
++ (NSString *)qrCodeScanHintMessage {
+    return internalQrCodeScanHintMessage;
+}
+
++ (void)setQrCodeScanHintMessage:(NSString *)qrCodeScanHintMessage {
+    NSParameterAssert(qrCodeScanHintMessage.length);
+    internalQrCodeScanHintMessage = qrCodeScanHintMessage;
+}
+
+static NSString *internalQrCodeScanCameraNotAvailableMessage = @"Camera permission is not granted. Please enable it in settings and try again.";
+
++ (NSString *)qrCodeScanCameraNotAvailableMessage {
+    return internalQrCodeScanCameraNotAvailableMessage;
+}
+
++ (void)setQrCodeScanCameraNotAvailableMessage:(NSString *)qrCodeScanCameraNotAvailableMessage {
+    NSParameterAssert(qrCodeScanCameraNotAvailableMessage.length);
+    internalQrCodeScanCameraNotAvailableMessage = qrCodeScanCameraNotAvailableMessage;
+}
+
+static NSString *internalQrCodeScanDismissButtonTitle = @"Dismiss";
+
++ (NSString *)qrCodeScanDismissButtonTitle {
+    return internalQrCodeScanDismissButtonTitle;
+}
+
++ (void)setQrCodeScanDismissButtonTitle:(NSString *)qrCodeScanDismissButtonTitle {
+    NSParameterAssert(qrCodeScanDismissButtonTitle.length);
+    internalQrCodeScanDismissButtonTitle = qrCodeScanDismissButtonTitle;
+}
+
+@end
diff --git a/YubiKit/YubiKit/YubiKitLogger.h b/YubiKit/YubiKit/YubiKitLogger.h
new file mode 100755
index 000000000..4100bb963
--- /dev/null
+++ b/YubiKit/YubiKit/YubiKitLogger.h
@@ -0,0 +1,46 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+
+NS_ASSUME_NONNULL_BEGIN
+/*!
+ @protocol YubiKitLoggerProtocol
+ 
+ @abstract
+    Provides the interface for custom logger.
+ */
+@protocol YubiKitLoggerProtocol <NSObject>
+
+- (void)log:(NSString*) message;
+
+@end
+
+/*!
+ @class YubiKitLogger
+ 
+ @abstract
+     YubiKitLogger allows the host application to configure a custom logger when the default logger of
+     the library is insufficient for the host application.
+ 
+ @note:
+    To configure YubiKitLogger set the customLogger property.
+ */
+@interface YubiKitLogger : NSObject
+
+@property (class, nonatomic, nullable) id<YubiKitLoggerProtocol> customLogger;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/YubiKit/YubiKit/YubiKitLogger.m b/YubiKit/YubiKit/YubiKitLogger.m
new file mode 100755
index 000000000..036df1226
--- /dev/null
+++ b/YubiKit/YubiKit/YubiKitLogger.m
@@ -0,0 +1,28 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "YubiKitLogger.h"
+
+@implementation YubiKitLogger
+
+static id<YubiKitLoggerProtocol> internalCustomLogger = nil;
+
++ (id<YubiKitLoggerProtocol>)customLogger {
+    return internalCustomLogger;
+}
++ (void)setCustomLogger:(id<YubiKitLoggerProtocol>)logger {
+    internalCustomLogger = logger;
+}
+
+@end
diff --git a/YubiKit/YubiKit/YubiKitManager.h b/YubiKit/YubiKit/YubiKitManager.h
new file mode 100755
index 000000000..0c3af3181
--- /dev/null
+++ b/YubiKit/YubiKit/YubiKitManager.h
@@ -0,0 +1,78 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+#import <UIKit/UIKit.h>
+
+#import "YKFNFCSession.h"
+#import "YKFQRReaderSession.h"
+#import "YKFAccessorySession.h"
+
+/*!
+ @protocol YubiKitManagerProtocol
+ 
+ @abstract
+    Provides the main access point interface for YubiKit.
+ */
+@protocol YubiKitManagerProtocol
+
+/*!
+ @property nfcReaderSession
+ 
+ @abstract
+    Returns the shared instance of YKFNFCSession to interact with the NFC reader.
+ */
+@property (nonatomic, readonly, nonnull) id<YKFNFCSessionProtocol> nfcSession NS_AVAILABLE_IOS(11.0);
+
+/*!
+ @property qrReaderSession
+ 
+ @abstract
+    Returns the shared instance of YKFQRReaderSession to interact with the QR Code reader.
+ */
+@property (nonatomic, readonly, nonnull) id<YKFQRReaderSessionProtocol> qrReaderSession;
+
+/*!
+ @property accessorySession
+ 
+ @abstract
+    Returns the shared instance of YKFAccessorySession to interact with a MFi accessory YubiKey.
+ */
+@property (nonatomic, readonly, nonnull) id<YKFAccessorySessionProtocol> accessorySession;
+
+@end
+
+
+/*!
+ @class YubiKitManager
+ 
+ @abstract
+    Provides the main access point for YubiKit.
+ */
+@interface YubiKitManager : NSObject<YubiKitManagerProtocol>
+
+/*!
+ @property shared
+ 
+ @abstract
+    YubiKitManager is a singleton and should be accessed only by using the shared instance provided by this property.
+ */
+@property (class, nonatomic, readonly, nonnull) id<YubiKitManagerProtocol> shared;
+
+/*
+ Not available: use the shared property from YubiKitManager to retreive the shared single instance.
+ */
+- (nonnull instancetype)init NS_UNAVAILABLE;
+
+@end
diff --git a/YubiKit/YubiKit/YubiKitManager.m b/YubiKit/YubiKit/YubiKitManager.m
new file mode 100755
index 000000000..85449f83d
--- /dev/null
+++ b/YubiKit/YubiKit/YubiKitManager.m
@@ -0,0 +1,59 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <ExternalAccessory/ExternalAccessory.h>
+
+#import "YubiKitManager.h"
+#import "YKFAccessorySessionConfiguration.h"
+
+#import "YKFNFCOTPService+Private.h"
+#import "YKFAccessorySession+Private.h"
+
+@interface YubiKitManager()
+
+@property (nonatomic, readwrite) id<YKFNFCSessionProtocol> nfcSession NS_AVAILABLE_IOS(11.0);
+@property (nonatomic, readwrite) id<YKFQRReaderSessionProtocol> qrReaderSession;
+@property (nonatomic, readwrite) id<YKFAccessorySessionProtocol> accessorySession;
+
+@end
+
+@implementation YubiKitManager
+
+static id<YubiKitManagerProtocol> sharedInstance;
+
++ (id<YubiKitManagerProtocol>)shared {
+    static dispatch_once_t onceToken;
+    dispatch_once(&onceToken, ^{
+        sharedInstance = [[YubiKitManager alloc] initOnce];
+    });
+    return sharedInstance;
+}
+
+- (instancetype)initOnce {
+    self = [super init];
+    if (self) {
+        if (@available(iOS 11, *)) {
+            self.nfcSession = [[YKFNFCSession alloc] init];
+        }
+        self.qrReaderSession = [[YKFQRReaderSession alloc] init];
+        
+        YKFAccessorySessionConfiguration *configuration = [[YKFAccessorySessionConfiguration alloc] init];
+        EAAccessoryManager *accessoryManager = [EAAccessoryManager sharedAccessoryManager];
+        
+        self.accessorySession = [[YKFAccessorySession alloc] initWithAccessoryManager:accessoryManager configuration:configuration];
+    }
+    return self;
+}
+
+@end
diff --git a/YubiKit/YubiKitTests/Fakes/FakeEAAccessory.h b/YubiKit/YubiKitTests/Fakes/FakeEAAccessory.h
new file mode 100755
index 000000000..54f390f38
--- /dev/null
+++ b/YubiKit/YubiKitTests/Fakes/FakeEAAccessory.h
@@ -0,0 +1,33 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+#import "EAAccessory+Testing.h"
+
+@interface FakeEAAccessory: NSObject<YKFEAAccessoryProtocol>
+
+@property(nonatomic, readwrite, getter=isConnected) BOOL connected;
+@property(nonatomic, readwrite) NSUInteger connectionID;
+
+@property(nonatomic, readwrite) NSString *manufacturer;
+@property(nonatomic, readwrite) NSString *name;
+@property(nonatomic, readwrite) NSString *modelNumber;
+@property(nonatomic, readwrite) NSString *serialNumber;
+@property(nonatomic, readwrite) NSString *firmwareRevision;
+@property(nonatomic, readwrite) NSString *hardwareRevision;
+@property(nonatomic, readwrite) NSString *dockType;
+
+@property(nonatomic, readwrite) NSArray<NSString *> *protocolStrings;
+
+@end
diff --git a/YubiKit/YubiKitTests/Fakes/FakeEAAccessory.m b/YubiKit/YubiKitTests/Fakes/FakeEAAccessory.m
new file mode 100755
index 000000000..1d8082040
--- /dev/null
+++ b/YubiKit/YubiKitTests/Fakes/FakeEAAccessory.m
@@ -0,0 +1,24 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "FakeEAAccessory.h"
+
+@interface FakeEAAccessory()
+@end
+
+@implementation FakeEAAccessory
+
+@synthesize delegate;
+
+@end
diff --git a/YubiKit/YubiKitTests/Fakes/FakeEASession.h b/YubiKit/YubiKitTests/Fakes/FakeEASession.h
new file mode 100755
index 000000000..db7c71822
--- /dev/null
+++ b/YubiKit/YubiKitTests/Fakes/FakeEASession.h
@@ -0,0 +1,23 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+#import "EASession+Testing.h"
+
+@interface FakeEASession: NSObject<YKFEASessionProtocol>
+
+- (instancetype)initWithInputData:(NSData *)inputData accessory:(id<YKFEAAccessoryProtocol>)accessory protocol:(NSString *)protocol;
+- (NSData *)outputStreamData;
+
+@end
diff --git a/YubiKit/YubiKitTests/Fakes/FakeEASession.m b/YubiKit/YubiKitTests/Fakes/FakeEASession.m
new file mode 100755
index 000000000..2a4e485d2
--- /dev/null
+++ b/YubiKit/YubiKitTests/Fakes/FakeEASession.m
@@ -0,0 +1,44 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "FakeEASession.h"
+
+@interface FakeEASession()
+
+@property (nonatomic, readwrite) id<YKFEAAccessoryProtocol> accessory;
+@property (nonatomic, readwrite) NSString *protocolString;
+@property (nonatomic, readwrite) NSInputStream *inputStream;
+@property (nonatomic, readwrite) NSOutputStream *outputStream;
+
+@end
+
+@implementation FakeEASession
+
+- (instancetype)initWithInputData:(NSData *)inputData accessory:(id<YKFEAAccessoryProtocol>)accessory protocol:(NSString *)protocol {
+    self = [super init];
+    if (self) {
+        self.inputStream = [[NSInputStream alloc] initWithData:inputData];
+        self.outputStream = [[NSOutputStream alloc] initToMemory];
+        
+        self.accessory = accessory;
+        self.protocolString = protocol;
+    }
+    return self;
+}
+
+- (NSData *)outputStreamData {
+    return [[self.outputStream propertyForKey:NSStreamDataWrittenToMemoryStreamKey] copy];
+}
+
+@end
diff --git a/YubiKit/YubiKitTests/Fakes/FakeNFCNDEFReaderSession.h b/YubiKit/YubiKitTests/Fakes/FakeNFCNDEFReaderSession.h
new file mode 100755
index 000000000..f90b16841
--- /dev/null
+++ b/YubiKit/YubiKitTests/Fakes/FakeNFCNDEFReaderSession.h
@@ -0,0 +1,28 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+#import <CoreNFC/CoreNFC.h>
+#import "NFCNDEFReaderSession+Testing.h"
+
+@interface FakeNFCNDEFReaderSession : NSObject<YKFNFCNDEFReaderSessionProtocol>
+
+@property (nonatomic, weak) id<NFCNDEFReaderSessionDelegate> delegate;
+@property (nonatomic, assign) BOOL invalidateAfterFirstRead;
+@property (nonatomic) dispatch_queue_t dispatchQueue;
+
+// Invocation properties
+@property (nonatomic, assign) BOOL sessionStarted;
+
+@end
diff --git a/YubiKit/YubiKitTests/Fakes/FakeNFCNDEFReaderSession.m b/YubiKit/YubiKitTests/Fakes/FakeNFCNDEFReaderSession.m
new file mode 100755
index 000000000..f9769f632
--- /dev/null
+++ b/YubiKit/YubiKitTests/Fakes/FakeNFCNDEFReaderSession.m
@@ -0,0 +1,35 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "FakeNFCNDEFReaderSession.h"
+
+@implementation FakeNFCNDEFReaderSession
+
+@synthesize alertMessage;
+
+- (instancetype)initWithDelegate:(id<NFCNDEFReaderSessionDelegate>)delegate queue:(nullable dispatch_queue_t)queue invalidateAfterFirstRead:(BOOL)invalidateAfterFirstRead {
+    self = [super init];
+    if (self) {
+        self.delegate = delegate;
+        self.dispatchQueue = queue;
+        self.invalidateAfterFirstRead = invalidateAfterFirstRead;
+    }
+    return self;
+}
+
+- (void)beginSession {
+    self.sessionStarted = YES;
+}
+
+@end
diff --git a/YubiKit/YubiKitTests/Fakes/FakeUIDevice.h b/YubiKit/YubiKitTests/Fakes/FakeUIDevice.h
new file mode 100755
index 000000000..aa91db8ed
--- /dev/null
+++ b/YubiKit/YubiKitTests/Fakes/FakeUIDevice.h
@@ -0,0 +1,23 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+#import "UIDevice+Testing.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FakeUIDevice: NSObject<YKFUIDeviceProtocol>
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/YubiKit/YubiKitTests/Fakes/FakeUIDevice.m b/YubiKit/YubiKitTests/Fakes/FakeUIDevice.m
new file mode 100755
index 000000000..5c9a39a31
--- /dev/null
+++ b/YubiKit/YubiKitTests/Fakes/FakeUIDevice.m
@@ -0,0 +1,22 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "FakeUIDevice.h"
+
+@implementation FakeUIDevice
+
+@synthesize systemVersion;
+@synthesize ykf_deviceModel;
+
+@end
diff --git a/YubiKit/YubiKitTests/Fakes/FakeYKFKeyConnectionController.h b/YubiKit/YubiKitTests/Fakes/FakeYKFKeyConnectionController.h
new file mode 100755
index 000000000..ee4d28e05
--- /dev/null
+++ b/YubiKit/YubiKitTests/Fakes/FakeYKFKeyConnectionController.h
@@ -0,0 +1,30 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+#import "YKFAccessoryConnectionController.h"
+
+@interface FakeYKFKeyConnectionController: NSObject<YKFKeyConnectionControllerProtocol>
+
+@property (nonatomic) YKFAPDU *executionCommand;
+
+@property (nonatomic) YKFKeyConnectionControllerCommandResponseBlock commandResponseBlock;
+@property (nonatomic) YKFKeyConnectionControllerCompletionBlock operationExecutionBlock;
+
+// Response customisation
+
+@property (nonatomic) NSArray *commandExecutionResponseDataSequence;
+@property (nonatomic) NSArray *commandExecutionResponseErrorSequence;
+
+@end
diff --git a/YubiKit/YubiKitTests/Fakes/FakeYKFKeyConnectionController.m b/YubiKit/YubiKitTests/Fakes/FakeYKFKeyConnectionController.m
new file mode 100755
index 000000000..41049aa1b
--- /dev/null
+++ b/YubiKit/YubiKitTests/Fakes/FakeYKFKeyConnectionController.m
@@ -0,0 +1,117 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "FakeYKFKeyConnectionController.h"
+
+@interface FakeYKFKeyConnectionController()
+
+@property (nonatomic, assign) NSUInteger commandExecutionSequenceIndex;
+
+@end
+
+@implementation FakeYKFKeyConnectionController
+
+- (void)setCommandExecutionResponseDataSequence:(NSArray *)commandExecutionResponseDataSequence {
+    _commandExecutionResponseDataSequence = commandExecutionResponseDataSequence;
+    self.commandExecutionSequenceIndex = 0;
+}
+
+- (void)setCommandExecutionResponseErrorSequence:(NSArray *)commandExecutionResponseErrorSequence {
+    _commandExecutionResponseErrorSequence = commandExecutionResponseErrorSequence;
+    self.commandExecutionSequenceIndex = 0;
+}
+
+#pragma mark - YKFKeyConnectionControllerProtocol
+
+- (void)execute:(YKFAPDU *)command completion:(YKFKeyConnectionControllerCommandResponseBlock)completion {
+    self.executionCommand = command;
+    self.commandResponseBlock = completion;
+    
+    NSData *responseData = [self nextResponseDataInSequence];
+    NSError *responseError = [self nextResponseErrorInSequence];
+    
+    dispatch_async(dispatch_get_main_queue(), ^{
+        completion(responseData, responseError, 0);
+    });
+    
+    ++self.commandExecutionSequenceIndex;
+}
+
+- (void)execute:(YKFAPDU *)command configuration:(YKFKeyCommandConfiguration *)configuration completion:(YKFKeyConnectionControllerCommandResponseBlock)completion {
+    self.executionCommand = command;
+    self.commandResponseBlock = completion;
+    
+    NSData *responseData = [self nextResponseDataInSequence];
+    NSError *responseError = [self nextResponseErrorInSequence];
+    
+    dispatch_async(dispatch_get_main_queue(), ^{
+        completion(responseData, responseError, 0);
+    });
+
+    ++self.commandExecutionSequenceIndex;
+}
+
+- (void)dispatchOnSequentialQueue:(YKFKeyConnectionControllerCompletionBlock)block delay:(NSTimeInterval)delay {
+    self.operationExecutionBlock = block;
+    
+    if (delay == 0) {
+        block();
+    } else {
+        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (double)(delay * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
+            block();
+        });
+    }
+}
+
+- (void)dispatchOnSequentialQueue:(nonnull YKFKeyConnectionControllerCompletionBlock)block {
+    [self dispatchOnSequentialQueue:block delay:0];
+}
+
+- (void)closeConnectionWithCompletion:(YKFKeyConnectionControllerCompletionBlock)completion {
+    self.operationExecutionBlock = completion;
+    
+    dispatch_async(dispatch_get_main_queue(), ^{
+        completion();
+    });
+}
+
+- (void)cancelAllCommands {
+    // Do nothing
+}
+
+#pragma mark - Helpers
+
+- (NSData *)nextResponseDataInSequence {
+    if (!self.commandExecutionResponseDataSequence.count) {
+        return nil;
+    }
+    if (self.commandExecutionSequenceIndex < self.commandExecutionResponseDataSequence.count) {
+        return self.commandExecutionResponseDataSequence[self.commandExecutionSequenceIndex];
+    }
+    
+    return nil;
+}
+
+- (NSError *)nextResponseErrorInSequence {
+    if (!self.commandExecutionResponseErrorSequence.count) {
+        return nil;
+    }
+    if (self.commandExecutionSequenceIndex < self.commandExecutionResponseErrorSequence.count) {
+        return self.commandExecutionResponseErrorSequence[self.commandExecutionSequenceIndex];
+    }
+
+    return self.commandExecutionResponseErrorSequence[self.commandExecutionSequenceIndex];
+}
+
+@end
diff --git a/YubiKit/YubiKitTests/Fakes/FakeYKFOTPTextParser.h b/YubiKit/YubiKitTests/Fakes/FakeYKFOTPTextParser.h
new file mode 100755
index 000000000..0deffffa4
--- /dev/null
+++ b/YubiKit/YubiKitTests/Fakes/FakeYKFOTPTextParser.h
@@ -0,0 +1,23 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+#import "YKFOTPTextParser.h"
+
+@interface FakeYKFOTPTextParser: NSObject<YKFOTPTextParserProtocol>
+
+@property (nonatomic, assign) BOOL tokenFromPayloadInvoked;
+@property (nonatomic, assign) BOOL textFromPayloadInvoked;
+
+@end
diff --git a/YubiKit/YubiKitTests/Fakes/FakeYKFOTPTextParser.m b/YubiKit/YubiKitTests/Fakes/FakeYKFOTPTextParser.m
new file mode 100755
index 000000000..898980b37
--- /dev/null
+++ b/YubiKit/YubiKitTests/Fakes/FakeYKFOTPTextParser.m
@@ -0,0 +1,29 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "FakeYKFOTPTextParser.h"
+
+@implementation FakeYKFOTPTextParser
+
+- (nonnull NSString *)tokenFromPayload:(nonnull NSString *)payload {
+    self.tokenFromPayloadInvoked = YES;
+    return @"";
+}
+
+- (nullable NSString *)textFromPayload:(nonnull NSString *)payload {
+    self.textFromPayloadInvoked = YES;
+    return nil;
+}
+
+@end
diff --git a/YubiKit/YubiKitTests/Fakes/FakeYKFOTPURIParser.h b/YubiKit/YubiKitTests/Fakes/FakeYKFOTPURIParser.h
new file mode 100755
index 000000000..c82dd38aa
--- /dev/null
+++ b/YubiKit/YubiKitTests/Fakes/FakeYKFOTPURIParser.h
@@ -0,0 +1,23 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+#import "YKFOTPURIParser.h"
+
+@interface FakeYKFOTPURIParser: NSObject<YKFOTPURIParserProtocol>
+
+@property (nonatomic, assign) BOOL tokenFromPayloadInvoked;
+@property (nonatomic, assign) BOOL uriFromPayloadInvoked;
+
+@end
diff --git a/YubiKit/YubiKitTests/Fakes/FakeYKFOTPURIParser.m b/YubiKit/YubiKitTests/Fakes/FakeYKFOTPURIParser.m
new file mode 100755
index 000000000..049917cab
--- /dev/null
+++ b/YubiKit/YubiKitTests/Fakes/FakeYKFOTPURIParser.m
@@ -0,0 +1,29 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "FakeYKFOTPURIParser.h"
+
+@implementation FakeYKFOTPURIParser
+
+- (nonnull NSString *)tokenFromPayload:(nonnull NSString *)payload {
+    self.tokenFromPayloadInvoked = YES;
+    return @"";
+}
+
+- (nullable NSString *)uriFromPayload:(nonnull NSString *)payload {
+    self.uriFromPayloadInvoked = YES;
+    return nil;
+}
+
+@end
diff --git a/YubiKit/YubiKitTests/Fakes/FakeYKFPCSCLayer.h b/YubiKit/YubiKitTests/Fakes/FakeYKFPCSCLayer.h
new file mode 100755
index 000000000..50ae40a27
--- /dev/null
+++ b/YubiKit/YubiKitTests/Fakes/FakeYKFPCSCLayer.h
@@ -0,0 +1,44 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+#import "YKFPCSCLayer.h"
+
+@interface FakeYKFPCSCLayer: NSObject<YKFPCSCLayerProtocol>
+
+@property (nonatomic) BOOL addCardToContextResponse;
+@property (nonatomic) BOOL addContextResponse;
+@property (nonatomic) BOOL cardIsValidResponse;
+@property (nonatomic) BOOL contextIsValidResponse;
+@property (nonatomic) BOOL removeCardResponse;
+@property (nonatomic) BOOL removeContextResponse;
+
+@property (nonatomic) SInt64 connectCardResponse;
+@property (nonatomic) SInt32 contextForCardResponse;
+@property (nonatomic) SInt64 disconnectCardResponse;
+@property (nonatomic) SInt64 reconnectCardResponse;
+@property (nonatomic) SInt32 getCardStateResponse;
+@property (nonatomic) SInt64 getStatusChangeResponse;
+
+@property (nonatomic) SInt64 listReadersResponse;
+@property (nonatomic) NSString *listReadersResponseParam;
+
+@property (nonatomic) SInt64 transmitResponse;
+@property (nonatomic) NSData *transmitResponseParam;
+@property (nonatomic) NSData *transmitCommand;
+
+@property (nonatomic) NSString *getCardSerialResponse;
+@property (nonatomic) NSString *stringifyErrorResponse;
+
+@end
diff --git a/YubiKit/YubiKitTests/Fakes/FakeYKFPCSCLayer.m b/YubiKit/YubiKitTests/Fakes/FakeYKFPCSCLayer.m
new file mode 100755
index 000000000..86fb4b742
--- /dev/null
+++ b/YubiKit/YubiKitTests/Fakes/FakeYKFPCSCLayer.m
@@ -0,0 +1,98 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "FakeYKFPCSCLayer.h"
+
+@implementation FakeYKFPCSCLayer
+
+@synthesize cardState;
+@synthesize cardSerial;
+@synthesize cardAtr;
+@synthesize statusChange;
+@synthesize deviceFriendlyName;
+@synthesize deviceModelName;
+@synthesize deviceVendorName;
+
+- (NSString *)cardSerial {
+    return self.getCardSerialResponse;
+}
+
+- (NSData *)cardAtr {
+    return [NSData data];
+}
+
+- (SInt32)cardState {
+    return self.getCardStateResponse;
+}
+
+- (SInt64)statusChange {
+    return self.getStatusChangeResponse;
+}
+
+- (BOOL)addCard:(SInt32)card toContext:(SInt32)context {
+    return self.addCardToContextResponse;
+}
+
+- (BOOL)addContext:(SInt32)context {
+    return self.addContextResponse;
+}
+
+- (BOOL)cardIsValid:(SInt32)card {
+    return self.cardIsValidResponse;
+}
+
+- (SInt64)connectCard {
+    return self.connectCardResponse;
+}
+
+- (SInt32)contextForCard:(SInt32)card {
+    return self.contextForCardResponse;
+}
+
+- (BOOL)contextIsValid:(SInt32)context {
+    return self.contextIsValidResponse;
+}
+
+- (SInt64)disconnectCard {
+    return self.disconnectCardResponse;
+}
+
+- (SInt64)listReaders:(NSString **)yubikeyReaderName {
+    *yubikeyReaderName = self.listReadersResponseParam;
+    return self.listReadersResponse;
+}
+
+- (SInt64)reconnectCard {
+    return self.reconnectCardResponse;
+}
+
+- (BOOL)removeCard:(SInt32)card {
+    return self.removeCardResponse;
+}
+
+- (BOOL)removeContext:(SInt32)context {
+    return self.removeContextResponse;
+}
+
+- (NSString *)stringifyError:(SInt64)errorCode {
+    return self.stringifyErrorResponse;
+}
+
+- (SInt64)transmit:(NSData *)commandData response:(NSData **)response {
+    self.transmitCommand = commandData;
+    *response = self.transmitResponseParam;
+    return self.transmitResponse;
+}
+
+@end
diff --git a/YubiKit/YubiKitTests/Fakes/FakeYubiKitDeviceCapabilities.h b/YubiKit/YubiKitTests/Fakes/FakeYubiKitDeviceCapabilities.h
new file mode 100755
index 000000000..14d74e0a2
--- /dev/null
+++ b/YubiKit/YubiKitTests/Fakes/FakeYubiKitDeviceCapabilities.h
@@ -0,0 +1,19 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+#import "YubiKitDeviceCapabilities.h"
+
+@interface FakeYubiKitDeviceCapabilities : NSObject<YubiKitDeviceCapabilitiesProtocol>
+@end
diff --git a/YubiKit/YubiKitTests/Fakes/FakeYubiKitDeviceCapabilities.m b/YubiKit/YubiKitTests/Fakes/FakeYubiKitDeviceCapabilities.m
new file mode 100755
index 000000000..f7032805d
--- /dev/null
+++ b/YubiKit/YubiKitTests/Fakes/FakeYubiKitDeviceCapabilities.m
@@ -0,0 +1,35 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "FakeYubiKitDeviceCapabilities.h"
+
+@implementation FakeYubiKitDeviceCapabilities
+
++ (BOOL)supportsQRCodeScanning {
+    return YES;
+}
+
++ (BOOL)supportsNFCScanning {
+    return YES;    
+}
+
++ (BOOL)supportsISO7816NFCTags {
+    return YES;
+}
+
++ (BOOL)supportsMFIAccessoryKey {
+    return YES;
+}
+
+@end
diff --git a/YubiKit/YubiKitTests/Info.plist b/YubiKit/YubiKitTests/Info.plist
new file mode 100755
index 000000000..6c40a6cd0
--- /dev/null
+++ b/YubiKit/YubiKitTests/Info.plist
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>CFBundleDevelopmentRegion</key>
+	<string>$(DEVELOPMENT_LANGUAGE)</string>
+	<key>CFBundleExecutable</key>
+	<string>$(EXECUTABLE_NAME)</string>
+	<key>CFBundleIdentifier</key>
+	<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
+	<key>CFBundleInfoDictionaryVersion</key>
+	<string>6.0</string>
+	<key>CFBundleName</key>
+	<string>$(PRODUCT_NAME)</string>
+	<key>CFBundlePackageType</key>
+	<string>BNDL</string>
+	<key>CFBundleShortVersionString</key>
+	<string>1.0</string>
+	<key>CFBundleVersion</key>
+	<string>1</string>
+</dict>
+</plist>
diff --git a/YubiKit/YubiKitTests/Tests/YKFAccessoryConnectionControllerTests.m b/YubiKit/YubiKitTests/Tests/YKFAccessoryConnectionControllerTests.m
new file mode 100755
index 000000000..37f40de96
--- /dev/null
+++ b/YubiKit/YubiKitTests/Tests/YKFAccessoryConnectionControllerTests.m
@@ -0,0 +1,219 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <XCTest/XCTest.h>
+
+#import "YKFTestCase.h"
+#import "YKFAccessoryConnectionController.h"
+#import "FakeEASession.h"
+#import "YKFAPDU+Private.h"
+
+@interface YKFAccessoryConnectionControllerTests: YKFTestCase
+
+@property (nonatomic) NSOperationQueue *operationQueue;
+@property (nonatomic) dispatch_queue_t sharedDispatchQueue;
+
+@property (nonatomic) FakeEASession *eaSession;
+
+@end
+
+@implementation YKFAccessoryConnectionControllerTests
+
+- (void)setUp {
+    [super setUp];
+    
+    self.operationQueue = [[NSOperationQueue alloc] init];
+    self.operationQueue.maxConcurrentOperationCount = 1;
+    
+    dispatch_queue_attr_t dispatchQueueAttributes = dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, DISPATCH_QUEUE_PRIORITY_HIGH, -1);
+    self.sharedDispatchQueue = dispatch_queue_create("com.yubico.YKCommunication", dispatchQueueAttributes);
+    
+    self.operationQueue.underlyingQueue = self.sharedDispatchQueue;
+}
+
+- (void)tearDown {
+    [super tearDown];
+    self.operationQueue = nil;
+    self.sharedDispatchQueue = nil;
+}
+
+- (void)test_WhenConnectionControllerIsCreated_SessionStreamsAreOpened {
+    NSData *inputData = [@"data" dataUsingEncoding:NSUTF8StringEncoding];
+    self.eaSession = [[FakeEASession alloc] initWithInputData:inputData accessory:nil protocol:@"YLP"];
+    
+    YKFAccessoryConnectionController *connectionController = [[YKFAccessoryConnectionController alloc] initWithSession:self.eaSession operationQueue:self.operationQueue];
+    [self waitForTimeInterval:0.2];
+    
+    XCTAssert(self.eaSession.inputStream.streamStatus == NSStreamStatusOpen);
+    XCTAssert(self.eaSession.outputStream.streamStatus == NSStreamStatusOpen);
+    
+    connectionController = nil;
+}
+
+- (void)test_WhenConnectionControllerIsClosed_SessionStreamsAreClosed {
+    NSData *inputData = [@"data" dataUsingEncoding:NSUTF8StringEncoding];
+    self.eaSession = [[FakeEASession alloc] initWithInputData:inputData accessory:nil protocol:@"YLP"];
+    
+    YKFAccessoryConnectionController *connectionController = [[YKFAccessoryConnectionController alloc] initWithSession:self.eaSession operationQueue:self.operationQueue];
+    
+    [self waitForTimeInterval:0.2];
+    
+    XCTAssert(self.eaSession.inputStream.streamStatus == NSStreamStatusOpen);
+    XCTAssert(self.eaSession.outputStream.streamStatus == NSStreamStatusOpen);
+    
+    XCTestExpectation *expectation = [[XCTestExpectation alloc] initWithDescription:@"Close key connection controller completion"];
+    [connectionController closeConnectionWithCompletion:^{
+        [expectation fulfill];
+    }];
+    XCTWaiterResult result = [XCTWaiter waitForExpectations:@[expectation] timeout:1];
+    XCTAssert(result == XCTWaiterResultCompleted);
+    
+    [self waitForTimeInterval:0.2];
+    
+    XCTAssert(self.eaSession.inputStream.streamStatus == NSStreamStatusClosed);
+    XCTAssert(self.eaSession.outputStream.streamStatus == NSStreamStatusClosed);
+}
+
+- (void)test_WhenConnectionControllerWritesCommands_CommandsAreWrittenToTheOutputStream {
+    UInt8 inputBytes[] = {0x00, 0x90, 0x00};
+    NSData *inputData = [[NSData alloc] initWithBytes:inputBytes length:3];
+    
+    self.eaSession = [[FakeEASession alloc] initWithInputData:inputData accessory:nil protocol:@"YLP"];
+    
+    YKFAccessoryConnectionController *connectionController = [[YKFAccessoryConnectionController alloc] initWithSession:self.eaSession operationQueue:self.operationQueue];
+    [self waitForTimeInterval:0.2];
+    
+    XCTAssert(self.eaSession.inputStream.streamStatus == NSStreamStatusOpen);
+    XCTAssert(self.eaSession.outputStream.streamStatus == NSStreamStatusOpen);
+    
+    // Execute command
+    
+    NSData *commandData = [@"command" dataUsingEncoding:NSUTF8StringEncoding];
+    YKFAPDU *command = [[YKFAPDU alloc] initWithData:commandData];
+    
+    XCTestExpectation *expectation = [[XCTestExpectation alloc] initWithDescription:@"Command execution completion."];
+    [connectionController execute:command completion:^(NSData *result, NSError *error, NSTimeInterval executionTime) {
+        [expectation fulfill];
+    }];
+    XCTWaiterResult result = [XCTWaiter waitForExpectations:@[expectation] timeout:1];
+    XCTAssert(result == XCTWaiterResultCompleted);
+
+    // Check the written data
+    
+    NSData *writtenData = [self.eaSession outputStreamData];
+    XCTAssert([writtenData isEqualToData:command.ylpApduData], @"Command data doesn't match written data.");
+}
+
+- (void)test_WhenConnectionControllerWritesCommands_ResponseIsReadFromTheInputStream {
+    UInt8 inputBytes[] = {0x00, 0x90, 0x00};
+    NSData *inputData = [[NSData alloc] initWithBytes:inputBytes length:3];
+
+    self.eaSession = [[FakeEASession alloc] initWithInputData:inputData accessory:nil protocol:@"YLP"];
+    
+    YKFAccessoryConnectionController *connectionController = [[YKFAccessoryConnectionController alloc] initWithSession:self.eaSession operationQueue:self.operationQueue];
+    [self waitForTimeInterval:0.2];
+    
+    XCTAssert(self.eaSession.inputStream.streamStatus == NSStreamStatusOpen);
+    XCTAssert(self.eaSession.outputStream.streamStatus == NSStreamStatusOpen);
+    
+    // Execute command
+    
+    NSData *commandData = [@"command" dataUsingEncoding:NSUTF8StringEncoding];
+    YKFAPDU *command = [[YKFAPDU alloc] initWithData:commandData];
+    
+    __block NSData *response = nil;
+    
+    XCTestExpectation *expectation = [[XCTestExpectation alloc] initWithDescription:@"Command execution completion."];
+    [connectionController execute:command completion:^(NSData *result, NSError *error, NSTimeInterval executionTime) {
+        response = result;
+        [expectation fulfill];
+    }];
+    XCTWaiterResult result = [XCTWaiter waitForExpectations:@[expectation] timeout:1];
+    XCTAssert(result == XCTWaiterResultCompleted);
+    
+    // Check the response data
+    
+    XCTAssert([response isEqualToData:[inputData subdataWithRange:NSMakeRange(1, 2)]], @"Response data doesn't match the input data.");
+}
+
+- (void)test_WhenDispatchingAnExecutionBlockOnTheCommunicationQueue_BlockIsExecuted {
+    NSData *inputData = [@"input_data" dataUsingEncoding:NSUTF8StringEncoding];
+    self.eaSession = [[FakeEASession alloc] initWithInputData:inputData accessory:nil protocol:@"YLP"];
+    
+    YKFAccessoryConnectionController *connectionController = [[YKFAccessoryConnectionController alloc] initWithSession:self.eaSession operationQueue:self.operationQueue];
+    [self waitForTimeInterval:0.2];
+    
+    XCTestExpectation *expectation = [[XCTestExpectation alloc] initWithDescription:@"Execution on communication queue completion."];
+    [connectionController dispatchOnSequentialQueue:^{
+        [expectation fulfill];
+    }];
+     
+    XCTWaiterResult result = [XCTWaiter waitForExpectations:@[expectation] timeout:1];
+    XCTAssert(result == XCTWaiterResultCompleted);
+}
+
+- (void)test_WhenDispatchingAnExecutionBlockOnTheCommunicationQueueWithDelay_BlockIsExecuted {
+    NSData *inputData = [@"input_data" dataUsingEncoding:NSUTF8StringEncoding];
+    self.eaSession = [[FakeEASession alloc] initWithInputData:inputData accessory:nil protocol:@"YLP"];
+    
+    YKFAccessoryConnectionController *connectionController = [[YKFAccessoryConnectionController alloc] initWithSession:self.eaSession operationQueue:self.operationQueue];
+    [self waitForTimeInterval:0.2];
+    
+    NSTimeInterval delay = 1;
+    NSTimeInterval deviation = 0.2;
+    
+    NSDate *startDate = [NSDate date];
+    
+    XCTestExpectation *expectation = [[XCTestExpectation alloc] initWithDescription:@"Execution on communication queue completion."];
+    [connectionController dispatchOnSequentialQueue:^{
+        [expectation fulfill];
+    } delay: delay];
+    
+    XCTWaiterResult result = [XCTWaiter waitForExpectations:@[expectation] timeout:delay + deviation];
+    XCTAssert(result == XCTWaiterResultCompleted);
+    
+    NSDate *endDate = [NSDate date];
+    NSTimeInterval time = [endDate timeIntervalSinceDate:startDate];
+    
+    XCTAssert(time <= delay + deviation, @"Execution time exceeded.");
+}
+
+#pragma mark - Delayed Responses
+
+- (void)test_WhenConnectionControllerReadsDelayedResponse_ControllerWaitsForResult {
+    
+    NSData *inputData = [self dataWithBytes:@[@(0x01), @(0x00), @(0x00)]];
+    self.eaSession = [[FakeEASession alloc] initWithInputData:inputData accessory:nil protocol:@"YLP"];
+    
+    YKFAccessoryConnectionController *connectionController = [[YKFAccessoryConnectionController alloc] initWithSession:self.eaSession operationQueue:self.operationQueue];
+    [self waitForTimeInterval:0.2];
+    
+    XCTAssert(self.eaSession.inputStream.streamStatus == NSStreamStatusOpen);
+    XCTAssert(self.eaSession.outputStream.streamStatus == NSStreamStatusOpen);
+    
+    // Execute command
+    
+    NSData *commandData = [@"command" dataUsingEncoding:NSUTF8StringEncoding];
+    YKFAPDU *command = [[YKFAPDU alloc] initWithData:commandData];
+    
+    XCTestExpectation *expectation = [[XCTestExpectation alloc] initWithDescription:@"Command execution completion."];
+    [connectionController execute:command completion:^(NSData *result, NSError *error, NSTimeInterval executionTime) {
+        [expectation fulfill];
+    }];
+    
+    XCTWaiterResult result = [XCTWaiter waitForExpectations:@[expectation] timeout:0.5];
+    XCTAssert(result == XCTWaiterResultTimedOut); // The result should time out because the key didn't reply to the request.
+}
+
+@end
diff --git a/YubiKit/YubiKitTests/Tests/YKFAccessoryDescriptionTests.m b/YubiKit/YubiKitTests/Tests/YKFAccessoryDescriptionTests.m
new file mode 100755
index 000000000..9f73d6224
--- /dev/null
+++ b/YubiKit/YubiKitTests/Tests/YKFAccessoryDescriptionTests.m
@@ -0,0 +1,62 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <XCTest/XCTest.h>
+
+#import "YKFTestCase.h"
+#import "YKFAccessoryDescription.h"
+#import "YKFAccessoryDescription+Private.h"
+#import "FakeEAAccessory.h"
+
+@interface YKFAccessoryDescriptionTests: YKFTestCase
+@end
+
+@implementation YKFAccessoryDescriptionTests
+
+- (void)test_WhenKeyDescriptionIsInitializedWithAccessory_TheDataIsCorrectlyCopiedOver {
+    FakeEAAccessory *accessory = [[FakeEAAccessory alloc] init];
+    
+    accessory.manufacturer = @"Yubico";
+    accessory.name = @"YubiKey";
+    accessory.modelNumber = @"5Ci";
+    accessory.serialNumber = @"AFF3BBEE";
+    accessory.firmwareRevision = @"1.0.0";
+    accessory.hardwareRevision = @"r1";
+
+    YKFAccessoryDescription *accessoryDescription = [[YKFAccessoryDescription alloc] initWithAccessory:accessory];
+    
+    XCTAssertEqual(accessoryDescription.manufacturer, accessory.manufacturer);
+    XCTAssertEqual(accessoryDescription.name, accessory.name);
+    XCTAssertEqual(accessoryDescription.modelNumber, accessory.modelNumber);
+    XCTAssertEqual(accessoryDescription.serialNumber, accessory.serialNumber);
+    XCTAssertEqual(accessoryDescription.firmwareRevision, accessory.firmwareRevision);
+    XCTAssertEqual(accessoryDescription.hardwareRevision, accessory.hardwareRevision);
+}
+
+- (void)test_WhenFirmwareVersionIsConcatenated_FirmwareVersionIsParsed {
+    FakeEAAccessory *accessory = [[FakeEAAccessory alloc] init];
+    
+    accessory.manufacturer = @"Yubico";
+    accessory.name = @"YubiKey";
+    accessory.modelNumber = @"5Ci";
+    accessory.serialNumber = @"AFF3BBEE";
+    accessory.firmwareRevision = @"100";
+    accessory.hardwareRevision = @"r1";
+    
+    YKFAccessoryDescription *accessoryDescription = [[YKFAccessoryDescription alloc] initWithAccessory:accessory];
+    
+    XCTAssert([accessoryDescription.firmwareRevision isEqualToString: @"1.0.0"]);
+}
+
+@end
diff --git a/YubiKit/YubiKitTests/Tests/YKFAccessorySessionConfigurationTests.m b/YubiKit/YubiKitTests/Tests/YKFAccessorySessionConfigurationTests.m
new file mode 100755
index 000000000..d907b8984
--- /dev/null
+++ b/YubiKit/YubiKitTests/Tests/YKFAccessorySessionConfigurationTests.m
@@ -0,0 +1,83 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <XCTest/XCTest.h>
+
+#import "YKFTestCase.h"
+#import "YKFAccessorySessionConfiguration.h"
+#import "FakeEAAccessory.h"
+#import "EAAccessory+Testing.h"
+
+@interface YKFAccessorySessionConfigurationTests: YKFTestCase
+
+@property (nonatomic) YKFAccessorySessionConfiguration *sessionConfiguration;
+@property (nonatomic) FakeEAAccessory *accessory;
+
+@end
+
+@implementation YKFAccessorySessionConfigurationTests
+
+- (void)setUp {
+    [super setUp];
+    self.sessionConfiguration = [[YKFAccessorySessionConfiguration alloc] init];
+    self.accessory = [[FakeEAAccessory alloc] init];
+}
+
+#pragma mark -  Protocol Tests
+
+- (void)test_WhenAccessoryProtocolIsYLP_AccessoryIsAllowed {
+    self.accessory.manufacturer = @"Yubico";
+    self.accessory.protocolStrings = @[@"com.yubico.ylp"];
+    
+    BOOL allowsAccessory = [self.sessionConfiguration allowsAccessory:self.accessory];
+    XCTAssertTrue(allowsAccessory, @"Does not allow accessory with YLP protocol.");
+}
+
+- (void)test_WhenAccessoryProtocolIsNotRecognised_AccessoryIsNotAllowed {
+    self.accessory.manufacturer = @"Yubico";
+    self.accessory.protocolStrings = @[@"Unknown"];
+    
+    BOOL allowsAccessory = [self.sessionConfiguration allowsAccessory:self.accessory];
+    XCTAssertFalse(allowsAccessory, @"Aallows accessory which has unknown protocol.");
+}
+
+#pragma mark -  Manufacturer Tests
+
+- (void)test_WhenAccessoryManufacturerIsYubico_AccessoryIsAllowed {
+    self.accessory.manufacturer = @"Yubico";
+    self.accessory.protocolStrings = @[@"com.yubico.ylp"];
+    
+    BOOL allowsAccessory = [self.sessionConfiguration allowsAccessory:self.accessory];
+    XCTAssertTrue(allowsAccessory, @"Does not allow accessory which is manufactured by Yubico.");
+}
+
+- (void)test_WhenAccessoryManufacturerIsNotYubico_AccessoryIsNotAllowed {
+    self.accessory.manufacturer = @"Unknown";
+    self.accessory.protocolStrings = @[@"com.yubico.ylp"];
+    
+    BOOL allowsAccessory = [self.sessionConfiguration allowsAccessory:self.accessory];
+    XCTAssertFalse(allowsAccessory, @"Allows accessory which is not manufactured by Yubico.");
+}
+
+#pragma mark -  Misc Tests
+
+- (void)test_WhenAccessoryIsYubiKey_AccessoryIsAllowed {
+    self.accessory.manufacturer = @"Yubico";
+    self.accessory.protocolStrings = @[@"com.yubico.ylp"];
+    
+    BOOL allowsAccessory = [self.sessionConfiguration allowsAccessory:self.accessory];
+    XCTAssertTrue(allowsAccessory, @"Does not allow accessory.");    
+}
+
+@end
diff --git a/YubiKit/YubiKitTests/Tests/YKFCBORDecoderTests.m b/YubiKit/YubiKitTests/Tests/YKFCBORDecoderTests.m
new file mode 100755
index 000000000..1b7d448a7
--- /dev/null
+++ b/YubiKit/YubiKitTests/Tests/YKFCBORDecoderTests.m
@@ -0,0 +1,342 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <XCTest/XCTest.h>
+#import "YKFTestCase.h"
+#import "YKFCBOREncoder.h"
+#import "YKFCBORDecoder.h"
+
+@interface YKFCBORDecoderTests: YKFTestCase
+
+@property (nonatomic) NSArray *testIntegers;
+@property (nonatomic) NSArray *testStrings;
+@property (nonatomic) NSArray *testLongData;
+
+@end
+
+@implementation YKFCBORDecoderTests
+
+- (void)setUp {
+    [super setUp];
+    
+    // Integers setup
+
+    NSUInteger noIntegers = 5;
+    NSMutableArray *integers = [[NSMutableArray alloc] initWithCapacity:noIntegers];
+    for (int i = 0; i < noIntegers; ++i) {
+        [integers addObject:YKFCBORInteger(i)];
+    }
+    self.testIntegers = [integers copy];
+    
+    // Strings setup
+    
+    NSUInteger noStrings = 5;
+    NSMutableArray *strings = [[NSMutableArray alloc] initWithCapacity:noIntegers];
+    for (unichar c = 'a'; c < 'a' + noStrings; ++c) {
+        [strings addObject:YKFCBORTextString([NSString stringWithCharacters:&c length:1])];
+    }
+    self.testStrings = [strings copy];
+    
+    // Long data setup
+    
+    NSMutableData *testLongData1 = [[NSMutableData alloc] initWithCapacity:23];
+    NSMutableData *testLongData2 = [[NSMutableData alloc] initWithCapacity:25];
+    NSMutableData *testLongData3 = [[NSMutableData alloc] initWithCapacity:100];
+    NSMutableData *testLongData4 = [[NSMutableData alloc] initWithCapacity:255];
+    
+    for (int i = 0; i <= 255; ++i) {
+        UInt8 byte = i;
+        if (i < 23) {
+            [testLongData1 appendBytes:&byte length:1];
+        }
+        if (i < 25) {
+            [testLongData2 appendBytes:&byte length:1];
+        }
+        if (i < 100) {
+            [testLongData3 appendBytes:&byte length:1];
+        }
+        [testLongData4 appendBytes:&byte length:1];
+    }
+    self.testLongData = @[testLongData1, testLongData2, testLongData3, testLongData4];
+}
+
+#pragma mark - Integer Tests (MT 0, 1)
+
+- (void)testIntegerDecoding {
+    NSArray *testVector = @[@(0), @(1), @(23), @(24), @(100), @(1000), @(1000000), @(1000000000000),
+                            @(-1), @(-23), @(-24), @(-100), @(-1000), @(-1000000), @(-1000000000000)];
+    
+    for (NSNumber *testEntry in testVector) {
+        YKFCBORInteger *cborInteger = YKFCBORInteger(testEntry.integerValue);
+        
+        NSData *encodedInteger = [YKFCBOREncoder encodeInteger:cborInteger];
+        
+        NSInputStream *inputStream = [NSInputStream inputStreamWithData:encodedInteger];
+        [inputStream open];
+        id decodedObject = [YKFCBORDecoder decodeObjectFrom:inputStream];
+        [inputStream close];
+        
+        XCTAssert([decodedObject isKindOfClass:YKFCBORInteger.class], @"CBOR - Wrong class decoded when parsing integers.");
+        YKFCBORInteger *decodedInteger = (YKFCBORInteger *)decodedObject;
+        
+        XCTAssertEqual(testEntry.integerValue, decodedInteger.value, @"CBOR - Wrong integer decoded.");
+    }
+}
+
+#pragma mark - Byte String Tests (MT 2)
+
+- (void)testByteStringDecoding {
+    NSArray *testVector = @[[NSData data],
+                            [NSData dataWithBytes:(UInt8[]){0x01, 0x02, 0x03, 0x04} length:4],
+                            self.testLongData[0],
+                            self.testLongData[1],
+                            self.testLongData[2],
+                            self.testLongData[3]];
+    
+    for (NSData *testEntry in testVector) {
+        YKFCBORByteString *cborByteString = YKFCBORByteString(testEntry);
+        
+        NSData *encodedByteString = [YKFCBOREncoder encodeByteString:cborByteString];
+        
+        NSInputStream *inputStream = [NSInputStream inputStreamWithData:encodedByteString];
+        [inputStream open];
+        id decodedObject = [YKFCBORDecoder decodeObjectFrom:inputStream];
+        [inputStream close];
+        
+        XCTAssert([decodedObject isKindOfClass:YKFCBORByteString.class], @"CBOR - Wrong class decoded when parsing byte strings.");
+        YKFCBORByteString *decodedByteString = (YKFCBORByteString *)decodedObject;
+        
+        XCTAssert([testEntry isEqualToData:decodedByteString.value],  @"CBOR - Wrong byte string decoded.");
+    }
+}
+
+#pragma mark - Text String Tests (MT 3)
+
+- (void)testTextStringDecoding {
+    NSArray *testVector = @[@"", @"a", @"IETF", @"\"\\", @"ü", @"水", @"𐅑"];
+    
+    for (NSString *testEntry in testVector) {
+        YKFCBORTextString *cborTextString = YKFCBORTextString(testEntry);
+        NSData *encodedTextString = [YKFCBOREncoder encodeTextString:cborTextString];
+        
+        NSInputStream *inputStream = [NSInputStream inputStreamWithData:encodedTextString];
+        [inputStream open];
+        id decodedObject = [YKFCBORDecoder decodeObjectFrom:inputStream];
+        [inputStream close];
+        
+        XCTAssert([decodedObject isKindOfClass:YKFCBORTextString.class], @"CBOR - Wrong class decoded when parsing text strings.");
+        YKFCBORTextString *decodedTextString = (YKFCBORTextString *)decodedObject;
+        
+        XCTAssert([testEntry isEqualToString:decodedTextString.value],  @"CBOR - Wrong text string decoded.");
+    }
+}
+
+#pragma mark - Array Tests (MT 4)
+
+- (void)testArrayDecoding {
+    NSArray *testVector = @[@[],
+                            @[self.testIntegers[0],
+                              self.testIntegers[1],
+                              self.testIntegers[2]],
+                            @[self.testIntegers[0],
+                              [YKFCBORArray cborArrayWithValue:@[self.testIntegers[0],
+                                                                 self.testIntegers[1]]],
+                              [YKFCBORArray cborArrayWithValue:@[self.testIntegers[2],
+                                                                 self.testIntegers[3]]]],
+                            @[self.testStrings[0], [YKFCBORMap cborMapWithValue:
+                                                    @{self.testStrings[1]:self.testStrings[2]}]]
+                            ];
+    
+    for (NSArray *testEntry in testVector) {
+        YKFCBORArray *cborArray = YKFCBORArray(testEntry);
+        NSData *encodedArray = [YKFCBOREncoder encodeArray:cborArray];
+        
+        NSInputStream *inputStream = [NSInputStream inputStreamWithData:encodedArray];
+        [inputStream open];
+        id decodedObject = [YKFCBORDecoder decodeObjectFrom:inputStream];
+        [inputStream close];
+        
+        XCTAssert([decodedObject isKindOfClass:YKFCBORArray.class], @"CBOR - Wrong class decoded when parsing array.");
+        YKFCBORArray *decodedArray = (YKFCBORArray *)decodedObject;
+        
+        XCTAssert([testEntry isEqualToArray:decodedArray.value],  @"CBOR - Wrong array decoded.");
+    }
+}
+
+#pragma mark - Map Tests (MT 5)
+
+- (void)testMapDecoding {
+    NSArray *testVector = @[@{},
+                            @{self.testIntegers[0]: self.testIntegers[1],
+                              self.testIntegers[2]: self.testIntegers[3]},
+                            @{self.testStrings[0]: self.testIntegers[0],
+                              self.testStrings[1]:[YKFCBORArray cborArrayWithValue:
+                                                   @[self.testIntegers[1],
+                                                     self.testIntegers[2]]]},
+                            @{self.testStrings[0]: self.testIntegers[0],
+                              self.testStrings[1]:[YKFCBORArray cborArrayWithValue:
+                                                   @[self.testIntegers[1],
+                                                     self.testIntegers[2]]]}
+                            ];
+    
+    for (NSDictionary *testEntry in testVector) {
+        YKFCBORMap *cborMap = YKFCBORMap(testEntry);
+        NSData *encodedMap = [YKFCBOREncoder encodeMap:cborMap];
+        
+        NSInputStream *inputStream = [NSInputStream inputStreamWithData:encodedMap];
+        [inputStream open];
+        id decodedObject = [YKFCBORDecoder decodeObjectFrom:inputStream];
+        [inputStream close];
+        
+        XCTAssert([decodedObject isKindOfClass:YKFCBORMap.class], @"CBOR - Wrong class decoded when parsing map.");
+        YKFCBORMap *decodedMap = (YKFCBORMap *)decodedObject;
+        
+        XCTAssert([testEntry isEqualToDictionary:decodedMap.value],  @"CBOR - Wrong map decoded.");
+    }
+}
+
+#pragma mark - Bool Tests
+
+- (void)testBooleanDecoding {
+    NSData *trueEncoded = [YKFCBOREncoder encodeBool: YKFCBORBool(YES)];
+    NSData *falseEncoded = [YKFCBOREncoder encodeBool: YKFCBORBool(NO)];
+    
+    NSMutableData *inputData = [[NSMutableData alloc] initWithCapacity:trueEncoded.length + falseEncoded.length];
+    [inputData appendData:trueEncoded];
+    [inputData appendData:falseEncoded];
+
+    NSInputStream *inputStream = [NSInputStream inputStreamWithData:inputData];
+    [inputStream open];
+    
+    id decodedObject = [YKFCBORDecoder decodeObjectFrom:inputStream];
+    XCTAssert([decodedObject isKindOfClass:YKFCBORBool.class], @"CBOR - Wrong class decoded when parsing bool.");
+    XCTAssert(((YKFCBORBool *)decodedObject).value == YES, @"CBOR - Wrong bool value decoded.");
+
+    decodedObject = [YKFCBORDecoder decodeObjectFrom:inputStream];
+    XCTAssert([decodedObject isKindOfClass:YKFCBORBool.class], @"CBOR - Wrong class decoded when parsing bool.");
+    XCTAssert(((YKFCBORBool *)decodedObject).value == NO, @"CBOR - Wrong bool value decoded.");
+    
+    [inputStream close];
+}
+
+#pragma mark - Mixed Tests
+
+- (void)testMixedInputMap {
+    NSDictionary *testInput = @{self.testIntegers[0]: self.testStrings[0],
+                                self.testIntegers[1]: self.testStrings[1],
+                                self.testIntegers[2]: self.testStrings[2],
+                                self.testIntegers[4]:
+                                    [YKFCBORMap cborMapWithValue:
+                                     @{self.testStrings[0]: self.testIntegers[0],
+                                       self.testStrings[1]: self.testIntegers[1],
+                                       self.testStrings[2]: [YKFCBORArray cborArrayWithValue:
+                                                             @[self.testIntegers[0],
+                                                               self.testIntegers[1],
+                                                               self.testStrings[0],
+                                                               self.testStrings[1]]
+                                                             ]
+                                       }]
+                                };
+    NSData *encodedMap = [YKFCBOREncoder encodeMap:YKFCBORMap(testInput)];
+    
+    NSInputStream *inputStream = [NSInputStream inputStreamWithData:encodedMap];
+    [inputStream open];
+    id decodedObject = [YKFCBORDecoder decodeObjectFrom:inputStream];
+    [inputStream close];
+
+    XCTAssert([decodedObject isKindOfClass:YKFCBORMap.class], @"CBOR - Wrong class decoded when parsing map.");
+    YKFCBORMap *decodedMap = (YKFCBORMap *)decodedObject;
+    
+    XCTAssert([testInput isEqualToDictionary:decodedMap.value],  @"CBOR - Wrong map decoded.");
+}
+
+- (void)testMixedInputArray {
+    NSArray *testInput = @[self.testIntegers[0],
+                           self.testStrings[0],
+                           self.testIntegers[1],
+                           self.testStrings[1],
+                           self.testIntegers[2],
+                           self.testStrings[2],
+                           self.testIntegers[3],
+                           [YKFCBORMap cborMapWithValue:
+                            @{self.testStrings[0]: self.testIntegers[0],
+                              self.testStrings[1]: self.testIntegers[1],
+                              self.testStrings[2]:[YKFCBORArray cborArrayWithValue:
+                                                   @[self.testIntegers[0],
+                                                     self.testIntegers[1],
+                                                     self.testStrings[0],
+                                                     self.testStrings[1]]
+                                                   ]
+                              }]
+                           ];
+    NSData *encodedArray = [YKFCBOREncoder encodeArray:YKFCBORArray(testInput)];
+    
+    NSInputStream *inputStream = [NSInputStream inputStreamWithData:encodedArray];
+    [inputStream open];
+    id decodedObject = [YKFCBORDecoder decodeObjectFrom:inputStream];
+    [inputStream close];
+    
+    XCTAssert([decodedObject isKindOfClass:YKFCBORArray.class], @"CBOR - Wrong class decoded when parsing array.");
+    YKFCBORArray *decodedArray = (YKFCBORArray *)decodedObject;
+    
+    XCTAssert([testInput isEqualToArray:decodedArray.value],  @"CBOR - Wrong array decoded.");
+}
+
+- (void)testMixedInputUngroupped {
+    NSDictionary *objectInput1 = @{self.testIntegers[0]: self.testStrings[0],
+                                   self.testIntegers[1]: self.testStrings[1],
+                                   self.testIntegers[2]: self.testStrings[2],
+                                   self.testIntegers[3]:
+                                       [YKFCBORMap cborMapWithValue:
+                                        @{self.testStrings[0]: self.testIntegers[0],
+                                          self.testStrings[1]: self.testIntegers[1],
+                                          self.testStrings[2]: [YKFCBORArray cborArrayWithValue:
+                                                                @[self.testIntegers[0],
+                                                                  self.testIntegers[1],
+                                                                  self.testStrings[0],
+                                                                  self.testStrings[1]]
+                                                                ]
+                                          }]
+                                   };
+    
+    NSDictionary *objectInput2 = @{self.testIntegers[0]: self.testStrings[0],
+                                   self.testIntegers[1]: self.testStrings[1],
+                                   self.testIntegers[2]: self.testStrings[2]};
+    
+    NSData *encodedMap1 = [YKFCBOREncoder encodeMap:YKFCBORMap(objectInput1)];
+    NSData *encodedMap2 = [YKFCBOREncoder encodeMap:YKFCBORMap(objectInput2)];
+    
+    NSMutableData *inputData = [[NSMutableData alloc] initWithCapacity:encodedMap1.length + encodedMap2.length];
+    [inputData appendData:encodedMap1];
+    [inputData appendData:encodedMap2];
+    
+    NSInputStream *inputStream = [NSInputStream inputStreamWithData:inputData];
+    [inputStream open];
+
+    // Check first map
+    id decodedObject = [YKFCBORDecoder decodeObjectFrom:inputStream];
+    XCTAssert([decodedObject isKindOfClass:YKFCBORMap.class], @"CBOR - Wrong class decoded when parsing map.");
+    YKFCBORMap *decodedMap = (YKFCBORMap *)decodedObject;
+    XCTAssert([objectInput1 isEqualToDictionary:decodedMap.value],  @"CBOR - Wrong map decoded.");
+
+    // Check second map
+    decodedObject = [YKFCBORDecoder decodeObjectFrom:inputStream];
+    XCTAssert([decodedObject isKindOfClass:YKFCBORMap.class], @"CBOR - Wrong class decoded when parsing map.");
+    decodedMap = (YKFCBORMap *)decodedObject;
+    XCTAssert([objectInput2 isEqualToDictionary:decodedMap.value],  @"CBOR - Wrong map decoded.");
+    
+    [inputStream close];
+}
+
+@end
diff --git a/YubiKit/YubiKitTests/Tests/YKFCBOREncoderTests.m b/YubiKit/YubiKitTests/Tests/YKFCBOREncoderTests.m
new file mode 100755
index 000000000..fa993f518
--- /dev/null
+++ b/YubiKit/YubiKitTests/Tests/YKFCBOREncoderTests.m
@@ -0,0 +1,211 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <XCTest/XCTest.h>
+#import "YKFTestCase.h"
+#import "YKFCBOREncoder.h"
+
+@interface YKFCBOREncoderTests: YKFTestCase
+@end
+
+@implementation YKFCBOREncoderTests
+
+#pragma mark - Integer Tests (MT 0, 1)
+
+- (void)testPositiveIntegerEncoding {
+    NSArray *testVectors =
+        @[@[@(0), [NSData dataWithBytes:(UInt8[]){0x00} length:1]],
+          @[@(1), [NSData dataWithBytes:(UInt8[]){0x01} length:1]],
+          @[@(10), [NSData dataWithBytes:(UInt8[]){0x0A} length:1]],
+          @[@(23), [NSData dataWithBytes:(UInt8[]){0x17} length:1]],
+          @[@(24), [NSData dataWithBytes:(UInt8[]){0x18, 0x18} length:2]],
+          @[@(25), [NSData dataWithBytes:(UInt8[]){0x18, 0x19} length:2]],
+          @[@(100), [NSData dataWithBytes:(UInt8[]){0x18, 0x64} length:2]],
+          @[@(1000), [NSData dataWithBytes:(UInt8[]){0x19, 0x03, 0xE8} length:3]],
+          @[@(1000000), [NSData dataWithBytes:(UInt8[]){0x1A, 0x00, 0x0F, 0x42, 0x40} length:5]],
+          @[@(1000000000000), [NSData dataWithBytes:(UInt8[]){0x1B, 0x00, 0x00, 0x00, 0xE8, 0xD4, 0xA5, 0x10, 0x00} length:9]]
+          ];
+    
+    for (NSArray *testEntry in testVectors) {
+        NSInteger integer = ((NSNumber *)testEntry[0]).integerValue;
+        YKFCBORInteger *cborInteger = YKFCBORInteger(integer);
+        
+        NSData *encodedInteger = [YKFCBOREncoder encodeInteger:cborInteger];
+        NSData *expectedEncodedData = (NSData *)testEntry[1];
+        
+        XCTAssert([encodedInteger isEqualToData:expectedEncodedData], @"Data encoding does not match for positive integer (%ld).", (long)integer);
+    }
+}
+
+- (void)testNegativeIntegerEncoding {
+    NSArray *testVectors =
+        @[@[@(-1), [NSData dataWithBytes:(UInt8[]){0x20} length:1]],
+          @[@(-10), [NSData dataWithBytes:(UInt8[]){0x29} length:1]],
+          @[@(-100), [NSData dataWithBytes:(UInt8[]){0x38, 0x63} length:2]],
+          @[@(-1000), [NSData dataWithBytes:(UInt8[]){0x39, 0x03, 0xE7} length:3]]
+          ];
+    
+    for (NSArray *testEntry in testVectors) {
+        NSInteger integer = ((NSNumber *)testEntry[0]).integerValue;
+        YKFCBORInteger *cborInteger = YKFCBORInteger(integer);
+        
+        NSData *encodedInteger = [YKFCBOREncoder encodeInteger:cborInteger];
+        NSData *expectedEncodedData = (NSData *)testEntry[1];
+        
+        XCTAssert([encodedInteger isEqualToData:expectedEncodedData], @"Data encoding does not match for negative integer (%ld).", (long)integer);
+    }
+}
+
+#pragma mark - Byte String Tests (MT 2)
+
+- (void)testByteStringEncoding {
+    NSArray *testVectors =
+        @[@[[NSData data], [NSData dataWithBytes:(UInt8[]){0x40} length:1]],
+          @[[NSData dataWithBytes:(UInt8[]){0x01, 0x02, 0x03, 0x04} length:4], [NSData dataWithBytes:(UInt8[]){0x44, 0x01, 0x02, 0x03, 0x04} length:5]]
+          ];
+
+    for (NSArray *testEntry in testVectors) {
+        NSData *data = ((NSData *)testEntry[0]);
+        YKFCBORByteString *cborByteString = YKFCBORByteString(data);
+        
+        NSData *encodedData = [YKFCBOREncoder encodeByteString:cborByteString];
+        NSData *expectedEncodedData = (NSData *)testEntry[1];
+        
+        XCTAssert([encodedData isEqualToData:expectedEncodedData], @"Data encoding does not match for byte string (%@).", encodedData.description);
+    }
+}
+
+#pragma mark - Text String Tests (MT 3)
+
+- (void)testTextStringEncoding {
+    NSArray *testVectors =
+        @[@[@"", [NSData dataWithBytes:(UInt8[]){0x60} length:1]],
+          @[@"a", [NSData dataWithBytes:(UInt8[]){0x61, 0x61} length:2]],
+          @[@"IETF", [NSData dataWithBytes:(UInt8[]){0x64, 0x49, 0x45, 0x54, 0x46} length:5]],
+          @[@"\"\\", [NSData dataWithBytes:(UInt8[]){0x62, 0x22, 0x5C} length:3]],
+          @[@"ü", [NSData dataWithBytes:(UInt8[]){0x62, 0xC3, 0xBC} length:3]],
+          @[@"水", [NSData dataWithBytes:(UInt8[]){0x63, 0xE6, 0xB0, 0xB4} length:4]],
+          @[@"𐅑", [NSData dataWithBytes:(UInt8[]){0x64, 0xF0, 0x90, 0x85, 0x91} length:5]]
+          ];
+
+    for (NSArray *testEntry in testVectors) {
+        NSString *string = ((NSString *)testEntry[0]);
+        YKFCBORTextString *cborTextString = YKFCBORTextString(string);
+        
+        NSData *encodedData = [YKFCBOREncoder encodeTextString:cborTextString];
+        NSData *expectedEncodedData = (NSData *)testEntry[1];
+        
+        XCTAssert([encodedData isEqualToData:expectedEncodedData], @"Data encoding does not match for text string (%@).", string);
+    }    
+}
+
+#pragma mark - Array Tests (MT 4)
+
+- (void)testArrayEncoding {
+    YKFCBORInteger *int1 = YKFCBORInteger(1);
+    YKFCBORInteger *int2 = YKFCBORInteger(2);
+    YKFCBORInteger *int3 = YKFCBORInteger(3);
+    YKFCBORInteger *int4 = YKFCBORInteger(4);
+    YKFCBORInteger *int5 = YKFCBORInteger(5);
+    
+    YKFCBORTextString *stringA = YKFCBORTextString(@"a");
+    YKFCBORTextString *stringB = YKFCBORTextString(@"b");
+    YKFCBORTextString *stringC = YKFCBORTextString(@"c");
+    
+    NSArray *testVectors =
+        @[@[@[], [NSData dataWithBytes:(UInt8[]){0x80} length:1]],
+          @[@[int1, int2, int3],
+            [NSData dataWithBytes:(UInt8[]){0x83, 0x01, 0x02, 0x03} length:4]],
+          @[@[int1, [YKFCBORArray cborArrayWithValue:@[int2, int3]], [YKFCBORArray cborArrayWithValue:@[int4, int5]]],
+            [NSData dataWithBytes:(UInt8[]){0x83, 0x01, 0x82, 0x02, 0x03, 0x82, 0x04, 0x05} length:8]],
+          @[@[stringA, [YKFCBORMap cborMapWithValue:@{stringB: stringC}]],
+            [NSData dataWithBytes:(UInt8[]){0x82, 0x61, 0x61, 0xA1, 0x61, 0x62, 0x61, 0x63} length:8]]
+          ];
+    
+    for (NSArray *testEntry in testVectors) {
+        NSArray *array = ((NSArray *)testEntry[0]);
+        YKFCBORArray *cborArray = YKFCBORArray(array);
+        
+        NSData *encodedData = [YKFCBOREncoder encodeArray:cborArray];
+        NSData *expectedEncodedData = (NSData *)testEntry[1];
+        
+        XCTAssert([encodedData isEqualToData:expectedEncodedData], @"Data encoding does not match for array.");
+    }
+}
+
+#pragma mark - Map Tests (MT 5)
+
+- (void)testMapEncoding {
+    YKFCBORInteger *int1 = YKFCBORInteger(1);
+    YKFCBORInteger *int2 = YKFCBORInteger(2);
+    YKFCBORInteger *int3 = YKFCBORInteger(3);
+    YKFCBORInteger *int4 = YKFCBORInteger(4);
+    
+    YKFCBORTextString *stringA = YKFCBORTextString(@"a");
+    YKFCBORTextString *stringB = YKFCBORTextString(@"b");
+    
+    NSArray *testVectors =
+        @[@[@{}, [NSData dataWithBytes:(UInt8[]){0xA0} length:1]],
+          @[@{int1: int2, int3: int4},
+            [NSData dataWithBytes:(UInt8[]){0xA2, 0x01, 0x02, 0x03, 0x04} length:5]],
+          @[@{stringA: int1, stringB: [YKFCBORArray cborArrayWithValue:@[int2, int3]]},
+            [NSData dataWithBytes:(UInt8[]){0xA2, 0x61, 0x61, 0x01, 0x61, 0x62, 0x82, 0x02, 0x03} length:9]],
+          @[@{stringA: int1, stringB: [YKFCBORArray cborArrayWithValue:@[int2, int3]]},
+            [NSData dataWithBytes:(UInt8[]){0xA2, 0x61, 0x61, 0x01, 0x61, 0x62, 0x82, 0x02, 0x03} length:9]]
+          ];
+
+    for (NSArray *testEntry in testVectors) {
+        NSDictionary *dictionary = ((NSDictionary *)testEntry[0]);
+        YKFCBORMap *cborMap = YKFCBORMap(dictionary);
+        
+        NSData *encodedData = [YKFCBOREncoder encodeMap:cborMap];
+        NSData *expectedEncodedData = (NSData *)testEntry[1];
+        
+        XCTAssert([encodedData isEqualToData:expectedEncodedData], @"Data encoding does not match for map.");
+    }
+}
+
+- (void)testMapKeysSorting {
+    YKFCBORInteger *int1 = YKFCBORInteger(1);
+    YKFCBORInteger *int2 = YKFCBORInteger(2);
+    YKFCBORInteger *int3 = YKFCBORInteger(3);
+    YKFCBORInteger *int4 = YKFCBORInteger(4);
+    
+    YKFCBORTextString *stringA = YKFCBORTextString(@"b");
+    YKFCBORTextString *stringB = YKFCBORTextString(@"aa");
+    YKFCBORTextString *stringC = YKFCBORTextString(@"bb");
+    YKFCBORTextString *stringD = YKFCBORTextString(@"aaa");
+    
+    NSDictionary *testMap = @{stringD: int1, stringC: int2, stringB: int3, stringA: int4}; // reversed order keys
+    YKFCBORMap *cborMap = YKFCBORMap(testMap);
+    
+    NSData *encodedData = [YKFCBOREncoder encodeMap:cborMap];
+    
+    UInt8 bytes[] = {0xa4, 0x61, 0x62, 0x04, 0x62, 0x61, 0x61, 0x03, 0x62, 0x62, 0x62, 0x02, 0x63, 0x61, 0x61, 0x61, 0x01};
+    NSData *expectedData = [NSData dataWithBytes:bytes length:17];
+    
+    XCTAssert([encodedData isEqualToData:expectedData], @"The encoded data does not match the content and the required CTAP2 order.");
+}
+
+#pragma mark - Bool Tests
+
+- (void)testBoolEncoding {
+    NSData *trueEncoded = [YKFCBOREncoder encodeBool: YKFCBORBool(YES)];
+    NSData *falseEncoded = [YKFCBOREncoder encodeBool: YKFCBORBool(NO)];
+    
+    XCTAssert([trueEncoded isEqualToData:[NSData dataWithBytes:(UInt8[]){0xF5} length:1]]);
+    XCTAssert([falseEncoded isEqualToData:[NSData dataWithBytes:(UInt8[]){0xF4} length:1]]);
+}
+
+@end
diff --git a/YubiKit/YubiKitTests/Tests/YKFKeyRawCommandServiceTests.m b/YubiKit/YubiKitTests/Tests/YKFKeyRawCommandServiceTests.m
new file mode 100755
index 000000000..0afa4df3d
--- /dev/null
+++ b/YubiKit/YubiKitTests/Tests/YKFKeyRawCommandServiceTests.m
@@ -0,0 +1,136 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <XCTest/XCTest.h>
+#import "YKFTestCase.h"
+#import "YKFKeyRawCommandService.h"
+#import "YKFKeyRawCommandService+Private.h"
+#import "FakeYKFKeyConnectionController.h"
+#import "YKFAPDU+Private.h"
+
+@interface YKFKeyRawCommandServiceTests: YKFTestCase
+
+@property (nonatomic) FakeYKFKeyConnectionController *keyConnectionController;
+@property (nonatomic) YKFKeyRawCommandService *rawCommandService;
+
+
+@end
+
+@implementation YKFKeyRawCommandServiceTests
+
+- (void)setUp {
+    self.keyConnectionController = [[FakeYKFKeyConnectionController alloc] init];
+    self.rawCommandService = [[YKFKeyRawCommandService alloc] initWithConnectionController:self.keyConnectionController];
+}
+
+#pragma mark - Sync commands
+
+- (void)test_WhenRunningSyncRawCommandsAgainstTheKey_CommandsAreForwardedToTheKey {
+    NSData *command = [self dataWithBytes:@[@(0x01), @(0x02)]];
+    NSData *commandResponse = [self dataWithBytes:@[@(0x90), @(0x00)]];
+    self.keyConnectionController.commandExecutionResponseDataSequence = @[commandResponse];
+    
+    NSData *responseData = [self executeSyncCommand:command];
+    XCTAssertNotNil(responseData);
+    
+    YKFAPDU *executionCommand = self.keyConnectionController.executionCommand;
+    XCTAssert([executionCommand.apduData isEqualToData:command], @"Command sent to the key does not match the initial command.");
+}
+
+- (void)test_WhenRunningSyncRawCommandsAgainstTheKey_StatusCodesAreReturned {
+    NSData *command = [self dataWithBytes:@[@(0x01), @(0x02)]];
+    NSData *commandResponse = [self dataWithBytes:@[@(0x90), @(0x00)]];
+    self.keyConnectionController.commandExecutionResponseDataSequence = @[commandResponse];
+    
+    NSData *responseData = [self executeSyncCommand:command];
+    
+    XCTAssertEqual(responseData.length, 2, @"Response data too short.");
+    XCTAssert([responseData isEqualToData:commandResponse]);
+}
+
+#pragma mark - Async commands
+
+- (void)test_WhenRunningAsyncRawCommandsAgainstTheKey_CommandsAreForwardedToTheKey {
+    NSData *command = [self dataWithBytes:@[@(0x01), @(0x02)]];
+    NSData *commandResponse = [self dataWithBytes:@[@(0x90), @(0x00)]];
+    self.keyConnectionController.commandExecutionResponseDataSequence = @[commandResponse];
+    
+    NSData *responseData = [self executeAsyncCommand:command];
+    XCTAssertNotNil(responseData);
+    
+    YKFAPDU *executionCommand = self.keyConnectionController.executionCommand;
+    XCTAssert([executionCommand.apduData isEqualToData:command], @"Command sent to the key does not match the initial command.");
+}
+
+- (void)test_WhenRunningAsyncRawCommandsAgainstTheKey_StatusCodesAreReturned {
+    NSData *command = [self dataWithBytes:@[@(0x01), @(0x02)]];
+    NSData *commandResponse = [self dataWithBytes:@[@(0x90), @(0x00)]];
+    self.keyConnectionController.commandExecutionResponseDataSequence = @[commandResponse];
+    
+    NSData *responseData = [self executeAsyncCommand:command];
+    
+    XCTAssertEqual(responseData.length, 2, @"Response data too short.");
+    XCTAssert([responseData isEqualToData:commandResponse]);
+}
+
+#pragma mark - Helpers
+
+- (NSData *)executeSyncCommand:(NSData *)command {
+    __block BOOL completionBlockExecuted = NO;
+    __block NSData *responseData = nil;
+    
+    XCTestExpectation *expectation = [[XCTestExpectation alloc] initWithDescription:@"Application selection."];
+    
+    dispatch_async(dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0), ^{
+        YKFAPDU *commandAPDU = [[YKFAPDU alloc] initWithData:command];
+        YKFKeyRawCommandServiceResponseBlock completionBlock = ^(NSData *response, NSError *error) {
+            if (error) {
+                return;
+            }            
+            completionBlockExecuted = YES;
+            responseData = response;
+        };
+        [self.rawCommandService executeSyncCommand:commandAPDU completion:completionBlock];
+        [expectation fulfill];
+    });
+    
+    [self waitForTimeInterval:0.2];
+    XCTAssertTrue(completionBlockExecuted, @"Completion block not executed.");
+
+    return responseData;
+}
+
+- (NSData *)executeAsyncCommand:(NSData *)command {
+    __block BOOL completionBlockExecuted = NO;
+    __block NSData *responseData = nil;
+    
+    XCTestExpectation *expectation = [[XCTestExpectation alloc] initWithDescription:@"Application selection."];
+    
+    YKFAPDU *commandAPDU = [[YKFAPDU alloc] initWithData:command];
+    [self.rawCommandService executeCommand:commandAPDU completion:^(NSData *response, NSError *error) {
+        if (error) {
+            return;
+        }
+        completionBlockExecuted = YES;
+        responseData = response;
+        [expectation fulfill];
+    }];
+    
+    [self waitForTimeInterval:0.2];
+    XCTAssertTrue(completionBlockExecuted, @"Completion block not executed.");
+    
+    return responseData;
+}
+
+@end
diff --git a/YubiKit/YubiKitTests/Tests/YKFKeyU2FServiceTests.m b/YubiKit/YubiKitTests/Tests/YKFKeyU2FServiceTests.m
new file mode 100755
index 000000000..af056ce2f
--- /dev/null
+++ b/YubiKit/YubiKitTests/Tests/YKFKeyU2FServiceTests.m
@@ -0,0 +1,285 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <XCTest/XCTest.h>
+
+#import "YKFTestCase.h"
+#import "YKFKeyU2FService.h"
+#import "YKFKeyU2FService+Private.h"
+#import "FakeYKFKeyConnectionController.h"
+
+#import "YKFKeyU2FSignRequest.h"
+#import "YKFKeyU2FRegisterRequest.h"
+
+#import "YKFKeyAPDUError.h"
+#import "YKFKeyU2FError.h"
+
+@interface YKFKeyU2FServiceTests: YKFTestCase
+
+@property (nonatomic) FakeYKFKeyConnectionController *keyConnectionController;
+@property (nonatomic) YKFKeyU2FService *u2fService;
+
+// Predefined U2F params
+@property (nonatomic) NSString *challenge;
+@property (nonatomic) NSString *keyHandle;
+@property (nonatomic) NSString *appId;
+
+@end
+
+@implementation YKFKeyU2FServiceTests
+
+- (void)setUp {
+    [super setUp];
+    
+    self.challenge = @"J3tMC4hiRP9PDQ1M4IsOp8A-_oh6hge0c38CqwiqYmo";
+    self.keyHandle  = @"UiC-Kth0iN3JmoSHFeHPu5M8GUvbhC-Gv8n0q0OBt42F3S1qTZBX81UudCuT29utRQZlTP5QpO_OncQFn5Mjaw";
+    self.appId = @"https://demo.yubico.com";
+    
+    self.keyConnectionController = [[FakeYKFKeyConnectionController alloc] init];
+    self.u2fService = [[YKFKeyU2FService alloc] initWithConnectionController:self.keyConnectionController];
+}
+
+- (void)test_WhenExecutingRegisterRequest_RequestIsForwarededToTheKey {
+    NSData *applicationSelectionResponse = [self dataWithBytes:@[@(0x00), @(0x90), @(0x00)]];
+    NSData *commandResponse = [self dataWithBytes:@[@(0x00), @(0x90), @(0x00)]];
+    self.keyConnectionController.commandExecutionResponseDataSequence = @[applicationSelectionResponse, commandResponse];
+    
+    __block BOOL completionBlockExecuted = NO;
+    YKFKeyU2FRegisterRequest *registerRequest = [[YKFKeyU2FRegisterRequest alloc] initWithChallenge:self.challenge appId:self.appId];
+    XCTestExpectation *expectation = [[XCTestExpectation alloc] initWithDescription:@"U2F"];
+    
+    YKFKeyU2FServiceRegisterCompletionBlock completionBlock = ^(YKFKeyU2FRegisterResponse *response, NSError *error) {
+        completionBlockExecuted = YES;
+        [expectation fulfill];
+    };
+    [self.u2fService executeRegisterRequest:registerRequest completion:completionBlock];
+
+    XCTWaiterResult result = [XCTWaiter waitForExpectations:@[expectation] timeout:10];
+    XCTAssert(result == XCTWaiterResultCompleted, @"");
+
+    XCTAssertNotNil(self.keyConnectionController.executionCommand, @"No command data executed on the connection controller.");
+    XCTAssertTrue(completionBlockExecuted, @"Completion block not executed.");
+}
+
+- (void)test_WhenExecutingSignRequest_RequestIsForwarededToTheKey {
+    NSData *applicationSelectionResponse = [self dataWithBytes:@[@(0x00), @(0x90), @(0x00)]];
+    NSData *commandResponse = [self dataWithBytes:@[@(0x00), @(0x90), @(0x00)]];
+    self.keyConnectionController.commandExecutionResponseDataSequence = @[applicationSelectionResponse, commandResponse];
+
+    __block BOOL completionBlockExecuted = NO;
+    YKFKeyU2FSignRequest *signRequest = [[YKFKeyU2FSignRequest alloc] initWithChallenge:self.challenge keyHandle:self.keyHandle appId:self.appId];
+    XCTestExpectation *expectation = [[XCTestExpectation alloc] initWithDescription:@"U2F"];
+    
+    YKFKeyU2FServiceSignCompletionBlock completionBlock = ^(YKFKeyU2FSignResponse *response, NSError *error) {
+        completionBlockExecuted = YES;
+        [expectation fulfill];
+    };
+    [self.u2fService executeSignRequest:signRequest completion:completionBlock];
+
+    XCTWaiterResult result = [XCTWaiter waitForExpectations:@[expectation] timeout:10];
+    XCTAssert(result == XCTWaiterResultCompleted, @"");
+    
+    XCTAssertNotNil(self.keyConnectionController.executionCommand, @"No command data executed on the connection controller.");
+    XCTAssertTrue(completionBlockExecuted, @"Completion block not executed.");
+}
+
+#pragma mark - Generic Error Tests
+
+- (void)test_WhenExecutingRegisterRequestWithStatusErrorResponse_ErrorIsReceivedBack {
+    NSData *applicationSelectionResponse = [self dataWithBytes:@[@(0x00), @(0x90), @(0x00)]];
+    NSData *errorResponse = [self dataWithBytes:@[@(0x00), @(0x6A), @(0x88)]];
+    NSUInteger expectedErrorCode = 0x6A88;
+    
+    self.keyConnectionController.commandExecutionResponseDataSequence = @[applicationSelectionResponse, errorResponse];
+    
+    __block BOOL errorReceived = NO;
+    YKFKeyU2FRegisterRequest *registerRequest = [[YKFKeyU2FRegisterRequest alloc] initWithChallenge:self.challenge appId:self.appId];
+    XCTestExpectation *expectation = [[XCTestExpectation alloc] initWithDescription:@"U2F"];
+    
+    YKFKeyU2FServiceRegisterCompletionBlock completionBlock = ^(YKFKeyU2FRegisterResponse *response, NSError *error) {
+        errorReceived = error.code == expectedErrorCode;
+        [expectation fulfill];
+    };
+    [self.u2fService executeRegisterRequest:registerRequest completion:completionBlock];
+    
+    XCTWaiterResult result = [XCTWaiter waitForExpectations:@[expectation] timeout:10];
+    XCTAssert(result == XCTWaiterResultCompleted, @"");
+    
+    XCTAssertTrue(errorReceived, @"Status error not received back.");
+}
+
+- (void)test_WhenExecutingSignRequestWithStatusErrorResponse_ErrorIsReceivedBack {
+    NSData *applicationSelectionResponse = [self dataWithBytes:@[@(0x00), @(0x90), @(0x00)]];
+    NSData *errorResponse = [self dataWithBytes:@[@(0x00), @(0x69), @(0x84)]];
+    NSUInteger expectedErrorCode = 0x6984;
+    
+    self.keyConnectionController.commandExecutionResponseDataSequence = @[applicationSelectionResponse, errorResponse];
+    __block BOOL errorReceived = NO;
+    XCTestExpectation *expectation = [[XCTestExpectation alloc] initWithDescription:@"U2F"];
+    
+    YKFKeyU2FSignRequest *signRequest = [[YKFKeyU2FSignRequest alloc] initWithChallenge:self.challenge keyHandle:self.keyHandle appId:self.appId];
+    YKFKeyU2FServiceSignCompletionBlock completionBlock = ^(YKFKeyU2FSignResponse *response, NSError *error) {
+        errorReceived = error.code == expectedErrorCode;
+        [expectation fulfill];
+    };
+    
+    [self.u2fService executeSignRequest:signRequest completion:completionBlock];
+
+    XCTWaiterResult result = [XCTWaiter waitForExpectations:@[expectation] timeout:10];
+    XCTAssert(result == XCTWaiterResultCompleted, @"");
+    
+    XCTAssertTrue(errorReceived, @"Status error not received back.");
+}
+
+- (void)test_WhenExecutingSignRequestWithKnownStatusErrorResponse_ErrorIsReceivedBack {
+    NSArray *listOfErrorStatusCodes = @[
+        @[@(0x00), @(0x69), @(0x84), @(YKFKeyAPDUErrorCodeDataInvalid)],
+        @[@(0x00), @(0x67), @(0x00), @(YKFKeyAPDUErrorCodeWrongLength)],
+        @[@(0x00), @(0x6E), @(0x00), @(YKFKeyAPDUErrorCodeCLANotSupported)],
+        @[@(0x00), @(0x6F), @(0x00), @(YKFKeyAPDUErrorCodeUnknown)]
+    ];
+    
+    for (NSArray *statusCode in listOfErrorStatusCodes) {
+        NSData *applicationSelectionResponse = [self dataWithBytes:@[@(0x00), @(0x90), @(0x00)]];
+        NSData *errorResponse = [self dataWithBytes:@[statusCode[0], statusCode[1], statusCode[2]]];
+        int expectedErrorCode = [statusCode[3] intValue];
+        
+        self.keyConnectionController.commandExecutionResponseDataSequence = @[applicationSelectionResponse, errorResponse];
+        
+        __block BOOL errorReceived = NO;
+        YKFKeyU2FSignRequest *signRequest = [[YKFKeyU2FSignRequest alloc] initWithChallenge:self.challenge keyHandle:self.keyHandle appId:self.appId];
+        XCTestExpectation *expectation = [[XCTestExpectation alloc] initWithDescription:@"U2F"];
+        
+        YKFKeyU2FServiceSignCompletionBlock completionBlock = ^(YKFKeyU2FSignResponse *response, NSError *error) {
+            errorReceived = error.code == expectedErrorCode;
+            [expectation fulfill];
+        };
+        
+        [self.u2fService executeSignRequest:signRequest completion:completionBlock];
+
+        XCTWaiterResult result = [XCTWaiter waitForExpectations:@[expectation] timeout:10];
+        XCTAssert(result == XCTWaiterResultCompleted, @"");
+        
+        XCTAssertTrue(errorReceived, @"Status error not received back.");
+    }
+}
+
+- (void)test_WhenExecutingU2FRequestWithU2FDisabled_DisabledApplicationErrorIsReceivedBack {
+    NSArray *listOfErrorStatusCodes = @[
+        @[@(0x00), @(0x6D), @(0x00), @(YKFKeySessionErrorMissingApplicationCode)], // Ins Not Supported
+        @[@(0x00), @(0x6A), @(0x82), @(YKFKeySessionErrorMissingApplicationCode)]  // Missing file
+    ];
+    
+    for (NSArray *statusCode in listOfErrorStatusCodes) {
+        NSData *applicationSelectionResponse = [self dataWithBytes:@[@(0x00), @(0x90), @(0x00)]];
+        if ([statusCode[1] intValue] == 0x6A) { // Missing file 
+            applicationSelectionResponse = [self dataWithBytes:@[statusCode[0], statusCode[1], statusCode[2]]];
+        }
+        
+        NSData *errorResponse = [self dataWithBytes:@[statusCode[0], statusCode[1], statusCode[2]]];
+        int expectedErrorCode = [statusCode[3] intValue];
+        
+        self.keyConnectionController.commandExecutionResponseDataSequence = @[applicationSelectionResponse, errorResponse];
+        
+        __block BOOL errorReceived = NO;
+        YKFKeyU2FSignRequest *signRequest = [[YKFKeyU2FSignRequest alloc] initWithChallenge:self.challenge keyHandle:self.keyHandle appId:self.appId];
+        XCTestExpectation *expectation = [[XCTestExpectation alloc] initWithDescription:@"U2F"];
+        
+        YKFKeyU2FServiceSignCompletionBlock completionBlock = ^(YKFKeyU2FSignResponse *response, NSError *error) {
+            errorReceived = error.code == expectedErrorCode;
+            [expectation fulfill];
+        };
+        
+        [self.u2fService executeSignRequest:signRequest completion:completionBlock];
+        
+        XCTWaiterResult result = [XCTWaiter waitForExpectations:@[expectation] timeout:10];
+        XCTAssert(result == XCTWaiterResultCompleted, @"");
+        
+        XCTAssertTrue(errorReceived, @"Disabled application error not received back.");
+    }
+}
+
+#pragma mark - Mapped Error Tests
+
+- (void)test_WhenExecutingSignRequestWithoutRegistration_MappedErrorIsReceivedBack {
+    NSData *applicationSelectionResponse = [self dataWithBytes:@[@(0x00), @(0x90), @(0x00)]];
+    NSData *errorResponse = [self dataWithBytes:@[@(0x00), @(0x6A), @(0x80)]]; // Wrong data code
+    NSUInteger expectedErrorCode = YKFKeyU2FErrorCodeU2FSigningUnavailable;
+    
+    self.keyConnectionController.commandExecutionResponseDataSequence = @[applicationSelectionResponse, errorResponse];
+    
+    __block BOOL errorReceived = NO;
+    YKFKeyU2FSignRequest *signRequest = [[YKFKeyU2FSignRequest alloc] initWithChallenge:self.challenge keyHandle:self.keyHandle appId:self.appId];
+    XCTestExpectation *expectation = [[XCTestExpectation alloc] initWithDescription:@"U2F"];
+    
+    YKFKeyU2FServiceSignCompletionBlock completionBlock = ^(YKFKeyU2FSignResponse *response, NSError *error) {
+        errorReceived = error.code == expectedErrorCode;
+        [expectation fulfill];
+    };
+    [self.u2fService executeSignRequest:signRequest completion:completionBlock];
+    
+    XCTWaiterResult result = [XCTWaiter waitForExpectations:@[expectation] timeout:10];
+    XCTAssert(result == XCTWaiterResultCompleted, @"");
+    
+    XCTAssertTrue(errorReceived, @"Status error not received back.");
+}
+
+#pragma mark - Key State Tests
+
+- (void)test_WhenExecutingRegisterRequestWithTouchRequired_KeyStateIsUpdatingToTouchKey {
+    NSData *applicationSelectionResponse = [self dataWithBytes:@[@(0x00), @(0x90), @(0x00)]];
+    NSData *errorResponse = [self dataWithBytes:@[@(0x00), @(0x69), @(0x85)]]; // Condition not satisified - touch the key
+    NSData *successResponse = [self dataWithBytes:@[@(0x00), @(0x90), @(0x00)]];
+    
+    self.keyConnectionController.commandExecutionResponseDataSequence = @[applicationSelectionResponse, errorResponse, successResponse];
+    
+    YKFKeyU2FRegisterRequest *registerRequest = [[YKFKeyU2FRegisterRequest alloc] initWithChallenge:self.challenge appId:self.appId];
+    YKFKeyU2FServiceRegisterCompletionBlock completionBlock = ^(YKFKeyU2FRegisterResponse *response, NSError *error) {};
+    [self.u2fService executeRegisterRequest:registerRequest completion:completionBlock];
+    
+    [self waitForTimeInterval:0.3]; // give time to update the property
+    
+    YKFKeyU2FServiceKeyState keyState = self.u2fService.keyState;
+    
+    XCTAssertTrue(keyState == YKFKeyU2FServiceKeyStateTouchKey, @"The keys state did not update to touch key.");
+}
+
+- (void)disabled_test_WhenExecutingSignRequestWithTouchRequired_KeyStateIsUpdatingToTouchKey {
+    NSData *applicationSelectionResponse = [self dataWithBytes:@[@(0x00), @(0x90), @(0x00)]];
+    NSData *errorResponse = [self dataWithBytes:@[@(0x00), @(0x69), @(0x85)]]; // Condition not satisified - touch the key
+    
+    self.keyConnectionController.commandExecutionResponseDataSequence = @[applicationSelectionResponse, errorResponse];
+    
+    YKFKeyU2FSignRequest *signRequest = [[YKFKeyU2FSignRequest alloc] initWithChallenge:self.challenge keyHandle:self.keyHandle appId:self.appId];
+    XCTestExpectation *expectation = [[XCTestExpectation alloc] initWithDescription:@"U2F"];
+    
+    YKFKeyU2FServiceSignCompletionBlock completionBlock = ^(YKFKeyU2FSignResponse *response, NSError *error) {
+        [expectation fulfill];
+    };
+    [self.u2fService executeSignRequest:signRequest completion:completionBlock];
+    
+    XCTWaiterResult result = [XCTWaiter waitForExpectations:@[expectation] timeout:10];
+    XCTAssert(result == XCTWaiterResultCompleted, @"");
+    
+    YKFKeyU2FServiceKeyState keyState = self.u2fService.keyState;
+    
+    XCTAssertTrue(keyState == YKFKeyU2FServiceKeyStateTouchKey, @"The keys state did not update to touch key.");
+}
+
+- (void)test_WhenNoRequestWasSentToTheKey_KeyStateIsIdle {
+    YKFKeyU2FServiceKeyState keyState = self.u2fService.keyState;
+    XCTAssertTrue(keyState == YYKFKeyU2FServiceKeyStateIdle, @"The keys state idle when the service does not execute a request.");
+}
+
+@end
diff --git a/YubiKit/YubiKitTests/Tests/YKFNFCOTPServiceTests.m b/YubiKit/YubiKitTests/Tests/YKFNFCOTPServiceTests.m
new file mode 100755
index 000000000..d7a8c1ff5
--- /dev/null
+++ b/YubiKit/YubiKitTests/Tests/YKFNFCOTPServiceTests.m
@@ -0,0 +1,96 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <XCTest/XCTest.h>
+
+#import "YKFTestCase.h"
+#import "YKFNFCOTPService.h"
+#import "YKFNFCOTPService+Private.h"
+#import "YKFOTPTokenParser.h"
+#import "YubiKitDeviceCapabilities+Testing.h"
+#import "FakeNFCNDEFReaderSession.h"
+#import "FakeYubiKitDeviceCapabilities.h"
+
+@interface YKFNFCOTPServiceTests: YKFTestCase
+
+@property (nonatomic) YKFNFCOTPService *nfcOtpService;
+@property (nonatomic) FakeNFCNDEFReaderSession *fakeReaderSession;
+@property (nonatomic) YKFOTPTokenParser *tokenParser;
+
+@end
+
+@implementation YKFNFCOTPServiceTests
+
+- (void)setUp {
+    [super setUp];
+    
+    YubiKitDeviceCapabilities.fakeDeviceCapabilities = [[FakeYubiKitDeviceCapabilities alloc] init];
+    
+    self.tokenParser = [[YKFOTPTokenParser alloc] init];
+    self.fakeReaderSession = [[FakeNFCNDEFReaderSession alloc] init];
+    self.nfcOtpService = [[YKFNFCOTPService alloc] initWithTokenParser:self.tokenParser session:self.fakeReaderSession];
+    
+    self.fakeReaderSession.delegate = (id<NFCNDEFReaderSessionDelegate>)self.nfcOtpService;
+}
+
+- (void)tearDown {    
+    YubiKitDeviceCapabilities.fakeDeviceCapabilities = nil;
+    
+    self.tokenParser = nil;
+    self.fakeReaderSession = nil;
+    self.nfcOtpService = nil;
+    
+    [super tearDown];
+}
+
+#pragma mark - Error handlig
+
+- (void)test_WhenNFCSessionInvalidatesWithError_ErrorIsReceived {
+    __block BOOL errorReceived = NO;
+    [self.nfcOtpService requestOTPToken:^(id<YKFOTPTokenProtocol> token, NSError *error) {
+        if (error) {
+            errorReceived = YES;
+        }
+    }];
+    
+    // Fire a random error
+    NSError *error = [NSError errorWithDomain:@"" code:1 userInfo:nil];
+    [self.fakeReaderSession.delegate readerSession:(NFCNDEFReaderSession *)self.fakeReaderSession didInvalidateWithError:error];
+    
+    XCTAssert(errorReceived, @"Error not correctly propagated.");
+}
+
+- (void)test_WhenSessionIsInvalidatedAfterFirstRead_ErrorIsSilenced {
+    __block BOOL errorReceived = NO;
+    [self.nfcOtpService requestOTPToken:^(id<YKFOTPTokenProtocol> token, NSError *error) {
+        if (error.code == NFCReaderSessionInvalidationErrorFirstNDEFTagRead) {
+            errorReceived = YES;
+        }
+    }];
+    
+    // Fire the error
+    NSError *error = [NSError errorWithDomain:@"" code:NFCReaderSessionInvalidationErrorFirstNDEFTagRead userInfo:nil];
+    [self.fakeReaderSession.delegate readerSession:(NFCNDEFReaderSession *)self.fakeReaderSession didInvalidateWithError:error];
+    
+    XCTAssert(!errorReceived, @"Error not correctly silenced.");
+}
+
+#pragma mark - Other
+
+- (void)test_WhenRequestingOTPToken_TheNFCSessionIsStarted {
+    [self.nfcOtpService requestOTPToken:^(id<YKFOTPTokenProtocol> token, NSError *error) {}];
+    XCTAssert(self.fakeReaderSession.sessionStarted, @"NFC session is not started when requesting an OTP token.");
+}
+
+@end
diff --git a/YubiKit/YubiKitTests/Tests/YKFOATHCredentialTests.m b/YubiKit/YubiKitTests/Tests/YKFOATHCredentialTests.m
new file mode 100755
index 000000000..be346943b
--- /dev/null
+++ b/YubiKit/YubiKitTests/Tests/YKFOATHCredentialTests.m
@@ -0,0 +1,277 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <XCTest/XCTest.h>
+
+#import "YKFTestCase.h"
+#import "YKFOATHCredential.h"
+#import "YKFOATHCredential+Private.h"
+
+@interface YKFOATHCredentialTests : XCTestCase
+@end
+
+@implementation YKFOATHCredentialTests
+
+#pragma mark - Basic valid URL tests
+
+- (void)test_WhenCredentialIsCreatedWithValidTOTPURL_CredentialIsNotNil {
+    NSString *url = @"otpauth://totp/ACME:john@example.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&issuer=ACME&algorithm=SHA1&digits=6&period=30";
+    YKFOATHCredential *credential = [[YKFOATHCredential alloc] initWithURL:[NSURL URLWithString:url]];
+    XCTAssertNotNil(credential, @"Valid TOTP url was not parsed correctly");
+}
+
+- (void)test_WhenCredentialIsCreatedWithValidHOTPURL_CredentialIsNotNil {
+    NSString *url = @"otpauth://hotp/ACME:john@example.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&issuer=ACME&algorithm=SHA1&digits=6&counter=1234";
+    YKFOATHCredential *credential = [[YKFOATHCredential alloc] initWithURL:[NSURL URLWithString:url]];
+    XCTAssertNotNil(credential, @"Valid HOTP url was not parsed correctly");
+}
+
+#pragma mark - HOTP URLs tests
+
+- (void)test_WhenCredentialIsCreatedWithHOTPURL_CredentialTypeIsHOTP {
+    NSString *url = @"otpauth://hotp/ACME:john@example.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&issuer=ACME&algorithm=SHA1&digits=6&counter=1234";
+    YKFOATHCredential *credential = [[YKFOATHCredential alloc] initWithURL:[NSURL URLWithString:url]];
+    XCTAssert(credential.type == YKFOATHCredentialTypeHOTP, @"Credential type incorrectly detected.");
+}
+
+- (void)test_WhenCredentialIsCreatedWithHOTPURLWithoutCounter_CredentialIsNil {
+    NSString *url = @"otpauth://hotp/ACME:john@example.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&issuer=ACME&algorithm=SHA1&digits=6";
+    YKFOATHCredential *credential = [[YKFOATHCredential alloc] initWithURL:[NSURL URLWithString:url]];
+    XCTAssertNil(credential, @"HOTP credential is not nil when counter is missing.");
+}
+
+- (void)test_WhenCredentialIsCreatedWithValidHOTPURL_PeriodIsZero {
+    NSString *url = @"otpauth://hotp/ACME:john@example.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&issuer=ACME&algorithm=SHA1&digits=6&counter=1234";
+    YKFOATHCredential *credential = [[YKFOATHCredential alloc] initWithURL:[NSURL URLWithString:url]];
+    XCTAssert(credential.period == 0, @"HOTP credential has a validity period.");
+}
+
+- (void)test_WhenCredentialIsCreatedWithHOTPURL_DefaultNameIsLabel {
+    NSString *url = @"otpauth://hotp/ACME:john@example.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&issuer=ACME&algorithm=SHA1&digits=6&counter=123";
+    YKFOATHCredential *credential = [[YKFOATHCredential alloc] initWithURL:[NSURL URLWithString:url]];
+    
+    XCTAssert([credential.key isEqualToString:credential.label], @"Credential key not correctly generated");
+}
+
+#pragma mark - TOTP URLs tests
+
+- (void)test_WhenCredentialIsCreatedWithTOTPURL_CredentialParametersAreCorrectlyParsed {
+    NSString *url = @"otpauth://totp/ACME:john@example.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&issuer=ACME&algorithm=SHA1&digits=6&period=40";
+    YKFOATHCredential *credential = [[YKFOATHCredential alloc] initWithURL:[NSURL URLWithString:url]];
+    
+    XCTAssert(credential.type == YKFOATHCredentialTypeTOTP, @"");
+    XCTAssert(credential.algorithm == YKFOATHCredentialAlgorithmSHA1, @"");
+    XCTAssert([credential.issuer isEqualToString:@"ACME"], @"");
+    XCTAssertNotNil(credential.secret, @"");
+    XCTAssert(credential.digits == 6, @"");
+    XCTAssert(credential.period == 40, @"");
+}
+
+- (void)test_WhenCredentialIsCreatedWithTOTPURL_CredentialTypeIsTOTP {
+    NSString *url = @"otpauth://totp/ACME:john@example.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&issuer=ACME&algorithm=SHA1&digits=6&period=30";
+    YKFOATHCredential *credential = [[YKFOATHCredential alloc] initWithURL:[NSURL URLWithString:url]];
+    XCTAssert(credential.type == YKFOATHCredentialTypeTOTP, @"Credential type incorrectly detected.");
+}
+
+- (void)test_WhenCredentialIsCreatedWithTOTPURLWithoutPeriod_CredentialPeriodIsDefault {
+    NSString *url = @"otpauth://totp/ACME:john@example.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&issuer=ACME&algorithm=SHA1&digits=6";
+    YKFOATHCredential *credential = [[YKFOATHCredential alloc] initWithURL:[NSURL URLWithString:url]];
+    XCTAssert(credential.period == 30, @"Credential period is not default.");
+}
+
+- (void)test_WhenCredentialIsCreatedWithTOTPURL_NameDoesNotContainThePeriodWhenDefault {
+    NSString *url = @"otpauth://totp/ACME:john@example.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&issuer=ACME&algorithm=SHA1&digits=6&period=30";
+    YKFOATHCredential *credential = [[YKFOATHCredential alloc] initWithURL:[NSURL URLWithString:url]];
+    
+    NSString *credentialName =credential.label;
+    XCTAssert([credential.key isEqualToString:credentialName], @"Credential key not correctly generated");
+}
+
+- (void)test_WhenCredentialIsCreatedWithTOTPURL_NameContainsThePeriodWhenNotDefault {
+    NSString *url = @"otpauth://totp/ACME:john@example.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&issuer=ACME&algorithm=SHA1&digits=6&period=40";
+    YKFOATHCredential *credential = [[YKFOATHCredential alloc] initWithURL:[NSURL URLWithString:url]];
+    
+    NSString *credentialName = [NSString stringWithFormat:@"%ld/%@", credential.period, credential.label];
+    XCTAssert([credential.key isEqualToString:credentialName], @"Credential key not correctly generated");
+}
+
+- (void)test_WhenCredentialIsCreatedWithTOTPURL_SmallerThanDefaultPeriodsAreAccepted {
+    NSString *url = @"otpauth://totp/ACME:john@example.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&issuer=ACME&algorithm=SHA1&digits=6&period=20";
+    YKFOATHCredential *credential = [[YKFOATHCredential alloc] initWithURL:[NSURL URLWithString:url]];
+    
+    XCTAssert(credential.period == 20, @"Credential period not correctly parsed.");
+}
+
+- (void)test_WhenCredentialIsCreatedWithTOTPURL_PeriodIsDefaultIfNotProvided {
+    NSString *url = @"otpauth://totp/ACME:john@example.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&issuer=ACME&algorithm=SHA1&digits=6";
+    YKFOATHCredential *credential = [[YKFOATHCredential alloc] initWithURL:[NSURL URLWithString:url]];
+    
+    XCTAssert(credential.period == 30, @"Default period was not correctly set.");
+}
+
+#pragma mark - Issuer
+
+- (void)test_WhenCredentialIsCreatedWithURLWithoutIssuerInURLParam_IssuerIsParsedFromTheLabel {
+    NSString *url = @"otpauth://totp/ACME:john@example.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&digits=6&period=30";
+    YKFOATHCredential *credential = [[YKFOATHCredential alloc] initWithURL:[NSURL URLWithString:url]];
+    XCTAssert([credential.issuer isEqualToString:@"ACME"], @"Credential is missing the issuer.");
+}
+
+- (void)test_WhenCredentialIsCreatedWithURLWithIssuerInURLParamButNotInLabel_IssuerIsParsedFromTheURLl {
+    NSString *url = @"otpauth://totp/john@example.com?issuer=ACME&secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&digits=6&period=30";
+    YKFOATHCredential *credential = [[YKFOATHCredential alloc] initWithURL:[NSURL URLWithString:url]];
+    XCTAssert([credential.issuer isEqualToString:@"ACME"], @"Credential is missing the issuer.");
+}
+
+- (void)test_WhenCredentialIsCreatedWithURLWithoutIssuer_CredentialCanBeCreated {
+    NSString *url = @"otpauth://totp/john@example.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&digits=6&period=30";
+    YKFOATHCredential *credential = [[YKFOATHCredential alloc] initWithURL:[NSURL URLWithString:url]];
+    
+    XCTAssertNil(credential.issuer, @"Issuer is not nil when key URI does not contain an issuer.");
+}
+
+#pragma mark - Label
+
+- (void)test_WhenCredentialIsManuallyCreatedWithLabel_AssignedLabelIsReturnedWhenReadingTheProperty {
+    YKFOATHCredential *credential = [[YKFOATHCredential alloc] init];
+    NSString *label = @"issuer:account";
+    
+    credential.label = label;
+    XCTAssert([credential.label isEqualToString:label], @"Credential label is not returned if assigned.");
+}
+
+- (void)test_WhenCredentialIsManuallyCreatedWithoutLabel_LabelIsBuildFromTheIssuerAndAccount {
+    YKFOATHCredential *credential = [[YKFOATHCredential alloc] init];
+    NSString *label = @"issuer:account";
+    
+    credential.issuer = @"issuer";
+    credential.account = @"account";
+    
+    XCTAssert([credential.label isEqualToString:label], @"Credential label is not built if missing.");
+}
+
+- (void)test_WhenCredentialIsManuallyCreatedWithoutLabelAndIssuer_LabelIsBuildFromTheAccount {
+    YKFOATHCredential *credential = [[YKFOATHCredential alloc] init];
+    credential.account = @"account";
+    
+    XCTAssert([credential.label isEqualToString:credential.account], @"Credential label is not built if missing.");
+}
+
+#pragma mark - Key
+
+- (void)test_WhenCredentialIsCreatedWithHOTPURL_KeyIsTheLabel {
+    NSString *url = @"otpauth://hotp/ACME:john@example.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&digits=6&counter=0";
+    YKFOATHCredential *credential = [[YKFOATHCredential alloc] initWithURL:[NSURL URLWithString:url]];
+    XCTAssert([credential.key isEqualToString:credential.label], @"Credential key for HOTP is not the label.");
+}
+
+- (void)test_WhenCredentialIsCreatedWithTOTPURLWithDefaultPeriod_KeyIsTheLabel {
+    NSString *url = @"otpauth://totp/ACME:john@example.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&digits=6";
+    YKFOATHCredential *credential = [[YKFOATHCredential alloc] initWithURL:[NSURL URLWithString:url]];
+    XCTAssert([credential.key isEqualToString:credential.label], @"Credential key for HOTP is not the label.");
+}
+
+- (void)test_WhenCredentialIsCreatedWithTOTPURLWithCustomPeriodAndIssuer_KeyContainsThePeriod {
+    NSString *url = @"otpauth://totp/ACME:john@example.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&digits=6&period=15";
+    YKFOATHCredential *credential = [[YKFOATHCredential alloc] initWithURL:[NSURL URLWithString:url]];
+    
+    NSString *expectedKey = [NSString stringWithFormat:@"%d/%@", 15, credential.label];
+    XCTAssert([credential.key isEqualToString:expectedKey], @"Credential key for TOTP with custom period does not contain the period.");
+}
+
+- (void)test_WhenCredentialIsCreatedWithTOTPURLWithCustomPeriod_KeyContainsThePeriod {
+    NSString *url = @"otpauth://totp/john@example.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&digits=6&period=15";
+    YKFOATHCredential *credential = [[YKFOATHCredential alloc] initWithURL:[NSURL URLWithString:url]];
+    
+    NSString *expectedKey = [NSString stringWithFormat:@"%d/%@", 15, credential.label];
+    XCTAssert([credential.key isEqualToString:expectedKey], @"Credential key for TOTP with custom period does not contain the period.");
+}
+
+#pragma mark - Digits
+
+- (void)test_WhenCredentialIsCreatedWithURLWith7DigitsLength_CredentialParametersAreCorrectlyParsed {
+    NSString *url = @"otpauth://totp/ACME:john@example.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&issuer=ACME&algorithm=SHA1&digits=7&period=40";
+    YKFOATHCredential *credential = [[YKFOATHCredential alloc] initWithURL:[NSURL URLWithString:url]];
+    XCTAssert(credential.digits == 7, @"");
+}
+- (void)test_WhenCredentialIsCreatedWithURLWith8DigitsLength_CredentialParametersAreCorrectlyParsed {
+    NSString *url = @"otpauth://totp/ACME:john@example.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&issuer=ACME&algorithm=SHA1&digits=8&period=40";
+    YKFOATHCredential *credential = [[YKFOATHCredential alloc] initWithURL:[NSURL URLWithString:url]];
+    XCTAssert(credential.digits == 8, @"");
+}
+
+- (void)test_WhenCredentialIsCreatedWithURLWithInvalidDigitsLength_CredentialIsNil {
+    NSString *url = @"otpauth://totp/ACME:john@example.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&issuer=ACME&algorithm=SHA1&digits=10&period=40";
+    YKFOATHCredential *credential = [[YKFOATHCredential alloc] initWithURL:[NSURL URLWithString:url]];
+    XCTAssertNil(credential, @"Credential with invalid digits secret is not nil.");
+}
+
+#pragma mark - Misc
+
+- (void)test_WhenCredentialIsCreatedWithHOTPURLWithoutSecret_CredentialIsNil {
+    NSString *url = @"otpauth://hotp/ACME:john@example.com?issuer=ACME&algorithm=SHA1&digits=6&counter=1234";
+    YKFOATHCredential *credential = [[YKFOATHCredential alloc] initWithURL:[NSURL URLWithString:url]];
+    XCTAssertNil(credential, @"Credential without secret is not nil.");
+}
+
+- (void)test_WhenCredentialIsCreatedWithShortSecret_CredentialSecretIsPadded {
+    NSString *url = @"otpauth://totp/Label?secret=HXDMVJEC&issuer=Issuer";
+    YKFOATHCredential *credential = [[YKFOATHCredential alloc] initWithURL:[NSURL URLWithString:url]];
+    XCTAssert(credential.secret.length == 14, @"Credential with short secret is not padded");
+}
+
+- (void)test_WhenCredentialIsCreatedWithLongSHA1Secret_CredentialSecretIsHashed {
+    NSString *url = @"otpauth://totp/Label?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZHXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZHXDMVJECJJWSRB3HWIZR4IFUGFTMXHXDMVJECJJWS&issuer=Issuer";
+    YKFOATHCredential *credential = [[YKFOATHCredential alloc] initWithURL:[NSURL URLWithString:url]];
+    XCTAssert(credential.secret.length <= 64, @"Credential with long secret is not hashed.");
+}
+
+- (void)test_WhenCredentialIsCreatedWithLongSHA256Secret_CredentialSecretIsHashed {
+    NSString *url = @"otpauth://totp/Label?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZHXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZHXDMVJECJJWSRB3HWIZR4IFUGFTMXHXDMVJECJJWS&issuer=Issuer&algorithm=SHA256";
+    YKFOATHCredential *credential = [[YKFOATHCredential alloc] initWithURL:[NSURL URLWithString:url]];
+    XCTAssert(credential.secret.length <= 64, @"Credential with long secret is not hashed.");
+}
+
+- (void)test_WhenCredentialIsCreatedWithLongSHA512Secret_CredentialSecretIsHashed {
+    NSString *url = @"otpauth://totp/Label?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZHXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZHXDMVJECJJWSRB3HWIZR4IFUGFTMXHXDMVJECJJWSHXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZHXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZHXDMVJECJJWSRB3HWIZR4IFUGFTMXHXDMVJECJJWS&issuer=Issuer&algorithm=SHA512";
+    YKFOATHCredential *credential = [[YKFOATHCredential alloc] initWithURL:[NSURL URLWithString:url]];
+    XCTAssert(credential.secret.length <= 128, @"Credential with long secret is not hashed.");
+}
+
+- (void)test_WhenCredentialIsCreatedWithTOTPURLWithoutSecret_CredentialIsNil {
+    NSString *url = @"otpauth://totp/ACME:john@example.com?issuer=ACME&algorithm=SHA1&digits=6&period=30";
+    YKFOATHCredential *credential = [[YKFOATHCredential alloc] initWithURL:[NSURL URLWithString:url]];
+    XCTAssertNil(credential, @"Credential without secret is not nil.");
+}
+
+
+- (void)test_WhenCredentialIsCreatedWithURLWithoutLabel_CredentialIsNil {
+    NSString *url = @"otpauth://totp?issuer=ACME&algorithm=SHA1&digits=6&period=30";
+    YKFOATHCredential *credential = [[YKFOATHCredential alloc] initWithURL:[NSURL URLWithString:url]];
+    XCTAssertNil(credential, @"Credential with missing label is not nil.");
+}
+
+- (void)test_WhenCredentialIsCreatedWithURLWithoutOTPType_CredentialIsNil {
+    NSString *url = @"otpauth://ACME:john@example.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&issuer=ACME&algorithm=SHA1&digits=6&period=30";
+    YKFOATHCredential *credential = [[YKFOATHCredential alloc] initWithURL:[NSURL URLWithString:url]];
+    XCTAssertNil(credential, @"Credential with missing label is not nil.");
+}
+
+- (void)test_WhenCredentialIsCreatedWithURLWithoutAlgorithm_CredentialAlgorithmIsSHA1 {
+    NSString *url = @"otpauth://totp/ACME:john@example.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&issuer=ACME&digits=6&period=30";
+    YKFOATHCredential *credential = [[YKFOATHCredential alloc] initWithURL:[NSURL URLWithString:url]];
+    XCTAssert(credential.algorithm == YKFOATHCredentialAlgorithmSHA1 , @"Credential does not default to SHA1.");
+}
+
+@end
diff --git a/YubiKit/YubiKitTests/Tests/YKFOATHCredentialValidatorTests.m b/YubiKit/YubiKitTests/Tests/YKFOATHCredentialValidatorTests.m
new file mode 100755
index 000000000..69c8228e6
--- /dev/null
+++ b/YubiKit/YubiKitTests/Tests/YKFOATHCredentialValidatorTests.m
@@ -0,0 +1,71 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <XCTest/XCTest.h>
+
+#import "YKFTestCase.h"
+#import "YKFOATHCredential.h"
+#import "YKFOATHCredentialValidator.h"
+#import "YKFKeyOATHError.h"
+
+static NSString* const YKFOATHCredentialValidatorTestsVeryLargeSecret = @"HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZHXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZHXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZHXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZHXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZHXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ";
+
+@interface YKFOATHCredentialValidatorTests: YKFTestCase
+@end
+
+@implementation YKFOATHCredentialValidatorTests
+
+- (void)test_WhenValidatorReceivesValidTOTPCredential_NoErrorIsReturned {
+    NSString *url = @"otpauth://totp/ACME:john@example.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&issuer=ACME&algorithm=SHA1&digits=6&period=30";
+    YKFOATHCredential *credential = [[YKFOATHCredential alloc] initWithURL:[NSURL URLWithString:url]];
+    XCTAssertNotNil(credential);
+    
+    YKFKeySessionError *error = [YKFOATHCredentialValidator validateCredential:credential includeSecret:YES];
+    XCTAssertNil(error);
+}
+
+- (void)test_WhenValidatorReceivesValidHOTPCredential_NoErrorIsReturned {
+    NSString *url = @"otpauth://hotp/ACME:john@example.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&issuer=ACME&algorithm=SHA1&digits=6&counter=123";
+    YKFOATHCredential *credential = [[YKFOATHCredential alloc] initWithURL:[NSURL URLWithString:url]];
+    XCTAssertNotNil(credential);
+    
+    YKFKeySessionError *error = [YKFOATHCredentialValidator validateCredential:credential includeSecret:YES];
+    XCTAssertNil(error);
+}
+
+- (void)test_WhenValidatorIsRequestedToValidateWithoutSecret_SecretIsNotValidated {
+    NSString *urlFormat = @"otpauth://hotp/ACME:john@example.com?secret=%@&issuer=ACME&algorithm=SHA256&digits=6&counter=123";
+    NSString *url = [NSString stringWithFormat:urlFormat, YKFOATHCredentialValidatorTestsVeryLargeSecret];
+    
+    YKFOATHCredential *credential = [[YKFOATHCredential alloc] initWithURL:[NSURL URLWithString:url]];
+    XCTAssertNotNil(credential);
+
+    YKFKeySessionError *error = [YKFOATHCredentialValidator validateCredential:credential includeSecret:NO];
+    XCTAssertNil(error);
+}
+
+#pragma mark - Large Key Tests
+
+- (void)test_WhenValidatorReceivesInvalidCredentialKey_ErrorIsReturnedBack {
+    NSString *urlFormat = @"otpauth://hotp/ACME:john_with_too_long_name_which_does_not_really_fit_in_the_key@example.com?secret=%@&issuer=ACME&algorithm=SHA1&digits=6&counter=123";
+    NSString *url = [NSString stringWithFormat:urlFormat, YKFOATHCredentialValidatorTestsVeryLargeSecret];
+    
+    YKFOATHCredential *credential = [[YKFOATHCredential alloc] initWithURL:[NSURL URLWithString:url]];
+    XCTAssertNotNil(credential);
+    
+    YKFKeySessionError *error = [YKFOATHCredentialValidator validateCredential:credential includeSecret:YES];
+    XCTAssertEqual(error.code, YKFKeyOATHErrorCodeNameTooLong);
+}
+
+@end
diff --git a/YubiKit/YubiKitTests/Tests/YKFOTPTextParserTests.m b/YubiKit/YubiKitTests/Tests/YKFOTPTextParserTests.m
new file mode 100755
index 000000000..fe3aa6eb9
--- /dev/null
+++ b/YubiKit/YubiKitTests/Tests/YKFOTPTextParserTests.m
@@ -0,0 +1,87 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <XCTest/XCTest.h>
+
+#import "YKFTestCase.h"
+#import "YKFOTPTextParser.h"
+
+@interface YKFOTPTextParserTests: YKFTestCase
+
+@property (nonatomic) YKFOTPTextParser *textParser;
+
+@end
+
+@implementation YKFOTPTextParserTests
+
+- (void)setUp {
+    [super setUp];
+    YKFOTPTokenValidator *tokenVlaidator = [[YKFOTPTokenValidator alloc] init];
+    self.textParser = [[YKFOTPTextParser alloc] initWithValidator:tokenVlaidator];
+}
+
+- (void)tearDown {
+    self.textParser = nil;
+    [super tearDown];
+}
+
+#pragma mark - Valid Yubico OTP URI tests
+
+- (void)test_WhenPayloadIsTextWithYubicoOTP_OTPAndTextAreParsed {
+    NSString *expectedToken = @"ccccccibhhbfbugtukgjtflbhikgetvhjggeilkffitk";
+    NSString *payload = @"en-US\\some/ccccccibhhbfbugtukgjtflbhikgetvhjggeilkffitk";
+    
+    NSString *token = [self.textParser tokenFromPayload:payload];
+    XCTAssert([token isEqualToString:expectedToken], @"OTP token not correctly parsed.");
+    
+    NSString *text = [self.textParser textFromPayload:payload];
+    XCTAssert([text isEqualToString:payload], @"Text metadata not correctly parsed.");
+}
+
+- (void)test_WhenPayloadIsOnlyYubicoOTP_OnlyOTPIsParsed {
+    NSString *expectedToken = @"ccccccibhhbfbugtukgjtflbhikgetvhjggeilkffitk";
+    NSString *payload = expectedToken;
+    
+    NSString *token = [self.textParser tokenFromPayload:payload];
+    XCTAssert([token isEqualToString:expectedToken], @"OTP token not correctly parsed.");
+    
+    NSString *text = [self.textParser textFromPayload:expectedToken];
+    XCTAssert([text isEqualToString:payload], @"Text metadata not correctly parsed.");
+}
+
+#pragma mark - Valid HOTP URI tests
+
+- (void)test_WhenPayloadIsTextWithHOTP_TextAndOTPAreParsed {
+    NSString *expectedToken = @"12345678";
+    NSString *payload = @"en-US\\some/12345678";
+
+    NSString *token = [self.textParser tokenFromPayload:payload];
+    XCTAssert([token isEqualToString:expectedToken], @"OTP token not correctly parsed.");
+    
+    NSString *text = [self.textParser textFromPayload:payload];
+    XCTAssert([text isEqualToString:payload], @"Text metadata not correctly parsed.");
+}
+
+- (void)test_WhenPayloadIsOnlyHOTP_OnlyOTPIsParsed {
+    NSString *expectedToken = @"12345678";
+    NSString *payload = expectedToken;
+    
+    NSString *token = [self.textParser tokenFromPayload:payload];
+    XCTAssert([token isEqualToString:expectedToken], @"OTP token not correctly parsed.");
+    
+    NSString *text = [self.textParser textFromPayload:payload];
+    XCTAssert([text isEqualToString:payload], @"Text metadata not correctly parsed.");
+}
+
+@end
diff --git a/YubiKit/YubiKitTests/Tests/YKFOTPTokenParserTests.m b/YubiKit/YubiKitTests/Tests/YKFOTPTokenParserTests.m
new file mode 100755
index 000000000..a2550a2d3
--- /dev/null
+++ b/YubiKit/YubiKitTests/Tests/YKFOTPTokenParserTests.m
@@ -0,0 +1,272 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <XCTest/XCTest.h>
+
+#import "YKFTestCase.h"
+#import "YKFOTPTokenParser.h"
+#import "YKFOTPURIParserProtocol.h"
+#import "YKFOTPTextParserProtocol.h"
+#import "YubiKitConfiguration.h"
+
+#import "FakeYKFOTPTextParser.h"
+#import "FakeYKFOTPURIParser.h"
+
+@interface YKFOTPTokenParserTests: YKFTestCase
+
+@property (nonatomic) YKFOTPTokenParser *tokenParser;
+
+@end
+
+@implementation YKFOTPTokenParserTests
+
+- (void)setUp {
+    [super setUp];
+    self.tokenParser = [[YKFOTPTokenParser alloc] init];
+}
+
+- (void)tearDown {
+    self.tokenParser = nil;
+    YubiKitConfiguration.customOTPURIParser = nil;
+    YubiKitConfiguration.customOTPTextParser = nil;
+    [super tearDown];
+}
+
+#pragma mark - Yubico OTP tests (URI)
+
+- (void)test_WhenTokenIsDefaultYubicoOTPAndURI_TokenIsCorrectlyParsed {
+    NSString *expectedToken = @"ccccccibhhbfbugtukgjtflbhikgetvhjggeilkffitk";
+    NSString *payload = @"https://my.yubico.com/ccccccibhhbfbugtukgjtflbhikgetvhjggeilkffitk";
+    
+    NSArray *nfcResponse = [self nfcResponseWithPayload:payload metadataType:YKFOTPMetadataTypeURI];
+    
+    id<YKFOTPTokenProtocol> token = [self.tokenParser otpTokenFromNfcMessages: nfcResponse];
+    
+    XCTAssert(token.type == YKFOTPTokenTypeYubicoOTP, @"Wrong token type detected.");
+    XCTAssert(token.metadataType == YKFOTPMetadataTypeURI, @"Wrong metadata detected.");
+    
+    XCTAssert([token.value isEqualToString:expectedToken], @"Wrong parsed value.");
+    XCTAssert([token.uri isEqualToString:payload], @"Wrong parsed URI.");
+}
+
+- (void)test_WhenShortYubicoOTPAndURI_TokenIsCorrectlyParsed {
+    NSString *expectedToken = @"ccccccibhhbfbugtukgjtflbhikgetvh";
+    NSString *payload = @"https://my.yubico.com/ccccccibhhbfbugtukgjtflbhikgetvh";
+    
+    NSArray *nfcResponse = [self nfcResponseWithPayload:payload metadataType:YKFOTPMetadataTypeURI];
+    
+    id<YKFOTPTokenProtocol> token = [self.tokenParser otpTokenFromNfcMessages: nfcResponse];
+    
+    XCTAssert(token.type == YKFOTPTokenTypeYubicoOTP, @"Wrong token type detected.");
+    XCTAssert(token.metadataType == YKFOTPMetadataTypeURI, @"Wrong metadata detected.");
+    
+    XCTAssert([token.value isEqualToString:expectedToken], @"Wrong parsed value.");
+    XCTAssert([token.uri isEqualToString:payload], @"Wrong parsed URI.");
+}
+
+- (void)test_WhenLongYubicoOTPAndURI_TokenIsCorrectlyParsed {
+    NSString *expectedToken = @"ccccccibhhbfbugtukgjtflbhikgetvhibhhbfbugtukgjtflbhikgetvhkgetvh";
+    NSString *payload = @"https://my.yubico.com/ccccccibhhbfbugtukgjtflbhikgetvhibhhbfbugtukgjtflbhikgetvhkgetvh";
+    
+    NSArray *nfcResponse = [self nfcResponseWithPayload:payload metadataType:YKFOTPMetadataTypeURI];
+    
+    id<YKFOTPTokenProtocol> token = [self.tokenParser otpTokenFromNfcMessages: nfcResponse];
+    
+    XCTAssert(token.type == YKFOTPTokenTypeYubicoOTP, @"Wrong token type detected.");
+    XCTAssert(token.metadataType == YKFOTPMetadataTypeURI, @"Wrong metadata detected.");
+    
+    XCTAssert([token.value isEqualToString:expectedToken], @"Wrong parsed value.");
+    XCTAssert([token.uri isEqualToString:payload], @"Wrong parsed URI.");
+}
+
+#pragma mark - Yubico OTP tests (Text)
+
+- (void)test_WhenDefaultYubicoOTPAndText_TokenIsCorrectlyParsed {
+    NSString *expectedToken = @"ccccccibhhbfbugtukgjtflbhikgetvhjggeilkffitk";
+    NSString *payload = @"en-US\\some/ccccccibhhbfbugtukgjtflbhikgetvhjggeilkffitk";
+    
+    NSArray *nfcResponse = [self nfcResponseWithPayload:payload metadataType:YKFOTPMetadataTypeText];
+    
+    id<YKFOTPTokenProtocol> token = [self.tokenParser otpTokenFromNfcMessages: nfcResponse];
+    
+    XCTAssert(token.type == YKFOTPTokenTypeYubicoOTP, @"Wrong token type detected.");
+    XCTAssert(token.metadataType == YKFOTPMetadataTypeText, @"Wrong metadata detected.");
+    
+    XCTAssert([token.value isEqualToString:expectedToken], @"Wrong parsed value.");
+    XCTAssert([token.text isEqualToString:payload], @"Wrong parsed text.");
+}
+
+- (void)test_WhenShortYubicoOTPAndText_TokenIsCorrectlyParsed {
+    NSString *expectedToken = @"ccccccibhhbfbugtukgjtflbhikgetvh";
+    NSString *payload = @"en-US\\some/ccccccibhhbfbugtukgjtflbhikgetvh";
+    
+    NSArray *nfcResponse = [self nfcResponseWithPayload:payload metadataType:YKFOTPMetadataTypeText];
+    
+    id<YKFOTPTokenProtocol> token = [self.tokenParser otpTokenFromNfcMessages: nfcResponse];
+    
+    XCTAssert(token.type == YKFOTPTokenTypeYubicoOTP, @"Wrong token type detected.");
+    XCTAssert(token.metadataType == YKFOTPMetadataTypeText, @"Wrong metadata detected.");
+    
+    XCTAssert([token.value isEqualToString:expectedToken], @"Wrong parsed value.");
+    XCTAssert([token.text isEqualToString:payload], @"Wrong parsed text.");
+}
+
+- (void)test_WhenLongYubicoOTPAndText_TokenIsCorrectlyParsed {
+    NSString *expectedToken = @"ccccccibhhbfbugtukgjtflbhikgetvhibhhbfbugtukgjtflbhikgetvhkgetvh";
+    NSString *payload = @"en-US\\some/ccccccibhhbfbugtukgjtflbhikgetvhibhhbfbugtukgjtflbhikgetvhkgetvh";
+    
+    NSArray *nfcResponse = [self nfcResponseWithPayload:payload metadataType:YKFOTPMetadataTypeText];
+    
+    id<YKFOTPTokenProtocol> token = [self.tokenParser otpTokenFromNfcMessages: nfcResponse];
+    
+    XCTAssert(token.type == YKFOTPTokenTypeYubicoOTP, @"Wrong token type detected.");
+    XCTAssert(token.metadataType == YKFOTPMetadataTypeText, @"Wrong metadata detected.");
+    
+    XCTAssert([token.value isEqualToString:expectedToken], @"Wrong parsed value.");
+    XCTAssert([token.text isEqualToString:payload], @"Wrong parsed text.");
+}
+
+#pragma mark - HOTP tests (URI)
+
+- (void)test_WhenLongHOTPAndURI_TokenIsCorrectlyParsed {
+    NSString *expectedToken = @"12345678";
+    NSString *payload = @"https://my.yubico.com/12345678";
+    
+    NSArray *nfcResponse = [self nfcResponseWithPayload:payload metadataType:YKFOTPMetadataTypeURI];
+    
+    id<YKFOTPTokenProtocol> token = [self.tokenParser otpTokenFromNfcMessages: nfcResponse];
+    
+    XCTAssert(token.type == YKFOTPTokenTypeHOTP, @"Wrong token type detected.");
+    XCTAssert(token.metadataType == YKFOTPMetadataTypeURI, @"Wrong metadata detected.");
+    
+    XCTAssert([token.value isEqualToString:expectedToken], @"Wrong parsed value.");
+    XCTAssert([token.uri isEqualToString:payload], @"Wrong parsed URI.");
+}
+
+- (void)test_WhenShortHOTPAndURI_TokenIsCorrectlyParsed {
+    NSString *expectedToken = @"123456";
+    NSString *payload = @"https://my.yubico.com/123456";
+    
+    NSArray *nfcResponse = [self nfcResponseWithPayload:payload metadataType:YKFOTPMetadataTypeURI];
+    
+    id<YKFOTPTokenProtocol> token = [self.tokenParser otpTokenFromNfcMessages: nfcResponse];
+    
+    XCTAssert(token.type == YKFOTPTokenTypeHOTP, @"Wrong token type detected.");
+    XCTAssert(token.metadataType == YKFOTPMetadataTypeURI, @"Wrong metadata detected.");
+    
+    XCTAssert([token.value isEqualToString:expectedToken], @"Wrong parsed value.");
+    XCTAssert([token.uri isEqualToString:payload], @"Wrong parsed URI.");
+}
+
+#pragma mark - HOTP tests (Text)
+
+- (void)test_WhenLongHOTPAndText_TokenIsCorrectlyParsed {
+    NSString *expectedToken = @"12345678";
+    NSString *payload = @"en-US\\some/12345678";
+
+    NSArray *nfcResponse = [self nfcResponseWithPayload:payload metadataType:YKFOTPMetadataTypeText];
+    
+    id<YKFOTPTokenProtocol> token = [self.tokenParser otpTokenFromNfcMessages: nfcResponse];
+    
+    XCTAssert(token.type == YKFOTPTokenTypeHOTP, @"Wrong token type detected.");
+    XCTAssert(token.metadataType == YKFOTPMetadataTypeText, @"Wrong metadata detected.");
+    
+    XCTAssert([token.value isEqualToString:expectedToken], @"Wrong parsed value.");
+    XCTAssert([token.text isEqualToString:payload], @"Wrong parsed text.");
+}
+
+- (void)test_WhenShortHOTPAndText_TokenIsCorrectlyParsed {
+    NSString *expectedToken = @"123456";
+    NSString *payload = @"en-US\\some/123456";
+    
+    NSArray *nfcResponse = [self nfcResponseWithPayload:payload metadataType:YKFOTPMetadataTypeText];
+    
+    id<YKFOTPTokenProtocol> token = [self.tokenParser otpTokenFromNfcMessages: nfcResponse];
+    
+    XCTAssert(token.type == YKFOTPTokenTypeHOTP, @"Wrong token type detected.");
+    XCTAssert(token.metadataType == YKFOTPMetadataTypeText, @"Wrong metadata detected.");
+    
+    XCTAssert([token.value isEqualToString:expectedToken], @"Wrong parsed value.");
+    XCTAssert([token.text isEqualToString:payload], @"Wrong parsed text.");
+}
+
+#pragma mark - Custom parsers
+
+- (void)test_WhenCustomURIParserIsSet_ParserCallsTheCustomURIParser {
+    FakeYKFOTPURIParser *customURIParser = [[FakeYKFOTPURIParser alloc] init];
+    YubiKitConfiguration.customOTPURIParser = customURIParser;
+    
+    self.tokenParser = [[YKFOTPTokenParser alloc] init];
+    
+    NSString *payload = @"https://my.yubico.com/123456";
+    NSArray *nfcResponse = [self nfcResponseWithPayload:payload metadataType:YKFOTPMetadataTypeURI];
+    [self.tokenParser otpTokenFromNfcMessages: nfcResponse];
+    
+    XCTAssert(customURIParser.tokenFromPayloadInvoked, @"Custom URI parser not invoked.");
+    XCTAssert(customURIParser.uriFromPayloadInvoked, @"Custom URI parser not invoked.");
+}
+
+- (void)test_WhenCustomTextParserIsSet_ParserCallsTheCustomTextParser {
+    FakeYKFOTPTextParser *customTextParser = [[FakeYKFOTPTextParser alloc] init];
+    YubiKitConfiguration.customOTPTextParser = customTextParser;
+    
+    self.tokenParser = [[YKFOTPTokenParser alloc] init];
+    
+    NSString *payload = @"en-US\\some/123456";
+    NSArray *nfcResponse = [self nfcResponseWithPayload:payload metadataType:YKFOTPMetadataTypeText];
+    [self.tokenParser otpTokenFromNfcMessages: nfcResponse];
+    
+    XCTAssert(customTextParser.tokenFromPayloadInvoked, @"Custom URI parser not invoked.");
+    XCTAssert(customTextParser.textFromPayloadInvoked, @"Custom URI parser not invoked.");
+}
+
+#pragma mark - Helpers
+
+- (NSArray *)nfcResponseWithPayload:(NSString *)payload metadataType:(YKFOTPMetadataType)type {
+    NFCNDEFPayload *nfcPayload = [NFCNDEFPayload new];
+    
+    UInt8 payloadTypeURIBytes[1] = {'U'};
+    UInt8 payloadTypeTextBytes[1] = {'T'};
+    
+    if (type == YKFOTPMetadataTypeURI) {
+        nfcPayload.type = [NSData dataWithBytes:payloadTypeURIBytes length:1];
+    } else if (type == YKFOTPMetadataTypeText){
+        nfcPayload.type = [NSData dataWithBytes:payloadTypeTextBytes length:1];
+    }
+    
+    nfcPayload.typeNameFormat = NFCTypeNameFormatNFCWellKnown;
+    nfcPayload.payload = [self uriNFCPayloadDataWithString:payload metadataType:type];
+    
+    NFCNDEFMessage *nfcMessage = [NFCNDEFMessage new];
+    nfcMessage.records = @[nfcPayload];
+    return @[nfcMessage];
+}
+
+- (NSData *)uriNFCPayloadDataWithString:(NSString *)string  metadataType:(YKFOTPMetadataType)type {
+    NSMutableData *mutableData = [[NSMutableData alloc] init];
+    
+    UInt8 typePrefix = 0;
+    if (type == YKFOTPMetadataTypeURI) {
+        typePrefix = 0x00;
+    } else if (type == YKFOTPMetadataTypeText){
+        typePrefix = 0x05;
+    }
+    
+    [mutableData appendBytes:&typePrefix length:1];
+    [mutableData appendData:[string dataUsingEncoding:NSUTF8StringEncoding]];
+    
+    return [mutableData copy];
+}
+
+@end
diff --git a/YubiKit/YubiKitTests/Tests/YKFOTPTokenValidatorTests.m b/YubiKit/YubiKitTests/Tests/YKFOTPTokenValidatorTests.m
new file mode 100755
index 000000000..30060d636
--- /dev/null
+++ b/YubiKit/YubiKitTests/Tests/YKFOTPTokenValidatorTests.m
@@ -0,0 +1,174 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <XCTest/XCTest.h>
+
+#import "YKFTestCase.h"
+#import "YKFOTPTokenValidator.h"
+
+@interface YKFOTPTokenValidatorTests: YKFTestCase
+
+@property (nonatomic) YKFOTPTokenValidator *otpTokenValidator;
+
+@end
+
+@implementation YKFOTPTokenValidatorTests
+
+- (void)setUp {
+    [super setUp];
+    self.otpTokenValidator = [[YKFOTPTokenValidator alloc] init];
+}
+
+- (void)tearDown {
+    self.otpTokenValidator = nil;
+    [super tearDown];
+}
+
+#pragma mark - Valid Yubico OTP tests
+
+- (void)test_WhenTokenIsDefaultYubicoOTP_TokenIsValid {
+    NSString *otp = @"ccccccibhhbfbugtukgjtflbhikgetvhjggeilkffitk";
+    
+    BOOL valid = [self.otpTokenValidator validateToken:otp];
+    XCTAssert(valid, @"Valid Yubico OTP not correctly detected.");
+}
+
+- (void)test_WhenTokenIsShortYubicoOTP_TokenIsValid {
+    NSString *otp = @"ccccccibhhbfbugtukgjtflbhikgetvh";
+    
+    BOOL valid = [self.otpTokenValidator validateToken:otp];
+    XCTAssert(valid, @"Valid Yubico OTP not correctly detected.");
+}
+
+- (void)test_WhenTokenIsLongYubicoOTP_TokenIsValid {
+    NSString *otp = @"ccccccibhhbfbugtukgjtflbhikgetvhibhhbfbugtukgjtflbhikgetvhkgetvh";
+    
+    BOOL valid = [self.otpTokenValidator validateToken:otp];
+    XCTAssert(valid, @"Valid Yubico OTP not correctly detected.");
+}
+
+#pragma mark - Invalid Yubico OTP tests
+
+- (void)test_WhenYubicoOTPIsTooShort_TokenIsNotValid {
+    NSString *otp = @"ccccccibhhbfbugtukgjtflbhik";
+    
+    BOOL valid = [self.otpTokenValidator validateToken:otp];
+    XCTAssert(!valid, @"Invalid Yubico OTP not correctly detected.");
+}
+
+- (void)test_WhenYubicoOTPIsTooLong_TokenIsNotValid {
+    NSString *otp = @"ccccccibhhbfbugtukgjtflbhikgetvhibhhbfbugtukgjtflbhikgetvhkgetvhttt";
+    
+    BOOL valid = [self.otpTokenValidator validateToken:otp];
+    XCTAssert(!valid, @"Invalid Yubico OTP not correctly detected.");
+}
+
+- (void)test_WhenYubicoOTPIsNotModhexEncoded_TokenIsNotValid {
+    NSString *otp = @"000000ibhhbfbugtukgjtflbhikgetvhjggeilkffitk";
+    
+    BOOL valid = [self.otpTokenValidator validateToken:otp];
+    XCTAssert(!valid, @"Invalid Yubico OTP not correctly detected.");
+}
+
+- (void)test_WhenTokenIsHOTP_TokenIsNotYubicoOTP {
+    NSArray *hotps = @[@"123456", @"A123456", @"AB123456", @"12345678", @"A12345678"];
+    
+    for (NSString *hotp in hotps) {
+        BOOL valid = [self.otpTokenValidator maybeYubicoOTP:hotp];
+        XCTAssert(!valid, @"Invalid Yubico OTP not correctly detected (%@).", hotp);
+    }
+}
+
+#pragma mark - Valid HOTP tests
+
+- (void)test_WhenTokenIsLongHOTP_TokenIsValid {
+    NSString *otp = @"12345678";
+    
+    BOOL valid = [self.otpTokenValidator validateToken:otp];
+    XCTAssert(valid, @"Valid HOTP not correctly detected.");
+}
+
+- (void)test_WhenTokenIsShortHOTP_TokenIsValid {
+    NSString *otp = @"123456";
+    
+    BOOL valid = [self.otpTokenValidator validateToken:otp];
+    XCTAssert(valid, @"Valid HOTP not correctly detected.");
+}
+
+- (void)test_WhenTokenIsPrefixedShortHOTP_TokenIsValid {
+    NSArray *hotps = @[@"ABCD123456", @"A123456"];
+    
+    for (NSString *hotp in hotps) {
+        BOOL valid = [self.otpTokenValidator validateToken:hotp];
+        XCTAssert(valid, @"Invalid HOTP not correctly detected (%@).", hotp);
+    }
+}
+
+- (void)test_WhenTokenIsPrefixedLongHOTP_TokenIsValid {
+    NSArray *hotps = @[@"ABCD12345678", @"A12345678"];
+    
+    for (NSString *hotp in hotps) {
+        BOOL valid = [self.otpTokenValidator validateToken:hotp];
+        XCTAssert(valid, @"Invalid HOTP not correctly detected (%@).", hotp);
+    }
+}
+
+#pragma mark - Invalid HOTP tests
+
+- (void)test_WhenTokenIsShortHOTPWithLetters_TokenIsNotValid {
+    NSArray *hotps = @[@"12345X", @"12345A", @"123X56", @"A23456"];
+    
+    for (NSString *hotp in hotps) {
+        BOOL valid = [self.otpTokenValidator validateToken:hotp];
+        XCTAssert(!valid, @"Invalid HOTP not correctly detected (%@).", hotp);
+    }
+}
+
+- (void)test_WhenHOTPTokenIsTooShort_TokenIsNotValid {
+    NSArray *hotps = @[@"12345", @"1234", @"123", @"12", @"1", @""];
+    
+    for (NSString *hotp in hotps) {
+        BOOL valid = [self.otpTokenValidator validateToken:hotp];
+        XCTAssert(!valid, @"Invalid HOTP not correctly detected (%@).", hotp);
+    }
+}
+
+- (void)test_WhenTokenIsYubicoOTP_TokenIsNotHOTP {
+    NSArray *otps = @[@"ccccccibhhbfbugtukgjtflbhikgetvhjggeilkffitk",
+                      @"ccccccibhhbfbugtukgjtflbhikgetvh",
+                      @"ccccccibhhbfbugtukgjtflbhikgetvhibhhbfbugtukgjtflbhikgetvhkgetvh"];
+    
+    for (NSString *otp in otps) {
+        BOOL valid = [self.otpTokenValidator maybeHOTP:otp];
+        XCTAssert(!valid, @"Invalid HOTP not correctly detected (%@).", otp);
+    }
+}
+
+#pragma mark - Other
+
+- (void)test_WhenTokenIsEmpty_TokenIsNotValid {
+    NSString *otp = @"";
+    
+    BOOL valid = [self.otpTokenValidator validateToken:otp];
+    XCTAssert(!valid, @"Invalid empty OTP not correctly detected.");
+}
+
+- (void)test_WhenTokenIsNil_TokenIsNotValid {
+    NSString *otp = nil;
+    
+    BOOL valid = [self.otpTokenValidator validateToken:otp];
+    XCTAssert(!valid, @"Invalid nil OTP not correctly detected.");
+}
+
+@end
diff --git a/YubiKit/YubiKitTests/Tests/YKFOTPURIParserTests.m b/YubiKit/YubiKitTests/Tests/YKFOTPURIParserTests.m
new file mode 100755
index 000000000..13e4722f2
--- /dev/null
+++ b/YubiKit/YubiKitTests/Tests/YKFOTPURIParserTests.m
@@ -0,0 +1,175 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <XCTest/XCTest.h>
+
+#import "YKFTestCase.h"
+#import "YKFOTPURIParser.h"
+#import "YKFOTPTokenValidator.h"
+
+@interface YKFOTPURIParserTests: YKFTestCase
+
+@property (nonatomic) YKFOTPURIParser *uriParser;
+
+@end
+
+@implementation YKFOTPURIParserTests
+
+- (void)setUp {
+    [super setUp];
+    YKFOTPTokenValidator *tokenValidator = [[YKFOTPTokenValidator alloc] init];
+    self.uriParser = [[YKFOTPURIParser alloc] initWithValidator:tokenValidator];
+}
+
+- (void)tearDown {
+    self.uriParser = nil;
+    [super tearDown];
+}
+
+#pragma mark - Valid Yubico OTP URI tests
+
+- (void)test_WhenPayloadIsURIWithYubicoOTP_OTPAndURIAreParsed {
+    NSString *expectedToken = @"ccccccibhhbfbugtukgjtflbhikgetvhjggeilkffitk";
+    NSString *payload = @"my.yubico.com/neo/ccccccibhhbfbugtukgjtflbhikgetvhjggeilkffitk";
+    
+    NSString *token = [self.uriParser tokenFromPayload:payload];
+    XCTAssert([token isEqualToString:expectedToken], @"OTP Token not correctly parsed.");
+    
+    NSString *uri = [self.uriParser uriFromPayload:payload];
+    XCTAssert([uri isEqualToString:payload], @"URI metadata not correctly parsed.");
+}
+
+- (void)test_WhenPayloadIsOnlyYubicoOTP_OnlyOTPIsParsed {
+    NSString *expectedToken = @"ccccccibhhbfbugtukgjtflbhikgetvhjggeilkffitk";
+    NSString *payload = expectedToken;
+    
+    NSString *token = [self.uriParser tokenFromPayload:payload];
+    XCTAssert([token isEqualToString:expectedToken], @"OTP Token not correctly parsed.");
+    
+    NSString *uri = [self.uriParser uriFromPayload:payload];
+    XCTAssert(uri.length == 0, @"URI metadata not correctly parsed.");
+}
+
+- (void)test_WhenPayloadIsURIWithYubicoOTPWithPoundSign_OTPAndURIAreParsed {
+    NSString *expectedToken = @"ccccccibhhbfbugtukgjtflbhikgetvhjggeilkffitk";
+    NSString *payload = @"my.yubico.com/neo/#ccccccibhhbfbugtukgjtflbhikgetvhjggeilkffitk";
+    
+    NSString *token = [self.uriParser tokenFromPayload:payload];
+    XCTAssert([token isEqualToString:expectedToken], @"OTP Token not correctly parsed.");
+    
+    NSString *uri = [self.uriParser uriFromPayload:payload];
+    XCTAssert([uri isEqualToString:payload], @"URI metadata not correctly parsed.");
+}
+
+- (void)test_WhenPayloadIsCustomURIWithYubicoOTPWithPoundSign_OTPAndURIAreParsed {
+    NSString *expectedToken = @"ccccccibhhbfbugtukgjtflbhikgetvhjggeilkffitk";
+    NSString *payload = @"https://www.example.com/custom/#ccccccibhhbfbugtukgjtflbhikgetvhjggeilkffitk";
+    
+    NSString *token = [self.uriParser tokenFromPayload:payload];
+    XCTAssert([token isEqualToString:expectedToken], @"OTP Token not correctly parsed.");
+    
+    NSString *uri = [self.uriParser uriFromPayload:payload];
+    XCTAssert([uri isEqualToString:payload], @"URI metadata not correctly parsed.");
+}
+
+- (void)test_WhenPayloadIsCustomURIWithYubicoOTP_OTPAndURIAreParsed {
+    NSString *expectedToken = @"ccccccibhhbfbugtukgjtflbhikgetvhjggeilkffitk";
+    NSString *payload = @"https://www.example.com/custom/ccccccibhhbfbugtukgjtflbhikgetvhjggeilkffitk";
+    
+    NSString *token = [self.uriParser tokenFromPayload:payload];
+    XCTAssert([token isEqualToString:expectedToken], @"OTP Token not correctly parsed.");
+    
+    NSString *uri = [self.uriParser uriFromPayload:payload];
+    XCTAssert([uri isEqualToString:payload], @"URI metadata not correctly parsed.");
+}
+
+- (void)test_WhenPayloadIsModernURIWithWithPoundSign_OTPAndURIAreParsed {
+    NSString *expectedToken = @"ccccccibhhbfbugtukgjtflbhikgetvhjggeilkffitk";
+    NSString *expectedURI = @"https://my.yubico.com/yk/#ccccccibhhbfbugtukgjtflbhikgetvhjggeilkffitk";
+    
+    NSString *payload = @"my.yubico.com/yk/#ccccccibhhbfbugtukgjtflbhikgetvhjggeilkffitk";
+    payload = [NSString stringWithFormat:@"%c%@", 0x04, payload];
+    
+    NSString *token = [self.uriParser tokenFromPayload:payload];
+    XCTAssert([token isEqualToString:expectedToken], @"OTP Token not correctly parsed.");
+    
+    NSString *uri = [self.uriParser uriFromPayload:payload];
+    XCTAssert([uri isEqualToString:expectedURI], @"URI metadata not correctly parsed.");
+}
+
+- (void)test_WhenPayloadIsPrependedURIWithYubicoOTP_OTPAndURIAreParsed {
+    NSString *expectedToken = @"ccccccibhhbfbugtukgjtflbhikgetvhjggeilkffitk";
+
+    // No prepending
+    
+    NSString *payload = @"my.yubico.com/neo/ccccccibhhbfbugtukgjtflbhikgetvhjggeilkffitk";
+    NSString *expectedURI = @"my.yubico.com/neo/ccccccibhhbfbugtukgjtflbhikgetvhjggeilkffitk";
+    payload = [NSString stringWithFormat:@"%c%@", 0x00, payload];
+
+    NSString *token = [self.uriParser tokenFromPayload:payload];
+    XCTAssert([token isEqualToString:expectedToken], @"OTP Token not correctly parsed.");
+    
+    NSString *uri = [self.uriParser uriFromPayload:payload];
+    XCTAssert([uri isEqualToString:expectedURI], @"URI metadata not correctly parsed.");
+    
+    // Https prepending
+    
+    payload = @"my.yubico.com/neo/ccccccibhhbfbugtukgjtflbhikgetvhjggeilkffitk";
+    expectedURI = @"https://my.yubico.com/neo/ccccccibhhbfbugtukgjtflbhikgetvhjggeilkffitk";
+    payload = [NSString stringWithFormat:@"%c%@", 0x04, payload];
+    
+    token = [self.uriParser tokenFromPayload:payload];
+    XCTAssert([token isEqualToString:expectedToken], @"OTP Token not correctly parsed.");
+    
+    uri = [self.uriParser uriFromPayload:payload];
+    XCTAssert([uri isEqualToString:expectedURI], @"URI metadata not correctly parsed.");
+    
+    // Http prepending
+    
+    payload = @"my.yubico.com/neo/ccccccibhhbfbugtukgjtflbhikgetvhjggeilkffitk";
+    expectedURI = @"http://my.yubico.com/neo/ccccccibhhbfbugtukgjtflbhikgetvhjggeilkffitk";
+    payload = [NSString stringWithFormat:@"%c%@", 0x03, payload];
+    
+    token = [self.uriParser tokenFromPayload:payload];
+    XCTAssert([token isEqualToString:expectedToken], @"OTP Token not correctly parsed.");
+    
+    uri = [self.uriParser uriFromPayload:payload];
+    XCTAssert([uri isEqualToString:expectedURI], @"URI metadata not correctly parsed.");
+}
+
+#pragma mark - Valid HOTP URI tests
+
+- (void)test_WhenPayloadIsURIWithHOTP_OTPAndURIAreParsed {
+    NSString *expectedToken = @"12345678";
+    NSString *payload = @"my.yubico.com/neo/12345678";
+    
+    NSString *token = [self.uriParser tokenFromPayload:payload];
+    XCTAssert([token isEqualToString:expectedToken], @"OTP Token not correctly parsed.");
+    
+    NSString *uri = [self.uriParser uriFromPayload:payload];
+    XCTAssert([uri isEqualToString:payload], @"URI metadata not correctly parsed.");
+}
+
+- (void)test_WhenPayloadIsOnlyHOTP_OnlyOTPIsParsed {
+    NSString *expectedToken = @"12345678";
+    NSString *payload = expectedToken;
+    
+    NSString *token = [self.uriParser tokenFromPayload:payload];
+    XCTAssert([token isEqualToString:expectedToken], @"OTP Token not correctly parsed.");
+    
+    NSString *uri = [self.uriParser uriFromPayload:payload];
+    XCTAssert(uri.length == 0, @"URI metadata not correctly parsed.");
+}
+
+@end
diff --git a/YubiKit/YubiKitTests/Tests/YKFPCSCTests.m b/YubiKit/YubiKitTests/Tests/YKFPCSCTests.m
new file mode 100755
index 000000000..c0b9af2bd
--- /dev/null
+++ b/YubiKit/YubiKitTests/Tests/YKFPCSCTests.m
@@ -0,0 +1,444 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <XCTest/XCTest.h>
+#import "YKFTestCase.h"
+#import "YKFPCSC.h"
+#import "FakeYKFPCSCLayer.h"
+
+@interface YKFPCSCTests: YKFTestCase
+
+@property (nonatomic) FakeYKFPCSCLayer *pcscLayer;
+
+@end
+
+@implementation YKFPCSCTests
+
+#pragma mark - Test Lifecycle
+
+- (void)setUp {
+    [super setUp];
+    self.pcscLayer = [[FakeYKFPCSCLayer alloc] init];
+    YKFPCSCLayer.fakePCSCLayer = self.pcscLayer;
+}
+
+- (void)tearDown {
+    [super tearDown];
+    YKFPCSCLayer.fakePCSCLayer = nil;
+}
+
+#pragma mark - Context Tests
+
+- (void)test_WhenEstablishingAndReleasingContexts_ContextsCanBeRequestedAndReleased {
+    self.pcscLayer.addContextResponse = YES;
+    self.pcscLayer.removeContextResponse = YES;
+    
+    [self executeOnBackgroundQueueAndWait:^{
+        SInt32 context = 0;
+        SInt64 result = YKF_SCARD_S_SUCCESS;
+        
+        result = YKFSCardEstablishContext(YKF_SCARD_SCOPE_USER, nil, nil, &context);
+        XCTAssertEqual(result, YKF_SCARD_S_SUCCESS, @"Could not create a new PCSC context.");
+        XCTAssert(context, @"PCSC context ID was not generated.");
+        
+        result = YKFSCardReleaseContext(context);
+        XCTAssertEqual(result, YKF_SCARD_S_SUCCESS, @"Could not release the PCSC context.");
+    }];
+}
+
+- (void)test_WhenEstablishingContexts_ErrorIsReturnedIfTooManyContexts {
+    self.pcscLayer.addContextResponse = NO;
+    
+    [self executeOnBackgroundQueueAndWait:^{
+        SInt32 context = 0;
+        SInt64 result = YKF_SCARD_S_SUCCESS;
+        
+        result = YKFSCardEstablishContext(YKF_SCARD_SCOPE_USER, nil, nil, &context);
+        XCTAssertEqual(result, YKF_SCARD_E_NO_MEMORY, @"Error is not returned when there are too many contexts.");
+        XCTAssertEqual(context, 0, @"PCSC context ID is generated when the establish context returned an error.");
+    }];
+}
+
+- (void)test_WhenReleasingContexts_ErrorIsReturnedIfContextIsUnknown {
+    self.pcscLayer.removeContextResponse = NO;
+    
+    [self executeOnBackgroundQueueAndWait:^{
+        SInt64 result = YKF_SCARD_S_SUCCESS;
+        
+        result = YKFSCardReleaseContext(0);
+        XCTAssertEqual(result, YKF_SCARD_E_INVALID_HANDLE, @"Error is not returned when the PC/SC context is unknown.");
+    }];
+}
+
+- (void)test_WhenEstablishingContextsWithNilContextPointer_ErrorIsReturned {
+    self.pcscLayer.addContextResponse = NO;
+    
+    [self executeOnBackgroundQueueAndWait:^{
+        SInt64 result = YKF_SCARD_S_SUCCESS;
+        
+        result = YKFSCardEstablishContext(YKF_SCARD_SCOPE_USER, nil, nil, nil);
+        XCTAssertEqual(result, YKF_SCARD_E_INVALID_PARAMETER, @"Error is not returned when the PC/SC context pointer is nil.");
+    }];
+}
+
+#pragma mark - Transaction Tests
+
+- (void)test_WhenUsingTransactions_TransactionsCanBeStartedAndEnded {
+    self.pcscLayer.cardIsValidResponse = YES;
+    
+    [self executeOnBackgroundQueueAndWait:^{
+        SInt64 result = YKF_SCARD_S_SUCCESS;
+        SInt32 card = 0;
+        
+        result = YKFSCardBeginTransaction(card);
+        XCTAssertEqual(result, YKF_SCARD_S_SUCCESS, @"Could not begin PC/SC transaction.");
+        
+        result = YKFSCardEndTransaction(card, 0);
+        XCTAssertEqual(result, YKF_SCARD_S_SUCCESS, @"Could not end PC/SC transaction.");
+    }];
+}
+
+- (void)test_WhenUsingTransactions_TransactionsWillFailIfTheCardIsUnknown {
+    self.pcscLayer.cardIsValidResponse = NO;
+    
+    [self executeOnBackgroundQueueAndWait:^{
+        SInt64 result = YKF_SCARD_S_SUCCESS;
+        SInt32 card = 0;
+        
+        result = YKFSCardBeginTransaction(card);
+        XCTAssertEqual(result, YKF_SCARD_E_INVALID_HANDLE, @"Begin PC/SC transaction could be executed when the card was unknown.");
+        
+        result = YKFSCardEndTransaction(card, 0);
+        XCTAssertEqual(result, YKF_SCARD_E_INVALID_HANDLE, @"End PC/SC transaction could be executed when the card was unknown.");
+    }];
+}
+
+#pragma mark - Listing Readers
+
+- (void)test_WhenListingReaders_CanAskForReadersLengthSize {
+    NSString *readerName = @"YubiKey";
+    
+    self.pcscLayer.listReadersResponse = YKF_SCARD_S_SUCCESS;
+    self.pcscLayer.listReadersResponseParam = readerName;
+    self.pcscLayer.contextIsValidResponse = YES;
+    
+    [self executeOnBackgroundQueueAndWait:^{
+        SInt64 result = YKF_SCARD_S_SUCCESS;
+        SInt32 context = 0;
+        UInt32 readersLength = 0;
+        
+        result = YKFSCardListReaders(context, nil, nil, &readersLength);
+        NSUInteger expectedLength = readerName.length + 2; // double null terminated multistring.
+        XCTAssertEqual(readersLength, expectedLength, @"PC/SC readers length does not match the expected length.");
+    }];
+}
+
+- (void)test_WhenListingReaders_TheKeyReaderNameIsReturned {
+    NSString *expectedReaderName = @"YubiKey";
+    
+    self.pcscLayer.listReadersResponse = YKF_SCARD_S_SUCCESS;
+    self.pcscLayer.listReadersResponseParam = expectedReaderName;
+    self.pcscLayer.contextIsValidResponse = YES;
+    
+    [self executeOnBackgroundQueueAndWait:^{
+        SInt64 result = YKF_SCARD_S_SUCCESS;
+        SInt32 context = 0;
+        UInt32 readersLength = (UInt32)expectedReaderName.length + 2;
+        
+        char *buffer = malloc(expectedReaderName.length + 2);
+        XCTAssert(buffer, @"Could not allocated buffer.");
+        
+        result = YKFSCardListReaders(context, nil, buffer, &readersLength);
+        NSUInteger expectedLength = expectedReaderName.length + 2; // double null terminated multistring.
+        
+        XCTAssertEqual(readersLength, expectedLength, @"PC/SC readers length does not match the expected length.");
+        XCTAssert(strcmp(expectedReaderName.UTF8String, buffer) == 0, @"The reader name was not copied to the out parameter buffer.");
+        
+        free(buffer);
+    }];
+}
+
+- (void)test_WhenListingReadersWithSmallBuffer_SmallBufferErrorIsReturned {
+    NSString *expectedReaderName = @"YubiKey";
+    
+    self.pcscLayer.listReadersResponse = YKF_SCARD_S_SUCCESS;
+    self.pcscLayer.listReadersResponseParam = expectedReaderName;
+    self.pcscLayer.contextIsValidResponse = YES;
+    
+    [self executeOnBackgroundQueueAndWait:^{
+        SInt64 result = YKF_SCARD_S_SUCCESS;
+        SInt32 context = 0;
+        UInt32 readersLength = (UInt32)expectedReaderName.length;
+        
+        char *buffer = malloc(expectedReaderName.length); // too small buffer
+        XCTAssert(buffer, @"Could not allocated buffer.");
+        
+        result = YKFSCardListReaders(context, nil, buffer, &readersLength);
+        NSUInteger expectedLength = expectedReaderName.length + 2; // double null terminated multistring.
+        
+        XCTAssertEqual(readersLength, expectedLength, @"PC/SC readers length does not match the expected length.");
+        XCTAssertEqual(result, YKF_SCARD_E_INSUFFICIENT_BUFFER, @"No error returened when the buffer is too small.");
+        
+        free(buffer);
+    }];
+}
+
+#pragma mark - Reader Status
+
+- (void)test_WhenAskingForCardStatus_CardIsPresentIfTheKeyIsConnected {
+    self.pcscLayer.getStatusChangeResponse = YKF_SCARD_STATE_PRESENT;
+    self.pcscLayer.contextIsValidResponse = YES;
+    self.pcscLayer.getCardSerialResponse = @"12345";
+    
+    [self executeOnBackgroundQueueAndWait:^{
+        SInt64 result = YKF_SCARD_S_SUCCESS;
+        SInt32 context = 0;
+        YKF_SCARD_READERSTATE readerState;
+        
+        result = YKFSCardGetStatusChange(context, 0, &readerState, 1);
+        
+        XCTAssertEqual(result, YKF_SCARD_S_SUCCESS, @"Could not execute PC/SC get status change.");
+        XCTAssert(readerState.eventState & YKF_SCARD_STATE_PRESENT, @"The card is not detected as present when the key is plugged in the device.");
+    }];
+}
+
+- (void)test_WhenAskingForCardStatusWithNoReaderStates_ErrorIsReturned {
+    self.pcscLayer.getStatusChangeResponse = YKF_SCARD_STATE_PRESENT;
+    self.pcscLayer.contextIsValidResponse = YES;
+    self.pcscLayer.getCardSerialResponse = @"12345";
+    
+    [self executeOnBackgroundQueueAndWait:^{
+        SInt64 result = YKF_SCARD_S_SUCCESS;
+        SInt32 context = 0;
+        
+        result = YKFSCardGetStatusChange(context, 0, nil, 1);
+        
+        XCTAssertEqual(result, YKF_SCARD_E_INVALID_PARAMETER, @"The PC/SC get status change did not return an error when no readers are passed.");
+    }];
+}
+
+#pragma mark - Card Connect/Disconnect
+
+- (void)test_WhenConnectingToTheKey_SuccessIsReturnedIfTheSessionIsOpen {
+    self.pcscLayer.contextIsValidResponse = YES;
+    self.pcscLayer.addCardToContextResponse = YES;
+    self.pcscLayer.connectCardResponse = YKF_SCARD_S_SUCCESS;
+    
+    [self executeOnBackgroundQueueAndWait:^{
+        const char *reader = "YubiKey";
+        SInt64 result = YKF_SCARD_S_SUCCESS;
+        SInt32 context = 0;
+        UInt32 activeProtocol = YKF_SCARD_PROTOCOL_T1;
+        SInt32 card = 0;
+        
+        result = YKFSCardConnect(context, reader, YKF_SCARD_SHARE_EXCLUSIVE, YKF_SCARD_PROTOCOL_T1, &card, &activeProtocol);
+        
+        XCTAssertEqual(result, YKF_SCARD_S_SUCCESS, @"PC/SC failed to connect to card.");
+        XCTAssertNotEqual(card, 0, @"PC/SC card ID not generated after connecting to the card.");
+    }];
+}
+
+- (void)test_WhenConnectingToTheKey_ErrorIsReturnedIfTheSessionIsClosed {
+    self.pcscLayer.contextIsValidResponse = YES;
+    self.pcscLayer.addCardToContextResponse = YES;
+    self.pcscLayer.connectCardResponse = YKF_SCARD_F_WAITED_TOO_LONG;
+    
+    [self executeOnBackgroundQueueAndWait:^{
+        const char *reader = "YubiKey";
+        SInt64 result = YKF_SCARD_S_SUCCESS;
+        SInt32 context = 0;
+        UInt32 activeProtocol = YKF_SCARD_PROTOCOL_T1;
+        SInt32 card = 0;
+        
+        result = YKFSCardConnect(context, reader, YKF_SCARD_SHARE_EXCLUSIVE, YKF_SCARD_PROTOCOL_T1, &card, &activeProtocol);
+        
+        XCTAssertEqual(result, YKF_SCARD_F_WAITED_TOO_LONG, @"PC/SC did not return an error when session opening failed.");
+        XCTAssertEqual(card, 0, @"PC/SC card generated after connecting to the card after failing to open the session.");
+    }];
+}
+
+- (void)test_WhenDisconnectingFromTheKey_SuccessIsReturnedIfTheSessionIsClosed {
+    self.pcscLayer.cardIsValidResponse = YES;
+    self.pcscLayer.removeCardResponse = YES;
+    self.pcscLayer.disconnectCardResponse = YKF_SCARD_S_SUCCESS;
+    
+    [self executeOnBackgroundQueueAndWait:^{
+        SInt64 result = YKF_SCARD_S_SUCCESS;
+        SInt32 card = 0;
+        
+        result = YKFSCardDisconnect(card, YKF_SCARD_LEAVE_CARD);
+        
+        XCTAssertEqual(result, YKF_SCARD_S_SUCCESS, @"PC/SC card was not disconnected when the session was closed.");
+    }];
+}
+
+- (void)test_WhenDisconnectingFromTheKey_ErrorIsReturnedIfTheSessionIsOpen {
+    self.pcscLayer.cardIsValidResponse = YES;
+    self.pcscLayer.removeCardResponse = YES;
+    self.pcscLayer.disconnectCardResponse = YKF_SCARD_F_WAITED_TOO_LONG;
+    
+    [self executeOnBackgroundQueueAndWait:^{
+        SInt64 result = YKF_SCARD_S_SUCCESS;
+        SInt32 card = 0;
+        
+        result = YKFSCardDisconnect(card, YKF_SCARD_LEAVE_CARD);
+        
+        XCTAssertEqual(result, YKF_SCARD_F_WAITED_TOO_LONG, @"PC/SC did not return an error when the session was not closed.");
+    }];
+}
+
+#pragma mark - Card Status
+
+- (void)test_WhenAskingForCardStatus_TheStateCanBeUsedToCheckKeyConnection {
+    self.pcscLayer.cardIsValidResponse = YES;
+    self.pcscLayer.contextIsValidResponse = YES;
+    self.pcscLayer.contextForCardResponse = 100;
+    self.pcscLayer.getCardStateResponse = YKF_SCARD_STATE_PRESENT;
+    self.pcscLayer.listReadersResponse = YKF_SCARD_S_SUCCESS;
+    self.pcscLayer.listReadersResponseParam = @"YubiKey";
+    
+    [self executeOnBackgroundQueueAndWait:^{
+        SInt64 result = YKF_SCARD_S_SUCCESS;
+        SInt32 card = 0;
+        UInt32 state = 0;
+        
+        result = YKFSCardStatus(card, nil, nil, &state, nil, nil, nil);
+        
+        XCTAssertEqual(state, YKF_SCARD_STATE_PRESENT, @"PC/SC did not return correct state when the card was plugged in.");
+        XCTAssertEqual(result, YKF_SCARD_S_SUCCESS, @"PC/SC did not return success when getting the card status.");
+    }];
+}
+
+- (void)test_WhenAskingForCardStatus_NilParametersCanBeSentIfSomeParametersAreNotRequired {
+    self.pcscLayer.cardIsValidResponse = YES;
+    self.pcscLayer.contextIsValidResponse = YES;
+    self.pcscLayer.contextForCardResponse = 100;
+    self.pcscLayer.getCardStateResponse = YKF_SCARD_STATE_PRESENT;
+    self.pcscLayer.listReadersResponse = YKF_SCARD_S_SUCCESS;
+    self.pcscLayer.listReadersResponseParam = @"YubiKey";
+    
+    [self executeOnBackgroundQueueAndWait:^{
+        SInt64 result = YKF_SCARD_S_SUCCESS;
+        SInt32 card = 0;
+        
+        result = YKFSCardStatus(card, nil, nil, nil, nil, nil, nil);
+        
+        XCTAssertEqual(result, YKF_SCARD_S_SUCCESS, @"PC/SC did not return success when calling YKFSCardStatus with nil params.");
+    }];
+}
+
+#pragma mark - Card Attributes
+
+- (void)test_WhenAskingForCardAttributes_KnownAttributesCanBeRetrieved {
+    self.pcscLayer.cardIsValidResponse = YES;
+    self.pcscLayer.contextIsValidResponse = YES;
+    self.pcscLayer.getCardSerialResponse = @"123456";
+    
+    [self executeOnBackgroundQueueAndWait:^{
+        SInt64 result = YKF_SCARD_S_SUCCESS;
+        SInt32 card = 0;
+        UInt32 attrLength = 0;
+        
+        result = YKFSCardGetAttrib(card, YKF_SCARD_ATTR_VENDOR_IFD_SERIAL_NO, nil, &attrLength);
+        XCTAssert(attrLength == self.pcscLayer.getCardSerialResponse.length + 1, @"Invalid length returned for the attribute.");
+        
+        XCTAssertEqual(result, YKF_SCARD_S_SUCCESS, @"PC/SC did not return success when reading the serial attribute.");
+    }];
+}
+
+- (void)test_WhenAskingForUnsupportedAttributes_ErrorIsReturned {
+    self.pcscLayer.cardIsValidResponse = YES;
+    self.pcscLayer.contextIsValidResponse = YES;
+    
+    [self executeOnBackgroundQueueAndWait:^{
+        SInt64 result = YKF_SCARD_S_SUCCESS;
+        SInt32 card = 0;
+        UInt32 attrLength = 0;
+        
+        result = YKFSCardGetAttrib(card, 0, nil, &attrLength);
+        XCTAssert(attrLength == 0, @"Invalid length returned for the attribute.");
+        
+        XCTAssertEqual(result, YKF_SCARD_E_UNSUPPORTED_FEATURE, @"PC/SC did not return error when reading unknown attribute.");
+    }];
+}
+
+#pragma mark - Transmit
+
+- (void)test_WhenSendingAPDUCommand_TheCommandIsSentToThePCSCLayer {
+    self.pcscLayer.cardIsValidResponse = YES;
+    self.pcscLayer.contextIsValidResponse = YES;
+    self.pcscLayer.transmitResponse = YKF_SCARD_S_SUCCESS;
+    
+    const UInt8 response[] = {0x90, 0x00};
+    self.pcscLayer.transmitResponseParam = [NSData dataWithBytes:response length:2];
+    
+    [self executeOnBackgroundQueueAndWait:^{
+        SInt64 result = YKF_SCARD_S_SUCCESS;
+        SInt32 card = 0;
+        UInt32 recvLength = 2;
+        const UInt8 command[] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06};
+        
+        UInt8 *recvBuffer = malloc(self.pcscLayer.transmitResponseParam.length);
+        XCTAssert(recvBuffer != nil, @"Could not allocate PC/SC receive buffer for transmit.");
+        
+        result = YKFSCardTransmit(card, nil, command, 6, nil, recvBuffer, &recvLength);
+
+        XCTAssertEqual(result, YKF_SCARD_S_SUCCESS, @"Could not send command over PC/SC.");
+        XCTAssertEqual(recvLength, 2, @"Invalid length returned by the PC/SC transmit API.");
+        
+        free(recvBuffer);
+    }];
+}
+
+- (void)test_WhenSendingAPDUCommandWithWrongBufferLength_ErrorIsReturned {
+    self.pcscLayer.cardIsValidResponse = YES;
+    self.pcscLayer.contextIsValidResponse = YES;
+    self.pcscLayer.transmitResponse = YKF_SCARD_S_SUCCESS;
+    
+    const UInt8 response[] = {0x01, 0x02, 0x03, 0x90, 0x00};
+    self.pcscLayer.transmitResponseParam = [NSData dataWithBytes:response length:5];
+    
+    [self executeOnBackgroundQueueAndWait:^{
+        SInt64 result = YKF_SCARD_S_SUCCESS;
+        SInt32 card = 0;
+        UInt32 recvLength = 2;
+        const UInt8 command[] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06};
+        
+        UInt8 *recvBuffer = malloc(self.pcscLayer.transmitResponseParam.length);
+        XCTAssert(recvBuffer != nil, @"Could not allocate PC/SC receive buffer for transmit.");
+        
+        result = YKFSCardTransmit(card, nil, command, 6, nil, recvBuffer, &recvLength);
+        
+        XCTAssertEqual(result, YKF_SCARD_E_INSUFFICIENT_BUFFER, @"Error not returned when sending the wrong buffer length.");
+        XCTAssertEqual(recvLength, 5, @"Correct length not returned by the PC/SC transmit.");
+        
+        free(recvBuffer);
+    }];
+}
+
+#pragma mark - Helpers
+
+- (void)executeOnBackgroundQueueAndWait:(void(^)(void))block {
+    XCTestExpectation *expectation = [[XCTestExpectation alloc] initWithDescription:@"PCSC execution expectation."];
+    
+    dispatch_async(dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0), ^{
+        block();
+        [expectation fulfill];
+    });
+    
+    XCTWaiterResult result = [XCTWaiter waitForExpectations:@[expectation] timeout:15];
+    XCTAssert(result == XCTWaiterResultCompleted, @"PCSC execution did timeout.");
+}
+
+@end
diff --git a/YubiKit/YubiKitTests/Tests/YKFTestCase.h b/YubiKit/YubiKitTests/Tests/YKFTestCase.h
new file mode 100755
index 000000000..d7f49ab4b
--- /dev/null
+++ b/YubiKit/YubiKitTests/Tests/YKFTestCase.h
@@ -0,0 +1,24 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <XCTest/XCTest.h>
+
+@interface YKFTestCase: XCTestCase
+ 
+- (void)waitForTimeInterval:(NSTimeInterval)timeInterval;
+
+/// Builds a data with an array of bytes
+- (NSData *)dataWithBytes:(NSArray *)bytes;
+
+@end
diff --git a/YubiKit/YubiKitTests/Tests/YKFTestCase.m b/YubiKit/YubiKitTests/Tests/YKFTestCase.m
new file mode 100755
index 000000000..a13be3eb8
--- /dev/null
+++ b/YubiKit/YubiKitTests/Tests/YKFTestCase.m
@@ -0,0 +1,36 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "YKFTestCase.h"
+
+@implementation YKFTestCase
+
+- (void)waitForTimeInterval:(NSTimeInterval)timeInterval {
+    XCTestExpectation *expectation = [[XCTestExpectation alloc] initWithDescription:@"Delay expectation"];
+    XCTWaiterResult result = [XCTWaiter waitForExpectations:@[expectation] timeout:timeInterval];
+    NSAssert(result == XCTWaiterResultTimedOut, @"Delay expectation failure");
+}
+
+#pragma mark - Data Construction
+
+- (NSData *)dataWithBytes:(NSArray *)bytes {
+    NSMutableData *mutableData = [[NSMutableData alloc] initWithCapacity:bytes.count];
+    for (NSNumber *byte in bytes) {
+        UInt8 byteValue = byte.unsignedCharValue;
+        [mutableData appendBytes:&byteValue length:1];
+    }
+    return [mutableData copy];
+}
+
+@end
diff --git a/YubiKit/YubiKitTests/Tests/YubiKitDeviceCapabilitiesTests.m b/YubiKit/YubiKitTests/Tests/YubiKitDeviceCapabilitiesTests.m
new file mode 100755
index 000000000..72326058c
--- /dev/null
+++ b/YubiKit/YubiKitTests/Tests/YubiKitDeviceCapabilitiesTests.m
@@ -0,0 +1,105 @@
+// Copyright 2018-2019 Yubico AB
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <XCTest/XCTest.h>
+
+#import "YubiKitDeviceCapabilities.h"
+#import "YubiKitDeviceCapabilities+Testing.h"
+
+#import "YKFTestCase.h"
+#import "FakeUIDevice.h"
+
+@interface YubiKitDeviceCapabilitiesTests: YKFTestCase
+@end
+
+@implementation YubiKitDeviceCapabilitiesTests
+
+- (void)tearDown {
+    [super tearDown];
+    YubiKitDeviceCapabilities.fakeUIDevice = nil;
+}
+
+#pragma mark - NFC
+
+- (void)test_WhenRunningInSimulator_NFCIsNotSupported {
+    FakeUIDevice *fakeDevice = [[FakeUIDevice alloc] init];
+    
+    YubiKitDeviceCapabilities.fakeUIDevice = fakeDevice;
+    fakeDevice.ykf_deviceModel = YKFDeviceModelSimulator;
+    
+    XCTAssert(!YubiKitDeviceCapabilities.supportsNFCScanning, @"The device capabilities do not block the simulator for NFC");
+}
+
+- (void)test_WhenRunningOnOldDevices_NFCIsNotSupported {
+    FakeUIDevice *fakeDevice = [[FakeUIDevice alloc] init];
+    
+    YubiKitDeviceCapabilities.fakeUIDevice = fakeDevice;
+    
+    fakeDevice.ykf_deviceModel = YKFDeviceModelIPhone5;
+    XCTAssert(!YubiKitDeviceCapabilities.supportsNFCScanning, @"The device capabilities do not block old devices for NFC");
+    
+    fakeDevice.ykf_deviceModel = YKFDeviceModelIPhone6;
+    XCTAssert(!YubiKitDeviceCapabilities.supportsNFCScanning, @"The device capabilities do not block old devices for NFC");
+    
+    fakeDevice.ykf_deviceModel = YKFDeviceModelIPhone6S;
+    XCTAssert(!YubiKitDeviceCapabilities.supportsNFCScanning, @"The device capabilities do not block old devices for NFC");
+}
+
+#pragma mark - MFI Accessory
+
+- (void)test_WhenRunningUnsupportedOSVersions_MFIAccessoryKeyIsNotSupported {
+    FakeUIDevice *fakeDevice = [[FakeUIDevice alloc] init];
+    
+    YubiKitDeviceCapabilities.fakeUIDevice = fakeDevice;
+    fakeDevice.ykf_deviceModel = YKFDeviceModelIPhone6;
+    
+    NSArray *unsupportedVersions = @[@"11.2", @"11.2.1", @"11.2.2", @"11.2.5"];
+    for (NSString *version in unsupportedVersions) {
+        fakeDevice.systemVersion = version;
+        XCTAssert(!YubiKitDeviceCapabilities.supportsMFIAccessoryKey, @"The device capabilities do not block unsupported OS version: %@.", version);
+    }
+}
+
+- (void)test_WhenRunningSupportedOSVersions_MFIAccessoryKeyIsSupported {
+    FakeUIDevice *fakeDevice = [[FakeUIDevice alloc] init];
+    
+    YubiKitDeviceCapabilities.fakeUIDevice = fakeDevice;
+    fakeDevice.ykf_deviceModel = YKFDeviceModelIPhone6;
+    
+    NSArray *unsupportedVersions = @[@"10.0", @"11.0", @"11.1", @"11.3", @"12.0", @"12.2", @"12.3"];
+    for (NSString *version in unsupportedVersions) {
+        fakeDevice.systemVersion = version;
+        XCTAssert(YubiKitDeviceCapabilities.supportsMFIAccessoryKey, @"The device capabilities do not allow supported OS version: %@.", version);
+    }
+}
+
+- (void)test_WhenRunningInSimulator_MFIAccessoryKeyIsNotSupported {
+    FakeUIDevice *fakeDevice = [[FakeUIDevice alloc] init];
+    
+    YubiKitDeviceCapabilities.fakeUIDevice = fakeDevice;
+    fakeDevice.ykf_deviceModel = YKFDeviceModelSimulator;
+    
+    XCTAssert(!YubiKitDeviceCapabilities.supportsMFIAccessoryKey, @"The device capabilities do not block the simulator.");
+}
+
+- (void)test_WhenRunningOnUSBCDevices_MFIAccessoryKeyIsNotSupported {
+    FakeUIDevice *fakeDevice = [[FakeUIDevice alloc] init];
+    
+    YubiKitDeviceCapabilities.fakeUIDevice = fakeDevice;
+    fakeDevice.ykf_deviceModel = YKFDeviceModelIPadPro3;
+    
+    XCTAssert(!YubiKitDeviceCapabilities.supportsMFIAccessoryKey, @"The device capabilities do not block USB-C devices.");
+}
+
+@end