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>
151 lines
4.4 KiB
Go
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 }
|