Add Tonuino Manager website and Sparkle updates
This commit is contained in:
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user