Refactor YubiKey decryptor (#663)

- Add YKFSmartCardInterface extension to simplify smart card related calls
- Use async/await to rewrite callback closures
- Update YubiKeyConnection
- Better error handling
This commit is contained in:
Mingshen Sun 2024-12-15 21:08:27 -08:00 committed by GitHub
parent fc35805565
commit a410c9480a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 344 additions and 320 deletions

View file

@ -0,0 +1,84 @@
//
// YKFSmartCardInterfaceExtension.swift
// pass
//
// Created by Mingshen Sun on 12/15/24.
// Copyright © 2024 Bob Sun. All rights reserved.
//
import CryptoTokenKit
import Gopenpgp
import YubiKit
public enum Algorithm {
case rsa
case others
}
public struct ApplicationRelatedData {
public let isCommandChaining: Bool
public let decryptionAlgorithm: Algorithm
}
public extension YKFSmartCardInterface {
func selectOpenPGPApplication() async throws {
try await selectApplication(YubiKeyAPDU.selectOpenPGPApplication())
}
func verify(password: String) async throws {
try await executeCommand(YubiKeyAPDU.verify(password: password))
}
func getApplicationRelatedData() async throws -> ApplicationRelatedData {
let data = try await executeCommand(YubiKeyAPDU.getApplicationRelatedData())
var isCommandChaining = false
var algorithm = Algorithm.others
let tlv = TKBERTLVRecord.sequenceOfRecords(from: data)!
for record in TKBERTLVRecord.sequenceOfRecords(from: tlv.first!.value)! {
if record.tag == 0x5F52 { // 0x5f52: Historical Bytes
let historical = record.value
if historical.count < 4 {
isCommandChaining = false
}
if historical[0] != 0 {
isCommandChaining = false
}
let dos = historical[1 ..< historical.endIndex - 3]
for record2 in TKCompactTLVRecord.sequenceOfRecords(from: dos)! where record2.tag == 7 && record2.value.count == 3 {
isCommandChaining = (record2.value[2] & 0x80) != 0
}
} else if record.tag == 0x73 { // 0x73: Discretionary data objects
// 0xC2: Algorithm attributes decryption, 0x01: RSA
for record2 in TKBERTLVRecord.sequenceOfRecords(from: record.value)! where record2.tag == 0xC2 && record2.value.first! == 0x01 {
algorithm = .rsa
}
}
}
return ApplicationRelatedData(isCommandChaining: isCommandChaining, decryptionAlgorithm: algorithm)
}
func decipher(ciphertext: Data) async throws -> Data {
let applicationRelatedData = try await getApplicationRelatedData()
guard applicationRelatedData.decryptionAlgorithm == .rsa else {
throw AppError.yubiKey(.decipher(message: "Encryption key algorithm is not supported. Supported algorithm: RSA."))
}
var error: NSError?
let message = createPGPMessage(from: ciphertext)
guard let mpi1 = Gopenpgp.HelperPassGetEncryptedMPI1(message, &error) else {
throw AppError.yubiKey(.decipher(message: "Failed to get encrypted MPI."))
}
let apdus = applicationRelatedData.isCommandChaining ? YubiKeyAPDU.decipherChained(data: mpi1) : YubiKeyAPDU.decipherExtended(data: mpi1)
for (idx, apdu) in apdus.enumerated() {
let data = try await executeCommand(apdu)
// the last response must have the data
if idx == apdus.endIndex - 1 {
return data
}
}
throw AppError.yubiKey(.verify(message: "Failed to execute decipher."))
}
}

View file

@ -36,12 +36,13 @@ public enum YubiKeyError: Error, Equatable {
case selectApplication(message: String)
case verify(message: String)
case decipher(message: String)
case other(message: String)
}
extension YubiKeyError: LocalizedError {
public var errorDescription: String? {
switch self {
case let .connection(message), let .decipher(message), let .selectApplication(message), let .verify(message):
case let .connection(message), let .decipher(message), let .other(message), let .selectApplication(message), let .verify(message):
return message
}
}
@ -56,6 +57,8 @@ extension AppError: LocalizedError {
return localizationKey.localize(name)
case let .pgpPrivateKeyNotFound(keyID), let .pgpPublicKeyNotFound(keyID):
return localizationKey.localize(keyID)
case let .yubiKey(error):
return error.errorDescription
case let .other(message):
return message.localize()
default:

View file

@ -13,30 +13,12 @@ public enum YubiKeyAPDU {
}
public static func verify(password: String) -> YKFAPDU {
let pw1: [UInt8] = Array(password.utf8)
var apdu: [UInt8] = []
apdu += [0x00] // CLA
apdu += [0x20] // INS: VERIFY
apdu += [0x00] // P1
apdu += [0x82] // P2: PW1
apdu += withUnsafeBytes(of: UInt8(pw1.count).bigEndian, Array.init)
apdu += pw1
return YKFAPDU(data: Data(apdu))!
YKFAPDU(cla: 0x00, ins: 0x20, p1: 0x00, p2: 0x82, data: Data(password.utf8), type: .extended)!
}
public static func decipherExtended(data: Data) -> [YKFAPDU] {
var apdu: [UInt8] = []
apdu += [0x00] // CLA (last or only command of a chain)
apdu += [0x2A, 0x80, 0x86] // INS, P1, P2: PSO.DECIPHER
// Lc, An extended Lc field consists of three bytes:
// one byte set to '00' followed by two bytes not set to '0000' (1 to 65535 dec.).
apdu += [0x00] + withUnsafeBytes(of: UInt16(data.count + 1).bigEndian, Array.init)
// Padding indicator byte (00) for RSA or (02) for AES followed by cryptogram Cipher DO 'A6' for ECDH
apdu += [0x00]
apdu += data
apdu += [0x02, 0x00]
return [YKFAPDU(data: Data(apdu))!]
let apdu = YKFAPDU(cla: 0x00, ins: 0x2A, p1: 0x80, p2: 0x86, data: data, type: .extended)!
return [apdu]
}
public static func decipherChained(data: Data) -> [YKFAPDU] {
@ -63,14 +45,8 @@ public enum YubiKeyAPDU {
return result
}
public static func get_application_related_data() -> YKFAPDU {
var apdu: [UInt8] = []
apdu += [0x00] // CLA
apdu += [0xCA] // INS: GET DATA
apdu += [0x00]
apdu += [0x6E] // P2: application related data
apdu += [0x00]
return YKFAPDU(data: Data(apdu))!
public static func getApplicationRelatedData() -> YKFAPDU {
YKFAPDU(cla: 0x00, ins: 0xCA, p1: 0x00, p2: 0x6E, data: Data(), type: .short)!
}
static func chunk(data: Data) -> [[UInt8]] {

View file

@ -2,62 +2,179 @@
// YubiKeyConnection.swift
// passKit
//
// Copyright © 2022 Bob Sun. All rights reserved.
// Copyright (C) 2024 Mingshen Sun.
//
// This file is part of yubioath-ios, modified from the original.
// Original code Copyright Yubico 2022.
//
// 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
import YubiKit
public class YubiKeyConnection: NSObject {
public static let shared = YubiKeyConnection()
var accessoryConnection: YKFAccessoryConnection?
var nfcConnection: YKFNFCConnection?
var connectionCallback: ((_ connection: YKFConnectionProtocol) -> Void)?
var cancellationCallback: ((_ error: Error) -> Void)?
override init() {
override public init() {
super.init()
if YubiKitDeviceCapabilities.supportsISO7816NFCTags {
YubiKitManager.shared.delegate = self
YubiKitManager.shared.startAccessoryConnection()
}
YubiKitManager.shared.delegate = self
}
public func connection(cancellation: @escaping (_ error: Error) -> Void, completion: @escaping (_ connection: YKFConnectionProtocol) -> Void) {
deinit {}
var connection: YKFConnectionProtocol? {
accessoryConnection ?? smartCardConnection ?? nfcConnection
}
private var nfcConnection: YKFNFCConnection?
private var smartCardConnection: YKFSmartCardConnection?
private var accessoryConnection: YKFAccessoryConnection?
private var connectionCallback: ((_ connection: YKFConnectionProtocol) -> Void)?
private var disconnectionCallback: ((_ connection: YKFConnectionProtocol?, _ error: Error?) -> Void)?
private var accessoryConnectionCallback: ((_ connection: YKFAccessoryConnection?) -> Void)?
private var nfcConnectionCallback: ((_ connection: YKFNFCConnection?) -> Void)?
private var smartCardConnectionCallback: ((_ connection: YKFSmartCardConnection?) -> Void)?
public func startConnection(completion: @escaping (_ connection: YKFConnectionProtocol) -> Void) {
YubiKitManager.shared.delegate = self
if let connection = accessoryConnection {
completion(connection)
} else if let connection = smartCardConnection {
completion(connection)
} else if let connection = nfcConnection {
completion(connection)
} else {
connectionCallback = completion
YubiKitManager.shared.startNFCConnection()
if YubiKitDeviceCapabilities.supportsISO7816NFCTags {
YubiKitManager.shared.startNFCConnection()
}
}
cancellationCallback = cancellation
}
public func startConnection() async -> YKFConnectionProtocol {
await withCheckedContinuation { continuation in
self.startConnection { connection in
continuation.resume(with: Result.success(connection))
}
}
}
func startWiredConnection(completion: @escaping (_ connection: YKFConnectionProtocol) -> Void) {
connectionCallback = completion
YubiKitManager.shared.delegate = self
}
func accessoryConnection(handler: @escaping (_ connection: YKFAccessoryConnection?) -> Void) {
if let connection = accessoryConnection {
handler(connection)
} else {
accessoryConnectionCallback = handler
}
}
func smartCardConnection(handler: @escaping (_ connection: YKFSmartCardConnection?) -> Void) {
if let connection = smartCardConnection {
handler(connection)
} else {
smartCardConnectionCallback = handler
}
}
func nfcConnection(handler: @escaping (_ connection: YKFNFCConnection?) -> Void) {
if let connection = nfcConnection {
handler(connection)
} else {
nfcConnectionCallback = handler
}
}
public func stop() {
if #available(iOSApplicationExtension 16.0, *) {
smartCardConnection?.stop()
}
accessoryConnection?.stop()
nfcConnection?.stop()
// stop() returns immediately but closing the connection will take a few cycles so we need to wait to make sure it's closed before restarting.
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
if YubiKitDeviceCapabilities.supportsMFIAccessoryKey {
YubiKitManager.shared.startAccessoryConnection()
}
if YubiKitDeviceCapabilities.supportsSmartCardOverUSBC, #available(iOSApplicationExtension 16.0, *) {
YubiKitManager.shared.startSmartCardConnection()
}
}
}
public func didDisconnect(handler: @escaping (_ connection: YKFConnectionProtocol?, _ error: Error?) -> Void) {
disconnectionCallback = handler
}
}
extension YubiKeyConnection: YKFManagerDelegate {
public func didConnectNFC(_ connection: YKFNFCConnection) {
nfcConnection = connection
if let callback = connectionCallback {
callback(connection)
}
nfcConnectionCallback?(connection)
nfcConnectionCallback = nil
connectionCallback?(connection)
connectionCallback = nil
}
public func didDisconnectNFC(_: YKFNFCConnection, error _: Error?) {
public func didDisconnectNFC(_ connection: YKFNFCConnection, error: Error?) {
nfcConnection = nil
nfcConnectionCallback = nil
connectionCallback = nil
disconnectionCallback?(connection, error)
disconnectionCallback = nil
}
public func didFailConnectingNFC(_ error: Error) {
nfcConnectionCallback = nil
connectionCallback = nil
disconnectionCallback?(nil, error)
disconnectionCallback = nil
}
public func didConnectAccessory(_ connection: YKFAccessoryConnection) {
accessoryConnection = connection
accessoryConnectionCallback?(connection)
accessoryConnectionCallback = nil
connectionCallback?(connection)
connectionCallback = nil
}
public func didDisconnectAccessory(_: YKFAccessoryConnection, error _: Error?) {
public func didDisconnectAccessory(_ connection: YKFAccessoryConnection, error: Error?) {
accessoryConnection = nil
accessoryConnectionCallback = nil
connectionCallback = nil
disconnectionCallback?(connection, error)
disconnectionCallback = nil
}
public func didFailConnectingNFC(_ error: Error) {
if let callback = cancellationCallback {
callback(error)
}
public func didConnectSmartCard(_ connection: YKFSmartCardConnection) {
smartCardConnection = connection
smartCardConnectionCallback?(connection)
smartCardConnectionCallback = nil
connectionCallback?(connection)
connectionCallback = nil
}
public func didDisconnectSmartCard(_ connection: YKFSmartCardConnection, error: Error?) {
smartCardConnection = nil
smartCardConnectionCallback = nil
connectionCallback = nil
disconnectionCallback?(connection, error)
disconnectionCallback = nil
}
}