ipa-forge 0.1.0__tar.gz

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.
Files changed (203) hide show
  1. ipa_forge-0.1.0/.github/workflows/ci.yml +50 -0
  2. ipa_forge-0.1.0/.gitignore +17 -0
  3. ipa_forge-0.1.0/.gitmodules +9 -0
  4. ipa_forge-0.1.0/CHANGELOG.md +38 -0
  5. ipa_forge-0.1.0/CLAUDE.md +200 -0
  6. ipa_forge-0.1.0/LICENSE +674 -0
  7. ipa_forge-0.1.0/Makefile +48 -0
  8. ipa_forge-0.1.0/PKG-INFO +914 -0
  9. ipa_forge-0.1.0/README.md +199 -0
  10. ipa_forge-0.1.0/ROADMAP.md +310 -0
  11. ipa_forge-0.1.0/STATE.md +139 -0
  12. ipa_forge-0.1.0/docs/README.md +44 -0
  13. ipa_forge-0.1.0/docs/adding-a-feature.md +143 -0
  14. ipa_forge-0.1.0/docs/adding-an-app.md +164 -0
  15. ipa_forge-0.1.0/docs/altstore_device_testing.md +46 -0
  16. ipa_forge-0.1.0/docs/architecture.md +215 -0
  17. ipa_forge-0.1.0/docs/extensibility.md +78 -0
  18. ipa_forge-0.1.0/docs/patch-reference.md +396 -0
  19. ipa_forge-0.1.0/docs/reverse-engineering.md +125 -0
  20. ipa_forge-0.1.0/docs/troubleshooting.md +188 -0
  21. ipa_forge-0.1.0/docs/usage.md +324 -0
  22. ipa_forge-0.1.0/fixtures/patches/assets/patched_asset.txt +1 -0
  23. ipa_forge-0.1.0/fixtures/patches/example.yaml +20 -0
  24. ipa_forge-0.1.0/fixtures/patches/example_dylib_inject.yaml +15 -0
  25. ipa_forge-0.1.0/fixtures/patches/example_plist_edit.yaml +19 -0
  26. ipa_forge-0.1.0/fixtures/synthetic_app/AppInfo.plist +33 -0
  27. ipa_forge-0.1.0/fixtures/synthetic_app/FrameworkInfo.plist +20 -0
  28. ipa_forge-0.1.0/fixtures/synthetic_app/asset.txt +1 -0
  29. ipa_forge-0.1.0/fixtures/synthetic_app/src/framework_lib.c +3 -0
  30. ipa_forge-0.1.0/fixtures/synthetic_app/src/hook_lib.c +5 -0
  31. ipa_forge-0.1.0/fixtures/synthetic_app/src/main.c +17 -0
  32. ipa_forge-0.1.0/fixtures/synthetic_app.ipa +0 -0
  33. ipa_forge-0.1.0/ipa_forge/__init__.py +1 -0
  34. ipa_forge-0.1.0/ipa_forge/altstore/__init__.py +1 -0
  35. ipa_forge-0.1.0/ipa_forge/altstore/source.py +44 -0
  36. ipa_forge-0.1.0/ipa_forge/analysis/__init__.py +18 -0
  37. ipa_forge-0.1.0/ipa_forge/analysis/classdump.py +103 -0
  38. ipa_forge-0.1.0/ipa_forge/analysis/diff.py +127 -0
  39. ipa_forge-0.1.0/ipa_forge/analysis/security.py +72 -0
  40. ipa_forge-0.1.0/ipa_forge/analysis/strings.py +40 -0
  41. ipa_forge-0.1.0/ipa_forge/analysis/symbols.py +42 -0
  42. ipa_forge-0.1.0/ipa_forge/analysis/type_encoding.py +164 -0
  43. ipa_forge-0.1.0/ipa_forge/bundle/__init__.py +1 -0
  44. ipa_forge-0.1.0/ipa_forge/bundle/inventory.py +96 -0
  45. ipa_forge-0.1.0/ipa_forge/bundle/ipa.py +81 -0
  46. ipa_forge-0.1.0/ipa_forge/bundle/models.py +58 -0
  47. ipa_forge-0.1.0/ipa_forge/bundle/plist.py +18 -0
  48. ipa_forge-0.1.0/ipa_forge/cli/__init__.py +1 -0
  49. ipa_forge-0.1.0/ipa_forge/cli/analysis.py +225 -0
  50. ipa_forge-0.1.0/ipa_forge/cli/common.py +41 -0
  51. ipa_forge-0.1.0/ipa_forge/cli/hooks.py +357 -0
  52. ipa_forge-0.1.0/ipa_forge/cli/main.py +170 -0
  53. ipa_forge-0.1.0/ipa_forge/gui/__init__.py +1 -0
  54. ipa_forge-0.1.0/ipa_forge/gui/analysis.html +229 -0
  55. ipa_forge-0.1.0/ipa_forge/gui/app.py +304 -0
  56. ipa_forge-0.1.0/ipa_forge/gui/index.html +244 -0
  57. ipa_forge-0.1.0/ipa_forge/hooks/__init__.py +0 -0
  58. ipa_forge-0.1.0/ipa_forge/hooks/scan.py +81 -0
  59. ipa_forge-0.1.0/ipa_forge/hooks/verify.py +348 -0
  60. ipa_forge-0.1.0/ipa_forge/machO/__init__.py +1 -0
  61. ipa_forge-0.1.0/ipa_forge/machO/arch.py +94 -0
  62. ipa_forge-0.1.0/ipa_forge/machO/detect.py +50 -0
  63. ipa_forge-0.1.0/ipa_forge/machO/injector.py +99 -0
  64. ipa_forge-0.1.0/ipa_forge/machO/objc.py +534 -0
  65. ipa_forge-0.1.0/ipa_forge/manifest.py +74 -0
  66. ipa_forge-0.1.0/ipa_forge/patch/__init__.py +1 -0
  67. ipa_forge-0.1.0/ipa_forge/patch/base.py +48 -0
  68. ipa_forge-0.1.0/ipa_forge/patch/binary.py +131 -0
  69. ipa_forge-0.1.0/ipa_forge/patch/dylib.py +59 -0
  70. ipa_forge-0.1.0/ipa_forge/patch/engine.py +48 -0
  71. ipa_forge-0.1.0/ipa_forge/patch/loader.py +88 -0
  72. ipa_forge-0.1.0/ipa_forge/patch/paths.py +19 -0
  73. ipa_forge-0.1.0/ipa_forge/patch/plist.py +61 -0
  74. ipa_forge-0.1.0/ipa_forge/patch/resolver.py +37 -0
  75. ipa_forge-0.1.0/ipa_forge/patch/resource.py +135 -0
  76. ipa_forge-0.1.0/ipa_forge/patch/schema.py +118 -0
  77. ipa_forge-0.1.0/ipa_forge/patch/version.py +25 -0
  78. ipa_forge-0.1.0/ipa_forge/patches.py +76 -0
  79. ipa_forge-0.1.0/ipa_forge/pipeline.py +235 -0
  80. ipa_forge-0.1.0/ipa_forge/signing/__init__.py +1 -0
  81. ipa_forge-0.1.0/ipa_forge/signing/backend.py +111 -0
  82. ipa_forge-0.1.0/ipa_forge/signing/pipeline.py +99 -0
  83. ipa_forge-0.1.0/ipa_forge/signing/profile.py +131 -0
  84. ipa_forge-0.1.0/ipa_forge/signing/provider.py +113 -0
  85. ipa_forge-0.1.0/ipa_forge/signing/reconcile.py +27 -0
  86. ipa_forge-0.1.0/ipa_forge/validators/__init__.py +1 -0
  87. ipa_forge-0.1.0/ipa_forge/validators/archive_validator.py +25 -0
  88. ipa_forge-0.1.0/ipa_forge/validators/bundle_validator.py +27 -0
  89. ipa_forge-0.1.0/ipa_forge/validators/ipa_validator.py +39 -0
  90. ipa_forge-0.1.0/patches/instagram/.gitignore +5 -0
  91. ipa_forge-0.1.0/patches/instagram/PLAYBOOK.md +82 -0
  92. ipa_forge-0.1.0/patches/instagram/README.md +92 -0
  93. ipa_forge-0.1.0/patches/instagram/SOURCES.md +69 -0
  94. ipa_forge-0.1.0/patches/instagram/dylib/.clangd +9 -0
  95. ipa_forge-0.1.0/patches/instagram/dylib/ABTesting.m +22 -0
  96. ipa_forge-0.1.0/patches/instagram/dylib/AdBlock.m +43 -0
  97. ipa_forge-0.1.0/patches/instagram/dylib/BetaFix.m +21 -0
  98. ipa_forge-0.1.0/patches/instagram/dylib/CopyText.m +54 -0
  99. ipa_forge-0.1.0/patches/instagram/dylib/Helpers.m +258 -0
  100. ipa_forge-0.1.0/patches/instagram/dylib/IGModHook.h +69 -0
  101. ipa_forge-0.1.0/patches/instagram/dylib/IGModHook.m +24 -0
  102. ipa_forge-0.1.0/patches/instagram/dylib/KeychainFix.m +14 -0
  103. ipa_forge-0.1.0/patches/instagram/dylib/MediaDownload.m +539 -0
  104. ipa_forge-0.1.0/patches/instagram/dylib/SafeMode.m +25 -0
  105. ipa_forge-0.1.0/patches/instagram/dylib/SettingsUI.m +35 -0
  106. ipa_forge-0.1.0/patches/instagram/dylib/StoryPrivacy.m +121 -0
  107. ipa_forge-0.1.0/patches/instagram/dylib/build.sh +7 -0
  108. ipa_forge-0.1.0/patches/instagram/instagram.yaml +202 -0
  109. ipa_forge-0.1.0/patches/spotify/.gitignore +5 -0
  110. ipa_forge-0.1.0/patches/spotify/PLAYBOOK.md +77 -0
  111. ipa_forge-0.1.0/patches/spotify/README.md +117 -0
  112. ipa_forge-0.1.0/patches/spotify/SOURCES.md +27 -0
  113. ipa_forge-0.1.0/patches/spotify/TESTING.md +342 -0
  114. ipa_forge-0.1.0/patches/spotify/dev/dylib-test/MinimalHook.m +10 -0
  115. ipa_forge-0.1.0/patches/spotify/dev/dylib-test/build.sh +11 -0
  116. ipa_forge-0.1.0/patches/spotify/dev/spotify-test.yaml +32 -0
  117. ipa_forge-0.1.0/patches/spotify/dylib/.clangd +18 -0
  118. ipa_forge-0.1.0/patches/spotify/dylib/AdBlock.m +121 -0
  119. ipa_forge-0.1.0/patches/spotify/dylib/AudioQuality.m +42 -0
  120. ipa_forge-0.1.0/patches/spotify/dylib/PBProto.h +64 -0
  121. ipa_forge-0.1.0/patches/spotify/dylib/PBProto.m +261 -0
  122. ipa_forge-0.1.0/patches/spotify/dylib/PremiumPatch.m +522 -0
  123. ipa_forge-0.1.0/patches/spotify/dylib/SessionProtection.m +188 -0
  124. ipa_forge-0.1.0/patches/spotify/dylib/SettingsUI.m +490 -0
  125. ipa_forge-0.1.0/patches/spotify/dylib/SideloadFix.m +40 -0
  126. ipa_forge-0.1.0/patches/spotify/dylib/SpotifyFeatures.m +139 -0
  127. ipa_forge-0.1.0/patches/spotify/dylib/SpotifyHook.h +140 -0
  128. ipa_forge-0.1.0/patches/spotify/dylib/SpotifyHook.m +47 -0
  129. ipa_forge-0.1.0/patches/spotify/dylib/StartupTab.m +111 -0
  130. ipa_forge-0.1.0/patches/spotify/dylib/TabBarFix.m +561 -0
  131. ipa_forge-0.1.0/patches/spotify/dylib/build.sh +24 -0
  132. ipa_forge-0.1.0/patches/spotify/spotify.yaml +137 -0
  133. ipa_forge-0.1.0/patches/youtube/.gitignore +5 -0
  134. ipa_forge-0.1.0/patches/youtube/PLAYBOOK.md +156 -0
  135. ipa_forge-0.1.0/patches/youtube/README.md +163 -0
  136. ipa_forge-0.1.0/patches/youtube/ROADMAP.md +637 -0
  137. ipa_forge-0.1.0/patches/youtube/SOURCES.md +33 -0
  138. ipa_forge-0.1.0/patches/youtube/TESTING.md +79 -0
  139. ipa_forge-0.1.0/patches/youtube/assets/90s-video.mp4 +0 -0
  140. ipa_forge-0.1.0/patches/youtube/dylib/.clangd +18 -0
  141. ipa_forge-0.1.0/patches/youtube/dylib/AdBlock.m +251 -0
  142. ipa_forge-0.1.0/patches/youtube/dylib/Appearance.m +185 -0
  143. ipa_forge-0.1.0/patches/youtube/dylib/DownloadCore.m +353 -0
  144. ipa_forge-0.1.0/patches/youtube/dylib/DownloadUI.m +224 -0
  145. ipa_forge-0.1.0/patches/youtube/dylib/FeedShorts.m +367 -0
  146. ipa_forge-0.1.0/patches/youtube/dylib/KeepScreenOn.m +22 -0
  147. ipa_forge-0.1.0/patches/youtube/dylib/LoopVideo.m +42 -0
  148. ipa_forge-0.1.0/patches/youtube/dylib/MiscFeatures.m +382 -0
  149. ipa_forge-0.1.0/patches/youtube/dylib/NativeShare.m +191 -0
  150. ipa_forge-0.1.0/patches/youtube/dylib/NavbarTabbar.m +295 -0
  151. ipa_forge-0.1.0/patches/youtube/dylib/PlayerFeatures.m +1017 -0
  152. ipa_forge-0.1.0/patches/youtube/dylib/PlayerGestures.m +275 -0
  153. ipa_forge-0.1.0/patches/youtube/dylib/RYDDislikes.m +166 -0
  154. ipa_forge-0.1.0/patches/youtube/dylib/SettingsCleaner.m +75 -0
  155. ipa_forge-0.1.0/patches/youtube/dylib/SettingsUI.m +514 -0
  156. ipa_forge-0.1.0/patches/youtube/dylib/SignInFix.m +333 -0
  157. ipa_forge-0.1.0/patches/youtube/dylib/SponsorBlock.m +190 -0
  158. ipa_forge-0.1.0/patches/youtube/dylib/YTFFeatures.m +421 -0
  159. ipa_forge-0.1.0/patches/youtube/dylib/YTFreedom.h +387 -0
  160. ipa_forge-0.1.0/patches/youtube/dylib/YTFreedom.m +40 -0
  161. ipa_forge-0.1.0/patches/youtube/dylib/build.sh +25 -0
  162. ipa_forge-0.1.0/patches/youtube/tools/generate_hooks_manifest.py +93 -0
  163. ipa_forge-0.1.0/patches/youtube/youtube.yaml +729 -0
  164. ipa_forge-0.1.0/pyproject.toml +102 -0
  165. ipa_forge-0.1.0/pyrightconfig.json +5 -0
  166. ipa_forge-0.1.0/scripts/rebuild_fixture.sh +47 -0
  167. ipa_forge-0.1.0/tests/__init__.py +1 -0
  168. ipa_forge-0.1.0/tests/conftest.py +165 -0
  169. ipa_forge-0.1.0/tests/integration/__init__.py +1 -0
  170. ipa_forge-0.1.0/tests/integration/test_gui.py +84 -0
  171. ipa_forge-0.1.0/tests/integration/test_gui_analysis.py +72 -0
  172. ipa_forge-0.1.0/tests/integration/test_pipeline.py +69 -0
  173. ipa_forge-0.1.0/tests/integration/test_pipeline_dylib_inject.py +42 -0
  174. ipa_forge-0.1.0/tests/integration/test_pipeline_plist_edit.py +39 -0
  175. ipa_forge-0.1.0/tests/integration/test_profile_parsing.py +40 -0
  176. ipa_forge-0.1.0/tests/integration/test_signing.py +115 -0
  177. ipa_forge-0.1.0/tests/unit/__init__.py +1 -0
  178. ipa_forge-0.1.0/tests/unit/test_altstore_export.py +32 -0
  179. ipa_forge-0.1.0/tests/unit/test_analysis_diff.py +81 -0
  180. ipa_forge-0.1.0/tests/unit/test_analysis_strings_symbols_security.py +50 -0
  181. ipa_forge-0.1.0/tests/unit/test_arch.py +87 -0
  182. ipa_forge-0.1.0/tests/unit/test_binary_patch.py +131 -0
  183. ipa_forge-0.1.0/tests/unit/test_classdump.py +61 -0
  184. ipa_forge-0.1.0/tests/unit/test_cli.py +329 -0
  185. ipa_forge-0.1.0/tests/unit/test_cli_analysis.py +82 -0
  186. ipa_forge-0.1.0/tests/unit/test_cli_analysis_diff.py +75 -0
  187. ipa_forge-0.1.0/tests/unit/test_cli_analysis_strings_symbols_security.py +59 -0
  188. ipa_forge-0.1.0/tests/unit/test_engine.py +56 -0
  189. ipa_forge-0.1.0/tests/unit/test_gui_app.py +103 -0
  190. ipa_forge-0.1.0/tests/unit/test_hooks_engine.py +439 -0
  191. ipa_forge-0.1.0/tests/unit/test_hooks_verify.py +276 -0
  192. ipa_forge-0.1.0/tests/unit/test_injector.py +59 -0
  193. ipa_forge-0.1.0/tests/unit/test_inventory_bundle_id.py +78 -0
  194. ipa_forge-0.1.0/tests/unit/test_ipa_roundtrip.py +37 -0
  195. ipa_forge-0.1.0/tests/unit/test_machO_detect.py +39 -0
  196. ipa_forge-0.1.0/tests/unit/test_machO_objc.py +86 -0
  197. ipa_forge-0.1.0/tests/unit/test_plist_edit.py +84 -0
  198. ipa_forge-0.1.0/tests/unit/test_profile_pool.py +99 -0
  199. ipa_forge-0.1.0/tests/unit/test_reconcile.py +45 -0
  200. ipa_forge-0.1.0/tests/unit/test_resolver_and_schema.py +169 -0
  201. ipa_forge-0.1.0/tests/unit/test_resources.py +124 -0
  202. ipa_forge-0.1.0/tests/unit/test_sign_bundle.py +113 -0
  203. ipa_forge-0.1.0/tests/unit/test_type_encoding.py +59 -0
@@ -0,0 +1,50 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ jobs:
10
+ # Pure static checks: OS-independent, no macOS tooling or patch submodules
11
+ # needed, so this runs on ubuntu-latest for speed.
12
+ lint:
13
+ runs-on: ubuntu-latest
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+ - uses: actions/setup-python@v5
17
+ with:
18
+ python-version: "3.11" # matches pyproject.toml's requires-python floor / [tool.mypy] python_version
19
+ cache: pip
20
+ - name: Install
21
+ run: pip install -e ".[dev]"
22
+ - name: Ruff check
23
+ run: ruff check .
24
+ - name: Ruff format
25
+ run: ruff format --check .
26
+ - name: Mypy
27
+ run: mypy ipa_forge/
28
+
29
+ # macOS-only: many unit tests compile real Mach-O binaries via `clang` and
30
+ # inspect them with `otool`/`lipo` (see tests/conftest.py) -- Apple-only
31
+ # tools not available on Linux runners, so the suite only runs here.
32
+ # macOS-marked integration tests that need a real Keychain signing
33
+ # identity self-skip gracefully (tests/conftest.py::synthetic_profile) --
34
+ # GitHub's macos-latest runners don't have one, so those run as skips.
35
+ # Single Python version (matches pyproject.toml's requires-python floor):
36
+ # macOS runner minutes are the expensive GH-hosted tier, and this repo
37
+ # doesn't carry per-version-specific code paths worth multiplying CI cost
38
+ # across the whole 3.11-3.13 support range to catch.
39
+ test:
40
+ runs-on: macos-latest
41
+ steps:
42
+ - uses: actions/checkout@v4
43
+ - uses: actions/setup-python@v5
44
+ with:
45
+ python-version: "3.11"
46
+ cache: pip
47
+ - name: Install
48
+ run: pip install -e ".[dev]"
49
+ - name: Pytest
50
+ run: pytest tests/
@@ -0,0 +1,17 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.pyc
4
+ .pytest_cache/
5
+ *.egg-info/
6
+ build/
7
+ dist/
8
+ .DS_Store
9
+ *.ipa.tmp/
10
+ .pi/
11
+ # staged tweak artifacts are built locally; keep the patch definitions only
12
+ patches/*/build*/
13
+ patches/*/dev/build*/
14
+ patches/*/dylib/build/
15
+ patches/*/dev/build-test/
16
+ patches/*/assets/*.dylib
17
+ .coverage
@@ -0,0 +1,9 @@
1
+ [submodule "patches/youtube"]
2
+ path = patches/youtube
3
+ url = https://github.com/nandan-varma/ipa-forge-patches-youtube.git
4
+ [submodule "patches/spotify"]
5
+ path = patches/spotify
6
+ url = https://github.com/nandan-varma/ipa-forge-patches-spotify.git
7
+ [submodule "patches/instagram"]
8
+ path = patches/instagram
9
+ url = https://github.com/nandan-varma/ipa-forge-patches-instagram.git
@@ -0,0 +1,38 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here. Format loosely
4
+ follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
5
+
6
+ ## [0.1.0] - 2026-08-25
7
+
8
+ Initial public release.
9
+
10
+ ### Added
11
+
12
+ - Core patch engine: 17-stage pipeline (extract → dry-run gate → resource
13
+ patches → binary patches → dylib injection → codesign → repackage →
14
+ re-validate) driven entirely by external YAML patch definitions.
15
+ - Patch operation types: `binary_replace`, `resource_replace`,
16
+ `resource_add`, `resource_remove`, `plist_edit`, `dylib_inject`.
17
+ - Hook verification (`forge hooks verify|extract|audit|find|manifest|diff`):
18
+ cross-checks a dylib's declared runtime hook targets against the actual
19
+ Mach-O class/method tables before signing, and `hooks audit` catches
20
+ hooks a dylib calls but the YAML never declared.
21
+ - General-purpose IPA reverse engineering (`forge analysis
22
+ classdump|strings|symbols|security|diff`), plus a read-only `/analysis`
23
+ viewer in the local GUI.
24
+ - Per-extension provisioning-profile signing (repeatable `--profile`,
25
+ `ProfilePool`).
26
+ - Local FastAPI GUI (`forge gui`) for novice-friendly patch application and
27
+ zip-based asset uploads.
28
+ - GPLv3-or-later license.
29
+
30
+ ### Known limitations
31
+
32
+ - macOS-only for signing and Mach-O/ObjC analysis (`hooks/`, `analysis/`,
33
+ `signing/`); `bundle/`, `patch/`, and dry-run work cross-platform.
34
+ - FairPlay/App Store DRM decryption is explicitly out of scope — every
35
+ command assumes an already-decrypted `.ipa`.
36
+ - Instruction-level disassembly, struct field expansion in the type-encoding
37
+ decoder, entitlements diffing, and Swift-native class support are tracked
38
+ in `ROADMAP.md`.
@@ -0,0 +1,200 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## What this is
6
+
7
+ A generic, data-driven iOS IPA patcher: extract a user-supplied `.ipa`, apply
8
+ version-aware patches from external YAML (binary byte patches, resource
9
+ replace/add/remove, dylib injection), and re-sign the result into a
10
+ standard-structure `.ipa` that AltStore Classic can install/refresh.
11
+
12
+ **Read [`STATE.md`](STATE.md) first, every session, before anything else in
13
+ this file or in `docs/`.** It is the single source of truth for *current*
14
+ status (which apps/features are shipped vs. beta/untested, where delivered
15
+ IPAs live right now, decisions already made that shouldn't be re-litigated).
16
+ This file (`CLAUDE.md`) covers what doesn't change session to session:
17
+ architecture, hard constraints, and where to find the doc that answers a
18
+ given question. If something here ever conflicts with `STATE.md`, `STATE.md`
19
+ wins — it's updated far more often.
20
+
21
+ ## Documentation map
22
+
23
+ Route by task, don't read everything:
24
+
25
+ | Task | Read |
26
+ | --- | --- |
27
+ | **Starting a session** | [`STATE.md`](STATE.md) — current status, in-flight work, decisions not to undo |
28
+ | Finding the right doc for anything else | [`docs/README.md`](docs/README.md) — the full doc index, table-of-contents style |
29
+ | Port a **new app** end-to-end | [`docs/adding-an-app.md`](docs/adding-an-app.md) |
30
+ | Add a **feature** to an existing hook dylib | [`docs/adding-a-feature.md`](docs/adding-a-feature.md) |
31
+ | The **YAML patch-definition** contract | [`docs/patch-reference.md`](docs/patch-reference.md) |
32
+ | The **CLI/GUI** reference | [`docs/usage.md`](docs/usage.md) |
33
+ | **Reverse-engineer** any IPA (`forge analysis`: class-dump, strings, symbols, security, diff) | [`docs/reverse-engineering.md`](docs/reverse-engineering.md) |
34
+ | An **error message** | [`docs/troubleshooting.md`](docs/troubleshooting.md) — message → cause → fix |
35
+ | Testing on a **real device** via AltStore | [`docs/altstore_device_testing.md`](docs/altstore_device_testing.md) |
36
+ | **Engine internals** before touching `pipeline.py`/`signing/` | [`docs/architecture.md`](docs/architecture.md) — component map, the 17-stage pipeline, two real bugs found empirically |
37
+ | **Extending** the engine (new operation type, new provider) | [`docs/extensibility.md`](docs/extensibility.md) |
38
+ | **Deferred work** (RE roadmap: disassembly, Swift support, etc.) | [`ROADMAP.md`](ROADMAP.md) — file/anchor pointers to resume |
39
+ | A specific app's **runbook** (build/apply/verify commands, gotchas) | `patches/<app>/PLAYBOOK.md` |
40
+
41
+ The patch sets (`patches/youtube/`, `patches/spotify/`, `patches/instagram/`)
42
+ are private git submodules — `git clone --recursive` to get them, and
43
+ **commit inside each submodule directory separately** before committing the
44
+ updated submodule pointer in this repo; a plain top-level `git add -A` will
45
+ silently skip submodule content changes.
46
+
47
+ ## Commands
48
+
49
+ ```bash
50
+ # Setup (macOS with Xcode CLT; Python 3.11+)
51
+ python3 -m venv .venv && source .venv/bin/activate
52
+ pip install -e .
53
+
54
+ # Run all tests (includes real codesign signing against your local Keychain identity)
55
+ pytest tests/
56
+
57
+ # Run only tests that don't need macOS signing tools (e.g. to check Linux-safe code paths)
58
+ pytest tests/ -m "not macos"
59
+
60
+ # Run a single test
61
+ pytest tests/unit/test_binary_patch.py::test_binary_replace_apply_mutates_file -v
62
+
63
+ # CLI (installed as `forge` via pyproject.toml's [project.scripts])
64
+ forge inspect path/to/App.ipa
65
+ forge validate path/to/App.ipa
66
+ forge patch --ipa <ipa> --patches <patches.yaml> --identity <id> --profile <profile> --output <out.ipa> [--dry-run] [--verbose]
67
+ forge export-source --ipa <patched.ipa> --download-url <url> --output source.json
68
+ forge gui # launches the local FastAPI GUI on 127.0.0.1:8765 (+ /analysis RE viewer)
69
+
70
+ # Hook verification (forge hooks --help): verify | extract | audit | find | manifest | diff
71
+ forge hooks verify --ipa <ipa> --patches <patches.yaml>
72
+ # Cross-check tweak source against the declared hooks: block -- catches a hook
73
+ # the source calls but the YAML forgot to declare (invisible to --dry-run
74
+ # otherwise; this is how real gaps were found in two of the three patch sets)
75
+ forge hooks audit --ipa <ipa> --dir dylib/ --patches <patches.yaml>
76
+
77
+ # General-purpose IPA reverse engineering (forge analysis --help), see docs/reverse-engineering.md
78
+ forge analysis classdump --ipa <ipa> [--class NAME | --search REGEX]
79
+ forge analysis diff --old <old.ipa> --new <new.ipa>
80
+
81
+ # Regenerate the synthetic test fixture (only needed if changing its shape)
82
+ scripts/rebuild_fixture.sh
83
+ ```
84
+
85
+ Lint and typecheck (config lives in `pyproject.toml`): `ruff check .`,
86
+ `ruff format --check .`, `mypy ipa_forge/`.
87
+
88
+ ## Architecture
89
+
90
+ **Hard constraint, never violate it**: Apple's code signature format
91
+ (CodeDirectory, CMS, SuperBlob, DER entitlements) is never reimplemented.
92
+ `ipa_forge/signing/backend.py` is the *only* module allowed to invoke
93
+ `codesign`/`security`; everything else shells out through it.
94
+
95
+ **Dependency direction is one-way and enforced by convention, not tooling**:
96
+ `patch/` and `signing/` both depend on `bundle/` but never on each other.
97
+ `machO/` (arch selection, dylib injection, and the shared ObjC/Mach-O
98
+ analysis engine in `machO/objc.py`) also depends on `bundle/`. `hooks/` and
99
+ `analysis/` are siblings that both depend on `machO/objc.py`'s
100
+ `MachOAnalysis` and never import each other. `pipeline.py` orchestrates
101
+ `patch/`, `signing/`, `hooks/`, `validators/`, and `manifest.py`.
102
+
103
+ **Hook verification (`ipa_forge/hooks/`)**: the `hooks:` block in a patch
104
+ definition declares the dylib's runtime hook targets; `pipeline.py` verifies
105
+ them against the app's main binary (class table + method lists + selrefs,
106
+ chained-fixup aware — parsed by `machO/objc.py`, shared with `analysis/`)
107
+ during the dry-run gate and fails when a `required` hook can't attach. This
108
+ is the safety net for version drift — a renamed/removed class silently
109
+ kills a hook otherwise, **but only for hooks actually declared in the
110
+ `hooks:` block** — a hook the dylib source calls but the block never
111
+ mentions is invisible to the dry-run gate by construction. `forge hooks
112
+ audit --patches <yaml>` closes that specific gap by cross-checking the
113
+ source scan against the declaration (`ipa_forge/hooks/scan.py` +
114
+ `ipa_forge/cli/hooks.py::hooks_audit`); it isn't run automatically by
115
+ `--dry-run`; run it as a standard part of the verify loop, not just when a
116
+ hook seems broken. The CLI surface is `forge hooks
117
+ verify|extract|audit|find|manifest|diff`. `cli/` and `gui/` call into
118
+ `pipeline.py` for patching — neither touches `patch/` or `signing/`
119
+ directly; `cli/` additionally uses `altstore/` (export-source) and
120
+ `validators/` (inspect/validate) directly, and structural IPA validation
121
+ (stage 1) is run by the pipeline, not by `bundle/`.
122
+
123
+ **General-purpose reverse engineering (`ipa_forge/analysis/`)**:
124
+ class-dump, strings, symbols, security posture, and version diffing for
125
+ *any* IPA — not tied to a patch definition. Built on the same
126
+ `machO/objc.py` engine `hooks/` uses; CLI surface is `forge analysis
127
+ classdump|strings|symbols|security|diff`, plus a read-only `/analysis`
128
+ page in the GUI. FairPlay decryption and instruction-level disassembly are
129
+ deliberately out of scope — see `ipa_forge/analysis/__init__.py`'s
130
+ docstring and `ROADMAP.md`.
131
+
132
+ **The core engine is app-agnostic**: it understands patch operation *types*
133
+ (`binary_replace`, `resource_replace`, `dylib_inject`, ...) via the
134
+ `PatchOperation` protocol (`patch/base.py`), never a specific bundle id or
135
+ byte pattern — those only ever come from user-supplied YAML parsed by
136
+ `patch/schema.py` (pydantic) and `patch/loader.py`.
137
+
138
+ ### The pipeline (`ipa_forge/pipeline.py`)
139
+
140
+ A fixed 17-stage sequence: extract → bottom-up inventory → parse Info.plist
141
+ → resolve applicable patch definitions → **dry-run gate** (every op must
142
+ pass before anything mutates) → apply resources → apply binary patches →
143
+ apply dylib injection (deliberately last — LIEF rewrites can shift binary
144
+ offsets) → re-validate → **emit manifest** (pre-signing, for debugging) →
145
+ load/validate provisioning profile → reconcile entitlements → embed profile
146
+ → recursive bottom-up codesign → verify → repackage → final re-validation
147
+ (re-extract from scratch). The ordering encodes real dependencies; see
148
+ `docs/architecture.md` for why each ordering constraint exists.
149
+
150
+ ### Two non-obvious things found empirically (both documented + tested)
151
+
152
+ 1. **`codesign` must target bundle *directories*, not raw executable
153
+ files**, for anything that is itself a bundle (main app, `.framework`,
154
+ `.appex`, watch app) — otherwise the `_CodeSignature/CodeResources` seal
155
+ covering `Info.plist` never gets created and a later
156
+ `codesign --verify --strict` fails. See
157
+ `signing/pipeline.py::sign_target_path`.
158
+ 2. **LIEF's `Binary.write()` on a single arch slice of a fat/universal
159
+ Mach-O silently discards every other architecture.** Injection code must
160
+ always write back through the parent `FatBinary` object
161
+ (`fat.write(path)`), never the extracted slice directly. See
162
+ `machO/injector.py`. `patch/binary.py`'s raw byte-offset read/write is
163
+ unaffected since it never goes through LIEF's write path.
164
+
165
+ ### Signing abstraction
166
+
167
+ `signing/provider.py::SigningProvider` is an ABC with `LocalIdentityProvider`
168
+ (signs via a Keychain identity — the only implementation that exists today)
169
+ and `AltStoreCredentialProvider` (deliberate `NotImplementedError` stub for
170
+ future AltServer-account-based signing — see `docs/extensibility.md` before
171
+ implementing it).
172
+
173
+ ### Linux support boundary
174
+
175
+ `ipa_forge/signing/` needs `codesign`/`security` (macOS only). Less obvious:
176
+ `ipa_forge/machO/objc.py` (the shared ObjC/Mach-O analysis engine —
177
+ `analyze_macho`/`analyze_bundle`) shells out to `otool`/`lipo`, which are
178
+ also macOS-only, unlike the rest of `machO/` (`arch.py`, `injector.py` are
179
+ LIEF-backed and Linux-safe). This means **`hooks/` and `analysis/` are not
180
+ Linux-safe either** — both are built on `machO/objc.py`. Everything else
181
+ (`bundle/`, `patch/`, `machO/arch.py`, `machO/injector.py`, dry-run) works
182
+ without macOS. The boundary is which modules a code path imports, not a
183
+ runtime OS check — no hooks/analysis tests are `@pytest.mark.macos`-gated
184
+ today even though they need Xcode CLT, which is only a gap if someone
185
+ actually runs them on a bare Linux CI runner.
186
+
187
+ ### Test fixture
188
+
189
+ `fixtures/synthetic_app.ipa` is a real, from-scratch arm64 iOS Mach-O app —
190
+ main executable + linked framework + an
191
+ *unlinked* standalone dylib (the dylib-injection target) + a resource file —
192
+ built via `scripts/rebuild_fixture.sh` directly against the iOS SDK rather
193
+ than a hand-authored Xcode project. It's checked in as a binary artifact;
194
+ tests consume it as-is. `tests/conftest.py` also has fixtures for compiling
195
+ throwaway Mach-O binaries on the fly (`compiled_macho_binary`,
196
+ `fat_macho_binary`) and for generating a self-signed test
197
+ `.mobileprovision` (`synthetic_profile`) using whatever real Apple
198
+ Development identity is in the local Keychain — tests requiring real
199
+ signing are marked `@pytest.mark.macos` and skip gracefully if no suitable
200
+ local identity/profile exists.