From e2b819d9f3164a25e25ce267536e3db31261d4ba Mon Sep 17 00:00:00 2001 From: Jens Utbult Date: Wed, 27 Mar 2024 15:38:24 +0100 Subject: [PATCH] Expose function for manually sending APDU's to the YubiKey with no handling of multiple read operations. --- .../Tests/ConnectionFullStackTests.swift | 30 +++++ .../Tests/ManagementFullStackTests.swift | 2 +- YubiKit/YubiKit/APDU.swift | 108 +++++++++++------- YubiKit/YubiKit/Connection.swift | 16 +-- YubiKit/YubiKit/LightningConnection.swift | 2 +- YubiKit/YubiKit/NFCConnection.swift | 2 +- YubiKit/YubiKit/Response.swift | 7 +- YubiKit/YubiKit/SmartCardConnection.swift | 2 +- .../Utilities/Connection+Extensions.swift | 4 +- 9 files changed, 112 insertions(+), 61 deletions(-) diff --git a/FullStackTests/Tests/ConnectionFullStackTests.swift b/FullStackTests/Tests/ConnectionFullStackTests.swift index 46f9691..957d616 100644 --- a/FullStackTests/Tests/ConnectionFullStackTests.swift +++ b/FullStackTests/Tests/ConnectionFullStackTests.swift @@ -14,6 +14,7 @@ import XCTest import YubiKit +import CryptoTokenKit @testable import FullStackTests @@ -114,4 +115,33 @@ class ConnectionFullStackTests: XCTestCase { XCTAssert([result1, result2, result3, result4].compactMap { $0 }.count == 1) } } + + func testSendManually() { + runAsyncTest { + let connection = try await Connection.connection() + // Select Management application + let apdu = APDU(cla: 0x00, ins: 0xa4, p1: 0x04, p2: 0x00, command: Data([0xA0, 0x00, 0x00, 0x05, 0x27, 0x47, 0x11, 0x17]), type: .short) + let result = try await connection.sendManually(apdu: apdu) + XCTAssertEqual(result.responseStatus.status, .ok) + /// Get version number + let deviceInfoApdu = APDU(cla: 0, ins: 0x1d, p1: 0, p2: 0) + let deviceInfoResult = try await connection.sendManually(apdu: deviceInfoApdu) + XCTAssertEqual(deviceInfoResult.responseStatus.status, .ok) + let records = TKBERTLVRecord.sequenceOfRecords(from: deviceInfoResult.data.subdata(in: 1.. 0 { - guard command.count < UInt8.max else { fatalError() } - let length = UInt8(command.count) - data.append(length) - data.append(command) - } - case .extended: - if let command, command.count > 0 { - let lengthHigh: UInt8 = UInt8(command.count / 256) - let lengthLow: UInt8 = UInt8(command.count % 256) - data.append(0x00) - data.append(lengthHigh) - data.append(lengthLow) - data.append(command) - } else { - data.append(0x00) - data.append(0x00) - data.append(0x00) + switch storage { + case .explicit(let apdu): + var data = Data() + data.append(apdu.cla) + data.append(apdu.ins) + data.append(apdu.p1) + data.append(apdu.p2) + switch apdu.type { + case .short: + if let command = apdu.command, command.count > 0 { + guard command.count < UInt8.max else { fatalError() } + let length = UInt8(command.count) + data.append(length) + data.append(command) + } + case .extended: + if let command = apdu.command, command.count > 0 { + let lengthHigh: UInt8 = UInt8(command.count / 256) + let lengthLow: UInt8 = UInt8(command.count % 256) + data.append(0x00) + data.append(lengthHigh) + data.append(lengthLow) + data.append(command) + } else { + data.append(0x00) + data.append(0x00) + data.append(0x00) + } } - } - return data + return data + case .rawData(let data): + return data + } + } + + public var description: String { + switch storage { + case .explicit(let apdu): + return "APDU(cla: \(apdu.cla.hexValue), ins: \(apdu.ins.hexValue), p1: \(apdu.p1.hexValue), p2: \(apdu.p2.hexValue), command: \(apdu.command?.hexEncodedString ?? "nil"), type: \(String(describing: apdu.type))" + case .rawData(let data): + return "APDU(data: \(data.hexEncodedString))" + } } } diff --git a/YubiKit/YubiKit/Connection.swift b/YubiKit/YubiKit/Connection.swift index 2d484d3..4d73ccd 100644 --- a/YubiKit/YubiKit/Connection.swift +++ b/YubiKit/YubiKit/Connection.swift @@ -56,19 +56,19 @@ public protocol Connection: AnyObject { /// wrapping the status code will be thrown. @discardableResult func send(apdu: APDU) async throws -> Data + + /// Send an APDU to the Connection and handle the result manually. + /// + /// This will send the APDU to the YubiKey using the Connection. The result will be wrapped in + /// a Response containing the Data and the ResponseStatus. If the returned data is to big for a + /// single read operation this has to be handled manually. + @discardableResult + func sendManually(apdu: APDU) async throws -> Response } internal protocol InternalConnection { - func session() async -> Session? func setSession(_ session: Session?) async - - // The internal version of the send() function returns a Response instead of Data. The reason for this is - // to handle reads of large chunks of data that will be split into multiple reads. If the result is - // to large for a single read that is signaled by sw1 being 0x61. The Response struct will return both - // the data and sw1 and sw2. sendRecursive() in Connection+Extensions will look at the sw1 code and if - // it indicates there's more data to read, it will call itself recursivly to retrieve the next chunk of data. - func send(apdu: APDU) async throws -> Response } extension InternalConnection { diff --git a/YubiKit/YubiKit/LightningConnection.swift b/YubiKit/YubiKit/LightningConnection.swift index 7039828..98259f9 100644 --- a/YubiKit/YubiKit/LightningConnection.swift +++ b/YubiKit/YubiKit/LightningConnection.swift @@ -80,7 +80,7 @@ public final actor LightningConnection: Connection, InternalConnection { } } - internal func send(apdu: APDU) async throws -> Response { + public func sendManually(apdu: APDU) async throws -> Response { guard let accessoryConnection, let outputStream = accessoryConnection.session.outputStream, let inputStream = accessoryConnection.session.inputStream else { throw ConnectionError.noConnection } var data = Data([0x00]) // YLP iAP2 Signal data.append(apdu.data) diff --git a/YubiKit/YubiKit/NFCConnection.swift b/YubiKit/YubiKit/NFCConnection.swift index ae6424b..9582fbe 100644 --- a/YubiKit/YubiKit/NFCConnection.swift +++ b/YubiKit/YubiKit/NFCConnection.swift @@ -100,7 +100,7 @@ public final actor NFCConnection: Connection, InternalConnection { } } - internal func send(apdu: APDU) async throws -> Response { + public func sendManually(apdu: APDU) async throws -> Response { guard let tag else { throw ConnectionError.noConnection } guard let apdu = apdu.nfcIso7816Apdu else { throw NFCConnectionError.malformedAPDU } let result: (Data, UInt8, UInt8) = try await tag.sendCommand(apdu: apdu) diff --git a/YubiKit/YubiKit/Response.swift b/YubiKit/YubiKit/Response.swift index f7bf389..ec62dd6 100644 --- a/YubiKit/YubiKit/Response.swift +++ b/YubiKit/YubiKit/Response.swift @@ -14,7 +14,7 @@ import Foundation -internal struct Response: CustomStringConvertible { +public struct Response: CustomStringConvertible { internal init(rawData: Data) { if rawData.count > 2 { @@ -31,16 +31,17 @@ internal struct Response: CustomStringConvertible { } /// The data returned in the response. + /// >Note: The data does not contain the response code. It is stored in the `ResponseStatus`. public let data: Data /// Status code of the response - internal let responseStatus: ResponseStatus + public let responseStatus: ResponseStatus public var description: String { return "" } } -public struct ResponseStatus { +public struct ResponseStatus: Equatable { public enum StatusCode: UInt16 { case ok = 0x9000 case noInputData = 0x6285 diff --git a/YubiKit/YubiKit/SmartCardConnection.swift b/YubiKit/YubiKit/SmartCardConnection.swift index 13c3a52..e45c593 100644 --- a/YubiKit/YubiKit/SmartCardConnection.swift +++ b/YubiKit/YubiKit/SmartCardConnection.swift @@ -78,7 +78,7 @@ public final actor SmartCardConnection: Connection, InternalConnection { } } - internal func send(apdu: APDU) async throws -> Response { + public func sendManually(apdu: APDU) async throws -> Response { guard let smartCard else { throw ConnectionError.noConnection } let data = try await smartCard.transmit(apdu.data) return Response(rawData: data) diff --git a/YubiKit/YubiKit/Utilities/Connection+Extensions.swift b/YubiKit/YubiKit/Utilities/Connection+Extensions.swift index 1e259b5..15568cf 100644 --- a/YubiKit/YubiKit/Utilities/Connection+Extensions.swift +++ b/YubiKit/YubiKit/Utilities/Connection+Extensions.swift @@ -74,9 +74,9 @@ extension Connection { } if readMoreData { let apdu = APDU(cla: 0, ins: ins, p1: 0, p2: 0, command: nil, type: .short) - response = try await internalConnection.send(apdu: apdu) + response = try await self.sendManually(apdu: apdu) } else { - response = try await internalConnection.send(apdu: apdu) + response = try await self.sendManually(apdu: apdu) } guard response.responseStatus.status == .ok || response.responseStatus.sw1 == 0x61 else {