The most common tools used for obfuscation in Android include ProGuard, DexGuard and R8. For most of this article the terms R8, DexGuard, and ProGuard will be used interchangeably as functionality is loosely the same between them, however, the main differences will be detailed below. ProGuard analyses and optimizes the Java bytecode rather than the direct Java/Kotlin code base. ProGuard implements a collection of techniques, these being:

  • Shrinking – Identifies and removes dead code that is not reachable or is unused. Including classes, fields, methods, and attributes.
  • Optimizer – Performs optimization on code and code flow where performance changes can be made.
  • Obfuscator – Renaming aspects of the codebase (i.e., classes, fields, and methods) to names that are deliberately obscure and meaningless.
  • Preverifier – Performs preverification checks on the bytecode, where if the checks are successful, the classfiles are annotated with preverification information. This is required for Java Micro Edition and for Java 6 and higher.

R8 and ProGuard

When enabled, in  Android Gradle plugin 3.4.0 and above ProGuard is no-longer used by default for obfuscation, optimization, and other tasks. Instead, R8 is used. R8 provides a similar suite of functionality to ProGuard (and DexGuard, it’s premium alternative), however, is being actively developed by the team at Android. By default, when compiling a release build of an Android application R8 will automatically perform these functions (as described above) on the codebase. Neither R8 nor ProGuard are categorically better than one another, instead they each come with their own benefits and drawbacks. Some reasons for why R8 was adopted in the Android Gradle plugin include: 

  • With ProGuard, the compiled Java bytecode is taken and converted into optimized Java bytecode and then further converted into optimized Dalvik bytecode. The process for R8 skips a step, taking the Java bytecode straight to optimized Dalvik bytecode – in turn saving time during the building process.   
  • It’s estimated that ProGuard reduces a shrunken application’s size by ~8.5%, while R8 reduces it by ~10%.
  • R8 comes with more widespread Kotlin support and optimizations.

Enabling Obfuscation 

Depending on the version of the Android Gradle plugin being used, the below will either enable ProGuard by default or R8. Edit the ‘buildTypes’ tag in the ‘gradle.build’ file to ‘minifyEnabled true’. To enable obfuscation on a debug build, switch out ‘release’ for ‘debug’. An example of using obfuscation on a release build can be seen below:

buildTypes {

    release {

        minifyEnabled true
        shrinkResources true
        proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 
        proguardFile 'proguard-project.txt' 

    }

}

For the above, if using an older version of the Android Gradle Plugin, and you wish to enable ProGuard over R8, you may need to manually disable R8 in your ‘gradle.properties’, as below:

android.enableR8=false 
android.enableR8.libraries=false

For steps including how to enable ProGuard on the newest versions of the Android Gradle Plugin see their official documentation

The Mapping File

After following the above when making a release build, using Gradle, the Java bytecode will have been analysed by ProGuard. ProGuard will provide a log file of the stages undergone. This is saved at the following relative path to the application project root.

Mapping file relative location:

app/build/outputs/mapping/release/mapping.txt

This file shows the changes that ProGuard has implemented. An example of part of this file is below. In this example it can be seen that the
function ‘showString’ in ‘MainActivity’ has been allow-listed while the other function in ‘MainActivity’ named ‘loadMe’ has not and is now renamed to ‘n’.

Example of mapping file:

com.example.java_dexloadable.MainActivity -> com.example.java_dexloadable.MainActivity:
java.lang.String loadMe() -> n
1:1:java.lang.String com.example.java_dexloadable.StringsClass.stringGetter(int):0:0 -> showString
1:1:void showString(android.content.Context,int):0 -> showString
2:2:void showString(android.content.Context,int):0:0 ->showString

If a function or class is not in this mapping, then it has been shrunken out (meaning that it wasn’t being used in the code), or it has not been
obfuscated (due to being allow-listed).

The ProGuard Allow-List

By default, ProGuard will shrink, optimize, and obfuscate everything in the Java bytecode. This can be controlled by editing the ProGuard
file located at ‘app\proguard-rules.pro’, in the root of an application directory. This file can be renamed and moved and is specified in the
‘gradle.build’ file.

The following example rule allow-lists the ‘showString’ function in the class ‘com.example.java_dexloadable.MainActivity’. Here you need
to specify the access level of the class and function (‘public’, ‘private’, ‘package-private’, etc.) as well as the parameters for the function:

-keep class com.example.java_dexloadable.MainActivity {
    public showString(android.content.Context, int);
}

The following example is the same; however, in this example all functions in the ‘MainActivity’ are allow-listed:

-keep class com.example.java_dexloadable.MainActivity {
    public *;
}

Entry Points

ProGuard automatically allow-lists (also known as white-lists) entry points to an application (e.g., an activity with the MAIN or LAUNCHER category). It’s important to bear in mind that entry points used as part of reflection will not be automatically allow-listed, and so if using refection, these entry points will have to be manually allow-listed. This, however, will minimize the effectiveness of obfuscation as the frequency of plain text components will be higher. The entry points that are automatically added to an allowlist by ProGuard typically include classes with main methods, applets, MIDlets, activities, etc. This also includes classes that have calls to native C code.

The Different Types of Keep

In the preceding two examples, the ‘keep’ keyword is used. There are, however, several different types of keep keyword.

  • No Rule – Shrinks Classes, Shrinks Members, Obfuscates Classes, Obfuscates Members
  • -keep – None
  • -keepclassmembers – Shrinks Classes, Obfuscates Classes
  • -keepnames – Shrinks Classes, Shrinks Members

Example Rules

In the following example, the Java Package Name is ‘java_dexloadable’, and all rules have been added to the ‘Proguard-rules.pro’ file.

Keeps (allow-lists) all methods in the ‘MainActivity’ class:

-keep class com.example.java_dexloadable.MainActivity {
    public *;
}

Keeps (allow-lists) the ‘showString’ function in the ‘MainActivity’ class as well as the ‘MainActivity’ class itself:

-keep class com.example.java_dexloadable.MainActivity {
    public showString(android.content.Context, int);
}

Keeps (allow-lists) everything under the top-level package (shouldn’t be used):

-keep class com.example.java_dexloadable.** { *; }

Keeps (allow-lists) the function stringGetter but not the class StringsClass itself:

-keepclassmembers class com.example.java_dexloadable.
StringsClass {
    public stringGetter(int);
}

Repackage whole package into a single root:

-repackageclasses

Doesn’t perform the ProGuard shrink step:

--dontshrink

 

Read More

This article was derived from a chapter in my book Android Software Internals Quick Reference. If you found it interesting please consider picking the book up for yourself! 

 

 

 

Adverts