diff --git a/FullStackTests/Tests/ManagementFullStackTests.swift b/FullStackTests/Tests/ManagementFullStackTests.swift index 66e84f4..249d7f5 100644 --- a/FullStackTests/Tests/ManagementFullStackTests.swift +++ b/FullStackTests/Tests/ManagementFullStackTests.swift @@ -43,7 +43,69 @@ class ManagementFullStackTests: XCTestCase { } } - func testDisableAndEnableOATH() throws { + func testTimeouts() throws { + runManagementTest { _, session, _ in + let deviceInfo = try await session.getDeviceInfo() + let config = deviceInfo.config.deviceConfig(autoEjectTimeout: 320.0, challengeResponseTimeout: 135.0) + try await session.updateDeviceConfig(config, reboot: false) + let info = try await session.getDeviceInfo() + XCTAssertEqual(info.config.challengeResponseTimeout, 135.0) + XCTAssertEqual(info.config.autoEjectTimeout, 320.0) + #if os(iOS) + await connection.nfcConnection?.close(message: "Test successful!") + #endif + } + } + + func testDisableAndEnableConfigOATHandPIVoverUSB() throws { + runManagementTest { connection, session, transport in + let deviceInfo = try await session.getDeviceInfo() + guard let disableConfig = deviceInfo.config.deviceConfig(enabling: false, application: .oath, overTransport: .usb)?.deviceConfig(enabling: false, application: .piv, overTransport: .usb) else { XCTFail(); return } + try await session.updateDeviceConfig(disableConfig, reboot: false) + let disabledInfo = try await session.getDeviceInfo() + XCTAssertFalse(disabledInfo.config.isApplicationEnabled(.oath, overTransport: .usb)) + XCTAssertFalse(disabledInfo.config.isApplicationEnabled(.piv, overTransport: .usb)) + let oathSession = try? await OATHSession.session(withConnection: connection) + if transport == .usb { + XCTAssert(oathSession == nil) + } + let managementSession = try await ManagementSession.session(withConnection: connection) + guard let enableConfig = deviceInfo.config.deviceConfig(enabling: true, application: .oath, overTransport: .usb)?.deviceConfig(enabling: true, application: .piv, overTransport: .usb) else { XCTFail(); return } + try await managementSession.updateDeviceConfig(enableConfig, reboot: false) + #if os(iOS) + await connection.nfcConnection?.close(message: "Test successful!") + #endif + let enabledInfo = try await managementSession.getDeviceInfo() + XCTAssert(enabledInfo.config.isApplicationEnabled(.oath, overTransport: .usb)) + XCTAssert(enabledInfo.config.isApplicationEnabled(.piv, overTransport: .usb)) + } + } + + func testDisableAndEnableConfigOATHandPIVoverNFC() throws { + runManagementTest { connection, session, transport in + let deviceInfo = try await session.getDeviceInfo() + guard let disableConfig = deviceInfo.config.deviceConfig(enabling: false, application: .oath, overTransport: .nfc)?.deviceConfig(enabling: false, application: .piv, overTransport: .nfc) else { XCTFail(); return } + try await session.updateDeviceConfig(disableConfig, reboot: false) + let disabledInfo = try await session.getDeviceInfo() + XCTAssertFalse(disabledInfo.config.isApplicationEnabled(.oath, overTransport: .nfc)) + XCTAssertFalse(disabledInfo.config.isApplicationEnabled(.piv, overTransport: .nfc)) + let oathSession = try? await OATHSession.session(withConnection: connection) + if transport == .nfc { + XCTAssert(oathSession == nil) + } + let managementSession = try await ManagementSession.session(withConnection: connection) + guard let enableConfig = deviceInfo.config.deviceConfig(enabling: true, application: .oath, overTransport: .nfc)?.deviceConfig(enabling: true, application: .piv, overTransport: .nfc) else { XCTFail(); return } + try await managementSession.updateDeviceConfig(enableConfig, reboot: false) + #if os(iOS) + await connection.nfcConnection?.close(message: "Test successful!") + #endif + let enabledInfo = try await managementSession.getDeviceInfo() + XCTAssert(enabledInfo.config.isApplicationEnabled(.oath, overTransport: .nfc)) + XCTAssert(enabledInfo.config.isApplicationEnabled(.piv, overTransport: .nfc)) + } + } + + func testDisableAndEnableWithHelperOATH() throws { runManagementTest { connection, session, transport in try await session.setEnabled(false, application: .oath, overTransport: transport) var info = try await session.getDeviceInfo() diff --git a/YubiKit/YubiKit.xcodeproj/project.pbxproj b/YubiKit/YubiKit.xcodeproj/project.pbxproj index cad0291..9197f60 100644 --- a/YubiKit/YubiKit.xcodeproj/project.pbxproj +++ b/YubiKit/YubiKit.xcodeproj/project.pbxproj @@ -27,6 +27,7 @@ B456E21B274FCA26004471DE /* ManagementSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = B456E21A274FCA26004471DE /* ManagementSession.swift */; }; B47FDD992939FAD100AFF70A /* ConnectionHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = B47FDD982939FAD100AFF70A /* ConnectionHelper.swift */; }; B47FDD9B293A15AE00AFF70A /* NSLock+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B47FDD9A293A15AE00AFF70A /* NSLock+Extensions.swift */; }; + B49F90C42B9F30A400C10F0B /* ManagementFeature.swift in Sources */ = {isa = PBXBuildFile; fileRef = B49F90C32B9F30A400C10F0B /* ManagementFeature.swift */; }; B4AEC5AB2B0CF38B004F3BE7 /* README.md in Resources */ = {isa = PBXBuildFile; fileRef = B4AEC5AA2B0CF38B004F3BE7 /* README.md */; }; B4B124732B98B33C0099BEDB /* OATHSessionFeature.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4B124722B98B33C0099BEDB /* OATHSessionFeature.swift */; }; B4BE3AA8292BCA1300CC30CB /* OATHSession+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4BE3AA7292BCA1300CC30CB /* OATHSession+Extensions.swift */; }; @@ -80,6 +81,7 @@ B456E21A274FCA26004471DE /* ManagementSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManagementSession.swift; sourceTree = ""; }; B47FDD982939FAD100AFF70A /* ConnectionHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectionHelper.swift; sourceTree = ""; }; B47FDD9A293A15AE00AFF70A /* NSLock+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSLock+Extensions.swift"; sourceTree = ""; }; + B49F90C32B9F30A400C10F0B /* ManagementFeature.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManagementFeature.swift; sourceTree = ""; }; B4AEC5AA2B0CF38B004F3BE7 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; name = README.md; path = ../README.md; sourceTree = ""; }; B4B124722B98B33C0099BEDB /* OATHSessionFeature.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OATHSessionFeature.swift; sourceTree = ""; }; B4BE3AA7292BCA1300CC30CB /* OATHSession+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OATHSession+Extensions.swift"; sourceTree = ""; }; @@ -205,6 +207,7 @@ B456E21A274FCA26004471DE /* ManagementSession.swift */, B40528322987C31E00FC33AB /* DeviceInfo.swift */, B405283629894E7600FC33AB /* DeviceConfig.swift */, + B49F90C32B9F30A400C10F0B /* ManagementFeature.swift */, ); path = Management; sourceTree = ""; @@ -373,6 +376,7 @@ B4FF44A32B862BCE0070750D /* PIVDataTypes.swift in Sources */, B408BA8F2948FA2100001B2F /* Stream+Extensions.swift in Sources */, B4BE3AB3292E1E6D00CC30CB /* TKTLVRecord+Extensions.swift in Sources */, + B49F90C42B9F30A400C10F0B /* ManagementFeature.swift in Sources */, B456E215274D2453004471DE /* LightningConnection.swift in Sources */, B4B124732B98B33C0099BEDB /* OATHSessionFeature.swift in Sources */, B4F937662B51EBAF0007D394 /* PIVPadding.swift in Sources */, diff --git a/YubiKit/YubiKit/Management/DeviceConfig.swift b/YubiKit/YubiKit/Management/DeviceConfig.swift index c47d12d..51b3b0d 100644 --- a/YubiKit/YubiKit/Management/DeviceConfig.swift +++ b/YubiKit/YubiKit/Management/DeviceConfig.swift @@ -13,25 +13,42 @@ // limitations under the License. import Foundation +import CryptoTokenKit /// Describes the configuration of a YubiKey which can be altered via the Management application. public struct DeviceConfig { - public let autoEjectTimeout: TimeInterval - public let challengeResponseTimeout: TimeInterval - public let deviceFlags: UInt + + public let autoEjectTimeout: TimeInterval? + public let challengeResponseTimeout: TimeInterval? + public let deviceFlags: UInt8? public let enabledCapabilities: [DeviceTransport: UInt] + internal let tagUSBEnabled: TKTLVTag = 0x03 + // private static final int TAG_USB_ENABLED = 0x03; + internal let tagAutoEjectTimeout: TKTLVTag = 0x06 + // private static final int TAG_AUTO_EJECT_TIMEOUT = 0x06; + internal let tagChallengeResponseTimeout: TKTLVTag = 0x07 + // private static final int TAG_CHALLENGE_RESPONSE_TIMEOUT = 0x07; + internal let tagDeviceFlags: TKTLVTag = 0x08 + // private static final int TAG_DEVICE_FLAGS = 0x08; + internal let tagNFCEnabled: TKTLVTag = 0x0e + // private static final int TAG_NFC_ENABLED = 0x0e; + internal let tagConfigurationLock: TKTLVTag = 0x0a + // private static final int TAG_CONFIGURATION_LOCK = 0x0a; + internal let tagUnlock: TKTLVTag = 0x0b + // private static final int TAG_UNLOCK = 0x0b; + internal let tagReboot: TKTLVTag = 0x0c + // private static final int TAG_REBOOT = 0x0c; + + public func isApplicationEnabled(_ application: ApplicationType, overTransport transport: DeviceTransport) -> Bool { guard let mask = enabledCapabilities[transport] else { return false } return (mask & application.rawValue) == application.rawValue } - public func deviceConfigWithEnabled(_ enabled: Bool, application: ApplicationType, overTransport transport: DeviceTransport) -> DeviceConfig? { - - guard let oldMask = enabledCapabilities[transport] else { - return nil - } - let newMask = enabled ? oldMask | application.rawValue : oldMask & ~application.rawValue + public func deviceConfig(enabling: Bool, application: ApplicationType, overTransport transport: DeviceTransport) -> DeviceConfig? { + guard let oldMask = enabledCapabilities[transport] else { return nil } + let newMask = enabling ? oldMask | application.rawValue : oldMask & ~application.rawValue var newEnabledCapabilities = enabledCapabilities newEnabledCapabilities[transport] = newMask @@ -40,4 +57,40 @@ public struct DeviceConfig { deviceFlags: deviceFlags, enabledCapabilities: newEnabledCapabilities) } + + public func deviceConfig(autoEjectTimeout: TimeInterval, challengeResponseTimeout: TimeInterval) -> DeviceConfig { + return Self.init(autoEjectTimeout: autoEjectTimeout, challengeResponseTimeout: challengeResponseTimeout, deviceFlags: self.deviceFlags, enabledCapabilities: self.enabledCapabilities) + } + + internal func data(reboot: Bool, lockCode: Data?, newLockCode: Data?) throws -> Data { + var data = Data() + if reboot { + data.append(TKBERTLVRecord(tag: tagReboot, value: Data()).data) + } + if let lockCode { + data.append(TKBERTLVRecord(tag: tagUnlock, value: lockCode).data) + } + if let usbEnabled = enabledCapabilities[.usb] { + data.append(TKBERTLVRecord(tag: tagUSBEnabled, value: UInt16(usbEnabled).bigEndian.data).data) + } + if let nfcEnabled = enabledCapabilities[.nfc] { + data.append(TKBERTLVRecord(tag: tagNFCEnabled, value: UInt16(nfcEnabled).bigEndian.data).data) + } + if let autoEjectTimeout { + data.append(TKBERTLVRecord(tag: tagAutoEjectTimeout, value: UInt16(autoEjectTimeout).bigEndian.data).data) + } + if let challengeResponseTimeout { + let timeout = UInt8(challengeResponseTimeout) + data.append(TKBERTLVRecord(tag: tagChallengeResponseTimeout, value: timeout.data).data) + } + if let deviceFlags { + data.append(TKBERTLVRecord(tag: tagDeviceFlags, value: deviceFlags.data).data) + } + if let newLockCode { + data.append(TKBERTLVRecord(tag: tagConfigurationLock, value: newLockCode).data) + } + guard data.count <= 0xff else { throw ManagementSessionError.configTooLarge } + + return UInt8(data.count).data + data + } } diff --git a/YubiKit/YubiKit/Management/DeviceInfo.swift b/YubiKit/YubiKit/Management/DeviceInfo.swift index 6f93373..64b241b 100644 --- a/YubiKit/YubiKit/Management/DeviceInfo.swift +++ b/YubiKit/YubiKit/Management/DeviceInfo.swift @@ -78,34 +78,34 @@ public struct DeviceInfo { /// The mutable configuration of the YubiKey. public let config: DeviceConfig - internal let isUSBSupportedTag: TKTLVTag = 0x01 - internal let serialNumberTag: TKTLVTag = 0x02 - internal let isUSBEnabledTag: TKTLVTag = 0x03 - internal let formFactorTag: TKTLVTag = 0x04 - internal let firmwareVersionTag: TKTLVTag = 0x05 - internal let autoEjectTimeoutTag: TKTLVTag = 0x06 - internal let challengeResponseTimeoutTag: TKTLVTag = 0x07 - internal let deviceFlagsTag: TKTLVTag = 0x08 - internal let isNFCSupportedTag: TKTLVTag = 0x0d - internal let isNFCEnabledTag: TKTLVTag = 0x0e - internal let isConfigLockedTag: TKTLVTag = 0x0a + internal let tagIsUSBSupported: TKTLVTag = 0x01 + internal let tagSerialNumber: TKTLVTag = 0x02 + internal let tagIsUSBEnabled: TKTLVTag = 0x03 + internal let tagFormFactor: TKTLVTag = 0x04 + internal let tagFirmwareVersion: TKTLVTag = 0x05 + internal let tagAutoEjectTimeout: TKTLVTag = 0x06 + internal let tagChallengeResponseTimeout: TKTLVTag = 0x07 + internal let tagDeviceFlags: TKTLVTag = 0x08 + internal let tagIsNFCSupported: TKTLVTag = 0x0d + internal let tagIsNFCEnabled: TKTLVTag = 0x0e + internal let tagIsConfigLocked: TKTLVTag = 0x0a internal init(withData data: Data, fallbackVersion: Version) throws { guard let count = data.bytes.first, count > 0 else { throw ManagementSessionError.missingData } guard let tlvs = TKBERTLVRecord.dictionaryOfData(from: data.subdata(in: 1.. Bool { + switch self { + case .deviceInfo: + return version >= Version(withString: "4.1.0")! + case .deviceConfig: + return version >= Version(withString: "5.0.0")! + } + } +} diff --git a/YubiKit/YubiKit/Management/ManagementSession.swift b/YubiKit/YubiKit/Management/ManagementSession.swift index 0510b46..9212ecd 100644 --- a/YubiKit/YubiKit/Management/ManagementSession.swift +++ b/YubiKit/YubiKit/Management/ManagementSession.swift @@ -25,6 +25,8 @@ public enum ManagementSessionError: Error { case unexpectedData /// YubiKey did not return any data. case missingData + /// Device configuration too large + case configTooLarge } /// An interface to the Management application on the YubiKey. @@ -70,18 +72,28 @@ public final actor ManagementSession: Session, InternalSession { await internalConnection?.setSession(nil) await setConnection(nil) } + + nonisolated public func supports(_ feature: SessionFeature) -> Bool { + return feature.isSupported(by: version) + } /// Returns the DeviceInfo for the connected YubiKey. public func getDeviceInfo() async throws -> DeviceInfo { Logger.management.debug("\(String(describing: self).lastComponent), \(#function)") + guard self.supports(ManagementFeature.deviceInfo) else { throw SessionError.notSupported } guard let connection = _connection else { throw SessionError.noConnection } let apdu = APDU(cla: 0, ins: 0x1d, p1: 0, p2: 0) let data = try await connection.send(apdu: apdu) return try DeviceInfo(withData: data, fallbackVersion: version) } - nonisolated public func supports(_ feature: SessionFeature) -> Bool { - return true + public func updateDeviceConfig(_ config: DeviceConfig, reboot: Bool, lockCode: Data? = nil, newLockCode: Data? = nil) async throws { + Logger.management.debug("\(String(describing: self).lastComponent), \(#function)") + guard self.supports(ManagementFeature.deviceConfig) else { throw SessionError.notSupported } + guard let connection = _connection else { throw SessionError.noConnection } + let data = try config.data(reboot: reboot, lockCode: lockCode, newLockCode: newLockCode) + let apdu = APDU(cla: 0, ins: 0x1c, p1: 0, p2: 0, command: data) + try await connection.send(apdu: apdu) } /// Check whether an application is supported over the specified transport. @@ -101,33 +113,18 @@ public final actor ManagementSession: Session, InternalSession { /// Enable or disable an application over the specified transport. public func setEnabled(_ enabled: Bool, application: ApplicationType, overTransport transport: DeviceTransport, reboot: Bool = false) async throws { Logger.management.debug("\(String(describing: self).lastComponent), \(#function): \(enabled), application: \(String(describing: application)), overTransport: \(String(describing: transport)), reboot: \(reboot)") - guard let connection = _connection else { throw SessionError.noConnection } let deviceInfo = try await getDeviceInfo() guard enabled != deviceInfo.config.isApplicationEnabled(application, overTransport: transport) else { return } guard deviceInfo.isApplicationSupported(application, overTransport: transport) else { throw ManagementSessionError.applicationNotSupported } - let config = deviceInfo.config.deviceConfigWithEnabled(enabled, application: application, overTransport: transport) + guard let config = deviceInfo.config.deviceConfig(enabling: enabled, application: application, overTransport: transport) else { return } - guard let newConfigValue = config?.enabledCapabilities[transport] else { + guard config.enabledCapabilities[transport] != nil else { throw ManagementSessionError.unexpectedYubikeyConfigState } - var data = Data() - data.append(UInt8(newConfigValue & 0xff00 >> 8)) - data.append(UInt8(newConfigValue & 0xff)) - - if reboot { - data.append([0x0c, 0x00], count: 2) - } - let tlv = TKBERTLVRecord(tag: transport == .nfc ? deviceInfo.isNFCEnabledTag : deviceInfo.isUSBEnabledTag , value: data) - - var command = Data() - command.append(tlv.data.count.data.uint8) - command.append(tlv.data) - - let apdu = APDU(cla: 0, ins: 0x1c, p1: 0, p2: 0, command: command) - try await connection.send(apdu: apdu) + try await updateDeviceConfig(config, reboot: reboot) } /// Disable an application over the specified transport.