SDK Neutralizer v3: full XAPK round-trip, auto-keystore, neutralize-all installer

- decode-apk.sh preserves XAPK structure (.xapk-origin/) with splits, manifest, and metadata for automatic reassembly
- rebuild-apk.sh detects .xapk-origin/, re-signs all split APKs, and assembles final .xapk output
- Add --auto-keystore flag: prioritizes ~/.android/debug.keystore → previous debug key → generated key
- Add neutralize-all compound target to install-dep.sh (java + apktool + apksigner + zip in one command)
- Add apktool version check (>= 2.9.0) with automatic upgrade from GitHub releases
- Add install_zip() and zip dependency check for XAPK rebuild
- All scripts now prepend ~/.local/bin to PATH for user-local tool installs
- Update SKILL.md Phase 3 with built-in catalog detection workflow and custom SDK discovery using Claude Code tools
- Update neutralize.md, CLAUDE.md, and README.md with XAPK and auto-keystore documentation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Simone Avogadro 2026-03-09 14:37:06 +01:00
parent 291e785c67
commit e70920cf87
10 changed files with 687 additions and 75 deletions

View File

@ -34,6 +34,9 @@ bash scripts/check-deps.sh
# Install a dependency (auto-detects OS/package manager) # Install a dependency (auto-detects OS/package manager)
bash scripts/install-dep.sh <dep> # e.g., jadx, vineflower, dex2jar bash scripts/install-dep.sh <dep> # 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 # Decompile an APK/JAR/AAR/XAPK
bash scripts/decompile.sh [--engine jadx|fernflower|both] [--deobf] [--no-res] [-o outdir] <file> bash scripts/decompile.sh [--engine jadx|fernflower|both] [--deobf] [--no-res] [-o outdir] <file>
@ -61,14 +64,14 @@ SDK neutralizer scripts under `plugins/android-reverse-engineering/skills/sdk-ne
# Check neutralization dependencies (including apktool >= 2.9.0) # Check neutralization dependencies (including apktool >= 2.9.0)
bash check-neutralize-deps.sh 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 <file.apk|file.xapk> [-o <decoded-dir>] bash decode-apk.sh <file.apk|file.xapk> [-o <decoded-dir>]
# Neutralize SDK entry points in decoded APK (dry-run first) # Neutralize SDK entry points in decoded APK (dry-run first)
bash neutralize.sh <decoded-dir> [--ads|--trackers|--all] [--dry-run] [--no-backup] [--no-manifest] [--targets-file <file>] [--replay] [--no-save-manifest] bash neutralize.sh <decoded-dir> [--ads|--trackers|--all] [--dry-run] [--no-backup] [--no-manifest] [--targets-file <file>] [--replay] [--no-save-manifest]
# Rebuild and sign neutralized APK # Rebuild and sign neutralized APK (auto-reassembles XAPK if .xapk-origin/ exists)
bash rebuild-apk.sh <decoded-dir> [--debug-key|--keystore <file>] [-o <output>] [--no-sign] [--no-res] [--zipalign] bash rebuild-apk.sh <decoded-dir> [--auto-keystore|--debug-key|--keystore <file>] [-o <output>] [--no-sign] [--no-res] [--zipalign]
``` ```
## Architecture ## Architecture

View File

@ -25,7 +25,8 @@ A Claude Code skill that decompiles Android APK/XAPK/JAR/AAR files, **extracts H
**For SDK neutralization (`/neutralize`):** **For SDK neutralization (`/neutralize`):**
- [apktool](https://apktool.org/) (required) — APK decode/rebuild - [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. 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 jadx
bash plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/install-dep.sh vineflower 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) # Decompile APK with jadx (default)
bash plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/decompile.sh app.apk 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/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 --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/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 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 # Replay previous patches after re-decoding
bash plugins/android-reverse-engineering/skills/sdk-neutralizer/scripts/neutralize.sh app-decoded --replay bash plugins/android-reverse-engineering/skills/sdk-neutralizer/scripts/neutralize.sh app-decoded --replay

View File

@ -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 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
bash ${CLAUDE_PLUGIN_ROOT}/skills/android-reverse-engineering/scripts/install-dep.sh <dep> 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 <full-path-to>/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 ### 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 ```bash
# Strip both .apk and .xapk extensions for the output dir name # 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:<path>`). Verify the decoded directory contains `smali/` and `AndroidManifest.xml` (the script does this automatically and outputs `DECODED_DIR:<path>`).
If the output includes `XAPK_ORIGIN:<path>`, 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 ### Step 5: Identify targets
Run entry point detection to find which SDK calls exist in the app code: 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 ### 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
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:<path>` — which keystore was used
- `KEYSTORE_SOURCE:<source>` — how it was resolved (debug-standard, debug-previous, debug-generated, custom)
- `SPLIT_SIGNED:<filename>` — each re-signed split APK (XAPK only)
- `XAPK_ASSEMBLED:<path>` — 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 ### 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.** 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 <path>` for APK, `adb install-multiple <base.apk> <splits...>` for XAPK
Tell the user what they can do next: Tell the user what they can do next:
- **Test thoroughly**: "Install via `adb install <apk>` and test for crashes — especially features tied to ads or analytics" - **Test thoroughly**: for APK: "Install via `adb install <apk>`"; 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" - **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" - **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" - **Deep analysis**: "Run `/find-trackers` or `/find-ads` for full SDK analysis"

View File

@ -9,6 +9,11 @@ errors=0
missing_required=() missing_required=()
missing_optional=() 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 "=== Android Reverse Engineering: Dependency Check ==="
echo echo

View File

@ -2,6 +2,11 @@
# decompile.sh — Decompile APK/JAR/AAR using jadx, fernflower, or both # decompile.sh — Decompile APK/JAR/AAR using jadx, fernflower, or both
set -euo pipefail 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() { usage() {
cat <<EOF cat <<EOF
Usage: decompile.sh [OPTIONS] <file> Usage: decompile.sh [OPTIONS] <file>

View File

@ -1,7 +1,8 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# install-dep.sh — Install a single dependency for Android reverse engineering # install-dep.sh — Install a single dependency for Android reverse engineering
# Usage: install-dep.sh <dependency> # Usage: install-dep.sh <dependency>
# 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: # Exit codes:
# 0 — installed successfully # 0 — installed successfully
@ -9,6 +10,11 @@
# 2 — requires manual action (e.g. sudo needed but not available) # 2 — requires manual action (e.g. sudo needed but not available)
set -euo pipefail 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() { usage() {
cat <<EOF cat <<EOF
Usage: install-dep.sh <dependency> Usage: install-dep.sh <dependency>
@ -24,6 +30,10 @@ Available dependencies:
adb Android Debug Bridge adb Android Debug Bridge
smali Smali/baksmali assembler/disassembler smali Smali/baksmali assembler/disassembler
apksigner Android APK signing tool 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: The script detects your OS and package manager, then:
- Installs directly if possible (brew, or user-local install) - Installs directly if possible (brew, or user-local install)
@ -434,23 +444,80 @@ install_dex2jar() {
} }
install_apktool() { 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 if command -v apktool &>/dev/null; then
ok "apktool already installed" 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 return 0
fi fi
else
need_install=true
fi
case "$PKG_MANAGER" in if [[ "$need_install" == true ]]; then
brew) info "Installing apktool via Homebrew..."; brew install apktool ;; # User-local install from GitHub (no sudo needed, takes PATH precedence)
apt) pkg_install "apktool" ;; info "Installing apktool from GitHub releases to ~/.local/bin..."
*) manual "Install apktool from https://apktool.org/docs/install" ;; local tag
esac 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 local version="${tag#v}"
ok "apktool installed" 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 else
fail "apktool installation may have failed." fail "apktool installation may have failed."
exit 1 exit 1
fi fi
fi
} }
install_adb() { install_adb() {
@ -656,6 +723,49 @@ install_apksigner() {
manual "Install Android SDK build-tools or run: sudo apt install apksigner (Debian/Ubuntu)" 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 # Dispatch
# ===================================================================== # =====================================================================
@ -669,9 +779,12 @@ case "$DEP" in
adb) install_adb ;; adb) install_adb ;;
smali|baksmali) install_smali ;; smali|baksmali) install_smali ;;
apksigner) install_apksigner ;; apksigner) install_apksigner ;;
zip) install_zip ;;
neutralize-all) install_neutralize_all ;;
*) *)
echo "Error: Unknown dependency '$DEP'" >&2 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 exit 1
;; ;;
esac esac

View File

@ -64,15 +64,22 @@ Check that all required tools are installed.
bash ${CLAUDE_PLUGIN_ROOT}/skills/sdk-neutralizer/scripts/check-neutralize-deps.sh 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
bash ${CLAUDE_PLUGIN_ROOT}/skills/android-reverse-engineering/scripts/install-dep.sh <dep> # 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 <plugin-root>/skills/android-reverse-engineering/scripts/install-dep.sh neutralize-all
``` ```
### Phase 2: Decode APK ### 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. **Action**: Run the decode script.
@ -82,24 +89,99 @@ bash ${CLAUDE_PLUGIN_ROOT}/skills/sdk-neutralizer/scripts/decode-apk.sh <apk-or-
The script verifies the output contains `smali/` and `AndroidManifest.xml` and outputs `DECODED_DIR:<path>`. The script verifies the output contains `smali/` and `AndroidManifest.xml` and outputs `DECODED_DIR:<path>`.
For XAPK input, the script also outputs `XAPK_ORIGIN:<path>` 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 ### 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 ```bash
# Detect ad SDK entry points called from app code bash ${CLAUDE_PLUGIN_ROOT}/skills/sdk-neutralizer/scripts/neutralize.sh <decoded-dir> --all --dry-run
bash ${CLAUDE_PLUGIN_ROOT}/skills/ad-analysis/scripts/find-ads.sh <decoded-dir> --entrypoints
# Detect tracker SDK entry points called from app code
bash ${CLAUDE_PLUGIN_ROOT}/skills/tracker-analysis/scripts/find-trackers.sh <decoded-dir> --entrypoints
``` ```
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:
```
<smali-class-path>:<method-name>
```
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: <decoded-dir>/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 - `--ads` — only ad SDKs
- `--trackers` — only tracker/analytics SDKs - `--trackers` — only tracker/analytics SDKs
- `--all` — both (default) - `--all` — both (default)
- Optionally exclude specific SDKs
### Phase 4: Neutralize ### Phase 4: Neutralize
@ -115,6 +197,16 @@ bash ${CLAUDE_PLUGIN_ROOT}/skills/sdk-neutralizer/scripts/neutralize.sh <decoded
bash ${CLAUDE_PLUGIN_ROOT}/skills/sdk-neutralizer/scripts/neutralize.sh <decoded-dir> --all bash ${CLAUDE_PLUGIN_ROOT}/skills/sdk-neutralizer/scripts/neutralize.sh <decoded-dir> --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 <decoded-dir> --all --dry-run --targets-file <decoded-dir>/custom-targets.txt
# Apply with custom targets
bash ${CLAUDE_PLUGIN_ROOT}/skills/sdk-neutralizer/scripts/neutralize.sh <decoded-dir> --all --targets-file <decoded-dir>/custom-targets.txt
```
Parse the output for `PATCHED:` and `MANIFEST_DISABLED:` lines to build the report. Parse the output for `PATCHED:` and `MANIFEST_DISABLED:` lines to build the report.
Options: Options:
@ -130,21 +222,38 @@ After a successful (non-dry-run) neutralization, a `neutralize-manifest.json` is
### Phase 5: Rebuild & Sign ### 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 <file> --key-alias <alias> --store-pass <pass> --key-pass <pass>`
- Option 3 → `--no-sign`
**Action**: Run the rebuild script with the chosen signing option.
```bash ```bash
bash ${CLAUDE_PLUGIN_ROOT}/skills/sdk-neutralizer/scripts/rebuild-apk.sh <decoded-dir> --debug-key # Example with auto-keystore (recommended default)
bash ${CLAUDE_PLUGIN_ROOT}/skills/sdk-neutralizer/scripts/rebuild-apk.sh <decoded-dir> --auto-keystore
``` ```
Options: Options:
- `-o <output.apk>` — custom output path - `-o <output>` — custom output path
- `--debug-key` — auto-generate debug keystore (default) - `--auto-keystore` — auto-detect best keystore (recommended)
- `--debug-key` — always generate new debug keystore
- `--keystore <file>` — use a custom keystore - `--keystore <file>` — use a custom keystore
- `--no-sign` — output unsigned APK - `--no-sign` — output unsigned APK
- `--zipalign` / `--no-zipalign` — control zipalign step - `--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 ### Phase 6: Verify & Report
Generate a structured neutralization report and suggest next steps. Generate a structured neutralization report and suggest next steps.
@ -201,9 +310,12 @@ and privacy compliance only.
## Output ## Output
- Sanitized APK: `<path>` - Sanitized APK/XAPK: `<path>`
- Signed with: debug key / custom keystore - Output format: APK (single) / XAPK (split bundle)
- Install via: `adb install <path>` - Signed with: auto-detected debug key / generated debug key / custom keystore
- Keystore used: `<path>` (source: `KEYSTORE_SOURCE:` value)
- Install via: `adb install <path>` (APK) or `adb install-multiple <base.apk> <split1.apk> ...` (XAPK)
- For XAPK: can also use SAI (Split APKs Installer) or unzip and `adb install-multiple *.apk`
``` ```
**Next steps to suggest:** **Next steps to suggest:**

View File

@ -7,6 +7,11 @@ REQUIRED_JAVA_MAJOR=17
missing_required=() missing_required=()
missing_optional=() 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 "=== SDK Neutralizer: Dependency Check ==="
echo echo
@ -69,11 +74,15 @@ fi
# --- apksigner or jarsigner (at least one required) --- # --- apksigner or jarsigner (at least one required) ---
signer_found=false signer_found=false
has_apksigner=false
if command -v apksigner &>/dev/null; then if command -v apksigner &>/dev/null; then
echo "[OK] apksigner detected" echo "[OK] apksigner detected"
signer_found=true signer_found=true
has_apksigner=true
elif command -v jarsigner &>/dev/null; then elif command -v jarsigner &>/dev/null; then
echo "[OK] jarsigner detected (fallback signer)" 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 signer_found=true
fi fi
if [[ "$signer_found" == false ]]; then if [[ "$signer_found" == false ]]; then
@ -111,6 +120,14 @@ else
fi fi
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 --- # --- Machine-readable summary ---
echo echo
if [[ ${#missing_required[@]} -gt 0 ]]; then if [[ ${#missing_required[@]} -gt 0 ]]; then
@ -131,7 +148,14 @@ echo
if [[ ${#missing_required[@]} -gt 0 ]]; then if [[ ${#missing_required[@]} -gt 0 ]]; then
echo "*** ${#missing_required[@]} required dependency/ies missing. ***" echo "*** ${#missing_required[@]} required dependency/ies missing. ***"
echo "Run install-dep.sh <name> 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 <name>"
exit 1 exit 1
else else
if [[ ${#missing_optional[@]} -gt 0 ]]; then if [[ ${#missing_optional[@]} -gt 0 ]]; then

View File

@ -8,14 +8,20 @@
# 1 — error (invalid input, missing tools, decode failed) # 1 — error (invalid input, missing tools, decode failed)
set -euo pipefail 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() { usage() {
cat <<EOF cat <<EOF
Usage: decode-apk.sh <file> [OPTIONS] Usage: decode-apk.sh <file> [OPTIONS]
Decode an APK or XAPK file into smali and resources using apktool. 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. For XAPK files, extracts the base APK and decodes it. Split APKs and
Split APKs (config.*.apk) are skipped with a warning. XAPK metadata are preserved in .xapk-origin/ inside the decoded directory
so that rebuild-apk.sh can reassemble a complete XAPK.
Arguments: Arguments:
<file> Path to .apk or .xapk file <file> Path to .apk or .xapk file
@ -28,6 +34,7 @@ Options:
Output: Output:
DECODED_DIR:<path> DECODED_DIR:<path>
XAPK_ORIGIN:<path> (only for XAPK input)
EOF EOF
exit 0 exit 0
} }
@ -91,20 +98,21 @@ if [[ -z "$OUTPUT_DIR" ]]; then
fi fi
# ===================================================================== # =====================================================================
# XAPK handling — extract base APK # XAPK handling — extract base APK, preserve structure for rebuild
# ===================================================================== # =====================================================================
APK_TO_DECODE="$INPUT_FILE_ABS" APK_TO_DECODE="$INPUT_FILE_ABS"
XAPK_TMPDIR="" XAPK_TMPDIR=""
IS_XAPK=false
cleanup_xapk() { cleanup_xapk() {
if [[ -n "$XAPK_TMPDIR" ]] && [[ -d "$XAPK_TMPDIR" ]]; then if [[ -n "$XAPK_TMPDIR" ]] && [[ -d "$XAPK_TMPDIR" ]]; then
rm -rf "$XAPK_TMPDIR" rm -rf "$XAPK_TMPDIR"
fi fi
} }
trap cleanup_xapk EXIT
if [[ "$ext_lower" == "xapk" ]]; then if [[ "$ext_lower" == "xapk" ]]; then
IS_XAPK=true
echo "=== Extracting XAPK archive ===" echo "=== Extracting XAPK archive ==="
XAPK_TMPDIR=$(mktemp -d "${TMPDIR:-/tmp}/xapk-decode-XXXXXX") XAPK_TMPDIR=$(mktemp -d "${TMPDIR:-/tmp}/xapk-decode-XXXXXX")
unzip -qo "$INPUT_FILE_ABS" -d "$XAPK_TMPDIR" unzip -qo "$INPUT_FILE_ABS" -d "$XAPK_TMPDIR"
@ -122,6 +130,7 @@ if [[ "$ext_lower" == "xapk" ]]; then
if [[ ${#all_apks[@]} -eq 0 ]]; then if [[ ${#all_apks[@]} -eq 0 ]]; then
echo "Error: No APK files found inside XAPK archive." >&2 echo "Error: No APK files found inside XAPK archive." >&2
rm -rf "$XAPK_TMPDIR"
exit 1 exit 1
fi fi
@ -158,23 +167,24 @@ if [[ "$ext_lower" == "xapk" ]]; then
if [[ -z "$base_apk" ]]; then if [[ -z "$base_apk" ]]; then
echo "Error: Could not identify a base APK inside the XAPK." >&2 echo "Error: Could not identify a base APK inside the XAPK." >&2
rm -rf "$XAPK_TMPDIR"
exit 1 exit 1
fi fi
BASE_APK_NAME=$(basename "$base_apk")
echo echo
echo "Selected base APK: $(basename "$base_apk")" echo "Selected base APK: $BASE_APK_NAME"
# Warn about skipped splits # List split APKs
skipped=0 split_apks=()
for f in "${all_apks[@]}"; do for f in "${all_apks[@]}"; do
if [[ "$f" != "$base_apk" ]]; then if [[ "$f" != "$base_apk" ]]; then
echo " [skipped] $(basename "$f")" split_apks+=("$(basename "$f")")
skipped=$((skipped + 1)) echo " [split] $(basename "$f")"
fi fi
done done
if (( skipped > 0 )); then if (( ${#split_apks[@]} > 0 )); then
echo "Warning: $skipped split APK(s) skipped. Only the base APK is decoded." echo "${#split_apks[@]} split APK(s) preserved in .xapk-origin/splits/ for rebuild."
echo " Split APKs contain config-specific resources (density, locale, ABI)."
fi fi
echo echo
@ -221,6 +231,101 @@ if [[ ! -f "$OUTPUT_DIR/AndroidManifest.xml" ]]; then
echo "Warning: AndroidManifest.xml not found in decoded output." >&2 echo "Warning: AndroidManifest.xml not found in decoded output." >&2
fi 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
echo "Decoded successfully: $OUTPUT_DIR" echo "Decoded successfully: $OUTPUT_DIR"
echo "DECODED_DIR:$OUTPUT_DIR" echo "DECODED_DIR:$OUTPUT_DIR"

View File

@ -9,17 +9,26 @@
# 2 — manual action needed (missing tools) # 2 — manual action needed (missing tools)
set -euo pipefail 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() { usage() {
cat <<EOF cat <<EOF
Usage: rebuild-apk.sh <decoded-dir> [OPTIONS] Usage: rebuild-apk.sh <decoded-dir> [OPTIONS]
Rebuild an apktool-decoded APK directory back into a signed APK. 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: Arguments:
<decoded-dir> Path to the apktool-decoded APK directory <decoded-dir> Path to the apktool-decoded APK directory
Options: Options:
-o, --output <file> Output APK path (default: <decoded-dir>-neutralized.apk) -o, --output <file> Output path (default: <decoded-dir>-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) --debug-key Sign with an auto-generated debug keystore (default)
--keystore <file> Path to a custom keystore file --keystore <file> Path to a custom keystore file
--key-alias <alias> Key alias within the keystore (default: key0) --key-alias <alias> Key alias within the keystore (default: key0)
@ -36,6 +45,10 @@ Output:
BUILD_WARNING:Resources were not recompiled (--no-res fallback) BUILD_WARNING:Resources were not recompiled (--no-res fallback)
SIGN_OK:<output-apk> SIGN_OK:<output-apk>
VERIFY_OK:<output-apk> VERIFY_OK:<output-apk>
KEYSTORE_USED:<path>
KEYSTORE_SOURCE:debug-standard|debug-previous|debug-generated|custom
SPLIT_SIGNED:<filename> (XAPK only)
XAPK_ASSEMBLED:<output-xapk> (XAPK only)
EOF EOF
exit 0 exit 0
} }
@ -47,6 +60,7 @@ EOF
DECODED_DIR="" DECODED_DIR=""
OUTPUT="" OUTPUT=""
USE_DEBUG_KEY=true USE_DEBUG_KEY=true
USE_AUTO_KEYSTORE=false
KEYSTORE="" KEYSTORE=""
KEY_ALIAS="key0" KEY_ALIAS="key0"
KEY_PASS="android" KEY_PASS="android"
@ -63,7 +77,8 @@ while [[ $# -gt 0 ]]; do
shift shift
if [[ $# -eq 0 ]]; then echo "Error: --output requires a file argument" >&2; exit 1; fi if [[ $# -eq 0 ]]; then echo "Error: --output requires a file argument" >&2; exit 1; fi
OUTPUT="$1"; shift ;; 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) --keystore)
shift shift
if [[ $# -eq 0 ]]; then echo "Error: --keystore requires a file argument" >&2; exit 1; fi 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 exit 1
fi 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 if [[ -z "$OUTPUT" ]]; then
# Strip trailing slash
local_dir="${DECODED_DIR%/}" local_dir="${DECODED_DIR%/}"
if [[ "$IS_XAPK" == true ]]; then
OUTPUT="${local_dir}-neutralized.xapk"
else
OUTPUT="${local_dir}-neutralized.apk" OUTPUT="${local_dir}-neutralized.apk"
fi
fi fi
# ===================================================================== # =====================================================================
@ -138,6 +163,23 @@ if [[ "$DO_SIGN" == true ]]; then
fi fi
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 # Check zipalign availability
ZIPALIGN_CMD="" ZIPALIGN_CMD=""
if [[ "$DO_ZIPALIGN" == true ]] && [[ "$NO_ZIPALIGN" == false ]]; then if [[ "$DO_ZIPALIGN" == true ]] && [[ "$NO_ZIPALIGN" == false ]]; then
@ -243,16 +285,68 @@ fi
# ===================================================================== # =====================================================================
if [[ "$DO_SIGN" == false ]]; then if [[ "$DO_SIGN" == false ]]; then
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" cp "$ALIGNED_APK" "$OUTPUT"
ok "Unsigned APK saved to: $OUTPUT" ok "Unsigned APK saved to: $OUTPUT"
echo "BUILD_OK:$OUTPUT" echo "BUILD_OK:$OUTPUT"
echo echo
echo "WARNING: APK is unsigned and cannot be installed without signing." echo "WARNING: APK is unsigned and cannot be installed without signing."
fi
exit 0 exit 0
fi fi
# Generate debug keystore if needed # Resolve keystore
if [[ "$USE_DEBUG_KEY" == true ]]; then 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" KEYSTORE="$DECODED_DIR/.neutralizer-debug.keystore"
if [[ ! -f "$KEYSTORE" ]]; then if [[ ! -f "$KEYSTORE" ]]; then
info "Generating debug keystore..." info "Generating debug keystore..."
@ -268,6 +362,10 @@ if [[ "$USE_DEBUG_KEY" == true ]]; then
2>/dev/null 2>/dev/null
ok "Debug keystore generated: $KEYSTORE" ok "Debug keystore generated: $KEYSTORE"
fi fi
KEYSTORE_SOURCE="debug-generated"
else
# Custom keystore provided via --keystore
KEYSTORE_SOURCE="custom"
fi fi
if [[ ! -f "$KEYSTORE" ]]; then if [[ ! -f "$KEYSTORE" ]]; then
@ -275,6 +373,9 @@ if [[ ! -f "$KEYSTORE" ]]; then
exit 1 exit 1
fi fi
echo "KEYSTORE_USED:$KEYSTORE"
echo "KEYSTORE_SOURCE:$KEYSTORE_SOURCE"
info "Signing APK with $SIGNER..." info "Signing APK with $SIGNER..."
if [[ "$SIGNER" == "apksigner" ]]; then if [[ "$SIGNER" == "apksigner" ]]; then
@ -330,17 +431,108 @@ elif [[ "$SIGNER" == "jarsigner" ]]; then
fi fi
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 # Summary
# ===================================================================== # =====================================================================
echo echo
echo "=== Rebuild Complete ===" 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") if [[ "$IS_XAPK" == true ]]; then
echo "APK size: $APK_SIZE bytes" 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 if [[ "$BUILD_USED_NO_RES" == true ]]; then
echo echo
@ -351,6 +543,12 @@ fi
echo echo
echo "WARNING: Play Integrity / SafetyNet will FAIL — expected for enterprise sideloading." 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 <base.apk> <split1.apk> <split2.apk> ..."
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 exit 0