chahinebrini b31066a04c feat(chat): native action sheet + Insta-style heart for DM messages
- 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/
2026-05-30 09:14:32 +02:00

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