diff --git a/Changelog.md b/Changelog.md index 324bc4f2..983f7490 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,5 +1,9 @@ # YubiKit Changelog +## 4.5.0 + +- Add support for sending and returning raw commands to the `YKFConnectionProtocol`. + ## 4.4.1 - Removed deprecated PCSCLayer. diff --git a/README.md b/README.md index 3035116a..eca40ab2 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.4.1' +pod 'YubiKit', '~> 4.5.0' ``` If you want to have latest changes, replace the last line with: diff --git a/YubiKit.podspec b/YubiKit.podspec index bb7eba7e..071bda9d 100644 --- a/YubiKit.podspec +++ b/YubiKit.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'YubiKit' - s.version = '4.4.1' + s.version = '4.5.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/Connections/AccessoryConnection/YKFAccessoryConnection.m b/YubiKit/YubiKit/Connections/AccessoryConnection/YKFAccessoryConnection.m index 576caf11..e14af7ee 100644 --- a/YubiKit/YubiKit/Connections/AccessoryConnection/YKFAccessoryConnection.m +++ b/YubiKit/YubiKit/Connections/AccessoryConnection/YKFAccessoryConnection.m @@ -182,6 +182,22 @@ - (void)managementSession:(YKFManagementSessionCompletion _Nonnull)callback { }]; } +- (void)executeRawCommand:(NSData *)data completion:(YKFRawComandCompletion)completion { + YKFAPDU *apdu = [[YKFAPDU alloc] initWithData:data]; + [self.connectionController execute:apdu completion:^(NSData * _Nullable data, NSError * _Nullable error, NSTimeInterval executionTime) { + completion(data, error); + }]; +} + +- (void)executeRawCommand:(NSData *)data timeout:(NSTimeInterval)timeout completion:(YKFRawComandCompletion)completion { + YKFAPDU *apdu = [[YKFAPDU alloc] initWithData:data]; + [self.connectionController execute:apdu + timeout:timeout + completion:^(NSData * _Nullable response, NSError * _Nullable error, NSTimeInterval executionTime) { + completion(response, error); + }]; +} + - (void)dealloc { self.observeAccessoryConnection = NO; self.observeApplicationState = NO; diff --git a/YubiKit/YubiKit/Connections/NFCConnection/YKFNFCConnection.m b/YubiKit/YubiKit/Connections/NFCConnection/YKFNFCConnection.m index 73a4a7d4..54cb5ba9 100644 --- a/YubiKit/YubiKit/Connections/NFCConnection/YKFNFCConnection.m +++ b/YubiKit/YubiKit/Connections/NFCConnection/YKFNFCConnection.m @@ -138,6 +138,22 @@ - (void)managementSession:(YKFManagementSessionCompletion _Nonnull)callback { }]; } +- (void)executeRawCommand:(NSData *)data completion:(YKFRawComandCompletion)completion { + YKFAPDU *apdu = [[YKFAPDU alloc] initWithData:data]; + [self.connectionController execute:apdu completion:^(NSData * _Nullable data, NSError * _Nullable error, NSTimeInterval executionTime) { + completion(data, error); + }]; +} + +- (void)executeRawCommand:(NSData *)data timeout:(NSTimeInterval)timeout completion:(YKFRawComandCompletion)completion { + YKFAPDU *apdu = [[YKFAPDU alloc] initWithData:data]; + [self.connectionController execute:apdu + timeout:timeout + completion:^(NSData * _Nullable response, NSError * _Nullable error, NSTimeInterval executionTime) { + completion(response, error); + }]; +} + - (void)dealloc { if (@available(iOS 13.0, *)) { [self unobserveIso7816TagAvailability]; diff --git a/YubiKit/YubiKit/Connections/SmartCardConnection/YKFSmartCardConnection.m b/YubiKit/YubiKit/Connections/SmartCardConnection/YKFSmartCardConnection.m index 2853af10..d9b5144c 100644 --- a/YubiKit/YubiKit/Connections/SmartCardConnection/YKFSmartCardConnection.m +++ b/YubiKit/YubiKit/Connections/SmartCardConnection/YKFSmartCardConnection.m @@ -169,4 +169,20 @@ - (void)u2fSession:(YKFU2FSessionCompletionBlock _Nonnull)completion { userInfo:@{NSLocalizedDescriptionKey: @"U2F session not supported by YKFSmartCardConnection."}]); } +- (void)executeRawCommand:(NSData *)data completion:(YKFRawComandCompletion)completion { + YKFAPDU *apdu = [[YKFAPDU alloc] initWithData:data]; + [self.connectionController execute:apdu completion:^(NSData * _Nullable data, NSError * _Nullable error, NSTimeInterval executionTime) { + completion(data, error); + }]; +} + +- (void)executeRawCommand:(NSData *)data timeout:(NSTimeInterval)timeout completion:(YKFRawComandCompletion)completion { + YKFAPDU *apdu = [[YKFAPDU alloc] initWithData:data]; + [self.connectionController execute:apdu + timeout:timeout + completion:^(NSData * _Nullable response, NSError * _Nullable error, NSTimeInterval executionTime) { + completion(response, error); + }]; +} + @end diff --git a/YubiKit/YubiKit/Connections/YKFConnectionProtocol.h b/YubiKit/YubiKit/Connections/YKFConnectionProtocol.h index a6804645..43ed5eae 100644 --- a/YubiKit/YubiKit/Connections/YKFConnectionProtocol.h +++ b/YubiKit/YubiKit/Connections/YKFConnectionProtocol.h @@ -15,7 +15,7 @@ #ifndef YKFConnectionProtocol_h #define YKFConnectionProtocol_h -@class YKFOATHSession, YKFU2FSession, YKFFIDO2Session, YKFPIVSession, YKFChallengeResponseSession, YKFManagementSession, YKFSmartCardInterface; +@class YKFOATHSession, YKFU2FSession, YKFFIDO2Session, YKFPIVSession, YKFChallengeResponseSession, YKFManagementSession, YKFSmartCardInterface, YKFAPDU; @protocol YKFConnectionProtocol @@ -66,6 +66,23 @@ typedef void (^YKFManagementSessionCompletion)(YKFManagementSession *_Nullable, /// when none of the supplied sessions can be used. @property (nonatomic, readonly) YKFSmartCardInterface *_Nullable smartCardInterface; +typedef void (^YKFRawComandCompletion)(NSData *_Nullable, NSError *_Nullable); + +/// @abstract Send a APDU and get the unparsed result as an NSData from the YubiKey. +/// @param apdu The APDU to send to the YubiKey. +/// @param completion The unparsed result from the YubiKey or an error. +/// @discussion Use this for communicating with the YubiKey by sending APDUs to the it. Only use this +/// when the `SmartCardInterface` or any of the supplied sessions can not be used. +- (void)executeRawCommand:(NSData *_Nonnull)apdu completion:(YKFRawComandCompletion _Nonnull)completion; + +/// @abstract Send command as NSData and get the unparsed result as an NSData from the YubiKey. +/// @param data The NSData to send to the YubiKey. +/// @param timeout The timeout to wait before cancelling the command sent to the YubiKey. +/// @param completion The unparsed result from the YubiKey or an error. +/// @discussion Use this for communicating with the YubiKey by sending APDUs to the it. Only use this +/// when the `SmartCardInterface` or any of the supplied sessions can not be used. +- (void)executeRawCommand:(NSData *_Nonnull)data timeout:(NSTimeInterval)timeout completion:(YKFRawComandCompletion _Nonnull)completion; + @end #endif diff --git a/YubiKit/YubiKit/YubiKit.h b/YubiKit/YubiKit/YubiKit.h index 02cbf1a4..ca639319 100644 --- a/YubiKit/YubiKit/YubiKit.h +++ b/YubiKit/YubiKit/YubiKit.h @@ -29,6 +29,8 @@ #import "YKFNFCError.h" #import "YKFNFCTagDescription.h" +#import "YKFConnectionProtocol.h" + #import "YKFAccessoryConnection.h" #import "YKFAccessoryDescription.h" diff --git a/YubiKitTests/Tests/ConnectionTests.swift b/YubiKitTests/Tests/ConnectionTests.swift index 737bfc24..1d001233 100644 --- a/YubiKitTests/Tests/ConnectionTests.swift +++ b/YubiKitTests/Tests/ConnectionTests.swift @@ -17,7 +17,9 @@ import XCTest class ConnectionTests: XCTestCase { func testConnectionDelegate() throws { - YubiKitManager.shared.startAccessoryConnection() + if YubiKitDeviceCapabilities.supportsMFIAccessoryKey { + YubiKitManager.shared.startAccessoryConnection() + } YubiKitManager.shared.startSmartCardConnection() let connectionExpectation = expectation(description: "Get a YubiKey Connection") let firstConnection = YubiKeyConnectionTester() @@ -45,7 +47,9 @@ class ConnectionTests: XCTestCase { 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 + if YubiKitDeviceCapabilities.supportsMFIAccessoryKey { + YubiKitManager.shared.startAccessoryConnection() // We need to start the accessory connection so we can skip this test if a 5Ci key is inserted + } YubiKitManager.shared.startSmartCardConnection() Thread.sleep(forTimeInterval: 0.5) @@ -76,7 +80,9 @@ class ConnectionTests: XCTestCase { 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 + if YubiKitDeviceCapabilities.supportsMFIAccessoryKey { + YubiKitManager.shared.startAccessoryConnection() // We need to start the accessory connection so we can skip this test if a 5Ci key is inserted + } YubiKitManager.shared.startSmartCardConnection() Thread.sleep(forTimeInterval: 0.5) @@ -101,6 +107,35 @@ class ConnectionTests: XCTestCase { } } } + + func testRawCommands() throws { + if YubiKitDeviceCapabilities.supportsMFIAccessoryKey { + YubiKitManager.shared.startAccessoryConnection() + } + YubiKitManager.shared.startSmartCardConnection() + let connectionExpectation = expectation(description: "Get a YubiKey Connection") + let firstConnection = YubiKeyConnectionTester() + Thread.sleep(forTimeInterval: 0.5) + firstConnection.connection { connection, error in + // Select Management application + let data = Data([0x00, 0xa4, 0x04, 0x00, 0x08, 0xa0, 0x00, 0x00, 0x05, 0x27, 0x47, 0x11, 0x17]) + connection.executeRawCommand(data) { data, error in + guard let data else { XCTFail("Failed with error: \(error!)"); return } + print(data.hexDescription) + XCTAssertEqual(data.statusCode, 0x9000) + connectionExpectation.fulfill() + } + } + + 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 { @@ -188,3 +223,15 @@ extension YubiKeyConnectionTester: YKFManagerDelegate { smartCardConnection = nil } } + +extension Data { + /// Returns the SW from a key response. + var statusCode: UInt16 { + get { + guard self.count >= 2 else { + return 0x00 + } + return UInt16(self[self.count - 2]) << 8 + UInt16(self[self.count - 1]) + } + } +}