Skip to content

Commit abc2806

Browse files
committed
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.
1 parent 83e0202 commit abc2806

5 files changed

Lines changed: 303 additions & 44 deletions

File tree

ios/imagemounter/imagedownloader.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,15 @@ func MatchAvailable(version string) string {
122122
}
123123

124124
func Download17Plus(baseDir string, version *semver.Version) (string, error) {
125+
restoreDir, err := downloadPersonalizedDDI(baseDir)
126+
if err != nil {
127+
log.Warnf("GitHub DDI download failed: %v, falling back to deviceboxhq.com", err)
128+
return download17PlusDevicebox(baseDir, version)
129+
}
130+
return restoreDir, nil
131+
}
132+
133+
func download17PlusDevicebox(baseDir string, version *semver.Version) (string, error) {
125134
downloadUrl := fmt.Sprintf("%s%s%s", devicebox, xcode15_4_ddi, ".zip")
126135
log.Infof("device iOS version: %s, getting developer image: %s", version.String(), downloadUrl)
127136

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
package imagemounter
2+
3+
import (
4+
"fmt"
5+
"io"
6+
"net/http"
7+
"os"
8+
"path/filepath"
9+
"time"
10+
11+
log "github.com/sirupsen/logrus"
12+
)
13+
14+
const (
15+
githubOwner = "doronz88"
16+
githubRepo = "DeveloperDiskImage"
17+
githubBranch = "main"
18+
personalizedDDI = "PersonalizedImages/Xcode_iOS_DDI_Personalized"
19+
latestDDI = "17E5179g"
20+
)
21+
22+
var githubHTTPClient = &http.Client{Timeout: 2 * time.Minute}
23+
24+
func downloadPersonalizedDDI(baseDir string) (string, error) {
25+
cacheDir := filepath.Join(baseDir, "ddi-latest")
26+
restoreDir := filepath.Join(cacheDir, "Restore")
27+
manifestPath := filepath.Join(restoreDir, "BuildManifest.plist")
28+
29+
if cached, err := isDDICacheValid(manifestPath); err == nil && cached {
30+
log.Infof("using already downloaded DDI (build %s) from: %s", latestDDI, restoreDir)
31+
return restoreDir, nil
32+
}
33+
34+
log.Infof("downloading personalized DDI (build %s) from GitHub", latestDDI)
35+
36+
if err := os.MkdirAll(restoreDir, 0o755); err != nil {
37+
return "", err
38+
}
39+
40+
manifestBytes, err := downloadGitHubFile(personalizedDDI + "/BuildManifest.plist")
41+
if err != nil {
42+
return "", fmt.Errorf("downloadPersonalizedDDI: failed to download BuildManifest.plist: %w", err)
43+
}
44+
if err := os.WriteFile(manifestPath, manifestBytes, 0o644); err != nil {
45+
return "", fmt.Errorf("downloadPersonalizedDDI: failed to write BuildManifest.plist: %w", err)
46+
}
47+
48+
manifest, err := loadBuildManifest(manifestPath)
49+
if err != nil {
50+
return "", fmt.Errorf("downloadPersonalizedDDI: failed to parse BuildManifest.plist: %w", err)
51+
}
52+
53+
dmgPath, trustPath := getFilePaths(manifest)
54+
if dmgPath == "" || trustPath == "" {
55+
return "", fmt.Errorf("downloadPersonalizedDDI: could not determine DMG/trustcache paths from manifest")
56+
}
57+
58+
dmgFullPath := filepath.Join(restoreDir, dmgPath)
59+
trustFullPath := filepath.Join(restoreDir, trustPath)
60+
61+
if err := downloadGitHubFileToDisk(personalizedDDI+"/Image.dmg", dmgFullPath); err != nil {
62+
return "", fmt.Errorf("downloadPersonalizedDDI: failed to download Image.dmg: %w", err)
63+
}
64+
if err := downloadGitHubFileToDisk(personalizedDDI+"/Image.dmg.trustcache", trustFullPath); err != nil {
65+
return "", fmt.Errorf("downloadPersonalizedDDI: failed to download Image.dmg.trustcache: %w", err)
66+
}
67+
68+
log.Infof("successfully downloaded DDI to: %s", restoreDir)
69+
return restoreDir, nil
70+
}
71+
72+
func getFilePaths(manifest buildManifest) (dmgPath string, trustPath string) {
73+
if len(manifest.BuildIdentities) == 0 {
74+
return "", ""
75+
}
76+
identity := manifest.BuildIdentities[0]
77+
for _, i := range manifest.BuildIdentities {
78+
if i.dmgPath() != "" {
79+
identity = i
80+
break
81+
}
82+
}
83+
return identity.dmgPath(), identity.trustCachePath()
84+
}
85+
86+
func isDDICacheValid(manifestPath string) (bool, error) {
87+
if _, err := os.Stat(manifestPath); os.IsNotExist(err) {
88+
return false, nil
89+
}
90+
manifest, err := loadBuildManifest(manifestPath)
91+
if err != nil {
92+
return false, nil
93+
}
94+
if manifest.ProductBuildVersion != latestDDI {
95+
log.Infof("cached DDI build is %q, need %q — will re-download",
96+
manifest.ProductBuildVersion, latestDDI)
97+
return false, nil
98+
}
99+
dmgPath, trustPath := getFilePaths(manifest)
100+
if dmgPath == "" || trustPath == "" {
101+
return false, nil
102+
}
103+
restoreDir := filepath.Dir(manifestPath)
104+
if _, err := os.Stat(filepath.Join(restoreDir, dmgPath)); os.IsNotExist(err) {
105+
return false, nil
106+
}
107+
if _, err := os.Stat(filepath.Join(restoreDir, trustPath)); os.IsNotExist(err) {
108+
return false, nil
109+
}
110+
return true, nil
111+
}
112+
113+
func downloadGitHubFile(path string) ([]byte, error) {
114+
url := fmt.Sprintf("https://raw.githubusercontent.com/%s/%s/%s/%s",
115+
githubOwner, githubRepo, githubBranch, path)
116+
resp, err := githubHTTPClient.Get(url)
117+
if err != nil {
118+
return nil, fmt.Errorf("downloadGitHubFile: request failed: %w", err)
119+
}
120+
defer resp.Body.Close()
121+
if resp.StatusCode != http.StatusOK {
122+
return nil, fmt.Errorf("downloadGitHubFile: unexpected status %d for %s", resp.StatusCode, url)
123+
}
124+
data, err := io.ReadAll(resp.Body)
125+
if err != nil {
126+
return nil, fmt.Errorf("downloadGitHubFile: failed to read response: %w", err)
127+
}
128+
return data, nil
129+
}
130+
131+
func downloadGitHubFileToDisk(path string, destPath string) error {
132+
url := fmt.Sprintf("https://raw.githubusercontent.com/%s/%s/%s/%s",
133+
githubOwner, githubRepo, githubBranch, path)
134+
resp, err := githubHTTPClient.Get(url)
135+
if err != nil {
136+
return fmt.Errorf("downloadGitHubFileToDisk: request failed: %w", err)
137+
}
138+
defer resp.Body.Close()
139+
if resp.StatusCode != http.StatusOK {
140+
return fmt.Errorf("downloadGitHubFileToDisk: unexpected status %d for %s", resp.StatusCode, url)
141+
}
142+
if err := os.MkdirAll(filepath.Dir(destPath), 0o755); err != nil {
143+
return err
144+
}
145+
f, err := os.Create(destPath)
146+
if err != nil {
147+
return fmt.Errorf("downloadGitHubFileToDisk: failed to create file: %w", err)
148+
}
149+
defer f.Close()
150+
if _, err := io.Copy(f, resp.Body); err != nil {
151+
return fmt.Errorf("downloadGitHubFileToDisk: failed to write file: %w", err)
152+
}
153+
return nil
154+
}

ios/imagemounter/personalized_image.go

Lines changed: 33 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ import (
1010
)
1111

1212
type buildManifest struct {
13-
BuildIdentities []buildIdentity
13+
ProductBuildVersion string `plist:"ProductBuildVersion"`
14+
BuildIdentities []buildIdentity
1415
}
1516

1617
func loadBuildManifest(p string) (buildManifest, error) {
@@ -37,25 +38,23 @@ func (m buildManifest) findIdentity(identifiers personalizationIdentifiers) (bui
3738
return buildIdentity{}, fmt.Errorf("findIdentity: failed to find identity for ApBoardId 0x%x and ApChipId 0x%x", identifiers.BoardId, identifiers.ChipID)
3839
}
3940

40-
type buildIdentity struct {
41-
BoardID string `plist:"ApBoardID"`
42-
ChipID string `plist:"ApChipID"`
43-
Manifest struct {
44-
LoadableTrustCache struct {
45-
Digest []byte
46-
Info struct {
47-
Path string
48-
}
49-
}
50-
PersonalizedDmg struct {
51-
Digest []byte
52-
Info struct {
53-
Path string
54-
}
55-
} `plist:"PersonalizedDMG"`
41+
type manifestEntry struct {
42+
Digest []byte
43+
Trusted bool `plist:"Trusted"`
44+
EPRO bool `plist:"EPRO"`
45+
ESEC bool `plist:"ESEC"`
46+
Name string
47+
Info struct {
48+
Path string
5649
}
5750
}
5851

52+
type buildIdentity struct {
53+
BoardID string `plist:"ApBoardID"`
54+
ChipID string `plist:"ApChipID"`
55+
Manifest map[string]manifestEntry
56+
}
57+
5958
func (b buildIdentity) ApBoardID() int {
6059
return hexToInt(b.BoardID)
6160
}
@@ -64,6 +63,23 @@ func (b buildIdentity) ApChipID() int {
6463
return hexToInt(b.ChipID)
6564
}
6665

66+
func (b buildIdentity) dmgPath() string {
67+
if entry, ok := b.Manifest["PersonalizedDMG"]; ok {
68+
return entry.Info.Path
69+
}
70+
if entry, ok := b.Manifest["PersonalizedDmg"]; ok {
71+
return entry.Info.Path
72+
}
73+
return ""
74+
}
75+
76+
func (b buildIdentity) trustCachePath() string {
77+
if entry, ok := b.Manifest["LoadableTrustCache"]; ok {
78+
return entry.Info.Path
79+
}
80+
return ""
81+
}
82+
6783
type personalizationIdentifiers struct {
6884
BoardId int
6985
ChipID int

ios/imagemounter/personalized_image_mounter.go

Lines changed: 84 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package imagemounter
22

33
import (
4+
"crypto/sha512"
45
"fmt"
56
"io"
67
"os"
@@ -22,6 +23,7 @@ type PersonalizedDeveloperDiskImageMounter struct {
2223
version *semver.Version
2324
tss tssClient
2425
ecid uint64
26+
entry ios.DeviceEntry
2527
}
2628

2729
// NewPersonalizedDeveloperDiskImageMounter creates a PersonalizedDeveloperDiskImageMounter for the device entry
@@ -46,6 +48,7 @@ func NewPersonalizedDeveloperDiskImageMounter(entry ios.DeviceEntry, version *se
4648
version: version,
4749
tss: newTssClient(),
4850
ecid: ecid,
51+
entry: entry,
4952
}, nil
5053
}
5154

@@ -62,8 +65,9 @@ func (p PersonalizedDeveloperDiskImageMounter) ListImages() ([][]byte, error) {
6265
// MountImage mounts the personalized developer disk image present at imagePath.
6366
// imagePath needs to point to the 'Restore' directory of the personalized developer disk image.
6467
//
65-
// MountImage gets device identifiers and a nonce from the device first, which needs to be signed by Apple
66-
// and after that the developer disk image is sent to the device with this signature to be able to mount it.
68+
// MountImage first tries to reuse an existing device-side manifest via QueryPersonalizationManifest
69+
// to avoid unnecessary Apple TSS requests on re-mounts. If no manifest exists, it falls back
70+
// to querying a nonce and getting a new signature from Apple's TSS server.
6771
func (p PersonalizedDeveloperDiskImageMounter) MountImage(imagePath string) error {
6872
manifest, err := loadBuildManifest(path.Join(imagePath, "BuildManifest.plist"))
6973
if err != nil {
@@ -74,24 +78,40 @@ func (p PersonalizedDeveloperDiskImageMounter) MountImage(imagePath string) erro
7478
if err != nil {
7579
return fmt.Errorf("MountImage: failed to query personalization identifiers: %w", err)
7680
}
77-
nonce, err := p.queryPersonalizedImageNonce()
78-
if err != nil {
79-
return fmt.Errorf("MountImage: failed to get nonce: %w", err)
80-
}
8181

8282
identity, err := manifest.findIdentity(identifiers)
8383
if err != nil {
8484
return fmt.Errorf("MountImage: could not find identity for identifiers %+v: %w", identifiers, err)
8585
}
8686

87-
signature, err := p.tss.getSignature(identity, identifiers, nonce, p.ecid)
87+
dmgPath := path.Join(imagePath, identity.dmgPath())
88+
89+
signature, err := p.queryPersonalizationManifest(dmgPath)
8890
if err != nil {
89-
return fmt.Errorf("MountImage: failed to get signature from Apple: %w", err)
90-
}
91+
log.Info("no existing device-side manifest, requesting new signature from Apple TSS")
92+
93+
p, err = p.closeAndReconnect()
94+
if err != nil {
95+
return fmt.Errorf("MountImage: failed to reconnect after manifest query: %w", err)
96+
}
9197

92-
dmgPath := path.Join(imagePath, identity.Manifest.PersonalizedDmg.Info.Path)
98+
nonce, err := p.queryPersonalizedImageNonce()
99+
if err != nil {
100+
return fmt.Errorf("MountImage: failed to get nonce: %w", err)
101+
}
102+
103+
signature, err = p.tss.getSignature(identity, identifiers, nonce, p.ecid)
104+
if err != nil {
105+
return fmt.Errorf("MountImage: failed to get signature from Apple: %w", err)
106+
}
107+
} else {
108+
log.Info("reusing existing device-side manifest, skipping Apple TSS")
109+
}
93110

94111
imageSize, err := getFileSize(dmgPath)
112+
if err != nil {
113+
return fmt.Errorf("MountImage: %w", err)
114+
}
95115

96116
err = sendUploadRequest(p.plistRw, "Personalized", signature, imageSize)
97117
if err != nil {
@@ -112,7 +132,7 @@ func (p PersonalizedDeveloperDiskImageMounter) MountImage(imagePath string) erro
112132
return err
113133
}
114134

115-
trustCache, err := os.ReadFile(path.Join(imagePath, identity.Manifest.LoadableTrustCache.Info.Path))
135+
trustCache, err := os.ReadFile(path.Join(imagePath, identity.trustCachePath()))
116136
if err != nil {
117137
return fmt.Errorf("MountImage: could not load trust-cache. %w", err)
118138
}
@@ -142,6 +162,59 @@ func (p PersonalizedDeveloperDiskImageMounter) UnmountImage() error {
142162
return nil
143163
}
144164

165+
func (p PersonalizedDeveloperDiskImageMounter) queryPersonalizationManifest(dmgPath string) ([]byte, error) {
166+
f, err := os.Open(dmgPath)
167+
if err != nil {
168+
return nil, fmt.Errorf("queryPersonalizationManifest: failed to open DMG: %w", err)
169+
}
170+
defer f.Close()
171+
172+
h := sha512.New384()
173+
if _, err := io.Copy(h, f); err != nil {
174+
return nil, fmt.Errorf("queryPersonalizationManifest: failed to hash DMG: %w", err)
175+
}
176+
digest := h.Sum(nil)
177+
178+
err = p.plistRw.Write(map[string]interface{}{
179+
"Command": "QueryPersonalizationManifest",
180+
"PersonalizedImageType": "DeveloperDiskImage",
181+
"ImageType": "DeveloperDiskImage",
182+
"ImageSignature": digest,
183+
})
184+
if err != nil {
185+
return nil, fmt.Errorf("queryPersonalizationManifest: failed to write command: %w", err)
186+
}
187+
188+
var resp map[string]interface{}
189+
err = p.plistRw.Read(&resp)
190+
if err != nil {
191+
return nil, fmt.Errorf("queryPersonalizationManifest: failed to read response: %w", err)
192+
}
193+
194+
if sig, ok := resp["ImageSignature"].([]byte); ok {
195+
return sig, nil
196+
}
197+
return nil, fmt.Errorf("queryPersonalizationManifest: no ImageSignature in response %+v", resp)
198+
}
199+
200+
func (p PersonalizedDeveloperDiskImageMounter) closeAndReconnect() (PersonalizedDeveloperDiskImageMounter, error) {
201+
p.deviceConn.Close()
202+
203+
deviceConn, err := ios.ConnectToService(p.entry, serviceName)
204+
if err != nil {
205+
return p, fmt.Errorf("closeAndReconnect: failed to reconnect to %s: %w", serviceName, err)
206+
}
207+
208+
return PersonalizedDeveloperDiskImageMounter{
209+
deviceConn: deviceConn,
210+
plistRw: ios.NewPlistCodecReadWriter(deviceConn.Reader(), deviceConn.Writer()),
211+
version: p.version,
212+
tss: p.tss,
213+
ecid: p.ecid,
214+
entry: p.entry,
215+
}, nil
216+
}
217+
145218
func (p PersonalizedDeveloperDiskImageMounter) queryPersonalizedImageNonce() ([]byte, error) {
146219
err := p.plistRw.Write(map[string]interface{}{
147220
"Command": "QueryNonce",

0 commit comments

Comments
 (0)