Härtung der öffentlich downloadbaren Magic-Apps gegen Reverse Engineering (Assessment: docs/specs/magic-re-hardening.md): - Windows: protection.json per ACL auf SYSTEM+Admins (DNS-Token nicht mehr von Standard-Usern lesbar) — setup.rs - Mac: MacProfileInstaller.remove() + Debug-Supervision-Modi/Reset nur noch #if DEBUG (kein Removal-/Debug-Pfad im Release-Binary) - Mac: staging-URL einmal als Konstante statt 4x Literal; interne Infra-Notizen aus String-Literalen raus - Backend: Rate-Limit (10/IP/min) auf /api/magic/pair/redeem NUR Backend-Teil deployt via Push; Mac/Win brauchen Xcode-/Cargo-Release-Build (zied) + Smoke-Tests vor Release. MagicAPIClient.swift trägt etwas vorbestehenden WIP mit (gleiche Magic-Client-Domäne). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
246 lines
7.6 KiB
Swift
246 lines
7.6 KiB
Swift
import Foundation
|
|
import Observation
|
|
|
|
#if DEBUG
|
|
enum DebugSupervisionMode: String, CaseIterable, Identifiable {
|
|
case none
|
|
case forceSupervised
|
|
case forceUnsupervised
|
|
|
|
var id: String { rawValue }
|
|
|
|
var title: String {
|
|
switch self {
|
|
case .none: return "Aus"
|
|
case .forceSupervised: return "Force Supervised"
|
|
case .forceUnsupervised: return "Force Unsupervised"
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
|
|
@MainActor
|
|
@Observable
|
|
final class WizardModel {
|
|
var step: WizardStep = .macRegistration
|
|
var device: DeviceState?
|
|
|
|
var supervisionLog: [String] = []
|
|
var supervisionRunning: Bool = false
|
|
var supervisionError: String?
|
|
|
|
var enrollmentLog: [String] = []
|
|
var enrollmentRunning: Bool = false
|
|
var enrollmentError: String?
|
|
|
|
var configureLog: [String] = []
|
|
var configureRunning: Bool = false
|
|
var configureError: String?
|
|
|
|
var showAdvancedLogs: Bool = false
|
|
|
|
var cooldownEndsAt: Date?
|
|
|
|
#if DEBUG
|
|
// Debug-Reset State — nur in Debug-Builds vorhanden
|
|
var supervisionMode: DebugSupervisionMode = .none
|
|
var resetRunning: Bool = false
|
|
var resetStatus: String?
|
|
var resetAll: Bool = true
|
|
var resetEnrollmentProfile: Bool = true
|
|
var resetLockProfile: Bool = true
|
|
var resetApp: Bool = true
|
|
#endif
|
|
|
|
// Auth + Magic State
|
|
var authSession: AuthSession?
|
|
var showingLogin: Bool = false
|
|
var showingHub: Bool = false
|
|
var showingManageBindings: Bool = false
|
|
var magicRegistration: MagicRegistration?
|
|
var registrationError: String?
|
|
|
|
init() {
|
|
// Load existing session from keychain
|
|
authSession = AuthService.shared.currentSession()
|
|
showingLogin = (authSession == nil)
|
|
// Nach Login direkt zum Hub statt Mac-Auto-Registrierung
|
|
showingHub = (authSession != nil)
|
|
}
|
|
|
|
func advance() {
|
|
if let next = WizardStep(rawValue: step.rawValue + 1) {
|
|
step = next
|
|
}
|
|
}
|
|
|
|
func goTo(_ s: WizardStep) {
|
|
step = s
|
|
}
|
|
|
|
// MARK: - Mac Registration
|
|
|
|
/// Registriert den aktuellen Mac im Backend.
|
|
/// Wirft MagicError.limitReached falls Device-Limit erreicht.
|
|
func registerMac() async throws {
|
|
registrationError = nil
|
|
|
|
do {
|
|
let macInfo = try MacDeviceDetector.detect()
|
|
|
|
let registration = try await MagicAPIClient.shared.register(
|
|
deviceId: macInfo.deviceId,
|
|
hostname: macInfo.hostname,
|
|
model: macInfo.model,
|
|
osVersion: macInfo.osVersion
|
|
)
|
|
|
|
magicRegistration = registration
|
|
|
|
} catch let error as MagicError {
|
|
// Bei limit_reached → öffne ManageBindingsView
|
|
if case .limitReached(_) = error {
|
|
showingManageBindings = true
|
|
}
|
|
registrationError = error.localizedDescription
|
|
throw error
|
|
} catch {
|
|
registrationError = error.localizedDescription
|
|
throw error
|
|
}
|
|
}
|
|
|
|
func handleLogin(session: AuthSession) {
|
|
authSession = session
|
|
showingLogin = false
|
|
showingHub = true
|
|
}
|
|
|
|
func handleLogout() async {
|
|
await AuthService.shared.signOut()
|
|
authSession = nil
|
|
showingLogin = true
|
|
showingHub = false
|
|
reset()
|
|
}
|
|
|
|
// MARK: - Hub Navigation
|
|
|
|
/// User wählt 'iOS-Gerät hinzufügen' im Hub.
|
|
func startIOSFlow() {
|
|
showingHub = false
|
|
step = .welcome
|
|
}
|
|
|
|
/// User wählt 'Mac schützen' im Hub.
|
|
func startMacFlow() {
|
|
showingHub = false
|
|
step = .macRegistration
|
|
}
|
|
|
|
/// Zurück zur Geräte-Übersicht.
|
|
func returnToHub() {
|
|
reset()
|
|
showingHub = true
|
|
}
|
|
|
|
func reset() {
|
|
step = .macRegistration
|
|
device = nil
|
|
supervisionLog = []
|
|
enrollmentLog = []
|
|
configureLog = []
|
|
supervisionError = nil
|
|
enrollmentError = nil
|
|
configureError = nil
|
|
showAdvancedLogs = false
|
|
cooldownEndsAt = nil
|
|
#if DEBUG
|
|
resetStatus = nil
|
|
#endif
|
|
magicRegistration = nil
|
|
registrationError = nil
|
|
}
|
|
|
|
#if DEBUG
|
|
func startDebugReset() {
|
|
guard device != nil else {
|
|
resetStatus = "Kein iPhone erkannt."
|
|
return
|
|
}
|
|
resetRunning = true
|
|
resetStatus = "Führe Debug-Reset aus …"
|
|
|
|
Task {
|
|
do {
|
|
var changes: [String] = []
|
|
|
|
let removeEnrollment = resetAll || resetEnrollmentProfile
|
|
let removeLock = resetAll || resetLockProfile
|
|
let removeApp = resetAll || resetApp
|
|
|
|
let installedProfileIDs = await DeviceDetector.installedProfileIDs()
|
|
var profileIDs: [String] = []
|
|
if removeEnrollment, installedProfileIDs.contains(DeviceState.enrollmentProfileID) {
|
|
profileIDs.append(DeviceState.enrollmentProfileID)
|
|
}
|
|
if removeLock, installedProfileIDs.contains(DeviceState.lockProfileID) {
|
|
profileIDs.append(DeviceState.lockProfileID)
|
|
}
|
|
if !profileIDs.isEmpty {
|
|
try await DeviceDetector.removeProfiles(identifiers: profileIDs)
|
|
changes.append("Profile gelöscht: \(profileIDs.joined(separator: ", "))")
|
|
}
|
|
|
|
if removeApp {
|
|
try await DeviceDetector.removeApp(bundleID: "org.rebreak.app")
|
|
changes.append("App gelöscht: org.rebreak.app")
|
|
}
|
|
|
|
switch supervisionMode {
|
|
case .forceSupervised:
|
|
_ = try await SuperviseRunner.supervise(verbose: false) { _ in }
|
|
changes.append("Mode gesetzt: supervised")
|
|
case .forceUnsupervised:
|
|
_ = try await SuperviseRunner.unsupervise { _ in }
|
|
changes.append("Mode gesetzt: unsupervised")
|
|
case .none:
|
|
break
|
|
}
|
|
|
|
let nowInstalledProfiles = await DeviceDetector.installedProfileIDs()
|
|
let nowApps = await DeviceDetector.installedAppBundleIDs()
|
|
let status = await DeviceDetector.readSupervisionStatus()
|
|
|
|
await MainActor.run {
|
|
if changes.isEmpty {
|
|
resetStatus = "Keine Aktion gewählt."
|
|
} else {
|
|
resetStatus = "✓ \(changes.joined(separator: " · "))"
|
|
}
|
|
|
|
if var device = self.device {
|
|
device.installedProfileIDs = nowInstalledProfiles
|
|
device.installedAppBundleIDs = nowApps
|
|
device.isSupervised = status.isSupervised
|
|
device.supervisorOrgName = status.organizationName
|
|
device.isFmiOn = status.findMyEnabled
|
|
device.isEnrolled = nowInstalledProfiles.contains(DeviceState.enrollmentProfileID)
|
|
if !nowApps.contains("org.rebreak.app") { device.isManaged = false }
|
|
if !nowInstalledProfiles.contains(DeviceState.lockProfileID) { device.isFilterActive = false }
|
|
self.device = device
|
|
}
|
|
|
|
resetRunning = false
|
|
}
|
|
} catch {
|
|
await MainActor.run {
|
|
resetStatus = "✗ Reset fehlgeschlagen: \(error.localizedDescription)"
|
|
resetRunning = false
|
|
}
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
}
|