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>
113 lines
4.3 KiB
Go
113 lines
4.3 KiB
Go
// 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
|
|
}
|