chahinebrini 01374c426e 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>
2026-05-27 01:55:10 +02:00

151 lines
4.4 KiB
Go

// 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 }