feat(supervise-magic): TechLockdown-clone v1 — supervise iPhones without erase

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>
This commit is contained in:
chahinebrini 2026-05-27 01:55:10 +02:00
parent 1ae86c03f4
commit 01374c426e
30 changed files with 3598 additions and 0 deletions

4
ops/mdm/supervise-magic/.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
/bin/
*.test
*.out
.DS_Store

View File

@ -0,0 +1,45 @@
.PHONY: build proxy test clean install
BINARY := rebreak-supervise-magic
PROXY := rebreak-usbmux-proxy
BIN_DIR := bin
build:
@mkdir -p $(BIN_DIR)
go build -o $(BIN_DIR)/$(BINARY) ./cmd/supervise
proxy:
@mkdir -p $(BIN_DIR)
go build -o $(BIN_DIR)/$(PROXY) ./cmd/usbmux-proxy
patcher:
@mkdir -p $(BIN_DIR)
go build -o $(BIN_DIR)/rebreak-tl-patcher ./cmd/tl-patcher
all: build proxy patcher
build-universal: build-arm64 build-amd64
lipo -create -output $(BIN_DIR)/$(BINARY)-universal \
$(BIN_DIR)/$(BINARY)-arm64 $(BIN_DIR)/$(BINARY)-amd64
@echo "Universal binary at $(BIN_DIR)/$(BINARY)-universal"
build-arm64:
@mkdir -p $(BIN_DIR)
GOOS=darwin GOARCH=arm64 go build -o $(BIN_DIR)/$(BINARY)-arm64 ./cmd/supervise
build-amd64:
@mkdir -p $(BIN_DIR)
GOOS=darwin GOARCH=amd64 go build -o $(BIN_DIR)/$(BINARY)-amd64 ./cmd/supervise
test:
go test ./...
clean:
rm -rf $(BIN_DIR)
install: build
cp $(BIN_DIR)/$(BINARY) $(HOME)/.local/bin/$(BINARY)
@echo "Installed to $(HOME)/.local/bin/$(BINARY)"
tidy:
go mod tidy

View File

@ -0,0 +1,80 @@
# ReBreak Supervise Magic
Go-CLI das ein **unsupervised iPhone/iPad** ohne Datenverlust in den supervised-Zustand überführt.
Mechanismus: Direktes Schreiben von `Library/ConfigurationProfiles/CloudConfigurationDetails.plist` via Apple's Lockdown-Protokoll (`gidevice`-Library, kein Apple Configurator nötig). Reverse-engineered aus TechLockdown's Supervise-Tool (2026-05-26).
**Unterschied zum Legacy [`bootstrap-tool/rebreak-supervise.sh`](../bootstrap-tool/rebreak-supervise.sh):** Kein Backup-Restore-Round-Trip, kein Erase. Nur Plist-Inject + Reboot.
## Status
🚧 Phase 1 — CLI MVP. Phase 2 (Mac-UI-Wrapper) geplant.
## Voraussetzungen
| Was | Wie |
|---|---|
| Go 1.22+ | `brew install go` |
| iPhone via USB | USB-C-Kabel, „Diesem Computer vertrauen" geklickt |
| Find My iPhone aus | `Settings → [Name] → Wo ist? → Mein iPhone suchen → Aus` |
| iPhone-Bildschirm entsperrt | Damit Trust-Prompt akzeptiert werden kann |
## Build
```bash
make tidy # einmalig: dependencies auflösen
make build # → bin/rebreak-supervise-magic
```
Universal-Binary (Intel + ARM):
```bash
make build-universal
```
## Usage
```bash
# Status checken
./bin/rebreak-supervise-magic check
# Supervise auslösen (interaktiv, mit Confirmations)
./bin/rebreak-supervise-magic supervise
# Verbose-Mode für Debug
./bin/rebreak-supervise-magic -v supervise
# Reverse: Supervised → Unsupervised (Plist zurückschreiben)
./bin/rebreak-supervise-magic unsupervise
```
## Mechanismus im Detail
1. **Pair + Trust** — gidevice verbindet sich zu `lockdownd` via `usbmuxd`. Falls iPhone noch nicht trusted, wird Trust-Prompt am iPhone gezeigt.
2. **Pre-Flight** — UDID + ProductType + OS-Version dumpen. Find-My-iPhone-Status checken (muss aus sein).
3. **Plist-Build**`CloudConfigurationDetails`-Go-struct → XML-Plist via `howett.net/plist`. Felder: `IsSupervised=true`, `IsMDMUnremovable=true`, `OrganizationName="ReBreak"`, `EscrowBag` optional.
4. **Plist-Write** — Via `MCInstall`- oder `AFC`-Service ins iPhone-Filesystem-Container `Library/ConfigurationProfiles/CloudConfigurationDetails.plist`.
5. **Reboot** — Lockdown-Reboot-Command. Wait-for-Re-Connect-Loop.
6. **Verify** — IsSupervised-Status via Lockdown-Query erneut lesen. Supervision sollte jetzt aktiv sein.
7. **Settings-Check (manual)** — iPhone-Settings → „Verwaltet von ReBreak" sichtbar.
## Sicherheits-Hinweise
- Tool macht USB-only, kein Network-Channel.
- iPhone behält alle Apps, Daten, Login-States (kein Backup-Restore-Round-Trip).
- Reverse-Operation (`unsupervise`) wirkt symmetrisch — Plist-Felder zurückschreiben + Reboot.
- Apple-Compliance: gleiche Public-API wie iMazing/TechLockdown nutzen. Siehe Memory `techlockdown-reverse-engineering`.
## Apple-Walls die wir damit umgehen
- ✅ Erase-Required für Supervision via Apple Configurator
- ✅ ABM/DEP-Enrollment für Supervised-Mode
## Apple-Walls die bleiben
- ❌ FMI muss vorher aus (Activation-Lock-Constraint)
- ❌ iOS-Update kann das jederzeit patchen (bisher 5+ Jahre nicht)
## Lizenz & Distribution
Phase 1: nur lokal nutzbar (Personal + Friends). Keine Mac-App-Store-Distribution geplant. Notarization nur falls wir Phase 2 (Mac-UI) als Distribution-Tool releasen.

View File

@ -0,0 +1,48 @@
package main
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/x509"
"crypto/x509/pkix"
"fmt"
"math/big"
"os"
"time"
"github.com/raynis/rebreak-supervise-magic/internal/cloudconfig"
"github.com/raynis/rebreak-supervise-magic/internal/mobilebackup2"
)
func main() {
// Generate fake supervisor cert for size-comparison only
priv, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
tmpl := &x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{CommonName: "ReBreak Supervisor"},
NotBefore: time.Now(),
NotAfter: time.Now().Add(10 * 365 * 24 * time.Hour),
}
certDER, _ := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &priv.PublicKey, priv)
cloudCfg, err := cloudconfig.Build(cloudconfig.BuildOptions{
OrganizationName: "ReBreak",
OrganizationEmail: "hello@rebreak.org",
SupervisorCert: certDER,
})
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
os.WriteFile("/tmp/ours-cloudconfig.plist", cloudCfg, 0644)
fmt.Printf("Our CloudConfig: %d bytes\n", len(cloudCfg))
mDB, err := mobilebackup2.BuildManifestDB(mobilebackup2.DefaultRestoreEntries(int64(len(cloudCfg))))
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
os.WriteFile("/tmp/ours-manifest.db", mDB, 0644)
fmt.Printf("Our Manifest.db: %d bytes\n", len(mDB))
}

View File

@ -0,0 +1,324 @@
// 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)
}

View File

@ -0,0 +1,98 @@
// Command tl-patcher: copies TechLockdown's Supervise_bin and binary-patches
// the hard-coded "/var/run/usbmuxd" path to "/tmp/mitm-usbmux" (exact 16 bytes).
// Then ad-hoc re-signs so macOS will allow execution.
//
// Result: a patched binary that connects to our proxy unix-socket instead of
// the real usbmuxd daemon — without needing sudo or env-vars.
//
// Usage:
//
// ./bin/rebreak-tl-patcher
// # Then run: /tmp/Supervise_bin_proxy
package main
import (
"bytes"
"fmt"
"io"
"os"
"os/exec"
)
const (
defaultSrc = "/Users/chahinebrini/Downloads/TechLockdown-supervise-mac-arm64.app/Contents/MacOS/Supervise_bin"
defaultDst = "/tmp/Supervise_bin_proxy"
origPath = "/var/run/usbmuxd" // 16 bytes
patchedPath = "/tmp/mitm-usbmux" // 16 bytes ✓
)
func main() {
src := defaultSrc
dst := defaultDst
if len(os.Args) > 1 {
src = os.Args[1]
}
if len(os.Args) > 2 {
dst = os.Args[2]
}
if len(origPath) != len(patchedPath) {
fmt.Fprintf(os.Stderr, "ERROR: path lengths must match — orig=%d patched=%d\n", len(origPath), len(patchedPath))
os.Exit(1)
}
fmt.Printf("Reading %s ...\n", src)
data, err := os.ReadFile(src)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
count := bytes.Count(data, []byte(origPath))
fmt.Printf("Found %d occurrence(s) of %q\n", count, origPath)
if count == 0 {
fmt.Fprintln(os.Stderr, "no patch needed?")
os.Exit(1)
}
patched := bytes.ReplaceAll(data, []byte(origPath), []byte(patchedPath))
fmt.Printf("Writing patched binary to %s ...\n", dst)
if err := os.WriteFile(dst, patched, 0o755); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
// Remove quarantine xattr (otherwise macOS blocks unsigned launch)
fmt.Println("Removing quarantine xattr ...")
exec.Command("xattr", "-d", "com.apple.quarantine", dst).Run() // ignore err
// Ad-hoc re-sign (otherwise macOS refuses to launch patched binary)
fmt.Println("Removing original signature ...")
out, err := exec.Command("codesign", "--remove-signature", dst).CombinedOutput()
if err != nil {
fmt.Printf(" warn: codesign remove: %v %s\n", err, out)
}
fmt.Println("Ad-hoc re-signing ...")
out, err = exec.Command("codesign", "-f", "-s", "-", dst).CombinedOutput()
if err != nil {
fmt.Printf(" warn: codesign sign: %v %s\n", err, out)
}
// Show how to launch
stat, _ := os.Stat(dst)
size := int64(0)
if stat != nil {
size = stat.Size()
}
fmt.Printf("\nDone. Patched binary: %s (%d bytes)\n", dst, size)
fmt.Printf("Path patch: %q → %q\n", origPath, patchedPath)
fmt.Println()
fmt.Println("Next steps:")
fmt.Println(" 1. In Terminal 1: ./bin/rebreak-usbmux-proxy -proxy /tmp/mitm-usbmux")
fmt.Println(" 2. In Terminal 2: " + dst)
fmt.Println(" 3. If macOS blocks: System Settings → Privacy & Security → 'Allow anyway'")
}
// suppress unused-import warning
var _ = io.Copy

View File

@ -0,0 +1,261 @@
// Command usbmux-proxy ist ein Man-in-the-Middle Unix-socket proxy zwischen
// einem Client (z.B. TechLockdown) und Apple's `/var/run/usbmuxd`-Daemon.
//
// Architektur:
//
// Client --[unix socket /tmp/usbmux-proxy]--> proxy --[unix socket /var/run/usbmuxd]--> usbmuxd
// |
// v
// /tmp/usbmux-capture-*.log
//
// Usage:
//
// # Terminal 1 — start proxy
// ./bin/rebreak-usbmux-proxy
//
// # Terminal 2 — run TL through proxy (kein sudo nötig!)
// USBMUXD_SOCKET_ADDRESS=unix:///tmp/usbmux-proxy \
// open ~/Downloads/TechLockdown-supervise-mac-arm64.app
//
// Log-Output: hex+ascii dump für jeden gesendeten/empfangenen byte.
// Plus: usbmux-frame-parsing (16-byte header + payload), plist-detection.
package main
import (
"bytes"
"encoding/binary"
"flag"
"fmt"
"io"
"net"
"os"
"strings"
"sync"
"sync/atomic"
"time"
"howett.net/plist"
)
const (
defaultProxyPath = "/tmp/usbmux-proxy"
defaultRealPath = "/var/run/usbmuxd"
)
var (
flagProxyPath = flag.String("proxy", defaultProxyPath, "unix-socket-pfad für proxy")
flagRealPath = flag.String("real", defaultRealPath, "unix-socket-pfad zum echten usbmuxd")
flagLogPath = flag.String("log", "", "log-output (default: /tmp/usbmux-capture-TIMESTAMP.log)")
flagQuiet = flag.Bool("q", false, "kein stdout output, nur file-log")
sessionCounter atomic.Uint64
)
func main() {
flag.Parse()
logPath := *flagLogPath
if logPath == "" {
logPath = fmt.Sprintf("/tmp/usbmux-capture-%s.log", time.Now().Format("20060102-150405"))
}
logFile, err := os.Create(logPath)
if err != nil {
fmt.Fprintln(os.Stderr, "log create:", err)
os.Exit(1)
}
defer logFile.Close()
logger := &mitm{
out: logFile,
quiet: *flagQuiet,
}
logger.printf("== usbmux-proxy starting — proxy=%s → real=%s, log=%s\n", *flagProxyPath, *flagRealPath, logPath)
// Cleanup any stale socket
os.Remove(*flagProxyPath)
listener, err := net.Listen("unix", *flagProxyPath)
if err != nil {
fmt.Fprintln(os.Stderr, "listen:", err)
os.Exit(1)
}
defer listener.Close()
defer os.Remove(*flagProxyPath)
logger.printf("== listening — point USBMUXD_SOCKET_ADDRESS=unix://%s\n", *flagProxyPath)
for {
clientConn, err := listener.Accept()
if err != nil {
logger.printf("accept error: %v\n", err)
continue
}
sessionID := sessionCounter.Add(1)
go handleSession(sessionID, clientConn, logger)
}
}
type mitm struct {
mu sync.Mutex
out *os.File
quiet bool
}
func (m *mitm) printf(format string, args ...interface{}) {
m.mu.Lock()
defer m.mu.Unlock()
s := fmt.Sprintf(format, args...)
m.out.WriteString(s)
if !m.quiet {
os.Stderr.WriteString(s)
}
}
func handleSession(sessionID uint64, client net.Conn, logger *mitm) {
defer client.Close()
logger.printf("\n=== SESSION %d START (client connected) ===\n", sessionID)
daemon, err := net.Dial("unix", *flagRealPath)
if err != nil {
logger.printf("[session %d] dial daemon: %v\n", sessionID, err)
return
}
defer daemon.Close()
var wg sync.WaitGroup
wg.Add(2)
// Client → Daemon
go func() {
defer wg.Done()
pipeWithLog(sessionID, "C→D", client, daemon, logger)
daemon.Close() // signal other side
}()
// Daemon → Client
go func() {
defer wg.Done()
pipeWithLog(sessionID, "D→C", daemon, client, logger)
client.Close()
}()
wg.Wait()
logger.printf("=== SESSION %d END ===\n", sessionID)
}
// pipeWithLog liest from src + writes to dst, logging each chunk.
func pipeWithLog(sessionID uint64, dir string, src, dst net.Conn, logger *mitm) {
buf := make([]byte, 64*1024)
for {
n, err := src.Read(buf)
if n > 0 {
chunk := buf[:n]
logger.printf("\n--- [session %d] %s | %d bytes | %s ---\n", sessionID, dir, n, time.Now().Format("15:04:05.000"))
dumpChunk(chunk, logger)
if _, werr := dst.Write(chunk); werr != nil {
logger.printf("write err: %v\n", werr)
return
}
}
if err != nil {
if err != io.EOF {
logger.printf("read err: %v\n", err)
}
return
}
}
}
// dumpChunk format-aware: tries to detect usbmux-frame, plist, etc.
// Falls back to hex+ascii.
func dumpChunk(data []byte, logger *mitm) {
// 1) Try usbmux-frame parsing (16-byte header + payload)
if len(data) >= 16 {
length := binary.LittleEndian.Uint32(data[0:4])
version := binary.LittleEndian.Uint32(data[4:8])
request := binary.LittleEndian.Uint32(data[8:12])
tag := binary.LittleEndian.Uint32(data[12:16])
if length > 16 && int(length) <= len(data) && version <= 2 && request <= 100 {
payload := data[16:length]
logger.printf(" [usbmux-frame] len=%d ver=%d req=%d tag=%d payload=%d bytes\n",
length, version, request, tag, len(payload))
tryParsePlist(payload, logger)
// remaining bytes
if int(length) < len(data) {
logger.printf(" [+%d bytes after frame]\n", len(data)-int(length))
dumpHex(data[length:], logger, 4)
}
return
}
}
// 2) Try mobilebackup2 / lockdown 4-byte length-prefix + plist
if len(data) >= 4 {
length := binary.BigEndian.Uint32(data[0:4])
if int(length)+4 <= len(data) && length > 0 && length < 1024*1024 {
payload := data[4 : 4+length]
logger.printf(" [length-prefixed] len=%d payload=%d bytes\n", length, len(payload))
tryParsePlist(payload, logger)
if int(length)+4 < len(data) {
logger.printf(" [+%d bytes after frame]\n", len(data)-int(length)-4)
dumpHex(data[int(length)+4:], logger, 4)
}
return
}
}
// 3) Raw hex dump
dumpHex(data, logger, 4)
}
// tryParsePlist — try XML or binary plist parse, log readable form.
func tryParsePlist(data []byte, logger *mitm) {
if bytes.HasPrefix(data, []byte("<?xml")) || bytes.HasPrefix(data, []byte("bplist00")) {
var parsed interface{}
if _, err := plist.Unmarshal(data, &parsed); err == nil {
// Re-encode as XML for readability
var buf bytes.Buffer
enc := plist.NewEncoderForFormat(&buf, plist.XMLFormat)
enc.Indent(" ")
if enc.Encode(parsed) == nil {
logger.printf(" [plist parsed]:\n%s\n", indent(buf.String(), " "))
return
}
}
}
dumpHex(data, logger, 4)
}
// dumpHex — print hex+ascii dump.
func dumpHex(data []byte, logger *mitm, indentSpaces int) {
const lineLen = 16
prefix := strings.Repeat(" ", indentSpaces)
for i := 0; i < len(data); i += lineLen {
end := i + lineLen
if end > len(data) {
end = len(data)
}
hex := ""
ascii := ""
for j := i; j < end; j++ {
hex += fmt.Sprintf("%02x ", data[j])
if data[j] >= 32 && data[j] < 127 {
ascii += string(data[j])
} else {
ascii += "."
}
}
hex += strings.Repeat(" ", lineLen-(end-i))
logger.printf("%s%04x: %s | %s\n", prefix, i, hex, ascii)
}
}
func indent(s, pad string) string {
lines := strings.Split(s, "\n")
for i, line := range lines {
if line != "" {
lines[i] = pad + line
}
}
return strings.Join(lines, "\n")
}

View File

@ -0,0 +1,35 @@
module github.com/raynis/rebreak-supervise-magic
go 1.22.0
require (
github.com/danielpaulus/go-ios v1.0.213
github.com/google/uuid v1.6.0
howett.net/plist v1.0.1
modernc.org/sqlite v1.34.5
)
require (
github.com/Masterminds/semver v1.5.0 // indirect
github.com/cenkalti/backoff v2.2.1+incompatible // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/grandcat/zeroconf v1.0.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/miekg/dns v1.1.57 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 // indirect
golang.org/x/crypto v0.24.0 // indirect
golang.org/x/exp v0.0.0-20230725093048-515e97ebf090 // indirect
golang.org/x/mod v0.17.0 // indirect
golang.org/x/net v0.26.0 // indirect
golang.org/x/sync v0.7.0 // indirect
golang.org/x/sys v0.22.0 // indirect
golang.org/x/text v0.16.0 // indirect
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
modernc.org/libc v1.55.3 // indirect
modernc.org/mathutil v1.6.0 // indirect
modernc.org/memory v1.8.0 // indirect
software.sslmate.com/src/go-pkcs12 v0.2.0 // indirect
)

View File

@ -0,0 +1,112 @@
github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4=
github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
github.com/danielpaulus/go-ios v1.0.213 h1:osoQQEqFlBrYtSCrqAljWIYcm7FAvDMUWSeCNoz31vw=
github.com/danielpaulus/go-ios v1.0.213/go.mod h1:f5q5S4XJT53AA8cdgp3rLA41YaIpyaDg+w8aURzLNhM=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo=
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/grandcat/zeroconf v1.0.0 h1:uHhahLBKqwWBV6WZUDAT71044vwOTL+McW0mBJvo6kE=
github.com/grandcat/zeroconf v1.0.0/go.mod h1:lTKmG1zh86XyCoUeIHSA4FJMBwCJiQmGfcP2PdzytEs=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/miekg/dns v1.1.27/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
github.com/miekg/dns v1.1.57 h1:Jzi7ApEIzwEPLHWRcafCN9LZSBbqQpxjt/wpgvg7wcM=
github.com/miekg/dns v1.1.57/go.mod h1:uqRjCRUuEAA6qsOiJvDd+CFo/vW+y5WR6SNmHE55hZk=
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 h1:CCriYyAfq1Br1aIYettdHZTy8mBTIPo7We18TuO/bak=
go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI=
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
golang.org/x/exp v0.0.0-20230725093048-515e97ebf090 h1:Di6/M8l0O2lCLc6VVRWhgCiApHV8MnQurBnFSHsQtNY=
golang.org/x/exp v0.0.0-20230725093048-515e97ebf090/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
howett.net/plist v1.0.1 h1:37GdZ8tP09Q35o9ych3ehygcsL+HqKSwzctveSlarvM=
howett.net/plist v1.0.1/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g=
modernc.org/cc/v4 v4.21.4 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ=
modernc.org/cc/v4 v4.21.4/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ=
modernc.org/ccgo/v4 v4.19.2 h1:lwQZgvboKD0jBwdaeVCTouxhxAyN6iawF3STraAal8Y=
modernc.org/ccgo/v4 v4.19.2/go.mod h1:ysS3mxiMV38XGRTTcgo0DQTeTmAO4oCmJl1nX9VFI3s=
modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
modernc.org/gc/v2 v2.4.1 h1:9cNzOqPyMJBvrUipmynX0ZohMhcxPtMccYgGOJdOiBw=
modernc.org/gc/v2 v2.4.1/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU=
modernc.org/libc v1.55.3 h1:AzcW1mhlPNrRtjS5sS+eW2ISCgSOLLNyFzRh/V3Qj/U=
modernc.org/libc v1.55.3/go.mod h1:qFXepLhz+JjFThQ4kzwzOjA/y/artDeg+pcYnY+Q83w=
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E=
modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU=
modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc=
modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss=
modernc.org/sqlite v1.34.5 h1:Bb6SR13/fjp15jt70CL4f18JIN7p7dnMExd+UFnF15g=
modernc.org/sqlite v1.34.5/go.mod h1:YLuNmX9NKs8wRNK2ko1LW1NGYcc9FkBO69JOt1AR9JE=
modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
software.sslmate.com/src/go-pkcs12 v0.2.0 h1:nlFkj7bTysH6VkC4fGphtjXRbezREPgrHuJG20hBGPE=
software.sslmate.com/src/go-pkcs12 v0.2.0/go.mod h1:23rNcYsMabIc1otwLpTkCCPwUq6kQsTyowttG/as0kQ=

View File

@ -0,0 +1,65 @@
// Package afclock implementiert das AFC-Sync-Lock-File-Pattern.
// iOS erwartet vor iTunes-style sync dass der Client `/com.apple.itunes.lock_sync`
// via AFC öffnet + lockt. Ohne diesen Lock interpretiert iOS den Restore
// als incomplete external-process und applied keine cloud-config.
//
// Reverse-engineered aus TechLockdown safesurfer.go:288 (AfcFile.Lock-Call).
package afclock
import (
"fmt"
ios "github.com/danielpaulus/go-ios/ios"
"github.com/danielpaulus/go-ios/ios/afc"
)
// SyncLockPath — der genaue Pfad den iOS erwartet (aus TL-binary-strings).
const SyncLockPath = "/com.apple.itunes.lock_sync"
// SyncLock kapselt einen offenen AFC-Lock auf der sync-file.
type SyncLock struct {
client *afc.Client
file *afc.File
}
// Acquire öffnet AFC + das sync-lock-file + erzeugt exclusive lock.
// Caller MUSS Release() callen wenn fertig.
func Acquire(device ios.DeviceEntry) (*SyncLock, error) {
client, err := afc.New(device)
if err != nil {
return nil, fmt.Errorf("afclock: open AFC: %w", err)
}
// Versuche das sync-lock file zu öffnen.
// READ_WRITE_CREATE = mode 0x2 — creates if not exists.
file, err := client.Open(SyncLockPath, afc.READ_WRITE_CREATE)
if err != nil {
client.Close()
return nil, fmt.Errorf("afclock: open %s: %w", SyncLockPath, err)
}
// Note: go-ios's afc.File doesn't expose Lock() directly. Apple's AFC
// has a Lock operation but go-ios doesn't wrap it. We rely on the
// open-file-handle alone signaling iOS that sync is in progress.
// TL calls Lock() explicitly — wenn das nicht reicht, müssten wir
// das AFC-Protocol-Level Lock-message direkt senden (8-byte op +
// lock-type-flag).
return &SyncLock{
client: client,
file: file,
}, nil
}
// Release schließt das file-handle + AFC-service. Signal an iOS dass sync done.
func (s *SyncLock) Release() error {
if s.file != nil {
s.file.Close()
s.file = nil
}
if s.client != nil {
s.client.Close()
s.client = nil
}
return nil
}

View File

@ -0,0 +1,131 @@
// Package cert managed die Supervision-Identity (Cert + Private-Key).
// Cert wird einmal generiert via go-ios und persistent unter
// ~/.rebreak-supervise/ abgelegt. Default-Path matched die existing
// Bootstrap-Tool-Konvention.
package cert
import (
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"errors"
"fmt"
"os"
"path/filepath"
ios "github.com/danielpaulus/go-ios/ios"
)
// Identity ist unsere komplette Supervision-Identity.
// CertDER ist das was wir SetCloudConfiguration als SupervisorHostCertificates übergeben.
type Identity struct {
CertDER []byte
PrivateKeyDER []byte
}
// DefaultDir matches die Konvention aus dem alten bootstrap-tool.
func DefaultDir() string {
home, _ := os.UserHomeDir()
return filepath.Join(home, ".rebreak-supervise")
}
// LoadOrCreate lädt eine existierende Identity aus dem default-dir, oder
// generiert eine neue + speichert sie. Idempotent: zweiter Call returnt
// die gleiche Identity wie der erste.
func LoadOrCreate() (*Identity, error) {
dir := DefaultDir()
certPath := filepath.Join(dir, "supervision-cert.pem")
keyPath := filepath.Join(dir, "supervision-key.pem")
id, err := load(certPath, keyPath)
if err == nil {
return id, nil
}
if !os.IsNotExist(err) && !errors.Is(err, errCorruptIdentity) {
return nil, fmt.Errorf("cert: load existing: %w", err)
}
// Generieren
id, err = generate()
if err != nil {
return nil, fmt.Errorf("cert: generate: %w", err)
}
if err := id.save(certPath, keyPath); err != nil {
return nil, fmt.Errorf("cert: save: %w", err)
}
return id, nil
}
// load aus PEM-Files.
func load(certPath, keyPath string) (*Identity, error) {
certPEM, err := os.ReadFile(certPath)
if err != nil {
return nil, err
}
keyPEM, err := os.ReadFile(keyPath)
if err != nil {
return nil, err
}
certBlock, _ := pem.Decode(certPEM)
keyBlock, _ := pem.Decode(keyPEM)
if certBlock == nil || keyBlock == nil {
return nil, errCorruptIdentity
}
return &Identity{
CertDER: certBlock.Bytes,
PrivateKeyDER: keyBlock.Bytes,
}, nil
}
// generate via go-ios's CreateDERFormattedSupervisionCert.
func generate() (*Identity, error) {
ca, err := ios.CreateDERFormattedSupervisionCert()
if err != nil {
return nil, err
}
return &Identity{
CertDER: ca.CertDER,
PrivateKeyDER: ca.PrivateKeyDER,
}, nil
}
func (id *Identity) save(certPath, keyPath string) error {
dir := filepath.Dir(certPath)
if err := os.MkdirAll(dir, 0o700); err != nil {
return err
}
certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: id.CertDER})
keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: id.PrivateKeyDER})
if err := os.WriteFile(certPath, certPEM, 0o600); err != nil {
return err
}
if err := os.WriteFile(keyPath, keyPEM, 0o600); err != nil {
return err
}
return nil
}
var errCorruptIdentity = errors.New("cert: identity files corrupt — delete + regenerate")
// Parse decodes the DER-encoded cert + private key for use with Escalate.
func (id *Identity) Parse() (*x509.Certificate, *rsa.PrivateKey, error) {
cert, err := x509.ParseCertificate(id.CertDER)
if err != nil {
return nil, nil, fmt.Errorf("cert: parse cert DER: %w", err)
}
// PrivateKey ist von go-ios als PKCS#1 DER encoded
key, err := x509.ParsePKCS1PrivateKey(id.PrivateKeyDER)
if err != nil {
// Fallback: PKCS#8
k8, err2 := x509.ParsePKCS8PrivateKey(id.PrivateKeyDER)
if err2 != nil {
return nil, nil, fmt.Errorf("cert: parse key DER (PKCS1 + PKCS8 failed): %w / %v", err, err2)
}
rsaKey, ok := k8.(*rsa.PrivateKey)
if !ok {
return nil, nil, fmt.Errorf("cert: key is not RSA")
}
key = rsaKey
}
return cert, key, nil
}

View File

@ -0,0 +1,3 @@
// Package cloudconfig — der eigentliche Inhalt ist in writer.go.
// Diese Datei ist Legacy aus Phase 1 (gidevice-Era) und absichtlich leer.
package cloudconfig

View File

@ -0,0 +1,112 @@
// Package cloudconfig baut die CloudConfigurationDetails.plist die
// während MobileBackup2-Restore ins iPhone-Filesystem injiziert wird.
//
// Schema verifiziert empirisch aus TechLockdown's extracted Manifest.db
// + live cloud-config dumps. Apple's DEP-fields-Set (ConfigurationSource=0
// + Org-metadata) ist der Bypass-Mechanismus für 14002-Validation auf
// already-supervised devices.
package cloudconfig
import (
"bytes"
"fmt"
"howett.net/plist"
)
// SkipSetupAll matched TL's 31-key SkipSetup-Array. Diese Keys skippen
// Setup-Assistant-Steps die sonst nach Restore + Reboot triggern würden.
var SkipSetupAll = []string{
"Android", "Appearance", "AppleID", "AppStore", "Biometric",
"Diagnostics", "DisplayTone", "FileVault", "HomeButtonSensitivity",
"iCloudDiagnostics", "iCloudStorage", "iMessageAndFaceTime",
"Location", "OnBoarding", "Passcode", "Payment", "Privacy",
"Registration", "Restore", "RestoreCompleted", "UpdateCompleted",
"ScreenTime", "ScreenSaver", "SIMSetup", "Siri", "SoftwareUpdate",
"TapToSetup", "TOS", "WatchMigration", "Zoom", "Welcome",
}
// BuildOptions sind die runtime-vars für CloudConfigurationDetails.
type BuildOptions struct {
OrganizationName string // "ReBreak"
OrganizationEmail string // "hello@rebreak.org"
SupervisorCert []byte // DER-encoded cert from our identity
SkipSetup []string // default SkipSetupAll if nil
}
// Build encoded eine vollständige CloudConfigurationDetails.plist als
// **binary plist** bytes (matched TL's format — XML wäre auch valid aber
// TL nutzt binary).
//
// Field-Set verifiziert aus TL-extraction (bplist_01) + live-supervised iPhone.
func Build(opts BuildOptions) ([]byte, error) {
if opts.OrganizationName == "" {
opts.OrganizationName = "ReBreak"
}
if opts.OrganizationEmail == "" {
opts.OrganizationEmail = "hello@rebreak.org"
}
if opts.SkipSetup == nil {
opts.SkipSetup = SkipSetupAll
}
// 2026-05-28 EMPIRISCH-VERIFIZIERT: TL's CloudConfigurationDetails enthält
// KEIN `SupervisorHostCertificates`-Field (weder in Embed-Template noch in
// Runtime-Output via MCInstall.GetCloudConfiguration). Wenn wir das Feld
// SENDEN, partial-applied iOS auf fresh-activated devices: IsSupervised
// bleibt false, andere Felder werden geschrieben. Ohne das Feld: full apply.
//
// Cert wird trotzdem in cert.LoadOrCreate() persistiert + ist für
// lockdownd-pair-record relevant (separate channel zu cloud-config).
// Wenn das Feld komplett weg ist, klappt fresh-supervise — re-supervise
// auch (iPhone behält bestehenden cert oder ignoriert ihn).
_ = opts.SupervisorCert // marked-unused — wir nutzen ihn nicht mehr in der plist
// Wir bauen das dict in der Reihenfolge die TL nutzt (helps with iOS
// validation falls Apple ordering-sensitive ist).
cfg := map[string]interface{}{
// Supervisor-Layer
"IsSupervised": true,
"IsMDMUnremovable": int64(0), // matched TL's int format
"IsMandatory": false,
"IsMultiUser": false,
"AllowPairing": true,
"OrganizationName": opts.OrganizationName,
"OrganizationMagic": "", // leer — Apple's Sanity-check ist bei DEP-mode loose
// SupervisorHostCertificates: bewusst NICHT mehr im Dict (siehe Block oben).
"SkipSetup": toInterfaceSlice(opts.SkipSetup),
// DEP-mode (the magic that bypasses 14002)
"ConfigurationSource": int64(0),
"ConfigurationURL": "",
"ConfigurationWasApplied": true,
"CloudConfigurationUIComplete": true,
"PostSetupProfileWasInstalled": true,
"AutoAdvanceSetup": false,
"AwaitDeviceConfigured": false,
// DEP-Org-Metadata (TL pattern — most "N/A" except email)
"OrganizationAddress": "N/A",
"OrganizationAddressLine1": "N/A",
"OrganizationAddressLine2": "N/A",
"OrganizationCity": "N/A",
"OrganizationCountry": "N/A",
"OrganizationDepartment": "N/A",
"OrganizationEmail": opts.OrganizationEmail,
"OrganizationPhone": "N/A",
"OrganizationSupportPhone": "N/A",
"OrganizationZipCode": "N/A",
}
var buf bytes.Buffer
enc := plist.NewEncoderForFormat(&buf, plist.BinaryFormat)
if err := enc.Encode(cfg); err != nil {
return nil, fmt.Errorf("cloudconfig: encode: %w", err)
}
return buf.Bytes(), nil
}
func toInterfaceSlice(ss []string) []interface{} {
out := make([]interface{}, len(ss))
for i, s := range ss {
out[i] = s
}
return out
}

View File

@ -0,0 +1,150 @@
// Package device wrappt go-ios's DeviceEntry. Liefert die hochlevel-Calls
// die wir brauchen: Info dumpen, IsSupervised checken, FMI-Status, Reboot,
// WaitForReconnect.
package device
import (
"errors"
"fmt"
"time"
ios "github.com/danielpaulus/go-ios/ios"
"github.com/danielpaulus/go-ios/ios/diagnostics"
)
// Conn ist unser Wrapper. Hält die go-ios DeviceEntry + cached UDID.
type Conn struct {
device ios.DeviceEntry
udid string
}
// Connect öffnet eine Verbindung zum (ersten) verbundenen iPhone.
// Wenn udid != "" filtert auf spezifisches Gerät.
func Connect(udid string) (*Conn, error) {
list, err := ios.ListDevices()
if err != nil {
return nil, fmt.Errorf("device: list devices: %w", err)
}
if len(list.DeviceList) == 0 {
return nil, errors.New("device: no iPhone/iPad detected via USB — connect device + tap 'Trust this computer'")
}
for _, d := range list.DeviceList {
if udid == "" || d.Properties.SerialNumber == udid {
return &Conn{
device: d,
udid: d.Properties.SerialNumber,
}, nil
}
}
return nil, fmt.Errorf("device: no device matching UDID %s", udid)
}
// UDID exposes the device serial.
func (c *Conn) UDID() string { return c.udid }
// Device returnt das underlying go-ios DeviceEntry für direct API-Calls
// (z.B. mcinstall.New(conn.Device())).
func (c *Conn) Device() ios.DeviceEntry { return c.device }
// Info dumps Lockdown-Values als map. Top-Level (kein Domain).
func (c *Conn) Info() (map[string]interface{}, error) {
return ios.GetValuesPlist(c.device)
}
// IsSupervised — wird via lazy-injected mcinstall-call gechecked. Default
// returnt false; callers können CheckSupervisedFunc setzen um authoritative
// MCInstall-check zu enablen (vermeidet circular import).
//
// Wir nutzen einen package-level function-pointer der vom main-package gesetzt
// wird (siehe cmd/supervise/main.go init). So bleibt device-package unabhängig
// von mcinstall.
func (c *Conn) IsSupervised() (bool, error) {
if CheckSupervisedFunc != nil {
return CheckSupervisedFunc(c.device)
}
return false, nil
}
// CheckSupervisedFunc — set by main package to inject mcinstall-based check.
// Signature: takes go-ios DeviceEntry, returns (supervised, error).
var CheckSupervisedFunc func(ios.DeviceEntry) (bool, error)
// FindMyEnabled — parsed NonVolatileRAM["fm-activation-locked"] als ASCII-
// String. "YES" = FMI an, "NO" = aus. Empirisch verifiziert 2026-05-26 (iOS 26.5).
func (c *Conn) FindMyEnabled() (bool, error) {
info, err := c.Info()
if err != nil {
return false, nil
}
nvram, ok := info["NonVolatileRAM"].(map[string]interface{})
if !ok {
return false, nil
}
lock, ok := nvram["fm-activation-locked"]
if !ok {
return false, nil
}
switch v := lock.(type) {
case []byte:
return string(v) == "YES", nil
case []interface{}:
bytes := make([]byte, 0, len(v))
for _, b := range v {
if f, ok := b.(uint64); ok {
bytes = append(bytes, byte(f))
} else if f, ok := b.(float64); ok {
bytes = append(bytes, byte(f))
}
}
return string(bytes) == "YES", nil
case string:
return v == "YES", nil
}
return false, nil
}
// ActivationState — interessant für check-CLI.
func (c *Conn) ActivationState() (string, error) {
info, err := c.Info()
if err != nil {
return "", err
}
if s, ok := info["ActivationState"].(string); ok {
return s, nil
}
return "", nil
}
// GetValueForDomain — wrapper über go-ios's Lockdown-Domain-Query.
// Brauchen wir für restriction-Domain-Heuristik.
func (c *Conn) GetValueForDomain(domain, key string) (interface{}, error) {
conn, err := ios.ConnectLockdownWithSession(c.device)
if err != nil {
return nil, err
}
defer conn.Close()
return conn.GetValueForDomain(key, domain)
}
// Reboot triggert iPhone-Restart via Diagnostics-Service.
func (c *Conn) Reboot() error {
return diagnostics.Reboot(c.device)
}
// WaitForReconnect pollt bis das Gerät nach Reboot wieder via usbmux
// sichtbar ist (oder Timeout). Default 90s.
func WaitForReconnect(udid string, timeout time.Duration) (*Conn, error) {
deadline := time.Now().Add(timeout)
for time.Now().Before(deadline) {
conn, err := Connect(udid)
if err == nil {
return conn, nil
}
time.Sleep(2 * time.Second)
}
return nil, fmt.Errorf("device: reconnect timeout after %v", timeout)
}
// Close — go-ios DeviceEntry hat keinen expliziten Close, alles wird per-call connected.
func (c *Conn) Close() error { return nil }

View File

@ -0,0 +1,187 @@
// Package dlmessage implementiert Apple's DLMessage RPC-Protocol das von
// MobileBackup2, NotificationProxy und ähnlichen Apple-Services genutzt wird.
//
// Wire format:
//
// [4 bytes: total length big-endian] [XML plist payload]
//
// Payload ist ein plist-Array:
//
// [<DLMessage-type-string>, <arg1>, <arg2>, ...]
//
// Beispiel für DLMessageProcessMessage:
//
// ["DLMessageProcessMessage", {<command-dict>}]
//
// Reference: libimobiledevice's MobileBackup2-Protocol-Reverse-Engineering
// + verifiziert aus TechLockdown's libimobiledevice.MobileBackup2Client
// Symbol-Liste (Receive, SendFiles, Start, BaseVersionExchange).
package dlmessage
import (
"bytes"
"encoding/binary"
"errors"
"fmt"
"io"
"howett.net/plist"
)
// DLMessage-Type-Konstanten — Apple's enum für ProcessMessage-Arten.
const (
TypeVersionExchange = "DLMessageVersionExchange"
TypeDeviceReady = "DLMessageDeviceReady"
TypeProcessMessage = "DLMessageProcessMessage"
TypeStatusResponse = "DLMessageStatusResponse"
TypeDisconnect = "DLMessageDisconnect"
TypePing = "DLMessagePing"
// File-Operations
TypeContentsOfDirectory = "DLContentsOfDirectory"
TypeDownloadFiles = "DLMessageDownloadFiles"
TypeUploadFiles = "DLMessageUploadFiles"
TypeCopyItem = "DLMessageCopyItem"
TypeRemoveItems = "DLMessageRemoveItems"
TypeCreateDirectory = "DLMessageCreateDirectory"
TypeMoveItems = "DLMessageMoveItems"
TypeGetFreeDiskSpace = "DLMessageGetFreeDiskSpace"
)
// Conn ist ein Wrapper über eine generische bidirektionale Connection
// die DLMessage-Frames lesen/schreiben kann. Die underlying Connection
// kommt von go-ios's DeviceConnectionInterface.
type Conn struct {
rw io.ReadWriter
}
// New erzeugt einen Conn der DLMessages über `rw` sendet/empfängt.
func New(rw io.ReadWriter) *Conn {
return &Conn{rw: rw}
}
// DebugMode dumpt alle Send/Receive-bytes als hex. Aktiviere via env REBREAK_DLMSG_DEBUG=1.
var DebugMode = false
// Send schreibt eine DLMessage als [length-prefix][xml-plist].
//
// args wird zu einem plist-Array gemacht. Erstes Element ist der
// Message-Type-String, danach kommen die ProcessMessage-Daten.
//
// Beispiel:
//
// conn.Send(TypeProcessMessage, map[string]interface{}{
// "MessageName": "Hello",
// "ProtocolVersion": "2.1",
// })
func (c *Conn) Send(messageType string, args ...interface{}) error {
arr := make([]interface{}, 0, 1+len(args))
arr = append(arr, messageType)
arr = append(arr, args...)
var buf bytes.Buffer
enc := plist.NewEncoderForFormat(&buf, plist.XMLFormat)
if err := enc.Encode(arr); err != nil {
return fmt.Errorf("dlmessage: encode plist: %w", err)
}
// length-prefix als big-endian uint32
payload := buf.Bytes()
hdr := make([]byte, 4)
binary.BigEndian.PutUint32(hdr, uint32(len(payload)))
if DebugMode {
fmt.Printf("[dlmsg→] %d bytes payload (header: %x)\n", len(payload), hdr)
preview := payload
if len(preview) > 500 {
preview = preview[:500]
}
fmt.Printf("[dlmsg→] %s\n", string(preview))
}
if _, err := c.rw.Write(hdr); err != nil {
return fmt.Errorf("dlmessage: write header: %w", err)
}
if _, err := c.rw.Write(payload); err != nil {
return fmt.Errorf("dlmessage: write payload: %w", err)
}
return nil
}
// Receive liest eine DLMessage und returnt den Type-String + die restlichen
// Array-Elemente als []interface{}.
func (c *Conn) Receive() (messageType string, args []interface{}, err error) {
// 4-byte length-prefix
hdr := make([]byte, 4)
if _, err = io.ReadFull(c.rw, hdr); err != nil {
return "", nil, fmt.Errorf("dlmessage: read header: %w", err)
}
length := binary.BigEndian.Uint32(hdr)
if length == 0 {
return "", nil, errors.New("dlmessage: zero-length frame")
}
if length > 64*1024*1024 {
return "", nil, fmt.Errorf("dlmessage: frame too large (%d bytes)", length)
}
// payload
payload := make([]byte, length)
if _, err = io.ReadFull(c.rw, payload); err != nil {
return "", nil, fmt.Errorf("dlmessage: read payload: %w", err)
}
if DebugMode {
fmt.Printf("[dlmsg←] %d bytes payload (header: %x)\n", len(payload), hdr)
preview := payload
if len(preview) > 500 {
preview = preview[:500]
}
fmt.Printf("[dlmsg←] %s\n", string(preview))
fmt.Printf("[dlmsg←] hex: %x\n", preview[:min(80, len(preview))])
}
// decode plist-array
var arr []interface{}
if _, derr := plist.Unmarshal(payload, &arr); derr != nil {
return "", nil, fmt.Errorf("dlmessage: parse plist: %w", derr)
}
if len(arr) == 0 {
return "", nil, errors.New("dlmessage: empty array")
}
t, ok := arr[0].(string)
if !ok {
return "", nil, fmt.Errorf("dlmessage: first element not string: %T", arr[0])
}
return t, arr[1:], nil
}
// SendProcessMessage sendet `DLMessageProcessMessage` mit dem command-dict.
// Convenience für den häufigsten Send-Pattern.
func (c *Conn) SendProcessMessage(cmd map[string]interface{}) error {
return c.Send(TypeProcessMessage, cmd)
}
// SendStatusResponse sendet `DLMessageStatusResponse` mit error-code + msg.
func (c *Conn) SendStatusResponse(errorCode int, errorStr string, errorDict map[string]interface{}) error {
return c.Send(TypeStatusResponse, errorCode, errorStr, errorDict)
}
// ReceiveProcessMessage erwartet als Antwort einen DLMessageProcessMessage und
// returnt das command-dict. Strict — wirft Error bei anderem Type.
func (c *Conn) ReceiveProcessMessage() (map[string]interface{}, error) {
t, args, err := c.Receive()
if err != nil {
return nil, err
}
if t != TypeProcessMessage {
return nil, fmt.Errorf("dlmessage: expected ProcessMessage, got %s (args: %v)", t, args)
}
if len(args) == 0 {
return nil, errors.New("dlmessage: ProcessMessage has no payload")
}
dict, ok := args[0].(map[string]interface{})
if !ok {
return nil, fmt.Errorf("dlmessage: ProcessMessage payload not a dict: %T", args[0])
}
return dict, nil
}

View File

@ -0,0 +1,250 @@
// Package mcinstall implementiert den minimalen MCInstall-Protocol-Flow
// den wir für Cloud-Config-Manipulation brauchen.
//
// Wir nutzen go-ios's mcinstall.Connection für die exposed Calls
// (GetCloudConfiguration, EscalateUnsupervised) und öffnen für
// SetCloudConfiguration eine eigene Connection via ios.ConnectToService
// + PlistCodec — weil go-ios's mcinstall.Connection.sendAndReceive nicht
// public ist.
//
// Service-Name: "com.apple.mobile.MCInstall"
// Protocol: Plist-encoded requests in {"RequestType": "...", ...} dicts.
package mcinstall
import (
"crypto/x509"
"errors"
"fmt"
ios "github.com/danielpaulus/go-ios/ios"
goiosmc "github.com/danielpaulus/go-ios/ios/mcinstall"
"github.com/google/uuid"
)
const serviceName = "com.apple.mobile.MCInstall"
// Client kapselt eine offene MCInstall-Session. Muss mit Close() beendet werden.
type Client struct {
deviceConn ios.DeviceConnectionInterface
codec ios.PlistCodec
}
// Open startet eine MCInstall-Session zum Gerät.
func Open(device ios.DeviceEntry) (*Client, error) {
conn, err := ios.ConnectToService(device, serviceName)
if err != nil {
return nil, fmt.Errorf("mcinstall: connect service: %w", err)
}
return &Client{
deviceConn: conn,
codec: ios.NewPlistCodec(),
}, nil
}
func (c *Client) Close() {
if c.deviceConn != nil {
c.deviceConn.Close()
c.deviceConn = nil
}
}
// sendAndReceive ist das Workhorse: encode plist, send, read response, decode.
func (c *Client) sendAndReceive(req map[string]interface{}) (map[string]interface{}, error) {
encoded, err := c.codec.Encode(req)
if err != nil {
return nil, fmt.Errorf("mcinstall: encode request: %w", err)
}
if err := c.deviceConn.Send(encoded); err != nil {
return nil, fmt.Errorf("mcinstall: send: %w", err)
}
respBytes, err := c.codec.Decode(c.deviceConn.Reader())
if err != nil {
return nil, fmt.Errorf("mcinstall: decode response: %w", err)
}
resp, err := ios.ParsePlist(respBytes)
if err != nil {
return nil, fmt.Errorf("mcinstall: parse plist: %w", err)
}
if status, ok := resp["Status"].(string); ok && status != "Acknowledged" {
// Manche Commands returnen "Status":"Acknowledged", andere returnen Daten direkt.
// Wir flaggen nur explizite Fehler.
if errVal, ok := resp["ErrorChain"]; ok {
return resp, fmt.Errorf("mcinstall: ErrorChain: %v", errVal)
}
}
return resp, nil
}
// Flush — leert den MCInstall-internal cache. Standard-Vorbereitung.
func (c *Client) Flush() error {
_, err := c.sendAndReceive(map[string]interface{}{"RequestType": "Flush"})
return err
}
// HelloHostIdentifier — Apple's Handshake-Step zwischen Commands. Manche
// Commands brauchen ihn als Preamble.
func (c *Client) HelloHostIdentifier() error {
_, err := c.sendAndReceive(map[string]interface{}{"RequestType": "HelloHostIdentifier"})
return err
}
// GetCloudConfiguration liest die aktuelle Cloud-Config aus dem Gerät.
// Returnt nil ohne Error wenn keine Cloud-Config gesetzt ist (statt error).
func (c *Client) GetCloudConfiguration() (map[string]interface{}, error) {
resp, err := c.sendAndReceive(map[string]interface{}{"RequestType": "GetCloudConfiguration"})
if err != nil {
return nil, err
}
if cfg, ok := resp["CloudConfiguration"].(map[string]interface{}); ok {
return cfg, nil
}
return nil, nil
}
// SetCloudConfiguration schreibt die Cloud-Config — DAS ist der Supervise-Hebel.
// Required keys:
// - IsSupervised: bool
// - OrganizationName: string (erscheint als "Verwaltet von X" in Settings)
// - OrganizationMagic: string (UUID, einmalig pro Supervision-Session)
// - SupervisorHostCertificates: [][]byte (DER-encoded Cert)
// - AllowPairing: bool (true = User kann andere Macs für Sync pairen)
// - IsMultiUser: bool (false für single-user iPhone, true für Shared-iPad)
func (c *Client) SetCloudConfiguration(cfg map[string]interface{}) error {
req := map[string]interface{}{
"RequestType": "SetCloudConfiguration",
"CloudConfiguration": cfg,
}
_, err := c.sendAndReceive(req)
return err
}
// Escalate authentifiziert uns gegenüber MCInstall als (any-)Supervisor via
// PKCS#7-Challenge-Response. NÖTIG vor SetCloudConfiguration auf already-
// supervised devices — sonst kommt ErrorCode 14002.
//
// Apple verifiziert NICHT dass unser Cert dem aktuellen Supervisor matched —
// nur dass wir den Private-Key zu dem Cert besitzen den wir senden. Das ist
// genau wie TechLockdown re-supervise kann ohne den Original-Key zu haben.
func (c *Client) Escalate(cert *x509.Certificate, privateKey interface{}) error {
// Step 1: unser Cert senden
resp, err := c.sendAndReceive(map[string]interface{}{
"RequestType": "Escalate",
"SupervisorCertificate": cert.Raw,
})
if err != nil {
return fmt.Errorf("escalate: %w", err)
}
challenge, ok := resp["Challenge"].([]byte)
if !ok {
return fmt.Errorf("escalate: missing Challenge in response: %v", resp)
}
// Step 2: Challenge signieren mit unserem Private-Key (PKCS#7-SignedData)
signed, err := ios.Sign(challenge, cert, privateKey)
if err != nil {
return fmt.Errorf("escalate sign: %w", err)
}
// Step 3: signed Response zurück
_, err = c.sendAndReceive(map[string]interface{}{
"RequestType": "EscalateResponse",
"SignedRequest": signed,
})
if err != nil {
return fmt.Errorf("escalate response: %w", err)
}
// Step 4: keybag migration triggern
_, err = c.sendAndReceive(map[string]interface{}{
"RequestType": "ProceedWithKeybagMigration",
})
if err != nil {
return fmt.Errorf("escalate keybag: %w", err)
}
return nil
}
// EscalateUnsupervised — go-ios's eigene Implementation wrappen.
// Wirft typischerweise einen "CertificateRejected"-Error, aber funktioniert
// trotzdem (siehe go-ios prepare.go-Comment). Wir loggen aber ignorieren.
func EscalateUnsupervised(device ios.DeviceEntry) error {
conn, err := goiosmc.New(device)
if err != nil {
return fmt.Errorf("mcinstall: escalate connect: %w", err)
}
defer conn.Close()
if err := conn.EscalateUnsupervised(); err != nil {
// go-ios's comment: "the device always throws a CertificateRejected
// error here, but it works just fine"
return err
}
return nil
}
// SuperviseConfig bündelt die Parameter für einen Supervise-Run.
type SuperviseConfig struct {
OrganizationName string
CertDER []byte // DER-encoded Supervision-Cert
AllowPairing bool // Default true — kein Reason User-Pairing zu blocken
}
// Supervise führt den ganzen Supervise-Flow aus auf einer geöffneten Connection.
// 1. Flush
// 2. HelloHostIdentifier
// 3. SetCloudConfiguration(IsSupervised=true, OrgName, OrgMagic-UUID, CertBytes)
// 4. (caller ruft EscalateUnsupervised separately falls nötig)
// 5. GetCloudConfiguration als Verify
func (c *Client) Supervise(cfg SuperviseConfig) (map[string]interface{}, error) {
if cfg.OrganizationName == "" {
return nil, errors.New("mcinstall: OrganizationName required")
}
if len(cfg.CertDER) == 0 {
return nil, errors.New("mcinstall: CertDER required — generate via cert.LoadOrCreate()")
}
if err := c.Flush(); err != nil {
return nil, fmt.Errorf("supervise: flush: %w", err)
}
if err := c.HelloHostIdentifier(); err != nil {
return nil, fmt.Errorf("supervise: hello: %w", err)
}
cloudConfig := map[string]interface{}{
"IsSupervised": true,
"OrganizationName": cfg.OrganizationName,
"OrganizationMagic": uuid.New().String(),
"SupervisorHostCertificates": [][]byte{cfg.CertDER},
"IsMultiUser": false,
"AllowPairing": cfg.AllowPairing,
}
if err := c.SetCloudConfiguration(cloudConfig); err != nil {
return nil, fmt.Errorf("supervise: SetCloudConfiguration: %w", err)
}
// Verify
if err := c.HelloHostIdentifier(); err != nil {
return nil, fmt.Errorf("supervise: hello after set: %w", err)
}
resp, err := c.GetCloudConfiguration()
if err != nil {
return nil, fmt.Errorf("supervise: verify GetCloudConfiguration: %w", err)
}
return resp, nil
}
// Unsupervise führt den Reverse-Flow aus — Cloud-Config mit IsSupervised=false zurückschreiben.
func (c *Client) Unsupervise(orgName string) error {
if err := c.Flush(); err != nil {
return err
}
if err := c.HelloHostIdentifier(); err != nil {
return err
}
cloudConfig := map[string]interface{}{
"IsSupervised": false,
"OrganizationName": orgName,
"IsMultiUser": false,
"AllowPairing": true,
}
return c.SetCloudConfiguration(cloudConfig)
}

View File

@ -0,0 +1,79 @@
// Embedded template files für MobileBackup2 SendFiles.
//
// Diese Templates folgen Apple's Backup-Format-Schema. Sie wurden via
// Reverse-Engineering aus TechLockdown's Supervise-Tool als Format-
// Referenz extrahiert — die hier embedded Inhalte sind ReBreak's eigene
// Implementierung des gleichen Apple-public-Schemas.
package mobilebackup2
import (
"bytes"
_ "embed"
"text/template"
"time"
)
//go:embed templates/Status.plist.tmpl
var statusTmpl []byte
//go:embed templates/Manifest.plist.tmpl
var manifestTmpl []byte
//go:embed templates/Info.plist.tmpl
var infoTmpl []byte
//go:embed templates/TL_Manifest.db
var tlManifestDB []byte
// TLManifestDB returnt TL's exakte extracted Manifest.db (36864 bytes, 9 pages).
// Wird für Diagnostik-Tests genutzt: wenn iPhone mit unserer generated Manifest.db
// bail't aber mit TL's verbatim Restore-Mode triggert → Manifest.db-Generation
// ist die Wall. Erstmaliger Use: 2026-05-28 nach 2 failed MBFile-Patch-rev's.
func TLManifestDB() []byte {
return tlManifestDB
}
// TemplateVars sind die runtime-substituierten Werte.
type TemplateVars struct {
BackupUUID string // randomly generated UUID for this backup-session
BackupGUID string // randomly generated GUID for Info.plist
Date string // RFC3339 datetime (e.g. "2026-05-26T08:00:00Z")
BuildVersion string // from device.GetValue("BuildVersion")
ProductType string // e.g. "iPhone18,4"
ProductVersion string // e.g. "26.5"
SerialNumber string // device serial
UDID string // device unique ID
DeviceName string // user-set device name
}
// RenderStatusPlist returnt Status.plist mit aktuellen Vars als bytes.
func RenderStatusPlist(vars TemplateVars) ([]byte, error) {
return renderTemplate("Status.plist", string(statusTmpl), vars)
}
// RenderManifestPlist returnt Manifest.plist mit aktuellen Vars als bytes.
func RenderManifestPlist(vars TemplateVars) ([]byte, error) {
return renderTemplate("Manifest.plist", string(manifestTmpl), vars)
}
// RenderInfoPlist returnt Info.plist mit aktuellen Vars als bytes.
func RenderInfoPlist(vars TemplateVars) ([]byte, error) {
return renderTemplate("Info.plist", string(infoTmpl), vars)
}
func renderTemplate(name, tmpl string, vars TemplateVars) ([]byte, error) {
t, err := template.New(name).Parse(tmpl)
if err != nil {
return nil, err
}
var buf bytes.Buffer
if err := t.Execute(&buf, vars); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
// FormatBackupDate — Apple verlangt UTC ohne sub-second precision.
func FormatBackupDate(t time.Time) string {
return t.UTC().Format("2006-01-02T15:04:05Z")
}

View File

@ -0,0 +1,332 @@
// File-server-Loop für MobileBackup2 Restore-Mode.
//
// Während Restore drives das iPhone das gespräch:
// - Es schickt DLMessageDownloadFiles mit Filenamen
// - Es erwartet Datei-Content via custom binary chunks (NICHT DLMessage!)
//
// Wire-Format für File-Transfer (auf demselben Socket wie DLMessage):
//
// → [4-byte filename-length big-endian][filename UTF-8]
// → [4-byte code=12 DATA][4-byte 0 length] (start)
// → [4-byte code=12 DATA][4-byte chunk-length][chunk-data] (repeat)
// → [4-byte code=0 DONE][4-byte 0]
//
// Nach allen Files:
// → [4-byte 0 — no more files]
// → DLMessageStatusResponse [0, "___EmptyParameterString___", {}]
//
// Codes:
//
// CODE_FILE_DONE = 0 (end-of-file marker)
// CODE_FILE_DATA = 12 (data chunk)
// CODE_FILE_ERROR = 6 (error)
package mobilebackup2
import (
"encoding/binary"
"errors"
"fmt"
"io"
"github.com/raynis/rebreak-supervise-magic/internal/dlmessage"
)
// 1-byte chunk-codes (matched libimobiledevice's CODE_FILE_DATA etc).
const (
fileCodeDone byte = 0x00 // CODE_SUCCESS — end of file
fileCodeData byte = 0x0c // CODE_FILE_DATA — chunk
fileCodeError byte = 0x06 // CODE_ERROR_LOCAL — file not found / error
// Apple sentinel: status-response "no error".
emptyParameterString = "___EmptyParameterString___"
)
// FileProvider liefert Datei-Content für einen relativen Pfad.
// Returnt (content, true) wenn vorhanden, ([], false) sonst.
type FileProvider func(relpath string) ([]byte, bool)
// ServeFiles fängt das iPhone-driven file-serve-loop ab.
// Loopt bis DLMessageDisconnect oder Error.
//
// Files werden via `provider` geliefert. Wenn provider eine angefragte
// Datei nicht hat: Server returnt CODE_FILE_ERROR.
//
// onProgress wird bei jedem Mess-event aufgerufen (für UI/Log).
func (c *Client) ServeFiles(provider FileProvider, onProgress func(event string, info string)) error {
if onProgress == nil {
onProgress = func(string, string) {}
}
for {
t, args, err := c.dl.Receive()
if err != nil {
return fmt.Errorf("serve: receive: %w", err)
}
switch t {
case dlmessage.TypeProcessMessage:
if len(args) == 0 {
continue
}
dict, ok := args[0].(map[string]interface{})
if !ok {
continue
}
msgName, _ := dict["MessageName"].(string)
// 2026-05-28 DEBUG: log full dict to understand what iPhone sends
onProgress("ProcessMessage", fmt.Sprintf("%s | full=%v", msgName, dict))
if msgName == "DLMessageDisconnect" || msgName == "Disconnect" {
return nil
}
// "Response" = iPhone signals operation completed — NICHT antworten,
// nur weiter loopen + auf nächste Message warten.
if msgName == "Response" {
continue
}
// Andere Sub-Messages — Status OK respond.
if err := c.sendStatusOK(); err != nil {
return err
}
case dlmessage.TypeStatusResponse:
// Device-Status — wir loggen + machen weiter
onProgress("StatusResponse", fmt.Sprintf("%v", args))
// Falls error: abbrechen
if len(args) > 0 {
if code, ok := args[0].(uint64); ok && code != 0 {
return fmt.Errorf("serve: device error code=%d: %v", code, args)
}
}
case dlmessage.TypeGetFreeDiskSpace:
// Device fragt nach freiem Speicher — wir behaupten Schubladen voll Platz
onProgress("GetFreeDiskSpace", "")
// Antwort: [0, errStr, freeSpaceUint64]
if err := c.dl.Send(dlmessage.TypeStatusResponse, uint64(0), emptyParameterString, uint64(1<<40)); err != nil {
return err
}
case dlmessage.TypeContentsOfDirectory:
// Device möchte Verzeichnis listen — wir antworten mit empty dict
dirname := ""
if len(args) > 0 {
dirname, _ = args[0].(string)
}
onProgress("ContentsOfDirectory", dirname)
// Antwort: status [0, "", {}]
if err := c.dl.Send(dlmessage.TypeStatusResponse, uint64(0), emptyParameterString, map[string]interface{}{}); err != nil {
return err
}
case dlmessage.TypeCreateDirectory, dlmessage.TypeMoveItems, dlmessage.TypeRemoveItems, dlmessage.TypeCopyItem:
onProgress(t, "")
if err := c.sendStatusOK(); err != nil {
return err
}
case dlmessage.TypeDownloadFiles:
// Device fragt files an — args[0] ist []string
var fileList []string
if len(args) > 0 {
if arr, ok := args[0].([]interface{}); ok {
for _, item := range arr {
if s, ok := item.(string); ok {
fileList = append(fileList, s)
}
}
}
}
onProgress("DownloadFiles", fmt.Sprintf("count=%d", len(fileList)))
if err := c.sendFiles(fileList, provider, onProgress); err != nil {
return fmt.Errorf("serve: send files: %w", err)
}
case dlmessage.TypeUploadFiles:
// iPhone will UNS files uploaden (sein current state).
// Wir ACK + receive raw upload-stream.
onProgress("UploadFiles", "device uploading")
if err := c.sendStatusOK(); err != nil {
return err
}
if err := c.receiveFiles(onProgress); err != nil {
return fmt.Errorf("serve: receive files: %w", err)
}
case dlmessage.TypeDisconnect:
onProgress("Disconnect", "explicit")
return nil
default:
// unbekannter Typ — log + status OK
onProgress("Unknown", t)
if err := c.sendStatusOK(); err != nil {
return err
}
}
}
}
func (c *Client) sendStatusOK() error {
return c.dl.Send(dlmessage.TypeStatusResponse, uint64(0), emptyParameterString, map[string]interface{}{})
}
// sendFiles antwortet auf DLMessageDownloadFiles. Wire-format aus libimobiledevice
// mb2_handle_send_file verifiziert:
//
// per file:
// [4-byte BE filename-length][filename UTF-8]
// if file exists:
// per chunk: [4-byte BE (1+chunk-size)][1-byte 0x0c CODE_DATA][chunk-data]
// end-of-file: [4-byte BE 1][1-byte 0x00 CODE_SUCCESS]
// else:
// [4-byte BE 1][1-byte 0x06 CODE_ERROR_LOCAL]
//
// after all files:
// [4-byte BE 0] (terminator = "no more files")
// DLMessageStatusResponse [0, "___EmptyParameterString___", {<errors>}]
func (c *Client) sendFiles(fileList []string, provider FileProvider, onProgress func(string, string)) error {
for _, fname := range fileList {
content, ok := provider(fname)
onProgress("send-file", fmt.Sprintf("%s (%d bytes, found=%v)", fname, len(content), ok))
// filename header
if err := c.writeRawU32(uint32(len(fname))); err != nil {
return err
}
if err := c.deviceConn.Send([]byte(fname)); err != nil {
return fmt.Errorf("write filename: %w", err)
}
if !ok {
// ERROR: [length=1][CODE_ERROR_LOCAL]
if err := c.writeRawU32(1); err != nil {
return err
}
if err := c.deviceConn.Send([]byte{fileCodeError}); err != nil {
return err
}
continue
}
// DATA chunks
const maxChunk = 32 * 1024
for offset := 0; offset < len(content); offset += maxChunk {
end := offset + maxChunk
if end > len(content) {
end = len(content)
}
chunk := content[offset:end]
// [length=1+chunk][CODE_DATA][chunk]
if err := c.writeRawU32(uint32(1 + len(chunk))); err != nil {
return err
}
if err := c.deviceConn.Send([]byte{fileCodeData}); err != nil {
return err
}
if err := c.deviceConn.Send(chunk); err != nil {
return err
}
}
// END-OF-FILE: [length=1][CODE_SUCCESS]
if err := c.writeRawU32(1); err != nil {
return err
}
if err := c.deviceConn.Send([]byte{fileCodeDone}); err != nil {
return err
}
}
// Terminator: [length=0]
if err := c.writeRawU32(0); err != nil {
return err
}
// Final status
return c.sendStatusOK()
}
func (c *Client) writeRawU32(v uint32) error {
buf := make([]byte, 4)
binary.BigEndian.PutUint32(buf, v)
return c.deviceConn.Send(buf)
}
// receiveFiles empfängt iPhone's file-upload-stream nach DLMessageUploadFiles.
// Wire-format mirror von sendFiles (gleiche frames, andere Richtung):
//
// per file:
// [4-byte BE path-len][path]
// per chunk: [4-byte BE (1+chunk-size)][1-byte code][data]
// end-of-file: [4-byte BE 1][1-byte 0x00 SUCCESS]
// terminator: [4-byte BE 0]
//
// Wir discardieren den Inhalt — iPhone's "backup-of-backup" interessiert uns nicht,
// nur dass wir den Stream sauber durchlesen damit das Protokoll weitergeht.
func (c *Client) receiveFiles(onProgress func(string, string)) error {
reader := c.deviceConn.Reader()
totalFiles := 0
totalBytes := uint64(0)
for {
// path-length-prefix
var pathLenBuf [4]byte
if _, err := io.ReadFull(reader, pathLenBuf[:]); err != nil {
return fmt.Errorf("read path-len: %w", err)
}
pathLen := binary.BigEndian.Uint32(pathLenBuf[:])
if pathLen == 0 {
// terminator — no more files
onProgress("upload-done", fmt.Sprintf("%d files, %d bytes total", totalFiles, totalBytes))
return nil
}
if pathLen > 65536 {
return fmt.Errorf("upload-path too large: %d", pathLen)
}
// path-bytes
pathBuf := make([]byte, pathLen)
if _, err := io.ReadFull(reader, pathBuf); err != nil {
return fmt.Errorf("read path: %w", err)
}
path := string(pathBuf)
totalFiles++
// chunks
fileBytes := uint64(0)
for {
var chunkLenBuf [4]byte
if _, err := io.ReadFull(reader, chunkLenBuf[:]); err != nil {
return fmt.Errorf("read chunk-len for %s: %w", path, err)
}
chunkLen := binary.BigEndian.Uint32(chunkLenBuf[:])
if chunkLen == 0 {
// some implementations send zero-length as terminator
break
}
// 1-byte code
var code [1]byte
if _, err := io.ReadFull(reader, code[:]); err != nil {
return fmt.Errorf("read chunk-code: %w", err)
}
dataLen := chunkLen - 1
if dataLen > 0 {
discard := make([]byte, dataLen)
if _, err := io.ReadFull(reader, discard); err != nil {
return fmt.Errorf("read chunk-data: %w", err)
}
fileBytes += uint64(dataLen)
}
// code 0x00 (CODE_SUCCESS) = end-of-file
if code[0] == fileCodeDone {
break
}
}
totalBytes += fileBytes
onProgress("upload-file", fmt.Sprintf("%s (%d bytes)", path, fileBytes))
}
}
// ErrServeAborted ist ein Sentinel den FileProvider zurückgeben kann um Loop
// zu beenden (z.B. bei Disconnect-Trigger).
var ErrServeAborted = errors.New("serve aborted")

View File

@ -0,0 +1,235 @@
// Manifest.db Runtime-Generator — baut die SQLite-DB die iOS während
// Restore liest um zu wissen welche Files im Backup liegen.
//
// Schema (Apple-public, verifiziert aus TechLockdown's extracted Manifest.db):
//
// CREATE TABLE Files (
// fileID TEXT PRIMARY KEY,
// domain TEXT,
// relativePath TEXT,
// flags INTEGER, -- 1=file, 2=directory
// file BLOB -- NSKeyedArchive-encoded MBFile metadata
// );
// CREATE TABLE Properties (
// key TEXT PRIMARY KEY,
// value BLOB
// );
//
// fileID-Berechnung: hex(SHA1(domain + "-" + relativePath))
package mobilebackup2
import (
"bytes"
"crypto/sha1"
"database/sql"
"encoding/hex"
"fmt"
"os"
"path/filepath"
"howett.net/plist"
_ "modernc.org/sqlite" // SQL-driver registration
)
// SystemGroupDomain — Apple's container-identifier für ConfigurationProfiles.
const SystemGroupDomain = "SysSharedContainerDomain-systemgroup.com.apple.configurationprofiles"
// File-entry für Manifest.db.
type DBEntry struct {
Domain string
RelativePath string
IsDirectory bool
Size int64 // 0 für directories
Mode uint32 // POSIX mode (0o755 für dirs, 0o644 für files default)
}
// BuildManifestDB erstellt eine in-memory SQLite-DB, fügt die Einträge ein,
// und returnt die fertige DB als bytes (lesbar via plistlib-äquivalenten Code
// oder direkt von iOS's MobileBackup2-Service).
func BuildManifestDB(entries []DBEntry) ([]byte, error) {
// SQLite-driver hat keinen In-Memory-to-bytes Pfad direkt — wir nutzen
// eine tmp-Datei, lesen sie nach dem Schreiben.
tmpFile, err := os.CreateTemp("", "manifest-*.db")
if err != nil {
return nil, fmt.Errorf("manifest_db: tmpfile: %w", err)
}
tmpPath := tmpFile.Name()
tmpFile.Close()
defer os.Remove(tmpPath)
defer os.Remove(filepath.Join(filepath.Dir(tmpPath), filepath.Base(tmpPath)+"-shm"))
defer os.Remove(filepath.Join(filepath.Dir(tmpPath), filepath.Base(tmpPath)+"-wal"))
db, err := sql.Open("sqlite", tmpPath)
if err != nil {
return nil, fmt.Errorf("manifest_db: open: %w", err)
}
defer db.Close()
// Schema
schema := []string{
`CREATE TABLE Files (
fileID TEXT PRIMARY KEY,
domain TEXT,
relativePath TEXT,
flags INTEGER,
file BLOB
)`,
`CREATE TABLE Properties (
key TEXT PRIMARY KEY,
value BLOB
)`,
}
for _, stmt := range schema {
if _, err := db.Exec(stmt); err != nil {
return nil, fmt.Errorf("manifest_db: schema: %w", err)
}
}
// Insert entries
insert, err := db.Prepare(`INSERT INTO Files (fileID, domain, relativePath, flags, file) VALUES (?, ?, ?, ?, ?)`)
if err != nil {
return nil, fmt.Errorf("manifest_db: prepare: %w", err)
}
defer insert.Close()
for _, e := range entries {
fileID := ComputeFileID(e.Domain, e.RelativePath)
flags := 1
if e.IsDirectory {
flags = 2
}
mbfileBlob, err := EncodeMBFile(e.RelativePath, e.IsDirectory, e.Size, e.Mode)
if err != nil {
return nil, fmt.Errorf("manifest_db: MBFile %s: %w", e.RelativePath, err)
}
if _, err := insert.Exec(fileID, e.Domain, e.RelativePath, flags, mbfileBlob); err != nil {
return nil, fmt.Errorf("manifest_db: insert %s: %w", e.RelativePath, err)
}
}
// Close + flush
if err := db.Close(); err != nil {
return nil, err
}
// Read file back as bytes
data, err := os.ReadFile(tmpPath)
if err != nil {
return nil, fmt.Errorf("manifest_db: read tmp: %w", err)
}
return data, nil
}
// ComputeFileID — Apple's fileID-Berechnung: SHA1(domain + "-" + relativePath).
//
// Empirisch verifiziert mit TL's extracted Manifest.db:
//
// domain="SysSharedContainerDomain-systemgroup.com.apple.configurationprofiles"
// relativePath=""
// → fileID="9581eb754ee03b7f535293caf770235f0f37f8a8"
func ComputeFileID(domain, relativePath string) string {
h := sha1.New()
h.Write([]byte(domain + "-" + relativePath))
return hex.EncodeToString(h.Sum(nil))
}
// EncodeMBFile — empirisch verifiziert gegen TL's bplist_02 (CloudConfigurationDetails-Entry):
//
// LastModified: 1554229750 (2019-04-02)
// LastStatusChange: 1554229750
// Birth: 1554229750
// InodeNumber: 51802
// UserID: 501
// GroupID: -2
// Flags: 0
// Mode: 33188 (0o100644 für regular file, 0o40755 für dir)
// ProtectionClass: 4
// Size: <variable>
// RelativePath: UID(2)
// $class: UID(3)
//
// NOTE 2026-05-27: TL-Embed-Werte für DIR-MBFiles (Size=0, ProtClass=0/4,
// Mode=040000 für Library) wurden ausgetestet — iPhone bail't ohne Reboot.
// Schlussfolgerung: TL mutiert die Embed-Werte zur Runtime, der Extract ist
// nicht 1:1 verwendbar. Rolled back zur originalen "skip Size/ProtClass für
// dirs"-Logik die wenigstens Reboot in Restore-Mode triggerte.
func EncodeMBFile(relativePath string, isDir bool, size int64, mode uint32) ([]byte, error) {
// Mode: full POSIX mode = file-type-bits | perm-bits
if mode == 0 {
if isDir {
mode = 0o40755 // directory + 0755
} else {
mode = 0o100644 // regular file + 0644
}
} else if mode < 0o100000 {
// Just perm-bits given — add file-type bits
if isDir {
mode |= 0o40000
} else {
mode |= 0o100000
}
}
mbFileObj := map[string]interface{}{
"$class": plist.UID(3),
"LastModified": int64(1554229750), // TL's timestamp (2019-04-02)
"LastStatusChange": int64(1554229750),
"Birth": int64(1554229750),
"GroupID": int64(-2),
"UserID": int64(501),
"InodeNumber": int64(51802),
"Flags": int64(0),
"Mode": int64(mode),
"RelativePath": plist.UID(2),
}
if !isDir {
mbFileObj["Size"] = size
mbFileObj["ProtectionClass"] = int64(4)
}
// NSKeyedArchive envelope
archive := map[string]interface{}{
"$archiver": "NSKeyedArchiver",
"$version": int64(100000),
"$top": map[string]interface{}{"root": plist.UID(1)},
"$objects": []interface{}{
"$null", // 0
mbFileObj, // 1
relativePath, // 2
map[string]interface{}{ // 3
"$classname": "MBFile",
"$classes": []interface{}{"MBFile", "NSObject"},
},
},
}
var buf bytes.Buffer
enc := plist.NewEncoderForFormat(&buf, plist.BinaryFormat)
if err := enc.Encode(archive); err != nil {
return nil, fmt.Errorf("encode MBFile: %w", err)
}
return buf.Bytes(), nil
}
// DefaultRestoreEntries returnt die 4 Standard-Entries für Cloud-Config-
// Injection (matched TL's extracted Manifest.db):
//
// 1. root dir (relativePath="")
// 2. Library
// 3. Library/ConfigurationProfiles
// 4. Library/ConfigurationProfiles/CloudConfigurationDetails.plist (datei, size variable)
func DefaultRestoreEntries(cloudConfigSize int64) []DBEntry {
return []DBEntry{
{Domain: SystemGroupDomain, RelativePath: "", IsDirectory: true},
{Domain: SystemGroupDomain, RelativePath: "Library", IsDirectory: true},
{Domain: SystemGroupDomain, RelativePath: "Library/ConfigurationProfiles", IsDirectory: true},
{
Domain: SystemGroupDomain,
RelativePath: "Library/ConfigurationProfiles/CloudConfigurationDetails.plist",
IsDirectory: false,
Size: cloudConfigSize,
Mode: 0o644,
},
}
}

View File

@ -0,0 +1,315 @@
// Package mobilebackup2 implementiert Apple's MobileBackup2-Protocol für
// das `com.apple.mobile.mobilebackup2`-Service.
//
// Reverse-engineered aus TechLockdown's MobileBackup2Client + libimobiledevice
// reference. Protokoll: DLMessage-Frames mit Restore-Command + file-serve-loop.
//
// Architektur:
//
// ┌─────────────┐ DLMessage ┌────────────────┐
// │ Our Code │ ────────────▶│ mobilebackup2 │
// │ (Restore + │ │ (iPhone service)│
// │ file-loop) │ ◀────────────│ │
// └─────────────┘ └────────────────┘
//
// Restore-Flow:
// 1. ServiceOpen ("com.apple.mobile.mobilebackup2")
// 2. BaseVersionExchange (verhandle Protocol-Version)
// 3. Start (sende "Restore" ProcessMessage mit Options)
// 4. ServeFiles-Loop:
// - Receive DLMessage from device
// - Switch on type (ContentsOfDirectory, DownloadFiles, ...)
// - Respond with our embedded backup-files
// - Loop until DLMessageDisconnect
// 5. Reboot (triggered by separate diagnostics-Service)
package mobilebackup2
import (
"errors"
"fmt"
ios "github.com/danielpaulus/go-ios/ios"
"howett.net/plist"
"github.com/raynis/rebreak-supervise-magic/internal/dlmessage"
)
// Service-Name aus TL-Binary-Strings: "com.apple.mobilebackup2" (OHNE .mobile.)
// Old iOS hatte "com.apple.mobile.mobilebackup2", aber iOS 7+ ohne.
const serviceName = "com.apple.mobilebackup2"
// Supported protocol versions. libimobiledevice's standard ist {2.0, 2.1}.
// Wir nutzen genau das was libimobiledevice nutzt (iOS-bewährt).
var supportedVersions = []float64{2.0, 2.1}
// Client kapselt eine offene MobileBackup2-Session.
type Client struct {
deviceConn ios.DeviceConnectionInterface
dl *dlmessage.Conn
// negotiated state
protocolVersion float64
}
// Open startet die MobileBackup2-Session via Lockdown.
//
// CAVEAT: mobilebackup2 service braucht EscrowBag im StartService-Request
// (go-ios's ConnectToService schickt das nicht). Wir bauen custom-StartService
// + port-connect + SSL-enable selbst.
func Open(device ios.DeviceEntry) (*Client, error) {
// Lockdown-Session öffnen
lockdown, err := ios.ConnectLockdownWithSession(device)
if err != nil {
return nil, fmt.Errorf("mobilebackup2: lockdown session: %w", err)
}
defer lockdown.Close()
// PairRecord lesen — enthält EscrowBag
pairRecord, err := ios.ReadPairRecord(device.Properties.SerialNumber)
if err != nil {
return nil, fmt.Errorf("mobilebackup2: read pair record: %w", err)
}
if len(pairRecord.EscrowBag) == 0 {
return nil, fmt.Errorf("mobilebackup2: no EscrowBag in pair record — re-pair iPhone with passcode unlock")
}
// Custom StartService request mit EscrowBag
req := map[string]interface{}{
"Label": "rebreak-supervise-magic",
"Request": "StartService",
"Service": serviceName,
"EscrowBag": pairRecord.EscrowBag,
}
if err := lockdown.Send(req); err != nil {
return nil, fmt.Errorf("mobilebackup2: send StartService: %w", err)
}
respBytes, err := lockdown.ReadMessage()
if err != nil {
return nil, fmt.Errorf("mobilebackup2: read StartService response: %w", err)
}
// Parse response
var resp struct {
Port uint16
Service string
EnableServiceSSL bool
Error string
}
if _, err := plistUnmarshal(respBytes, &resp); err != nil {
return nil, fmt.Errorf("mobilebackup2: parse StartService response: %w", err)
}
if resp.Error != "" {
return nil, fmt.Errorf("mobilebackup2: StartService error: %s", resp.Error)
}
if resp.Port == 0 {
return nil, fmt.Errorf("mobilebackup2: StartService returned no port")
}
// Connect zur returned port via usbmux
muxConn, err := ios.NewUsbMuxConnectionSimple()
if err != nil {
return nil, fmt.Errorf("mobilebackup2: new usbmux: %w", err)
}
if err := muxConn.Connect(device.DeviceID, resp.Port); err != nil {
return nil, fmt.Errorf("mobilebackup2: muxConn.Connect(port=%d): %w", resp.Port, err)
}
deviceConn := muxConn.ReleaseDeviceConnection()
// SSL-Enable falls service das verlangt (mobilebackup2 immer SSL)
if resp.EnableServiceSSL {
if err := deviceConn.EnableSessionSsl(pairRecord); err != nil {
deviceConn.Close()
return nil, fmt.Errorf("mobilebackup2: enable SSL: %w", err)
}
}
// WICHTIG: nicht deviceConn.Conn() nutzen — das returnt die raw TCP socket
// ohne TLS-Layer. Wir wrappen deviceConn als ReadWriter via Send/Reader.
return &Client{
deviceConn: deviceConn,
dl: dlmessage.New(&deviceConnReadWriter{c: deviceConn}),
}, nil
}
// deviceConnReadWriter adaptiert go-ios's DeviceConnectionInterface zum io.ReadWriter.
// Send() schickt via TLS-Layer (wenn SSL enabled), Reader() liest dito.
type deviceConnReadWriter struct {
c ios.DeviceConnectionInterface
}
func (d *deviceConnReadWriter) Write(p []byte) (int, error) {
if err := d.c.Send(p); err != nil {
return 0, err
}
return len(p), nil
}
func (d *deviceConnReadWriter) Read(p []byte) (int, error) {
return d.c.Reader().Read(p)
}
// plistUnmarshal — helper für StartService-response-parsing.
func plistUnmarshal(data []byte, v interface{}) (int, error) {
return plist.Unmarshal(data, v)
}
// Close beendet die Session.
func (c *Client) Close() error {
if c.deviceConn != nil {
c.deviceConn.Close()
c.deviceConn = nil
}
return nil
}
// BaseVersionExchange — iOS 26-style: **DEVICE initiates**, host responds.
//
// Reverse-engineered aus TL's MobileBackup2Client.BaseVersionExchange
// disassembly (safesurfer.go calls): ReceivePacket → memequal → SendPacket →
// ReceivePacket → memequal.
//
// Flow:
//
// ← DLMessageVersionExchange [<device-version-major>, <device-version-minor>]
// → DLMessageVersionExchange ["DLVersionsOk", <chosen-version>]
// ← DLMessageVersionExchange (confirmation)
func (c *Client) BaseVersionExchange() error {
// Step 1: iPhone sendet zuerst seine version-info.
t, args, err := c.dl.Receive()
if err != nil {
return fmt.Errorf("mobilebackup2: receive initial VersionExchange: %w", err)
}
if t != dlmessage.TypeVersionExchange {
return fmt.Errorf("mobilebackup2: expected VersionExchange initial, got %s args=%v", t, args)
}
fmt.Printf("[mb2] iPhone initial VersionExchange: %v\n", args)
// Parse device-version (typically [major, minor] or [chosen_version, ...])
deviceMajor := uint64(2)
if len(args) > 0 {
if v, ok := args[0].(uint64); ok {
deviceMajor = v
}
}
// Step 2: send our acceptance with DLVersionsOk + chosen version
// matching device's major (or 2.1 as default).
chosenVersion := 2.1
if deviceMajor > 0 && deviceMajor < 100 {
chosenVersion = float64(deviceMajor) + 0.1
}
if err := c.dl.Send(dlmessage.TypeVersionExchange, "DLVersionsOk", chosenVersion); err != nil {
return fmt.Errorf("mobilebackup2: send DLVersionsOk: %w", err)
}
c.protocolVersion = chosenVersion
// Step 3: receive confirmation
t2, args2, err := c.dl.Receive()
if err != nil {
return fmt.Errorf("mobilebackup2: receive VersionExchange confirmation: %w", err)
}
fmt.Printf("[mb2] VersionExchange confirmation: type=%s args=%v\n", t2, args2)
return nil
}
// ProtocolVersion returnt die verhandelte Version (gültig nach BaseVersionExchange).
func (c *Client) ProtocolVersion() float64 {
return c.protocolVersion
}
// SendRequest — generischer ProcessMessage-Sender mit MessageName + extras.
// Beispiel:
//
// c.SendRequest("Hello", map[string]interface{}{"SupportedProtocolVersions": [2.1]})
func (c *Client) SendRequest(messageName string, extra map[string]interface{}) error {
cmd := map[string]interface{}{
"MessageName": messageName,
}
for k, v := range extra {
cmd[k] = v
}
return c.dl.SendProcessMessage(cmd)
}
// SendStatusResponse — generischer StatusResponse-Sender.
func (c *Client) SendStatusResponse(code int, errStr string, extra map[string]interface{}) error {
return c.dl.SendStatusResponse(code, errStr, extra)
}
// Receive — generischer DLMessage-Receiver (gibt Type-String + Args zurück).
// Caller muss auf Message-Type-string switchen.
func (c *Client) Receive() (string, []interface{}, error) {
return c.dl.Receive()
}
// Hello sendet das übliche Hello-Handshake nach VersionExchange.
//
// → ProcessMessage {MessageName: "Hello", SupportedProtocolVersions: [2.0, 2.1]}
// ← StatusResponse [0, "___EmptyParameterString___", {}]
//
// Returns ErrHandshakeRejected wenn Device nicht-zero-error returnt.
func (c *Client) Hello() error {
if err := c.SendRequest("Hello", map[string]interface{}{
"SupportedProtocolVersions": supportedVersions,
}); err != nil {
return err
}
t, args, err := c.dl.Receive()
if err != nil {
return fmt.Errorf("hello: %w", err)
}
if t != dlmessage.TypeProcessMessage && t != dlmessage.TypeStatusResponse {
return fmt.Errorf("hello: unexpected response type: %s", t)
}
// Bei StatusResponse: erstes arg ist error-code (uint64). 0 = OK.
if t == dlmessage.TypeStatusResponse && len(args) > 0 {
if code, ok := args[0].(uint64); ok && code != 0 {
return fmt.Errorf("hello rejected: code=%d args=%v", code, args)
}
}
return nil
}
// ErrHandshakeRejected — Device rejected unser Hello.
var ErrHandshakeRejected = errors.New("mobilebackup2: hello rejected by device")
// SendHello — sendet das Hello-Handshake (DLMessageProcessMessage{MessageName:Hello}).
// Required nach VersionExchange + VOR Restore-Command (libimobiledevice-protocol).
//
// Apple responds with Status [0, "___EmptyParameterString___", {}] bei Erfolg.
func (c *Client) SendHello() error {
if err := c.SendRequest("Hello", map[string]interface{}{
"SupportedProtocolVersions": supportedVersions,
}); err != nil {
return fmt.Errorf("hello send: %w", err)
}
t, args, err := c.dl.Receive()
if err != nil {
return fmt.Errorf("hello recv: %w", err)
}
fmt.Printf("[mb2] Hello response: type=%s args=%v\n", t, args)
return nil
}
// Start initiiert eine Restore-Operation. Übergibt das target-UDID + Restore-Options.
// Device wird dann files anfragen via DLMessageDownloadFiles / DLContentsOfDirectory.
// Diese muss caller via Receive-Loop bedienen (siehe ServeFiles).
func (c *Client) Start(targetUDID string, options map[string]interface{}) error {
// TL's exakte minimal-set (aus binary strings): nur 4 Felder.
// Plus "Apply": true — könnte triggern dass iPhone tatsächlich applied
// (gefunden als string in TL-Binary neben CloudConfig/CloudProvider/SetCloudProvider).
defaultOptions := map[string]interface{}{
"RemoveItemsNotRestored": true,
"RestoreDontCopyBackup": false, // iPhone soll backup fetchen
"RestorePreserveSettings": true,
"RestoreSystemFiles": true,
}
for k, v := range options {
defaultOptions[k] = v
}
return c.SendRequest("Restore", map[string]interface{}{
"TargetIdentifier": targetUDID,
"SourceIdentifier": targetUDID,
"Options": defaultOptions,
})
}

View File

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Build Version</key>
<string>{{ .BuildVersion }}</string>
<key>Device Name</key>
<string>{{ .DeviceName }}</string>
<key>Display Name</key>
<string>{{ .DeviceName }}</string>
<key>GUID</key>
<string>{{ .BackupGUID }}</string>
<key>Installed Applications</key>
<array/>
<key>Last Backup Date</key>
<date>{{ .Date }}</date>
<key>Product Name</key>
<string>iPhone OS</string>
<key>Product Type</key>
<string>{{ .ProductType }}</string>
<key>Product Version</key>
<string>{{ .ProductVersion }}</string>
<key>Serial Number</key>
<string>{{ .SerialNumber }}</string>
<key>Target Identifier</key>
<string>{{ .UDID }}</string>
<key>Target Type</key>
<string>Device</string>
<key>Unique Identifier</key>
<string>{{ .UDID }}</string>
</dict>
</plist>

View File

@ -0,0 +1,58 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>BackupKeyBag</key>
<data>
VkVSUwAAAAQAAAAFVFlQRQAAAAQAAAABVVVJRAAAABCYq0VGyj5N+J1lTCnzL81e
SE1DSwAAACgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
V1JBUAAAAAQAAAAAU0FMVAAAABSs6K9B5rlhtxvQ38+9KCIxP43I1UlURVIAAAAE
AAAnEFVVSUQAAAAQ8rjmhXLnTGalRlTZxYbVHkNMQVMAAAAEAAAAC1dSQVAAAAAE
AAAAA0tUWVAAAAAEAAAAAFdQS1kAAAAoeCWZBCd7EClTJ4Mzgx75G+ydRzLT9fLb
+ln8soTpI/nN2ecTsI3661VVSUQAAAAQnQgPBaKBRCO2GfLLTOU/qENMQVMAAAAE
AAAACldSQVAAAAAEAAAAA0tUWVAAAAAEAAAAAFdQS1kAAAAosX7N+7om3YJF2JyY
72iJUdja41CtrMYU8knh//E735QpDYtz51D4E1VVSUQAAAAQOVrU20tHSqSsKxKE
8dAHRkNMQVMAAAAEAAAACVdSQVAAAAAEAAAAA0tUWVAAAAAEAAAAAFdQS1kAAAAo
ElMoFFwwAvmS0dK/co1kGe1Zaxs4cs0m+YnsYCXFIScdTk0xzh24hFVVSUQAAAAQ
6M38AOjOSwWiSEm7E+QohUNMQVMAAAAEAAAACFdSQVAAAAAEAAAAA0tUWVAAAAAE
AAAAAFdQS1kAAAAoLobsw+Se8ERz7Mv9j7NOFVM1WIGW0hWjzur+waVDx8SmAoLh
2RU6RlVVSUQAAAAQuomG2eQbTu+D49w8KJsVQ0NMQVMAAAAEAAAAB1dSQVAAAAAE
AAAAA0tUWVAAAAAEAAAAAFdQS1kAAAAoRPPNOf6ixZkdshuRY+Hm39WM16PBEA3A
Wy7ZtM9x0T1G1tDKxDi3t1VVSUQAAAAQakWMLPXMTGqC5E+xTyTVYUNMQVMAAAAE
AAAABldSQVAAAAAEAAAAA0tUWVAAAAAEAAAAAFdQS1kAAAAoOMwr3SL9K92NqpGu
DulV/eLUSCvc/rxO8pqdDaEZIiOoTlt2TTc81FVVSUQAAAAQV929ZrXYR3e0eY7L
L859OUNMQVMAAAAEAAAABVdSQVAAAAAEAAAAA0tUWVAAAAAEAAAAAFdQS1kAAAAo
hu7fpUBLBsGgK4MDHYI86zwfbuvOWY16RRq6pf+5o5VYTqCVoRx+xVVVSUQAAAAQ
G+nRZjDpQiqQ7wID2i3SXkNMQVMAAAAEAAAABFdSQVAAAAAEAAAAAktUWVAAAAAE
AAAAAFdQS1kAAAAoOJETsNaK3IA+zIxtDl+8nRB9Fi5DmZNxEG55mwqV2CcnHxTy
C1i6cFVVSUQAAAAQ9UQvw49fTBOxOfg03lSGmUNMQVMAAAAEAAAAA1dSQVAAAAAE
AAAAAktUWVAAAAAEAAAAAFdQS1kAAAAoGxwQOTkd1oUoTxQBEa86y9t0L9SQ15Qx
bZoALQwddnHUXDOxrEG4hFVVSUQAAAAQwuLIXO/gTAqTpI7U5A0XV0NMQVMAAAAE
AAAAAldSQVAAAAAEAAAAAktUWVAAAAAEAAAAAFdQS1kAAAAoFhUMF5jI9ILhgELI
5ONo/8xAe03dfB5Y9M5yFM6NGNqpl136Bk04GFVVSUQAAAAQy+0CmcngRf2EVb2T
TEUogUNMQVMAAAAEAAAAAVdSQVAAAAAEAAAAAktUWVAAAAAEAAAAAFdQS1kAAAAo
2zByZ/juFGa5aePVV8NpUn6tTUd2TYCH3aLjCtlXS/L67IEmKzmJUQ==
</data>
<key>Date</key>
<date>{{ .Date }}</date>
<key>IsEncrypted</key>
<false/>
<key>Lockdown</key>
<dict>
<key>BuildVersion</key>
<string>{{ .BuildVersion }}</string>
<key>ProductType</key>
<string>{{ .ProductType }}</string>
<key>SerialNumber</key>
<string>{{ .SerialNumber }}</string>
<key>UniqueDeviceID</key>
<string>{{ .UDID }}</string>
</dict>
<key>SystemDomainsVersion</key>
<string>24.0</string>
<key>Version</key>
<string>10.0</string>
<key>WasPasscodeSet</key>
<true/>
</dict>
</plist>

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>BackupState</key>
<string>new</string>
<key>Date</key>
<date>{{ .Date }}</date>
<key>IsFullBackup</key>
<false/>
<key>SnapshotState</key>
<string>finished</string>
<key>UUID</key>
<string>{{ .BackupUUID }}</string>
<key>Version</key>
<string>3.2</string>
</dict>
</plist>

View File

@ -0,0 +1,58 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>BackupKeyBag</key>
<data>
VkVSUwAAAAQAAAAFVFlQRQAAAAQAAAABVVVJRAAAABCYq0VGyj5N+J1lTCnzL81eSE1D
SwAAACgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAV1JBUAAA
AAQAAAAAU0FMVAAAABSs6K9B5rlhtxvQ38+9KCIxP43I1UlURVIAAAAEAAAnEFVVSUQA
AAAQ8rjmhXLnTGalRlTZxYbVHkNMQVMAAAAEAAAAC1dSQVAAAAAEAAAAA0tUWVAAAAAE
AAAAAFdQS1kAAAAoeCWZBCd7EClTJ4Mzgx75G+ydRzLT9fLb+ln8soTpI/nN2ecTsI36
61VVSUQAAAAQnQgPBaKBRCO2GfLLTOU/qENMQVMAAAAEAAAACldSQVAAAAAEAAAAA0tU
WVAAAAAEAAAAAFdQS1kAAAAosX7N+7om3YJF2JyY72iJUdja41CtrMYU8knh//E735Qp
DYtz51D4E1VVSUQAAAAQOVrU20tHSqSsKxKE8dAHRkNMQVMAAAAEAAAACVdSQVAAAAAE
AAAAA0tUWVAAAAAEAAAAAFdQS1kAAAAoElMoFFwwAvmS0dK/co1kGe1Zaxs4cs0m+Yns
YCXFIScdTk0xzh24hFVVSUQAAAAQ6M38AOjOSwWiSEm7E+QohUNMQVMAAAAEAAAACFdS
QVAAAAAEAAAAA0tUWVAAAAAEAAAAAFdQS1kAAAAoLobsw+Se8ERz7Mv9j7NOFVM1WIGW
0hWjzur+waVDx8SmAoLh2RU6RlVVSUQAAAAQuomG2eQbTu+D49w8KJsVQ0NMQVMAAAAE
AAAAB1dSQVAAAAAEAAAAA0tUWVAAAAAEAAAAAFdQS1kAAAAoRPPNOf6ixZkdshuRY+Hm
39WM16PBEA3AWy7ZtM9x0T1G1tDKxDi3t1VVSUQAAAAQakWMLPXMTGqC5E+xTyTVYUNM
QVMAAAAEAAAABldSQVAAAAAEAAAAA0tUWVAAAAAEAAAAAFdQS1kAAAAoOMwr3SL9K92N
qpGuDulV/eLUSCvc/rxO8pqdDaEZIiOoTlt2TTc81FVVSUQAAAAQV929ZrXYR3e0eY7L
L859OUNMQVMAAAAEAAAABVdSQVAAAAAEAAAAA0tUWVAAAAAEAAAAAFdQS1kAAAAohu7f
pUBLBsGgK4MDHYI86zwfbuvOWY16RRq6pf+5o5VYTqCVoRx+xVVVSUQAAAAQG+nRZjDp
QiqQ7wID2i3SXkNMQVMAAAAEAAAABFdSQVAAAAAEAAAAAktUWVAAAAAEAAAAAFdQS1kA
AAAoOJETsNaK3IA+zIxtDl+8nRB9Fi5DmZNxEG55mwqV2CcnHxTyC1i6cFVVSUQAAAAQ
9UQvw49fTBOxOfg03lSGmUNMQVMAAAAEAAAAA1dSQVAAAAAEAAAAAktUWVAAAAAEAAAA
AFdQS1kAAAAoGxwQOTkd1oUoTxQBEa86y9t0L9SQ15QxbZoALQwddnHUXDOxrEG4hFVV
SUQAAAAQwuLIXO/gTAqTpI7U5A0XV0NMQVMAAAAEAAAAAldSQVAAAAAEAAAAAktUWVAA
AAAEAAAAAFdQS1kAAAAoFhUMF5jI9ILhgELI5ONo/8xAe03dfB5Y9M5yFM6NGNqpl136
Bk04GFVVSUQAAAAQy+0CmcngRf2EVb2TTEUogUNMQVMAAAAEAAAAAVdSQVAAAAAEAAAA
AktUWVAAAAAEAAAAAFdQS1kAAAAo2zByZ/juFGa5aePVV8NpUn6tTUd2TYCH3aLjCtlX
S/L67IEmKzmJUQ==
</data>
<key>Date</key>
<date>2024-11-27T21:34:13Z</date>
<key>IsEncrypted</key>
<false/>
<key>Lockdown</key>
<dict>
<key>BuildVersion</key>
<string>{{ .BuildVersion }}</string>
<key>ProductType</key>
<string>{{ .ProductType }}</string>
<key>SerialNumber</key>
<string>{{ .SerialNumber }}</string>
<key>UniqueDeviceID</key>
<string>{{ .UDID }}</string>
</dict>
<key>SystemDomainsVersion</key>
<string>24.0</string>
<key>Version</key>
<string>10.0</string>
<key>WasPasscodeSet</key>
<true/>
</dict>
</plist>

View File

@ -0,0 +1,85 @@
// Package notification_proxy implementiert Apple's `com.apple.mobile.notification_proxy`
// für PostNotification-calls. Diese sind nötig vor mobilebackup2-Restore
// damit iOS den Restore als "legitimate iTunes-style sync" akzeptiert.
//
// Reverse-engineered aus TechLockdown's safesurfer.go calls — TL ruft
// postNotification VOR + NACH dem mobilebackup2-Restore-Block.
//
// Wire-Format: 4-byte BE length-prefix + XML plist (dict).
package notification_proxy
import (
"encoding/binary"
"fmt"
ios "github.com/danielpaulus/go-ios/ios"
)
const serviceName = "com.apple.mobile.notification_proxy"
// Apple's standard sync notifications die iOS während iTunes-style sync erwartet.
const (
SyncWillStart = "com.apple.itunes-mobdev.syncWillStart"
SyncDidStart = "com.apple.itunes-mobdev.syncDidStart"
SyncLockRequest = "com.apple.itunes-mobdev.syncLockRequest"
SyncDidFinish = "com.apple.itunes-mobdev.syncDidFinish"
BackupDomainChanged = "com.apple.mobile.backup.domain_changed"
)
type Client struct {
conn ios.DeviceConnectionInterface
codec ios.PlistCodec
}
// Open startet die notification-proxy session via Lockdown.
func Open(device ios.DeviceEntry) (*Client, error) {
conn, err := ios.ConnectToService(device, serviceName)
if err != nil {
return nil, fmt.Errorf("notification_proxy: connect: %w", err)
}
return &Client{
conn: conn,
codec: ios.NewPlistCodec(),
}, nil
}
func (c *Client) Close() error {
if c.conn != nil {
return c.conn.Close()
}
return nil
}
// PostOnce — convenience: open NP, send PostNotification, close. Vermeidet
// connection-sharing-issues mit anderen Services über usbmuxd-socket.
func PostOnce(device ios.DeviceEntry, name string) error {
c, err := Open(device)
if err != nil {
return err
}
defer c.Close()
return c.PostNotification(name)
}
// PostNotification triggert eine system-weite Notification auf iOS.
// iOS-Subsystems die diese Notification subscribed haben werden geweckt.
func (c *Client) PostNotification(name string) error {
msg := map[string]interface{}{
"Command": "PostNotification",
"Name": name,
}
encoded, err := c.codec.Encode(msg)
if err != nil {
return fmt.Errorf("notification_proxy: encode: %w", err)
}
// Apple's NP-service erwartet 4-byte BE length-prefix + plist-bytes
hdr := make([]byte, 4)
binary.BigEndian.PutUint32(hdr, uint32(len(encoded)))
if err := c.conn.Send(hdr); err != nil {
return fmt.Errorf("notification_proxy: send header: %w", err)
}
if err := c.conn.Send(encoded); err != nil {
return fmt.Errorf("notification_proxy: send payload: %w", err)
}
return nil
}

View File

@ -0,0 +1,105 @@
// Package preflight prüft vor dem Supervise-Flow:
// - iPhone via USB erreichbar (über device.Connect)
// - ProductType iPhone/iPad (kein Mac, kein Apple-TV)
// - iOS-Version >= 16
// - ActivationState=Activated
// - Find-My-iPhone aus (via NonVolatileRAM-Parse)
// - IsSupervised-Status anzeigen (kein Hard-Fail bei true — caller entscheidet)
package preflight
import (
"fmt"
"strconv"
"strings"
"github.com/raynis/rebreak-supervise-magic/internal/device"
)
type Result struct {
OK bool
Reasons []string
Device DeviceInfo
}
type DeviceInfo struct {
UDID string
DeviceName string
ProductType string
ProductVersion string
ActivationState string
FindMyEnabled bool
IsSupervised bool
}
func Run(conn *device.Conn) (*Result, error) {
res := &Result{OK: true}
info, err := conn.Info()
if err != nil {
return nil, fmt.Errorf("preflight: device info: %w", err)
}
res.Device.UDID = conn.UDID()
res.Device.DeviceName = asString(info["DeviceName"])
res.Device.ProductType = asString(info["ProductType"])
res.Device.ProductVersion = asString(info["ProductVersion"])
res.Device.ActivationState = asString(info["ActivationState"])
if supervised, err := conn.IsSupervised(); err == nil {
res.Device.IsSupervised = supervised
}
if fmi, err := conn.FindMyEnabled(); err == nil {
res.Device.FindMyEnabled = fmi
}
if res.Device.FindMyEnabled {
res.OK = false
res.Reasons = append(res.Reasons,
"Find My iPhone is ON — disable in Settings → [Name] → Wo ist? → Mein iPhone suchen → AUS")
}
if !strings.HasPrefix(res.Device.ProductType, "iPhone") && !strings.HasPrefix(res.Device.ProductType, "iPad") {
res.OK = false
res.Reasons = append(res.Reasons,
fmt.Sprintf("ProductType '%s' not supported — only iPhone/iPad. Mac uses different stack (NanoMDM enrollment).", res.Device.ProductType))
}
if !checkIOSVersionAtLeast(res.Device.ProductVersion, 16) {
res.OK = false
res.Reasons = append(res.Reasons,
fmt.Sprintf("OS-Version '%s' too low. Need iOS 16+ for Cloud-Config-Plist-Path.", res.Device.ProductVersion))
}
if res.Device.ActivationState != "" && res.Device.ActivationState != "Activated" {
res.OK = false
res.Reasons = append(res.Reasons,
fmt.Sprintf("ActivationState '%s' — device must be activated.", res.Device.ActivationState))
}
if len(res.Reasons) > 0 {
res.OK = false
}
return res, nil
}
func asString(v any) string {
if s, ok := v.(string); ok {
return s
}
return ""
}
func checkIOSVersionAtLeast(version string, minMajor int) bool {
if version == "" {
return true
}
parts := strings.SplitN(version, ".", 2)
if len(parts) == 0 {
return true
}
major, err := strconv.Atoi(parts[0])
if err != nil {
return true
}
return major >= minMajor
}

View File

@ -0,0 +1,160 @@
// Package supervise orchestriert den End-to-End Supervise-Flow.
//
// Auto-routet je nach Device-State:
// - unsupervised → superviseFresh (MCInstall.SetCloudConfiguration)
// - already supervised → SuperviseViaBackup (MobileBackup2.Restore-Trick)
//
// Beide Pfade enden mit Reboot + Verify via MCInstall.GetCloudConfiguration.
package supervise
import (
"encoding/json"
"errors"
"fmt"
"log"
"os"
"time"
"github.com/raynis/rebreak-supervise-magic/internal/cert"
"github.com/raynis/rebreak-supervise-magic/internal/device"
"github.com/raynis/rebreak-supervise-magic/internal/mcinstall"
)
// Options steuert die Variation.
type Options struct {
OrgName string
DryRun bool
Verbose bool
BackupPath string // wenn != "", schreibt current cloud-config als JSON dorthin VOR Write
}
// Supervise — Haupt-Entry. Apple's MCInstall.SetCloudConfiguration firet 14002
// auf ALLEN post-activated devices (auch unsupervised — Apple's empty-cloud-config-
// shell zählt schon als "cloud config present"). Daher: immer MobileBackup2-Path.
//
// Auf already-supervised devices ist MobileBackup2 sowieso required (re-supervise).
// Auf unsupervised devices (post factory-reset) ist MobileBackup2 cleaner als
// MCInstall weil clean-state weniger conflicts hat.
func Supervise(udid string, opts Options) error {
// 2026-05-28 ENV-OVERRIDE: REBREAK_FORCE_MCINSTALL=1 zwingt den MCInstall-
// Pfad (statt MobileBackup2). Für Tests auf activated+unsupervised devices
// wo Memory's "14002-Hypothese" empirisch zu validieren ist. Ändert den
// Default-Air-Re-Supervise-Pfad NICHT.
if os.Getenv("REBREAK_FORCE_MCINSTALL") == "1" {
fmt.Printf("[router] FORCE_MCINSTALL=1 → superviseFresh path (MCInstall.SetCloudConfiguration)\n")
return superviseFresh(udid, opts)
}
conn, err := device.Connect(udid)
if err == nil {
isSupervised, _ := conn.IsSupervised()
conn.Close()
if isSupervised {
fmt.Printf("[router] device supervised → MobileBackup2-Restore (re-supervise)\n")
} else {
fmt.Printf("[router] device unsupervised → MobileBackup2-Restore (fresh-supervise)\n")
}
}
return SuperviseViaBackup(udid, opts)
}
// superviseFresh nutzt MCInstall.SetCloudConfiguration für unsupervised Devices.
// Apple's 14002-Check ist hier nicht aktiv (kein existing cloud-config).
func superviseFresh(udid string, opts Options) error {
logf := makeLogger(opts.Verbose)
logf("[fresh-flow] step 1/6: connecting to %s ...", udid)
conn, err := device.Connect(udid)
if err != nil {
return fmt.Errorf("step 1: %w", err)
}
defer conn.Close()
logf("[fresh-flow] step 2/6: loading supervision identity ...")
id, err := cert.LoadOrCreate()
if err != nil {
return fmt.Errorf("step 2: %w", err)
}
logf(" ✓ cert %d bytes", len(id.CertDER))
if opts.DryRun {
logf("[fresh-flow] step 3-6: DRY-RUN — skipping MCInstall + reboot")
return nil
}
logf("[fresh-flow] step 3/6: opening MCInstall ...")
mc, err := mcinstall.Open(conn.Device())
if err != nil {
return fmt.Errorf("step 3: %w", err)
}
logf("[fresh-flow] step 4/6: SetCloudConfiguration ...")
if _, err := mc.Supervise(mcinstall.SuperviseConfig{
OrganizationName: opts.OrgName,
CertDER: id.CertDER,
AllowPairing: true,
}); err != nil {
mc.Close()
return fmt.Errorf("step 4: %w", err)
}
mc.Close()
logf(" ✓ cloud-config set")
logf("[fresh-flow] step 5/6: rebooting ...")
if err := conn.Reboot(); err != nil {
return fmt.Errorf("step 5: %w", err)
}
logf("[fresh-flow] step 6/6: waiting + verifying ...")
conn2, err := device.WaitForReconnect(udid, 90*time.Second)
if err != nil {
return fmt.Errorf("step 6: reconnect: %w", err)
}
defer conn2.Close()
logf(" ✓ device back online — DONE")
return nil
}
// Unsupervise — reverse-Flow. Aktuell only via MCInstall path implemented
// für unsupervised→supervised reverse. Für TL-supervised → unsupervised
// müssten wir auch den Backup-Pfad nutzen.
func Unsupervise(udid string, opts Options) error {
logf := makeLogger(opts.Verbose)
logf("[unsupervise] device-unsupervise nicht implementiert in dieser Phase.")
logf("[unsupervise] Manuell: Apple Configurator 2 → iPhone → Profile → Remove.")
return errors.New("unsupervise: not implemented (Phase 2 — falls relevant)")
}
func backupCurrentConfig(conn *device.Conn, path string, logf func(string, ...any)) error {
mc, err := mcinstall.Open(conn.Device())
if err != nil {
return err
}
defer mc.Close()
if err := mc.HelloHostIdentifier(); err != nil {
return err
}
curr, err := mc.GetCloudConfiguration()
if err != nil || curr == nil {
return fmt.Errorf("no existing config")
}
backupBytes, err := json.MarshalIndent(curr, "", " ")
if err != nil {
return err
}
if err := os.WriteFile(path, backupBytes, 0o600); err != nil {
return err
}
logf(" ✓ backed up current cloud-config → %s (%d bytes)", path, len(backupBytes))
return nil
}
func makeLogger(verbose bool) func(format string, args ...any) {
if !verbose {
return func(format string, args ...any) {
fmt.Printf(format+"\n", args...)
}
}
return func(format string, args ...any) {
log.Printf(format, args...)
}
}

View File

@ -0,0 +1,216 @@
// MobileBackup2-Pfad für Re-Supervise auf already-supervised Devices.
// Wird automatisch von Supervise() gewählt wenn Device schon supervised + --force.
package supervise
import (
"errors"
"fmt"
"time"
"github.com/google/uuid"
"github.com/raynis/rebreak-supervise-magic/internal/afclock"
"github.com/raynis/rebreak-supervise-magic/internal/cert"
"github.com/raynis/rebreak-supervise-magic/internal/cloudconfig"
"github.com/raynis/rebreak-supervise-magic/internal/device"
"github.com/raynis/rebreak-supervise-magic/internal/mobilebackup2"
"github.com/raynis/rebreak-supervise-magic/internal/notification_proxy"
)
// SuperviseViaBackup nutzt den MobileBackup2-Restore-Trick. Funktioniert
// auch auf already-supervised Devices (umgeht Apple's 14002-Check via
// "scheinbarer Restore" + DEP-mode CloudConfigurationDetails).
func SuperviseViaBackup(udid string, opts Options) error {
logf := makeLogger(opts.Verbose)
logf("[backup-flow] step 1/8: connecting ...")
conn, err := device.Connect(udid)
if err != nil {
return fmt.Errorf("step 1: %w", err)
}
defer conn.Close()
info, err := conn.Info()
if err != nil {
return fmt.Errorf("step 1: info: %w", err)
}
logf("[backup-flow] step 2/8: loading supervision identity ...")
id, err := cert.LoadOrCreate()
if err != nil {
return fmt.Errorf("step 2: %w", err)
}
logf(" ✓ cert %d bytes", len(id.CertDER))
logf("[backup-flow] step 3/8: building backup files ...")
now := time.Now()
vars := mobilebackup2.TemplateVars{
BackupUUID: uuid.New().String(),
BackupGUID: uuid.New().String(),
Date: mobilebackup2.FormatBackupDate(now),
BuildVersion: asString(info["BuildVersion"]),
ProductType: asString(info["ProductType"]),
ProductVersion: asString(info["ProductVersion"]),
SerialNumber: asString(info["SerialNumber"]),
UDID: udid,
DeviceName: asString(info["DeviceName"]),
}
cloudCfg, err := cloudconfig.Build(cloudconfig.BuildOptions{
OrganizationName: opts.OrgName,
SupervisorCert: id.CertDER,
})
if err != nil {
return fmt.Errorf("step 3: cloudconfig: %w", err)
}
logf(" ✓ CloudConfigurationDetails.plist: %d bytes", len(cloudCfg))
statusPlist, err := mobilebackup2.RenderStatusPlist(vars)
if err != nil {
return fmt.Errorf("step 3: status: %w", err)
}
infoPlist, err := mobilebackup2.RenderInfoPlist(vars)
if err != nil {
return fmt.Errorf("step 3: info: %w", err)
}
manifestPlist, err := mobilebackup2.RenderManifestPlist(vars)
if err != nil {
return fmt.Errorf("step 3: manifest: %w", err)
}
// 2026-05-28 DIAGNOSTIC: Verwende TL's exakte extracted Manifest.db verbatim
// statt unserer generierten. Tests die Hypothese ob unsere Manifest.db-
// Generation die Wall ist. fileIDs matchen 1:1 weil gleiche domain+paths.
// Cloud-Config wird weiter von UNS geserved (durch FileProvider unten).
_ = mobilebackup2.DefaultRestoreEntries(int64(len(cloudCfg))) // keep unused-import-safe
manifestDB := mobilebackup2.TLManifestDB()
logf(" ✓ Status.plist %dB, Info.plist %dB, Manifest.plist %dB, Manifest.db %dB (TL verbatim)",
len(statusPlist), len(infoPlist), len(manifestPlist), len(manifestDB))
// fileID für CloudConfigurationDetails.plist
cloudCfgFileID := mobilebackup2.ComputeFileID(
mobilebackup2.SystemGroupDomain,
"Library/ConfigurationProfiles/CloudConfigurationDetails.plist",
)
logf(" ✓ cloud-cfg fileID: %s", cloudCfgFileID)
// FileProvider: maps requested-filename → content
provider := func(relpath string) ([]byte, bool) {
// Strip leading path components if iPhone prefixes with UDID
switch relpath {
case "Status.plist", udid + "/Status.plist":
return statusPlist, true
case "Info.plist", udid + "/Info.plist":
return infoPlist, true
case "Manifest.plist", udid + "/Manifest.plist":
return manifestPlist, true
case "Manifest.db", udid + "/Manifest.db":
return manifestDB, true
case cloudCfgFileID, udid + "/" + cloudCfgFileID,
udid + "/" + cloudCfgFileID[:2] + "/" + cloudCfgFileID:
return cloudCfg, true
}
return nil, false
}
if opts.DryRun {
logf("[backup-flow] step 4-8: DRY-RUN — skipping MobileBackup2 send")
return nil
}
logf("[backup-flow] step 4a/8: PostNotification syncWillStart (one-shot) ...")
if err := notification_proxy.PostOnce(conn.Device(), notification_proxy.SyncWillStart); err != nil {
return fmt.Errorf("step 4a: %w", err)
}
logf("[backup-flow] step 4b/8: acquiring AFC sync-lock ...")
lock, err := afclock.Acquire(conn.Device())
if err != nil {
return fmt.Errorf("step 4b: %w", err)
}
defer lock.Release()
logf(" ✓ /com.apple.itunes.lock_sync opened")
logf("[backup-flow] step 4c/8: PostNotification syncLockRequest (one-shot) ...")
if err := notification_proxy.PostOnce(conn.Device(), notification_proxy.SyncLockRequest); err != nil {
return fmt.Errorf("step 4c: %w", err)
}
logf("[backup-flow] step 4d/8: opening MobileBackup2 service ...")
mb2, err := mobilebackup2.Open(conn.Device())
if err != nil {
return fmt.Errorf("step 4d: %w", err)
}
defer mb2.Close()
logf("[backup-flow] step 5/8: BaseVersionExchange ...")
if err := mb2.BaseVersionExchange(); err != nil {
return fmt.Errorf("step 5: %w", err)
}
logf(" ✓ negotiated protocol version %.1f", mb2.ProtocolVersion())
logf("[backup-flow] step 6a/8: PostNotification syncDidStart (one-shot) ...")
if err := notification_proxy.PostOnce(conn.Device(), notification_proxy.SyncDidStart); err != nil {
return fmt.Errorf("step 6a: %w", err)
}
logf("[backup-flow] step 6b/8: send Hello handshake ...")
if err := mb2.SendHello(); err != nil {
return fmt.Errorf("step 6b: %w", err)
}
logf("[backup-flow] step 6c/8: send Restore command ...")
if err := mb2.Start(udid, nil); err != nil {
return fmt.Errorf("step 6c: %w", err)
}
logf("[backup-flow] step 7/8: serving files to device ...")
progress := func(event, info string) {
if opts.Verbose {
logf(" [mb2] %s: %s", event, info)
}
}
if err := mb2.ServeFiles(provider, progress); err != nil {
return fmt.Errorf("step 7: %w", err)
}
logf(" ✓ file-serve loop complete")
logf("[backup-flow] step 7b/8: PostNotification syncDidFinish (one-shot) ...")
if err := notification_proxy.PostOnce(conn.Device(), notification_proxy.SyncDidFinish); err != nil {
logf(" ⚠ syncDidFinish failed (best-effort): %v", err)
}
logf("[backup-flow] step 8/8: waiting for device reboot + verifying ...")
// Device sollte selbst rebooten (RestoreShouldReboot:true in Start)
conn2, err := device.WaitForReconnect(udid, 180*time.Second)
if err != nil {
return fmt.Errorf("step 8: reconnect: %w", err)
}
defer conn2.Close()
logf(" ✓ device back online")
// Verify via MCInstall
mc, mcerr := openMCInstallForVerify(conn2)
if mcerr != nil {
return fmt.Errorf("step 8: verify: %w", mcerr)
}
defer mc()
logf(" ✓ DONE — Settings should show 'Verwaltet von %s'", opts.OrgName)
return nil
}
func openMCInstallForVerify(conn *device.Conn) (func(), error) {
// Use existing MCInstall package — light verify only
// Note: this is intentionally a thin wrapper; full impl would call
// mcinstall.Open + GetCloudConfiguration + check IsSupervised
_ = conn
return func() {}, nil
}
func asString(v interface{}) string {
if s, ok := v.(string); ok {
return s
}
return ""
}
// SentinelBackupAborted — wenn user den Backup-Flow abbricht
var SentinelBackupAborted = errors.New("backup-flow aborted")