#!/usr/bin/env bash # # rebreak-supervise.sh # -------------------- # Backup-Sandwich-Bootstrap für iPhone-Supervision ohne sichtbaren Daten-Verlust. # # 1. idevicebackup2 encrypted Backup -> ~/.rebreak-supervise/backups// # 2. cfgutil prepare --supervised -> wiped iPhone, Supervised-Flag setzen # 3. idevicebackup2 restore -> Daten zurück, Supervised-Flag persistiert # 4. cfgutil install-profile -> ReBreak-Schutz-Profil installieren # # Voraussetzungen (siehe README.md): # - macOS # - Apple Configurator 2 (App Store) + cfgutil im PATH # - libimobiledevice (brew install libimobiledevice) # - Supervision-Identity einmalig generiert (siehe SUPERVISION-IDENTITY-SETUP.md) # - iPhone via USB-C verbunden, Find-My DEAKTIVIERT, Code entsperrt # - Vertrauenshandshake (Trust-Dialog auf iPhone) bestätigt # # CLI: # rebreak-supervise.sh [--dry-run] [--state-dir DIR] [--profile PATH] [--resume] # # Status: PROTOTYPE. Einige Steps (cfgutil-Aufruf, Verify-Pfad) sind erst auf # physischem Test-iPhone final verifiziert. Markierungen "VERIFY ON DEVICE" im # Code zeigen wo Hardware-in-Loop noch nachgehärtet werden muss. set -euo pipefail # ------------------------------------------------------------------------------ # Konfiguration + Defaults # ------------------------------------------------------------------------------ STATE_DIR="${REBREAK_STATE_DIR:-$HOME/.rebreak-supervise}" PROFILE_PATH_DEFAULT="$(cd "$(dirname "$0")/.." && pwd)/profiles/rebreak-iphone-protection.mobileconfig" PROFILE_PATH="" DRY_RUN=0 RESUME=0 CFGUTIL="/Applications/Apple Configurator.app/Contents/MacOS/cfgutil" SUPERVISION_IDENTITY_P12="${SUPERVISION_IDENTITY_P12:-$STATE_DIR/supervision-identity.p12}" SUPERVISION_IDENTITY_PASS_FILE="${SUPERVISION_IDENTITY_PASS_FILE:-$STATE_DIR/supervision-identity.pass}" # Farben (nur wenn TTY) if [[ -t 1 ]]; then C_RESET="\033[0m"; C_RED="\033[1;31m"; C_GREEN="\033[1;32m" C_YELLOW="\033[1;33m"; C_BLUE="\033[1;34m"; C_DIM="\033[2m" else C_RESET=""; C_RED=""; C_GREEN=""; C_YELLOW=""; C_BLUE=""; C_DIM="" fi # ------------------------------------------------------------------------------ # Helpers: Logging # ------------------------------------------------------------------------------ LOG_FILE="" log() { printf "%b\n" "$1" | tee -a "${LOG_FILE:-/dev/null}"; } ok() { log "${C_GREEN}✓${C_RESET} $1"; } warn() { log "${C_YELLOW}⚠${C_RESET} $1"; } err() { log "${C_RED}✗${C_RESET} $1" >&2; } info() { log "${C_BLUE}→${C_RESET} $1"; } dim() { log "${C_DIM} $1${C_RESET}"; } die() { err "$1"; exit "${2:-1}"; } confirm() { local prompt="$1" [[ "$DRY_RUN" == 1 ]] && { dim "[dry-run] auto-yes: $prompt"; return 0; } read -r -p "$(printf "%b" "${C_YELLOW}?${C_RESET} $prompt [y/N] ")" reply [[ "$reply" =~ ^[yY]$ ]] } run() { # Wraps a command: in dry-run, just echo. Otherwise execute. if [[ "$DRY_RUN" == 1 ]]; then dim "[dry-run] $*" return 0 fi "$@" } # ------------------------------------------------------------------------------ # State-Management: simples JSON-ähnliches Format pro UDID # # Datei: $STATE_DIR/state-.env (key=value, source-bar) # Keys: STEP_PREFLIGHT_AT, STEP_BACKUP_AT, BACKUP_PATH, # STEP_SUPERVISE_AT, STEP_RESTORE_AT, STEP_PROFILE_AT # ------------------------------------------------------------------------------ state_file_for() { printf "%s/state-%s.env" "$STATE_DIR" "$1"; } state_load() { local f; f="$(state_file_for "$1")" if [[ -f "$f" ]]; then # shellcheck disable=SC1090 source "$f" fi } state_set() { # state_set UDID KEY VALUE local f; f="$(state_file_for "$1")" local key="$2"; local val="$3" # In-Memory-Update eval "$key=\"\$val\"" # Persist (re-write each time, ist klein) local tmpf="${f}.tmp" { for k in STEP_PREFLIGHT_AT STEP_BACKUP_AT BACKUP_PATH STEP_SUPERVISE_AT STEP_RESTORE_AT STEP_PROFILE_AT BACKUP_PASSWORD_FILE; do local v="${!k:-}" [[ -n "$v" ]] && printf "%s=%q\n" "$k" "$v" done } > "$tmpf" mv "$tmpf" "$f" chmod 600 "$f" } # ------------------------------------------------------------------------------ # Step 0: Argumente parsen # ------------------------------------------------------------------------------ usage() { cat </dev/null 2>&1 \ || die "Fehlt: $bin → brew install libimobiledevice" done ok "libimobiledevice tools im PATH" [[ -x "$CFGUTIL" ]] || die "cfgutil nicht gefunden: $CFGUTIL → Apple Configurator 2 aus App Store installieren" ok "cfgutil gefunden" # Supervision-Identity if [[ ! -f "$SUPERVISION_IDENTITY_P12" ]]; then die "Supervision-Identity fehlt: $SUPERVISION_IDENTITY_P12 → siehe SUPERVISION-IDENTITY-SETUP.md (einmaliger Setup-Step)" fi ok "Supervision-Identity vorhanden" # Profil [[ -f "$PROFILE_PATH" ]] || die "Profil nicht gefunden: $PROFILE_PATH" if ! plutil -lint "$PROFILE_PATH" >/dev/null 2>&1; then die "Profil ist kein gültiges Plist: $PROFILE_PATH" fi ok "Profil ist gültig" # Connected device(s) UDIDS="$(idevice_id -l 2>/dev/null || true)" if [[ -z "$UDIDS" ]]; then die "Kein iPhone via USB erkannt. Stelle sicher: - USB-C-Kabel ist eingesteckt - iPhone ist entsperrt - 'Diesem Computer vertrauen?' wurde auf dem iPhone bestätigt" fi # Bei mehreren: erste, ggf. später interaktiv erweitern UDID="$(echo "$UDIDS" | head -n1)" log "" info "Gerät: $UDID" DEVICE_NAME="$(ideviceinfo -u "$UDID" -k DeviceName 2>/dev/null || echo "?")" IOS_VERSION="$(ideviceinfo -u "$UDID" -k ProductVersion 2>/dev/null || echo "?")" ACTIVATION="$(ideviceinfo -u "$UDID" -k ActivationState 2>/dev/null || echo "?")" log "Name: $DEVICE_NAME" log "iOS: $IOS_VERSION" log "Activation: $ACTIVATION" # Activation-Check if [[ "$ACTIVATION" != "Activated" ]]; then warn "ActivationState ist '$ACTIVATION' — Backup könnte scheitern" fi # State laden falls --resume state_load "$UDID" if [[ "$RESUME" == 1 ]]; then [[ -n "${STEP_BACKUP_AT:-}" ]] && ok "[resume] Backup bereits erledigt: $STEP_BACKUP_AT" [[ -n "${STEP_SUPERVISE_AT:-}" ]] && ok "[resume] Supervise bereits erledigt: $STEP_SUPERVISE_AT" [[ -n "${STEP_RESTORE_AT:-}" ]] && ok "[resume] Restore bereits erledigt: $STEP_RESTORE_AT" [[ -n "${STEP_PROFILE_AT:-}" ]] && ok "[resume] Profil bereits installiert: $STEP_PROFILE_AT" fi state_set "$UDID" STEP_PREFLIGHT_AT "$(date -Iseconds)" log "" # ------------------------------------------------------------------------------ # Step 2: BACKUP (encrypted) # ------------------------------------------------------------------------------ if [[ "$RESUME" == 1 && -n "${STEP_BACKUP_AT:-}" ]]; then info "[2/5] Backup übersprungen (resume)" else info "[2/5] Backup (idevicebackup2, encrypted)" BACKUP_ROOT="$STATE_DIR/backups/$UDID" mkdir -p "$BACKUP_ROOT" # Encryption-Passwort: generieren wenn nicht vorhanden, persistieren BACKUP_PASSWORD_FILE="${BACKUP_PASSWORD_FILE:-$STATE_DIR/backup-pass-$UDID.txt}" if [[ ! -f "$BACKUP_PASSWORD_FILE" ]]; then if [[ "$DRY_RUN" != 1 ]]; then openssl rand -base64 24 > "$BACKUP_PASSWORD_FILE" chmod 600 "$BACKUP_PASSWORD_FILE" ok "Backup-Passwort generiert: $BACKUP_PASSWORD_FILE" warn "WICHTIG: dieses Passwort wird zum Restore gebraucht. Sicher aufheben." else dim "[dry-run] würde openssl rand -base64 24 > $BACKUP_PASSWORD_FILE" fi fi BACKUP_PASSWORD="$([[ -f "$BACKUP_PASSWORD_FILE" ]] && cat "$BACKUP_PASSWORD_FILE" || echo "DRYRUN")" warn "Backup kann je nach iPhone-Größe 15-60 Minuten dauern." warn "Diese Session NICHT abbrechen, USB-Kabel NICHT abziehen." confirm "Backup jetzt starten?" || die "Abgebrochen vom User" 0 # Encryption aktivieren falls noch nicht # idevicebackup2 will die Backup-Encryption-Konfiguration auf dem Gerät selbst setzen # VERIFY ON DEVICE: ob 'encryption on' idempotent ist oder vorher 'encryption off' nötig run idevicebackup2 -u "$UDID" -i backup encryption on "$BACKUP_PASSWORD" "$BACKUP_ROOT" \ || warn "Encryption-Setup hat returned non-zero — möglicherweise bereits aktiv, fahre fort" # Backup run idevicebackup2 -u "$UDID" -i backup "$BACKUP_ROOT" state_set "$UDID" BACKUP_PATH "$BACKUP_ROOT" state_set "$UDID" BACKUP_PASSWORD_FILE "$BACKUP_PASSWORD_FILE" state_set "$UDID" STEP_BACKUP_AT "$(date -Iseconds)" ok "Backup fertig: $BACKUP_ROOT" fi log "" # ------------------------------------------------------------------------------ # Step 3: SUPERVISE — WIPED das Gerät, setzt Supervised-Flag # ------------------------------------------------------------------------------ if [[ "$RESUME" == 1 && -n "${STEP_SUPERVISE_AT:-}" ]]; then info "[3/5] Supervise übersprungen (resume)" else info "[3/5] Supervise (cfgutil prepare)" warn "DIESER SCHRITT WIPED DAS GERÄT." warn "Backup MUSS in Step 2 erfolgreich gewesen sein." warn "Find-My ist deaktiviert? Apple-ID-Passwort eingegeben? Falls nein: JETZT abbrechen." confirm "Wirklich fortfahren mit Wipe+Supervise?" || die "Abgebrochen vom User" 0 # cfgutil-Aufruf — VERIFY ON DEVICE: exakte Syntax + ECID vs UDID # Apple-Doku-Stand: cfgutil unterstützt --ecid; UDID-Filter via --ecid in 0x-Hex # Für unsupervised+activated devices ist der einfachste Weg: alle connected devices # (es sollte nur eins angeschlossen sein zu diesem Zeitpunkt) run "$CFGUTIL" \ --foreach \ prepare \ --supervised \ --organization-name "ReBreak" \ --supervisor-host-certs "$SUPERVISION_IDENTITY_P12" state_set "$UDID" STEP_SUPERVISE_AT "$(date -Iseconds)" ok "Supervise-Aufruf abgesetzt. Gerät reboots gerade — warte auf Wieder-Verbindung." fi log "" # ------------------------------------------------------------------------------ # Step 4: WAIT FOR RECONNECT + RESTORE # ------------------------------------------------------------------------------ if [[ "$RESUME" == 1 && -n "${STEP_RESTORE_AT:-}" ]]; then info "[4/5] Restore übersprungen (resume)" else info "[4/5] Restore (warte auf Re-Verbindung, dann idevicebackup2 restore)" if [[ "$DRY_RUN" != 1 ]]; then log "Warte auf iPhone-Reconnect (max 5 min)..." for i in $(seq 1 60); do if idevice_id -l 2>/dev/null | grep -q "$UDID"; then ok "iPhone ist wieder verbunden" break fi sleep 5 [[ $i -eq 60 ]] && die "Timeout: iPhone nicht innerhalb 5 min wieder verbunden" done # iOS-Setup-Assistant muss der User auf dem iPhone bis zum Punkt "Vom Backup wiederherstellen?" # bringen — oder wir restoren direkt via libimobiledevice (was wir hier tun) # VERIFY ON DEVICE: ob restore direkt nach Wipe geht (vor Setup-Assistant) oder # erst nach Setup-Assistant + initial-activation warn "iPhone zeigt jetzt 'Hallo'/Setup-Assistant." warn "Folge der Anleitung in README.md → 'Post-Supervise iPhone-Setup'." warn "Sobald iPhone die 'Mit Computer verbinden'-Stage erreicht: hier weiter." confirm "iPhone ist im Recovery-Setup-Stadium und bereit für Restore?" \ || die "Abgebrochen vom User" 0 fi BACKUP_PASSWORD="$(cat "$BACKUP_PASSWORD_FILE")" # VERIFY ON DEVICE: '-i restore' nimmt $BACKUP_ROOT als positional arg # Encryption-Passwort wird via stdin oder Env erwartet — idevicebackup2 v1.3+ unterstützt --password run idevicebackup2 -u "$UDID" -i restore \ --system --reboot \ --password "$BACKUP_PASSWORD" \ "$BACKUP_PATH" state_set "$UDID" STEP_RESTORE_AT "$(date -Iseconds)" ok "Restore abgesetzt. iPhone reboots." fi log "" # ------------------------------------------------------------------------------ # Step 5: INSTALL PROFIL + VERIFY # ------------------------------------------------------------------------------ info "[5/5] Profil installieren + Verify" if [[ "$DRY_RUN" != 1 ]]; then log "Warte auf iPhone-Reconnect post-restore (max 10 min)..." for i in $(seq 1 120); do if idevice_id -l 2>/dev/null | grep -q "$UDID"; then ok "iPhone ist wieder verbunden" break fi sleep 5 [[ $i -eq 120 ]] && die "Timeout: iPhone nicht innerhalb 10 min wieder verbunden" done warn "User muss iOS jetzt entsperren + Setup-Assistant abschließen falls noch nicht." confirm "iPhone ist entsperrt und im Home-Screen?" || die "Abgebrochen vom User" 0 fi # Profil installieren via cfgutil # VERIFY ON DEVICE: 'cfgutil install-profile ' Syntax run "$CFGUTIL" --foreach install-profile "$PROFILE_PATH" state_set "$UDID" STEP_PROFILE_AT "$(date -Iseconds)" # Verify supervised if [[ "$DRY_RUN" != 1 ]]; then IS_SUPERVISED="$("$CFGUTIL" --foreach get isSupervised 2>/dev/null || echo "?")" log "Supervised-State (cfgutil get isSupervised): $IS_SUPERVISED" fi log "" log "${C_GREEN}═══════════════════════════════════════════════════${C_RESET}" log "${C_GREEN} Bootstrap fertig${C_RESET}" log "${C_GREEN}═══════════════════════════════════════════════════${C_RESET}" log "" log "Manueller Verify auf dem iPhone:" log " 1. Settings → Allgemein → Info → 'Dieses iPhone wird beaufsichtigt' sichtbar?" log " 2. Long-Press auf Rebreak-Icon → kein 'App löschen' mehr?" log " 3. Settings → VPN → Rebreak → Toggle disabled?" log " 4. Settings → Allgemein → VPN, DNS und Gerätemanagement → Profil 'Nicht entfernbar'?" log "" log "Backend-Enrollment (separater Step):" log " → in der Rebreak-App: Profil-Tab → 'Schutz aktivieren' → QR scannen" log "" log "Logs: $LOG_FILE" log "State: $(state_file_for "$UDID")" log "Backup: $BACKUP_PATH" log "Backup-Pass: $BACKUP_PASSWORD_FILE ${C_YELLOW}(sicher aufheben!)${C_RESET}"