Skip to content

Commit

Permalink
More OATH documentation.
Browse files Browse the repository at this point in the history
  • Loading branch information
jensutbult committed Nov 23, 2023
1 parent 3b75494 commit e8cb486
Show file tree
Hide file tree
Showing 3 changed files with 145 additions and 19 deletions.
67 changes: 56 additions & 11 deletions YubiKit/YubiKit/OATH/OATHSession+Extensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -62,26 +62,47 @@ extension OATHSession {
case SHA512 = 0x03
}

/// A reference to an OATH Credential stored on a YubiKey.
public struct Credential: Identifiable, CustomStringConvertible {

/// Device ID of the YubiKey.
public let deviceId: String

/// The ID of a Credential which is used to identify it to the YubiKey.
public let id: Data

/// OATH type of the credential (TOTP or HOTP).
public let type: OATHSession.CredentialType

/// Hash algorithm used by the credential (SHA1, SHA265 or SHA 512).
public let hashAlgorithm: OATHSession.HashAlgorithm?

/// The name of the account (typically a username or email address).
public let name: String

/// The name of the Credential issuer (e.g. Google, Amazon, Facebook, etc.)
public let issuer: String?

/// Label of the Credential. Will return `issuer:name` if issuer is set, otherwise `name`.
public var label: String {
if let issuer {
return "\(issuer):\(name)"
} else {
return name
}
}

/// Validity time period in seconds for a Code generated from this Credential.
public let validityTime: TimeInterval = 30.0 //FIXME: implement this

/// Whether or not the Credential requires touch.
public var requiresTouch = false // FIXME: implement this

public var description: String {
return "Credential(type: \(type), label:\(label), algorithm: \(hashAlgorithm.debugDescription)"
}

init(deviceId: String, id: Data, type: OATHSession.CredentialType, hashAlgorithm: OATHSession.HashAlgorithm? = nil, name: String, issuer: String?) {
internal init(deviceId: String, id: Data, type: OATHSession.CredentialType, hashAlgorithm: OATHSession.HashAlgorithm? = nil, name: String, issuer: String?) {
self.deviceId = deviceId
self.id = id
self.type = type
Expand All @@ -91,7 +112,7 @@ extension OATHSession {
}
}

struct CredentialIdParser {
internal struct CredentialIdParser {

let account: String
let issuer: String?
Expand Down Expand Up @@ -127,6 +148,7 @@ extension OATHSession {
}
}

/// A one-time OATH code, calculated from a ``Credential`` stored in a YubiKey.
public struct Code: Identifiable, CustomStringConvertible {

public var description: String {
Expand All @@ -136,7 +158,11 @@ extension OATHSession {
}

public let id = UUID()

/// String representation of the code, typically a 6-8 digit code.
public let code: String

/// The date this code will be valid from.
public var validFrom: Date {
switch credentialType {
case .HOTP(_):
Expand All @@ -145,6 +171,8 @@ extension OATHSession {
return Date(timeIntervalSince1970: timestamp.timeIntervalSince1970 - timestamp.timeIntervalSince1970.truncatingRemainder(dividingBy: period))
}
}

/// The date this code ends being valid.
public var validTo: Date {
switch credentialType {
case .HOTP(_):
Expand All @@ -154,7 +182,7 @@ extension OATHSession {
}
}

init(code: String, timestamp: Date, credentialType: CredentialType) {
internal init(code: String, timestamp: Date, credentialType: CredentialType) {
self.code = code
self.timestamp = timestamp
self.credentialType = credentialType
Expand All @@ -165,11 +193,15 @@ extension OATHSession {

}

/// Template object holding all required information to add a new ``Credential`` to a YubiKey.
public struct CredentialTemplate {

private static let minSecretLenght = 14

public var key: String {
/// Credential identifier, as used to identify it on a YubiKey.
///
/// The Credential ID is calculated based on the combination of the issuer, the name, and (for TOTP credentials) the validity period.
public var identifier: String {
let key: String
if let issuer {
key = "\(issuer):\(name)"
Expand All @@ -187,6 +219,10 @@ extension OATHSession {
}
}

/// Creates a CredentialTemplate by parsing a [otpauth:// URI](https://github.com/google/google-authenticator/wiki/Key-Uri-Format).
/// - Parameters:
/// - url: The otpauth:// URI to parse.
/// - skipValidation: Set to true to skip input validation when parsing the uri.
public init(withURL url: URL, skipValidation: Bool = false) throws {
guard url.scheme == "otpauth" else { throw CredentialTemplateError.missingScheme }

Expand Down Expand Up @@ -222,6 +258,15 @@ extension OATHSession {
self.init(type: type, algorithm: algorithm, secret: secret, issuer: issuer, name: name, digits: digits)
}

/// Creates a CredentialTemplate.
/// - Parameters:
/// - type: OATH type of the credential (TOTP or HOTP).
/// - algorithm: Hash algorithm used by the credential (SHA1, SHA265 or SHA 512).
/// - secret: Secret key of the credential, in raw bytes (__not__ Base32 encoded)
/// - issuer: Name of the credential issuer (e.g. Google, Amazon, Facebook, etc.).
/// - name: The name/label of the account, typically a username or email address
/// - digits: Number of digits to display for generated ``Code``s
/// - requiresTouch: Set to true if the credential should require touch to be used.
public init(type: CredentialType, algorithm: HashAlgorithm, secret: Data, issuer: String?, name: String, digits: UInt8 = 6, requiresTouch: Bool = false) {
self.type = type
self.algorithm = algorithm
Expand All @@ -246,13 +291,13 @@ extension OATHSession {
self.requiresTouch = requiresTouch
}

public let type: CredentialType
public let algorithm: HashAlgorithm
public let secret: Data
public let issuer: String?
public let name: String
public let digits: UInt8
public let requiresTouch: Bool
internal let type: CredentialType
internal let algorithm: HashAlgorithm
internal let secret: Data
internal let issuer: String?
internal let name: String
internal let digits: UInt8
internal let requiresTouch: Bool
}

}
Expand Down
59 changes: 51 additions & 8 deletions YubiKit/YubiKit/OATH/OATHSession.swift
Original file line number Diff line number Diff line change
Expand Up @@ -122,16 +122,21 @@ public final class OATHSession: Session, InternalSession {
selectResponse = try await Self.selectApplication(withConnection: connection)
}

@discardableResult public func addCredential(template: CredentialTemplate) async throws -> Credential? {
/// Adds a new Credential to the YubiKey.
///
/// The Credential ID (see ``OATHSession/CredentialTemplate/identifier``) must be unique to the YubiKey, or the
/// existing Credential with the same ID will be overwritten.
///
/// Setting requireTouch requires support for FEATURE_TOUCH, available on YubiKey 4.2 or later.
/// Using SHA-512 requires support for FEATURE_SHA512, available on YubiKey 4.3.1 or later.
/// - Parameter template: The template describing the credential.
/// - Returns: The newly added credential.
@discardableResult public func addCredential(template: CredentialTemplate) async throws -> Credential {
guard let connection = _connection else { throw SessionError.noConnection }
// name
print(template.key)
guard let nameData = template.key.data(using: .utf8) else { throw OATHSessionError.unexpectedData }
guard let nameData = template.identifier.data(using: .utf8) else { throw OATHSessionError.unexpectedData }
let nameTlv = TKBERTLVRecord(tag: 0x71, value: nameData)
// key
var keyData = Data()

// keyData.append(UInt8(template.type.code | template.algorithm.rawValue).data)
keyData.append(template.type.code | template.algorithm.rawValue)
keyData.append(template.digits.data)
keyData.append(template.secret)
Expand All @@ -149,16 +154,20 @@ public final class OATHSession: Session, InternalSession {

let apdu = APDU(cla: 0x00, ins: 0x01, p1: 0x00, p2: 0x00, command: data)
let _ = try await connection.send(apdu: apdu)
return nil
return Credential(deviceId: selectResponse.deviceId, id: nameData, type: template.type, name: template.name, issuer: template.issuer)
}

/// Deletes an existing Credential from the YubiKey.
/// - Parameter credential: The credential that will be deleted from the YubiKey.
public func deleteCredential(_ credential: Credential) async throws {
guard let connection = _connection else { throw SessionError.noConnection }
let deleteTlv = TKBERTLVRecord(tag: 0x71, value: credential.id)
let apdu = APDU(cla: 0, ins: 0x02, p1: 0, p2: 0, command: deleteTlv.data)
let _ = try await connection.send(apdu: apdu)
}

/// List credentials on YubiKey.
/// - Returns: An array of Credentials.
public func listCredentials() async throws -> [Credential] {
guard let connection = _connection else { throw SessionError.noConnection }
let apdu = APDU(cla: 0, ins: 0xa1, p1: 0, p2: 0)
Expand All @@ -184,6 +193,11 @@ public final class OATHSession: Session, InternalSession {
}
}

/// Returns a new Code for a stored Credential.
/// - Parameters:
/// - credential: The stored Credential to calculate a new code for.
/// - timestamp: The timestamp which is used as start point for TOTP, this is ignored for HOTP.
/// - Returns: Calculated code.
public func calculateCode(credential: Credential, timestamp: Date = Date()) async throws -> Code {
guard let connection = _connection else { throw SessionError.noConnection }

Expand Down Expand Up @@ -212,6 +226,12 @@ public final class OATHSession: Session, InternalSession {
return Code(code: stringCode, timestamp: timestamp, credentialType: credential.type)
}

/// List all credentials on the YubiKey and calculate each credentials code.
///
/// Credentials which use HOTP, or which require touch, will not be calculated.
/// They will still be present in the result, but with a nil value.
/// - Parameter timestamp: The timestamp which is used as start point for TOTP, this is ignored for HOTP.
/// - Returns: An array of tuples containing a ``Credential`` and an optional ``Code``.
public func calculateCodes(timestamp: Date = Date()) async throws -> [(Credential, Code?)] {
print("Start OATH calculateCodes")
let time = timestamp.timeIntervalSince1970
Expand Down Expand Up @@ -253,16 +273,29 @@ public final class OATHSession: Session, InternalSession {
}
}

/// Sets an Access Key derived from a password. Once a key is set, any usage of the credentials stored will
/// require the application to be unlocked via one of the unlock functions. Also see ``setAccessKey(_:)``.
/// - Parameter password: The user-supplied password to set.
public func setPassword(_ password: String) async throws {
let derivedKey = try deriveAccessKey(from: password)
try await self.setAccessKey(derivedKey)
}

/// Unlock with password.
/// - Parameter password: The user-supplied password used to unlock the application.
public func unlockWithPassword(_ password: String) async throws {
let derivedKey = try deriveAccessKey(from: password)
try await self.unlockWithAccessKey(derivedKey)
}


/// Sets an access key.
///
/// Once an access key is set, any usage of the credentials stored will require the application
/// to be unlocked via one of the unlock methods, which requires knowledge of the access key.
/// Typically this key is derived from a password (see ``deriveAccessKey(from:)``). This method
/// sets the raw 16 byte key.
/// - Parameter accessKey: The shared secret key used to unlock access to the application.
public func setAccessKey(_ accessKey: Data) async throws {
guard let connection = _connection else { throw SessionError.noConnection }
let header = CredentialType.TOTP().code | HashAlgorithm.SHA1.rawValue
Expand All @@ -281,6 +314,11 @@ public final class OATHSession: Session, InternalSession {
let _ = try await connection.send(apdu: apdu)
}

/// Unlock OATH application on the YubiKey. Once unlocked other commands may be sent to the key.
///
/// Once unlocked, the application will remain unlocked for the duration of the session.
/// See the [YKOATH protocol specification](https://developers.yubico.com/OATH/) for further details.
/// - Parameter accessKey: The shared access key.
public func unlockWithAccessKey(_ accessKey: Data) async throws {
guard let connection = _connection, let responseChallenge = self.selectResponse.challenge else { throw SessionError.noConnection }
let reponseTlv = TKBERTLVRecord(tag: tagSetCodeResponse, value: responseChallenge.hmacSha1(usingKey: accessKey))
Expand Down Expand Up @@ -314,6 +352,11 @@ struct DeriveAccessKeyError: Error {
}

extension OATHSession {

/// Derives an access key from a password and the device-specific salt. The key is derived by running
/// 1000 rounds of PBKDF2 using the password and salt as inputs, with a 16 byte output.
/// - Parameter password: A user-supplied password.
/// - Returns: Access key for unlocking the session.
public func deriveAccessKey(from password: String) throws -> Data {
var derivedKey = Data(count: 16)
try derivedKey.withUnsafeMutableBytes { (outputBytes: UnsafeMutableRawBufferPointer) in
Expand All @@ -335,7 +378,7 @@ extension OATHSession {
}

extension Data {
func hmacSha1(usingKey key: Data) -> Data {
internal func hmacSha1(usingKey key: Data) -> Data {
var digest = [UInt8](repeating: 0, count: Int(CC_SHA1_DIGEST_LENGTH))
CCHmac(CCHmacAlgorithm(kCCHmacAlgSHA1), key.bytes, key.bytes.count, self.bytes, self.bytes.count, &digest)
return Data(digest)
Expand Down
38 changes: 38 additions & 0 deletions YubiKit/YubiKit/YubiKit.docc/Resources/OATHSessionExtension.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# ``YubiKit/OATHSession``

@Metadata {
@DocumentationExtension(mergeBehavior: append)
}

## Topics

### Managing the OATHSession

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

### Running commands in the OATH application

- ``addCredential(template:)``
- ``deleteCredential(_:)``
- ``listCredentials()``
- ``calculateCode(credential:timestamp:)``
- ``calculateCodes(timestamp:)``
- ``deriveAccessKey(from:)``
- ``setAccessKey(_:)``
- ``setPassword(_:)``
- ``unlockWithAccessKey(_:)``
- ``unlockWithPassword(_:)``

### Return types

- ``Credential``
- ``Code``

### Errors

- ``OATHSessionError``
- ``CredentialTemplateError``

0 comments on commit e8cb486

Please sign in to comment.