ipa-dlp 0.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,414 @@
1
+ Metadata-Version: 2.4
2
+ Name: ipa-dlp
3
+ Version: 0.3.0
4
+ Summary: Fetch, decrypt, and inspect personalized App Store IPAs.
5
+ Author: Benjamin Liou
6
+ Requires-Python: >=3.9
7
+ Description-Content-Type: text/markdown
8
+ License-File: LICENSE
9
+
10
+ # ipa-dlp
11
+
12
+ A cross-platform CLI and Python library for fetching, decrypting, and
13
+ extracting App Store IPAs and Mach-O binaries (powered by
14
+ [ipatool](https://github.com/majd/ipatool) and
15
+ [UnfairPlay](https://github.com/DrWhax/unfairplay)). It turns this:
16
+
17
+ ```sh
18
+ ipatool auth login ...
19
+ ipatool download ...
20
+ unfairplay setup ...
21
+ unfairplay decrypt ...
22
+ ```
23
+
24
+ into this:
25
+
26
+ ```sh
27
+ ipa-dlp get spotify@9.1.88
28
+ ```
29
+
30
+ It prompts for your Apple ID in the terminal (including 2FA) by default;
31
+ `--gui` switches to native macOS dialogs. On an Apple Silicon Mac it installs
32
+ pinned copies of both tools, recovers the reusable FairPlay model on first use
33
+ with one admin prompt, downloads the requested app version, decrypts it,
34
+ strips the App Store account data, and verifies the plaintext before reporting
35
+ success. On macOS Intel, Linux, and Windows, search, version history,
36
+ `get --encrypted`, and local extraction work; FairPlay decryption requires an
37
+ Apple Silicon Mac.
38
+
39
+ ## Requirements
40
+
41
+ - Python 3.9+
42
+ - macOS, Linux, or Windows for search, `get -F`, `get --encrypted`, and local
43
+ extraction (`ipatool` is downloaded automatically for your platform)
44
+ - An Apple Silicon Mac for FairPlay decryption (`get` without `--encrypted`,
45
+ `decrypt`, `setup --seed`), plus admin rights for the one-time model recovery
46
+
47
+ ## Platform support
48
+
49
+ | Capability | macOS arm64 | macOS Intel | Linux | Windows |
50
+ | --- | --- | --- | --- | --- |
51
+ | Search, `get -F`, `get --encrypted` | yes | yes | yes | yes |
52
+ | `extract` / `load_macho` on decrypted inputs | yes | yes | yes | yes |
53
+ | `get` (decrypt), `decrypt`, `setup --seed` | yes | no | no | no |
54
+
55
+ Linux amd64/arm64 and Windows amd64/arm64 are covered by ipatool's official
56
+ release builds. `--gui` is macOS-only; the other platforms use terminal
57
+ prompts.
58
+
59
+ ## Install
60
+
61
+ ### Homebrew (macOS, recommended)
62
+
63
+ ```sh
64
+ brew tap ipakit/ipa-dlp
65
+ brew install ipa-dlp
66
+ ```
67
+
68
+ If Homebrew complains that the tap is untrusted (some Homebrew 7 builds gate
69
+ third-party taps), run `brew trust ipakit/ipa-dlp` and retry.
70
+
71
+ Then:
72
+
73
+ ```sh
74
+ ipa-dlp setup
75
+ ```
76
+
77
+ ### From source (macOS)
78
+
79
+ ```sh
80
+ git clone https://github.com/ipakit/ipa-dlp.git
81
+ cd ipa-dlp
82
+ ./install.sh
83
+ ```
84
+
85
+ This installs the `ipa-dlp` entrypoint and its `ipa_dlp.py` module into
86
+ `~/.local/bin/`, registers that directory in the Python user site-packages
87
+ (`ipa_dlp.pth`) so `import ipa_dlp` works from any script, then runs
88
+ `ipa-dlp setup`. No `sudo` is needed; the first decryption asks for admin
89
+ once.
90
+
91
+ If `~/.local/bin` is not on your `PATH`:
92
+
93
+ ```sh
94
+ ./install.sh --add-path
95
+ ```
96
+
97
+ or call the full path.
98
+
99
+ ### pip / pipx (any OS)
100
+
101
+ ```sh
102
+ pipx install ipa-dlp
103
+ # or
104
+ python3 -m pip install ipa-dlp
105
+ ```
106
+
107
+ On Linux and Windows this is the supported install path; `install.sh` is
108
+ macOS-only. `ipatool` is fetched for your OS/architecture on first use.
109
+
110
+ ## Use
111
+
112
+ ### CLI
113
+
114
+ ```sh
115
+ ipa-dlp setup
116
+ ipa-dlp get spotify
117
+ ipa-dlp get spotify@9.1.88
118
+ ipa-dlp get spotify -v 9.1.88
119
+ ipa-dlp get spotify --version 9.1.88
120
+ ipa-dlp get com.spotify.client --version 9.1.88
121
+ ipa-dlp get 324684580 --version 9.1.88
122
+ ipa-dlp get spotify --version 9.1.88 -o ~/Desktop/Spotify.ipa
123
+ ```
124
+
125
+ To download the raw FairPlay-encrypted IPA without decrypting (supported on
126
+ any OS):
127
+
128
+ ```sh
129
+ ipa-dlp get spotify --encrypted
130
+ ```
131
+
132
+ On macOS Intel, Linux, and Windows, a plain `ipa-dlp get` exits with a clear
133
+ message; `--encrypted` is the supported download path there. `-F` and
134
+ `extract` work normally. The encrypted IPA still contains `SC_Info`
135
+ licenses and account metadata, so treat it like `--keep-account-data`.
136
+
137
+ The final path is printed on stdout; progress and diagnostics go to stderr.
138
+ The default output is:
139
+
140
+ ```text
141
+ ~/Downloads/Spotify-9.1.88.ipa
142
+ ```
143
+
144
+ Account data is stripped by default: `SC_Info` licenses are removed and the
145
+ Apple ID/purchaser fields are scrubbed from `iTunesMetadata.plist`.
146
+ ipa-dlp checks the decryption report and the output archive
147
+ before printing `account data stripped: yes`; if either check fails,
148
+ it deletes the output and exits with an error.
149
+
150
+ Use `--keep-account-data` if you need the licenses for further analysis, and
151
+ `--full` to keep the extra artifacts:
152
+
153
+ ```sh
154
+ ipa-dlp get spotify --version 9.1.88 --keep-account-data
155
+ ipa-dlp get spotify --version 9.1.88 --full
156
+ ```
157
+
158
+ `--full` also keeps:
159
+
160
+ ```text
161
+ Spotify-9.1.88.encrypted.ipa
162
+ Spotify-9.1.88.report.json
163
+ ```
164
+
165
+ For `ipa-dlp decrypt`, `--full` keeps the report; the encrypted input
166
+ already exists where you put it.
167
+
168
+ ### Extracting the executable
169
+
170
+ `--extract-binary` also writes the app's arm64 Mach-O executable next to the
171
+ decrypted IPA, ready for a disassembler or symbol dumper:
172
+
173
+ ```sh
174
+ ipa-dlp get spotify --version 9.1.88 --extract-binary
175
+ ipa-dlp decrypt /path/to/App.ipa --extract-binary
176
+ ```
177
+
178
+ ```text
179
+ ~/Downloads/Spotify-9.1.88.ipa
180
+ ~/Downloads/Spotify-9.1.88.bin
181
+ ```
182
+
183
+ The slice is read from the decrypted archive, so it is already FairPlay-free.
184
+ Universal ("fat") executables are reduced to the arm64 slice (falling back to
185
+ x86_64), written with mode `0755`, and the `.bin` path is what gets printed on
186
+ stdout. The IPA is still written; pass `--force` to replace an existing `.bin`.
187
+
188
+ ### Extracting from an existing IPA, .app, or binary
189
+
190
+ `ipa-dlp extract` works on files you already have and does not need an App
191
+ Store account:
192
+
193
+ ```sh
194
+ ipa-dlp extract ~/Downloads/Spotify-9.1.88.ipa # -> Spotify-9.1.88.bin
195
+ ipa-dlp extract ~/Applications/MyApp.app -o MyApp.bin
196
+ ipa-dlp extract ./SomeBinary -o SomeBinary.arm64
197
+ ```
198
+
199
+ For an IPA it reads `CFBundleExecutable`, checks the Mach-O encryption command,
200
+ and extracts directly when the payload is already decrypted. If `cryptid` is
201
+ non-zero, the command requires `SC_Info`, decrypts through UnfairPlay into a
202
+ temporary directory on an Apple Silicon Mac (the usual model/admin rules
203
+ apply), and extracts from that copy. A `.app` or raw Mach-O that is still encrypted cannot be decrypted
204
+ in place; it is extracted with a warning. `-o` templates and `--exec` work as
205
+ described below.
206
+
207
+ `extract` accepts `--json` like the other commands.
208
+
209
+ ### Output templates and hooks
210
+
211
+ `-o` on `get`, `decrypt`, and `extract`, plus `--exec` on all three, accept
212
+ tokens expanded from the app's `Info.plist`:
213
+
214
+ | Token | Value |
215
+ | --- | --- |
216
+ | `%(name)s` | app display name |
217
+ | `%(bundle_id)s` | bundle identifier |
218
+ | `%(version)s` | `CFBundleShortVersionString` |
219
+ | `%(build)s` | `CFBundleVersion` |
220
+ | `%(external_id)s` | Apple external version ID, when known |
221
+
222
+ ```sh
223
+ ipa-dlp get spotify --version 9.1.88 \
224
+ -o '~/Research/spotify/%(version)s-%(build)s.ipa'
225
+
226
+ ipa-dlp get spotify --version 9.1.88 --extract-binary \
227
+ --exec 'cp "{}" ~/Research/spotify/current.bin'
228
+ ```
229
+
230
+ `--exec` runs after the artifacts are written. `{}` is replaced with the final
231
+ artifact path — the `.bin` when `--extract-binary` is set, otherwise the
232
+ `.ipa` — and the command runs through the shell. Expansion uses Python
233
+ `%`-formatting, so write `%%` for a literal `%`. A failing hook exits non-zero
234
+ after the artifacts are kept.
235
+
236
+ For `extract`, tokens are read from an IPA or `.app`; a raw Mach-O only has
237
+ `%(name)s` (its filename stem). `%(external_id)s` is empty for `decrypt` and
238
+ `extract` because the input's version ID is not resolved.
239
+
240
+ ### JSON output
241
+
242
+ `--json` prints a JSON object on stdout and keeps all progress and
243
+ diagnostics on stderr, so a pipeline can parse it without stripping noise:
244
+
245
+ ```sh
246
+ ipa-dlp --json get spotify --version 9.1.88 > result.json
247
+ ipa-dlp --json get spotify --list-versions --limit 5
248
+ ipa-dlp --json status
249
+ ```
250
+
251
+ Results carry `ok`, `command`, the resolved app, `external_id`, the
252
+ `artifact` path, `ipa`/`binary`/`encrypted_ipa` paths where relevant,
253
+ `encrypted`, `verified`, and `account_data_stripped`. `--list-versions` returns a `versions` array. A
254
+ failure still prints one object, `{"ok": false, "error": "..."}`, and
255
+ exits non-zero. With `--exec`, the hook's stdout is redirected to stderr so
256
+ the object stays alone on stdout.
257
+
258
+ ### Library API
259
+
260
+ ```python
261
+ from ipa_dlp import download, decrypt, extract_binary, load_macho
262
+
263
+ ipa = download("spotify", version="9.1.88") # -> Path (decrypted IPA)
264
+ raw = download("spotify", encrypted=True) # -> Path (encrypted IPA)
265
+ ipa = decrypt("/path/to/App.ipa", output="App.ipa")
266
+ bin_path = extract_binary(ipa) # -> Path (<base>.bin)
267
+ data = load_macho("spotify", version="9.1.88") # -> bytes (arm64 slice)
268
+ ```
269
+
270
+ Each function returns its result instead of printing it. Pass `exec_hook=` or
271
+ `reveal=True` only when the side effects are wanted.
272
+
273
+ Other commands:
274
+
275
+ ```sh
276
+ ipa-dlp status
277
+ ipa-dlp decrypt /path/to/App.ipa # Apple Silicon Mac
278
+ ipa-dlp extract /path/to/App.ipa
279
+ ipa-dlp setup --update # reinstall pinned ipatool (and UnfairPlay on Mac)
280
+ ipa-dlp setup --reauth # force a new Apple ID login
281
+ ipa-dlp setup --seed spotify # Apple Silicon Mac: create the model now
282
+ ipa-dlp setup --seed spotify --version 9.1.88
283
+ ```
284
+
285
+ ### Version selection
286
+
287
+ `--version` matches the IPA's `CFBundleShortVersionString`. ipa-dlp
288
+ maps it to Apple's external version identifier by asking the App Store for
289
+ metadata, newest versions first, and caches the mapping in
290
+ `~/Library/Caches/ipa-dlp/versions/`. If the App Store will not serve a version to
291
+ your account, you get an error; `--external-version-id` lets you bypass the
292
+ lookup once you know the ID.
293
+
294
+ The version can be given as `--version 9.1.88` (or `-v`), as a second
295
+ positional (`ipa-dlp get spotify 9.1.88`), or appended to the query
296
+ (`ipa-dlp get spotify@9.1.88`). The `@` form also works with `load_macho`.
297
+
298
+ ### Version history
299
+
300
+ `--list-versions` (`-F`) prints the App Store's version history for an app,
301
+ newest first, without downloading anything:
302
+
303
+ ```sh
304
+ ipa-dlp get spotify --list-versions # newest 15
305
+ ipa-dlp get spotify -F --limit 100 # newest 100
306
+ ```
307
+
308
+ The table goes to stdout:
309
+
310
+ ```text
311
+ EXTERNAL_ID VERSION RELEASE_DATE
312
+ 871294812 9.1.88 2026-03-14
313
+ 870912300 9.1.86 2026-03-07
314
+ ```
315
+
316
+ Metadata for versions that were not resolved before is fetched in parallel and
317
+ stored in the same version cache used by `--version`, so later runs are
318
+ instant. Pipe an `EXTERNAL_ID` into `--external-version-id` to download a
319
+ version that is no longer listed.
320
+
321
+ ### Licenses and paid apps
322
+
323
+ Free apps are licensed automatically when needed. For a paid app, buy it in the
324
+ App Store first; downloading an owned paid app works without extra flags.
325
+ `--purchase` asks Apple to obtain a missing license, which can charge your
326
+ account, so ipa-dlp asks for confirmation before using it on a paid app.
327
+
328
+ ```sh
329
+ ipa-dlp get some-paid-app --purchase
330
+ ```
331
+
332
+ ## On first run
333
+
334
+ 1. Pinned `ipatool 2.7.0` (and `UnfairPlay 0.2.0` on Apple Silicon Macs)
335
+ releases are downloaded and SHA-256 verified into
336
+ `~/.local/share/ipa-dlp/bin/`.
337
+ 2. You are prompted for your Apple ID and password in the terminal. On macOS
338
+ and Linux the password is fed to `ipatool` over a PTY, not as a
339
+ command-line argument; on Windows ipatool's `--password` flag is used for
340
+ that login (visible in the process list while it runs). ipa-dlp does not
341
+ store the password. If Apple asks for a 2FA
342
+ code, you are prompted for it. Pass `--gui` (macOS) to use native dialogs.
343
+ 3. `setup` checks the App Store session and reports the signed-in account.
344
+ 4. On an Apple Silicon Mac, the first `get` downloads the IPA, unpacks its app
345
+ bundle, and `UnfairPlay` recovers the enrollment model. You are asked for
346
+ your login password once (in the terminal by default, or in a macOS dialog
347
+ with `--gui`). The model is saved under
348
+ `~/Library/Application Support/unfairplay/models` and reused for later IPAs
349
+ from the same enrollment/account.
350
+ 5. The IPA is decrypted, account data is stripped, and the output is checked
351
+ against its pre-encryption page hashes before success is reported.
352
+
353
+ On macOS Intel, Linux, and Windows, steps 4-5 do not apply; use
354
+ `ipa-dlp get --encrypted` to keep the raw IPA. Later `get` calls on a Mac do
355
+ not need the admin password.
356
+
357
+ ## Important limitations
358
+
359
+ - **Decryption requires an Apple Silicon Mac.** UnfairPlay depends on
360
+ macOS's FairPlay services, so `get` without `--encrypted`, `decrypt`, and
361
+ `setup --seed` are unavailable on Intel Macs, Linux, and Windows. Those
362
+ platforms can still search, list versions, download with `--encrypted`,
363
+ and extract already-decrypted binaries.
364
+ - **`--encrypted` output is still personalized.** It keeps `SC_Info`
365
+ licenses and account metadata, so treat it like `--keep-account-data`.
366
+ - **UnfairPlay is build-sensitive.** It may fail closed after a macOS update if
367
+ Apple changes the FairPlay state or `fairplayd`. ipa-dlp reports the error.
368
+ - **The model is tied to the Mac's enrollment and Apple ID.** If you sign in to
369
+ a different account, use `ipa-dlp setup --reauth`; a different enrollment
370
+ needs its own model.
371
+ - **Only "store-owned" apps are supported.** If the account cannot obtain the app/version, the download fails.
372
+ - **No App Store password is stored.** `ipatool` stores its own session token in
373
+ the system credential store (the macOS Keychain on Mac). Revoke it with
374
+ `ipatool auth revoke` if needed.
375
+
376
+ ## Privacy and legal use
377
+
378
+ Use this only with apps and accounts you lawfully control, and follow Apple's
379
+ terms. Decrypting an app does not grant redistribution rights. Do not share
380
+ personalized or decrypted IPAs. The default output is stripped, but
381
+ `--keep-account-data` and `--encrypted` output still contain your account
382
+ licenses and metadata.
383
+
384
+ The model file is private. Keep it on encrypted storage and never share it.
385
+
386
+ ## Development / testing
387
+
388
+ Environment overrides:
389
+
390
+ ```sh
391
+ IPADLP_IPATOOL=/path/to/ipatool
392
+ IPADLP_UNFAIRPLAY=/path/to/unfairplay
393
+ IPADLP_BIN_DIR=/tmp/ipa-dlp-bin
394
+ IPADLP_CACHE_DIR=/tmp/ipa-dlp-cache
395
+ IPADLP_DATA_DIR=/tmp/ipa-dlp-data
396
+ UNFAIRPLAY_MODEL_DIR=/tmp/unfairplay-models
397
+ ```
398
+
399
+ Terminal prompts are the default, which works over SSH and in scripts. Pass
400
+ `--gui` (macOS only) to use native dialogs instead.
401
+
402
+ Useful checks:
403
+
404
+ ```sh
405
+ python3 -m py_compile ipa_dlp.py ipa-dlp
406
+ ipa-dlp status
407
+ ipa-dlp --json status
408
+ ipa-dlp get spotify --version 9.1.88 --dry-run
409
+ python3 -m build # wheel + sdist (needs flit_core)
410
+ ```
411
+
412
+ See [RELEASING.md](RELEASING.md) for the automated tag → PyPI → Homebrew
413
+ pipeline and the one-time repository secrets.
414
+
@@ -0,0 +1,6 @@
1
+ ipa_dlp.py,sha256=lVw6BWIqXHDLW3mbOe9VMo621Yfqf3KxuqSSRdtVk0Q,133048
2
+ ipa_dlp-0.3.0.dist-info/entry_points.txt,sha256=VAPH_fMh-ePQ2oNt2Lm1FdQVI1IkeA6pErt3Q6puUYA,40
3
+ ipa_dlp-0.3.0.dist-info/licenses/LICENSE,sha256=HSWVgCD2xJ6_tP1cjGKoFQSKdKGyRifZ69nduZVXVtc,1070
4
+ ipa_dlp-0.3.0.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82
5
+ ipa_dlp-0.3.0.dist-info/METADATA,sha256=X_9RMkZgITJFIGE3kzS9belXh-7Xz4Fmo2DhKnUrYI0,14421
6
+ ipa_dlp-0.3.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: flit 3.12.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ ipa-dlp=ipa_dlp:main
3
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Benjamin Liou
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.