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.
- ipa_forge-0.1.0/.github/workflows/ci.yml +50 -0
- ipa_forge-0.1.0/.gitignore +17 -0
- ipa_forge-0.1.0/.gitmodules +9 -0
- ipa_forge-0.1.0/CHANGELOG.md +38 -0
- ipa_forge-0.1.0/CLAUDE.md +200 -0
- ipa_forge-0.1.0/LICENSE +674 -0
- ipa_forge-0.1.0/Makefile +48 -0
- ipa_forge-0.1.0/PKG-INFO +914 -0
- ipa_forge-0.1.0/README.md +199 -0
- ipa_forge-0.1.0/ROADMAP.md +310 -0
- ipa_forge-0.1.0/STATE.md +139 -0
- ipa_forge-0.1.0/docs/README.md +44 -0
- ipa_forge-0.1.0/docs/adding-a-feature.md +143 -0
- ipa_forge-0.1.0/docs/adding-an-app.md +164 -0
- ipa_forge-0.1.0/docs/altstore_device_testing.md +46 -0
- ipa_forge-0.1.0/docs/architecture.md +215 -0
- ipa_forge-0.1.0/docs/extensibility.md +78 -0
- ipa_forge-0.1.0/docs/patch-reference.md +396 -0
- ipa_forge-0.1.0/docs/reverse-engineering.md +125 -0
- ipa_forge-0.1.0/docs/troubleshooting.md +188 -0
- ipa_forge-0.1.0/docs/usage.md +324 -0
- ipa_forge-0.1.0/fixtures/patches/assets/patched_asset.txt +1 -0
- ipa_forge-0.1.0/fixtures/patches/example.yaml +20 -0
- ipa_forge-0.1.0/fixtures/patches/example_dylib_inject.yaml +15 -0
- ipa_forge-0.1.0/fixtures/patches/example_plist_edit.yaml +19 -0
- ipa_forge-0.1.0/fixtures/synthetic_app/AppInfo.plist +33 -0
- ipa_forge-0.1.0/fixtures/synthetic_app/FrameworkInfo.plist +20 -0
- ipa_forge-0.1.0/fixtures/synthetic_app/asset.txt +1 -0
- ipa_forge-0.1.0/fixtures/synthetic_app/src/framework_lib.c +3 -0
- ipa_forge-0.1.0/fixtures/synthetic_app/src/hook_lib.c +5 -0
- ipa_forge-0.1.0/fixtures/synthetic_app/src/main.c +17 -0
- ipa_forge-0.1.0/fixtures/synthetic_app.ipa +0 -0
- ipa_forge-0.1.0/ipa_forge/__init__.py +1 -0
- ipa_forge-0.1.0/ipa_forge/altstore/__init__.py +1 -0
- ipa_forge-0.1.0/ipa_forge/altstore/source.py +44 -0
- ipa_forge-0.1.0/ipa_forge/analysis/__init__.py +18 -0
- ipa_forge-0.1.0/ipa_forge/analysis/classdump.py +103 -0
- ipa_forge-0.1.0/ipa_forge/analysis/diff.py +127 -0
- ipa_forge-0.1.0/ipa_forge/analysis/security.py +72 -0
- ipa_forge-0.1.0/ipa_forge/analysis/strings.py +40 -0
- ipa_forge-0.1.0/ipa_forge/analysis/symbols.py +42 -0
- ipa_forge-0.1.0/ipa_forge/analysis/type_encoding.py +164 -0
- ipa_forge-0.1.0/ipa_forge/bundle/__init__.py +1 -0
- ipa_forge-0.1.0/ipa_forge/bundle/inventory.py +96 -0
- ipa_forge-0.1.0/ipa_forge/bundle/ipa.py +81 -0
- ipa_forge-0.1.0/ipa_forge/bundle/models.py +58 -0
- ipa_forge-0.1.0/ipa_forge/bundle/plist.py +18 -0
- ipa_forge-0.1.0/ipa_forge/cli/__init__.py +1 -0
- ipa_forge-0.1.0/ipa_forge/cli/analysis.py +225 -0
- ipa_forge-0.1.0/ipa_forge/cli/common.py +41 -0
- ipa_forge-0.1.0/ipa_forge/cli/hooks.py +357 -0
- ipa_forge-0.1.0/ipa_forge/cli/main.py +170 -0
- ipa_forge-0.1.0/ipa_forge/gui/__init__.py +1 -0
- ipa_forge-0.1.0/ipa_forge/gui/analysis.html +229 -0
- ipa_forge-0.1.0/ipa_forge/gui/app.py +304 -0
- ipa_forge-0.1.0/ipa_forge/gui/index.html +244 -0
- ipa_forge-0.1.0/ipa_forge/hooks/__init__.py +0 -0
- ipa_forge-0.1.0/ipa_forge/hooks/scan.py +81 -0
- ipa_forge-0.1.0/ipa_forge/hooks/verify.py +348 -0
- ipa_forge-0.1.0/ipa_forge/machO/__init__.py +1 -0
- ipa_forge-0.1.0/ipa_forge/machO/arch.py +94 -0
- ipa_forge-0.1.0/ipa_forge/machO/detect.py +50 -0
- ipa_forge-0.1.0/ipa_forge/machO/injector.py +99 -0
- ipa_forge-0.1.0/ipa_forge/machO/objc.py +534 -0
- ipa_forge-0.1.0/ipa_forge/manifest.py +74 -0
- ipa_forge-0.1.0/ipa_forge/patch/__init__.py +1 -0
- ipa_forge-0.1.0/ipa_forge/patch/base.py +48 -0
- ipa_forge-0.1.0/ipa_forge/patch/binary.py +131 -0
- ipa_forge-0.1.0/ipa_forge/patch/dylib.py +59 -0
- ipa_forge-0.1.0/ipa_forge/patch/engine.py +48 -0
- ipa_forge-0.1.0/ipa_forge/patch/loader.py +88 -0
- ipa_forge-0.1.0/ipa_forge/patch/paths.py +19 -0
- ipa_forge-0.1.0/ipa_forge/patch/plist.py +61 -0
- ipa_forge-0.1.0/ipa_forge/patch/resolver.py +37 -0
- ipa_forge-0.1.0/ipa_forge/patch/resource.py +135 -0
- ipa_forge-0.1.0/ipa_forge/patch/schema.py +118 -0
- ipa_forge-0.1.0/ipa_forge/patch/version.py +25 -0
- ipa_forge-0.1.0/ipa_forge/patches.py +76 -0
- ipa_forge-0.1.0/ipa_forge/pipeline.py +235 -0
- ipa_forge-0.1.0/ipa_forge/signing/__init__.py +1 -0
- ipa_forge-0.1.0/ipa_forge/signing/backend.py +111 -0
- ipa_forge-0.1.0/ipa_forge/signing/pipeline.py +99 -0
- ipa_forge-0.1.0/ipa_forge/signing/profile.py +131 -0
- ipa_forge-0.1.0/ipa_forge/signing/provider.py +113 -0
- ipa_forge-0.1.0/ipa_forge/signing/reconcile.py +27 -0
- ipa_forge-0.1.0/ipa_forge/validators/__init__.py +1 -0
- ipa_forge-0.1.0/ipa_forge/validators/archive_validator.py +25 -0
- ipa_forge-0.1.0/ipa_forge/validators/bundle_validator.py +27 -0
- ipa_forge-0.1.0/ipa_forge/validators/ipa_validator.py +39 -0
- ipa_forge-0.1.0/patches/instagram/.gitignore +5 -0
- ipa_forge-0.1.0/patches/instagram/PLAYBOOK.md +82 -0
- ipa_forge-0.1.0/patches/instagram/README.md +92 -0
- ipa_forge-0.1.0/patches/instagram/SOURCES.md +69 -0
- ipa_forge-0.1.0/patches/instagram/dylib/.clangd +9 -0
- ipa_forge-0.1.0/patches/instagram/dylib/ABTesting.m +22 -0
- ipa_forge-0.1.0/patches/instagram/dylib/AdBlock.m +43 -0
- ipa_forge-0.1.0/patches/instagram/dylib/BetaFix.m +21 -0
- ipa_forge-0.1.0/patches/instagram/dylib/CopyText.m +54 -0
- ipa_forge-0.1.0/patches/instagram/dylib/Helpers.m +258 -0
- ipa_forge-0.1.0/patches/instagram/dylib/IGModHook.h +69 -0
- ipa_forge-0.1.0/patches/instagram/dylib/IGModHook.m +24 -0
- ipa_forge-0.1.0/patches/instagram/dylib/KeychainFix.m +14 -0
- ipa_forge-0.1.0/patches/instagram/dylib/MediaDownload.m +539 -0
- ipa_forge-0.1.0/patches/instagram/dylib/SafeMode.m +25 -0
- ipa_forge-0.1.0/patches/instagram/dylib/SettingsUI.m +35 -0
- ipa_forge-0.1.0/patches/instagram/dylib/StoryPrivacy.m +121 -0
- ipa_forge-0.1.0/patches/instagram/dylib/build.sh +7 -0
- ipa_forge-0.1.0/patches/instagram/instagram.yaml +202 -0
- ipa_forge-0.1.0/patches/spotify/.gitignore +5 -0
- ipa_forge-0.1.0/patches/spotify/PLAYBOOK.md +77 -0
- ipa_forge-0.1.0/patches/spotify/README.md +117 -0
- ipa_forge-0.1.0/patches/spotify/SOURCES.md +27 -0
- ipa_forge-0.1.0/patches/spotify/TESTING.md +342 -0
- ipa_forge-0.1.0/patches/spotify/dev/dylib-test/MinimalHook.m +10 -0
- ipa_forge-0.1.0/patches/spotify/dev/dylib-test/build.sh +11 -0
- ipa_forge-0.1.0/patches/spotify/dev/spotify-test.yaml +32 -0
- ipa_forge-0.1.0/patches/spotify/dylib/.clangd +18 -0
- ipa_forge-0.1.0/patches/spotify/dylib/AdBlock.m +121 -0
- ipa_forge-0.1.0/patches/spotify/dylib/AudioQuality.m +42 -0
- ipa_forge-0.1.0/patches/spotify/dylib/PBProto.h +64 -0
- ipa_forge-0.1.0/patches/spotify/dylib/PBProto.m +261 -0
- ipa_forge-0.1.0/patches/spotify/dylib/PremiumPatch.m +522 -0
- ipa_forge-0.1.0/patches/spotify/dylib/SessionProtection.m +188 -0
- ipa_forge-0.1.0/patches/spotify/dylib/SettingsUI.m +490 -0
- ipa_forge-0.1.0/patches/spotify/dylib/SideloadFix.m +40 -0
- ipa_forge-0.1.0/patches/spotify/dylib/SpotifyFeatures.m +139 -0
- ipa_forge-0.1.0/patches/spotify/dylib/SpotifyHook.h +140 -0
- ipa_forge-0.1.0/patches/spotify/dylib/SpotifyHook.m +47 -0
- ipa_forge-0.1.0/patches/spotify/dylib/StartupTab.m +111 -0
- ipa_forge-0.1.0/patches/spotify/dylib/TabBarFix.m +561 -0
- ipa_forge-0.1.0/patches/spotify/dylib/build.sh +24 -0
- ipa_forge-0.1.0/patches/spotify/spotify.yaml +137 -0
- ipa_forge-0.1.0/patches/youtube/.gitignore +5 -0
- ipa_forge-0.1.0/patches/youtube/PLAYBOOK.md +156 -0
- ipa_forge-0.1.0/patches/youtube/README.md +163 -0
- ipa_forge-0.1.0/patches/youtube/ROADMAP.md +637 -0
- ipa_forge-0.1.0/patches/youtube/SOURCES.md +33 -0
- ipa_forge-0.1.0/patches/youtube/TESTING.md +79 -0
- ipa_forge-0.1.0/patches/youtube/assets/90s-video.mp4 +0 -0
- ipa_forge-0.1.0/patches/youtube/dylib/.clangd +18 -0
- ipa_forge-0.1.0/patches/youtube/dylib/AdBlock.m +251 -0
- ipa_forge-0.1.0/patches/youtube/dylib/Appearance.m +185 -0
- ipa_forge-0.1.0/patches/youtube/dylib/DownloadCore.m +353 -0
- ipa_forge-0.1.0/patches/youtube/dylib/DownloadUI.m +224 -0
- ipa_forge-0.1.0/patches/youtube/dylib/FeedShorts.m +367 -0
- ipa_forge-0.1.0/patches/youtube/dylib/KeepScreenOn.m +22 -0
- ipa_forge-0.1.0/patches/youtube/dylib/LoopVideo.m +42 -0
- ipa_forge-0.1.0/patches/youtube/dylib/MiscFeatures.m +382 -0
- ipa_forge-0.1.0/patches/youtube/dylib/NativeShare.m +191 -0
- ipa_forge-0.1.0/patches/youtube/dylib/NavbarTabbar.m +295 -0
- ipa_forge-0.1.0/patches/youtube/dylib/PlayerFeatures.m +1017 -0
- ipa_forge-0.1.0/patches/youtube/dylib/PlayerGestures.m +275 -0
- ipa_forge-0.1.0/patches/youtube/dylib/RYDDislikes.m +166 -0
- ipa_forge-0.1.0/patches/youtube/dylib/SettingsCleaner.m +75 -0
- ipa_forge-0.1.0/patches/youtube/dylib/SettingsUI.m +514 -0
- ipa_forge-0.1.0/patches/youtube/dylib/SignInFix.m +333 -0
- ipa_forge-0.1.0/patches/youtube/dylib/SponsorBlock.m +190 -0
- ipa_forge-0.1.0/patches/youtube/dylib/YTFFeatures.m +421 -0
- ipa_forge-0.1.0/patches/youtube/dylib/YTFreedom.h +387 -0
- ipa_forge-0.1.0/patches/youtube/dylib/YTFreedom.m +40 -0
- ipa_forge-0.1.0/patches/youtube/dylib/build.sh +25 -0
- ipa_forge-0.1.0/patches/youtube/tools/generate_hooks_manifest.py +93 -0
- ipa_forge-0.1.0/patches/youtube/youtube.yaml +729 -0
- ipa_forge-0.1.0/pyproject.toml +102 -0
- ipa_forge-0.1.0/pyrightconfig.json +5 -0
- ipa_forge-0.1.0/scripts/rebuild_fixture.sh +47 -0
- ipa_forge-0.1.0/tests/__init__.py +1 -0
- ipa_forge-0.1.0/tests/conftest.py +165 -0
- ipa_forge-0.1.0/tests/integration/__init__.py +1 -0
- ipa_forge-0.1.0/tests/integration/test_gui.py +84 -0
- ipa_forge-0.1.0/tests/integration/test_gui_analysis.py +72 -0
- ipa_forge-0.1.0/tests/integration/test_pipeline.py +69 -0
- ipa_forge-0.1.0/tests/integration/test_pipeline_dylib_inject.py +42 -0
- ipa_forge-0.1.0/tests/integration/test_pipeline_plist_edit.py +39 -0
- ipa_forge-0.1.0/tests/integration/test_profile_parsing.py +40 -0
- ipa_forge-0.1.0/tests/integration/test_signing.py +115 -0
- ipa_forge-0.1.0/tests/unit/__init__.py +1 -0
- ipa_forge-0.1.0/tests/unit/test_altstore_export.py +32 -0
- ipa_forge-0.1.0/tests/unit/test_analysis_diff.py +81 -0
- ipa_forge-0.1.0/tests/unit/test_analysis_strings_symbols_security.py +50 -0
- ipa_forge-0.1.0/tests/unit/test_arch.py +87 -0
- ipa_forge-0.1.0/tests/unit/test_binary_patch.py +131 -0
- ipa_forge-0.1.0/tests/unit/test_classdump.py +61 -0
- ipa_forge-0.1.0/tests/unit/test_cli.py +329 -0
- ipa_forge-0.1.0/tests/unit/test_cli_analysis.py +82 -0
- ipa_forge-0.1.0/tests/unit/test_cli_analysis_diff.py +75 -0
- ipa_forge-0.1.0/tests/unit/test_cli_analysis_strings_symbols_security.py +59 -0
- ipa_forge-0.1.0/tests/unit/test_engine.py +56 -0
- ipa_forge-0.1.0/tests/unit/test_gui_app.py +103 -0
- ipa_forge-0.1.0/tests/unit/test_hooks_engine.py +439 -0
- ipa_forge-0.1.0/tests/unit/test_hooks_verify.py +276 -0
- ipa_forge-0.1.0/tests/unit/test_injector.py +59 -0
- ipa_forge-0.1.0/tests/unit/test_inventory_bundle_id.py +78 -0
- ipa_forge-0.1.0/tests/unit/test_ipa_roundtrip.py +37 -0
- ipa_forge-0.1.0/tests/unit/test_machO_detect.py +39 -0
- ipa_forge-0.1.0/tests/unit/test_machO_objc.py +86 -0
- ipa_forge-0.1.0/tests/unit/test_plist_edit.py +84 -0
- ipa_forge-0.1.0/tests/unit/test_profile_pool.py +99 -0
- ipa_forge-0.1.0/tests/unit/test_reconcile.py +45 -0
- ipa_forge-0.1.0/tests/unit/test_resolver_and_schema.py +169 -0
- ipa_forge-0.1.0/tests/unit/test_resources.py +124 -0
- ipa_forge-0.1.0/tests/unit/test_sign_bundle.py +113 -0
- 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.
|