162 lines
8.2 KiB
Swift
162 lines
8.2 KiB
Swift
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))
|
|
}
|
|
}
|
|
}
|