Skip to content

Commit

Permalink
ManagementSession documentation. Fixed a possible wrap-around error p…
Browse files Browse the repository at this point in the history
…arsing UInt from Data.
  • Loading branch information
jensutbult committed Nov 29, 2023
1 parent 5c9c155 commit f799611
Show file tree
Hide file tree
Showing 6 changed files with 91 additions and 19 deletions.
1 change: 1 addition & 0 deletions YubiKit/YubiKit/Management/DeviceConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

import Foundation

/// 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
Expand Down
41 changes: 34 additions & 7 deletions YubiKit/YubiKit/Management/DeviceInfo.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,28 @@
import Foundation
import CryptoTokenKit


/// Identifies the type of data transport a YubiKey is using.
public enum DeviceTransport {
case usb, nfc
}

/// Identifies a feature (typically an application) on a YubiKey which may or may not be supported, and which can be enabled or disabled.
public enum ApplicationType: UInt {
/// Identifies the YubiOTP application.
case otp = 0x01
/// Identifies the U2F (CTAP1) portion of the FIDO application.
case u2f = 0x02
/// Identifies the OpenPGP application, implementing the OpenPGP Card protocol.
case opgp = 0x08
/// Identifies the PIV application, implementing the PIV protocol.
case piv = 0x10
/// Identifies the OATH application, implementing the YKOATH protocol.
case oath = 0x20
/// Identifies the FIDO2 (CTAP2) portion of the FIDO application.
case ctap2 = 0x0200
}

/// The physical form factor of a YubiKey.
public enum FormFactor: UInt8 {
// Used when information about the YubiKey's form factor isn't available.
case unknown = 0x00
Expand All @@ -48,14 +56,26 @@ public enum FormFactor: UInt8 {
case usbCBio = 0x07
}

/// Contains metadata, including Device Configuration, of a YubiKey.
public struct DeviceInfo {
/// Returns the serial number of the YubiKey, if available.
///
/// The serial number can be read if the YubiKey has a serial number, and one of the YubiOTP slots
/// is configured with the SERIAL_API_VISIBLE flag.
public let serialNumber: UInt
/// Returns the version number of the YubiKey firmware.
public let version: Version
/// Returns the form factor of the YubiKey.
public let formFactor: FormFactor
/// Returns the supported (not necessarily enabled) capabilities for a given transport.
public let supportedCapabilities: [DeviceTransport: UInt]
/// Returns whether or not a Configuration Lock is set for the Management application on the YubiKey.
public let isConfigLocked: Bool
/// Returns whether or not this is a FIPS compliant device.
public let isFips: Bool
/// Returns whether or not this is a Security key.
public let isSky: Bool
/// The mutable configuration of the YubiKey.
public let config: DeviceConfig

internal let isUSBSupportedTag: TKTLVTag = 0x01
Expand All @@ -70,12 +90,12 @@ public struct DeviceInfo {
internal let isNFCEnabledTag: TKTLVTag = 0x0e
internal let isConfigLockedTag: TKTLVTag = 0x0a

init(withData data: Data, fallbackVersion: Version) throws {
guard let count = data.bytes.first, count > 0 else { throw "No data" }
guard let tlvs = TKBERTLVRecord.dictionaryOfData(from: data.subdata(in: 1..<data.count)) else { throw "Failed parsing result" }
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] {
guard let parsedVersion = Version(withData: versionData) else { throw ManagementSessionError.versionParseError }
guard let parsedVersion = Version(withData: versionData) else { throw ManagementSessionError.unexpectedData }
self.version = parsedVersion
} else {
self.version = fallbackVersion
Expand Down Expand Up @@ -139,6 +159,12 @@ public struct DeviceInfo {
self.config = DeviceConfig(autoEjectTimeout: autoEjectTimeout, challengeResponseTimeout: challengeResponseTimeout, deviceFlags: deviceFlags, enabledCapabilities: enabledCapabilities)
}

/// Returns whether or not a specific transport is available on this YubiKey.
public func hasTransport(_ transport: DeviceTransport) -> Bool {
return supportedCapabilities.keys.contains(transport)
}

/// Returns whether the application is supported over the specific transport.
public func isApplicationSupported(_ application: ApplicationType, overTransport transport: DeviceTransport) -> Bool {
guard let mask = supportedCapabilities[transport] else { return false }
return (mask & application.rawValue) == application.rawValue
Expand All @@ -147,9 +173,10 @@ public struct DeviceInfo {


extension Data {
internal var integer: UInt {
internal var integer: UInt? {
let bytes = self.bytes
if bytes.isEmpty { return 0 }
guard !bytes.isEmpty else { return 0 }
guard bytes.count <= UInt.bitWidth / UInt8.bitWidth else { return nil }
var value: UInt = 0
bytes.forEach { byte in
value = value << 8
Expand Down
28 changes: 24 additions & 4 deletions YubiKit/YubiKit/Management/ManagementSession.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,22 @@
import Foundation
import CryptoTokenKit

enum ManagementSessionError: Error {
public enum ManagementSessionError: Error {
/// Application is not supported on this YubiKey.
case applicationNotSupported
/// Unexpected configuration state.
case unexpectedYubikeyConfigState
case versionParseError
/// Unexpected data returned by YubiKey.
case unexpectedData
/// YubiKey did not return any data.
case missingData
}

/// An interface to the Management application on the YubiKey.
///
/// Use the Management application to get information and configure a YubiKey.
/// Read more about the Management application on the
/// [Yubico developer website](https://developers.yubico.com/yubikey-manager/Config_Reference.html).
public final actor ManagementSession: Session, InternalSession {

var _connection: Connection?
Expand All @@ -42,7 +51,7 @@ public final actor ManagementSession: Session, InternalSession {

private init(connection: Connection) async throws {
let result = try await connection.selectApplication(application: .management)
guard let version = Version(withManagementResult: result) else { throw ManagementSessionError.versionParseError }
guard let version = Version(withManagementResult: result) else { throw ManagementSessionError.unexpectedData }
self.version = version
self.connection = connection
let internalConnection = await self.internalConnection()
Expand Down Expand Up @@ -72,18 +81,27 @@ public final actor ManagementSession: Session, InternalSession {
return nil
}

/// Returns the DeviceInfo for the connected YubiKey.
public func getDeviceInfo() async throws -> DeviceInfo {
guard let 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)
}

/// Check whether an application is supported over the specified transport.
public func isApplicationSupported(_ application: ApplicationType, overTransport transport: DeviceTransport) async throws -> Bool {
let deviceInfo = try await getDeviceInfo()
return deviceInfo.isApplicationSupported(application, overTransport: transport)
}

/// Check whether an application is enabled over the specified transport.
public func isApplicationEnabled(_ application: ApplicationType, overTransport transport: DeviceTransport) async throws -> Bool {
let deviceInfo = try await getDeviceInfo()
return deviceInfo.config.isApplicationEnabled(application, overTransport: transport)
}

/// Enable or disable an application over the specified transport.
public func setEnabled(_ enabled: Bool, application: ApplicationType, overTransport transport: DeviceTransport, reboot: Bool = false) async throws {
guard let connection else { throw SessionError.noConnection }
let deviceInfo = try await getDeviceInfo()
Expand Down Expand Up @@ -114,10 +132,12 @@ public final actor ManagementSession: Session, InternalSession {
try await connection.send(apdu: apdu)
}

/// Disable an application over the specified transport.
public func disableApplication(_ application: ApplicationType, overTransport transport: DeviceTransport, reboot: Bool = false) async throws {
try await setEnabled(false, application: application, overTransport: transport, reboot: reboot)
}

/// Enable an application over the specified transport.
public func enableApplication(_ application: ApplicationType, overTransport transport: DeviceTransport, reboot: Bool = false) async throws {
try await setEnabled(true, application: application, overTransport: transport, reboot: reboot)
}
Expand All @@ -128,7 +148,7 @@ public final actor ManagementSession: Session, InternalSession {
}

extension Version {
init?(withManagementResult data: Data) {
internal init?(withManagementResult data: Data) {
guard let resultString = String(bytes: data.bytes, encoding: .ascii) else { return nil }
guard let versions = resultString.components(separatedBy: " ").last?.components(separatedBy: "."), versions.count == 3 else {
return nil
Expand Down
2 changes: 1 addition & 1 deletion YubiKit/YubiKit/Version.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public struct Version: Comparable, CustomStringConvertible {
public let minor: UInt8
public let micro: UInt8

init?(withData data: Data) {
internal init?(withData data: Data) {
guard data.count == 3 else { return nil }
let bytes = data.bytes
major = bytes[0]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# ``YubiKit/ManagementSession``

@Metadata {
@DocumentationExtension(mergeBehavior: append)
}

## Topics

### Managing the ManagementSession

- ``session(withConnection:)``
- ``end()``
- ``sessionDidEnd()``

### Running commands in the Management application

- ``getDeviceInfo()``
- ``isApplicationSupported(_:overTransport:)``
- ``isApplicationEnabled(_:overTransport:)``
- ``setEnabled(_:application:overTransport:reboot:)``
- ``enableApplication(_:overTransport:reboot:)``
- ``disableApplication(_:overTransport:reboot:)``

### Return types

- ``DeviceInfo``
- ``DeviceConfig``

### Errors

- ``ManagementSessionError``
7 changes: 0 additions & 7 deletions YubiKit/YubiKit/YubiKit.docc/Resources/YubiKit.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,3 @@ communication with the YubiKey.
- ``Session``
- ``OATHSession``
- ``ManagementSession``
- ``DeviceInfo``
- ``DeviceConfig``

### Using the wrappers

- ``YubiKitWrapper``
- ``YubiKitWrapperDelegate``

0 comments on commit f799611

Please sign in to comment.