diff --git a/CLAUDE.md b/CLAUDE.md index 7bec02f..1dde649 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -34,6 +34,9 @@ bash scripts/check-deps.sh # Install a dependency (auto-detects OS/package manager) bash scripts/install-dep.sh # e.g., jadx, vineflower, dex2jar +# Install ALL neutralizer dependencies at once (java, apktool, apksigner, zip) +bash scripts/install-dep.sh neutralize-all + # Decompile an APK/JAR/AAR/XAPK bash scripts/decompile.sh [--engine jadx|fernflower|both] [--deobf] [--no-res] [-o outdir] @@ -61,14 +64,14 @@ SDK neutralizer scripts under `plugins/android-reverse-engineering/skills/sdk-ne # Check neutralization dependencies (including apktool >= 2.9.0) bash check-neutralize-deps.sh -# Decode APK or XAPK (extracts base APK from XAPK automatically) +# Decode APK or XAPK (for XAPK: decodes base APK, preserves splits in .xapk-origin/) bash decode-apk.sh [-o ] # Neutralize SDK entry points in decoded APK (dry-run first) bash neutralize.sh [--ads|--trackers|--all] [--dry-run] [--no-backup] [--no-manifest] [--targets-file ] [--replay] [--no-save-manifest] -# Rebuild and sign neutralized APK -bash rebuild-apk.sh [--debug-key|--keystore ] [-o ] [--no-sign] [--no-res] [--zipalign] +# Rebuild and sign neutralized APK (auto-reassembles XAPK if .xapk-origin/ exists) +bash rebuild-apk.sh [--auto-keystore|--debug-key|--keystore ] [-o ] [--no-sign] [--no-res] [--zipalign] ``` ## Architecture diff --git a/README.md b/README.md index bc86b2d..e5934ba 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,8 @@ A Claude Code skill that decompiles Android APK/XAPK/JAR/AAR files, **extracts H **For SDK neutralization (`/neutralize`):** - [apktool](https://apktool.org/) (required) — APK decode/rebuild -- apksigner or jarsigner (required) — APK signing +- apksigner or jarsigner (required) — APK signing (apksigner required for XAPK) +- zip (required for XAPK rebuild) See `plugins/android-reverse-engineering/skills/android-reverse-engineering/references/setup-guide.md` for detailed installation instructions. @@ -133,6 +134,9 @@ bash plugins/android-reverse-engineering/skills/android-reverse-engineering/scri bash plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/install-dep.sh jadx bash plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/install-dep.sh vineflower +# Install ALL neutralizer dependencies at once (java, apktool, apksigner, zip) +bash plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/install-dep.sh neutralize-all + # Decompile APK with jadx (default) bash plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/decompile.sh app.apk @@ -164,10 +168,15 @@ bash plugins/android-reverse-engineering/skills/ad-analysis/scripts/find-ads.sh bash plugins/android-reverse-engineering/skills/sdk-neutralizer/scripts/decode-apk.sh app.apk -o app-decoded bash plugins/android-reverse-engineering/skills/sdk-neutralizer/scripts/neutralize.sh app-decoded --all --dry-run bash plugins/android-reverse-engineering/skills/sdk-neutralizer/scripts/neutralize.sh app-decoded --all -bash plugins/android-reverse-engineering/skills/sdk-neutralizer/scripts/rebuild-apk.sh app-decoded --debug-key +bash plugins/android-reverse-engineering/skills/sdk-neutralizer/scripts/rebuild-apk.sh app-decoded --auto-keystore -# XAPK support — decode-apk.sh extracts the base APK automatically +# XAPK full round-trip — decode preserves splits, rebuild reassembles XAPK bash plugins/android-reverse-engineering/skills/sdk-neutralizer/scripts/decode-apk.sh app-bundle.xapk -o app-decoded +# .xapk-origin/ now contains splits/, manifest.json, metadata.json +bash plugins/android-reverse-engineering/skills/sdk-neutralizer/scripts/neutralize.sh app-decoded --all --dry-run +bash plugins/android-reverse-engineering/skills/sdk-neutralizer/scripts/neutralize.sh app-decoded --all +bash plugins/android-reverse-engineering/skills/sdk-neutralizer/scripts/rebuild-apk.sh app-decoded --auto-keystore +# → produces app-decoded-neutralized.xapk with all splits re-signed # Replay previous patches after re-decoding bash plugins/android-reverse-engineering/skills/sdk-neutralizer/scripts/neutralize.sh app-decoded --replay diff --git a/plugins/android-reverse-engineering/commands/neutralize.md b/plugins/android-reverse-engineering/commands/neutralize.md index e059caa..11ca8d3 100644 --- a/plugins/android-reverse-engineering/commands/neutralize.md +++ b/plugins/android-reverse-engineering/commands/neutralize.md @@ -48,15 +48,23 @@ Run the dependency check to ensure all required tools are installed: bash ${CLAUDE_PLUGIN_ROOT}/skills/sdk-neutralizer/scripts/check-neutralize-deps.sh ``` -If any `INSTALL_REQUIRED:` lines appear, install the missing dependencies: +If any `INSTALL_REQUIRED:` lines appear, install all dependencies at once: ```bash -bash ${CLAUDE_PLUGIN_ROOT}/skills/android-reverse-engineering/scripts/install-dep.sh +bash ${CLAUDE_PLUGIN_ROOT}/skills/android-reverse-engineering/scripts/install-dep.sh neutralize-all ``` +If the script exits with code 2 (sudo needed but no TTY — common inside Claude Code), tell the user to run this command in their terminal: + +``` +sudo bash /install-dep.sh neutralize-all +``` + +Provide the **full resolved path** (replace `${CLAUDE_PLUGIN_ROOT}` with the actual path) so the user can copy-paste directly. + ### Step 4: Decode APK/XAPK -Decode the APK or XAPK using decode-apk.sh (handles both formats; for XAPKs extracts the base APK automatically): +Decode the APK or XAPK using decode-apk.sh (handles both formats; for XAPKs extracts and decodes the base APK while preserving the full XAPK structure for rebuild): ```bash # Strip both .apk and .xapk extensions for the output dir name @@ -66,6 +74,8 @@ bash ${CLAUDE_PLUGIN_ROOT}/skills/sdk-neutralizer/scripts/decode-apk.sh "$APK_PA Verify the decoded directory contains `smali/` and `AndroidManifest.xml` (the script does this automatically and outputs `DECODED_DIR:`). +If the output includes `XAPK_ORIGIN:`, inform the user: "This is an XAPK (split APK bundle). The base APK has been decoded for neutralization, and all split APKs are preserved. During rebuild, all APKs (base + splits) will be re-signed with the same key and reassembled into a new XAPK." + ### Step 5: Identify targets Run entry point detection to find which SDK calls exist in the app code: @@ -102,18 +112,46 @@ Parse the `PATCHED:` and `MANIFEST_DISABLED:` output lines for the report. ### Step 8: Rebuild & sign -Rebuild the APK with a debug signing key: +**Before rebuilding**, ask the user their signing preference: + +> How would you like to sign the rebuilt APK? +> +> 1. **Auto-detect** (recommended) — checks `~/.android/debug.keystore` first, then generates a debug key +> 2. **Custom keystore** — provide path, alias, and password +> 3. **No signing** — output unsigned APK + +Then rebuild with the appropriate flag: ```bash -bash ${CLAUDE_PLUGIN_ROOT}/skills/sdk-neutralizer/scripts/rebuild-apk.sh "${DECODED_DIR}" --debug-key +# Auto-detect keystore (recommended) +bash ${CLAUDE_PLUGIN_ROOT}/skills/sdk-neutralizer/scripts/rebuild-apk.sh "${DECODED_DIR}" --auto-keystore + +# Or with custom keystore +bash ${CLAUDE_PLUGIN_ROOT}/skills/sdk-neutralizer/scripts/rebuild-apk.sh "${DECODED_DIR}" --keystore /path/to/keystore + +# Or unsigned +bash ${CLAUDE_PLUGIN_ROOT}/skills/sdk-neutralizer/scripts/rebuild-apk.sh "${DECODED_DIR}" --no-sign ``` +Parse the output for: +- `KEYSTORE_USED:` — which keystore was used +- `KEYSTORE_SOURCE:` — how it was resolved (debug-standard, debug-previous, debug-generated, custom) +- `SPLIT_SIGNED:` — each re-signed split APK (XAPK only) +- `XAPK_ASSEMBLED:` — final XAPK output (XAPK only) + +For XAPK output: inform the user that install requires `adb install-multiple` or a split APK installer (SAI). + ### Step 9: Report & next steps Generate a neutralization report following the format in `${CLAUDE_PLUGIN_ROOT}/skills/sdk-neutralizer/SKILL.md` (Phase 6). **The report must include the "Side Effects & Legal Notice" section.** +Include in the report: +- **Output format**: APK or XAPK (split bundle) +- **Keystore used**: path and source (from `KEYSTORE_USED:` / `KEYSTORE_SOURCE:` output) +- **Install command**: `adb install ` for APK, `adb install-multiple ` for XAPK + Tell the user what they can do next: -- **Test thoroughly**: "Install via `adb install ` and test for crashes — especially features tied to ads or analytics" +- **Test thoroughly**: for APK: "Install via `adb install `"; for XAPK: "Install via `adb install-multiple` or use SAI (Split APKs Installer)" — test for crashes, especially features tied to ads or analytics - **Verify**: "I can re-run entry point detection on the rebuilt APK to confirm neutralization" - **Custom targets**: "If the app uses obfuscated SDK calls, provide a targets file for additional patching" - **Deep analysis**: "Run `/find-trackers` or `/find-ads` for full SDK analysis" diff --git a/plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/check-deps.sh b/plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/check-deps.sh index b7f1bf7..0af87ea 100755 --- a/plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/check-deps.sh +++ b/plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/check-deps.sh @@ -9,6 +9,11 @@ errors=0 missing_required=() missing_optional=() +# Ensure user-local bin is in PATH (install-dep.sh installs tools there) +if [[ -d "$HOME/.local/bin" ]] && [[ ":$PATH:" != *":$HOME/.local/bin:"* ]]; then + export PATH="$HOME/.local/bin:$PATH" +fi + echo "=== Android Reverse Engineering: Dependency Check ===" echo diff --git a/plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/decompile.sh b/plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/decompile.sh index e36ca1a..39601da 100755 --- a/plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/decompile.sh +++ b/plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/decompile.sh @@ -2,6 +2,11 @@ # decompile.sh — Decompile APK/JAR/AAR using jadx, fernflower, or both set -euo pipefail +# Ensure user-local bin is in PATH (install-dep.sh installs tools there) +if [[ -d "$HOME/.local/bin" ]] && [[ ":$PATH:" != *":$HOME/.local/bin:"* ]]; then + export PATH="$HOME/.local/bin:$PATH" +fi + usage() { cat < diff --git a/plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/install-dep.sh b/plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/install-dep.sh index 1c16b30..9813ffd 100755 --- a/plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/install-dep.sh +++ b/plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/install-dep.sh @@ -1,7 +1,8 @@ #!/usr/bin/env bash # install-dep.sh — Install a single dependency for Android reverse engineering # Usage: install-dep.sh -# Dependencies: java, jadx, vineflower, dex2jar, apktool, adb, smali, apksigner +# Dependencies: java, jadx, vineflower, dex2jar, apktool, adb, smali, apksigner, zip +# Compound: neutralize-all (java + apktool + apksigner + zip) # # Exit codes: # 0 — installed successfully @@ -9,6 +10,11 @@ # 2 — requires manual action (e.g. sudo needed but not available) set -euo pipefail +# Ensure user-local bin is in PATH (previous installs may have placed tools there) +if [[ -d "$HOME/.local/bin" ]] && [[ ":$PATH:" != *":$HOME/.local/bin:"* ]]; then + export PATH="$HOME/.local/bin:$PATH" +fi + usage() { cat < @@ -24,6 +30,10 @@ Available dependencies: adb Android Debug Bridge smali Smali/baksmali assembler/disassembler apksigner Android APK signing tool + zip zip archiver (needed for XAPK rebuild) + +Compound targets: + neutralize-all Install all SDK neutralizer deps (java, apktool, apksigner, zip) The script detects your OS and package manager, then: - Installs directly if possible (brew, or user-local install) @@ -434,22 +444,79 @@ install_dex2jar() { } install_apktool() { + # Minimum version required by sdk-neutralizer (modern APKs, targetSdk 34+) + local MIN_MAJOR=2 MIN_MINOR=9 MIN_PATCH=0 + local need_install=false + if command -v apktool &>/dev/null; then - ok "apktool already installed" - return 0 + local ver_raw + ver_raw=$(apktool --version 2>/dev/null | head -1 | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1) + if [[ -n "$ver_raw" ]]; then + local cur_major cur_minor cur_patch + IFS='.' read -r cur_major cur_minor cur_patch <<< "$ver_raw" + cur_major=${cur_major:-0}; cur_minor=${cur_minor:-0}; cur_patch=${cur_patch:-0} + + if (( cur_major > MIN_MAJOR )) || \ + (( cur_major == MIN_MAJOR && cur_minor > MIN_MINOR )) || \ + (( cur_major == MIN_MAJOR && cur_minor == MIN_MINOR && cur_patch >= MIN_PATCH )); then + ok "apktool $ver_raw already installed (>= ${MIN_MAJOR}.${MIN_MINOR}.${MIN_PATCH})" + return 0 + else + info "apktool $ver_raw found but >= ${MIN_MAJOR}.${MIN_MINOR}.${MIN_PATCH} is required — upgrading..." + need_install=true + fi + else + ok "apktool detected (could not parse version — assuming compatible)" + return 0 + fi + else + need_install=true fi - case "$PKG_MANAGER" in - brew) info "Installing apktool via Homebrew..."; brew install apktool ;; - apt) pkg_install "apktool" ;; - *) manual "Install apktool from https://apktool.org/docs/install" ;; - esac + if [[ "$need_install" == true ]]; then + # User-local install from GitHub (no sudo needed, takes PATH precedence) + info "Installing apktool from GitHub releases to ~/.local/bin..." + local tag + tag=$(gh_latest_tag "iBotPeaches/Apktool") + if [[ -z "$tag" ]]; then + tag="v2.10.0" # fallback known-good version + info "Could not fetch latest tag, using fallback: $tag" + fi - if command -v apktool &>/dev/null; then - ok "apktool installed" - else - fail "apktool installation may have failed." - exit 1 + local version="${tag#v}" + local install_dir="$HOME/.local/share/apktool" + mkdir -p "$install_dir" "$HOME/.local/bin" + + info "Downloading apktool $version..." + download "https://bitbucket.org/iBotPeaches/apktool/downloads/apktool_${version}.jar" \ + "$install_dir/apktool.jar" 2>/dev/null || \ + download "https://github.com/iBotPeaches/Apktool/releases/download/${tag}/apktool_${version}.jar" \ + "$install_dir/apktool.jar" + + if [[ ! -f "$install_dir/apktool.jar" ]]; then + fail "Failed to download apktool $version." + manual "Download from https://apktool.org/docs/install" + fi + + # Create wrapper script + cat > "$HOME/.local/bin/apktool" <<'WRAPPER' +#!/usr/bin/env bash +exec java -jar "$HOME/.local/share/apktool/apktool.jar" "$@" +WRAPPER + chmod +x "$HOME/.local/bin/apktool" + + export PATH="$HOME/.local/bin:$PATH" + add_to_profile 'export PATH="$HOME/.local/bin:$PATH"' + + # Verify + local installed_ver + installed_ver=$("$HOME/.local/bin/apktool" --version 2>/dev/null | head -1 | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1) + if [[ -n "$installed_ver" ]]; then + ok "apktool $installed_ver installed to $install_dir" + else + fail "apktool installation may have failed." + exit 1 + fi fi } @@ -656,6 +723,49 @@ install_apksigner() { manual "Install Android SDK build-tools or run: sudo apt install apksigner (Debian/Ubuntu)" } +install_zip() { + if command -v zip &>/dev/null; then + ok "zip already installed" + return 0 + fi + + info "Installing zip..." + case "$PKG_MANAGER" in + brew) brew install zip ;; + apt) pkg_install "zip" ;; + dnf) pkg_install "zip" ;; + pacman) pkg_install "zip" ;; + *) manual "Install zip using your system package manager." ;; + esac + + if command -v zip &>/dev/null; then + ok "zip installed" + else + fail "zip installation may have failed." + exit 1 + fi +} + +install_neutralize_all() { + echo "=== Installing all SDK Neutralizer dependencies ===" + echo + local failed=() + for dep_fn in install_java install_apktool install_apksigner install_zip; do + dep_name="${dep_fn#install_}" + info "--- $dep_name ---" + if ! $dep_fn; then + failed+=("$dep_name") + fi + echo + done + + if [[ ${#failed[@]} -gt 0 ]]; then + fail "Failed to install: ${failed[*]}" + exit 1 + fi + ok "All SDK Neutralizer dependencies installed." +} + # ===================================================================== # Dispatch # ===================================================================== @@ -669,9 +779,12 @@ case "$DEP" in adb) install_adb ;; smali|baksmali) install_smali ;; apksigner) install_apksigner ;; + zip) install_zip ;; + neutralize-all) install_neutralize_all ;; *) echo "Error: Unknown dependency '$DEP'" >&2 - echo "Available: java, jadx, vineflower, dex2jar, apktool, adb, smali, apksigner" >&2 + echo "Available: java, jadx, vineflower, dex2jar, apktool, adb, smali, apksigner, zip" >&2 + echo "Compound: neutralize-all" >&2 exit 1 ;; esac diff --git a/plugins/android-reverse-engineering/skills/sdk-neutralizer/SKILL.md b/plugins/android-reverse-engineering/skills/sdk-neutralizer/SKILL.md index 4f6643c..106c5d4 100644 --- a/plugins/android-reverse-engineering/skills/sdk-neutralizer/SKILL.md +++ b/plugins/android-reverse-engineering/skills/sdk-neutralizer/SKILL.md @@ -64,15 +64,22 @@ Check that all required tools are installed. bash ${CLAUDE_PLUGIN_ROOT}/skills/sdk-neutralizer/scripts/check-neutralize-deps.sh ``` -If any `INSTALL_REQUIRED:` lines appear, install the missing dependencies: +If any `INSTALL_REQUIRED:` lines appear, ask the user to install all dependencies at once: ```bash -bash ${CLAUDE_PLUGIN_ROOT}/skills/android-reverse-engineering/scripts/install-dep.sh +# Install all neutralizer deps (java, apktool, apksigner, zip) in one command +bash ${CLAUDE_PLUGIN_ROOT}/skills/android-reverse-engineering/scripts/install-dep.sh neutralize-all +``` + +If the script exits with code 2 (sudo needed but no TTY), tell the user to run in their terminal: + +``` +sudo bash /skills/android-reverse-engineering/scripts/install-dep.sh neutralize-all ``` ### Phase 2: Decode APK -Decode the APK (or XAPK) into smali and resources using decode-apk.sh. This script handles both `.apk` and `.xapk` files — for XAPKs it automatically extracts the base APK, skipping split/config APKs. +Decode the APK (or XAPK) into smali and resources using decode-apk.sh. This script handles both `.apk` and `.xapk` files — for XAPKs it automatically extracts and decodes the base APK, while preserving the full XAPK structure (split APKs, manifest, icon) in a `.xapk-origin/` directory inside the decoded output for automatic reassembly during rebuild. **Action**: Run the decode script. @@ -82,24 +89,99 @@ bash ${CLAUDE_PLUGIN_ROOT}/skills/sdk-neutralizer/scripts/decode-apk.sh `. +For XAPK input, the script also outputs `XAPK_ORIGIN:` and creates: +- `.xapk-origin/metadata.json` — XAPK metadata (package, version, split list) +- `.xapk-origin/manifest.json` — original XAPK manifest +- `.xapk-origin/splits/` — all split APKs (config.arm64_v8a.apk, config.en.apk, etc.) + +If the input is an XAPK, inform the user that it's a split APK bundle and that all splits will be automatically re-signed during rebuild. + ### Phase 3: Identify Targets -Run the tracker and ad detection scripts on the decoded smali to identify which SDKs are present and which entry points the app uses. +Target identification has three sub-phases. The goal is to find all SDK entry points to neutralize while minimizing manual approval prompts. -**Action**: Run entry point detection. +#### Phase 3a — Built-in Catalog Detection + +Use `neutralize.sh --dry-run` to detect known SDK targets. This is more reliable than `find-ads.sh`/`find-trackers.sh` because it searches smali directly (not Java source) and matches the exact patterns that will be patched. + +**Action**: Run dry-run detection. ```bash -# Detect ad SDK entry points called from app code -bash ${CLAUDE_PLUGIN_ROOT}/skills/ad-analysis/scripts/find-ads.sh --entrypoints - -# Detect tracker SDK entry points called from app code -bash ${CLAUDE_PLUGIN_ROOT}/skills/tracker-analysis/scripts/find-trackers.sh --entrypoints +bash ${CLAUDE_PLUGIN_ROOT}/skills/sdk-neutralizer/scripts/neutralize.sh --all --dry-run ``` -Present the detected SDKs and entry points to the user. Ask which categories to neutralize: +Parse the output for: +- `DRY_RUN:WOULD_PATCH:` lines — smali methods that would be stubbed +- `DRY_RUN:WOULD_DISABLE:` lines — manifest components that would be disabled + +**NOTE**: Do NOT use `find-ads.sh` or `find-trackers.sh` here — those scripts search Java/Kotlin source (`.java`, `.kt`), not smali. The decoded directory from Phase 2 contains only smali bytecode, so those scripts will find nothing. + +#### Phase 3b — Custom/Proprietary SDK Discovery (if needed) + +Activate this sub-phase when: +- The user mentions a specific SDK not in the built-in catalog (e.g., `guru/ads/fusion`, `com/proprietary/analytics`) +- The dry-run found few or no targets but the user expects more +- The user asks to find "all" trackers/ads, including custom/proprietary ones + +**CRITICAL — Use Claude Code built-in tools, NOT bash commands.** Glob, Grep, and Read are auto-approved and require no manual user approval. Using `bash find`, `bash grep`, or `bash head` forces the user to approve each command individually. + +**Discovery workflow using built-in tools:** + +1. **Find smali files by package pattern** — use Glob: + ``` + Glob: **/smali*/com/guru/**/*.smali + Glob: **/smali*/com/proprietary/analytics/**/*.smali + ``` + +2. **Find SDK method signatures** — use Grep on the matched files: + ``` + Grep: pattern="^\.method.*(init|track|log|show|load|send|report|emit)" + Grep: pattern="^\.method.*getInstance" + ``` + +3. **Find SDK invocations from app code** — use Grep on the app's own smali: + ``` + Grep: pattern="invoke-(static|virtual|direct).*Lcom/guru/ads/" + Grep: pattern="invoke-(static|virtual|direct).*Lcom/proprietary/analytics/" + ``` + +4. **Examine specific files** — use Read to inspect method bodies and confirm they are SDK entry points worth neutralizing. + +**Build the custom targets file** with discovered entry points, one per line, in the format: + +``` +: +``` + +For example: +``` +smali/com/guru/ads/fusion/FusionAd.smali:initialize +smali/com/guru/ads/fusion/FusionAd.smali:showAd +smali/com/guru/ads/fusion/FusionTracker.smali:trackEvent +``` + +**Action**: Write the targets file. + +``` +Write: /custom-targets.txt +``` + +#### Phase 3c — Compile Target List and Confirm + +Present a summary table of all targets to the user: + +| SDK | Category | Source | Entry Points | +|---|---|---|---| +| AdMob | Ads | Built-in catalog | 5 methods, 2 components | +| Firebase Analytics | Trackers | Built-in catalog | 3 methods, 1 component | +| guru/ads/fusion | Ads | Custom discovery | 3 methods | +| ... | | | | + +Ask which categories/SDKs to neutralize: - `--ads` — only ad SDKs - `--trackers` — only tracker/analytics SDKs - `--all` — both (default) +- Optionally exclude specific SDKs ### Phase 4: Neutralize @@ -115,6 +197,16 @@ bash ${CLAUDE_PLUGIN_ROOT}/skills/sdk-neutralizer/scripts/neutralize.sh --all ``` +**If Phase 3b produced custom targets**, add `--targets-file` to both commands: + +```bash +# Preview with custom targets +bash ${CLAUDE_PLUGIN_ROOT}/skills/sdk-neutralizer/scripts/neutralize.sh --all --dry-run --targets-file /custom-targets.txt + +# Apply with custom targets +bash ${CLAUDE_PLUGIN_ROOT}/skills/sdk-neutralizer/scripts/neutralize.sh --all --targets-file /custom-targets.txt +``` + Parse the output for `PATCHED:` and `MANIFEST_DISABLED:` lines to build the report. Options: @@ -130,21 +222,38 @@ After a successful (non-dry-run) neutralization, a `neutralize-manifest.json` is ### Phase 5: Rebuild & Sign -Rebuild the decoded directory back into a signed APK. +Rebuild the decoded directory back into a signed APK (or XAPK if the original was an XAPK). -**Action**: Run the rebuild script. +**Before calling rebuild**, you **MUST ask the user** their signing preference: + +> How would you like to sign the rebuilt APK? +> +> 1. **Auto-detect** (recommended) — checks for `~/.android/debug.keystore` first, then generates a debug key +> 2. **Custom keystore** — provide path, alias, and password +> 3. **No signing** — output unsigned APK (cannot be installed directly) + +Map the user's choice to the corresponding flag: +- Option 1 → `--auto-keystore` +- Option 2 → `--keystore --key-alias --store-pass --key-pass ` +- Option 3 → `--no-sign` + +**Action**: Run the rebuild script with the chosen signing option. ```bash -bash ${CLAUDE_PLUGIN_ROOT}/skills/sdk-neutralizer/scripts/rebuild-apk.sh --debug-key +# Example with auto-keystore (recommended default) +bash ${CLAUDE_PLUGIN_ROOT}/skills/sdk-neutralizer/scripts/rebuild-apk.sh --auto-keystore ``` Options: -- `-o ` — custom output path -- `--debug-key` — auto-generate debug keystore (default) +- `-o ` — custom output path +- `--auto-keystore` — auto-detect best keystore (recommended) +- `--debug-key` — always generate new debug keystore - `--keystore ` — use a custom keystore - `--no-sign` — output unsigned APK - `--zipalign` / `--no-zipalign` — control zipalign step +For XAPK input, the rebuild is automatic: the script detects `.xapk-origin/`, re-signs all split APKs with the same keystore, and produces a `.xapk` output. Parse the output for `KEYSTORE_USED:`, `KEYSTORE_SOURCE:`, `SPLIT_SIGNED:`, and `XAPK_ASSEMBLED:` lines. + ### Phase 6: Verify & Report Generate a structured neutralization report and suggest next steps. @@ -201,9 +310,12 @@ and privacy compliance only. ## Output -- Sanitized APK: `` -- Signed with: debug key / custom keystore -- Install via: `adb install ` +- Sanitized APK/XAPK: `` +- Output format: APK (single) / XAPK (split bundle) +- Signed with: auto-detected debug key / generated debug key / custom keystore +- Keystore used: `` (source: `KEYSTORE_SOURCE:` value) +- Install via: `adb install ` (APK) or `adb install-multiple ...` (XAPK) +- For XAPK: can also use SAI (Split APKs Installer) or unzip and `adb install-multiple *.apk` ``` **Next steps to suggest:** diff --git a/plugins/android-reverse-engineering/skills/sdk-neutralizer/scripts/check-neutralize-deps.sh b/plugins/android-reverse-engineering/skills/sdk-neutralizer/scripts/check-neutralize-deps.sh index 2239bbf..37150cc 100755 --- a/plugins/android-reverse-engineering/skills/sdk-neutralizer/scripts/check-neutralize-deps.sh +++ b/plugins/android-reverse-engineering/skills/sdk-neutralizer/scripts/check-neutralize-deps.sh @@ -7,6 +7,11 @@ REQUIRED_JAVA_MAJOR=17 missing_required=() missing_optional=() +# Ensure user-local bin is in PATH (install-dep.sh installs tools there) +if [[ -d "$HOME/.local/bin" ]] && [[ ":$PATH:" != *":$HOME/.local/bin:"* ]]; then + export PATH="$HOME/.local/bin:$PATH" +fi + echo "=== SDK Neutralizer: Dependency Check ===" echo @@ -69,11 +74,15 @@ fi # --- apksigner or jarsigner (at least one required) --- signer_found=false +has_apksigner=false if command -v apksigner &>/dev/null; then echo "[OK] apksigner detected" signer_found=true + has_apksigner=true elif command -v jarsigner &>/dev/null; then echo "[OK] jarsigner detected (fallback signer)" + echo " Note: XAPK rebuild requires apksigner (APK Signature Scheme v2/v3)." + echo " jarsigner only supports v1 signatures — insufficient for split APKs." signer_found=true fi if [[ "$signer_found" == false ]]; then @@ -111,6 +120,14 @@ else fi fi +# --- zip (optional, required for XAPK rebuild) --- +if command -v zip &>/dev/null; then + echo "[OK] zip detected (required for XAPK rebuild)" +else + echo "[MISSING] zip not found (required for XAPK rebuild — install: apt install zip / brew install zip)" + missing_optional+=("zip") +fi + # --- Machine-readable summary --- echo if [[ ${#missing_required[@]} -gt 0 ]]; then @@ -131,7 +148,14 @@ echo if [[ ${#missing_required[@]} -gt 0 ]]; then echo "*** ${#missing_required[@]} required dependency/ies missing. ***" - echo "Run install-dep.sh to install, or see references/neutralization-guide.md." + echo + echo "Install all neutralizer dependencies at once:" + echo " bash install-dep.sh neutralize-all" + echo + echo "If sudo is needed (e.g. inside Claude Code where there's no TTY):" + echo " sudo bash install-dep.sh neutralize-all" + echo + echo "Or install individually: install-dep.sh " exit 1 else if [[ ${#missing_optional[@]} -gt 0 ]]; then diff --git a/plugins/android-reverse-engineering/skills/sdk-neutralizer/scripts/decode-apk.sh b/plugins/android-reverse-engineering/skills/sdk-neutralizer/scripts/decode-apk.sh index 6fbfd3f..0f4a24a 100755 --- a/plugins/android-reverse-engineering/skills/sdk-neutralizer/scripts/decode-apk.sh +++ b/plugins/android-reverse-engineering/skills/sdk-neutralizer/scripts/decode-apk.sh @@ -8,14 +8,20 @@ # 1 — error (invalid input, missing tools, decode failed) set -euo pipefail +# Ensure user-local bin is in PATH (install-dep.sh installs tools there) +if [[ -d "$HOME/.local/bin" ]] && [[ ":$PATH:" != *":$HOME/.local/bin:"* ]]; then + export PATH="$HOME/.local/bin:$PATH" +fi + usage() { cat < [OPTIONS] Decode an APK or XAPK file into smali and resources using apktool. -For XAPK files, extracts the base APK from the archive and decodes it. -Split APKs (config.*.apk) are skipped with a warning. +For XAPK files, extracts the base APK and decodes it. Split APKs and +XAPK metadata are preserved in .xapk-origin/ inside the decoded directory +so that rebuild-apk.sh can reassemble a complete XAPK. Arguments: Path to .apk or .xapk file @@ -28,6 +34,7 @@ Options: Output: DECODED_DIR: + XAPK_ORIGIN: (only for XAPK input) EOF exit 0 } @@ -91,20 +98,21 @@ if [[ -z "$OUTPUT_DIR" ]]; then fi # ===================================================================== -# XAPK handling — extract base APK +# XAPK handling — extract base APK, preserve structure for rebuild # ===================================================================== APK_TO_DECODE="$INPUT_FILE_ABS" XAPK_TMPDIR="" +IS_XAPK=false cleanup_xapk() { if [[ -n "$XAPK_TMPDIR" ]] && [[ -d "$XAPK_TMPDIR" ]]; then rm -rf "$XAPK_TMPDIR" fi } -trap cleanup_xapk EXIT if [[ "$ext_lower" == "xapk" ]]; then + IS_XAPK=true echo "=== Extracting XAPK archive ===" XAPK_TMPDIR=$(mktemp -d "${TMPDIR:-/tmp}/xapk-decode-XXXXXX") unzip -qo "$INPUT_FILE_ABS" -d "$XAPK_TMPDIR" @@ -122,6 +130,7 @@ if [[ "$ext_lower" == "xapk" ]]; then if [[ ${#all_apks[@]} -eq 0 ]]; then echo "Error: No APK files found inside XAPK archive." >&2 + rm -rf "$XAPK_TMPDIR" exit 1 fi @@ -158,23 +167,24 @@ if [[ "$ext_lower" == "xapk" ]]; then if [[ -z "$base_apk" ]]; then echo "Error: Could not identify a base APK inside the XAPK." >&2 + rm -rf "$XAPK_TMPDIR" exit 1 fi + BASE_APK_NAME=$(basename "$base_apk") echo - echo "Selected base APK: $(basename "$base_apk")" + echo "Selected base APK: $BASE_APK_NAME" - # Warn about skipped splits - skipped=0 + # List split APKs + split_apks=() for f in "${all_apks[@]}"; do if [[ "$f" != "$base_apk" ]]; then - echo " [skipped] $(basename "$f")" - skipped=$((skipped + 1)) + split_apks+=("$(basename "$f")") + echo " [split] $(basename "$f")" fi done - if (( skipped > 0 )); then - echo "Warning: $skipped split APK(s) skipped. Only the base APK is decoded." - echo " Split APKs contain config-specific resources (density, locale, ABI)." + if (( ${#split_apks[@]} > 0 )); then + echo "${#split_apks[@]} split APK(s) preserved in .xapk-origin/splits/ for rebuild." fi echo @@ -221,6 +231,101 @@ if [[ ! -f "$OUTPUT_DIR/AndroidManifest.xml" ]]; then echo "Warning: AndroidManifest.xml not found in decoded output." >&2 fi +# ===================================================================== +# Preserve XAPK structure for rebuild +# ===================================================================== + +if [[ "$IS_XAPK" == true ]] && [[ -n "$XAPK_TMPDIR" ]]; then + echo + echo "=== Preserving XAPK structure ===" + + XAPK_ORIGIN_DIR="$OUTPUT_DIR/.xapk-origin" + mkdir -p "$XAPK_ORIGIN_DIR/splits" + + # Copy manifest.json from XAPK + if [[ -f "$XAPK_TMPDIR/manifest.json" ]]; then + cp "$XAPK_TMPDIR/manifest.json" "$XAPK_ORIGIN_DIR/manifest.json" + echo " Copied manifest.json" + fi + + # Copy icon if present + for icon_file in "$XAPK_TMPDIR"/icon.png "$XAPK_TMPDIR"/icon.jpg; do + if [[ -f "$icon_file" ]]; then + cp "$icon_file" "$XAPK_ORIGIN_DIR/" + echo " Copied $(basename "$icon_file")" + break + fi + done + + # Copy split APKs + for f in "${all_apks[@]}"; do + if [[ "$f" != "$base_apk" ]]; then + cp "$f" "$XAPK_ORIGIN_DIR/splits/" + echo " Copied split: $(basename "$f")" + fi + done + + # Extract metadata from XAPK manifest.json using sed (no jq dependency) + xapk_package_name="" + xapk_version_code="" + xapk_version_name="" + if [[ -f "$XAPK_ORIGIN_DIR/manifest.json" ]]; then + xapk_package_name=$(sed -n 's/.*"package_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$XAPK_ORIGIN_DIR/manifest.json" | head -1) + xapk_version_code=$(sed -n 's/.*"version_code"[[:space:]]*:[[:space:]]*"\{0,1\}\([0-9]*\)"\{0,1\}.*/\1/p' "$XAPK_ORIGIN_DIR/manifest.json" | head -1) + xapk_version_name=$(sed -n 's/.*"version_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$XAPK_ORIGIN_DIR/manifest.json" | head -1) + fi + + # Detect OBB files (registered but NOT copied — can be gigabytes) + obb_json="[]" + obb_entries=() + while IFS= read -r -d '' obb_file; do + obb_name=$(basename "$obb_file") + obb_size=$(stat -c%s "$obb_file" 2>/dev/null || stat -f%z "$obb_file" 2>/dev/null || echo 0) + obb_entries+=("{\"name\": \"$obb_name\", \"size_bytes\": $obb_size}") + done < <(find "$XAPK_TMPDIR" -name "*.obb" -print0 2>/dev/null) + if (( ${#obb_entries[@]} > 0 )); then + obb_json="[" + for i in "${!obb_entries[@]}"; do + if (( i > 0 )); then obb_json+=", "; fi + obb_json+="${obb_entries[$i]}" + done + obb_json+="]" + echo " OBB files detected (not copied — registered in metadata only):" + for entry in "${obb_entries[@]}"; do echo " $entry"; done + fi + + # Build split_apks JSON array + splits_json="[" + for i in "${!split_apks[@]}"; do + if (( i > 0 )); then splits_json+=", "; fi + splits_json+="\"${split_apks[$i]}\"" + done + splits_json+="]" + + # Write metadata.json + decoded_ts=$(date -u +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || date +"%Y-%m-%dT%H:%M:%SZ") + printf '{\n "format": "xapk",\n "original_file": "%s",\n "package_name": "%s",\n "version_code": "%s",\n "version_name": "%s",\n "base_apk": "%s",\n "split_apks": %s,\n "obb_files": %s,\n "decoded_timestamp": "%s"\n}\n' \ + "$INPUT_FILE_ABS" \ + "$xapk_package_name" \ + "$xapk_version_code" \ + "$xapk_version_name" \ + "$BASE_APK_NAME" \ + "$splits_json" \ + "$obb_json" \ + "$decoded_ts" \ + > "$XAPK_ORIGIN_DIR/metadata.json" + echo " Wrote metadata.json" + + echo + echo "XAPK structure preserved in: $XAPK_ORIGIN_DIR" + echo "XAPK_ORIGIN:$XAPK_ORIGIN_DIR" +fi + +# Clean up XAPK tmpdir now that everything is copied +cleanup_xapk +# Set trap to no-op since we already cleaned up +trap - EXIT + echo echo "Decoded successfully: $OUTPUT_DIR" echo "DECODED_DIR:$OUTPUT_DIR" diff --git a/plugins/android-reverse-engineering/skills/sdk-neutralizer/scripts/rebuild-apk.sh b/plugins/android-reverse-engineering/skills/sdk-neutralizer/scripts/rebuild-apk.sh index 63006f6..5e7546d 100755 --- a/plugins/android-reverse-engineering/skills/sdk-neutralizer/scripts/rebuild-apk.sh +++ b/plugins/android-reverse-engineering/skills/sdk-neutralizer/scripts/rebuild-apk.sh @@ -9,17 +9,26 @@ # 2 — manual action needed (missing tools) set -euo pipefail +# Ensure user-local bin is in PATH (install-dep.sh installs tools there) +if [[ -d "$HOME/.local/bin" ]] && [[ ":$PATH:" != *":$HOME/.local/bin:"* ]]; then + export PATH="$HOME/.local/bin:$PATH" +fi + usage() { cat < [OPTIONS] Rebuild an apktool-decoded APK directory back into a signed APK. +If the decoded dir contains .xapk-origin/ (from decode-apk.sh), automatically +reassembles a complete XAPK with all split APKs re-signed. Arguments: Path to the apktool-decoded APK directory Options: - -o, --output Output APK path (default: -neutralized.apk) + -o, --output Output path (default: -neutralized.apk/.xapk) + --auto-keystore Auto-detect best keystore: ~/.android/debug.keystore, + then previous .neutralizer-debug.keystore, then generate new --debug-key Sign with an auto-generated debug keystore (default) --keystore Path to a custom keystore file --key-alias Key alias within the keystore (default: key0) @@ -36,6 +45,10 @@ Output: BUILD_WARNING:Resources were not recompiled (--no-res fallback) SIGN_OK: VERIFY_OK: + KEYSTORE_USED: + KEYSTORE_SOURCE:debug-standard|debug-previous|debug-generated|custom + SPLIT_SIGNED: (XAPK only) + XAPK_ASSEMBLED: (XAPK only) EOF exit 0 } @@ -47,6 +60,7 @@ EOF DECODED_DIR="" OUTPUT="" USE_DEBUG_KEY=true +USE_AUTO_KEYSTORE=false KEYSTORE="" KEY_ALIAS="key0" KEY_PASS="android" @@ -63,7 +77,8 @@ while [[ $# -gt 0 ]]; do shift if [[ $# -eq 0 ]]; then echo "Error: --output requires a file argument" >&2; exit 1; fi OUTPUT="$1"; shift ;; - --debug-key) USE_DEBUG_KEY=true; shift ;; + --auto-keystore) USE_AUTO_KEYSTORE=true; USE_DEBUG_KEY=false; shift ;; + --debug-key) USE_DEBUG_KEY=true; USE_AUTO_KEYSTORE=false; shift ;; --keystore) shift if [[ $# -eq 0 ]]; then echo "Error: --keystore requires a file argument" >&2; exit 1; fi @@ -100,11 +115,21 @@ if [[ ! -d "$DECODED_DIR" ]]; then exit 1 fi -# Default output name +# Auto-detect XAPK origin +IS_XAPK=false +XAPK_ORIGIN_DIR="$DECODED_DIR/.xapk-origin" +if [[ -f "$XAPK_ORIGIN_DIR/metadata.json" ]]; then + IS_XAPK=true +fi + +# Default output name — .xapk if original was XAPK, .apk otherwise if [[ -z "$OUTPUT" ]]; then - # Strip trailing slash local_dir="${DECODED_DIR%/}" - OUTPUT="${local_dir}-neutralized.apk" + if [[ "$IS_XAPK" == true ]]; then + OUTPUT="${local_dir}-neutralized.xapk" + else + OUTPUT="${local_dir}-neutralized.apk" + fi fi # ===================================================================== @@ -138,6 +163,23 @@ if [[ "$DO_SIGN" == true ]]; then fi fi +# XAPK requires apksigner (v2/v3 signatures needed for split APKs on Android 7+) +if [[ "$IS_XAPK" == true ]] && [[ "$DO_SIGN" == true ]] && [[ "$SIGNER" != "apksigner" ]]; then + fail "XAPK rebuild requires apksigner for APK Signature Scheme v2/v3." + echo " jarsigner only supports v1 signatures, which do not work with split APKs on Android 7+." >&2 + echo " Install apksigner (part of Android SDK build-tools) or use --no-sign." >&2 + exit 1 +fi + +# XAPK rebuild requires zip +if [[ "$IS_XAPK" == true ]]; then + if ! command -v zip &>/dev/null; then + fail "XAPK rebuild requires 'zip' command to assemble the final XAPK." + echo " Install zip: apt install zip / brew install zip" >&2 + exit 1 + fi +fi + # Check zipalign availability ZIPALIGN_CMD="" if [[ "$DO_ZIPALIGN" == true ]] && [[ "$NO_ZIPALIGN" == false ]]; then @@ -243,16 +285,68 @@ fi # ===================================================================== if [[ "$DO_SIGN" == false ]]; then - cp "$ALIGNED_APK" "$OUTPUT" - ok "Unsigned APK saved to: $OUTPUT" - echo "BUILD_OK:$OUTPUT" - echo - echo "WARNING: APK is unsigned and cannot be installed without signing." + if [[ "$IS_XAPK" == true ]]; then + # For unsigned XAPK, just output the base APK — cannot assemble unsigned XAPK + cp "$ALIGNED_APK" "$OUTPUT" + ok "Unsigned base APK saved to: $OUTPUT" + echo "BUILD_OK:$OUTPUT" + echo + echo "WARNING: APK is unsigned and cannot be installed without signing." + echo " XAPK assembly skipped (split APKs require signing for Android 7+)." + echo " Split APKs are preserved in: $XAPK_ORIGIN_DIR/splits/" + else + cp "$ALIGNED_APK" "$OUTPUT" + ok "Unsigned APK saved to: $OUTPUT" + echo "BUILD_OK:$OUTPUT" + echo + echo "WARNING: APK is unsigned and cannot be installed without signing." + fi exit 0 fi -# Generate debug keystore if needed -if [[ "$USE_DEBUG_KEY" == true ]]; then +# Resolve keystore +KEYSTORE_SOURCE="" + +if [[ "$USE_AUTO_KEYSTORE" == true ]]; then + # Priority 1: Android SDK standard debug keystore + ANDROID_DEBUG_KS="$HOME/.android/debug.keystore" + if [[ -f "$ANDROID_DEBUG_KS" ]]; then + KEYSTORE="$ANDROID_DEBUG_KS" + KEY_ALIAS="androiddebugkey" + KEY_PASS="android" + STORE_PASS="android" + KEYSTORE_SOURCE="debug-standard" + info "Using Android SDK debug keystore: $KEYSTORE" + fi + + # Priority 2: Previous neutralizer debug keystore + if [[ -z "$KEYSTORE_SOURCE" ]]; then + PREV_DEBUG_KS="$DECODED_DIR/.neutralizer-debug.keystore" + if [[ -f "$PREV_DEBUG_KS" ]]; then + KEYSTORE="$PREV_DEBUG_KS" + KEYSTORE_SOURCE="debug-previous" + info "Using previous neutralizer debug keystore: $KEYSTORE" + fi + fi + + # Priority 3: Generate new debug keystore (fallback) + if [[ -z "$KEYSTORE_SOURCE" ]]; then + KEYSTORE="$DECODED_DIR/.neutralizer-debug.keystore" + info "Generating debug keystore..." + keytool -genkeypair \ + -keystore "$KEYSTORE" \ + -alias "$KEY_ALIAS" \ + -keyalg RSA \ + -keysize 2048 \ + -validity 10000 \ + -storepass "$STORE_PASS" \ + -keypass "$KEY_PASS" \ + -dname "CN=SDK Neutralizer Debug Key, OU=Debug, O=Debug, L=Unknown, ST=Unknown, C=US" \ + 2>/dev/null + ok "Debug keystore generated: $KEYSTORE" + KEYSTORE_SOURCE="debug-generated" + fi +elif [[ "$USE_DEBUG_KEY" == true ]]; then KEYSTORE="$DECODED_DIR/.neutralizer-debug.keystore" if [[ ! -f "$KEYSTORE" ]]; then info "Generating debug keystore..." @@ -268,6 +362,10 @@ if [[ "$USE_DEBUG_KEY" == true ]]; then 2>/dev/null ok "Debug keystore generated: $KEYSTORE" fi + KEYSTORE_SOURCE="debug-generated" +else + # Custom keystore provided via --keystore + KEYSTORE_SOURCE="custom" fi if [[ ! -f "$KEYSTORE" ]]; then @@ -275,6 +373,9 @@ if [[ ! -f "$KEYSTORE" ]]; then exit 1 fi +echo "KEYSTORE_USED:$KEYSTORE" +echo "KEYSTORE_SOURCE:$KEYSTORE_SOURCE" + info "Signing APK with $SIGNER..." if [[ "$SIGNER" == "apksigner" ]]; then @@ -330,17 +431,108 @@ elif [[ "$SIGNER" == "jarsigner" ]]; then fi fi +# ===================================================================== +# Step 5: XAPK assembly (if original was XAPK) +# ===================================================================== + +if [[ "$IS_XAPK" == true ]] && [[ "$DO_SIGN" == true ]]; then + echo + echo "=== Assembling XAPK ===" + + XAPK_WORKDIR=$(mktemp -d "${TMPDIR:-/tmp}/xapk-rebuild-XXXXXX") + xapk_cleanup() { rm -rf "$XAPK_WORKDIR"; } + trap xapk_cleanup EXIT + + # Read base APK name from metadata + BASE_APK_NAME=$(sed -n 's/.*"base_apk"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$XAPK_ORIGIN_DIR/metadata.json" | head -1) + if [[ -z "$BASE_APK_NAME" ]]; then + BASE_APK_NAME="base.apk" + fi + + # Copy signed base APK + cp "$OUTPUT" "$XAPK_WORKDIR/$BASE_APK_NAME" + info "Copied signed base APK as $BASE_APK_NAME" + + # Re-sign and copy each split APK + if [[ -d "$XAPK_ORIGIN_DIR/splits" ]]; then + for split_apk in "$XAPK_ORIGIN_DIR/splits/"*.apk; do + if [[ ! -f "$split_apk" ]]; then continue; fi + split_name=$(basename "$split_apk") + info "Signing split: $split_name" + apksigner sign \ + --ks "$KEYSTORE" \ + --ks-key-alias "$KEY_ALIAS" \ + --ks-pass "pass:$STORE_PASS" \ + --key-pass "pass:$KEY_PASS" \ + --out "$XAPK_WORKDIR/$split_name" \ + "$split_apk" + echo "SPLIT_SIGNED:$split_name" + done + fi + + # Copy manifest.json and icon from .xapk-origin/ + if [[ -f "$XAPK_ORIGIN_DIR/manifest.json" ]]; then + cp "$XAPK_ORIGIN_DIR/manifest.json" "$XAPK_WORKDIR/" + fi + for icon_file in "$XAPK_ORIGIN_DIR"/icon.png "$XAPK_ORIGIN_DIR"/icon.jpg; do + if [[ -f "$icon_file" ]]; then + cp "$icon_file" "$XAPK_WORKDIR/" + break + fi + done + + # Assemble XAPK (zip with no compression for APKs) + # Make output path absolute for the subshell cd + XAPK_OUTPUT=$(realpath -m "$OUTPUT" 2>/dev/null || echo "$(pwd)/$OUTPUT") + # Remove the base APK output (it's now inside the XAPK) + rm -f "$XAPK_OUTPUT" 2>/dev/null || true + + (cd "$XAPK_WORKDIR" && zip -r -0 "$XAPK_OUTPUT" . 2>&1) || { + fail "Failed to assemble XAPK archive" + exit 1 + } + + if [[ ! -f "$XAPK_OUTPUT" ]]; then + fail "XAPK output not found at: $XAPK_OUTPUT" + exit 1 + fi + # Update OUTPUT to the absolute path used + OUTPUT="$XAPK_OUTPUT" + + ok "XAPK assembled: $XAPK_OUTPUT" + echo "XAPK_ASSEMBLED:$XAPK_OUTPUT" + + # Clean up workdir + xapk_cleanup + trap - EXIT +fi + # ===================================================================== # Summary # ===================================================================== echo echo "=== Rebuild Complete ===" -echo "Output APK: $OUTPUT" -echo "Signed with: $SIGNER ($( [[ "$USE_DEBUG_KEY" == true ]] && echo "debug key" || echo "custom keystore" ))" -APK_SIZE=$(stat -f%z "$OUTPUT" 2>/dev/null || stat -c%s "$OUTPUT" 2>/dev/null || echo "unknown") -echo "APK size: $APK_SIZE bytes" +if [[ "$IS_XAPK" == true ]]; then + echo "Output XAPK: $OUTPUT" +else + echo "Output APK: $OUTPUT" +fi + +if [[ "$DO_SIGN" == true ]]; then + SIGN_DESC="$KEYSTORE_SOURCE" + case "$KEYSTORE_SOURCE" in + debug-standard) SIGN_DESC="Android SDK debug key (~/.android/debug.keystore)" ;; + debug-previous) SIGN_DESC="previous neutralizer debug key" ;; + debug-generated) SIGN_DESC="auto-generated debug key" ;; + custom) SIGN_DESC="custom keystore ($KEYSTORE)" ;; + esac + echo "Signed with: $SIGNER ($SIGN_DESC)" +fi + +OUTPUT_SIZE=$(stat -f%z "$OUTPUT" 2>/dev/null || stat -c%s "$OUTPUT" 2>/dev/null || echo "unknown") +echo "Output size: $OUTPUT_SIZE bytes" if [[ "$BUILD_USED_NO_RES" == true ]]; then echo @@ -351,6 +543,12 @@ fi echo echo "WARNING: Play Integrity / SafetyNet will FAIL — expected for enterprise sideloading." -echo "Install via: adb install $OUTPUT" +if [[ "$IS_XAPK" == true ]]; then + echo "Install via: adb install-multiple ..." + echo " or: use a split APK installer (e.g., SAI — Split APKs Installer)" + echo " or: unzip the XAPK and run: adb install-multiple *.apk" +else + echo "Install via: adb install $OUTPUT" +fi exit 0