Skip to content

Commit

Permalink
ManagementSession refactored to align better with the Android SDK.
Browse files Browse the repository at this point in the history
  • Loading branch information
jensutbult committed Mar 13, 2024
1 parent bea194a commit 573e913
Show file tree
Hide file tree
Showing 6 changed files with 198 additions and 53 deletions.
64 changes: 63 additions & 1 deletion FullStackTests/Tests/ManagementFullStackTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
4 changes: 4 additions & 0 deletions YubiKit/YubiKit.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -80,6 +81,7 @@
B456E21A274FCA26004471DE /* ManagementSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManagementSession.swift; sourceTree = "<group>"; };
B47FDD982939FAD100AFF70A /* ConnectionHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectionHelper.swift; sourceTree = "<group>"; };
B47FDD9A293A15AE00AFF70A /* NSLock+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSLock+Extensions.swift"; sourceTree = "<group>"; };
B49F90C32B9F30A400C10F0B /* ManagementFeature.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManagementFeature.swift; sourceTree = "<group>"; };
B4AEC5AA2B0CF38B004F3BE7 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; name = README.md; path = ../README.md; sourceTree = "<group>"; };
B4B124722B98B33C0099BEDB /* OATHSessionFeature.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OATHSessionFeature.swift; sourceTree = "<group>"; };
B4BE3AA7292BCA1300CC30CB /* OATHSession+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OATHSession+Extensions.swift"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -205,6 +207,7 @@
B456E21A274FCA26004471DE /* ManagementSession.swift */,
B40528322987C31E00FC33AB /* DeviceInfo.swift */,
B405283629894E7600FC33AB /* DeviceConfig.swift */,
B49F90C32B9F30A400C10F0B /* ManagementFeature.swift */,
);
path = Management;
sourceTree = "<group>";
Expand Down Expand Up @@ -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 */,
Expand Down
71 changes: 62 additions & 9 deletions YubiKit/YubiKit/Management/DeviceConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
}
}
46 changes: 23 additions & 23 deletions YubiKit/YubiKit/Management/DeviceInfo.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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..<data.count)) else { throw ManagementSessionError.unexpectedData }

if let versionData = tlvs[firmwareVersionTag] {
if let versionData = tlvs[tagFirmwareVersion] {
guard let parsedVersion = Version(withData: versionData) else { throw ManagementSessionError.unexpectedData }
self.version = parsedVersion
} else {
self.version = fallbackVersion
}

self.isConfigLocked = tlvs[isConfigLockedTag]?.integer == 1
self.isConfigLocked = tlvs[tagIsConfigLocked]?.integer == 1

self.serialNumber = tlvs[serialNumberTag]?.integer ?? 0
self.serialNumber = tlvs[tagSerialNumber]?.integer ?? 0

if let rawFormFactor = tlvs[formFactorTag]?.uint8 {
if let rawFormFactor = tlvs[tagFormFactor]?.uint8 {
self.isFips = (rawFormFactor & 0x80) != 0
self.isSky = (rawFormFactor & 0x40) != 0
if let formFactor = FormFactor(rawValue: rawFormFactor) {
Expand All @@ -124,37 +124,37 @@ public struct DeviceInfo {
// 4.2.4 doesn't report supported capabilities correctly, but they are always 0x3f.
supportedCapabilities[DeviceTransport.usb] = 0x3f
} else {
supportedCapabilities[DeviceTransport.usb] = tlvs[isUSBSupportedTag]?.integer ?? 0
supportedCapabilities[DeviceTransport.usb] = tlvs[tagIsUSBSupported]?.integer ?? 0
}

var enabledCapabilities = [DeviceTransport: UInt]()
if tlvs[isUSBEnabledTag] != nil && version.major != 4 {
if tlvs[tagIsUSBEnabled] != nil && version.major != 4 {
// YK4 reports this incorrectly, instead use supportedCapabilities and USB mode.
enabledCapabilities[DeviceTransport.usb] = tlvs[isUSBEnabledTag]?.integer ?? 0
enabledCapabilities[DeviceTransport.usb] = tlvs[tagIsUSBEnabled]?.integer ?? 0
}

if let nfcSupported = tlvs[isNFCSupportedTag]?.integer {
if let nfcSupported = tlvs[tagIsNFCSupported]?.integer {
supportedCapabilities[DeviceTransport.nfc] = nfcSupported
enabledCapabilities[DeviceTransport.nfc] = tlvs[isNFCEnabledTag]?.integer ?? 0
enabledCapabilities[DeviceTransport.nfc] = tlvs[tagIsNFCEnabled]?.integer ?? 0
}
self.supportedCapabilities = supportedCapabilities

// DeviceConfig
let autoEjectTimeout: TimeInterval
if let timeout = tlvs[autoEjectTimeoutTag]?.integer {
if let timeout = tlvs[tagAutoEjectTimeout]?.integer {
autoEjectTimeout = TimeInterval(timeout)
} else {
autoEjectTimeout = 0
}

let challengeResponseTimeout: TimeInterval
if let timeout = tlvs[challengeResponseTimeoutTag]?.integer {
if let timeout = tlvs[tagChallengeResponseTimeout]?.integer {
challengeResponseTimeout = TimeInterval(timeout)
} else {
challengeResponseTimeout = 0
}

let deviceFlags: UInt = tlvs[deviceFlagsTag]?.integer ?? 0
let deviceFlags = UInt8(tlvs[tagDeviceFlags]?.integer ?? 0)

self.config = DeviceConfig(autoEjectTimeout: autoEjectTimeout, challengeResponseTimeout: challengeResponseTimeout, deviceFlags: deviceFlags, enabledCapabilities: enabledCapabilities)
}
Expand Down
29 changes: 29 additions & 0 deletions YubiKit/YubiKit/Management/ManagementFeature.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Copyright Yubico AB
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import Foundation

public enum ManagementFeature: SessionFeature {

case deviceInfo, deviceConfig

public func isSupported(by version: Version) -> Bool {
switch self {
case .deviceInfo:
return version >= Version(withString: "4.1.0")!
case .deviceConfig:
return version >= Version(withString: "5.0.0")!
}
}
}
Loading

0 comments on commit 573e913

Please sign in to comment.