Stuart Kent
(Android Developer)

proguardFiles: A Cautionary Tale

This week, an assumption I made about the Android Gradle plugin method proguardFiles nearly resulted in a minor security slip. Let’s all learn from my mistake.

Background

The client codebase I’m currently working on has three build types: debug, beta, and release. The beta build type is a minor variation of the debug build type, so it’s configured using the Android Gradle plugin’s initWith method as shown below:

buildTypes {

  debug {
    // ...
  }

  beta {
    initWith(buildTypes.debug)
    // ...
  }

  release {
    // ...
  }

}

My tasks today included enabling ProGuard for every single build type. Here’s the code I initially wrote to accomplish this:

buildTypes {

  debug {
    // ...

    proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro', 'proguard-debug.pro'
  }

  beta {
    initWith(buildTypes.debug)
    // ...

    proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
  }

  release {
    // ...

    proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
  }

}

The common ProGuard rules defined in the proguard-android.txt and proguard-rules.pro files are applied to all three build types. The extra ProGuard rules defined in the proguard-debug.pro file are applied to the debug build type only.

Right?

Wrong!

The proguard-debug.pro file contains exactly one line:

-dontobfuscate

Obfuscation is a useful (but certainly not impenetrable) defense against reverse engineering of a compiled application. However, I have found in the past that it interferes with Android Studio’s debugger, so I like to disable it for the non-production build variants I actively develop with.

As described above, my intention was to disable obfuscation for the debug build type only, leaving obfuscation enabled for the beta and release build types. To test that this was working as expected, I assembled a beta build and inspected the APK contents using ClassyShark. Here’s what our Parcelable utility class looked like in ClassyShark:

Those method names are definitely not obfuscated.

Huh?

Confused, I jumped to the definition of the proguardFiles method commonly used to apply ProGuard configuration to a build type (remember that in Android Studio you can do this using the “Go To Declaration” shortcut):

public BuildType proguardFiles(Object... files) {
  Object[] var2 = files;
  int var3 = files.length;

  for(int var4 = 0; var4 < var3; ++var4) {
    Object file = var2[var4];
    this.proguardFile(file);
  }

  return this;
}

Inside the for loop, each configuration file is passed to the proguardFile method:

public BuildType proguardFile(Object proguardFile) {
  this.getProguardFiles().add(this.project.file(proguardFile));
  return this;
}

Ah-ha!

The proguardFiles method adds to an internal list of configuration files rather than specifying a new list of configuration files. In my opinion this is not obvious from the method name. At least the documented behavior is clear, though confusingly the name proguardFiles may also be used to refer to a getter defined on the same type!

Armed with this knowledge, we can now walk through exactly what happens (with respect to ProGuard rules) when the beta build type is configured:

  1. initWith(buildTypes.debug) is called, which results in three ProGuard configuration files (proguard-android.txt, proguard-rules.pro, and proguard-debug.pro) being added to the beta build type’s internal list;

  2. proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' is called, which results in duplicates of the proguard-android.txt and proguard-rules.pro being added to the beta build type’s internal list.

The result: proguard-debug.pro is unintentionally included in the beta build type’s internal list of configuration files, and the -dontobfuscate ProGuard rule is therefore applied when packaging our application. This is consistent with what we found in the decompiled APK.

setProguardFiles to the rescue

The most obvious solution to this problem might be to avoid initializing the beta build type using the debug build type. However, this would have lead to a lot more duplication in our build.gradle file. Luckily there’s a better way.

The BuildType class exposes a setProguardFiles method, defined as follows:

public BuildType setProguardFiles(Iterable<?> proguardFileIterable) {
  this.getProguardFiles().clear();
  this.proguardFiles(Iterables.toArray(proguardFileIterable, Object.class));
  return this;
}

This is exactly how I originally assumed the proguardFiles method worked! Any existing configuration files are explicitly replaced by those contained in the argument passed to setProguardFiles. So, here’s a fixed version of our build.gradle file:

buildTypes {

  debug {
    // ...

    proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro', 'proguard-debug.pro'
  }

  beta {
    initWith(buildTypes.debug)
    // ...

    // New!
    setProguardFiles([getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'])
  }

  release {
    // ...

    proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
  }

}

To check that obfuscation was now properly enabled for beta builds, I assembled a new APK and navigated to our Parcelable utility class using ClassyShark:

Much better!

Takeaways