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>
111 lines
4.1 KiB
Swift
111 lines
4.1 KiB
Swift
import Foundation
|
|
import AppKit
|
|
|
|
/// Service für Mac-DNS-Profile-Download + Installation.
|
|
enum MacProfileInstaller {
|
|
|
|
enum InstallerError: Error, LocalizedError {
|
|
case noRegistration
|
|
case downloadFailed(String)
|
|
case installFailed(String)
|
|
|
|
var errorDescription: String? {
|
|
switch self {
|
|
case .noRegistration:
|
|
return "Mac ist nicht registriert. Bitte zuerst registrieren."
|
|
case .downloadFailed(let msg):
|
|
return "Profile-Download fehlgeschlagen: \(msg)"
|
|
case .installFailed(let msg):
|
|
return "Profile-Installation fehlgeschlagen: \(msg)"
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Lädt Mac-DNS-Profile von Backend und öffnet es in System Settings → Profiles.
|
|
/// Ab macOS 15+ ist `profiles install` für Configuration Profiles entfernt
|
|
/// ("profiles tool no longer supports installs. Use System Settings
|
|
/// Profiles to add configuration profiles."). Einzig zulässiger Weg ohne
|
|
/// MDM-Enrollment: NSWorkspace öffnet die .mobileconfig → Profiles-Pane
|
|
/// erscheint → User muss manuell „Installieren" klicken + Admin-PW geben.
|
|
static func downloadAndInstall(registration: MagicRegistration) async throws {
|
|
// 1. Download profile
|
|
let profileURL: URL
|
|
do {
|
|
profileURL = try await MagicAPIClient.shared.downloadProfile(token: registration.dnsToken)
|
|
} catch {
|
|
throw InstallerError.downloadFailed(error.localizedDescription)
|
|
}
|
|
|
|
// 2. Open in System Settings → Profiles (user must confirm in UI)
|
|
let opened = await MainActor.run {
|
|
NSWorkspace.shared.open(profileURL)
|
|
}
|
|
|
|
if !opened {
|
|
try? FileManager.default.removeItem(at: profileURL)
|
|
throw InstallerError.installFailed(
|
|
"System Settings konnte das Profil nicht öffnen. Datei liegt unter: \(profileURL.path)"
|
|
)
|
|
}
|
|
|
|
// NICHT löschen — System Settings braucht die Datei evtl. noch.
|
|
// OS räumt /tmp selbst auf.
|
|
}
|
|
|
|
/// Prüft ob ReBreak-DNS-Profile bereits installiert ist.
|
|
/// Verwendet `profiles show -type configuration`.
|
|
static func isInstalled() async -> Bool {
|
|
guard let result = try? await ProcessRunner.run(
|
|
"/usr/bin/profiles",
|
|
arguments: ["show", "-type", "configuration"]
|
|
), result.exitCode == 0 else {
|
|
return false
|
|
}
|
|
|
|
// Suche nach PayloadIdentifier pattern org.rebreak.protection.dns.filter*
|
|
return result.stdout.localizedCaseInsensitiveContains("org.rebreak.protection.dns.filter")
|
|
|| result.stdout.localizedCaseInsensitiveContains("org.rebreak.protection.profile")
|
|
}
|
|
|
|
// MARK: - Debug only
|
|
|
|
#if DEBUG
|
|
/// Entfernt ReBreak-DNS-Profile.
|
|
/// NUR in Debug-Builds verfügbar — darf nicht im Release-Binary landen.
|
|
static func remove() async throws {
|
|
// 1. Find identifier
|
|
guard let result = try? await ProcessRunner.run(
|
|
"/usr/bin/profiles",
|
|
arguments: ["show", "-type", "configuration"]
|
|
), result.exitCode == 0 else {
|
|
return
|
|
}
|
|
|
|
// Parse identifier aus Output (format: " <identifier>: <displayName>")
|
|
let lines = result.stdout.split(separator: "\n")
|
|
var identifier: String?
|
|
|
|
for line in lines {
|
|
let trimmed = line.trimmingCharacters(in: .whitespaces)
|
|
if trimmed.hasPrefix("org.rebreak.protection.profile") {
|
|
// Format: "org.rebreak.protection.profile.abc123: ReBreak Protection"
|
|
identifier = trimmed.split(separator: ":").first.map(String.init)
|
|
break
|
|
}
|
|
}
|
|
|
|
guard let id = identifier else { return }
|
|
|
|
// 2. Remove profile
|
|
let removeResult = try await ProcessRunner.run(
|
|
"/usr/bin/profiles",
|
|
arguments: ["remove", "-identifier", id]
|
|
)
|
|
|
|
if removeResult.exitCode != 0 {
|
|
throw InstallerError.installFailed(removeResult.stderr)
|
|
}
|
|
}
|
|
#endif
|
|
}
|