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)
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
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)
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>]
# 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]
# Rebuild and sign neutralized APK
bash rebuild-apk.sh <decoded-dir> [--debug-key|--keystore <file>] [-o <output>] [--no-sign] [--no-res] [--zipalign]
# Rebuild and sign neutralized APK (auto-reassembles XAPK if .xapk-origin/ exists)
bash rebuild-apk.sh <decoded-dir> [--auto-keystore|--debug-key|--keystore <file>] [-o <output>] [--no-sign] [--no-res] [--zipalign]
```
## 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`):**
- [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

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
```
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 <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
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:<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
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:<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
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:
- **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"
- **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"

View File

@ -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

View File

@ -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 <<EOF
Usage: decompile.sh [OPTIONS] <file>

View File

@ -1,7 +1,8 @@
#!/usr/bin/env bash
# install-dep.sh — Install a single dependency for Android reverse engineering
# 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:
# 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 <<EOF
Usage: install-dep.sh <dependency>
@ -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

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
```
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 <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
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 <apk-or-
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
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 <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
bash ${CLAUDE_PLUGIN_ROOT}/skills/sdk-neutralizer/scripts/neutralize.sh <decoded-dir> --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:
```
<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
- `--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 <decoded
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.
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 <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 ${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:
- `-o <output.apk>` — custom output path
- `--debug-key` — auto-generate debug keystore (default)
- `-o <output>` — custom output path
- `--auto-keystore` — auto-detect best keystore (recommended)
- `--debug-key` — always generate new debug keystore
- `--keystore <file>` — 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: `<path>`
- Signed with: debug key / custom keystore
- Install via: `adb install <path>`
- Sanitized APK/XAPK: `<path>`
- Output format: APK (single) / XAPK (split bundle)
- 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:**

View File

@ -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 <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
else
if [[ ${#missing_optional[@]} -gt 0 ]]; then

View File

@ -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 <<EOF
Usage: decode-apk.sh <file> [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:
<file> Path to .apk or .xapk file
@ -28,6 +34,7 @@ Options:
Output:
DECODED_DIR:<path>
XAPK_ORIGIN:<path> (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"

View File

@ -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 <<EOF
Usage: rebuild-apk.sh <decoded-dir> [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:
<decoded-dir> Path to the apktool-decoded APK directory
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)
--keystore <file> Path to a custom keystore file
--key-alias <alias> Key alias within the keystore (default: key0)
@ -36,6 +45,10 @@ Output:
BUILD_WARNING:Resources were not recompiled (--no-res fallback)
SIGN_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
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 <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