From 620178c61d4074601e2ab0717c8290d8b3bc57f6 Mon Sep 17 00:00:00 2001 From: ztluo Date: Sun, 10 May 2026 21:13:05 +0800 Subject: [PATCH] imagemounter: fetch DDI from GitHub and improve mount reliability Switch iOS 17+ DDI source from deviceboxhq.com to github.com/doronz88/DeveloperDiskImage (build 17E5179g), resolving identity-not-found failures on newer chips (A19 Pro). Add QueryPersonalizationManifest fast path: try existing device-side manifest before requesting Apple TSS, and reconnect the service on failure since the socket is closed by the device. This skips nonce + TSS on re-mounts when the manifest is still valid, based on pymobiledevice3's PersonalizedImageMounter.mount(). Replace hardcoded Manifest struct with dynamic map entries and iterate all Trusted entries when building the TSS request. Update TSS @VersionInfo and add @UUID field, matching pymobiledevice3's TSSRequest construction. Also add ProductBuildVersion-based DDI cache validation and stream DMG/trustcache directly to disk during download. --- ios/imagemounter/imagedownloader.go | 9 + ios/imagemounter/imagedownloader_github.go | 154 ++++++++++++++++++ ios/imagemounter/personalized_image.go | 50 ++++-- .../personalized_image_mounter.go | 95 +++++++++-- ios/imagemounter/tss.go | 39 +++-- 5 files changed, 303 insertions(+), 44 deletions(-) create mode 100644 ios/imagemounter/imagedownloader_github.go diff --git a/ios/imagemounter/imagedownloader.go b/ios/imagemounter/imagedownloader.go index b934b96f..3255e786 100644 --- a/ios/imagemounter/imagedownloader.go +++ b/ios/imagemounter/imagedownloader.go @@ -122,6 +122,15 @@ func MatchAvailable(version string) string { } func Download17Plus(baseDir string, version *semver.Version) (string, error) { + restoreDir, err := downloadPersonalizedDDI(baseDir) + if err != nil { + log.Warnf("GitHub DDI download failed: %v, falling back to deviceboxhq.com", err) + return download17PlusDevicebox(baseDir, version) + } + return restoreDir, nil +} + +func download17PlusDevicebox(baseDir string, version *semver.Version) (string, error) { downloadUrl := fmt.Sprintf("%s%s%s", devicebox, xcode15_4_ddi, ".zip") log.Infof("device iOS version: %s, getting developer image: %s", version.String(), downloadUrl) diff --git a/ios/imagemounter/imagedownloader_github.go b/ios/imagemounter/imagedownloader_github.go new file mode 100644 index 00000000..57698a38 --- /dev/null +++ b/ios/imagemounter/imagedownloader_github.go @@ -0,0 +1,154 @@ +package imagemounter + +import ( + "fmt" + "io" + "net/http" + "os" + "path/filepath" + "time" + + log "github.com/sirupsen/logrus" +) + +const ( + githubOwner = "doronz88" + githubRepo = "DeveloperDiskImage" + githubBranch = "main" + personalizedDDI = "PersonalizedImages/Xcode_iOS_DDI_Personalized" + latestDDI = "17E5179g" +) + +var githubHTTPClient = &http.Client{Timeout: 2 * time.Minute} + +func downloadPersonalizedDDI(baseDir string) (string, error) { + cacheDir := filepath.Join(baseDir, "ddi-latest") + restoreDir := filepath.Join(cacheDir, "Restore") + manifestPath := filepath.Join(restoreDir, "BuildManifest.plist") + + if cached, err := isDDICacheValid(manifestPath); err == nil && cached { + log.Infof("using already downloaded DDI (build %s) from: %s", latestDDI, restoreDir) + return restoreDir, nil + } + + log.Infof("downloading personalized DDI (build %s) from GitHub", latestDDI) + + if err := os.MkdirAll(restoreDir, 0o755); err != nil { + return "", err + } + + manifestBytes, err := downloadGitHubFile(personalizedDDI + "/BuildManifest.plist") + if err != nil { + return "", fmt.Errorf("downloadPersonalizedDDI: failed to download BuildManifest.plist: %w", err) + } + if err := os.WriteFile(manifestPath, manifestBytes, 0o644); err != nil { + return "", fmt.Errorf("downloadPersonalizedDDI: failed to write BuildManifest.plist: %w", err) + } + + manifest, err := loadBuildManifest(manifestPath) + if err != nil { + return "", fmt.Errorf("downloadPersonalizedDDI: failed to parse BuildManifest.plist: %w", err) + } + + dmgPath, trustPath := getFilePaths(manifest) + if dmgPath == "" || trustPath == "" { + return "", fmt.Errorf("downloadPersonalizedDDI: could not determine DMG/trustcache paths from manifest") + } + + dmgFullPath := filepath.Join(restoreDir, dmgPath) + trustFullPath := filepath.Join(restoreDir, trustPath) + + if err := downloadGitHubFileToDisk(personalizedDDI+"/Image.dmg", dmgFullPath); err != nil { + return "", fmt.Errorf("downloadPersonalizedDDI: failed to download Image.dmg: %w", err) + } + if err := downloadGitHubFileToDisk(personalizedDDI+"/Image.dmg.trustcache", trustFullPath); err != nil { + return "", fmt.Errorf("downloadPersonalizedDDI: failed to download Image.dmg.trustcache: %w", err) + } + + log.Infof("successfully downloaded DDI to: %s", restoreDir) + return restoreDir, nil +} + +func getFilePaths(manifest buildManifest) (dmgPath string, trustPath string) { + if len(manifest.BuildIdentities) == 0 { + return "", "" + } + identity := manifest.BuildIdentities[0] + for _, i := range manifest.BuildIdentities { + if i.dmgPath() != "" { + identity = i + break + } + } + return identity.dmgPath(), identity.trustCachePath() +} + +func isDDICacheValid(manifestPath string) (bool, error) { + if _, err := os.Stat(manifestPath); os.IsNotExist(err) { + return false, nil + } + manifest, err := loadBuildManifest(manifestPath) + if err != nil { + return false, nil + } + if manifest.ProductBuildVersion != latestDDI { + log.Infof("cached DDI build is %q, need %q — will re-download", + manifest.ProductBuildVersion, latestDDI) + return false, nil + } + dmgPath, trustPath := getFilePaths(manifest) + if dmgPath == "" || trustPath == "" { + return false, nil + } + restoreDir := filepath.Dir(manifestPath) + if _, err := os.Stat(filepath.Join(restoreDir, dmgPath)); os.IsNotExist(err) { + return false, nil + } + if _, err := os.Stat(filepath.Join(restoreDir, trustPath)); os.IsNotExist(err) { + return false, nil + } + return true, nil +} + +func downloadGitHubFile(path string) ([]byte, error) { + url := fmt.Sprintf("https://raw.githubusercontent.com/%s/%s/%s/%s", + githubOwner, githubRepo, githubBranch, path) + resp, err := githubHTTPClient.Get(url) + if err != nil { + return nil, fmt.Errorf("downloadGitHubFile: request failed: %w", err) + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("downloadGitHubFile: unexpected status %d for %s", resp.StatusCode, url) + } + data, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("downloadGitHubFile: failed to read response: %w", err) + } + return data, nil +} + +func downloadGitHubFileToDisk(path string, destPath string) error { + url := fmt.Sprintf("https://raw.githubusercontent.com/%s/%s/%s/%s", + githubOwner, githubRepo, githubBranch, path) + resp, err := githubHTTPClient.Get(url) + if err != nil { + return fmt.Errorf("downloadGitHubFileToDisk: request failed: %w", err) + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("downloadGitHubFileToDisk: unexpected status %d for %s", resp.StatusCode, url) + } + if err := os.MkdirAll(filepath.Dir(destPath), 0o755); err != nil { + return err + } + f, err := os.Create(destPath) + if err != nil { + return fmt.Errorf("downloadGitHubFileToDisk: failed to create file: %w", err) + } + defer f.Close() + if _, err := io.Copy(f, resp.Body); err != nil { + return fmt.Errorf("downloadGitHubFileToDisk: failed to write file: %w", err) + } + return nil +} diff --git a/ios/imagemounter/personalized_image.go b/ios/imagemounter/personalized_image.go index dd15b094..a9894685 100644 --- a/ios/imagemounter/personalized_image.go +++ b/ios/imagemounter/personalized_image.go @@ -10,7 +10,8 @@ import ( ) type buildManifest struct { - BuildIdentities []buildIdentity + ProductBuildVersion string `plist:"ProductBuildVersion"` + BuildIdentities []buildIdentity } func loadBuildManifest(p string) (buildManifest, error) { @@ -37,25 +38,23 @@ func (m buildManifest) findIdentity(identifiers personalizationIdentifiers) (bui return buildIdentity{}, fmt.Errorf("findIdentity: failed to find identity for ApBoardId 0x%x and ApChipId 0x%x", identifiers.BoardId, identifiers.ChipID) } -type buildIdentity struct { - BoardID string `plist:"ApBoardID"` - ChipID string `plist:"ApChipID"` - Manifest struct { - LoadableTrustCache struct { - Digest []byte - Info struct { - Path string - } - } - PersonalizedDmg struct { - Digest []byte - Info struct { - Path string - } - } `plist:"PersonalizedDMG"` +type manifestEntry struct { + Digest []byte + Trusted bool `plist:"Trusted"` + EPRO bool `plist:"EPRO"` + ESEC bool `plist:"ESEC"` + Name string + Info struct { + Path string } } +type buildIdentity struct { + BoardID string `plist:"ApBoardID"` + ChipID string `plist:"ApChipID"` + Manifest map[string]manifestEntry +} + func (b buildIdentity) ApBoardID() int { return hexToInt(b.BoardID) } @@ -64,6 +63,23 @@ func (b buildIdentity) ApChipID() int { return hexToInt(b.ChipID) } +func (b buildIdentity) dmgPath() string { + if entry, ok := b.Manifest["PersonalizedDMG"]; ok { + return entry.Info.Path + } + if entry, ok := b.Manifest["PersonalizedDmg"]; ok { + return entry.Info.Path + } + return "" +} + +func (b buildIdentity) trustCachePath() string { + if entry, ok := b.Manifest["LoadableTrustCache"]; ok { + return entry.Info.Path + } + return "" +} + type personalizationIdentifiers struct { BoardId int ChipID int diff --git a/ios/imagemounter/personalized_image_mounter.go b/ios/imagemounter/personalized_image_mounter.go index 17ca629b..048be6ba 100644 --- a/ios/imagemounter/personalized_image_mounter.go +++ b/ios/imagemounter/personalized_image_mounter.go @@ -1,6 +1,7 @@ package imagemounter import ( + "crypto/sha512" "fmt" "io" "os" @@ -22,6 +23,7 @@ type PersonalizedDeveloperDiskImageMounter struct { version *semver.Version tss tssClient ecid uint64 + entry ios.DeviceEntry } // NewPersonalizedDeveloperDiskImageMounter creates a PersonalizedDeveloperDiskImageMounter for the device entry @@ -46,6 +48,7 @@ func NewPersonalizedDeveloperDiskImageMounter(entry ios.DeviceEntry, version *se version: version, tss: newTssClient(), ecid: ecid, + entry: entry, }, nil } @@ -62,8 +65,9 @@ func (p PersonalizedDeveloperDiskImageMounter) ListImages() ([][]byte, error) { // MountImage mounts the personalized developer disk image present at imagePath. // imagePath needs to point to the 'Restore' directory of the personalized developer disk image. // -// MountImage gets device identifiers and a nonce from the device first, which needs to be signed by Apple -// and after that the developer disk image is sent to the device with this signature to be able to mount it. +// MountImage first tries to reuse an existing device-side manifest via QueryPersonalizationManifest +// to avoid unnecessary Apple TSS requests on re-mounts. If no manifest exists, it falls back +// to querying a nonce and getting a new signature from Apple's TSS server. func (p PersonalizedDeveloperDiskImageMounter) MountImage(imagePath string) error { manifest, err := loadBuildManifest(path.Join(imagePath, "BuildManifest.plist")) if err != nil { @@ -74,24 +78,40 @@ func (p PersonalizedDeveloperDiskImageMounter) MountImage(imagePath string) erro if err != nil { return fmt.Errorf("MountImage: failed to query personalization identifiers: %w", err) } - nonce, err := p.queryPersonalizedImageNonce() - if err != nil { - return fmt.Errorf("MountImage: failed to get nonce: %w", err) - } identity, err := manifest.findIdentity(identifiers) if err != nil { return fmt.Errorf("MountImage: could not find identity for identifiers %+v: %w", identifiers, err) } - signature, err := p.tss.getSignature(identity, identifiers, nonce, p.ecid) + dmgPath := path.Join(imagePath, identity.dmgPath()) + + signature, err := p.queryPersonalizationManifest(dmgPath) if err != nil { - return fmt.Errorf("MountImage: failed to get signature from Apple: %w", err) - } + log.Info("no existing device-side manifest, requesting new signature from Apple TSS") + + p, err = p.closeAndReconnect() + if err != nil { + return fmt.Errorf("MountImage: failed to reconnect after manifest query: %w", err) + } - dmgPath := path.Join(imagePath, identity.Manifest.PersonalizedDmg.Info.Path) + nonce, err := p.queryPersonalizedImageNonce() + if err != nil { + return fmt.Errorf("MountImage: failed to get nonce: %w", err) + } + + signature, err = p.tss.getSignature(identity, identifiers, nonce, p.ecid) + if err != nil { + return fmt.Errorf("MountImage: failed to get signature from Apple: %w", err) + } + } else { + log.Info("reusing existing device-side manifest, skipping Apple TSS") + } imageSize, err := getFileSize(dmgPath) + if err != nil { + return fmt.Errorf("MountImage: %w", err) + } err = sendUploadRequest(p.plistRw, "Personalized", signature, imageSize) if err != nil { @@ -112,7 +132,7 @@ func (p PersonalizedDeveloperDiskImageMounter) MountImage(imagePath string) erro return err } - trustCache, err := os.ReadFile(path.Join(imagePath, identity.Manifest.LoadableTrustCache.Info.Path)) + trustCache, err := os.ReadFile(path.Join(imagePath, identity.trustCachePath())) if err != nil { return fmt.Errorf("MountImage: could not load trust-cache. %w", err) } @@ -142,6 +162,59 @@ func (p PersonalizedDeveloperDiskImageMounter) UnmountImage() error { return nil } +func (p PersonalizedDeveloperDiskImageMounter) queryPersonalizationManifest(dmgPath string) ([]byte, error) { + f, err := os.Open(dmgPath) + if err != nil { + return nil, fmt.Errorf("queryPersonalizationManifest: failed to open DMG: %w", err) + } + defer f.Close() + + h := sha512.New384() + if _, err := io.Copy(h, f); err != nil { + return nil, fmt.Errorf("queryPersonalizationManifest: failed to hash DMG: %w", err) + } + digest := h.Sum(nil) + + err = p.plistRw.Write(map[string]interface{}{ + "Command": "QueryPersonalizationManifest", + "PersonalizedImageType": "DeveloperDiskImage", + "ImageType": "DeveloperDiskImage", + "ImageSignature": digest, + }) + if err != nil { + return nil, fmt.Errorf("queryPersonalizationManifest: failed to write command: %w", err) + } + + var resp map[string]interface{} + err = p.plistRw.Read(&resp) + if err != nil { + return nil, fmt.Errorf("queryPersonalizationManifest: failed to read response: %w", err) + } + + if sig, ok := resp["ImageSignature"].([]byte); ok { + return sig, nil + } + return nil, fmt.Errorf("queryPersonalizationManifest: no ImageSignature in response %+v", resp) +} + +func (p PersonalizedDeveloperDiskImageMounter) closeAndReconnect() (PersonalizedDeveloperDiskImageMounter, error) { + p.deviceConn.Close() + + deviceConn, err := ios.ConnectToService(p.entry, serviceName) + if err != nil { + return p, fmt.Errorf("closeAndReconnect: failed to reconnect to %s: %w", serviceName, err) + } + + return PersonalizedDeveloperDiskImageMounter{ + deviceConn: deviceConn, + plistRw: ios.NewPlistCodecReadWriter(deviceConn.Reader(), deviceConn.Writer()), + version: p.version, + tss: p.tss, + ecid: p.ecid, + entry: p.entry, + }, nil +} + func (p PersonalizedDeveloperDiskImageMounter) queryPersonalizedImageNonce() ([]byte, error) { err := p.plistRw.Write(map[string]interface{}{ "Command": "QueryNonce", diff --git a/ios/imagemounter/tss.go b/ios/imagemounter/tss.go index 14250640..488c5376 100644 --- a/ios/imagemounter/tss.go +++ b/ios/imagemounter/tss.go @@ -10,6 +10,7 @@ import ( "strings" "time" + "github.com/google/uuid" "howett.net/plist" ) @@ -34,7 +35,8 @@ func (t tssClient) getSignature(identity buildIdentity, identifiers personalizat "@ApImg4Ticket": true, "@BBTicket": true, "@HostPlatformInfo": "mac", - "@VersionInfo": "libauthinstall-973.40.2", + "@VersionInfo": "libauthinstall-1104.0.9", + "@UUID": uuid.New().String(), "ApBoardID": identifiers.BoardId, "ApChipID": identifiers.ChipID, "ApECID": ecid, @@ -42,23 +44,28 @@ func (t tssClient) getSignature(identity buildIdentity, identifiers personalizat "ApProductionMode": true, "ApSecurityDomain": identifiers.SecurityDomain, "ApSecurityMode": true, - "LoadableTrustCache": map[string]interface{}{ - "Digest": identity.Manifest.LoadableTrustCache.Digest, - "EPRO": true, - "ESEC": true, - "Trusted": true, - }, + "SepNonce": make([]byte, 20), + "UID_MODE": false, + } - "PersonalizedDMG": map[string]interface{}{ - "Digest": identity.Manifest.PersonalizedDmg.Digest, - "EPRO": true, - "ESEC": true, - "Name": "DeveloperDiskImage", + for key, entry := range identity.Manifest { + if !entry.Trusted { + continue + } + entryParams := map[string]interface{}{ + "Digest": entry.Digest, "Trusted": true, - }, - - "SepNonce": make([]byte, 20), - "UID_MODE": false, + "EPRO": entry.EPRO, + "ESEC": entry.ESEC, + } + if key == "PersonalizedDMG" || key == "PersonalizedDmg" { + if entry.Name != "" { + entryParams["Name"] = entry.Name + } else { + entryParams["Name"] = "DeveloperDiskImage" + } + } + params[key] = entryParams } for k, v := range identifiers.AdditionalIdentifiers {