// 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: // 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, }, } }