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

114 lines
4.3 KiB
Rust

//! Elevated One-Shot-Setup: EIN UAC-Prompt erledigt alles —
//! DoH-Konfiguration, ProgramData-State, Tamper-Service-Installation.
//! Danach läuft alles silent (Service als SYSTEM, Deaktivierung server-gesteuert).
use protection_core::{apply_script, ProtectionState, SERVICE_NAME};
use std::net::ToSocketAddrs;
use std::path::PathBuf;
use protection_core::{ps_run, DOH_FALLBACK_IP, DOH_HOST};
/// Löst die IPv4 des DoH-Hosts auf (solange DNS noch normal funktioniert).
/// Fallback auf die bekannte Hetzner-IP wenn Resolve fehlschlägt.
pub fn resolve_doh_ip() -> String {
if let Ok(addrs) = format!("{DOH_HOST}:443").to_socket_addrs() {
for addr in addrs {
if let std::net::SocketAddr::V4(v4) = addr {
return v4.ip().to_string();
}
}
}
DOH_FALLBACK_IP.to_string()
}
/// Pfad des Tamper-Service-Binaries (Tauri-Sidecar, liegt neben der Haupt-Exe).
pub fn service_exe_path() -> Result<PathBuf, String> {
let exe = std::env::current_exe().map_err(|e| e.to_string())?;
let dir = exe
.parent()
.ok_or_else(|| "Exe-Verzeichnis nicht ermittelbar".to_string())?;
Ok(dir.join("rebreak-protection-service.exe"))
}
/// Baut das komplette elevated Setup-Script:
/// 1. DoH anwenden 2. State nach %ProgramData%\ReBreak 3. ACL härten 4. Service installieren+starten
pub fn build_setup_script(state: &ProtectionState, service_exe: &str) -> String {
let mut script = String::new();
script.push_str(&apply_script(&state.server_ip, &state.doh_template));
let state_dir = protection_core::state_dir();
let state_path = protection_core::state_path();
let json = serde_json::to_string_pretty(state).unwrap_or_default();
script.push_str(&format!(
r#"
# Persistenter State für den Tamper-Service (SYSTEM liest das beim Polling)
New-Item -ItemType Directory -Force -Path '{state_dir}' | Out-Null
@'
{json}
'@ | Set-Content -Path '{state_path}' -Encoding UTF8
# ACL härten: nur SYSTEM + Administratoren dürfen lesen/schreiben.
# Standard-User verlieren Read-Zugriff → DNS-Token nicht trivial auslesbar.
$acl = New-Object System.Security.AccessControl.FileSecurity
$acl.SetAccessRuleProtection($true, $false) # Vererbung deaktivieren
$ruleSystem = New-Object System.Security.AccessControl.FileSystemAccessRule(
"NT AUTHORITY\SYSTEM",
"FullControl",
"Allow"
)
$ruleAdmins = New-Object System.Security.AccessControl.FileSystemAccessRule(
"BUILTIN\Administrators",
"FullControl",
"Allow"
)
$acl.AddAccessRule($ruleSystem)
$acl.AddAccessRule($ruleAdmins)
Set-Acl -Path '{state_path}' -AclObject $acl
# Tamper-Protection-Service als SYSTEM installieren (idempotent: alte Instanz weg)
sc.exe stop {SERVICE_NAME} 2>$null | Out-Null
sc.exe delete {SERVICE_NAME} 2>$null | Out-Null
sc.exe create {SERVICE_NAME} binPath= '"{service_exe}"' start= auto obj= LocalSystem DisplayName= "ReBreak Protection" | Out-Null
sc.exe description {SERVICE_NAME} "ReBreak DNS-Schutz — Tamper-Monitor (alle 5 Minuten)" | Out-Null
sc.exe failure {SERVICE_NAME} reset= 86400 actions= restart/60000/restart/60000/restart/60000 | Out-Null
sc.exe start {SERVICE_NAME} | Out-Null
exit 0
"#,
state_dir = state_dir.display(),
state_path = state_path.display(),
));
script
}
/// Schreibt das Script als Temp-.ps1 und startet es elevated (UAC) mit -Wait.
/// Gibt Err zurück wenn der User den UAC-Prompt abbricht oder das Script failt.
pub fn run_elevated(script: &str) -> Result<(), String> {
let tmp = std::env::temp_dir().join(format!(
"rebreak-setup-{}.ps1",
std::process::id()
));
std::fs::write(&tmp, script).map_err(|e| format!("Temp-Script: {e}"))?;
// Outer-PS startet Inner-PS elevated; -Wait + -PassThru für Exit-Code
let outer = format!(
r#"$p = Start-Process powershell.exe -Verb RunAs -Wait -PassThru -WindowStyle Hidden -ArgumentList @('-NonInteractive','-NoProfile','-ExecutionPolicy','Bypass','-File','{}')
exit $p.ExitCode"#,
tmp.display()
);
let result = ps_run(&outer);
let _ = std::fs::remove_file(&tmp);
match result {
Ok(_) => Ok(()),
Err(e) if e.contains("canceled") || e.contains("abgebrochen") => {
Err("Administrator-Bestätigung wurde abgebrochen.".into())
}
Err(e) => Err(format!("Setup fehlgeschlagen: {e}")),
}
}