chahinebrini 5fb441817f feat(magic): RE-hardening Quick Wins (ACL, #if DEBUG guards, rate-limit)
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>
2026-06-09 05:19:10 +02:00

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
}