import CryptoTokenKit import Combine import Foundation @MainActor final class CardModel: ObservableObject { @Published private(set) var readerNames: [String] = [] @Published var selectedReader = "" @Published private(set) var cardStatus = "Kein Reader verbunden" @Published private(set) var currentUID = "" @Published private(set) var currentCard: TonuinoCard? @Published private(set) var serialPorts = TonuinoSerialService.ports @Published var selectedSerialPort = "" @Published private(set) var serialConnected = false @Published private(set) var firmwareVersion: String? @Published private(set) var tonuinoSettings: TonuinoDeviceSettings? @Published var writing = false @Published var configuringSettings = false @Published private(set) var serialAdminMenuActive = false private let serial = TonuinoSerialService() private let reader = PCSCReader() func refreshReaders() { readerNames = reader.slotNames; if !readerNames.contains(selectedReader) { selectedReader = readerNames.first ?? "" } } func connectSerial() async throws { guard !selectedSerialPort.isEmpty else { throw SerialError.disconnected } let port = selectedSerialPort let service = serial let connection = try await Task.detached(priority: .userInitiated) { try service.connect(port) }.value firmwareVersion = connection.firmwareVersion tonuinoSettings = connection.settings serialConnected = true } func disconnectSerial() { serial.disconnect(); serialConnected = false; firmwareVersion = nil; tonuinoSettings = nil; serialAdminMenuActive = false } func refreshSerialPorts() { serialPorts = TonuinoSerialService.ports; if !serialPorts.contains(selectedSerialPort) { selectedSerialPort = serialPorts.first ?? "" } } func writeViaTonuino(_ card: TonuinoCard) async throws { guard serialConnected else { throw SerialError.disconnected }; writing = true defer { writing = false } let service = serial try await Task.detached(priority: .userInitiated) { try await service.writeCard(card) }.value serialAdminMenuActive = true } func cancelWrite() throws { try serial.cancelWrite(); serialAdminMenuActive = false } func exitTonuinoAdminMenu() throws { try serial.exitAdminMenu(); serialAdminMenuActive = false } func applyTonuinoSetting(menuItem: Int, option: Int, currentPIN: String = "", newPIN: String = "") async throws { guard serialConnected else { throw SerialError.disconnected } configuringSettings = true defer { configuringSettings = false } let service = serial try await Task.detached(priority: .userInitiated) { try await service.applyAdminSetting(menuItem: menuItem, option: option, currentPIN: currentPIN, newPIN: newPIN) }.value serialAdminMenuActive = true } func applyTonuinoVolume(minimum: Int, maximum: Int, initial: Int, currentPIN: String = "") async throws { guard serialConnected else { throw SerialError.disconnected } configuringSettings = true defer { configuringSettings = false } let service = serial try await Task.detached(priority: .userInitiated) { try await service.applyVolumeSettings(minimum: minimum, maximum: maximum, initial: initial, currentPIN: currentPIN) }.value serialAdminMenuActive = true } func resetTonuinoSettings(currentPIN: String = "") async throws { guard serialConnected else { throw SerialError.disconnected } configuringSettings = true defer { configuringSettings = false } let service = serial try await Task.detached(priority: .userInitiated) { try await service.resetAdminSettings(currentPIN: currentPIN) }.value serialAdminMenuActive = false } func readViaReader() async throws -> (String, TonuinoCard?) { try await reader.readCard(slotName: selectedReader) } func writeViaReader(_ card: TonuinoCard) async throws { try await reader.writeCard(card, slotName: selectedReader) } func eraseViaReader() async throws { try await reader.writeCard(nil, slotName: selectedReader) } } final class PCSCReader { private var manager: TKSmartCardSlotManager? { TKSmartCardSlotManager.default } var slotNames: [String] { manager?.slotNames ?? [] } func readCard(slotName: String) async throws -> (String, TonuinoCard?) { let (card, _) = try await connectedCard(slotName) defer { card.endSession() } let uidData = try await transmit(card, [0xFF, 0xCA, 0x00, 0x00, 0x00]) let uid = uidData.map { String(format: "%02X", $0) }.joined() let bytes = try await readTonuinoData(card) return (uid, TonuinoCard.decode(bytes)) } func writeCard(_ value: TonuinoCard?, slotName: String) async throws { let (card, atr) = try await connectedCard(slotName) defer { card.endSession() } let type = CardType(atr: atr) guard type != .unknown else { throw PCSCError.unsupportedCard } let payload = value?.payload() ?? Data(repeating: 0, count: 16) if type == .ultralight { for page in 0..<4 { let chunk = payload.subdata(in: page * 4..<(page + 1) * 4) _ = try await transmit(card, [0xFF, 0xD6, 0x00, UInt8(8 + page), 0x04] + chunk) } } else { try await authenticate(card, block: 4) _ = try await transmit(card, [0xFF, 0xD6, 0x00, 0x04, 0x10] + payload) } } private func readTonuinoData(_ card: TKSmartCard) async throws -> Data { let atr = Array(card.slot.atr?.bytes ?? Data()) if CardType(atr: atr) == .ultralight { return try await transmit(card, [0xFF, 0xB0, 0x00, 0x08, 0x10]) } try await authenticate(card, block: 4) return try await transmit(card, [0xFF, 0xB0, 0x00, 0x04, 0x10]) } private func authenticate(_ card: TKSmartCard, block: UInt8) async throws { _ = try await transmit(card, [0xFF, 0x82, 0x00, 0x00, 0x06, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF]) _ = try await transmit(card, [0xFF, 0x86, 0x00, 0x00, 0x05, 0x01, 0x00, block, 0x60, 0x00]) } private func connectedCard(_ name: String) async throws -> (TKSmartCard, [UInt8]) { guard let slot = manager?.slotNamed(name), let card = slot.makeSmartCard() else { throw PCSCError.noCard } try await card.beginSession() return (card, Array(slot.atr?.bytes ?? Data())) } private func transmit(_ card: TKSmartCard, _ command: [UInt8]) async throws -> Data { try await withCheckedThrowingContinuation { continuation in card.transmit(Data(command)) { response, error in if let error { continuation.resume(throwing: error); return } guard let response, response.count >= 2 else { continuation.resume(throwing: PCSCError.communication); return } let status = response.suffix(2) guard status.elementsEqual([0x90, 0x00]) else { continuation.resume(throwing: PCSCError.status(UInt8(status.first ?? 0), UInt8(status.last ?? 0))); return } continuation.resume(returning: response.dropLast(2)) } } } } enum CardType: Equatable { case classic, ultralight, unknown init(atr: [UInt8]) { let marker: [UInt8] = [0xA0, 0x00, 0x00, 0x03, 0x06, 0x03, 0x00] guard let i = atr.indices.dropLast(marker.count).first(where: { Array(atr[$0..<$0 + marker.count]) == marker }), atr.count > i + marker.count else { self = .unknown; return } switch atr[i + marker.count] { case 0x01, 0x02, 0x26: self = .classic; case 0x03: self = .ultralight; default: self = .unknown } } } enum PCSCError: LocalizedError { case noCard, unsupportedCard, communication, status(UInt8, UInt8) var errorDescription: String? { switch self { case .noCard: "Lege eine Karte auf den ausgewählten Reader." case .unsupportedCard: "Dieser Kartentyp wird nicht unterstützt." case .communication: "Keine gültige Antwort vom Kartenleser." case .status(let a, let b): "Kartenleser meldet Fehlerstatus %02X%02X.".replacingOccurrences(of: "%02X%02X", with: String(format: "%02X%02X", a, b)) } } }