Skip to content

Commit

Permalink
Added support for renaming OATH credentials.
Browse files Browse the repository at this point in the history
  • Loading branch information
jensutbult committed Mar 6, 2024
1 parent 9af5901 commit 673c3eb
Show file tree
Hide file tree
Showing 8 changed files with 126 additions and 21 deletions.
34 changes: 34 additions & 0 deletions FullStackTests/Tests/OATHFullStackTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,40 @@ class OATHFullStackTests: XCTestCase {
}
}

func testRenameCredential() throws {
runOATHTest(populated: false) { session in
let template = OATHSession.CredentialTemplate(type: .TOTP(), algorithm: .SHA1, secret: "abba".base32DecodedData!, issuer: "Original Issuer", name: "Original Name", digits: 6)
try await session.addCredential(template: template)
guard let credential = try await session.listCredentials().first else { XCTFail("Failed adding credential to YubiKey."); return }
do {
try await session.renameCredential(credential, newName: "New Name", newIssuer: "New Issuer")
guard let renamedCredential = try await session.listCredentials().first else { XCTFail("Failed reading renamed credential from YubiKey."); return }
XCTAssertEqual(renamedCredential.name, "New Name")
XCTAssertEqual(renamedCredential.issuer, "New Issuer")
} catch {
guard let error = error as? SessionError, error == .notSupported else { XCTFail("Unexpected error: \(error)"); return }
print("⚠️ Skip testRenameCredential()")
}
}
}

func testRenameCredentialNoIssuer() throws {
runOATHTest(populated: false) { session in
let template = OATHSession.CredentialTemplate(type: .TOTP(), algorithm: .SHA1, secret: "abba".base32DecodedData!, issuer: "Original Issuer", name: "Original Name", digits: 6)
try await session.addCredential(template: template)
guard let credential = try await session.listCredentials().first else { XCTFail("Failed adding credential to YubiKey."); return }
do {
try await session.renameCredential(credential, newName: "New Name", newIssuer: nil)
guard let renamedCredential = try await session.listCredentials().first else { XCTFail("Failed reading renamed credential from YubiKey."); return }
XCTAssertEqual(renamedCredential.name, "New Name")
XCTAssertNil(renamedCredential.issuer)
} catch {
guard let error = error as? SessionError, error == .notSupported else { XCTFail("Unexpected error: \(error)"); return }
print("⚠️ Skip testRenameCredentialNoIssuer()")
}
}
}

func testDeleteCredential() throws {
runOATHTest() { session in
let credentials = try await session.listCredentials()
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 @@ -28,6 +28,7 @@
B47FDD992939FAD100AFF70A /* ConnectionHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = B47FDD982939FAD100AFF70A /* ConnectionHelper.swift */; };
B47FDD9B293A15AE00AFF70A /* NSLock+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B47FDD9A293A15AE00AFF70A /* NSLock+Extensions.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 */; };
B4BE3AAD292E1C8600CC30CB /* OATHSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4BE3AAC292E1C8600CC30CB /* OATHSession.swift */; };
B4BE3AAF292E1CBC00CC30CB /* Data+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4BE3AAE292E1CBC00CC30CB /* Data+Extensions.swift */; };
Expand Down Expand Up @@ -80,6 +81,7 @@
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>"; };
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>"; };
B4BE3AAC292E1C8600CC30CB /* OATHSession.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OATHSession.swift; sourceTree = "<group>"; };
B4BE3AAE292E1CBC00CC30CB /* Data+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+Extensions.swift"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -173,6 +175,7 @@
children = (
B4BE3AAC292E1C8600CC30CB /* OATHSession.swift */,
B4BE3AA7292BCA1300CC30CB /* OATHSession+Extensions.swift */,
B4B124722B98B33C0099BEDB /* OATHSessionFeature.swift */,
);
path = OATH;
sourceTree = "<group>";
Expand Down Expand Up @@ -371,6 +374,7 @@
B408BA8F2948FA2100001B2F /* Stream+Extensions.swift in Sources */,
B4BE3AB3292E1E6D00CC30CB /* TKTLVRecord+Extensions.swift in Sources */,
B456E215274D2453004471DE /* LightningConnection.swift in Sources */,
B4B124732B98B33C0099BEDB /* OATHSessionFeature.swift in Sources */,
B4F937662B51EBAF0007D394 /* PIVPadding.swift in Sources */,
B4BE3AB5292E1ECB00CC30CB /* Sequence+Extensions.swift in Sources */,
B4F10D4F2AFD39D600F0AFCA /* String+Extensions.swift in Sources */,
Expand Down
36 changes: 21 additions & 15 deletions YubiKit/YubiKit/OATH/OATHSession+Extensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,26 @@ extension OATHSession {

}

internal struct CredentialIdentifier {
static func identifier(name: String, issuer: String?, type: OATHSession.CredentialType) -> String {
let key: String
if let issuer {
key = "\(issuer):\(name)"
} else {
key = name
}
if case let .TOTP(period) = type {
if period != oathDefaultPeriod {
return "\(String(format: "%.0f", period))/\(key)"
} else {
return key
}
} else {
return key
}
}
}

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

Expand All @@ -230,21 +250,7 @@ extension OATHSession {
///
/// 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)"
} else {
key = name
}
if case let .TOTP(period) = type {
if period != oathDefaultPeriod {
return "\(String(format: "%.0f", period))/\(key)"
} else {
return key
}
} else {
return key
}
return CredentialIdentifier.identifier(name: name, issuer: issuer, type: type)
}

/// Creates a CredentialTemplate by parsing a [otpauth:// URI](https://github.com/google/google-authenticator/wiki/Key-Uri-Format).
Expand Down
26 changes: 25 additions & 1 deletion YubiKit/YubiKit/OATH/OATHSession.swift
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ public final actor OATHSession: Session, InternalSession {
}

private let selectResponse: SelectResponse
public nonisolated var version: Version {
selectResponse.version
}

private init(connection: Connection) async throws {
self.selectResponse = try await Self.selectApplication(withConnection: connection)
Expand Down Expand Up @@ -115,7 +118,7 @@ public final actor OATHSession: Session, InternalSession {
}

nonisolated public func supports(_ feature: SessionFeature) -> Bool {
return true
return feature.isSupported(by: version)
}

/// Adds a new Credential to the YubiKey.
Expand Down Expand Up @@ -155,6 +158,27 @@ public final actor OATHSession: Session, InternalSession {
return Credential(deviceId: selectResponse.deviceId, id: nameData, type: template.type, name: template.name, issuer: template.issuer, requiresTouch: template.requiresTouch)
}

/// Sends to the key an OATH Rename request to update issuer and account on an existing credential.
///
/// >Note: This functionality requires support for renaming, available on YubiKey 5.3 or later.
///
/// - Parameters:
/// - credential: The credential to rename.
/// - newName: The new account name.
/// - newIssuer: The new issuer.
public func renameCredential(_ credential: Credential, newName: String, newIssuer: String?) async throws {
guard self.supports(OATHSessionFeature.rename) else { throw SessionError.notSupported }
guard let connection = _connection else { throw SessionError.noConnection }
guard let currentId = CredentialIdentifier.identifier(name: credential.name, issuer: credential.issuer, type: credential.type).data(using: .utf8),
let renamedId = CredentialIdentifier.identifier(name: newName, issuer: newIssuer, type: credential.type).data(using: .utf8)
else { throw OATHSessionError.unexpectedData }
var data = Data()
data.append(TKBERTLVRecord(tag: 0x71, value: currentId).data)
data.append(TKBERTLVRecord(tag: 0x71, value: renamedId).data)
let apdu = APDU(cla: 0, ins: 0x05, p1: 0, p2: 0, command: data)
try await connection.send(apdu: apdu)
}

/// 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 {
Expand Down
27 changes: 27 additions & 0 deletions YubiKit/YubiKit/OATH/OATHSessionFeature.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// 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 OATHSessionFeature: SessionFeature {

case rename

public func isSupported(by version: Version) -> Bool {
switch self {
case .rename:
return version >= Version(withString: "5.3.0")!
}
}
}
13 changes: 10 additions & 3 deletions YubiKit/YubiKit/PIV/PIVSessionFeature.swift
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
// Copyright Yubico AB
//
// PIVSessionFeature.swift
// YubiKit
// 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
//
// Created by Jens Utbult on 2024-02-12.
// 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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
### Running commands in the OATH application

- ``addCredential(template:)``
- ``renameCredential(_:newName:newIssuer:)``
- ``deleteCredential(_:)``
- ``listCredentials()``
- ``calculateCode(credential:timestamp:)``
Expand Down
6 changes: 4 additions & 2 deletions YubiKit/YubiKit/YubiKit.docc/Resources/PIVSessionExtension.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,6 @@
- ``blockPin(counter:)``
- ``blockPuk(counter:)``



### Return types

- ``PIVTouchPolicy``
Expand All @@ -55,6 +53,10 @@
- ``PIVPinPukMetadata``
- ``PIVManagementKeyType``

### PIV Sesson features

-``PIVSessionFeature``

### Errors

- ``PIVSessionError``
Expand Down

0 comments on commit 673c3eb

Please sign in to comment.