# SMALI Patching — Subscription Bypass & ProGuard Deobfuscation

**Context:** Patching ProGuard/R8-obfuscated Android APKs to remove subscription checks at the SMALI level. Covers RevenueCat and generic entitlement patterns.

## Toolkit
- `baksmali.jar` — DEX → SMALI disassembler
- `smali.jar` — SMALI → DEX assembler
- `jarsigner` (JDK) — APK signing
- `keytool` (JDK) — keystore generation
- `zipalign` (Android SDK) — APK alignment (optional, needed if `extractNativeLibs=false`)

## Workflow

### 1. Identify the Entitlement Check
Use jadx to decompile the APK, then search for RevenueCat/CustomerInfo patterns in the **clean (non-obfuscated)** package:

```bash
grep -rn "getEntitlements\|containsKey\|getActive\|CustomerInfo" jadx-out/sources/ \
  | grep -v "revenuecat/" | grep -v "R.java" | head -20
```

Key RevenueCat patterns:
- `getEntitlements().getActive().containsKey("pro")` — most common
- `getEntitlements().getActive().containsKey("premium")`
- `getPurchaserInfo()` — older RevenueCat API
- `CustomerInfo` → entitlement map lookup

### 2. Verify the Catch Block
The decompiled code usually wraps the entitlement check in try/catch:

```java
try {
    // RevenueCat call chain
    zContainsKey = info.getEntitlements().getActive().containsKey("pro");
} catch (Throwable e) {
    zContainsKey = false;  // FAILS CLOSED — must also patch
}
```

**Both paths need patching:** the success path (skip RevenueCat) AND the catch path (return true instead of false).

### 3. Extract DEX Files

```bash
unzip app.apk "classes*.dex" -d dex_out/
```

Check which DEX file contains the target class by searching strings:

```bash
for dex in dex_out/*.dex; do echo "$(strings $dex | grep -c 'C0741y0') $dex"; done
```

**Note:** ProGuard/R8 renames classes internally. The SMALI class name will be the original obfuscated name (e.g., `V2/y0.smali`), not the jadx-assigned `C0741y0`. Search by method body content instead.

### 4. Disassemble to SMALI

```bash
java -jar baksmali.jar d dex_out/classes.dex -o smali_out/
```

### 5. Find the Target in SMALI

Search for the entitlement string in the SMALI:

```bash
grep -rn 'const-string.*"pro"' smali_out/ 2>/dev/null
```

Also search for the call chain:
```bash
grep -rn "getCustomerInfo\|getEntitlements\|getActive\|containsKey" smali_out/ \
  | grep -v "com/revenuecat/" | head -20
```

### 6. Patch the SMALI

Typical SMALI flow for coroutine-based entitlement check:

```smali
# Before (calls RevenueCat):
invoke-virtual {p3}, Lcom/revenuecat/purchases/CustomerInfo;->getEntitlements()Lcom/revenuecat/purchases/EntitlementInfos;
move-result-object p1
invoke-virtual {p1}, Lcom/revenuecat/purchases/EntitlementInfos;->getActive()Ljava/util/Map;
move-result-object p1
const-string p2, "pro"
invoke-interface {p1, p2}, Ljava/util/Map;->containsKey(Ljava/lang/Object;)Z
move-result p1           ; result in p1
:try_end
.catchall {:try_start .. :try_end} :catchall
:goto_93
invoke-static {p1}, Ljava/lang/Boolean;->valueOf(Z)Ljava/lang/Boolean;
move-result-object p1
return-object p1
:catchall
const/4 p1, 0x0           ; FAILS CLOSED — patch to 0x1
goto :goto_93
```

**Patched version:**

```smali
# Skip RevenueCat entirely:
const/4 p1, 0x1            ; hardcoded true
:try_end
.catchall {:try_start .. :try_end} :catchall
:goto_93
invoke-static {p1}, Ljava/lang/Boolean;->valueOf(Z)Ljava/lang/Boolean;
move-result-object p1
return-object p1
:catchall
const/4 p1, 0x1            ; also return true on error
goto :goto_93
```

**Changes:**
1. Replace the entire `getEntitlements → getActive → containsKey → move-result` block with `const/4 p1, 0x1`
2. Change catch block from `const/4 p1, 0x0` to `const/4 p1, 0x1`

### 7. Reassemble

```bash
java -jar smali.jar a smali_out/ -o patched_classes.dex
```

### 8. Inject into APK (Python)

```python
import zipfile

with zipfile.ZipFile('original.apk', 'r') as zin:
    with zipfile.ZipFile('patched.apk', 'w', zipfile.ZIP_DEFLATED) as zout:
        for item in zin.infolist():
            data = zin.read(item.filename)
            if item.filename.startswith('META-INF/'):
                continue  # strip old signatures
            if item.filename == 'classes.dex':
                with open('patched_classes.dex', 'rb') as f:
                    data = f.read()
            # Native libs must be stored uncompressed
            is_native = item.filename.startswith('lib/') and item.filename.endswith('.so')
            compress = zipfile.ZIP_STORED if is_native else zipfile.ZIP_DEFLATED
            zout.writestr(item, data, compress_type=compress)
```

### 9. Sign

```bash
# Generate keystore (one-time)
keytool -genkey -v -keystore debug.keystore -alias signer \
  -keyalg RSA -keysize 2048 -validity 10000 \
  -storepass android -keypass android \
  -dname "CN=Patched, O=Patched, C=US"

# Sign
jarsigner -sigalg SHA256withRSA -digestalg SHA-256 \
  -keystore debug.keystore -storepass android -keypass android \
  patched.apk signer

# Verify
jarsigner -verify -keystore debug.keystore patched.apk

# Align (optional, fix if INSTALL_FAILED_INVALID_APK)
zipalign -p 4 patched.apk patched-aligned.apk
```

## RevenueCat-Specific Notes

### RevenueCat API Key
RevenueCat API keys (format: `goog_...`, `appl_...`) are **public SDK keys** — not secrets. Every app using RevenueCat has them embedded. They only work with that specific RevenueCat project. No security impact from exposure.

### RevenueCat Pattern Recognition
- Entitlement key is almost always `"pro"` (sometimes `"premium"` or `"full"`)
- Check is typically in a ViewModel or repository class
- The method usually takes `(Context, FirebaseUser, Continuation)` if the app uses Firebase Auth
- RevenueCat's `Purchases.configure()` call with the API key is in the same class or nearby

### Common Gotchas
- **Coroutine state machines**: Kotlin coroutines compile to complex SMALI state machines. Don't try to understand every label — find the entitlement check and the catch block, patch those two spots.
- **Multiple entitlement checks**: Some apps check entitlements in multiple places. Search for `containsKey` with the entitlement string across ALL SMALI files.
- **Server-side validation**: If the app additionally validates subscription on its own backend (Firestore REST call, custom API), SMALI patching alone won't work — you'd need MITM or Frida.

## Signing Pitfalls
- Use `SHA256withRSA` not `SHA1withRSA` — Android API 28+ rejects SHA-1 signed APKs
- Native libs (.so files) must be `ZIP_STORED` (uncompressed) in the zip — `ZIP_DEFLATED` causes `INSTALL_FAILED_INVALID_APK: Failed to extract native libraries`
- If `extractNativeLibs=false` in manifest, zipalign is required OR patch manifest `extractNativeLibs` to `true`
