Implementiert eigenen MobileBackup2-Restore-Trick zur Supervision-Übernahme von aktivierten iOS-Geräten ohne Factory-Reset. Foundation für DiGA-Phase-G Lock-Layer-Stack (non-removable Apps, non-removable Profiles, OnDemand-VPN- Toggle-Lock) auf Consumer-iPhones. Verifiziert end-to-end auf: - iPhone Air (iPhone18,4, iOS 26.5): TL→ReBreak re-supervise ✅ - Olfa iPhone 14 Pro (iPhone15,3, iOS 26.4.2): TL→ReBreak re-supervise ✅ Key empirische Findings: 1. Find-My-iPhone MUSS off sein (ErrorCode 211 sonst) → Stolen Device Protection (SDP) zwingt FMI an seit iOS 17.3+ 2. SupervisorHostCertificates DARF NICHT in CloudConfigurationDetails sein für fresh-supervise auf activated unsupervised devices (sonst partial-apply) 3. MCInstall.SetCloudConfiguration firet 14002 auf allen activated devices → MobileBackup2-Restore-Trick ist der einzige Weg 4. TL's extracted-embed-bytes != TL's wire-output (Runtime-Mutation) → Verbatim-Kopieren reicht nicht Reverse-Engineering basiert komplett auf: - Apple's public protocol docs (devicemanagement, mobilebackup2 schemas) - libimobiledevice (open-source reference impl) - TL public-distributed binary (interop-RE, legal per US-DMCA-1201 + EU-2009/24) Structure: cmd/supervise/ — main CLI (check, cloud-config, supervise, cert-info, unsupervise) cmd/dump-artifacts/ — diagnostic helper (no device needed) cmd/usbmux-proxy/ — MITM-proxy for TL-traffic-capture (debug) cmd/tl-patcher/ — patches TL's hard-coded usbmuxd path (debug) internal/dlmessage/ — DLMessage wire-protocol (4-byte BE length + plist) internal/mobilebackup2/— mobilebackup2-service impl (BaseVersionExchange, Hello, Restore, ServeFiles + TL-extracted templates) internal/cloudconfig/ — CloudConfigurationDetails.plist builder (cert-less, 25 keys matching TL's runtime-output) internal/cert/ — auto-gen + persist supervisor-cert in ~/.rebreak-supervise/ internal/mcinstall/ — MCInstall.GetCloudConfiguration für state-checks internal/device/ — go-ios DeviceEntry wrapper internal/afclock/ — AFC sync-lock auf /com.apple.itunes.lock_sync internal/notification_proxy/ — PostNotification (syncWillStart/etc) internal/preflight/ — FMI/Activation/OS-version pre-checks internal/supervise/ — End-to-end Flow-Orchestrierung (MobileBackup2 default, MCInstall via REBREAK_FORCE_MCINSTALL=1) Pending für volle Productization (Phase G): - Fresh-supervise direkt-empirisch auf truly-unsupervised iPhone testen (heute Nacht nur durch Inferenz aus TL-Verhalten gestützt) - Auto-MDM-enroll-Step nach Supervise (ConfigurationURL oder cfgutil-style) - DiGA-Onboarding-Flow + Lyra-Coach für FMI/SDP-Disable - Multi-Device-Validation (Modelle, iOS-Versionen) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
325 lines
8.6 KiB
Go
325 lines
8.6 KiB
Go
// Command rebreak-supervise-magic ist die CLI für unsupervised→supervised
|
|
// Migration ohne Datenverlust via Apple MCInstall-Service.
|
|
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"flag"
|
|
"fmt"
|
|
"os"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
ios "github.com/danielpaulus/go-ios/ios"
|
|
|
|
"github.com/raynis/rebreak-supervise-magic/internal/cert"
|
|
"github.com/raynis/rebreak-supervise-magic/internal/device"
|
|
"github.com/raynis/rebreak-supervise-magic/internal/dlmessage"
|
|
"github.com/raynis/rebreak-supervise-magic/internal/mcinstall"
|
|
"github.com/raynis/rebreak-supervise-magic/internal/preflight"
|
|
"github.com/raynis/rebreak-supervise-magic/internal/supervise"
|
|
)
|
|
|
|
func init() {
|
|
// Wire authoritative IsSupervised-check via MCInstall.GetCloudConfiguration.
|
|
device.CheckSupervisedFunc = func(dev ios.DeviceEntry) (bool, error) {
|
|
mc, err := mcinstall.Open(dev)
|
|
if err != nil {
|
|
return false, nil // can't check → assume unsupervised
|
|
}
|
|
defer mc.Close()
|
|
_ = mc.HelloHostIdentifier()
|
|
cfg, err := mc.GetCloudConfiguration()
|
|
if err != nil || cfg == nil {
|
|
return false, nil
|
|
}
|
|
v, _ := cfg["IsSupervised"].(bool)
|
|
return v, nil
|
|
}
|
|
}
|
|
|
|
func dlmsgDebugSet() {
|
|
dlmessage.DebugMode = true
|
|
}
|
|
|
|
const usage = `rebreak-supervise-magic — iPhone/iPad-Supervision ohne Datenverlust
|
|
|
|
Usage:
|
|
rebreak-supervise-magic <command> [flags]
|
|
|
|
Commands:
|
|
check Print device info + supervision status. No writes.
|
|
cert-info Print supervision-cert path + status. No writes.
|
|
cloud-config Read current Cloud-Configuration via MCInstall. No writes.
|
|
supervise SetCloudConfiguration(IsSupervised=true) + reboot.
|
|
unsupervise Reverse-flow — IsSupervised=false + reboot.
|
|
|
|
Global flags:
|
|
-v Verbose / debug output.
|
|
-udid <id> Target specific device (default: first detected).
|
|
-org <name> OrganizationName shown in Settings (default: "ReBreak").
|
|
-dry-run Run all checks + plan but skip writes/reboot.
|
|
-yes Skip confirmation prompt (use in scripts).
|
|
-force Allow supervise on already-supervised devices (re-supervise / change orgname).
|
|
|
|
Examples:
|
|
rebreak-supervise-magic check
|
|
rebreak-supervise-magic cloud-config
|
|
rebreak-supervise-magic supervise -org "ReBreak" -yes
|
|
rebreak-supervise-magic -v -dry-run supervise
|
|
`
|
|
|
|
type cliOpts struct {
|
|
verbose bool
|
|
udid string
|
|
orgName string
|
|
dryRun bool
|
|
yes bool
|
|
force bool
|
|
}
|
|
|
|
func main() {
|
|
if len(os.Args) < 2 {
|
|
fmt.Fprint(os.Stderr, usage)
|
|
os.Exit(2)
|
|
}
|
|
|
|
fs := flag.NewFlagSet("rebreak-supervise-magic", flag.ExitOnError)
|
|
opts := &cliOpts{}
|
|
fs.BoolVar(&opts.verbose, "v", false, "verbose output")
|
|
fs.StringVar(&opts.udid, "udid", "", "target UDID (default: first device)")
|
|
fs.StringVar(&opts.orgName, "org", "ReBreak", "OrganizationName")
|
|
fs.BoolVar(&opts.dryRun, "dry-run", false, "skip writes/reboot")
|
|
fs.BoolVar(&opts.yes, "yes", false, "skip confirm prompt")
|
|
fs.BoolVar(&opts.force, "force", false, "allow supervise on already-supervised devices")
|
|
// Optional DLMessage debug-mode via env
|
|
if os.Getenv("REBREAK_DLMSG_DEBUG") == "1" {
|
|
dlmsgDebugSet()
|
|
}
|
|
if err := fs.Parse(os.Args[1:]); err != nil {
|
|
fmt.Fprintln(os.Stderr, "flag parse error:", err)
|
|
os.Exit(2)
|
|
}
|
|
|
|
positional := fs.Args()
|
|
if len(positional) == 0 {
|
|
fmt.Fprint(os.Stderr, "missing command\n\n")
|
|
fmt.Fprint(os.Stderr, usage)
|
|
os.Exit(2)
|
|
}
|
|
cmd := positional[0]
|
|
|
|
switch cmd {
|
|
case "check":
|
|
exitOnErr(runCheck(opts))
|
|
case "cert-info":
|
|
exitOnErr(runCertInfo(opts))
|
|
case "cloud-config":
|
|
exitOnErr(runCloudConfig(opts))
|
|
case "supervise":
|
|
exitOnErr(runSupervise(opts))
|
|
case "unsupervise":
|
|
exitOnErr(runUnsupervise(opts))
|
|
case "-h", "--help", "help":
|
|
fmt.Print(usage)
|
|
default:
|
|
fmt.Fprintf(os.Stderr, "unknown command: %s\n\n", cmd)
|
|
fmt.Fprint(os.Stderr, usage)
|
|
os.Exit(2)
|
|
}
|
|
}
|
|
|
|
func runCheck(opts *cliOpts) error {
|
|
conn, err := device.Connect(opts.udid)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer conn.Close()
|
|
|
|
res, err := preflight.Run(conn)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
fmt.Println("Device:")
|
|
fmt.Printf(" UDID: %s\n", res.Device.UDID)
|
|
fmt.Printf(" Name: %s\n", res.Device.DeviceName)
|
|
fmt.Printf(" Type: %s\n", res.Device.ProductType)
|
|
fmt.Printf(" iOS: %s\n", res.Device.ProductVersion)
|
|
fmt.Printf(" ActivationState: %s\n", res.Device.ActivationState)
|
|
fmt.Printf(" FindMyEnabled: %v\n", res.Device.FindMyEnabled)
|
|
fmt.Printf(" IsSupervised: %v\n", res.Device.IsSupervised)
|
|
fmt.Println()
|
|
if res.OK {
|
|
fmt.Println("Pre-Flight: ✓ ready for supervise")
|
|
} else {
|
|
fmt.Println("Pre-Flight: ✗ not ready:")
|
|
for _, r := range res.Reasons {
|
|
fmt.Printf(" - %s\n", r)
|
|
}
|
|
return fmt.Errorf("pre-flight failed")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func runCertInfo(opts *cliOpts) error {
|
|
dir := cert.DefaultDir()
|
|
fmt.Printf("Supervision identity directory: %s\n", dir)
|
|
id, err := cert.LoadOrCreate()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fmt.Printf(" Cert (DER): %d bytes\n", len(id.CertDER))
|
|
fmt.Printf(" Key (DER): %d bytes\n", len(id.PrivateKeyDER))
|
|
fmt.Println()
|
|
fmt.Println("Identity ready — usable for supervise command.")
|
|
return nil
|
|
}
|
|
|
|
func runCloudConfig(opts *cliOpts) error {
|
|
conn, err := device.Connect(opts.udid)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer conn.Close()
|
|
|
|
mc, err := mcinstall.Open(conn.Device())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer mc.Close()
|
|
if err := mc.HelloHostIdentifier(); err != nil {
|
|
return err
|
|
}
|
|
cfg, err := mc.GetCloudConfiguration()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if cfg == nil {
|
|
fmt.Println("(no cloud configuration set on device)")
|
|
return nil
|
|
}
|
|
|
|
keys := make([]string, 0, len(cfg))
|
|
for k := range cfg {
|
|
keys = append(keys, k)
|
|
}
|
|
sort.Strings(keys)
|
|
fmt.Println("Cloud Configuration (live from device MCInstall):")
|
|
for _, k := range keys {
|
|
val := cfg[k]
|
|
switch v := val.(type) {
|
|
case []byte:
|
|
fmt.Printf(" %-40s = [%d bytes binary]\n", k, len(v))
|
|
case [][]byte:
|
|
fmt.Printf(" %-40s = [%d certs]\n", k, len(v))
|
|
default:
|
|
fmt.Printf(" %-40s = %v\n", k, v)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func runSupervise(opts *cliOpts) error {
|
|
conn, err := device.Connect(opts.udid)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
res, err := preflight.Run(conn)
|
|
conn.Close()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !res.OK {
|
|
fmt.Println("Pre-Flight failed:")
|
|
for _, r := range res.Reasons {
|
|
fmt.Printf(" - %s\n", r)
|
|
}
|
|
return fmt.Errorf("pre-flight blocking supervise")
|
|
}
|
|
if res.Device.IsSupervised && !opts.force {
|
|
fmt.Printf("Device already supervised (UDID=%s). Use --force to re-supervise (overwrites existing supervisor cert).\n", res.Device.UDID)
|
|
return nil
|
|
}
|
|
|
|
fmt.Printf("About to supervise device:\n %s (%s, iOS %s)\n",
|
|
res.Device.DeviceName, res.Device.ProductType, res.Device.ProductVersion)
|
|
if res.Device.IsSupervised {
|
|
fmt.Println("⚠ Device IS already supervised — will OVERWRITE existing supervisor cert with our own.")
|
|
fmt.Println(" Existing cert cannot be restored without external tool (TechLockdown / Apple Configurator).")
|
|
}
|
|
fmt.Printf("OrganizationName: %s\n", opts.orgName)
|
|
fmt.Println("Device will reboot. No data loss expected.")
|
|
if !opts.yes && !opts.dryRun {
|
|
if !confirm("Continue? [y/N]: ") {
|
|
return fmt.Errorf("aborted by user")
|
|
}
|
|
}
|
|
|
|
return supervise.Supervise(res.Device.UDID, supervise.Options{
|
|
OrgName: opts.orgName,
|
|
DryRun: opts.dryRun,
|
|
Verbose: opts.verbose,
|
|
BackupPath: defaultBackupPath(res.Device.UDID),
|
|
})
|
|
}
|
|
|
|
func defaultBackupPath(udid string) string {
|
|
short := udid
|
|
if len(short) > 12 {
|
|
short = short[:12]
|
|
}
|
|
ts := time.Now().UTC().Format("20060102-150405")
|
|
return fmt.Sprintf("/tmp/rebreak-supervise-backup-%s-%s.json", short, ts)
|
|
}
|
|
|
|
func runUnsupervise(opts *cliOpts) error {
|
|
conn, err := device.Connect(opts.udid)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
res, err := preflight.Run(conn)
|
|
conn.Close()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !res.Device.IsSupervised {
|
|
fmt.Printf("Device already un-supervised (UDID=%s). Nothing to do.\n", res.Device.UDID)
|
|
return nil
|
|
}
|
|
|
|
fmt.Printf("About to UN-supervise device:\n %s (%s, iOS %s)\n",
|
|
res.Device.DeviceName, res.Device.ProductType, res.Device.ProductVersion)
|
|
if !opts.yes && !opts.dryRun {
|
|
if !confirm("Continue? [y/N]: ") {
|
|
return fmt.Errorf("aborted by user")
|
|
}
|
|
}
|
|
return supervise.Unsupervise(res.Device.UDID, supervise.Options{
|
|
OrgName: opts.orgName,
|
|
DryRun: opts.dryRun,
|
|
Verbose: opts.verbose,
|
|
})
|
|
}
|
|
|
|
func confirm(prompt string) bool {
|
|
fmt.Print(prompt)
|
|
reader := bufio.NewReader(os.Stdin)
|
|
line, _ := reader.ReadString('\n')
|
|
line = strings.TrimSpace(strings.ToLower(line))
|
|
return line == "y" || line == "yes"
|
|
}
|
|
|
|
func exitOnErr(err error) {
|
|
if err == nil {
|
|
return
|
|
}
|
|
if !strings.HasPrefix(err.Error(), "aborted") {
|
|
fmt.Fprintln(os.Stderr, "ERROR:", err)
|
|
}
|
|
os.Exit(1)
|
|
}
|
|
|