Add Tonuino Manager website and Sparkle updates

This commit is contained in:
maddin committed 2026-10-31 16:40:48 +01:00
commit 1cac427ff2
44 files changed
+5861

No files matched your search

@@ -0,0 +1,161 @@
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))
}
}
}