# LSParanoid LSParanoid is a string obfuscator for Android applications, implemented as a Gradle plugin. It works by transforming compiled bytecode at build time: every string literal and static final string constant in annotated classes (or all classes, when global obfuscation is enabled) is replaced with a call to a generated `Deobfuscator.getString(id)` method, making the original strings invisible to static analysis tools. The plugin integrates cleanly with the Android Gradle Plugin (AGP 8+) via the Variant API and fully supports Gradle's configuration cache. The plugin is composed of three modules: `core` (the `@Obfuscate` annotation and the `DeobfuscatorHelper` runtime), `processor` (the bytecode transformation engine built on ASM and Grip), and `gradle-plugin` (the AGP-integrated Gradle plugin that wires everything together). At build time, the `LSParanoidTask` intercepts all compiled classes, runs the `ParanoidProcessor` over them, and emits a transformed JAR containing obfuscated classes plus a freshly generated `Deobfuscator` class that decodes strings at runtime using a seeded XorShift-based PRNG. --- ## Plugin Setup via `settings.gradle.kts` Apply the plugin through `pluginManagement` so it resolves from Maven Central before any project is configured. ```kotlin // settings.gradle.kts pluginManagement { repositories { mavenCentral() } plugins { id("org.lsposed.lsparanoid") version "0.6.0" } } ``` --- ## Applying the Plugin to an Android Module After adding it to `pluginManagement`, apply the plugin ID in the module's `build.gradle.kts` alongside the Android plugin. ```kotlin // app/build.gradle.kts plugins { id("com.android.application") id("org.lsposed.lsparanoid") } android { namespace = "com.example.myapp" compileSdk = 35 defaultConfig { applicationId = "com.example.myapp" minSdk = 24 targetSdk = 35 } compileOptions { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 } } ``` The plugin automatically adds `org.lsposed.lsparanoid:core` as an `implementation` dependency, so `@Obfuscate` is available on the classpath without a manual dependency declaration. --- ## `@Obfuscate` Annotation A class-level annotation (`@Target(ElementType.TYPE)`, `@Retention(RetentionPolicy.CLASS)`) that marks a class for string obfuscation. Only classes carrying this annotation are obfuscated unless `classFilter` is set to override the selection logic. ```java // MainActivity.java import org.lsposed.lsparanoid.Obfuscate; import android.app.Activity; import android.os.Bundle; import android.widget.TextView; @Obfuscate // <-- all strings in this class will be obfuscated at compile time public class MainActivity extends Activity { private static final String API_KEY = "super-secret-key-1234"; private static final String BASE_URL = "https://api.example.com/v1"; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // At runtime these become: Deobfuscator.getString(0), Deobfuscator.getString(1), etc. TextView tv = new TextView(this); tv.setText(String.format("Connecting to %s", BASE_URL)); setContentView(tv); } } ``` **Before obfuscation** the compiled class contains the literal `"super-secret-key-1234"`. **After obfuscation** the class reads: ```java private static final String API_KEY = Deobfuscator.getString(4L); private static final String BASE_URL = Deobfuscator.getString(5L); ``` --- ## `lsparanoid` Extension — `seed` An `Int?` that seeds the PRNG used to obfuscate strings. When `null` (default) a fresh `SecureRandom` seed is picked on every build, producing non-deterministic obfuscated output. Setting a fixed integer makes the obfuscation task **cacheable** by Gradle's build cache. ```kotlin // build.gradle.kts lsparanoid { seed = 114514 // fixed seed → deterministic output → task can be UP-TO-DATE / restored from cache } ``` --- ## `lsparanoid` Extension — `classFilter` A lambda `(String) -> Boolean` that receives the fully-qualified class name and returns `true` to obfuscate that class. When `null` (default) only `@Obfuscate`-annotated classes are processed. Set to `{ true }` for global obfuscation of every class, or supply a predicate for fine-grained control. ```kotlin // build.gradle.kts — three representative patterns // 1. Global obfuscation (obfuscate every class, no annotation needed) lsparanoid { classFilter = { true } } // 2. Obfuscate only classes in a specific package lsparanoid { classFilter = { className -> className.startsWith("com.example.secret.") } } // 3. Exclude the synthetic module-info class that Kotlin emits lsparanoid { classFilter = { className -> className != "module-info" } } ``` --- ## `lsparanoid` Extension — `includeDependencies` A `Boolean` (default `false`) that controls whether obfuscation is applied to **all** scoped artifacts (including AAR/JAR dependencies) or only to project source classes. When `true`, the plugin uses `Scope.ALL` instead of `Scope.PROJECT`. ```kotlin // build.gradle.kts lsparanoid { includeDependencies = true // also transforms strings inside dependency JARs/AARs } ``` --- ## `lsparanoid` Extension — `variantFilter` A lambda `(Variant) -> Boolean` that is evaluated for every Android build variant. Return `false` to skip obfuscation for that variant entirely. The lambda also provides a convenient place to set `seed`, `classFilter`, and `includeDependencies` per-variant before returning. ```kotlin // build.gradle.kts — release-only obfuscation with per-flavor configuration lsparanoid { variantFilter = { variant -> when { // globalObfuscate flavor + release: obfuscate every class with a stable seed variant.flavorName == "globalObfuscate" && variant.buildType == "release" -> { seed = 114514 classFilter = { true } includeDependencies = true true } // All other release builds: annotation-based obfuscation with a stable seed variant.buildType == "release" -> { seed = 1919810 classFilter = null true } // Debug builds: skip obfuscation entirely else -> false } } } ``` --- ## Library Module Configuration Library modules can also apply the plugin. Strings in the compiled AAR will be obfuscated before the AAR is published or consumed by an application. ```kotlin // library/build.gradle.kts plugins { id("com.android.library") id("org.lsposed.lsparanoid") } lsparanoid { classFilter = { true } // obfuscate all classes in this library globally seed = 42 // stable seed for reproducible AAR output } android { namespace = "com.example.mylibrary" compileSdk = 35 defaultConfig { minSdk = 24 } compileOptions { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 } } ``` When an application applies `includeDependencies = true`, strings in this AAR are **re-obfuscated** by the application's `LSParanoidTask`, ensuring a consistent deobfuscation key across the whole APK. --- ## Kotlin Class Obfuscation The `@Obfuscate` annotation works identically on Kotlin classes. The plugin also adds `-Xstring-concat=inline` to all Kotlin compilation tasks so that string concatenation is emitted as plain bytecode rather than `invokedynamic`, ensuring the processor can recognize and transform every string. ```kotlin // NetworkClient.kt import org.lsposed.lsparanoid.Obfuscate @Obfuscate class NetworkClient { private val apiKey = "my-private-api-key" private val userAgent = "MyApp/1.0 (Android)" fun buildAuthHeader(): String = "Bearer $apiKey" // concatenation inlined → obfuscated fun buildUserAgentHeader(): String = userAgent } ``` --- ## `DeobfuscatorHelper.getString` (Runtime Internals) The runtime helper that every generated `Deobfuscator` class delegates to. It accepts an encoded `long` ID and a `String[]` of encoded chunks, uses `RandomHelper` to derive a position and XOR key, then reconstructs and returns the plaintext string. This class is part of the public `core` artifact but is only called by generated bytecode — never by user code directly. ```java // Illustrative call emitted by the bytecode transformer (not written by hand): // String decoded = DeobfuscatorHelper.getString(0x0000000200000001L, Deobfuscator.CHUNKS); // DeobfuscatorHelper.MAX_CHUNK_LENGTH = 0x1FFF (8191) // Each entry in CHUNKS[] holds up to 8191 encoded characters; // long-form strings are split across multiple chunks automatically. public static String getString(final long id, final String[] chunks) { // id encodes both the lookup index and a per-string checksum // RandomHelper.seed / RandomHelper.next derive the XOR decryption key // Returns the decoded plaintext String } ``` --- ## Full Multi-Module Sample Setup A complete example combining a library with global obfuscation and an application with release-only, dependency-inclusive obfuscation. ``` my-project/ ├── settings.gradle.kts ├── library/ │ └── build.gradle.kts └── app/ └── build.gradle.kts ``` ```kotlin // settings.gradle.kts pluginManagement { repositories { mavenCentral() } plugins { id("com.android.application") version "8.8.0" id("com.android.library") version "8.8.0" id("org.lsposed.lsparanoid") version "0.6.0" } } dependencyResolutionManagement { repositories { mavenCentral(); google() } } rootProject.name = "my-project" include(":library", ":app") ``` ```kotlin // library/build.gradle.kts plugins { id("com.android.library") id("org.lsposed.lsparanoid") } lsparanoid { classFilter = { true } seed = 11111 } android { namespace = "com.example.library" compileSdk = 35 defaultConfig { minSdk = 24 } } ``` ```kotlin // app/build.gradle.kts plugins { id("com.android.application") id("org.lsposed.lsparanoid") } lsparanoid { seed = 99999 includeDependencies = true variantFilter = { variant -> variant.buildType == "release" } } android { namespace = "com.example.app" compileSdk = 35 defaultConfig { applicationId = "com.example.app" minSdk = 24 targetSdk = 35 } } dependencies { implementation(project(":library")) } ``` ```java // app/src/main/java/com/example/app/MainActivity.java import org.lsposed.lsparanoid.Obfuscate; import android.app.Activity; @Obfuscate public class MainActivity extends Activity { private static final String SECRET = "do-not-reverse-me"; // After release build: private static final String SECRET = Deobfuscator.getString(0L); } ``` --- LSParanoid's primary use case is hardening Android release builds against static reverse engineering — protecting API keys, internal endpoint URLs, license strings, and other sensitive literals that would otherwise be trivially readable with `apktool` or `jadx`. By operating at the bytecode level as a Gradle transform, it requires zero changes to how code is written beyond adding the `@Obfuscate` annotation (or enabling `classFilter = { true }` for fully transparent obfuscation). Integration follows a straightforward pattern: add the plugin to `pluginManagement`, apply it in each module that needs protection, optionally configure `seed` for build-cache compatibility, and restrict execution to release variants via `variantFilter`. Because the `core` runtime dependency is injected automatically and the `Deobfuscator` class is generated per-project with a name-scoped suffix, multiple LSParanoid-enabled modules in the same application do not conflict, and the resulting APK carries a self-contained, deterministic deobfuscation mechanism with no external runtime requirements.