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

252 lines
7.6 KiB
Go

// Manifest.db Runtime-Generator — baut die SQLite-DB die iOS während
// Restore liest um zu wissen welche Files im Backup liegen.
//
// Schema (Apple-public, verifiziert aus TechLockdown's extracted Manifest.db):
//
// CREATE TABLE Files (
// fileID TEXT PRIMARY KEY,
// domain TEXT,
// relativePath TEXT,
// flags INTEGER, -- 1=file, 2=directory
// file BLOB -- NSKeyedArchive-encoded MBFile metadata
// );
// CREATE TABLE Properties (
// key TEXT PRIMARY KEY,
// value BLOB
// );
//
// fileID-Berechnung: hex(SHA1(domain + "-" + relativePath))
package mobilebackup2
import (
"bytes"
"crypto/sha1"
"database/sql"
"encoding/hex"
"fmt"
"os"
"path/filepath"
"howett.net/plist"
_ "modernc.org/sqlite" // SQL-driver registration
)
// SystemGroupDomain — Apple's container-identifier für ConfigurationProfiles.
const SystemGroupDomain = "SysSharedContainerDomain-systemgroup.com.apple.configurationprofiles"
// File-entry für Manifest.db.
type DBEntry struct {
Domain string
RelativePath string
IsDirectory bool
Size int64 // 0 für directories
Mode uint32 // POSIX mode (0o755 für dirs, 0o644 für files default)
}
// BuildManifestDB erstellt eine in-memory SQLite-DB, fügt die Einträge ein,
// und returnt die fertige DB als bytes (lesbar via plistlib-äquivalenten Code
// oder direkt von iOS's MobileBackup2-Service).
func BuildManifestDB(entries []DBEntry) ([]byte, error) {
// SQLite-driver hat keinen In-Memory-to-bytes Pfad direkt — wir nutzen
// eine tmp-Datei, lesen sie nach dem Schreiben.
tmpFile, err := os.CreateTemp("", "manifest-*.db")
if err != nil {
return nil, fmt.Errorf("manifest_db: tmpfile: %w", err)
}
tmpPath := tmpFile.Name()
tmpFile.Close()
defer os.Remove(tmpPath)
defer os.Remove(filepath.Join(filepath.Dir(tmpPath), filepath.Base(tmpPath)+"-shm"))
defer os.Remove(filepath.Join(filepath.Dir(tmpPath), filepath.Base(tmpPath)+"-wal"))
db, err := sql.Open("sqlite", tmpPath)
if err != nil {
return nil, fmt.Errorf("manifest_db: open: %w", err)
}
defer db.Close()
// PRAGMA-Settings matched TL's extracted Manifest.db (2026-05-28 verifiziert):
// user_version=2 (app-defined backup-format magic — iOS may check this)
// auto_vacuum=2 (incremental — TL setzt das)
//
// journal_mode=wal NICHT gesetzt: WAL erzeugt -wal/-shm Files die wir nicht
// mit auf das iPhone schicken können. iPhone bekommt die DB self-contained.
pragmas := []string{
`PRAGMA auto_vacuum = 2`, // muss VOR CREATE TABLE gesetzt sein
`PRAGMA user_version = 2`,
}
for _, stmt := range pragmas {
if _, err := db.Exec(stmt); err != nil {
return nil, fmt.Errorf("manifest_db: pragma %q: %w", stmt, err)
}
}
// Schema
schema := []string{
`CREATE TABLE Files (
fileID TEXT PRIMARY KEY,
domain TEXT,
relativePath TEXT,
flags INTEGER,
file BLOB
)`,
`CREATE TABLE Properties (
key TEXT PRIMARY KEY,
value BLOB
)`,
}
for _, stmt := range schema {
if _, err := db.Exec(stmt); err != nil {
return nil, fmt.Errorf("manifest_db: schema: %w", err)
}
}
// Insert entries
insert, err := db.Prepare(`INSERT INTO Files (fileID, domain, relativePath, flags, file) VALUES (?, ?, ?, ?, ?)`)
if err != nil {
return nil, fmt.Errorf("manifest_db: prepare: %w", err)
}
defer insert.Close()
for _, e := range entries {
fileID := ComputeFileID(e.Domain, e.RelativePath)
flags := 1
if e.IsDirectory {
flags = 2
}
mbfileBlob, err := EncodeMBFile(e.RelativePath, e.IsDirectory, e.Size, e.Mode)
if err != nil {
return nil, fmt.Errorf("manifest_db: MBFile %s: %w", e.RelativePath, err)
}
if _, err := insert.Exec(fileID, e.Domain, e.RelativePath, flags, mbfileBlob); err != nil {
return nil, fmt.Errorf("manifest_db: insert %s: %w", e.RelativePath, err)
}
}
// Close + flush
if err := db.Close(); err != nil {
return nil, err
}
// Read file back as bytes
data, err := os.ReadFile(tmpPath)
if err != nil {
return nil, fmt.Errorf("manifest_db: read tmp: %w", err)
}
return data, nil
}
// ComputeFileID — Apple's fileID-Berechnung: SHA1(domain + "-" + relativePath).
//
// Empirisch verifiziert mit TL's extracted Manifest.db:
//
// domain="SysSharedContainerDomain-systemgroup.com.apple.configurationprofiles"
// relativePath=""
// → fileID="9581eb754ee03b7f535293caf770235f0f37f8a8"
func ComputeFileID(domain, relativePath string) string {
h := sha1.New()
h.Write([]byte(domain + "-" + relativePath))
return hex.EncodeToString(h.Sum(nil))
}
// EncodeMBFile — empirisch verifiziert gegen TL's bplist_02 (CloudConfigurationDetails-Entry):
//
// LastModified: 1554229750 (2019-04-02)
// LastStatusChange: 1554229750
// Birth: 1554229750
// InodeNumber: 51802
// UserID: 501
// GroupID: -2
// Flags: 0
// Mode: 33188 (0o100644 für regular file, 0o40755 für dir)
// ProtectionClass: 4
// Size: <variable>
// RelativePath: UID(2)
// $class: UID(3)
//
// NOTE 2026-05-27: TL-Embed-Werte für DIR-MBFiles (Size=0, ProtClass=0/4,
// Mode=040000 für Library) wurden ausgetestet — iPhone bail't ohne Reboot.
// Schlussfolgerung: TL mutiert die Embed-Werte zur Runtime, der Extract ist
// nicht 1:1 verwendbar. Rolled back zur originalen "skip Size/ProtClass für
// dirs"-Logik die wenigstens Reboot in Restore-Mode triggerte.
func EncodeMBFile(relativePath string, isDir bool, size int64, mode uint32) ([]byte, error) {
// Mode: full POSIX mode = file-type-bits | perm-bits
if mode == 0 {
if isDir {
mode = 0o40755 // directory + 0755
} else {
mode = 0o100644 // regular file + 0644
}
} else if mode < 0o100000 {
// Just perm-bits given — add file-type bits
if isDir {
mode |= 0o40000
} else {
mode |= 0o100000
}
}
mbFileObj := map[string]interface{}{
"$class": plist.UID(3),
"LastModified": int64(1554229750), // TL's timestamp (2019-04-02)
"LastStatusChange": int64(1554229750),
"Birth": int64(1554229750),
"GroupID": int64(-2),
"UserID": int64(501),
"InodeNumber": int64(51802),
"Flags": int64(0),
"Mode": int64(mode),
"RelativePath": plist.UID(2),
}
if !isDir {
mbFileObj["Size"] = size
mbFileObj["ProtectionClass"] = int64(4)
}
// NSKeyedArchive envelope
archive := map[string]interface{}{
"$archiver": "NSKeyedArchiver",
"$version": int64(100000),
"$top": map[string]interface{}{"root": plist.UID(1)},
"$objects": []interface{}{
"$null", // 0
mbFileObj, // 1
relativePath, // 2
map[string]interface{}{ // 3
"$classname": "MBFile",
"$classes": []interface{}{"MBFile", "NSObject"},
},
},
}
var buf bytes.Buffer
enc := plist.NewEncoderForFormat(&buf, plist.BinaryFormat)
if err := enc.Encode(archive); err != nil {
return nil, fmt.Errorf("encode MBFile: %w", err)
}
return buf.Bytes(), nil
}
// DefaultRestoreEntries returnt die 4 Standard-Entries für Cloud-Config-
// Injection (matched TL's extracted Manifest.db):
//
// 1. root dir (relativePath="")
// 2. Library
// 3. Library/ConfigurationProfiles
// 4. Library/ConfigurationProfiles/CloudConfigurationDetails.plist (datei, size variable)
func DefaultRestoreEntries(cloudConfigSize int64) []DBEntry {
return []DBEntry{
{Domain: SystemGroupDomain, RelativePath: "", IsDirectory: true},
{Domain: SystemGroupDomain, RelativePath: "Library", IsDirectory: true},
{Domain: SystemGroupDomain, RelativePath: "Library/ConfigurationProfiles", IsDirectory: true},
{
Domain: SystemGroupDomain,
RelativePath: "Library/ConfigurationProfiles/CloudConfigurationDetails.plist",
IsDirectory: false,
Size: cloudConfigSize,
Mode: 0o644,
},
}
}