- ChatBubble: useActionSheet replaces custom Modal (native iOS popup, Android bottom sheet) - DM mode (isDM prop): hides like-count, shows Insta-style heart badge under bubble when liked - Group chat unchanged - Cleanup: remove unused Modal/Platform imports, sheet styles, actionsOpen state - deploy.sh: auto-detect ANDROID_HOME + auto-create local.properties for local Gradle - NEXT_RELEASE.md: DM reactions release note - Includes other staged work across binder-mac, marketing, ops/mdm, ios/
351 lines
12 KiB
Go
351 lines
12 KiB
Go
// 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"
|
|
"os"
|
|
|
|
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 {
|
|
// Defaults verifiziert 2026-05-28 für TL-parity (install-screen + Settings preserved).
|
|
// Siehe memory: project_supervise_tl_parity_achieved
|
|
// RemoveItemsNotRestored=true → triggert iOS install-screen + Setup-Skip zu Kamera
|
|
// RestoreSystemFiles=false → verhindert System-File-Reset (Sprache/Region/Apple-Account bleiben)
|
|
// RestorePreserveSettings=true → User-Settings bleiben
|
|
// RestoreDontCopyBackup=false → iPhone fetcht Backup-Files
|
|
defaultOptions := map[string]interface{}{
|
|
"RemoveItemsNotRestored": true,
|
|
"RestoreDontCopyBackup": false,
|
|
"RestorePreserveSettings": true,
|
|
"RestoreSystemFiles": false,
|
|
}
|
|
if v, ok := envBool("REBREAK_MB2_REMOVE_ITEMS_NOT_RESTORED"); ok {
|
|
defaultOptions["RemoveItemsNotRestored"] = v
|
|
}
|
|
if v, ok := envBool("REBREAK_MB2_RESTORE_PRESERVE_SETTINGS"); ok {
|
|
defaultOptions["RestorePreserveSettings"] = v
|
|
}
|
|
if v, ok := envBool("REBREAK_MB2_RESTORE_SYSTEM_FILES"); ok {
|
|
defaultOptions["RestoreSystemFiles"] = v
|
|
}
|
|
if v, ok := envBool("REBREAK_MB2_DONT_COPY_BACKUP"); ok {
|
|
defaultOptions["RestoreDontCopyBackup"] = v
|
|
}
|
|
if v, ok := envBool("REBREAK_MB2_APPLY"); ok {
|
|
defaultOptions["Apply"] = v
|
|
}
|
|
for k, v := range options {
|
|
defaultOptions[k] = v
|
|
}
|
|
fmt.Printf("[mb2] Restore options: %+v\n", defaultOptions)
|
|
return c.SendRequest("Restore", map[string]interface{}{
|
|
"TargetIdentifier": targetUDID,
|
|
"SourceIdentifier": targetUDID,
|
|
"Options": defaultOptions,
|
|
})
|
|
}
|
|
|
|
func envBool(name string) (bool, bool) {
|
|
v, ok := os.LookupEnv(name)
|
|
if !ok {
|
|
return false, false
|
|
}
|
|
switch v {
|
|
case "1", "true", "TRUE", "True":
|
|
return true, true
|
|
case "0", "false", "FALSE", "False":
|
|
return false, true
|
|
default:
|
|
return false, false
|
|
}
|
|
}
|