From 7e75fe8f057acf9bf7ac134d375783a1d409f79e Mon Sep 17 00:00:00 2001 From: Jens Utbult Date: Tue, 26 Oct 2021 17:38:33 +0200 Subject: [PATCH] YubiKit 4.1 (#71) * Add timestamp to calculate OATH OTP. * Add timestamp to calculate-all OATH OTP. * Make version public in YKFManagementSession. * If a connection is present when the delegate is set on the YubiKitManager it will immediately call the didConnect delegate methods. * Refactored Management session to use TKTLVRecord instead of our own tlv-parsing. * Removed extra dot at end of version string. * Forgot to make delegate weak when moving it to a class variable. * Fixed bug that made it impossible to unlock with password if a command had failed with not authenticated. * Added stopWithErrorMessage: to nfc connection. * Added stopNFCConnectionWithErrorMessage: to YubiKitManager. * Improved Swift method translation. * Added stopNFCConnectionWithMessage: to YubiKitManager. * Forgot implementation of stopWithMessage:. * Added dispatchAfterCurrentCommands to sessions to enable running a code block after all enqueued commands have completed. * Only signal NFC disconnect if previous state was YKFNFCConnectionStateOpen. * Send proper error codes when unlocking the OATH application * Remove unused error definition. * Remove unused files from project file as well. * Return defined errors for wrong pin and pin entry locked. * Less fragile comparision of algorithm type. * Fixed bug when we tried to calculate more than 8 OATH credentials. * Accidentally sent a auth required error instead of touch timeout. * Fixed bug in authentication/touch timeout error handling. * Reverted to previous behaviour where we singaled an error for NFC modal timeout and user cancel. * New delegate method to catch NFC timeout and user cancelling the NFC modal. * Make the didFailConnectingNFC method optional in the connection protocol. * Fix broken non-trunkated calculation of OATH code. * Remove broken OATH tests. * Improved error checks and fixes a bug where authenticateWithManagementKey would fail to call its completion handler upon failure. * Improved array out of bounds checks and more cautious logging. * Bumped version number to 4.1.0 and updated changes. --- Changelog.md | 21 ++ README.md | 2 +- YubiKit.podspec | 2 +- YubiKit/YubiKit.xcodeproj/project.pbxproj | 48 ++-- .../YKFAccessoryConnection+Private.h | 1 + .../YKFAccessoryConnection.m | 4 + .../YKFAccessoryConnectionController.m | 39 --- .../NFCConnection/YKFNFCConnection+Private.h | 2 + .../NFCConnection/YKFNFCConnection.h | 16 ++ .../NFCConnection/YKFNFCConnection.m | 52 +++- .../YKFNFCConnectionController.m | 39 --- .../YKFQRCodeScanViewController.m | 2 - .../Shared/APDU/MGMT/YKFManagementWriteAPDU.m | 5 +- .../Shared/APDU/U2F/YKFU2FRegisterAPDU.m | 21 +- .../Shared/Errors/YKFManagementError.h | 23 -- .../Shared/Errors/YKFManagementError.m | 35 --- ...agementReadConfigurationResponse+Private.h | 25 -- .../YKFManagementReadConfigurationResponse.h | 39 --- .../YKFManagementReadConfigurationResponse.m | 104 -------- .../ChalResp/YKFChallengeResponseSession.h | 4 +- .../ChalResp/YKFChallengeResponseSession.m | 6 - .../Sessions/FIDO2/CBOR/YKFCBOREncoder.m | 4 +- .../Shared/Sessions/FIDO2/YKFFIDO2Session.h | 3 +- .../Shared/Sessions/FIDO2/YKFFIDO2Session.m | 9 +- ...ManagementInterfaceConfiguration+Private.h | 17 +- .../YKFManagementInterfaceConfiguration.h | 13 +- .../YKFManagementInterfaceConfiguration.m | 41 ++-- .../Sessions/MGMT/YKFManagementSession.h | 144 +++++------ .../Sessions/MGMT/YKFManagementSession.m | 46 ++-- .../Shared/Sessions/OATH/YKFOATHCode.m | 13 +- .../Shared/Sessions/OATH/YKFOATHSession.h | 46 +++- .../Shared/Sessions/OATH/YKFOATHSession.m | 33 ++- .../Shared/Sessions/PIV/YKFPIVPadding.m | 20 +- .../Shared/Sessions/PIV/YKFPIVSession.h | 9 +- .../Shared/Sessions/PIV/YKFPIVSession.m | 35 ++- .../Shared/Sessions/U2F/YKFU2FSession.h | 3 +- .../Shared/Sessions/U2F/YKFU2FSession.m | 1 - .../YKFManagementDeviceInfo+Private.h | 50 ++++ .../Shared/Sessions/YKFManagementDeviceInfo.h | 47 ++++ .../Shared/Sessions/YKFManagementDeviceInfo.m | 86 +++++++ .../Sessions/YKFManagementSessionFeatures.h | 19 ++ .../Sessions/YKFManagementSessionFeatures.m | 35 +++ .../Shared/Sessions/YKFSession+Private.h | 28 +++ .../Connections/Shared/Sessions/YKFSession.h | 10 +- .../Connections/Shared/Sessions/YKFSession.m | 25 +- .../Shared/YKFConnectionControllerProtocol.h | 4 +- .../YubiKit/Connections/Shared/YKFVersion.h | 1 + .../YubiKit/Connections/Shared/YKFVersion.m | 15 ++ .../YKFSmartCardInterface.h | 6 +- .../YKFSmartCardInterface.m | 10 + YubiKit/YubiKit/Layers/PCSC/YKFPCSCLayer.m | 8 +- .../YKFManagementDeviceInfo+Private.h | 1 + .../SPMHeaderLinks/YKFManagementDeviceInfo.h | 1 + .../SPMHeaderLinks/YKFManagementError.h | 1 - ...agementReadConfigurationResponse+Private.h | 1 - .../YKFManagementReadConfigurationResponse.h | 1 - .../YKFManagementSessionFeatures.h | 1 + .../SPMHeaderLinks/YKFSession+Private.h | 1 + .../YubiKit/SharedModel/YKFOTPTokenParser.m | 8 +- YubiKit/YubiKit/YubiKit.h | 2 + YubiKit/YubiKit/YubiKitManager.h | 41 +++- YubiKit/YubiKit/YubiKitManager.m | 50 +++- .../Fakes/FakeYKFConnectionController.m | 4 + .../YKFAccessoryConnectionControllerTests.m | 42 ---- .../Tests/YKFOTPTokenParserTests.m | 16 +- YubiKitTests/Tests/ConnectionTests.swift | 164 +++++++++++++ YubiKitTests/Tests/ManagementTests.swift | 43 +++- YubiKitTests/Tests/OATHTests.swift | 227 +++++++++++++----- YubiKitTests/Tests/PIVTests.swift | 12 +- YubiKitTests/Tests/SessionTests.swift | 30 +++ .../Tests/Utilities/YubiKeyConnection.swift | 6 +- .../YubiKitTests.xcodeproj/project.pbxproj | 8 +- 72 files changed, 1210 insertions(+), 721 deletions(-) delete mode 100644 YubiKit/YubiKit/Connections/Shared/Errors/YKFManagementError.h delete mode 100644 YubiKit/YubiKit/Connections/Shared/Errors/YKFManagementError.m delete mode 100644 YubiKit/YubiKit/Connections/Shared/Requests/MGMT/YKFManagementReadConfigurationResponse+Private.h delete mode 100644 YubiKit/YubiKit/Connections/Shared/Requests/MGMT/YKFManagementReadConfigurationResponse.h delete mode 100644 YubiKit/YubiKit/Connections/Shared/Requests/MGMT/YKFManagementReadConfigurationResponse.m create mode 100644 YubiKit/YubiKit/Connections/Shared/Sessions/YKFManagementDeviceInfo+Private.h create mode 100644 YubiKit/YubiKit/Connections/Shared/Sessions/YKFManagementDeviceInfo.h create mode 100644 YubiKit/YubiKit/Connections/Shared/Sessions/YKFManagementDeviceInfo.m create mode 100644 YubiKit/YubiKit/Connections/Shared/Sessions/YKFManagementSessionFeatures.h create mode 100644 YubiKit/YubiKit/Connections/Shared/Sessions/YKFManagementSessionFeatures.m create mode 100644 YubiKit/YubiKit/Connections/Shared/Sessions/YKFSession+Private.h create mode 120000 YubiKit/YubiKit/SPMHeaderLinks/YKFManagementDeviceInfo+Private.h create mode 120000 YubiKit/YubiKit/SPMHeaderLinks/YKFManagementDeviceInfo.h delete mode 120000 YubiKit/YubiKit/SPMHeaderLinks/YKFManagementError.h delete mode 120000 YubiKit/YubiKit/SPMHeaderLinks/YKFManagementReadConfigurationResponse+Private.h delete mode 120000 YubiKit/YubiKit/SPMHeaderLinks/YKFManagementReadConfigurationResponse.h create mode 120000 YubiKit/YubiKit/SPMHeaderLinks/YKFManagementSessionFeatures.h create mode 120000 YubiKit/YubiKit/SPMHeaderLinks/YKFSession+Private.h create mode 100644 YubiKitTests/Tests/ConnectionTests.swift diff --git a/Changelog.md b/Changelog.md index 7b357936..71face2b 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,5 +1,26 @@ # YubiKit Changelog +## 4.1.0 + +- Optional timestamp parameter added to OATH calculate and calculateAll methods. +- Firmware version is now a public variable on `YKFManagementSession`. +- If a connection is already present when setting the `YKFManagerDelegate` it will return that connection immideatly. +- Extra dot at the end of `YKFVersion` string removed. +- Fixed memory issues where we retained the `YKFManagerDelegate`. +- Fixed issue where failing to unlock a key with passcode before sending an OATH command got the session in a non recoverable state. +- Improved control over the messages displayed in the NFC dialog. +- Added `- (void)dispatchBlockOnCommunicationQueue:(YKFConnectionControllerCommunicationQueueBlock)block` to `YKFConnectionControllerProtocol` that will run a block after all enqueued commands has finished. +- Improved error handling in OATH session. +- More robust algorithm comparison in PIV session. +- Fixed bug where an auth required error was sent instead of touch timeout in OATH session. +- Fixed bug where the number of OATH accounts you could read was limited to around 8 +- Added new optional connection delegate method that will signal if the NFC dialog was cancelled by the user or timed out. +- Swift package manager header files exluded from Cocoapod distribution. +- Various array out of bounds checks +- Improved error checks +- Fixes bug where authenticateWithManagementKey in the YKFPIVSession would fail to call its completion handler upon failure. +- Fixes broken implementation of non truncated OATH codes + ## 4.0.0 This release breaks backwards compatibility with previous versions of the SDK. The reason for this is to make the SDK easier to diff --git a/README.md b/README.md index b1c83be3..186a38a4 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ Add YubiKit to your [Podfile](https://guides.cocoapods.org/using/the-podfile.htm ```ruby use_frameworks! -pod 'YubiKit', '~> 4.0.0' +pod 'YubiKit', '~> 4.1.0' ``` If you want to have latest changes, replace the last line with: diff --git a/YubiKit.podspec b/YubiKit.podspec index f5e9b036..934c7745 100644 --- a/YubiKit.podspec +++ b/YubiKit.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'YubiKit' - s.version = '4.0.0' + s.version = '4.1.0' s.license = 'Apache 2.0' s.summary = 'YubiKit is an iOS library provided by Yubico to interact with YubiKeys on iOS devices.' s.homepage = 'https://github.com/Yubico/yubikit-ios' diff --git a/YubiKit/YubiKit.xcodeproj/project.pbxproj b/YubiKit/YubiKit.xcodeproj/project.pbxproj index 7f3d9b84..110966bc 100644 --- a/YubiKit/YubiKit.xcodeproj/project.pbxproj +++ b/YubiKit/YubiKit.xcodeproj/project.pbxproj @@ -28,10 +28,11 @@ 51E1B97B25765454003C1CA4 /* YKFOATHCredentialTemplate.m in Sources */ = {isa = PBXBuildFile; fileRef = 51E1B97A25765454003C1CA4 /* YKFOATHCredentialTemplate.m */; }; 51E1B9862577993C003C1CA4 /* YKFOATHCredentialUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 51E1B9852577993C003C1CA4 /* YKFOATHCredentialUtils.m */; }; 51E1B9932577EF05003C1CA4 /* YKFOATHCredentialWithCode.m in Sources */ = {isa = PBXBuildFile; fileRef = 51E1B9922577EF05003C1CA4 /* YKFOATHCredentialWithCode.m */; }; + 51F8E3C42639856A0010686B /* YKFManagementDeviceInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = 51F8E3C32639856A0010686B /* YKFManagementDeviceInfo.m */; }; + 51F8E3C7263989520010686B /* YKFManagementSessionFeatures.m in Sources */ = {isa = PBXBuildFile; fileRef = 51F8E3C6263989520010686B /* YKFManagementSessionFeatures.m */; }; 81311F3A23AAFA4A00765522 /* YKFChallengeResponseSession.m in Sources */ = {isa = PBXBuildFile; fileRef = 81311F3923AAFA4A00765522 /* YKFChallengeResponseSession.m */; }; 813271EC240F265B0084E105 /* YKFManagementSession.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 814813E923EA381F0003893B /* YKFManagementSession.h */; }; 814813D123EA37F60003893B /* YKFManagementWriteAPDU.m in Sources */ = {isa = PBXBuildFile; fileRef = 814813CD23EA37F60003893B /* YKFManagementWriteAPDU.m */; }; - 814813E023EA380A0003893B /* YKFManagementReadConfigurationResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = 814813D323EA380A0003893B /* YKFManagementReadConfigurationResponse.m */; }; 814813EB23EA381F0003893B /* YKFManagementSession.m in Sources */ = {isa = PBXBuildFile; fileRef = 814813E823EA381F0003893B /* YKFManagementSession.m */; }; 814813EC23EA381F0003893B /* YKFManagementInterfaceConfiguration.m in Sources */ = {isa = PBXBuildFile; fileRef = 814813EA23EA381F0003893B /* YKFManagementInterfaceConfiguration.m */; }; 815233FE23B56A6F004D4788 /* YKFChalRespSendRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = 815233FD23B56A6F004D4788 /* YKFChalRespSendRequest.m */; }; @@ -42,8 +43,6 @@ 8152341223BAE9D2004D4788 /* YKFChallengeResponseError.m in Sources */ = {isa = PBXBuildFile; fileRef = 8152341123BAE9D2004D4788 /* YKFChallengeResponseError.m */; }; 816C684B2343126100209342 /* YKFNFCTagDescription.m in Sources */ = {isa = PBXBuildFile; fileRef = 816C684A2343126100209342 /* YKFNFCTagDescription.m */; }; 816C684E23431BE600209342 /* YKFNFCTagDescription.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 816C68492343126100209342 /* YKFNFCTagDescription.h */; }; - 81F94D8A23F2371200475A70 /* YKFManagementError.m in Sources */ = {isa = PBXBuildFile; fileRef = 81F94D8923F2371200475A70 /* YKFManagementError.m */; }; - 81F94D8F23F4DF1E00475A70 /* YKFManagementReadConfigurationResponse.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 814813D523EA380A0003893B /* YKFManagementReadConfigurationResponse.h */; }; 81F94D9123F4DF4400475A70 /* YKFManagementInterfaceConfiguration.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 81F94D8523EE246C00475A70 /* YKFManagementInterfaceConfiguration.h */; }; 81FD3B9424048295004C4FE9 /* YKFOATHSelectApplicationResponse.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 95D61A052170B26F001E7AC8 /* YKFOATHSelectApplicationResponse.h */; }; 81FD3B982404889C004C4FE9 /* YKFVersion.m in Sources */ = {isa = PBXBuildFile; fileRef = 81FD3B972404889C004C4FE9 /* YKFVersion.m */; }; @@ -237,7 +236,6 @@ 813271EC240F265B0084E105 /* YKFManagementSession.h in CopyFiles */, 81FD3B99240488F6004C4FE9 /* YKFVersion.h in CopyFiles */, 81F94D9123F4DF4400475A70 /* YKFManagementInterfaceConfiguration.h in CopyFiles */, - 81F94D8F23F4DF1E00475A70 /* YKFManagementReadConfigurationResponse.h in CopyFiles */, 8152340D23B59B85004D4788 /* YKFSlot.h in CopyFiles */, 8152340C23B59A4A004D4788 /* YKFChallengeResponseSession.h in CopyFiles */, 816C684E23431BE600209342 /* YKFNFCTagDescription.h in CopyFiles */, @@ -327,6 +325,7 @@ 51ACC34925E7EC910069214B /* YKFPIVError.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YKFPIVError.h; sourceTree = ""; }; 51ACC34A25E7ECA80069214B /* YKFPIVError.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YKFPIVError.m; sourceTree = ""; }; 51B48A0B2552D60B00D4C7E8 /* YKFChallengeResponseSession+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "YKFChallengeResponseSession+Private.h"; sourceTree = ""; }; + 51D1E84F2643179E00BDA3FF /* YKFSession+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "YKFSession+Private.h"; sourceTree = ""; }; 51E1B97A25765454003C1CA4 /* YKFOATHCredentialTemplate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YKFOATHCredentialTemplate.m; sourceTree = ""; }; 51E1B98025765547003C1CA4 /* YKFOATHCredentialTemplate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YKFOATHCredentialTemplate.h; sourceTree = ""; }; 51E1B9812576565E003C1CA4 /* YKFOATHCredentialTypes.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YKFOATHCredentialTypes.h; sourceTree = ""; }; @@ -334,13 +333,16 @@ 51E1B9852577993C003C1CA4 /* YKFOATHCredentialUtils.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YKFOATHCredentialUtils.m; sourceTree = ""; }; 51E1B9922577EF05003C1CA4 /* YKFOATHCredentialWithCode.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YKFOATHCredentialWithCode.m; sourceTree = ""; }; 51E1B9962577EF25003C1CA4 /* YKFOATHCredentialWithCode.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YKFOATHCredentialWithCode.h; sourceTree = ""; }; + 51F8E3C2263985560010686B /* YKFManagementDeviceInfo.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YKFManagementDeviceInfo.h; sourceTree = ""; }; + 51F8E3C32639856A0010686B /* YKFManagementDeviceInfo.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YKFManagementDeviceInfo.m; sourceTree = ""; }; + 51F8E3C52639893D0010686B /* YKFManagementSessionFeatures.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YKFManagementSessionFeatures.h; sourceTree = ""; }; + 51F8E3C6263989520010686B /* YKFManagementSessionFeatures.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YKFManagementSessionFeatures.m; sourceTree = ""; }; + 51F8E3C8263A95AB0010686B /* YKFManagementDeviceInfo+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "YKFManagementDeviceInfo+Private.h"; sourceTree = ""; }; 811F91EF2383C57B002158ED /* Changelog.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; name = Changelog.md; path = ../Changelog.md; sourceTree = ""; }; 81311F3823AAFA4A00765522 /* YKFChallengeResponseSession.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YKFChallengeResponseSession.h; sourceTree = ""; }; 81311F3923AAFA4A00765522 /* YKFChallengeResponseSession.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YKFChallengeResponseSession.m; sourceTree = ""; }; 814813CC23EA37F60003893B /* YKFManagementWriteAPDU.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = YKFManagementWriteAPDU.h; path = MGMT/YKFManagementWriteAPDU.h; sourceTree = ""; }; 814813CD23EA37F60003893B /* YKFManagementWriteAPDU.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = YKFManagementWriteAPDU.m; path = MGMT/YKFManagementWriteAPDU.m; sourceTree = ""; }; - 814813D323EA380A0003893B /* YKFManagementReadConfigurationResponse.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = YKFManagementReadConfigurationResponse.m; path = MGMT/YKFManagementReadConfigurationResponse.m; sourceTree = ""; }; - 814813D523EA380A0003893B /* YKFManagementReadConfigurationResponse.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = YKFManagementReadConfigurationResponse.h; path = MGMT/YKFManagementReadConfigurationResponse.h; sourceTree = ""; }; 814813E823EA381F0003893B /* YKFManagementSession.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = YKFManagementSession.m; path = MGMT/YKFManagementSession.m; sourceTree = ""; }; 814813E923EA381F0003893B /* YKFManagementSession.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = YKFManagementSession.h; path = MGMT/YKFManagementSession.h; sourceTree = ""; }; 814813EA23EA381F0003893B /* YKFManagementInterfaceConfiguration.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = YKFManagementInterfaceConfiguration.m; path = MGMT/YKFManagementInterfaceConfiguration.m; sourceTree = ""; }; @@ -360,9 +362,6 @@ 81DF8FD9237B531700737268 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; name = README.md; path = ../README.md; sourceTree = ""; }; 81DF8FDA237B534D00737268 /* docs */ = {isa = PBXFileReference; lastKnownFileType = folder; name = docs; path = ../docs; sourceTree = ""; }; 81F94D8523EE246C00475A70 /* YKFManagementInterfaceConfiguration.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = YKFManagementInterfaceConfiguration.h; path = MGMT/YKFManagementInterfaceConfiguration.h; sourceTree = ""; }; - 81F94D8623F2060A00475A70 /* YKFManagementReadConfigurationResponse+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "YKFManagementReadConfigurationResponse+Private.h"; path = "MGMT/YKFManagementReadConfigurationResponse+Private.h"; sourceTree = ""; }; - 81F94D8823F2371200475A70 /* YKFManagementError.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = YKFManagementError.h; sourceTree = ""; }; - 81F94D8923F2371200475A70 /* YKFManagementError.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = YKFManagementError.m; sourceTree = ""; }; 81F94D8C23F26F7B00475A70 /* YKFManagementInterfaceConfiguration+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "YKFManagementInterfaceConfiguration+Private.h"; path = "MGMT/YKFManagementInterfaceConfiguration+Private.h"; sourceTree = ""; }; 81FD3B962404889C004C4FE9 /* YKFVersion.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YKFVersion.h; sourceTree = ""; }; 81FD3B972404889C004C4FE9 /* YKFVersion.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YKFVersion.m; sourceTree = ""; }; @@ -695,16 +694,6 @@ name = Management; sourceTree = ""; }; - 814813D223EA37FD0003893B /* Management */ = { - isa = PBXGroup; - children = ( - 814813D523EA380A0003893B /* YKFManagementReadConfigurationResponse.h */, - 814813D323EA380A0003893B /* YKFManagementReadConfigurationResponse.m */, - 81F94D8623F2060A00475A70 /* YKFManagementReadConfigurationResponse+Private.h */, - ); - name = Management; - sourceTree = ""; - }; 814813E623EA38120003893B /* Management */ = { isa = PBXGroup; children = ( @@ -714,6 +703,11 @@ 81F94D8523EE246C00475A70 /* YKFManagementInterfaceConfiguration.h */, 814813EA23EA381F0003893B /* YKFManagementInterfaceConfiguration.m */, 81F94D8C23F26F7B00475A70 /* YKFManagementInterfaceConfiguration+Private.h */, + 51F8E3C2263985560010686B /* YKFManagementDeviceInfo.h */, + 51F8E3C8263A95AB0010686B /* YKFManagementDeviceInfo+Private.h */, + 51F8E3C32639856A0010686B /* YKFManagementDeviceInfo.m */, + 51F8E3C52639893D0010686B /* YKFManagementSessionFeatures.h */, + 51F8E3C6263989520010686B /* YKFManagementSessionFeatures.m */, ); name = Management; sourceTree = ""; @@ -990,14 +984,15 @@ 9581394E21590652008558F3 /* Sessions */ = { isa = PBXGroup; children = ( - 51ACC2F425D5857F0069214B /* PIV */, 958D0B62215D106F00942CB9 /* YKFSession.h */, + 51D1E84F2643179E00BDA3FF /* YKFSession+Private.h */, 958D0B63215D106F00942CB9 /* YKFSession.m */, 51202093255150FF00B0384D /* YKFSessionProtocol+Private.h */, - 814813E623EA38120003893B /* Management */, 81311F3723AAF9F400765522 /* ChalResp */, 95D9D3D921D510A700473888 /* FIDO2 */, + 814813E623EA38120003893B /* Management */, 958139562159281D008558F3 /* OATH */, + 51ACC2F425D5857F0069214B /* PIV */, 958139552159280B008558F3 /* U2F */, ); path = Sessions; @@ -1301,8 +1296,6 @@ 955188272265E4B9001A4191 /* YKFU2FError.m */, 8152341023BAE9D2004D4788 /* YKFChallengeResponseError.h */, 8152341123BAE9D2004D4788 /* YKFChallengeResponseError.m */, - 81F94D8823F2371200475A70 /* YKFManagementError.h */, - 81F94D8923F2371200475A70 /* YKFManagementError.m */, 51ACC34925E7EC910069214B /* YKFPIVError.h */, 51ACC34A25E7ECA80069214B /* YKFPIVError.m */, ); @@ -1315,7 +1308,6 @@ 95081DEC2214255B006CD08C /* YKFRequest.h */, 95081DED2214255B006CD08C /* YKFRequest.m */, 815233FA23B569A1004D4788 /* YKFSlot.h */, - 814813D223EA37FD0003893B /* Management */, 815233FB23B56A00004D4788 /* ChalResp */, 956DBB8A21EE00EC004D6EE3 /* FIDO2 */, 955BCBFE215A431200C2EA2B /* OATH */, @@ -1469,6 +1461,7 @@ buildActionMask = 2147483647; files = ( 95A04D1E2253920B008E3036 /* YKFFIDO2GetNextAssertionAPDU.m in Sources */, + 51F8E3C7263989520010686B /* YKFManagementSessionFeatures.m in Sources */, 95233E552330DEE000C51F92 /* YubiKitLogger.m in Sources */, 814813EC23EA381F0003893B /* YKFManagementInterfaceConfiguration.m in Sources */, 814813D123EA37F60003893B /* YKFManagementWriteAPDU.m in Sources */, @@ -1491,6 +1484,7 @@ 5110D6A72601FBC800467680 /* TKTLVRecordAdditions.m in Sources */, 95DD658E21663C3C00BA85C9 /* YKFOATHDeleteAPDU.m in Sources */, 95C2965420629FD10091318B /* YubiKitDeviceCapabilities.m in Sources */, + 51F8E3C42639856A0010686B /* YKFManagementDeviceInfo.m in Sources */, 95C2964D20628EBE0091318B /* YKFNFCOTPSession.m in Sources */, 95C29635206259050091318B /* UIDeviceAdditions.m in Sources */, 955188302265F4EE001A4191 /* YKFAPDUError.m in Sources */, @@ -1549,7 +1543,6 @@ 957BDF4F21F5C3A700899B5B /* YKFFIDO2MakeCredentialResponse.m in Sources */, 956DB67220639C7D006B1738 /* YKFQRCodeScanViewController.m in Sources */, 95B0CAB021F098C6009C6A34 /* YKFFIDO2GetInfoResponse.m in Sources */, - 814813E023EA380A0003893B /* YKFManagementReadConfigurationResponse.m in Sources */, 95C2963A20625A180091318B /* YKFView.m in Sources */, 958793EE216E0DB2001A0406 /* YKFOATHListResponse.m in Sources */, 95DD408E2099A88500363FEE /* YKFSessionError.m in Sources */, @@ -1561,7 +1554,6 @@ 95B0CAAD21EF53E1009C6A34 /* YKFFIDO2Error.m in Sources */, 95DD40A72099A8A400363FEE /* YKFAccessoryConnection.m in Sources */, 95DD409D2099A89600363FEE /* YKFU2FRegisterResponse.m in Sources */, - 81F94D8A23F2371200475A70 /* YKFManagementError.m in Sources */, 95A4566C2177663800AD5A94 /* YKFOATHCalculateAllResponse.m in Sources */, 953A5079213E9F4600929ABB /* YKFAccessoryDescription.m in Sources */, 956DBB9021EE1E5E004D6EE3 /* YKFFIDO2GetInfoAPDU.m in Sources */, @@ -1605,7 +1597,7 @@ CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = LQA3CS5MM7; INFOPLIST_FILE = YubiKitTests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; OTHER_LDFLAGS = "-ObjcC"; PRODUCT_BUNDLE_IDENTIFIER = com.yubico.YubiKitTests; @@ -1621,7 +1613,7 @@ CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = LQA3CS5MM7; INFOPLIST_FILE = YubiKitTests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; OTHER_LDFLAGS = "-ObjcC"; PRODUCT_BUNDLE_IDENTIFIER = com.yubico.YubiKitTests; diff --git a/YubiKit/YubiKit/Connections/AccessoryConnection/YKFAccessoryConnection+Private.h b/YubiKit/YubiKit/Connections/AccessoryConnection/YKFAccessoryConnection+Private.h index cb64dfb1..247ccae2 100644 --- a/YubiKit/YubiKit/Connections/AccessoryConnection/YKFAccessoryConnection+Private.h +++ b/YubiKit/YubiKit/Connections/AccessoryConnection/YKFAccessoryConnection+Private.h @@ -29,6 +29,7 @@ typedef void (^YKFAccessoryConnectionStateChangeBlock)(YKFAccessoryConnectionSta @interface YKFAccessoryConnection() +@property (nonatomic, readonly) YKFAccessoryConnectionState state; @property(nonatomic, weak) id _Nullable delegate; /* diff --git a/YubiKit/YubiKit/Connections/AccessoryConnection/YKFAccessoryConnection.m b/YubiKit/YubiKit/Connections/AccessoryConnection/YKFAccessoryConnection.m index 018f5d12..576caf11 100644 --- a/YubiKit/YubiKit/Connections/AccessoryConnection/YKFAccessoryConnection.m +++ b/YubiKit/YubiKit/Connections/AccessoryConnection/YKFAccessoryConnection.m @@ -117,6 +117,10 @@ - (instancetype)initWithAccessoryManager:(id)acce return self; } +- (YKFAccessoryConnectionState)state { + return _connectionState; +} + - (YKFSmartCardInterface *)smartCardInterface { if (!self.connectionController) { return nil; diff --git a/YubiKit/YubiKit/Connections/AccessoryConnection/YKFAccessoryConnectionController.m b/YubiKit/YubiKit/Connections/AccessoryConnection/YKFAccessoryConnectionController.m index 478792e5..1802f107 100644 --- a/YubiKit/YubiKit/Connections/AccessoryConnection/YKFAccessoryConnectionController.m +++ b/YubiKit/YubiKit/Connections/AccessoryConnection/YKFAccessoryConnectionController.m @@ -23,8 +23,6 @@ #import "YKFSessionError+Private.h" #import "YKFAPDU+Private.h" -typedef void (^YKFConnectionControllerCommunicationQueueBlock)(NSOperation *operation); - @interface YKFAccessoryConnectionController() @property (nonatomic) NSOperationQueue *communicationQueue; @@ -310,43 +308,6 @@ - (void)execute:(YKFAPDU *)command timeout:(NSTimeInterval)timeout completion:(Y }]; } -- (void)dispatchOnSequentialQueue:(YKFConnectionControllerCompletionBlock)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:(YKFConnectionControllerCompletionBlock)block { - YKFParameterAssertReturn(block); - [self dispatchOnSequentialQueue:block delay:0]; -} - - (void)cancelAllCommands { self.communicationQueue.suspended = YES; dispatch_suspend(self.communicationQueue.underlyingQueue); diff --git a/YubiKit/YubiKit/Connections/NFCConnection/YKFNFCConnection+Private.h b/YubiKit/YubiKit/Connections/NFCConnection/YKFNFCConnection+Private.h index e18a46c7..62ad9964 100644 --- a/YubiKit/YubiKit/Connections/NFCConnection/YKFNFCConnection+Private.h +++ b/YubiKit/YubiKit/Connections/NFCConnection/YKFNFCConnection+Private.h @@ -21,11 +21,13 @@ - (void)didConnectNFC:(YKFNFCConnection *_Nonnull)connection; - (void)didDisconnectNFC:(YKFNFCConnection *_Nonnull)connection error:(NSError *_Nullable)error; +- (void)didFailConnectingNFC:(NSError *_Nonnull)error; @end @interface YKFNFCConnection() +@property (nonatomic, readonly) YKFNFCConnectionState state; @property(nonatomic, weak) id _Nullable delegate; @end diff --git a/YubiKit/YubiKit/Connections/NFCConnection/YKFNFCConnection.h b/YubiKit/YubiKit/Connections/NFCConnection/YKFNFCConnection.h index 7d27df01..aabff39b 100644 --- a/YubiKit/YubiKit/Connections/NFCConnection/YKFNFCConnection.h +++ b/YubiKit/YubiKit/Connections/NFCConnection/YKFNFCConnection.h @@ -73,6 +73,22 @@ typedef NS_ENUM(NSUInteger, YKFNFCConnectionState) { */ - (void)stop API_AVAILABLE(ios(13.0)); +/*! + @method stopWithMessage: + + @abstract + Closes the communication with the key and display a message. + */ +- (void)stopWithMessage:(NSString *)message API_AVAILABLE(ios(13.0)); + +/*! + @method stopWithErrorMessage: + + @abstract + Closes the communication with the key and display an error message. + */ +- (void)stopWithErrorMessage:(NSString *)errorMessage API_AVAILABLE(ios(13.0)); + /*! @method cancelCommands diff --git a/YubiKit/YubiKit/Connections/NFCConnection/YKFNFCConnection.m b/YubiKit/YubiKit/Connections/NFCConnection/YKFNFCConnection.m index a37cfa7f..3854480d 100644 --- a/YubiKit/YubiKit/Connections/NFCConnection/YKFNFCConnection.m +++ b/YubiKit/YubiKit/Connections/NFCConnection/YKFNFCConnection.m @@ -69,6 +69,10 @@ - (instancetype)init { return self; } +- (YKFNFCConnectionState)state { + return _nfcConnectionState; +} + - (YKFSmartCardInterface *)smartCardInterface { if (!self.connectionController) { return nil; @@ -162,9 +166,29 @@ - (void)stop API_AVAILABLE(ios(13.0)) { } [self setAlertMessage:YubiKitExternalLocalization.nfcScanSuccessAlertMessage]; - [self updateServicesForSession:self.nfcTagReaderSession tag:nil state:YKFNFCConnectionStateClosed]; + [self updateServicesForSession:self.nfcTagReaderSession tag:nil state:YKFNFCConnectionStateClosed errorMessage:nil]; +} + +- (void)stopWithMessage:(NSString *)message API_AVAILABLE(ios(13.0)) { + if (!self.nfcTagReaderSession) { + YKFLogInfo(@"NFC session already stopped. Ignoring stop request."); + return; + } + + [self setAlertMessage:message]; + [self updateServicesForSession:self.nfcTagReaderSession tag:nil state:YKFNFCConnectionStateClosed errorMessage:nil]; +} + +- (void)stopWithErrorMessage:(NSString *)errorMessage API_AVAILABLE(ios(13.0)) { + if (!self.nfcTagReaderSession) { + YKFLogInfo(@"NFC session already stopped. Ignoring stop request."); + return; + } + + [self updateServicesForSession:self.nfcTagReaderSession tag:nil state:YKFNFCConnectionStateClosed errorMessage:errorMessage]; } + - (void)cancelCommands API_AVAILABLE(ios(13.0)) { [self.connectionController cancelAllCommands]; } @@ -204,7 +228,7 @@ - (void)tagReaderSession:(NFCTagReaderSession *)session didInvalidateWithError:( - (void)tagReaderSessionDidBecomeActive:(NFCTagReaderSession *)session API_AVAILABLE(ios(13.0)) { YKFLogInfo(@"NFC session did become active."); self.nfcTagReaderSession = session; - [self updateServicesForSession:session tag:nil state:YKFNFCConnectionStatePolling]; + [self updateServicesForSession:session tag:nil state:YKFNFCConnectionStatePolling errorMessage:nil]; } - (void)tagReaderSession:(NFCTagReaderSession *)session didDetectTags:(NSArray<__kindof id> *)tags API_AVAILABLE(ios(13.0)) { @@ -240,7 +264,7 @@ - (void)tagReaderSession:(NFCTagReaderSession *)session didDetectTags:(NSArray<_ } YKFLogInfo(@"NFC session did connect to tag."); - [strongSelf updateServicesForSession:session tag:activeTag state:YKFNFCConnectionStateOpen]; + [strongSelf updateServicesForSession:session tag:activeTag state:YKFNFCConnectionStateOpen errorMessage:nil]; }]; } @@ -258,10 +282,10 @@ - (void)updateServicesForSession:(NFCTagReaderSession *)session error:(NSError * self.nfcConnectionError = error; [self.nfcTagReaderSession invalidateSessionWithErrorMessage:error.localizedDescription]; - [self updateServicesForSession:session tag:nil state:YKFNFCConnectionStateClosed]; + [self updateServicesForSession:session tag:nil state:YKFNFCConnectionStateClosed errorMessage:nil]; } -- (void)updateServicesForSession:(NFCTagReaderSession *)session tag:(id)tag state:(YKFNFCConnectionState)state API_AVAILABLE(ios(13.0)) { +- (void)updateServicesForSession:(NFCTagReaderSession *)session tag:(id)tag state:(YKFNFCConnectionState)state errorMessage:(NSString *)errorMessage API_AVAILABLE(ios(13.0)) { if (self.nfcConnectionState == state) { return; } @@ -269,16 +293,27 @@ - (void)updateServicesForSession:(NFCTagReaderSession *)session tag:(id -#import "YKFSessionError.h" -NS_ASSUME_NONNULL_BEGIN - -typedef NS_ENUM(NSUInteger, YKFManagementErrorCode) { - /*! Key returned malformed response - */ - YKFManagementErrorCodeUnexpectedResponse = 0x300, -}; - -@interface YKFManagementError : YKFSessionError - -@end - -NS_ASSUME_NONNULL_END diff --git a/YubiKit/YubiKit/Connections/Shared/Errors/YKFManagementError.m b/YubiKit/YubiKit/Connections/Shared/Errors/YKFManagementError.m deleted file mode 100644 index 999460d6..00000000 --- a/YubiKit/YubiKit/Connections/Shared/Errors/YKFManagementError.m +++ /dev/null @@ -1,35 +0,0 @@ -// -// YKFManagementError.m -// YubiKit -// -// Created by Irina Makhalova on 2/10/20. -// Copyright © 2020 Yubico. All rights reserved. -// - -#import "YKFManagementError.h" -#import "YKFSessionError+Private.h" - -static NSString* const YKFManagementErrorCodeUnexpectedResponseDescription = @"Invalid response returned"; - -@implementation YKFManagementError - -static NSDictionary *errorMap = nil; - -+ (YKFSessionError *)errorWithCode:(NSUInteger)code { - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - [YKFManagementError buildErrorMap]; - }); - - NSString *errorDescription = errorMap[@(code)]; - if (!errorDescription) { - return [super errorWithCode:code]; - } - return [[YKFSessionError alloc] initWithCode:code message:errorDescription]; -} - -+ (void)buildErrorMap { - errorMap = @{@(YKFManagementErrorCodeUnexpectedResponse): YKFManagementErrorCodeUnexpectedResponseDescription }; -} - -@end diff --git a/YubiKit/YubiKit/Connections/Shared/Requests/MGMT/YKFManagementReadConfigurationResponse+Private.h b/YubiKit/YubiKit/Connections/Shared/Requests/MGMT/YKFManagementReadConfigurationResponse+Private.h deleted file mode 100644 index 866cfb02..00000000 --- a/YubiKit/YubiKit/Connections/Shared/Requests/MGMT/YKFManagementReadConfigurationResponse+Private.h +++ /dev/null @@ -1,25 +0,0 @@ -// -// YKFManagementReadConfigurationResponse.h -// YubiKit -// -// Created by Irina Makhalova on 2/10/20. -// Copyright © 2020 Yubico. All rights reserved. -// - -#import "YKFManagementReadConfigurationResponse.h" - -@interface YKFManagementReadConfigurationResponse() - -@property (nonatomic, readwrite) NSUInteger usbSupportedMask; -@property (nonatomic, readwrite) NSUInteger nfcSupportedMask; - -@property (nonatomic, readwrite) NSUInteger usbEnabledMask; -@property (nonatomic, readwrite) NSUInteger nfcEnabledMask; - -@property (nonatomic, nullable, readwrite) NSData *configurationLocked; - -- (nullable instancetype)initWithKeyResponseData:(nonnull NSData *)responseData version:(YKFVersion *_Nonnull)version NS_DESIGNATED_INITIALIZER; - -@end - - diff --git a/YubiKit/YubiKit/Connections/Shared/Requests/MGMT/YKFManagementReadConfigurationResponse.h b/YubiKit/YubiKit/Connections/Shared/Requests/MGMT/YKFManagementReadConfigurationResponse.h deleted file mode 100644 index 60478322..00000000 --- a/YubiKit/YubiKit/Connections/Shared/Requests/MGMT/YKFManagementReadConfigurationResponse.h +++ /dev/null @@ -1,39 +0,0 @@ -// -// YKFManagementReadConfigurationResponse.h -// YubiKit -// -// Created by Irina Makhalova on 2/3/20. -// Copyright © 2020 Yubico. All rights reserved. -// - -#import -#import "YKFVersion.h" -#import "YKFManagementInterfaceConfiguration.h" - -NS_ASSUME_NONNULL_BEGIN - -typedef NS_ENUM(NSUInteger, YKFManagementReadConfigurationTags) { - YKFManagementReadConfigurationTagsNone = 0x00, - YKFManagementReadConfigurationTagsUsbSupported = 0x01, - YKFManagementReadConfigurationTagsSerialNumber = 0x02, - YKFManagementReadConfigurationTagsUsbEnabled = 0x03, - YKFManagementReadConfigurationTagsFormFactor = 0x04, - YKFManagementReadConfigurationTagsFirmwareVersion = 0x05, - YKFManagementReadConfigurationTagsConfigLocked = 0x0a, - YKFManagementReadConfigurationTagsNfcSupported = 0x0d, - YKFManagementReadConfigurationTagsNfcEnabled = 0x0e -}; - -@interface YKFManagementReadConfigurationResponse : NSObject - -@property (nonatomic, readonly, nullable) YKFManagementInterfaceConfiguration* configuration; -@property (nonatomic, readonly, nonnull) YKFVersion* version; - -@property (nonatomic, readonly) NSUInteger serialNumber; -@property (nonatomic, readonly) NSUInteger formFactor; - -- (instancetype)init NS_UNAVAILABLE; - -@end - -NS_ASSUME_NONNULL_END diff --git a/YubiKit/YubiKit/Connections/Shared/Requests/MGMT/YKFManagementReadConfigurationResponse.m b/YubiKit/YubiKit/Connections/Shared/Requests/MGMT/YKFManagementReadConfigurationResponse.m deleted file mode 100644 index badedafb..00000000 --- a/YubiKit/YubiKit/Connections/Shared/Requests/MGMT/YKFManagementReadConfigurationResponse.m +++ /dev/null @@ -1,104 +0,0 @@ -// -// YKFManagementReadConfigurationResponse.m -// YubiKit -// -// Created by Irina Makhalova on 2/3/20. -// Copyright © 2020 Yubico. All rights reserved. -// - -#import "YKFManagementReadConfigurationResponse.h" -#import "YKFManagementReadConfigurationResponse+Private.h" -#import "YKFAssert.h" -#import "YKFNSDataAdditions+Private.h" -#import "YKFManagementInterfaceConfiguration+Private.h" - -@interface YKFManagementReadConfigurationResponse() - -@property (nonatomic, readwrite, nonnull) YKFVersion *version; -@property (nonatomic, readwrite) NSUInteger serialNumber; -@property (nonatomic, readwrite) NSUInteger formFactor; - -@end - -@implementation YKFManagementReadConfigurationResponse - -- (nullable instancetype)initWithKeyResponseData:(nonnull NSData *)responseData version:(YKFVersion *)version { - YKFAssertAbortInit(responseData.length); - YKFAssertAbortInit(version) - - self = [super init]; - if (self) { - // skipping first byte - NSRange range = NSMakeRange(1, responseData.length - 1); - responseData = [responseData subdataWithRange:range]; - - UInt8 *responseBytes = (UInt8 *)responseData.bytes; - NSUInteger readIndex = 0; - - // setting default version that received from selection of management application on YubiKey - // this value may be overriden with whatever returned with reading configuration command response - self.version = version; - while (readIndex < responseData.length) { - UInt8 responseTag = responseBytes[readIndex]; - - ++readIndex; - YKFAssertAbortInit([responseData ykf_containsIndex:readIndex]); - - UInt8 tagLength = responseBytes[readIndex]; - YKFAssertAbortInit(tagLength > 0); - - ++readIndex; - NSRange tagValueRange = NSMakeRange(readIndex, tagLength); - YKFAssertAbortInit([responseData ykf_containsRange:tagValueRange]); - NSData* value = [responseData subdataWithRange:tagValueRange]; - UInt8* valueBytes = (UInt8*)value.bytes; - - switch (responseTag) { - case YKFManagementReadConfigurationTagsUsbEnabled: - self.usbEnabledMask = [value ykf_integerValue]; - break; - - case YKFManagementReadConfigurationTagsUsbSupported: - self.usbSupportedMask = [value ykf_integerValue]; - break; - - case YKFManagementReadConfigurationTagsSerialNumber: - self.serialNumber = [value ykf_integerValue]; - break; - - case YKFManagementReadConfigurationTagsFormFactor: - self.formFactor = [value ykf_integerValue]; - break; - - case YKFManagementReadConfigurationTagsFirmwareVersion: - YKFAssertAbortInit(tagLength == 3); - self.version = [[YKFVersion alloc] initWithBytes:valueBytes[0] minor:valueBytes[1] micro:valueBytes[2]]; - break; - - case YKFManagementReadConfigurationTagsConfigLocked: - self.configurationLocked = value; - break; - - case YKFManagementReadConfigurationTagsNfcEnabled: - self.nfcEnabledMask = [value ykf_integerValue]; - break; - - case YKFManagementReadConfigurationTagsNfcSupported: - self.nfcSupportedMask = [value ykf_integerValue]; - break; - - default: - break; - } - - readIndex += tagLength; - } - } - return self; -} - -- (nullable YKFManagementInterfaceConfiguration*)configuration { - return [[YKFManagementInterfaceConfiguration alloc] initWithResponse:self]; -} - -@end diff --git a/YubiKit/YubiKit/Connections/Shared/Sessions/ChalResp/YKFChallengeResponseSession.h b/YubiKit/YubiKit/Connections/Shared/Sessions/ChalResp/YKFChallengeResponseSession.h index 3809e8c3..a5debeb0 100644 --- a/YubiKit/YubiKit/Connections/Shared/Sessions/ChalResp/YKFChallengeResponseSession.h +++ b/YubiKit/YubiKit/Connections/Shared/Sessions/ChalResp/YKFChallengeResponseSession.h @@ -8,7 +8,7 @@ #import -#import "YKFSession.h" +#import "YKFSession+Private.h" #import "YKFSlot.h" /** @@ -44,7 +44,7 @@ NS_ASSUME_NONNULL_BEGIN @abstract Defines the interface for YKFChallengeResponseSession. */ -@interface YKFChallengeResponseSession: NSObject +@interface YKFChallengeResponseSession: YKFSession /*! @method sendChallenge:slot:completion: diff --git a/YubiKit/YubiKit/Connections/Shared/Sessions/ChalResp/YKFChallengeResponseSession.m b/YubiKit/YubiKit/Connections/Shared/Sessions/ChalResp/YKFChallengeResponseSession.m index 976e5cd6..ca06f97c 100644 --- a/YubiKit/YubiKit/Connections/Shared/Sessions/ChalResp/YKFChallengeResponseSession.m +++ b/YubiKit/YubiKit/Connections/Shared/Sessions/ChalResp/YKFChallengeResponseSession.m @@ -17,12 +17,6 @@ #import "YKFSessionError+Private.h" #import "YKFSelectApplicationAPDU.h" -@interface YKFChallengeResponseSession() - -@property (nonatomic, readwrite) YKFSmartCardInterface *smartCardInterface; - -@end - @implementation YKFChallengeResponseSession + (void)sessionWithConnectionController:(nonnull id)connectionController diff --git a/YubiKit/YubiKit/Connections/Shared/Sessions/FIDO2/CBOR/YKFCBOREncoder.m b/YubiKit/YubiKit/Connections/Shared/Sessions/FIDO2/CBOR/YKFCBOREncoder.m index 7c2d8d57..3e1a63e5 100644 --- a/YubiKit/YubiKit/Connections/Shared/Sessions/FIDO2/CBOR/YKFCBOREncoder.m +++ b/YubiKit/YubiKit/Connections/Shared/Sessions/FIDO2/CBOR/YKFCBOREncoder.m @@ -72,9 +72,9 @@ + (NSData *)encodeTextString:(YKFCBORTextString *)cborTextString { + (NSData *)encodeArray:(YKFCBORArray *)cborArray { YKFAssertReturnValue(cborArray, @"CBOR Encoding - Cannot encode empty CBOR array.", nil); - + YKFAssertReturnValue(cborArray.value, @"CBOR Encoding - Cannot encode empty/nil 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]; diff --git a/YubiKit/YubiKit/Connections/Shared/Sessions/FIDO2/YKFFIDO2Session.h b/YubiKit/YubiKit/Connections/Shared/Sessions/FIDO2/YKFFIDO2Session.h index de7b7c94..63d24e0a 100644 --- a/YubiKit/YubiKit/Connections/Shared/Sessions/FIDO2/YKFFIDO2Session.h +++ b/YubiKit/YubiKit/Connections/Shared/Sessions/FIDO2/YKFFIDO2Session.h @@ -13,6 +13,7 @@ // limitations under the License. #import +#import "YKFSession+Private.h" @class YKFFIDO2MakeCredentialRequest, YKFFIDO2GetAssertionRequest, YKFFIDO2VerifyPinRequest, YKFFIDO2SetPinRequest, YKFFIDO2ChangePinRequest, YKFFIDO2GetInfoResponse, YKFFIDO2MakeCredentialResponse, YKFFIDO2GetAssertionResponse, YKFFIDO2PublicKeyCredentialRpEntity, YKFFIDO2PublicKeyCredentialUserEntity; @@ -184,7 +185,7 @@ typedef NS_ENUM(NSUInteger, YKFFIDO2SessionKeyState) { create one. It has to use only the single shared instance from YKFAccessorySession and sync its usage with the session state. */ -@interface YKFFIDO2Session: NSObject +@interface YKFFIDO2Session: YKFSession /*! @abstract diff --git a/YubiKit/YubiKit/Connections/Shared/Sessions/FIDO2/YKFFIDO2Session.m b/YubiKit/YubiKit/Connections/Shared/Sessions/FIDO2/YKFFIDO2Session.m index 04c4b808..9e6a751e 100644 --- a/YubiKit/YubiKit/Connections/Shared/Sessions/FIDO2/YKFFIDO2Session.m +++ b/YubiKit/YubiKit/Connections/Shared/Sessions/FIDO2/YKFFIDO2Session.m @@ -76,8 +76,6 @@ @interface YKFFIDO2Session() // Keeps the state of the application selection to avoid reselecting the application. @property BOOL applicationSelected; -@property (nonatomic, readwrite) YKFSmartCardInterface *smartCardInterface; - @end @implementation YKFFIDO2Session @@ -255,8 +253,10 @@ - (void)changePin:(nonnull NSString *)oldPin to:(nonnull NSString *)newPin compl - (void)setPin:(nonnull NSString *)pin completion:(nonnull YKFFIDO2SessionGenericCompletionBlock)completion { YKFParameterAssertReturn(pin); YKFParameterAssertReturn(completion); - - if (pin.length < 4 || pin.length > 255) { + + NSData *pinData = [[pin dataUsingEncoding:NSUTF8StringEncoding] ykf_fido2PaddedPinData]; + + if (pin.length < 4 || pinData.length > 65) { completion([YKFFIDO2Error errorWithCode:YKFFIDO2ErrorCodePIN_POLICY_VIOLATION]); return; } @@ -277,7 +277,6 @@ - (void)setPin:(nonnull NSString *)pin completion:(nonnull YKFFIDO2SessionGeneri setPinRequest.subCommand = YKFFIDO2ClientPinRequestSubCommandSetPIN; setPinRequest.keyAgreement = cosePlatformPublicKey; - NSData *pinData = [[pin dataUsingEncoding:NSUTF8StringEncoding] ykf_fido2PaddedPinData]; setPinRequest.pinEnc = [pinData ykf_aes256EncryptedDataWithKey:sharedSecret]; setPinRequest.pinAuth = [[setPinRequest.pinEnc ykf_fido2HMACWithKey:sharedSecret] subdataWithRange:NSMakeRange(0, 16)]; diff --git a/YubiKit/YubiKit/Connections/Shared/Sessions/MGMT/YKFManagementInterfaceConfiguration+Private.h b/YubiKit/YubiKit/Connections/Shared/Sessions/MGMT/YKFManagementInterfaceConfiguration+Private.h index 0ff0b000..f7eec557 100644 --- a/YubiKit/YubiKit/Connections/Shared/Sessions/MGMT/YKFManagementInterfaceConfiguration+Private.h +++ b/YubiKit/YubiKit/Connections/Shared/Sessions/MGMT/YKFManagementInterfaceConfiguration+Private.h @@ -1,17 +1,22 @@ +// Copyright 2018-2021 Yubico AB // -// YKFManagementInterfaceConfiguration+Private.h -// YubiKit +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at // -// Created by Irina Makhalova on 2/10/20. -// Copyright © 2020 Yubico. All rights reserved. +// http://www.apache.org/licenses/LICENSE-2.0 // +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and #ifndef YKFManagementInterfaceConfiguration_Private_h #define YKFManagementInterfaceConfiguration_Private_h #import "YKFManagementInterfaceConfiguration.h" -#import "YKFManagementReadConfigurationResponse.h" +@class YKFManagementDeviceInfo; @interface YKFManagementInterfaceConfiguration() @@ -24,7 +29,7 @@ @property (nonatomic, readonly) BOOL usbMaskChanged; @property (nonatomic, readonly) BOOL nfcMaskChanged; -- (nullable instancetype)initWithResponse:(nonnull YKFManagementReadConfigurationResponse *)response NS_DESIGNATED_INITIALIZER; +- (nullable instancetype)initWithDeviceInfo:(nonnull YKFManagementDeviceInfo *)response NS_DESIGNATED_INITIALIZER; @end diff --git a/YubiKit/YubiKit/Connections/Shared/Sessions/MGMT/YKFManagementInterfaceConfiguration.h b/YubiKit/YubiKit/Connections/Shared/Sessions/MGMT/YKFManagementInterfaceConfiguration.h index 985ccfd3..e233f19b 100644 --- a/YubiKit/YubiKit/Connections/Shared/Sessions/MGMT/YKFManagementInterfaceConfiguration.h +++ b/YubiKit/YubiKit/Connections/Shared/Sessions/MGMT/YKFManagementInterfaceConfiguration.h @@ -1,10 +1,15 @@ +// Copyright 2018-2021 Yubico AB // -// YKFManagementConfiguration.h -// YubiKit +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at // -// Created by Irina Makhalova on 2/4/20. -// Copyright © 2020 Yubico. All rights reserved. +// http://www.apache.org/licenses/LICENSE-2.0 // +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and #import diff --git a/YubiKit/YubiKit/Connections/Shared/Sessions/MGMT/YKFManagementInterfaceConfiguration.m b/YubiKit/YubiKit/Connections/Shared/Sessions/MGMT/YKFManagementInterfaceConfiguration.m index ac81d0f8..9f95a547 100644 --- a/YubiKit/YubiKit/Connections/Shared/Sessions/MGMT/YKFManagementInterfaceConfiguration.m +++ b/YubiKit/YubiKit/Connections/Shared/Sessions/MGMT/YKFManagementInterfaceConfiguration.m @@ -1,14 +1,19 @@ +// Copyright 2018-2021 Yubico AB // -// YKFManagementConfiguration.m -// YubiKit +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at // -// Created by Irina Makhalova on 2/4/20. -// Copyright © 2020 Yubico. All rights reserved. +// http://www.apache.org/licenses/LICENSE-2.0 // +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and #import "YKFManagementInterfaceConfiguration.h" -#import "YKFManagementReadConfigurationResponse.h" -#import "YKFManagementReadConfigurationResponse+Private.h" +#import "YKFManagementDeviceInfo+Private.h" +#import "YKFManagementDeviceInfo.h" #import "YKFAssert.h" @interface YKFManagementInterfaceConfiguration() @@ -28,26 +33,16 @@ @interface YKFManagementInterfaceConfiguration() @implementation YKFManagementInterfaceConfiguration -- (nullable instancetype)initWithResponse:(nonnull YKFManagementReadConfigurationResponse *)response { - YKFAssertAbortInit(response); +- (nullable instancetype)initWithDeviceInfo:(nonnull YKFManagementDeviceInfo *)deviceInfo { + YKFAssertAbortInit(deviceInfo); self = [super init]; if (self) { - self.isConfigurationLocked = false; - if (response.configurationLocked != nil && response.configurationLocked.length > 0) { - const char* configBytes = (const char*)[response.configurationLocked bytes]; - for (NSUInteger index = 0; index < response.configurationLocked.length; index++) { - if (configBytes[index] != 0) { - self.isConfigurationLocked = true; - break; - } - } - } - - self.usbSupportedMask = response.usbSupportedMask; - self.nfcSupportedMask = response.nfcSupportedMask; - self.usbEnabledMask = response.usbEnabledMask; - self.nfcEnabledMask = response.nfcEnabledMask; + self.isConfigurationLocked = deviceInfo.isConfigurationLocked; + self.usbSupportedMask = deviceInfo.usbSupportedMask; + self.nfcSupportedMask = deviceInfo.nfcSupportedMask; + self.usbEnabledMask = deviceInfo.usbEnabledMask; + self.nfcEnabledMask = deviceInfo.nfcEnabledMask; } return self; } diff --git a/YubiKit/YubiKit/Connections/Shared/Sessions/MGMT/YKFManagementSession.h b/YubiKit/YubiKit/Connections/Shared/Sessions/MGMT/YKFManagementSession.h index 2778cac7..24bebb4f 100644 --- a/YubiKit/YubiKit/Connections/Shared/Sessions/MGMT/YKFManagementSession.h +++ b/YubiKit/YubiKit/Connections/Shared/Sessions/MGMT/YKFManagementSession.h @@ -1,82 +1,86 @@ +// Copyright 2018-2021 Yubico AB // -// YKFManagementService.h -// YubiKit +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at // -// Created by Irina Makhalova on 2/4/20. -// Copyright © 2020 Yubico. All rights reserved. +// http://www.apache.org/licenses/LICENSE-2.0 // +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and #import -#import "YKFManagementReadConfigurationResponse.h" - -/** - * --------------------------------------------------------------------------------------------------------------------- - * @name Management Service Response Blocks - * --------------------------------------------------------------------------------------------------------------------- - */ -/*! - @abstract - Response block for [readConfigurationWithCompletion: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 (^YKFManagementSessionReadCompletionBlock) - (YKFManagementReadConfigurationResponse* _Nullable response, NSError* _Nullable error); - - - +#import "YKFVersion.h" +#import "YKFSession+Private.h" + +@class YKFManagementDeviceInfo, YKFManagementInterfaceConfiguration; + +/// Management error domain. +extern NSString* _Nonnull const YKFManagementErrorDomain; + +/// Management error codes. +typedef NS_ENUM(NSUInteger, YKFManagementErrorCode) { + YKFManagementErrorCodeUnsupportedOperation = 1, +}; + +/// @abstract +/// Response block for [getDeviceInfoWithCompletion:] which provides the device info for the +/// currently connected YubiKey. +/// +/// @param deviceInfo +/// 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 (^YKFManagementSessionGetDeviceInfoBlock) + (YKFManagementDeviceInfo* _Nullable deviceInfo, NSError* _Nullable error); + +/// @abstract +/// Response block for [writeConfiguration:reboot:completion:] which writes a new configuration to +/// the YubiKey. +/// +/// @param error +/// In case of a failed request this parameter contains the error. If the request was successful this +/// parameter is nil. typedef void (^YKFManagementSessionWriteCompletionBlock) (NSError* _Nullable error); - NS_ASSUME_NONNULL_BEGIN -/*! -@abstract - Defines the interface for YKFManagementSessionProtocol. -*/ -@interface YKFManagementSession : NSObject - -/*! -@method readConfigurationWithCompletion: - -@abstract - Reads configuration from YubiKey (what interfaces/applications are enabled and supported) - -@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)readConfigurationWithCompletion:(YKFManagementSessionReadCompletionBlock)completion; - -/*! -@method writeConfiguration:completion - -@abstract - Writes configuration to YubiKey (allos to enable and disable applications on YubiKey) - -@param configuration - The configurations that represent information on which interfaces/applications need to be enabled - -@param reboot - The device reboots after setting configuration. - -@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)writeConfiguration:(YKFManagementInterfaceConfiguration*) configuration reboot: (BOOL) reboot completion: (nonnull YKFManagementSessionWriteCompletionBlock) completion; +/// @abstract Defines the interface for YKFManagementSessionProtocol. +@interface YKFManagementSession : YKFSession + +/// @abstract +/// Reads configuration from YubiKey (what interfaces/applications are enabled and supported) +/// +/// @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 requires support for device info, available in YubiKey 4.1 or later. +/// The method is thread safe and can be invoked from any thread (main or a background thread). +- (void)getDeviceInfoWithCompletion:(YKFManagementSessionGetDeviceInfoBlock)completion; + +/// @abstract +/// Writes configuration to YubiKey (allos to enable and disable applications on YubiKey) +/// +/// @param configuration +/// The configurations that represent information on which interfaces/applications need to be enabled +/// +/// @param reboot +/// The device reboots after setting configuration. +/// +/// @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 requires support for device config, available in YubiKey 5.0 or later. +/// The method is thread safe and can be invoked from any thread (main or a background thread). +- (void)writeConfiguration:(YKFManagementInterfaceConfiguration*)configuration reboot:(BOOL)reboot completion:(nonnull YKFManagementSessionWriteCompletionBlock)completion; - (instancetype)init NS_UNAVAILABLE; diff --git a/YubiKit/YubiKit/Connections/Shared/Sessions/MGMT/YKFManagementSession.m b/YubiKit/YubiKit/Connections/Shared/Sessions/MGMT/YKFManagementSession.m index 5ba26d12..b299e0d6 100644 --- a/YubiKit/YubiKit/Connections/Shared/Sessions/MGMT/YKFManagementSession.m +++ b/YubiKit/YubiKit/Connections/Shared/Sessions/MGMT/YKFManagementSession.m @@ -1,24 +1,32 @@ +// Copyright 2018-2021 Yubico AB // -// YKFManagementSession.m -// YubiKit +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at // -// Created by Irina Makhalova on 2/4/20. -// Copyright © 2020 Yubico. All rights reserved. +// http://www.apache.org/licenses/LICENSE-2.0 // +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and -#import "YKFManagementReadConfigurationResponse.h" -#import "YKFManagementReadConfigurationResponse+Private.h" #import "YKFManagementWriteAPDU.h" #import "YKFAssert.h" #import "YKFAPDUError.h" #import "YKFManagementSession+Private.h" #import "YKFSmartCardInterface.h" #import "YKFSelectApplicationAPDU.h" +#import "YKFManagementSessionFeatures.h" +#import "YKFFeature.h" +#import "YKFManagementDeviceInfo+Private.h" + +NSString* const YKFManagementErrorDomain = @"com.yubico.management"; @interface YKFManagementSession() @property (nonatomic, readwrite) YKFVersion *version; -@property (nonatomic, readwrite) YKFSmartCardInterface *smartCardInterface; +@property (nonatomic, readwrite) YKFManagementSessionFeatures * _Nonnull features; - (YKFVersion *)versionFromResponse:(nonnull NSData *)data; @@ -31,6 +39,7 @@ + (void)sessionWithConnectionController:(nonnull id 3) + UInt8 *bytes = (UInt8 *)responseData.bytes; - + UInt8 responseType = bytes[0]; if (truncate) { YKFAssertAbortInit(responseType == YKFOATHCalculateResponseTypeTruncated); @@ -57,16 +59,15 @@ - (nullable instancetype)initWithKeyResponseData:(nonnull NSData *)responseData UInt8 responseLength = bytes[1]; UInt8 digits = bytes[2]; YKFAssertAbortInit(digits == 6 || digits == 7 || digits == 8); - UInt8 otpBytesLength = responseLength - 1; + UInt8 offset; if (truncate) { YKFAssertAbortInit(otpBytesLength == 4); - self.otp = [responseData ykf_parseOATHOTPFromIndex:3 digits:digits]; + offset = 3; } else { - UInt8 offset = bytes[otpBytesLength - 1] & 0xF + 3; - UInt32 otpResponseValue = CFSwapInt32BigToHost(*((UInt32 *)&bytes[offset])); - self.otp = [NSString stringWithFormat:@"%d", (unsigned int)otpResponseValue]; + offset = (bytes[responseData.length - 1] & 0xF) + 3; } + self.otp = [responseData ykf_parseOATHOTPFromIndex:offset digits:digits]; YKFAssertAbortInit(self.otp); if (period > 0) { diff --git a/YubiKit/YubiKit/Connections/Shared/Sessions/OATH/YKFOATHSession.h b/YubiKit/YubiKit/Connections/Shared/Sessions/OATH/YKFOATHSession.h index 48d7be79..bfe20372 100644 --- a/YubiKit/YubiKit/Connections/Shared/Sessions/OATH/YKFOATHSession.h +++ b/YubiKit/YubiKit/Connections/Shared/Sessions/OATH/YKFOATHSession.h @@ -13,7 +13,7 @@ // limitations under the License. #import -#import "YKFSession.h" +#import "YKFSession+Private.h" #import "YKFVersion.h" @class YKFOATHCode, @@ -116,7 +116,7 @@ NS_ASSUME_NONNULL_BEGIN The OATH session is mantained by the YKFConnection which controls its lifecycle. The application must not create one. */ -@interface YKFOATHSession: NSObject +@interface YKFOATHSession: YKFSession @property (nonatomic, readonly) YKFVersion* version; @@ -209,6 +209,28 @@ NS_ASSUME_NONNULL_BEGIN */ - (void)calculateCredential:(YKFOATHCredential *)credential completion:(YKFOATHSessionCalculateCompletionBlock)completion; +/*! + @method calculateCredential:timestamp: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 credential + The credential to calculate. + + @param timestamp + The timestamp used when calculating the OTP. + + @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)calculateCredential:(YKFOATHCredential *)credential timestamp:(NSDate *)timestamp completion:(YKFOATHSessionCalculateCompletionBlock)completion; /*! @method calculateAllWithCompletion: @@ -226,6 +248,26 @@ NS_ASSUME_NONNULL_BEGIN */ - (void)calculateAllWithCompletion:(YKFOATHSessionCalculateAllCompletionBlock)completion; +/*! + @method calculateAllWithTimestamp:completion: + + @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 timestamp + The timestamp used when calculating the OTP. + + @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)calculateAllWithTimestamp:(NSDate *)timestamp completion:(YKFOATHSessionCalculateAllCompletionBlock)completion; + /*! @method listCredentialsWithCompletion: diff --git a/YubiKit/YubiKit/Connections/Shared/Sessions/OATH/YKFOATHSession.m b/YubiKit/YubiKit/Connections/Shared/Sessions/OATH/YKFOATHSession.m index 98d013f9..f11eb67c 100644 --- a/YubiKit/YubiKit/Connections/Shared/Sessions/OATH/YKFOATHSession.m +++ b/YubiKit/YubiKit/Connections/Shared/Sessions/OATH/YKFOATHSession.m @@ -59,8 +59,6 @@ @interface YKFOATHSession() -@property (nonatomic, readwrite) YKFSmartCardInterface *smartCardInterface; - /* 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 @@ -170,16 +168,21 @@ - (void)renameCredential:(nonnull YKFOATHCredential *)credential #pragma mark - Credential Calculation - (void)calculateCredential:(YKFOATHCredential *)credential completion:(YKFOATHSessionCalculateCompletionBlock)completion { + NSDate *timestamp = [NSDate date]; + [self calculateCredential:credential timestamp:timestamp completion:completion]; +} + +- (void)calculateCredential:(YKFOATHCredential *)credential timestamp:(NSDate *)timestamp completion:(YKFOATHSessionCalculateCompletionBlock)completion { YKFParameterAssertReturn(credential); YKFParameterAssertReturn(completion); - + YKFParameterAssertReturn(timestamp); + YKFSessionError *credentialError = [YKFOATHCredentialUtils validateCredential:credential]; if (credentialError) { completion(nil, credentialError); return; } - NSDate *timestamp = [NSDate date]; YKFAPDU *apdu = [[YKFOATHCalculateAPDU alloc] initWithCredential:credential timestamp:timestamp]; [self executeOATHCommand:apdu completion:^(NSData * _Nullable result, NSError * _Nullable error) { @@ -199,10 +202,15 @@ - (void)calculateCredential:(YKFOATHCredential *)credential completion:(YKFOATHS }]; } + - (void)calculateAllWithCompletion:(YKFOATHSessionCalculateAllCompletionBlock)completion { + NSDate *timestamp = [NSDate date]; + [self calculateAllWithTimestamp:timestamp completion:completion]; +} + +- (void)calculateAllWithTimestamp:(NSDate *)timestamp completion:(YKFOATHSessionCalculateAllCompletionBlock)completion { YKFParameterAssertReturn(completion); - NSDate *timestamp = [NSDate date]; YKFAPDU *apdu = [[YKFOATHCalculateAllAPDU alloc] initWithTimestamp:timestamp]; [self executeOATHCommand:apdu completion:^(NSData * _Nullable result, NSError * _Nullable error) { @@ -282,7 +290,15 @@ - (void)setPassword:(NSString *)password completion:(YKFOATHSessionGenericComple // Build the request APDU with the select ID salt YKFOATHSetPasswordAPDU *apdu = [[YKFOATHSetPasswordAPDU alloc] initWithPassword:password salt:self.cachedSelectApplicationResponse.selectID]; [self.smartCardInterface executeCommand:apdu completion:^(NSData * _Nullable data, NSError * _Nullable error) { - completion(error); + if (error) { + if (error.code == YKFAPDUErrorCodeAuthenticationRequired) { + completion([YKFOATHError errorWithCode:YKFOATHErrorCodeAuthenticationRequired]); + } else { + completion(error); + } + } else { + completion(nil); + } }]; } @@ -330,16 +346,15 @@ - (void)executeOATHCommand:(YKFAPDU *)apdu completion:(YKFOATHServiceResultCompl } NSDate *startTime = [NSDate date]; - [self.smartCardInterface executeCommand:apdu completion:^(NSData * _Nullable data, NSError * _Nullable error) { + [self.smartCardInterface executeCommand:apdu sendRemainingIns:YKFSmartCardInterfaceSendRemainingInsOATH completion:^(NSData * _Nullable data, NSError * _Nullable error) { if (data) { completion(data, nil); return; } - NSTimeInterval executionTime = [startTime timeIntervalSinceNow]; + NSTimeInterval executionTime = -[startTime timeIntervalSinceNow]; switch(error.code) { case YKFAPDUErrorCodeAuthenticationRequired: if (executionTime < YKFOATHServiceTimeoutThreshold) { - self.cachedSelectApplicationResponse = nil; // Clear the cache to allow the application selection again. completion(nil, [YKFOATHError errorWithCode:YKFOATHErrorCodeAuthenticationRequired]); } else { completion(nil, [YKFOATHError errorWithCode:YKFOATHErrorCodeTouchTimeout]); diff --git a/YubiKit/YubiKit/Connections/Shared/Sessions/PIV/YKFPIVPadding.m b/YubiKit/YubiKit/Connections/Shared/Sessions/PIV/YKFPIVPadding.m index 1ce8adee..c47a72a1 100644 --- a/YubiKit/YubiKit/Connections/Shared/Sessions/PIV/YKFPIVPadding.m +++ b/YubiKit/YubiKit/Connections/Shared/Sessions/PIV/YKFPIVPadding.m @@ -48,32 +48,32 @@ + (NSData *)padData:(NSData *)data keyType:(YKFPIVKeyType)keyType algorithm:(Sec } else if (keyType == YKFPIVKeyTypeECCP256 || keyType == YKFPIVKeyTypeECCP384) { int keySize = YKFPIVSizeFromKeyType(keyType); NSMutableData *hash = nil; - if (algorithm == kSecKeyAlgorithmECDSASignatureMessageX962SHA224) { + if ([(__bridge NSString *)algorithm isEqualToString:(__bridge NSString *)kSecKeyAlgorithmECDSASignatureMessageX962SHA224]) { hash = [NSMutableData dataWithLength:(NSUInteger)CC_SHA256_DIGEST_LENGTH]; CC_SHA224(data.bytes, (CC_LONG)data.length, hash.mutableBytes); } - if (algorithm == kSecKeyAlgorithmECDSASignatureMessageX962SHA256) { + if ([(__bridge NSString *)algorithm isEqualToString:(__bridge NSString *)kSecKeyAlgorithmECDSASignatureMessageX962SHA256]) { hash = [NSMutableData dataWithLength:(NSUInteger)CC_SHA256_DIGEST_LENGTH]; CC_SHA256(data.bytes, (CC_LONG)data.length, hash.mutableBytes); } - if (algorithm == kSecKeyAlgorithmECDSASignatureMessageX962SHA384) { + if ([(__bridge NSString *)algorithm isEqualToString:(__bridge NSString *)kSecKeyAlgorithmECDSASignatureMessageX962SHA384]) { hash = [NSMutableData dataWithLength:(NSUInteger)CC_SHA512_DIGEST_LENGTH]; CC_SHA384(data.bytes, (CC_LONG)data.length, hash.mutableBytes); } - if (algorithm == kSecKeyAlgorithmECDSASignatureMessageX962SHA512) { + if ([(__bridge NSString *)algorithm isEqualToString:(__bridge NSString *)kSecKeyAlgorithmECDSASignatureMessageX962SHA512]) { hash = [NSMutableData dataWithLength:(NSUInteger)CC_SHA512_DIGEST_LENGTH]; CC_SHA512(data.bytes, (CC_LONG)data.length, hash.mutableBytes); } - if (algorithm == kSecKeyAlgorithmECDSASignatureMessageX962SHA1) { + if ([(__bridge NSString *)algorithm isEqualToString:(__bridge NSString *)kSecKeyAlgorithmECDSASignatureMessageX962SHA1]) { hash = [NSMutableData dataWithLength:(NSUInteger)CC_SHA1_DIGEST_LENGTH]; CC_SHA1(data.bytes, (CC_LONG)data.length, hash.mutableBytes); } - if (algorithm == kSecKeyAlgorithmECDSASignatureDigestX962SHA1 || - algorithm == kSecKeyAlgorithmECDSASignatureDigestX962SHA224 || - algorithm == kSecKeyAlgorithmECDSASignatureDigestX962SHA256 || - algorithm == kSecKeyAlgorithmECDSASignatureDigestX962SHA384 || - algorithm == kSecKeyAlgorithmECDSASignatureDigestX962SHA512) { + if ([(__bridge NSString *)algorithm isEqualToString:(__bridge NSString *)kSecKeyAlgorithmECDSASignatureDigestX962SHA1] || + [(__bridge NSString *)algorithm isEqualToString:(__bridge NSString *)kSecKeyAlgorithmECDSASignatureDigestX962SHA224] || + [(__bridge NSString *)algorithm isEqualToString:(__bridge NSString *)kSecKeyAlgorithmECDSASignatureDigestX962SHA256] || + [(__bridge NSString *)algorithm isEqualToString:(__bridge NSString *)kSecKeyAlgorithmECDSASignatureDigestX962SHA384] || + [(__bridge NSString *)algorithm isEqualToString:(__bridge NSString *)kSecKeyAlgorithmECDSASignatureDigestX962SHA512]) { hash = [data mutableCopy]; } if (hash.length == keySize) { diff --git a/YubiKit/YubiKit/Connections/Shared/Sessions/PIV/YKFPIVSession.h b/YubiKit/YubiKit/Connections/Shared/Sessions/PIV/YKFPIVSession.h index d815b788..3a359dca 100644 --- a/YubiKit/YubiKit/Connections/Shared/Sessions/PIV/YKFPIVSession.h +++ b/YubiKit/YubiKit/Connections/Shared/Sessions/PIV/YKFPIVSession.h @@ -16,6 +16,7 @@ #define YKFPIVSession_h #import "YKFVersion.h" +#import "YKFSession+Private.h" #import "YKFPIVKeyType.h" /// Touch policy for PIV application. @@ -51,7 +52,11 @@ typedef NS_ENUM(NSUInteger, YKFPIVFErrorCode) { YKFPIVFErrorCodeInvalidCipherTextLength = 1, YKFPIVFErrorCodeUnsupportedOperation = 2, YKFPIVFErrorCodeDataParseError = 3, - YKFPIVFErrorCodeUnknownKeyType = 4 + YKFPIVFErrorCodeUnknownKeyType = 4, + YKFPIVFErrorCodeInvalidPin = 5, + YKFPIVFErrorCodePinLocked = 6, + YKFPIVFErrorCodeInvalidResponse = 7, + YKFPIVFErrorCodeAuthenticationFailed = 8 }; @class YKFPIVSessionFeatures, YKFPIVManagementKeyType, YKFPIVManagementKeyMetadata; @@ -145,7 +150,7 @@ typedef void (^YKFPIVSessionManagementKeyMetadataCompletionBlock) /// @abstract Provides the interface for executing PIV requests with the key. /// @discussion The PIV session is mantained by the YKFConnection which controls its lifecycle. The application /// must not create one. -@interface YKFPIVSession: NSObject +@interface YKFPIVSession: YKFSession /// @abstract This property provides the version of the currently connected YubiKey. @property (nonatomic, readonly) YKFVersion * _Nonnull version; diff --git a/YubiKit/YubiKit/Connections/Shared/Sessions/PIV/YKFPIVSession.m b/YubiKit/YubiKit/Connections/Shared/Sessions/PIV/YKFPIVSession.m index 23adca42..0b58fcaf 100644 --- a/YubiKit/YubiKit/Connections/Shared/Sessions/PIV/YKFPIVSession.m +++ b/YubiKit/YubiKit/Connections/Shared/Sessions/PIV/YKFPIVSession.m @@ -84,7 +84,6 @@ typedef void (^YKFPIVSessionDataCompletionBlock) @interface YKFPIVSession() -@property (nonatomic, readwrite) YKFSmartCardInterface *smartCardInterface; @property (nonatomic, readonly) BOOL isValid; @property (nonatomic, readwrite) YKFVersion * _Nonnull version; @property (nonatomic, readwrite) YKFPIVSessionFeatures * _Nonnull features; @@ -131,6 +130,10 @@ + (void)sessionWithConnectionController:(nonnull id *records = [TKBERTLVRecord sequenceOfRecordsFromData:data]; - NSData *objectData = [records ykfTLVRecordWithTag:YKFPIVTagObjectData].value; - NSData *certificateData = [[TKBERTLVRecord sequenceOfRecordsFromData:objectData] ykfTLVRecordWithTag:YKFPIVTagCertificate].value; - CFDataRef cfCertDataRef = (__bridge CFDataRef)certificateData; - SecCertificateRef certificate = SecCertificateCreateWithData(nil, cfCertDataRef); - completion(certificate, error); + if (error != nil) { + completion(nil, error); + } else { + NSArray *records = [TKBERTLVRecord sequenceOfRecordsFromData:data]; + NSData *objectData = [records ykfTLVRecordWithTag:YKFPIVTagObjectData].value; + NSData *certificateData = [[TKBERTLVRecord sequenceOfRecordsFromData:objectData] ykfTLVRecordWithTag:YKFPIVTagCertificate].value; + CFDataRef cfCertDataRef = (__bridge CFDataRef)certificateData; + SecCertificateRef certificate = SecCertificateCreateWithData(nil, cfCertDataRef); + completion(certificate, nil); + } }]; } @@ -514,6 +521,7 @@ - (void)authenticateWithManagementKey:(nonnull NSData *)managementKey type:(nonn NSData *encryptedData = encryptedRecord.value; NSData *expectedData = [challenge ykf_encryptDataWithAlgorithm:[keyType.name ykfCCAlgorithm] key:managementKey]; if (![encryptedData isEqual:expectedData]) { + completion([[NSError alloc] initWithDomain:YKFPIVErrorDomain code:YKFPIVFErrorCodeAuthenticationFailed userInfo:@{NSLocalizedDescriptionKey: @"Authentication failed."}]); return; } completion(nil); @@ -549,6 +557,10 @@ - (void)getSerialNumberWithCompletion:(YKFPIVSessionSerialNumberCompletionBlock) YKFAPDU *apdu = [[YKFAPDU alloc] initWithCla:0 ins:YKFPIVInsGetSerial p1:0 p2:0 data:[NSData data] type:YKFAPDUTypeShort]; [self.smartCardInterface executeCommand:apdu completion:^(NSData * _Nullable data, NSError * _Nullable error) { if (data != nil) { + if ([data length] != 4) { + completion(-1, [[NSError alloc] initWithDomain:YKFPIVErrorDomain code:YKFPIVFErrorCodeInvalidResponse userInfo:@{NSLocalizedDescriptionKey: @"Invalid response when reading serial number."}]); + return; + } UInt32 serialNumber = CFSwapInt32BigToHost(*(UInt32*)([data bytes])); completion(serialNumber, nil); } else { @@ -569,12 +581,17 @@ - (void)verifyPin:(nonnull NSString *)pin completion:(nonnull YKFPIVSessionVerif YKFSessionError *sessionError = (YKFSessionError *)error; if ([sessionError isKindOfClass:[YKFSessionError class]]) { int retries = [self getRetriesFromStatusCode:(int)sessionError.code]; - if (retries >= 0) { + if (retries > 0) { currentPinAttempts = retries; - completion(currentPinAttempts, error); + completion(currentPinAttempts, [[NSError alloc] initWithDomain:YKFPIVErrorDomain code:YKFPIVFErrorCodeInvalidPin userInfo:@{NSLocalizedDescriptionKey: @"Invalid PIN code."}]); + return; + + } else if (retries == 0) { + completion(retries, [[NSError alloc] initWithDomain:YKFPIVErrorDomain code:YKFPIVFErrorCodePinLocked userInfo:@{NSLocalizedDescriptionKey: @"PIN code entry locked."}]); return; } } + // Not wrong pin nor locked pin entry, pass on original error completion(-1, error); } }]; diff --git a/YubiKit/YubiKit/Connections/Shared/Sessions/U2F/YKFU2FSession.h b/YubiKit/YubiKit/Connections/Shared/Sessions/U2F/YKFU2FSession.h index 2e46a025..b1cf244d 100644 --- a/YubiKit/YubiKit/Connections/Shared/Sessions/U2F/YKFU2FSession.h +++ b/YubiKit/YubiKit/Connections/Shared/Sessions/U2F/YKFU2FSession.h @@ -13,6 +13,7 @@ // limitations under the License. #import +#import "YKFSession+Private.h" @class YKFU2FSignRequest, YKFU2FSignResponse, YKFU2FRegisterRequest, YKFU2FRegisterResponse; /** @@ -88,7 +89,7 @@ NS_ASSUME_NONNULL_BEGIN @discussion The U2F session is mantained by the YKFConnection which controls its lifecycle. The application must not create one. */ -@interface YKFU2FSession: NSObject +@interface YKFU2FSession: YKFSession /*! @property keyState diff --git a/YubiKit/YubiKit/Connections/Shared/Sessions/U2F/YKFU2FSession.m b/YubiKit/YubiKit/Connections/Shared/Sessions/U2F/YKFU2FSession.m index 481018a4..f5bd4d5c 100644 --- a/YubiKit/YubiKit/Connections/Shared/Sessions/U2F/YKFU2FSession.m +++ b/YubiKit/YubiKit/Connections/Shared/Sessions/U2F/YKFU2FSession.m @@ -44,7 +44,6 @@ @interface YKFU2FSession() @property (nonatomic, assign, readwrite) YKFU2FSessionKeyState keyState; -@property (nonatomic, readwrite) YKFSmartCardInterface *smartCardInterface; @end diff --git a/YubiKit/YubiKit/Connections/Shared/Sessions/YKFManagementDeviceInfo+Private.h b/YubiKit/YubiKit/Connections/Shared/Sessions/YKFManagementDeviceInfo+Private.h new file mode 100644 index 00000000..3b6efac7 --- /dev/null +++ b/YubiKit/YubiKit/Connections/Shared/Sessions/YKFManagementDeviceInfo+Private.h @@ -0,0 +1,50 @@ +// Copyright 2018-2021 Yubico AB +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef YKFManagementDeviceInfo_Private_h +#define YKFManagementDeviceInfo_Private_h + +#import "YKFManagementDeviceInfo.h" + +static const NSUInteger YKFManagementTagUSBSupported = 0x01; +static const NSUInteger YKFManagementTagSerialNumber = 0x02; +static const NSUInteger YKFManagementTagUSBEnabled = 0x03; +static const NSUInteger YKFManagementTagFormfactor = 0x04; +static const NSUInteger YKFManagementTagFirmwareVersion = 0x05; +static const NSUInteger YKFManagementTagAutoEjectTimeout = 0x06; +static const NSUInteger YKFManagementTagChallengeResponseTimeout = 0x07; +static const NSUInteger YKFManagementTagDeviceFlags = 0x08; +static const NSUInteger YKFManagementTagNFCSupported = 0x0d; +static const NSUInteger YKFManagementTagNFCEnabled = 0x0e; +static const NSUInteger YKFManagementTagConfigLocked = 0x0a; + +NS_ASSUME_NONNULL_BEGIN + +@interface YKFManagementDeviceInfo() + +@property (nonatomic, readwrite) NSUInteger usbSupportedMask; +@property (nonatomic, readwrite) NSUInteger nfcSupportedMask; + +@property (nonatomic, readwrite) NSUInteger usbEnabledMask; +@property (nonatomic, readwrite) NSUInteger nfcEnabledMask; + +- (nullable instancetype)initWithResponseData:(NSData *)data defaultVersion:(YKFVersion *)defaultVersion NS_DESIGNATED_INITIALIZER; + +- (instancetype)init NS_UNAVAILABLE; + +@end + +NS_ASSUME_NONNULL_END + +#endif /* YKFManagementDeviceInfo_Private_h */ diff --git a/YubiKit/YubiKit/Connections/Shared/Sessions/YKFManagementDeviceInfo.h b/YubiKit/YubiKit/Connections/Shared/Sessions/YKFManagementDeviceInfo.h new file mode 100644 index 00000000..eac984c8 --- /dev/null +++ b/YubiKit/YubiKit/Connections/Shared/Sessions/YKFManagementDeviceInfo.h @@ -0,0 +1,47 @@ +// Copyright 2018-2021 Yubico AB +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef YKFDeviceInfo_h +#define YKFDeviceInfo_h + + +@class YKFVersion, YKFManagementInterfaceConfiguration; + +typedef NS_ENUM(NSUInteger, YKFFormFactor) { + /// Used when information about the YubiKey's form factor isn't available. + YKFFormFactorUnknown = 0x00, + /// A keychain-sized YubiKey with a USB-A connector. + YKFFormFactorUSBAKeychain = 0x01, + /// A keychain-sized YubiKey with a USB-C connector. + YKFFormFactorUSBCKeychain = 0x03, + /// A keychain-sized YubiKey with both USB-C and Lightning connectors. + YKFFormFactorUSBCLightning = 0x05, +}; + +NS_ASSUME_NONNULL_BEGIN + +@interface YKFManagementDeviceInfo : NSObject + +@property (nonatomic, readonly, nullable) YKFManagementInterfaceConfiguration* configuration; + +@property (nonatomic, readonly) YKFVersion *version; +@property (nonatomic, readonly) YKFFormFactor formFactor; +@property (nonatomic, readonly) NSUInteger serialNumber; +@property (nonatomic, readonly) bool isConfigurationLocked; + +@end + +NS_ASSUME_NONNULL_END + +#endif /* YKFDeviceInfo_h */ diff --git a/YubiKit/YubiKit/Connections/Shared/Sessions/YKFManagementDeviceInfo.m b/YubiKit/YubiKit/Connections/Shared/Sessions/YKFManagementDeviceInfo.m new file mode 100644 index 00000000..89483ac3 --- /dev/null +++ b/YubiKit/YubiKit/Connections/Shared/Sessions/YKFManagementDeviceInfo.m @@ -0,0 +1,86 @@ +// Copyright 2018-2021 Yubico AB +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and + +#import +#import "YKFManagementDeviceInfo+Private.h" +#import "YKFAssert.h" +#import +#import "NSArray+TKTLVRecord.h" +#import "YKFNSDataAdditions+Private.h" +#import "YKFVersion.h" +#import "YKFManagementInterfaceConfiguration+Private.h" + + + +@interface YKFManagementDeviceInfo() + +@property (nonatomic, readwrite) YKFVersion *version; +@property (nonatomic, readwrite) YKFFormFactor formFactor; +@property (nonatomic, readwrite) NSUInteger serialNumber; +@property (nonatomic, readwrite) bool isLocked; + +@property (nonatomic, readwrite) YKFManagementInterfaceConfiguration *configuration; + +@end + +@implementation YKFManagementDeviceInfo + +- (nullable instancetype)initWithResponseData:(nonnull NSData *)data defaultVersion:(nonnull YKFVersion *)defaultVersion { + YKFAssertAbortInit(data.length); + YKFAssertAbortInit(defaultVersion) + self = [super init]; + if (self) { + const char* bytes = (const char*)[data bytes]; + int length = bytes[0] & 0xff; + if (length != data.length - 1) { + return nil; + } + NSArray *records = [TKBERTLVRecord sequenceOfRecordsFromData:[data subdataWithRange:NSMakeRange(1, data.length - 1)]]; + + self.isLocked = [[records ykfTLVRecordWithTag:YKFManagementTagConfigLocked].value ykf_integerValue] == 1; + + self.serialNumber = [[records ykfTLVRecordWithTag:YKFManagementTagSerialNumber].value ykf_integerValue]; + + NSData *versionData = [records ykfTLVRecordWithTag:YKFManagementTagFirmwareVersion].value; + if (versionData != nil) { + self.version = [[YKFVersion alloc] initWithData:versionData]; + } else { + self.version = defaultVersion; + } + + NSUInteger reportedFormFactor = [[records ykfTLVRecordWithTag:YKFManagementTagFormfactor].value ykf_integerValue]; + switch (reportedFormFactor & 0xf) { + case YKFFormFactorUSBAKeychain: + self.formFactor = YKFFormFactorUSBAKeychain; + break; + case YKFFormFactorUSBCKeychain: + self.formFactor = YKFFormFactorUSBCKeychain; + break; + case YKFFormFactorUSBCLightning: + self.formFactor = YKFFormFactorUSBCLightning; + break; + default: + self.formFactor = YKFFormFactorUnknown; + } + + self.usbSupportedMask = [[records ykfTLVRecordWithTag:YKFManagementTagUSBSupported].value ykf_integerValue]; + self.usbEnabledMask = [[records ykfTLVRecordWithTag:YKFManagementTagUSBEnabled].value ykf_integerValue]; + self.nfcSupportedMask = [[records ykfTLVRecordWithTag:YKFManagementTagNFCSupported].value ykf_integerValue]; + self.nfcEnabledMask = [[records ykfTLVRecordWithTag:YKFManagementTagNFCEnabled].value ykf_integerValue]; + + self.configuration = [[YKFManagementInterfaceConfiguration alloc] initWithDeviceInfo:self]; + } + return self; +} + +@end diff --git a/YubiKit/YubiKit/Connections/Shared/Sessions/YKFManagementSessionFeatures.h b/YubiKit/YubiKit/Connections/Shared/Sessions/YKFManagementSessionFeatures.h new file mode 100644 index 00000000..7ee87835 --- /dev/null +++ b/YubiKit/YubiKit/Connections/Shared/Sessions/YKFManagementSessionFeatures.h @@ -0,0 +1,19 @@ +// +// YKFManagementSessionFeatures.h +// YubiKit +// +// Created by Jens Utbult on 2021-04-28. +// Copyright © 2021 Yubico. All rights reserved. +// + +#ifndef YKFManagementSessionFeatures_h +#define YKFManagementSessionFeatures_h + +@class YKFFeature; + +@interface YKFManagementSessionFeatures : NSObject +@property (nonatomic, readonly) YKFFeature * _Nonnull deviceInfo; +@property (nonatomic, readonly) YKFFeature * _Nonnull deviceConfig; +@end + +#endif /* YKFManagementSessionFeatures_h */ diff --git a/YubiKit/YubiKit/Connections/Shared/Sessions/YKFManagementSessionFeatures.m b/YubiKit/YubiKit/Connections/Shared/Sessions/YKFManagementSessionFeatures.m new file mode 100644 index 00000000..957a4b48 --- /dev/null +++ b/YubiKit/YubiKit/Connections/Shared/Sessions/YKFManagementSessionFeatures.m @@ -0,0 +1,35 @@ +// Copyright 2018-2021 Yubico AB +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import +#import "YKFManagementSessionFeatures.h" +#import "YKFFeature.h" + +@interface YKFManagementSessionFeatures() +@property (nonatomic, readwrite) YKFFeature * _Nonnull deviceInfo; +@property (nonatomic, readwrite) YKFFeature * _Nonnull deviceConfig; +@end + +@implementation YKFManagementSessionFeatures + +- (instancetype)init { + self = [super init]; + if (self) { + self.deviceInfo = [[YKFFeature alloc] initWithName:@"Device info" versionString:@"4.1.0"]; + self.deviceConfig = [[YKFFeature alloc] initWithName:@"Device config" versionString:@"5.0.0"]; + } + return self; +} + +@end diff --git a/YubiKit/YubiKit/Connections/Shared/Sessions/YKFSession+Private.h b/YubiKit/YubiKit/Connections/Shared/Sessions/YKFSession+Private.h new file mode 100644 index 00000000..bf3f2812 --- /dev/null +++ b/YubiKit/YubiKit/Connections/Shared/Sessions/YKFSession+Private.h @@ -0,0 +1,28 @@ +// Copyright 2018-2021 Yubico AB +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef YKFSession_Private_h +#define YKFSession_Private_h + +#import "YKFSession.h" + +@class YKFSmartCardInterface; + +@interface YKFSession() + +@property (nonatomic, readwrite) YKFSmartCardInterface *smartCardInterface; + +@end + +#endif /* YKFSession_Private_h */ diff --git a/YubiKit/YubiKit/Connections/Shared/Sessions/YKFSession.h b/YubiKit/YubiKit/Connections/Shared/Sessions/YKFSession.h index 0a5ab287..e0b167b7 100644 --- a/YubiKit/YubiKit/Connections/Shared/Sessions/YKFSession.h +++ b/YubiKit/YubiKit/Connections/Shared/Sessions/YKFSession.h @@ -17,13 +17,13 @@ NS_ASSUME_NONNULL_BEGIN -@interface YKFSession: NSObject +typedef void (^YKFSessionCommandBlock)(void); -/// Removes the YLP headers and status code from the response data received from a key command response. -+ (NSData *)dataFromKeyResponse:(NSData *)response; +@interface YKFSession: NSObject -/// Returns the status code from a response received from a key command response. -+ (UInt16)statusCodeFromKeyResponse:(NSData *)response; +/// @abstract Dispatch a code block for execution once all currently scheduled commands have completed. +/// @param block The block that gets called. +- (void)dispatchAfterCurrentCommands:(YKFSessionCommandBlock)block NS_SWIFT_NAME(dispatchAfterCurrentCommands(block:)); @end diff --git a/YubiKit/YubiKit/Connections/Shared/Sessions/YKFSession.m b/YubiKit/YubiKit/Connections/Shared/Sessions/YKFSession.m index cf04d98e..b8eeeb3c 100644 --- a/YubiKit/YubiKit/Connections/Shared/Sessions/YKFSession.m +++ b/YubiKit/YubiKit/Connections/Shared/Sessions/YKFSession.m @@ -13,7 +13,9 @@ // limitations under the License. #import "YKFSession.h" +#import "YKFSession+Private.h" #import "YKFAccessoryConnectionController.h" +#import "YKFSmartCardInterface.h" #import "YKFNSDataAdditions.h" #import "YKFNSDataAdditions+Private.h" #import "YKFAPDUError.h" @@ -21,27 +23,8 @@ @implementation YKFSession -#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, YKFAPDUErrorCodeWrongLength); - YKFAssertReturnValue(response.length >= 2, @"Key response data is too short.", YKFAPDUErrorCodeWrongLength); - - return [response ykf_getBigEndianIntegerInRange:NSMakeRange([response length] - 2, 2)]; +- (void)dispatchAfterCurrentCommands:(YKFSessionCommandBlock)block { + [self.smartCardInterface dispatchAfterCurrentCommands:block]; } @end diff --git a/YubiKit/YubiKit/Connections/Shared/YKFConnectionControllerProtocol.h b/YubiKit/YubiKit/Connections/Shared/YKFConnectionControllerProtocol.h index 434721d5..541428d5 100644 --- a/YubiKit/YubiKit/Connections/Shared/YKFConnectionControllerProtocol.h +++ b/YubiKit/YubiKit/Connections/Shared/YKFConnectionControllerProtocol.h @@ -18,14 +18,14 @@ NS_ASSUME_NONNULL_BEGIN typedef void (^YKFConnectionControllerCommandResponseBlock)(NSData* _Nullable, NSError* _Nullable, NSTimeInterval); typedef void (^YKFConnectionControllerCompletionBlock)(void); +typedef void (^YKFConnectionControllerCommunicationQueueBlock)(NSOperation *operation); @protocol YKFConnectionControllerProtocol - (void)execute:(YKFAPDU *)command completion:(YKFConnectionControllerCommandResponseBlock)completion; - (void)execute:(YKFAPDU *)command timeout:(NSTimeInterval)timeout completion:(YKFConnectionControllerCommandResponseBlock)completion; -- (void)dispatchOnSequentialQueue:(YKFConnectionControllerCompletionBlock)block delay:(NSTimeInterval)delay; -- (void)dispatchOnSequentialQueue:(YKFConnectionControllerCompletionBlock)block; +- (void)dispatchBlockOnCommunicationQueue:(YKFConnectionControllerCommunicationQueueBlock)block; - (void)closeConnectionWithCompletion:(YKFConnectionControllerCompletionBlock)completion; - (void)cancelAllCommands; diff --git a/YubiKit/YubiKit/Connections/Shared/YKFVersion.h b/YubiKit/YubiKit/Connections/Shared/YKFVersion.h index 9c9b63df..f3207eb4 100644 --- a/YubiKit/YubiKit/Connections/Shared/YKFVersion.h +++ b/YubiKit/YubiKit/Connections/Shared/YKFVersion.h @@ -26,6 +26,7 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, readonly) UInt8 minor; @property (nonatomic, readonly) UInt8 micro; +- (instancetype)initWithData:(NSData *)data NS_DESIGNATED_INITIALIZER; - (instancetype)initWithBytes:(UInt8)major minor:(UInt8)minor micro:(UInt8)micro NS_DESIGNATED_INITIALIZER; - (instancetype)initWithString:(NSString *)versionString NS_DESIGNATED_INITIALIZER; - (instancetype)init NS_UNAVAILABLE; diff --git a/YubiKit/YubiKit/Connections/Shared/YKFVersion.m b/YubiKit/YubiKit/Connections/Shared/YKFVersion.m index eb9d9ec3..2a35e8e3 100644 --- a/YubiKit/YubiKit/Connections/Shared/YKFVersion.m +++ b/YubiKit/YubiKit/Connections/Shared/YKFVersion.m @@ -24,6 +24,17 @@ @interface YKFVersion() @implementation YKFVersion +- (instancetype)initWithData:(NSData *)data { + self = [super init]; + if (self) { + UInt8 *bytes = (UInt8 *)data.bytes; + self.major = bytes[0]; + self.minor = bytes[1]; + self.micro = bytes[2]; + } + return self; +} + - (instancetype)initWithBytes:(UInt8)major minor:(UInt8)minor micro:(UInt8)micro { self = [super init]; if (self) { @@ -60,4 +71,8 @@ - (NSComparisonResult)compare:(YKFVersion *)version { return [[NSNumber numberWithUnsignedShort:self.micro] compare:[NSNumber numberWithUnsignedShort:version.micro]]; } +- (NSString *)description { + return [NSString stringWithFormat:@"%i.%i.%i", self.major, self.minor, self.micro]; +} + @end diff --git a/YubiKit/YubiKit/Connections/SmartCardInterface/YKFSmartCardInterface.h b/YubiKit/YubiKit/Connections/SmartCardInterface/YKFSmartCardInterface.h index 0ba68206..cd1ac816 100644 --- a/YubiKit/YubiKit/Connections/SmartCardInterface/YKFSmartCardInterface.h +++ b/YubiKit/YubiKit/Connections/SmartCardInterface/YKFSmartCardInterface.h @@ -21,11 +21,11 @@ @class YKFAPDU, YKFSelectApplicationAPDU; @protocol YKFConnectionControllerProtocol; -//typedef void (^YKFSmartCardInterfaceSelectApplicationResponseBlock) -// (NSError* _Nullable error); typedef void (^YKFSmartCardInterfaceResponseBlock) (NSData* _Nullable data, NSError* _Nullable error); +typedef void (^YKFSmartCardInterfaceCommandBlock)(void); + typedef NS_ENUM(NSUInteger, YKFSmartCardInterfaceSendRemainingIns) { /// The APDU instruction to read the remaining data from the Yubikey. @@ -53,6 +53,8 @@ NS_ASSUME_NONNULL_BEGIN - (void)executeCommand:(YKFAPDU *)apdu sendRemainingIns:(YKFSmartCardInterfaceSendRemainingIns)sendRemainingIns timeout:(NSTimeInterval)timeout completion:(YKFSmartCardInterfaceResponseBlock)completion; +- (void)dispatchAfterCurrentCommands:(YKFSmartCardInterfaceCommandBlock)block; + NS_ASSUME_NONNULL_END @end diff --git a/YubiKit/YubiKit/Connections/SmartCardInterface/YKFSmartCardInterface.m b/YubiKit/YubiKit/Connections/SmartCardInterface/YKFSmartCardInterface.m index a3b736f5..df071de1 100644 --- a/YubiKit/YubiKit/Connections/SmartCardInterface/YKFSmartCardInterface.m +++ b/YubiKit/YubiKit/Connections/SmartCardInterface/YKFSmartCardInterface.m @@ -134,6 +134,16 @@ - (void)executeCommand:(YKFAPDU *)apdu sendRemainingIns:(YKFSmartCardInterfaceSe [self executeCommand:apdu sendRemainingIns:sendRemainingIns timeout:timeout data:data completion:completion]; } +- (void)dispatchAfterCurrentCommands:(YKFSmartCardInterfaceCommandBlock)block { + [self.connectionController dispatchBlockOnCommunicationQueue:^(NSOperation *operation) { + // Return if operation is cancelled + if (operation.isCancelled) { + return; + } + block(); + }]; +} + #pragma mark - Helpers - (NSData *)dataFromKeyResponse:(NSData *)response { diff --git a/YubiKit/YubiKit/Layers/PCSC/YKFPCSCLayer.m b/YubiKit/YubiKit/Layers/PCSC/YKFPCSCLayer.m index a4cb2298..261f745b 100644 --- a/YubiKit/YubiKit/Layers/PCSC/YKFPCSCLayer.m +++ b/YubiKit/YubiKit/Layers/PCSC/YKFPCSCLayer.m @@ -42,8 +42,8 @@ static const NSUInteger YKFPCSCLayerCardLimitPerContext = 10; @interface YubiKitManager() -@property (nonatomic, readonly, nonnull) YKFNFCConnection *nfcSession NS_AVAILABLE_IOS(11.0); -@property (nonatomic, readonly, nonnull) YKFAccessoryConnection *accessorySession; +@property (nonatomic, readonly, nonnull) YKFNFCConnection *nfcConnection NS_AVAILABLE_IOS(11.0); +@property (nonatomic, readonly, nonnull) YKFAccessoryConnection *accessoryConnection; @end @interface YKFPCSCLayer() @@ -82,7 +82,7 @@ @implementation YKFPCSCLayer #endif static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ - sharedInstance = [[YKFPCSCLayer alloc] initWithAccessorySession:YubiKitManager.shared.accessorySession]; + sharedInstance = [[YKFPCSCLayer alloc] initWithAccessorySession:YubiKitManager.shared.accessoryConnection]; }); return sharedInstance; } @@ -190,7 +190,7 @@ - (SInt64)transmit:(NSData *)commandData response:(NSData **)response { dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); - YKFSmartCardInterface *smartCard = YubiKitManager.shared.accessorySession.smartCardInterface; + YKFSmartCardInterface *smartCard = YubiKitManager.shared.accessoryConnection.smartCardInterface; if (smartCard) { [smartCard executeCommand:command completion:^(NSData * _Nullable data, NSError * _Nullable error) { diff --git a/YubiKit/YubiKit/SPMHeaderLinks/YKFManagementDeviceInfo+Private.h b/YubiKit/YubiKit/SPMHeaderLinks/YKFManagementDeviceInfo+Private.h new file mode 120000 index 00000000..25af73bb --- /dev/null +++ b/YubiKit/YubiKit/SPMHeaderLinks/YKFManagementDeviceInfo+Private.h @@ -0,0 +1 @@ +..//Connections/Shared/Sessions/YKFManagementDeviceInfo+Private.h \ No newline at end of file diff --git a/YubiKit/YubiKit/SPMHeaderLinks/YKFManagementDeviceInfo.h b/YubiKit/YubiKit/SPMHeaderLinks/YKFManagementDeviceInfo.h new file mode 120000 index 00000000..1d1c5eea --- /dev/null +++ b/YubiKit/YubiKit/SPMHeaderLinks/YKFManagementDeviceInfo.h @@ -0,0 +1 @@ +..//Connections/Shared/Sessions/YKFManagementDeviceInfo.h \ No newline at end of file diff --git a/YubiKit/YubiKit/SPMHeaderLinks/YKFManagementError.h b/YubiKit/YubiKit/SPMHeaderLinks/YKFManagementError.h deleted file mode 120000 index 80cd100b..00000000 --- a/YubiKit/YubiKit/SPMHeaderLinks/YKFManagementError.h +++ /dev/null @@ -1 +0,0 @@ -..//Connections/Shared/Errors/YKFManagementError.h \ No newline at end of file diff --git a/YubiKit/YubiKit/SPMHeaderLinks/YKFManagementReadConfigurationResponse+Private.h b/YubiKit/YubiKit/SPMHeaderLinks/YKFManagementReadConfigurationResponse+Private.h deleted file mode 120000 index 95afb6b1..00000000 --- a/YubiKit/YubiKit/SPMHeaderLinks/YKFManagementReadConfigurationResponse+Private.h +++ /dev/null @@ -1 +0,0 @@ -..//Connections/Shared/Requests/MGMT/YKFManagementReadConfigurationResponse+Private.h \ No newline at end of file diff --git a/YubiKit/YubiKit/SPMHeaderLinks/YKFManagementReadConfigurationResponse.h b/YubiKit/YubiKit/SPMHeaderLinks/YKFManagementReadConfigurationResponse.h deleted file mode 120000 index f1720c0a..00000000 --- a/YubiKit/YubiKit/SPMHeaderLinks/YKFManagementReadConfigurationResponse.h +++ /dev/null @@ -1 +0,0 @@ -..//Connections/Shared/Requests/MGMT/YKFManagementReadConfigurationResponse.h \ No newline at end of file diff --git a/YubiKit/YubiKit/SPMHeaderLinks/YKFManagementSessionFeatures.h b/YubiKit/YubiKit/SPMHeaderLinks/YKFManagementSessionFeatures.h new file mode 120000 index 00000000..680a866d --- /dev/null +++ b/YubiKit/YubiKit/SPMHeaderLinks/YKFManagementSessionFeatures.h @@ -0,0 +1 @@ +..//Connections/Shared/Sessions/YKFManagementSessionFeatures.h \ No newline at end of file diff --git a/YubiKit/YubiKit/SPMHeaderLinks/YKFSession+Private.h b/YubiKit/YubiKit/SPMHeaderLinks/YKFSession+Private.h new file mode 120000 index 00000000..0dfd5325 --- /dev/null +++ b/YubiKit/YubiKit/SPMHeaderLinks/YKFSession+Private.h @@ -0,0 +1 @@ +..//Connections/Shared/Sessions/YKFSession+Private.h \ No newline at end of file diff --git a/YubiKit/YubiKit/SharedModel/YKFOTPTokenParser.m b/YubiKit/YubiKit/SharedModel/YKFOTPTokenParser.m index 7d13897b..8db77831 100644 --- a/YubiKit/YubiKit/SharedModel/YKFOTPTokenParser.m +++ b/YubiKit/YubiKit/SharedModel/YKFOTPTokenParser.m @@ -86,20 +86,20 @@ - (instancetype)init { YKFLogInfo(@"Metdata is URI"); token.value = [self.uriParser tokenFromPayload:payload]; - YKFLogInfo(@"OTP value %@", token.value); + YKFLogVerbose(@"OTP value %@", token.value); token.uri = [self.uriParser uriFromPayload:payload]; - YKFLogInfo(@"OTP uri %@", token.uri); + YKFLogVerbose(@"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); + YKFLogVerbose(@"OTP value %@", token.value); token.text = [self.textParser textFromPayload:payload]; - YKFLogInfo(@"OTP text %@", token.text); + YKFLogVerbose(@"OTP text %@", token.text); } if (!token.value.length) { // Payload was extracted but it was not a valid or malformed URL/Text diff --git a/YubiKit/YubiKit/YubiKit.h b/YubiKit/YubiKit/YubiKit.h index 48fca770..41456524 100644 --- a/YubiKit/YubiKit/YubiKit.h +++ b/YubiKit/YubiKit/YubiKit.h @@ -53,6 +53,8 @@ #import "YKFPIVSessionFeatures.h" #import "YKFPIVManagementKeyType.h" #import "YKFPIVManagementKeyMetadata.h" +#import "YKFManagementDeviceInfo.h" +#import "YKFManagementInterfaceConfiguration.h" #import "YKFPIVKeyType.h" #import "YKFChallengeResponseSession.h" #import "YKFManagementSession.h" diff --git a/YubiKit/YubiKit/YubiKitManager.h b/YubiKit/YubiKit/YubiKitManager.h index d837722a..d87e4967 100644 --- a/YubiKit/YubiKit/YubiKitManager.h +++ b/YubiKit/YubiKit/YubiKitManager.h @@ -77,6 +77,19 @@ */ - (void)didDisconnectAccessory:(YKFAccessoryConnection *_Nonnull)connection error:(NSError *_Nullable)error; +/*! + @method didFailConnectingNFC + + @abstract + The YubiKey SDK did receive a NFC connection error. This is typically that the user pressed cancel (error code 200) in the + NFC modal or it timed out (error code 201) waiting for a NFC YubiKey. + + @method error + The NSError passed by the SDK. + */ +@optional +- (void)didFailConnectingNFC:(NSError *_Nonnull)error; + @end @@ -93,7 +106,9 @@ @abstract The delegate must conform to the YKFManagerDelegate protocol. Setting this delegate will allow you - to get notifications when a connection to the YubiKey is established or broken. + to get notifications when a connection to the YubiKey is established or broken. If a connection is + already established when the delegate is assigned the didConnect delegate methods will be called + immediately. */ @property(nonatomic, weak) id _Nullable delegate; @@ -117,6 +132,30 @@ */ - (void)stopNFCConnection API_AVAILABLE(ios(13.0)); +/*! + @method stopNFCConnectionWithMessage: + + @abstract + Stop the NFC connection and display a message. + + @discussion + Use this method to close the NFC connection and display a message to the user. + */ +- (void)stopNFCConnectionWithMessage:(NSString *_Nonnull)message API_AVAILABLE(ios(13.0)) NS_SWIFT_NAME(stopNFCConnection(withMessage:)); + +/*! + @method stopNFCConnectionWithErrorMessage: + + @abstract + Stop the NFC connection and display a error message. + + @discussion + Use this method to close the NFC connection and display a message to the user when an error condition + occurs after the app successfully executed a command. This type of error can occur if, for example, + you send a password to unlock the YubiKey and it is not the matching password. + */ +- (void)stopNFCConnectionWithErrorMessage:(NSString *_Nonnull)errorMessage API_AVAILABLE(ios(13.0)) NS_SWIFT_NAME(stopNFCConnection(withErrorMessage:)); + /*! @method startAccessoryConnection diff --git a/YubiKit/YubiKit/YubiKitManager.m b/YubiKit/YubiKit/YubiKitManager.m index 652fd2a3..ceaed6d6 100644 --- a/YubiKit/YubiKit/YubiKitManager.m +++ b/YubiKit/YubiKit/YubiKitManager.m @@ -23,15 +23,15 @@ @interface YubiKitManager() -@property (nonatomic, readwrite) YKFNFCConnection *nfcSession NS_AVAILABLE_IOS(11.0); -@property (nonatomic, readwrite) YKFAccessoryConnection *accessorySession; -@property (nonatomic, readwrite) YKFNFCOTPSession *otpSession NS_AVAILABLE_IOS(11.0); +@property (nonatomic, readwrite) YKFNFCConnection *nfcConnection; +@property (nonatomic, readwrite) YKFAccessoryConnection *accessoryConnection; +@property (nonatomic, readwrite) YKFNFCOTPSession *otpSession; @end @implementation YubiKitManager -@synthesize delegate; +__weak id _delegate; static YubiKitManager *sharedInstance; @@ -49,14 +49,14 @@ - (instancetype)initOnce { if (@available(iOS 11, *)) { YKFNFCConnection *nfcConnection = [[YKFNFCConnection alloc] init]; nfcConnection.delegate = self; - self.nfcSession = nfcConnection; + self.nfcConnection = nfcConnection; } YKFAccessoryConnectionConfiguration *configuration = [[YKFAccessoryConnectionConfiguration alloc] init]; EAAccessoryManager *accessoryManager = [EAAccessoryManager sharedAccessoryManager]; YKFAccessoryConnection *accessoryConnection = [[YKFAccessoryConnection alloc] initWithAccessoryManager:accessoryManager configuration:configuration]; accessoryConnection.delegate = self; - self.accessorySession = accessoryConnection; + self.accessoryConnection = accessoryConnection; if (@available(iOS 11.0, *)) { self.otpSession = [[YKFNFCOTPSession alloc] initWithTokenParser:nil session:nil]; @@ -65,26 +65,44 @@ - (instancetype)initOnce { return self; } -//- (void)setDelegate:(id)delegate { -// self.delegate = delegate; -//} +- (void)setDelegate:(id)delegate { + _delegate = delegate; + if (self.accessoryConnection.state == YKFAccessoryConnectionStateOpen) { + [self.delegate didConnectAccessory:self.accessoryConnection]; + } else if (self.nfcConnection.state == YKFNFCConnectionStateOpen) { + [self.delegate didConnectNFC:self.nfcConnection]; + } +} + +-(id)delegate { + return _delegate; +} - (void)startAccessoryConnection { - [self.accessorySession start]; + [self.accessoryConnection start]; } - (void)stopAccessoryConnection { - [self.accessorySession stop]; + [self.accessoryConnection stop]; } - (void)startNFCConnection API_AVAILABLE(ios(13.0)) { - [self.nfcSession start]; + [self.nfcConnection start]; } - (void)stopNFCConnection API_AVAILABLE(ios(13.0)) { - [self.nfcSession stop]; + [self.nfcConnection stop]; +} + +- (void)stopNFCConnectionWithMessage:(NSString *_Nonnull)message API_AVAILABLE(ios(13.0)) { + [self.nfcConnection stopWithMessage:message]; } +- (void)stopNFCConnectionWithErrorMessage:(NSString *_Nonnull)errorMessage API_AVAILABLE(ios(13.0)) { + [self.nfcConnection stopWithErrorMessage:errorMessage]; +} + + - (void)didConnectAccessory:(YKFAccessoryConnection *_Nonnull)connection { [self.delegate didConnectAccessory:connection]; } @@ -102,4 +120,10 @@ - (void)didDisconnectNFC:(YKFNFCConnection *_Nonnull)connection error:(NSError * [self.delegate didDisconnectNFC:connection error:error]; } +- (void)didFailConnectingNFC:(NSError *)error { + if ([self.delegate respondsToSelector:@selector(didFailConnectingNFC:)]) { + [self.delegate didFailConnectingNFC:error]; + } +} + @end diff --git a/YubiKit/YubiKitTests/Fakes/FakeYKFConnectionController.m b/YubiKit/YubiKitTests/Fakes/FakeYKFConnectionController.m index b6ee2458..68c61b29 100644 --- a/YubiKit/YubiKitTests/Fakes/FakeYKFConnectionController.m +++ b/YubiKit/YubiKitTests/Fakes/FakeYKFConnectionController.m @@ -90,6 +90,10 @@ - (void)cancelAllCommands { // Do nothing } +- (void)dispatchBlockOnCommunicationQueue:(nonnull YKFConnectionControllerCommunicationQueueBlock)block { + // Do nothing +} + #pragma mark - Helpers - (NSData *)nextResponseDataInSequence { diff --git a/YubiKit/YubiKitTests/Tests/YKFAccessoryConnectionControllerTests.m b/YubiKit/YubiKitTests/Tests/YKFAccessoryConnectionControllerTests.m index e6c608e7..e873e7dd 100644 --- a/YubiKit/YubiKitTests/Tests/YKFAccessoryConnectionControllerTests.m +++ b/YubiKit/YubiKitTests/Tests/YKFAccessoryConnectionControllerTests.m @@ -147,48 +147,6 @@ - (void)test_WhenConnectionControllerWritesCommands_ResponseIsReadFromTheInputSt 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 { diff --git a/YubiKit/YubiKitTests/Tests/YKFOTPTokenParserTests.m b/YubiKit/YubiKitTests/Tests/YKFOTPTokenParserTests.m index a2550a2d..cc8057ce 100644 --- a/YubiKit/YubiKitTests/Tests/YKFOTPTokenParserTests.m +++ b/YubiKit/YubiKitTests/Tests/YKFOTPTokenParserTests.m @@ -234,22 +234,20 @@ - (void)test_WhenCustomTextParserIsSet_ParserCallsTheCustomTextParser { #pragma mark - Helpers - (NSArray *)nfcResponseWithPayload:(NSString *)payload metadataType:(YKFOTPMetadataType)type { - NFCNDEFPayload *nfcPayload = [NFCNDEFPayload new]; UInt8 payloadTypeURIBytes[1] = {'U'}; UInt8 payloadTypeTextBytes[1] = {'T'}; + NSData *payloadType; if (type == YKFOTPMetadataTypeURI) { - nfcPayload.type = [NSData dataWithBytes:payloadTypeURIBytes length:1]; + payloadType = [NSData dataWithBytes:payloadTypeURIBytes length:1]; } else if (type == YKFOTPMetadataTypeText){ - nfcPayload.type = [NSData dataWithBytes:payloadTypeTextBytes length:1]; + payloadType = [NSData dataWithBytes:payloadTypeTextBytes length:1]; } - - nfcPayload.typeNameFormat = NFCTypeNameFormatNFCWellKnown; - nfcPayload.payload = [self uriNFCPayloadDataWithString:payload metadataType:type]; - - NFCNDEFMessage *nfcMessage = [NFCNDEFMessage new]; - nfcMessage.records = @[nfcPayload]; + + NFCNDEFPayload *nfcPayload = [[NFCNDEFPayload alloc] initWithFormat:NFCTypeNameFormatNFCWellKnown type:payloadType identifier:[NSData new] payload:[self uriNFCPayloadDataWithString:payload metadataType:type]]; + + NFCNDEFMessage *nfcMessage = [[NFCNDEFMessage alloc] initWithNDEFRecords:@[nfcPayload]]; return @[nfcMessage]; } diff --git a/YubiKitTests/Tests/ConnectionTests.swift b/YubiKitTests/Tests/ConnectionTests.swift new file mode 100644 index 00000000..c7dc980b --- /dev/null +++ b/YubiKitTests/Tests/ConnectionTests.swift @@ -0,0 +1,164 @@ +// Copyright 2018-2020 Yubico AB +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES 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 + +class ConnectionTests: XCTestCase { + + func testConnectionDelegate() throws { + YubiKitManager.shared.startAccessoryConnection() + let connectionExpectation = expectation(description: "Get a YubiKey Connection") + let firstConnection = YubiKeyConnectionTester() + Thread.sleep(forTimeInterval: 0.5) + firstConnection.connection { first, error in + print("✅ got first connection") + let secondConnection = YubiKeyConnectionTester() + secondConnection.connection { second, error in + print("✅ got second connection") + connectionExpectation.fulfill(); + } + } + waitForExpectations(timeout: 10.0) { error in + // If we get an error then the expectation has timed out and we need to stop all connections + if error != nil { + YubiKitManager.shared.stopAccessoryConnection() + YubiKitManager.shared.stopNFCConnection() + Thread.sleep(forTimeInterval: 5.0) // In case it was a NFC connection wait for the modal to dismiss + } + } + } + + func testNFCTimeOutError() throws { + let connectionExpectation = expectation(description: "Get a YubiKey Connection") + let connectionTester = YubiKeyConnectionTester() + YubiKitManager.shared.startAccessoryConnection() // We need to start the accessory connection so we can skip this test if a 5Ci key is inserted + Thread.sleep(forTimeInterval: 0.5) + print("👉 Wait for NFC modal to dismiss (this will take a long time)!") + + connectionTester.nfcConnectionErrorCallback = { error in + XCTAssert((error as NSError).code == 201) + print("✅ got expected NFC timeout error (201)") + connectionExpectation.fulfill(); + } + + connectionTester.connection { connection, error in + print("YubiKey 5Ci connected. Skip test.") + connectionExpectation.fulfill() + return + } + + waitForExpectations(timeout: 120.0) { error in + // If we get an error then the expectation has timed out and we need to stop all connections + if error != nil { + YubiKitManager.shared.stopNFCConnection() + Thread.sleep(forTimeInterval: 5.0) // In case it was a NFC connection wait for the modal to dismiss + } + } + } + + func testNFCUserCancelError() throws { + let connectionExpectation = expectation(description: "Got a YubiKey failed to connect to NFC error") + let connectionTester = YubiKeyConnectionTester() + YubiKitManager.shared.startAccessoryConnection() // We need to start the accessory connection so we can skip this test if a 5Ci key is inserted + Thread.sleep(forTimeInterval: 0.5) + + connectionTester.nfcConnectionErrorCallback = { error in + XCTAssert((error as NSError).code == 200) + print("✅ got expected user canceled NFC error (200)") + connectionExpectation.fulfill(); + } + + print("👉 Press cancel in NFC modal!") + connectionTester.connection { connection, error in + print("YubiKey connected. Skip test.") + connectionExpectation.fulfill() + return + } + + waitForExpectations(timeout: 20.0) { error in + // If we get an error then the expectation has timed out and we need to stop all connections + if error != nil { + YubiKitManager.shared.stopNFCConnection() + Thread.sleep(forTimeInterval: 5.0) // In case it was a NFC connection wait for the modal to dismiss + } + } + } +} + +class YubiKeyConnectionTester: NSObject { + + var accessoryConnection: YKFAccessoryConnection? + var nfcConnection: YKFNFCConnection? + var connectionCallback: ((_ connection: YKFConnectionProtocol, _ error: Error?) -> Void)? + var errorCallback: ((_ error: Error) -> Void)? + var nfcConnectionErrorCallback: ((_ error: Error) -> Void)? + + override init() { + super.init() + YubiKitManager.shared.delegate = self + } + + func connection(completion: @escaping (_ connection: YKFConnectionProtocol, _ error: Error?) -> Void) { + if let connection = accessoryConnection { + completion(connection, nil) + } else if let connection = nfcConnection { + completion(connection, nil) + } else { + connectionCallback = completion + YubiKitManager.shared.startNFCConnection() + } + } +} + +extension YubiKeyConnectionTester: YKFManagerDelegate { + func didFailConnectingNFC(_ error: Error) { + if let callback = nfcConnectionErrorCallback { + callback(error) + } + } + + func didReceiveConnectionError(_ error: Error) { + if let callback = errorCallback { + callback(error) + } + } + + func didConnectNFC(_ connection: YKFNFCConnection) { + nfcConnection = connection + if let callback = connectionCallback { + callback(connection, nil) + } + } + + func didDisconnectNFC(_ connection: YKFNFCConnection, error: Error?) { + if let callback = connectionCallback { + callback(connection, error) + } + nfcConnection = nil + } + + func didConnectAccessory(_ connection: YKFAccessoryConnection) { + accessoryConnection = connection + if let callback = connectionCallback { + callback(connection, nil) + } + } + + func didDisconnectAccessory(_ connection: YKFAccessoryConnection, error: Error?) { + if let callback = connectionCallback { + callback(connection, error) + } + accessoryConnection = nil + } +} diff --git a/YubiKitTests/Tests/ManagementTests.swift b/YubiKitTests/Tests/ManagementTests.swift index 9ea615b7..97177ad6 100644 --- a/YubiKitTests/Tests/ManagementTests.swift +++ b/YubiKitTests/Tests/ManagementTests.swift @@ -24,8 +24,8 @@ class ManagementTests: XCTestCase { } else { transport = .USB } - connection.managementSessionAndConfiguration { session, response in - guard let configuration = response.configuration else { XCTAssertTrue(false, "No configuration"); return } + connection.managementSessionAndDeviceInfo { session, deviceInfo in + guard let configuration = deviceInfo.configuration else { XCTAssertTrue(false, "No configuration"); return } configuration.setEnabled(false, application: .OATH, overTransport: transport) session.write(configuration, reboot: false) { error in guard error == nil else { XCTAssertTrue(false, "Error while writing configuration: \(error!)"); return } @@ -54,8 +54,8 @@ class ManagementTests: XCTestCase { } else { transport = .USB } - connection.managementSessionAndConfiguration { session, response in - guard let configuration = response.configuration else { XCTAssertTrue(false, "No configuration"); return } + connection.managementSessionAndDeviceInfo { session, deviceInfo in + guard let configuration = deviceInfo.configuration else { XCTAssertTrue(false, "No configuration"); return } configuration.setEnabled(false, application: .U2F, overTransport: transport) session.write(configuration, reboot: false) { error in guard error == nil else { XCTAssertTrue(false, "Error while writing configuration: \(error!)"); return } @@ -78,25 +78,42 @@ class ManagementTests: XCTestCase { func testReadKeyVersion() { runYubiKitTest { connection, completion in - connection.managementSessionAndConfiguration { session, response in + connection.managementSessionAndDeviceInfo { session, deviceInfo in // Only assert major and minor version - XCTAssert(response.version.major == 5) - XCTAssert(response.version.minor == 2 || response.version.minor == 3 || response.version.minor == 4) - print("✅ Got version: \(response.version.major).\(response.version.minor).\(response.version.micro)") + XCTAssert(deviceInfo.version.major == 5) + XCTAssert(deviceInfo.version.minor == 2 || deviceInfo.version.minor == 3 || deviceInfo.version.minor == 4) + print("✅ Got version: \(deviceInfo.version)") completion() } } } + + func testDeviceInfo() { + runYubiKitTest { connection, completion in + connection.managementSession { session, error in + guard let session = session else { XCTFail("Failed to get Management Session"); return } + session.getDeviceInfo { deviceInfo, error in + guard let deviceInfo = deviceInfo else { XCTFail("Failed to get DeviceInfo: \(error!)"); return } + print("✅ Got device info:") + print(" is locked: \(deviceInfo.isConfigurationLocked)") + print(" serial number: \(deviceInfo.serialNumber)") + print(" form factor: \(deviceInfo.formFactor.rawValue)") + print(" firmware version: \(deviceInfo.version)") + completion() + } + } + } + } } extension YKFConnectionProtocol { - func managementSessionAndConfiguration(completion: @escaping (_ session: YKFManagementSession, - _ response: YKFManagementReadConfigurationResponse) -> Void) { + func managementSessionAndDeviceInfo(completion: @escaping (_ session: YKFManagementSession, + _ response: YKFManagementDeviceInfo) -> Void) { self.managementSession { session, error in guard let session = session else { XCTAssertTrue(false, "Failed to get Management Session: \(error!)"); return } - session.readConfiguration { response, error in - guard let response = response else { XCTAssertTrue(false, "Failed to read Configuration: \(error!)"); return } - completion(session, response) + session.getDeviceInfo { deviceInfo, error in + guard let deviceInfo = deviceInfo else { XCTAssertTrue(false, "Failed to read device info: \(error!)"); return } + completion(session, deviceInfo) } } } diff --git a/YubiKitTests/Tests/OATHTests.swift b/YubiKitTests/Tests/OATHTests.swift index 67a25f58..bfa12ff3 100644 --- a/YubiKitTests/Tests/OATHTests.swift +++ b/YubiKitTests/Tests/OATHTests.swift @@ -38,6 +38,64 @@ class OATHTests: XCTestCase { } } + func testCalculateLotsOfTOTP() throws { + runYubiKitTest { connection, completion in + connection.oathTestSession { session in + for n in 0...19 { + session.storeRandomCredential(number: n) + } + session.calculateAll { credentials, error in + guard let credentials = credentials else { XCTFail("Error: \(error!)"); completion(); return } + print("✅ calculated \(credentials.count) credentials") + completion() + } + } + } + } + + func testCalculateAllTouchRequiredTOTP() throws { + runYubiKitTest { connection, completion in + connection.oathTestSession { session in + let totpUrl = URL(string: "otpauth://totp/Yubico:test-totp@yubico.com?secret=UOA6FJYR76R7IRZBGDJKLYICL3MUR7QH&issuer=test-requires-touch&algorithm=SHA1&digits=6&counter=30")! + let template = YKFOATHCredentialTemplate(url: totpUrl)! + session.put(template, requiresTouch: true) { error in + guard error == nil else { XCTFail("Failed creating TOTP"); return } + session.calculateAll { credentials, error in + guard let credential = credentials?.first else { XCTFail("No credentials calculated"); return } + XCTAssert(credential.credential.issuer == "test-requires-touch") + XCTAssert(credential.credential.accountName == "test-totp@yubico.com") + XCTAssert(credential.credential.requiresTouch == true) + print("✅ create OATH TOTP credential that requires touch") + completion() + } + } + } + } + } + + func testCreateListAndCalculateTOTP() throws { + runYubiKitTest { connection, completion in + connection.oathTestSession { session in + let url = URL(string: "otpauth://totp/Yubico:test@yubico.com?secret=UOA6FJYR76R7IRZBGDJKLYICL3MUR7QH&issuer=test-create-and-calculate&algorithm=SHA1&digits=6&counter=30")! + let template = YKFOATHCredentialTemplate(url: url)! + session.put(template, requiresTouch: false) { error in + guard error == nil else { XCTAssertTrue(false); return } + session.listCredentials { credentials, error in + guard let credential = credentials?.first else { XCTAssertTrue(false); return } +// credential.notTruncated = true + XCTAssert(credential.issuer == "test-create-and-calculate") + XCTAssert(credential.accountName == "test@yubico.com") + session.calculate(credential, timestamp: Date(timeIntervalSince1970: 0)) { code, error in + XCTAssert(code?.otp == "239396") + print("✅ create, list and calculate OATH HOTP credential") + completion() + } + } + } + } + } + } + func testCreateListAndCalculateHOTP() throws { runYubiKitTest { connection, completion in connection.oathTestSession { session in @@ -66,7 +124,7 @@ class OATHTests: XCTestCase { let url = URL(string: "otpauth://totp/Yubico:test@yubico.com?secret=UOA6FJYR76R7IRZBGDJKLYICL3MUR7QH&issuer=test-rename&algorithm=SHA1&digits=6&period=30")! let template = YKFOATHCredentialTemplate(url: url)! session.put(template, requiresTouch: false) { error in - guard error == nil else { XCTAssertTrue(false); return } + guard error == nil else { XCTFail("Failed saving credential: \(error!)"); return } session.listCredentials { credentials, error in guard let credentials = credentials, let credential = credentials.first else { XCTAssertTrue(false); return } session.renameCredential(credential, newIssuer: "test-rename-renamed", newAccount: "renamed@yubico.com") { error in @@ -84,49 +142,70 @@ class OATHTests: XCTestCase { } } - func testSetCodeAndUnlock() throws { + func testUnlock() throws { runYubiKitTest { connection, completion in - connection.oathTestSession { session in - session.setPassword("271828") { error in - guard error == nil else { XCTAssertTrue(false); return } - connection.fido2Session { fidoSession, error in - guard error == nil else { XCTAssertTrue(false); return } - connection.oathSession { session, error in - guard let session = session else { XCTAssert(false); return } - session.unlock(withPassword:"271828") { error in - guard error == nil else { XCTAssert(false); return } - session.listCredentials { credentials, error in - XCTAssert(error == nil) - print("✅ set OATH password and unlock") - completion() - } - } + connection.oathTestSessionWithPassword { session in + session.unlock(withPassword:"271828") { error in + guard error == nil else { XCTAssert(false); return } + session.listCredentials { credentials, error in + XCTAssert(error == nil) + print("✅ set OATH password and unlock") + completion() + } + } + } + } + } + + + func testUnlockWithWrongCode() throws { + runYubiKitTest { connection, completion in + connection.oathTestSessionWithPassword { session in + session.unlock(withPassword:"271844") { error in + guard error != nil else { XCTAssert(false); return } + // Reset OATH on test YubiKey so password is not set when we're done. + // We only do this here since this is the last test in the OATH test suite + // that will run during testing. + session.unlock(withPassword:"271828") { error in + session.reset() { error in + print("✅ set OATH password and try unlock with wrong password") + completion() } } } } } } + + func testSetPasswordWithoutUnlock() throws { + runYubiKitTest { connection, completion in + connection.oathTestSessionWithPassword { session in + session.setPassword("77773") { error in + guard let error = error else { XCTFail("Password probably set without unlock."); return } + let errorCode = YKFOATHErrorCode(rawValue: UInt((error as NSError).code)) + XCTAssertEqual(errorCode, .authenticationRequired) + print("✅ got authenticationRequired when trying to set password without unlock.") + completion() + } + } + } + } - func testSetCodeAndUnlockWithWrongCode() throws { + func testRemoveCode() throws { runYubiKitTest { connection, completion in - connection.oathTestSession { session in - session.setPassword("271828") { error in - guard error == nil else { XCTAssertTrue(false); return } - connection.fido2Session { fidoSession, error in + connection.oathTestSessionWithPassword { session in + session.unlock(withPassword:"271828") { error in + guard error == nil else { XCTAssert(false); return } + session.setPassword("") { error in guard error == nil else { XCTAssertTrue(false); return } - connection.oathSession { session, error in - guard let session = session else { XCTAssert(false); return } - session.unlock(withPassword:"271844") { error in - guard error != nil else { XCTAssert(false); return } - // Reset OATH on test YubiKey so password is not set when we're done. - // We only do this here since this is the last test in the OATH test suite - // that will run during testing. - session.unlock(withPassword:"271828") { error in - session.reset() { error in - print("✅ set OATH password and try unlock with wrong password") - completion() - } + connection.fido2Session { fidoSession, error in + guard error == nil else { XCTAssertTrue(false); return } + connection.oathSession { session, error in + guard let session = session else { XCTAssert(false); return } + session.listCredentials { credentials, error in + XCTAssert(error == nil) + print("✅ set and remove OATH password") + completion() } } } @@ -136,49 +215,73 @@ class OATHTests: XCTestCase { } } - - func testSetAndRemoveCode() throws { + func testAuthFailAndThenAuthAgain() throws { runYubiKitTest { connection, completion in - connection.oathTestSession { session in - session.setPassword("271828") { error in - guard error == nil else { XCTAssertTrue(false); return } - connection.fido2Session { fidoSession, error in - guard error == nil else { XCTAssertTrue(false); return } - connection.oathSession { session, error in - guard let session = session else { XCTAssertTrue(false); return } - session.unlock(withPassword:"271828") { error in - guard error == nil else { XCTAssert(false); return } - session.setPassword("") { error in - guard error == nil else { XCTAssertTrue(false); return } - connection.fido2Session { fidoSession, error in - guard error == nil else { XCTAssertTrue(false); return } - connection.oathSession { session, error in - guard let session = session else { XCTAssert(false); return } - session.listCredentials { credentials, error in - XCTAssert(error == nil) - print("✅ set and remove OATH password") - completion() - } - } - } - } - } + connection.oathTestSessionWithPassword { session in + session.calculateAll { credentials, error in + guard let error = error else { XCTFail("Did not get auth error"); return } + let errorCode = YKFOATHErrorCode(rawValue: UInt((error as NSError).code)) + XCTAssertEqual(errorCode, .authenticationRequired) + session.unlock(withPassword:"271828") { error in + guard error == nil else { XCTFail("Failed to unlock: \(error!)"); return } + session.listCredentials { credentials, error in + XCTAssert(error == nil) + print("✅ set OATH password and unlock") + completion() } } } } } } + + // This test is here to remove any lingering password for the OATH application + func testZeeLastOne() throws { + runYubiKitTest { connection, completion in + connection.oathTestSession { session in + print("✅ last OATH test, reset application") + completion() + } + } + } +} + +extension YKFOATHSession { + func storeRandomCredential(number: Int) { + let account = "test-\(number)@yubico.com" + let issuer = "Account-\(number)" + let secret = "UOA6FJYR76R\(number)BGDJKLYICL3MUR7QH" + let url = URL(string: "otpauth://totp/Yubico:\(account)?secret=\(secret)&\(issuer)&algorithm=SHA1&digits=6&period=30")! + let template = YKFOATHCredentialTemplate(url: url)! + self.put(template, requiresTouch: false) { error in + if error != nil { XCTFail("Error: \(error!)") } + } + } } extension YKFConnectionProtocol { func oathTestSession(completion: @escaping (_ session: YKFOATHSession) -> Void) { self.oathSession { session, error in - guard let session = session else { XCTAssertTrue(false, "Failed to get OATH session"); return } + guard let session = session else { XCTFail("Failed to get OATH session: \(error!)"); return } session.reset { error in - guard error == nil else { XCTAssertTrue(false, "Failed to reset OATH"); return } + guard error == nil else { XCTFail("Failed to reset OATH session: \(error!)"); return } completion(session) } } } + + func oathTestSessionWithPassword(password: String = "271828", completion: @escaping (_ session: YKFOATHSession) -> Void) { + self.oathTestSession { session in + session.setPassword(password) { error in + guard error == nil else { XCTFail("Failed to set password '\(password)'"); return } + self.fido2Session { fidoSession, error in + guard error == nil else { XCTFail("Failed to reset OATH by getting a FIDO2 session"); return } + self.oathSession { session, error in + guard let session = session else { XCTFail("Failed to get OATH session: \(error!)"); return } + completion(session) + } + } + } + } + } } diff --git a/YubiKitTests/Tests/PIVTests.swift b/YubiKitTests/Tests/PIVTests.swift index 7a101a70..2c7d0db0 100644 --- a/YubiKitTests/Tests/PIVTests.swift +++ b/YubiKitTests/Tests/PIVTests.swift @@ -468,19 +468,23 @@ class PIVTests: XCTestCase { runYubiKitTest { connection, completion in connection.pivTestSession { session in session.verifyPin("333333") { retries, error in - XCTAssertNotNil(error) + guard let error = error else { XCTFail("Error was not nil"); return } + XCTAssert((error as NSError).code == YKFPIVFErrorCode.invalidPin.rawValue) XCTAssert(retries == 2) print("✅ PIN retry count \(retries)") session.verifyPin("111111") { retries, error in - XCTAssertNotNil(error) + guard let error = error else { XCTFail("Error was not nil"); return } + XCTAssert((error as NSError).code == YKFPIVFErrorCode.invalidPin.rawValue) XCTAssert(retries == 1) print("✅ PIN retry count \(retries)") session.verifyPin("444444") { retries, error in - XCTAssertNotNil(error) + guard let error = error else { XCTFail("Error was not nil"); return } + XCTAssert((error as NSError).code == YKFPIVFErrorCode.pinLocked.rawValue) XCTAssert(retries == 0) print("✅ PIN retry count \(retries)") session.verifyPin("111111") { retries, error in - XCTAssertNotNil(error) + guard let error = error else { XCTFail("Error was not nil"); return } + XCTAssert((error as NSError).code == YKFPIVFErrorCode.pinLocked.rawValue) XCTAssert(retries == 0) print("✅ PIN retry count \(retries)") completion() diff --git a/YubiKitTests/Tests/SessionTests.swift b/YubiKitTests/Tests/SessionTests.swift index df4c1061..16daf2b2 100644 --- a/YubiKitTests/Tests/SessionTests.swift +++ b/YubiKitTests/Tests/SessionTests.swift @@ -15,6 +15,36 @@ import XCTest class SessionTests: XCTestCase { + + func testDispatchAfterCommands() throws { + runYubiKitTest { connection, completion in + connection.oathSession { session, error in + guard let session = session else { completion(); XCTFail("Failed to get session: \(error!)"); return } + session.calculateAll { credentials, error in + print("Finished calculating first set of credentials...") + } + session.calculateAll { credentials, error in + print("Finished calculating second set of credentials...") + } + session.calculateAll { credentials, error in + print("Finished calculating third set of credentials...") + } + session.calculateAll { credentials, error in + print("Finished calculating fourth set of credentials...") + } + session.dispatchAfterCurrentCommands { + print("✅ Block enqueued after fourth set executed...") + } + session.calculateAll { credentials, error in + print("Finished calculating fifth set of credentials...") + } + session.dispatchAfterCurrentCommands { + print("✅ Block enqueued after fifth set executed...") + completion() + } + } + } + } func testOATHSession() throws { runYubiKitTest { connection, completion in diff --git a/YubiKitTests/Tests/Utilities/YubiKeyConnection.swift b/YubiKitTests/Tests/Utilities/YubiKeyConnection.swift index d2588f8a..7de013c0 100644 --- a/YubiKitTests/Tests/Utilities/YubiKeyConnection.swift +++ b/YubiKitTests/Tests/Utilities/YubiKeyConnection.swift @@ -17,7 +17,8 @@ class YubiKeyConnection: NSObject { var accessoryConnection: YKFAccessoryConnection? var nfcConnection: YKFNFCConnection? var connectionCallback: ((_ connection: YKFConnectionProtocol) -> Void)? - + var connectionErrorCallback: ((_ error: Error) -> Void)? + override init() { super.init() YubiKitManager.shared.delegate = self @@ -47,6 +48,9 @@ extension YubiKeyConnection: YKFManagerDelegate { nfcConnection = nil } + func didFailConnectingNFC(_ error: Error) { + } + func didConnectAccessory(_ connection: YKFAccessoryConnection) { accessoryConnection = connection } diff --git a/YubiKitTests/YubiKitTests.xcodeproj/project.pbxproj b/YubiKitTests/YubiKitTests.xcodeproj/project.pbxproj index f242bc9d..0bf15dd8 100644 --- a/YubiKitTests/YubiKitTests.xcodeproj/project.pbxproj +++ b/YubiKitTests/YubiKitTests.xcodeproj/project.pbxproj @@ -22,6 +22,7 @@ 516329F4258A20A20043CBB5 /* YubiKeyConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 516329F3258A20A20043CBB5 /* YubiKeyConnection.swift */; }; 516329FA258A213C0043CBB5 /* XCTestCase+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 516329F9258A213C0043CBB5 /* XCTestCase+Extensions.swift */; }; 51ACC30625D6C8370069214B /* PIVTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51ACC30525D6C8370069214B /* PIVTests.swift */; }; + 51F8E3C12638977E0010686B /* ConnectionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51F8E3C02638977E0010686B /* ConnectionTests.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -57,6 +58,7 @@ 516329F3258A20A20043CBB5 /* YubiKeyConnection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = YubiKeyConnection.swift; sourceTree = ""; }; 516329F9258A213C0043CBB5 /* XCTestCase+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "XCTestCase+Extensions.swift"; sourceTree = ""; }; 51ACC30525D6C8370069214B /* PIVTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PIVTests.swift; sourceTree = ""; }; + 51F8E3C02638977E0010686B /* ConnectionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectionTests.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -129,12 +131,13 @@ 510339EC2589130400EB3692 /* Tests */ = { isa = PBXGroup; children = ( + 51F8E3C02638977E0010686B /* ConnectionTests.swift */, + 510339ED2589130400EB3692 /* SessionTests.swift */, 510A9D932590E4B50015313E /* ChallengeResponseTests.swift */, 510A9D8D258BA73D0015313E /* FIDO2Tests.swift */, 5105C052258B482800278F7F /* ManagementTests.swift */, 5105C045258A4A4800278F7F /* OATHTests.swift */, 51ACC30525D6C8370069214B /* PIVTests.swift */, - 510339ED2589130400EB3692 /* SessionTests.swift */, 510A9DAD2591D6FD0015313E /* U2FTests.swift */, 516329F2258A20840043CBB5 /* Utilities */, 510339EF2589130400EB3692 /* Info.plist */, @@ -272,6 +275,7 @@ 510339EE2589130400EB3692 /* SessionTests.swift in Sources */, 510A9D8E258BA73D0015313E /* FIDO2Tests.swift in Sources */, 5105C04D258A541900278F7F /* NSDate+Utils.m in Sources */, + 51F8E3C12638977E0010686B /* ConnectionTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -337,6 +341,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -391,6 +396,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = iphoneos;