Learn More About the Alternative App Future

Obfuscated Code as an API

During the development of modules, libraries, or SDKs for public audiences, companies often face conflicting demands. On one hand, a company may provide a public API accessible to everyone, while on the other hand, it may need to offer a "hidden" API for specific partners or internal departments. The primary objective of the "hidden" API is to ensure that it remains concealed from public view, difficult to access by unauthorized or uncertified developers, while still being available and accessible to trusted partners.

At DT, we sought to find an optimal solution that could meet these requirements. The goal was to balance security and accessibility, save development time, and minimize the impact on support, integration, and release processes. Furthermore, it was important that the solution would not negatively affect future development timelines.

The solution we have implemented is the use of Obfuscated Code as an API. This approach involves embedding a “hidden” API within our SDK, which will be accessible exclusively to our partners. But before we talk about that, first let's explore the alternatives that we also considered and dismissed: separate flavor and service as API.

Possible Solutions Overview

Separate Flavor

One possible approach is to create a separate flavor that keeps the necessary API exposed while leaving other parts obfuscated. We could then provide the artifact to our partners. However, the major challenge with this approach is the increased support and development time.

In this scenario, we may need to handle multiple versions of our SDK, which can quickly become a nightmare in terms of the release process and ongoing support. Of course, we would rely on the CI mechanism for deployment, but this still presents significant challenges.

PROS:
  • Fast development. The development process can be relatively quick.
  • Same codebase. Only the flavor needs to be separated, so the underlying project and codebase remain the same.
CONS:
  • Code duplication. Developers may support and sometimes duplicate the code for the additional flavor
  • Development effort. This is the biggest issue, as development effort will increase with this solution.
  • Supporting multiple variants. QA will need to spend more time testing the different variants, and managing the CI processes will also require additional time.
  • Additional maven repository.  Another major issue is the need for a second public Maven repository. This breaks our key requirement of keeping the API hidden, as it would no longer be accessible only to authorized users. If a partner develops their own SDK or library and uses our hidden API, they would need to add our SDK as a dependency and provide credentials to access our private repository. This could expose private credentials to end-users who use the partner’s SDK or library.

Service as an API

Another, more generic, but also more complex solution would involve developing a service that communicates with the partner's authorized code and provides secure access to the hidden APIs. While this approach offers flexibility, it will require significantly more development, support, and QA effort.

PROS:
  • Single codebase. The underlying codebase remains unified.
  • Single maven repository. We only need to manage one Maven repository.
CONS:
  • Increased support, development, and QA effort. This solution will require much more time to develop and support, and this time commitment is unlikely to decrease in the future.
  • Continuous modification. Ongoing changes, such as adding new APIs, functionalities, or updates, will require constant development and maintenance.
  • Complex authentication. The authentication process is likely to be excessive for our needs.
  • Network connectivity. Managing the network connectivity required for the authorization process introduces additional complexity.

Our Solution: Obfuscated Code as an API

The solution relies on a Gradle plugin that is published in a private Maven repository. This ensures that the plugin is available only to authorized partners, adding an extra layer of security to access the hidden API.

The plugin wraps the selected obfuscated code and generates a standard API within the partner's project. This allows the partner to use the API seamlessly, just as they would with any standard API.

PROS:
  • Simplicity. The solution is straightforward and easy to implement.
  • No code duplication. There is no need to duplicate code, which simplifies development.
  • Flexibility with code changes. This solution allows for easy modifications to the code without major issues.
  • No changes in development/support/QA effort. The solution does not require additional resources.
  • Relatively secure solution. The use of a private repository and obfuscation ensures a secure environment.
CONS:
  • Additional Maven repository. While this introduces an extra Maven repository, it is also a benefit because the repository can be private, ensuring that access is secured and restricted to authorized developers and partners.
  • Plugin development time. Developing the plugin initially will require time; however, this is a one-time effort. In the future, the time required for updates will be minimal, as the plugin will only need occasional maintenance.

As a result, many of the cons can be viewed as advantages in the long run, providing a solution that aligns with our expectations and helps us achieve our goals. The solution is generic and ensures that development, QA, and support time remain largely unchanged, while still meeting our requirements.

How It Works

During the initialization step, the plugin receives the following input parameters:

  • Output Path: The location where the generated code will be saved.
  • Package Name: The package under which the generated code will be placed.
  • Mapping Path: The path to the ProGuard mapping file.
  • List of Target Classes: A list of original class names that the plugin should wrap, based on the mapping.

The plugin reads the mapping file and begins parsing it to map the original target classes to their obfuscated counterparts. This process includes analyzing the methods and members of the classes.

The final step involves creating class wrappers and saving them to the specified output path.

Implementation Details

Now let’s go through the main steps of plugin creation.

Step 1: Initialize the Gradle Plugin Project

Init the Gradle plugin project with following  commands and let Gradle to deal with it:

mkdir obfuscated-api-plugin
cd obfuscated-api-plugin
obfuscated-api-plugin gradle init \
--use-defaults \
--type kotlin-gradle-plugin \
--dsl kotlin \
--project-name obfuscated-api-plugin \
--package com.example\
--no-split-project \    
--no-incubating \
--java-version 21

Create the plugin with extension:

open class ObfusctaedApiPluginExtension {
    @Input
    var packagename: String = "com.some.packagename"// The wrapped code should be placed under this packagename
    @Input
    var outputPath: String = "path/where/to/place/generated/code"

    @Input
    var mappingFilePath: String = "path/to/mapping.txt"

}

class ObfuscatedApiPlugin: Plugin<Project> {
    override fun apply(project: Project) {
        val extension = project.extensions.create("ObfuscatedApi", ObfusctaedApiPluginExtension::class.java)
        project.tasks.register("mapping", ObfuscatedApiTask::class.java) { task ->
            task.libname = "the_target_lib"
            task.outputPackage =  extension.packagename
            task.outputPath = extension.outputPath
            task.mappingFilePath = extension.mappingFilePath
            task.targetClasses = mutableListOf("com.target.ClassA",
                                               "com.target.ClassBListener")

        }
    }
}

Create the task itself:

abstract class ODTApiMappingTask : DefaultTask() {

    @Input
    lateinit var libname: String
    @Input
    lateinit var outputPath: String
    @Input
    lateinit var outputPackage: String
    @Input
    lateinit var mappingFilePath: String
    @Input
    lateinit var targetClasses: MutableList<String>

    @TaskAction
    fun map() {
        val mappingFile = File(mappingFilePath)
        val result : Map<String, Container> = Processor.process(mappingFile, searchKeys)
        result.values.forEach { cls ->
            val builder = WrapperBuilderFactory.createBuilder(outputPath, outputPackage, cls)
            builder.build()
        }
    }
}

It is a simple task which passes the mapping file and list of classes, along with the API to be wrapped, to the processor (parser). 

Step 2: Implement the Mapping File Processor

While the implementation of the processor it is out of the post’s scope, we will highlight some important points.

The processor should read and parse the mapping.txt and return a result. In our case, this is a map with target class as key and container (content) which represents the target class:

  • Original class name
  • Obfuscated class name
  • Method:
    • Return type
    • Method arguments’ types
    • Original method name
    • Obfuscated method name

Example:

com.example.ClassA -> {Original class name: com.example.ClassA : Obfuscated class name: com.example.b}
methods: {
    <init>=(name: <init>, 
            returnType: void, 
            argTypes: [android.content.Context, com.example.a,
   boolean, com.example.listener.a]), 
    someMethod=(name: a, returnType: void, argTypes: [java.lang.String,
                                                       java.lang.String])
}

It is important to ensure that the argument types in the method are replaced with their obfuscated versions. However, note that SomeListener is not obfuscated in the method’s arguments:

4:6:void <init>(android.content.Context,com.example.a,boolean,com.example.listeners.SomeListener):33:35 -> <init>

But it was obfuscated below in mapping.txt:

com.example.listeners.SomeListener -> com.example.listeners.a:
Step 3: Implement the Wrapper Builder

The wrapper builder is  the mechanism which creates the wrappers for obfuscated classes under passed path and package name provided to plugin via plugin extension. Different wrappers are used for classes and listeners (interfaces).

The class wrapper generates the class which will contain the instance of the wrapped class as instance.

Example:

package com.some.packagename;

public class ClassA {

	private com.example.b mTarget; //original ClassA after obfuscation
	public Class(android.content.Context arg1, boolean arg2, com.example.listeners.a arg3) {
		mTarget = new com.example.b(arg1, arg2, arg3);
	}

	public void someMethod(java.lang.String arg1, java.lang.String arg2) {
		mTarget.a(arg1, arg2);
	}

}

It is better to provide to the wrapper the class and method names that are the same as original names, as this will enhance usability  for developers working with this API.

The listener wrapper generates abstract classes and adds abstract methods which should be called by implemented methods.

Example:

package com.some.packagename;

public abstract class MyListener implements com.example.listeners.a {

	public abstract void doOnSomeAction(android.content.ComponentName arg0, android.os.IBinder arg1);

	@Override
	public void b(android.content.ComponentName arg0, android.os.IBinder arg1) {
		doOnSomeAction(arg0, arg1);
	}
}

In these examples, we can observe how an obfuscated class or listener is used as an argument in a method of another obfuscated class. This is why, during the implementation of the parser, special attention must be given to handling this case, as the mapping file contains the argument in its non-obfuscated form.

The abstract class wrapper uses the combination of both solutions for  class and interface wrappers.

Step 4: Prepare the Plugin for Publication

Next, the plugin must be prepared to be published.  In the example below the plugin is being published to local Maven.

Modify plugin’s build.gradle.kts and define the plugin details:

version = "0.0.1"
group = "com.exmaple"
gradlePlugin {
    val obfusacatedApi by plugins.creating {
        id = "com.example"
        implementationClass = "com.example.ObfusctaedApiPlugin"
        version = "0.0.1"
        description = "Plugin's Description"
        displayName = "Plugin's Display Name"
    }
}

Run the next command to publish to local Maven:

./gradlew publishToMavenLocal
Step 5: Apply the Plugin in the Project

After the plugin was published, apply it in the project which should work with the obfuscated code. In the settings.gradle, add mavenLocal() to the repositories inside pluginManagement:

pluginManagement {
    repositories {
        mavenLocal()
        //...
        mavenCentral()
        gradlePluginPortal()
    }
}

Define plugin dependency by adding the next lines in the libs.versions.toml:

[versions]
...
oapi = "0.0.1"


[libraries]
...

[plugins]
...
oapi-android = { id = "com.example", version.ref = "oapi"}

Apply the plugin in build.gradle.kts:

plugins {
    alias(libs.plugins.android.application) apply false
    alias(libs.plugins.kotlin.android) apply false
    alias(libs.plugins.oapi.android) apply false
    id ("org.jetbrains.gradle.plugin.idea-ext") version "1.1.8"
}

And finally configure the plugin inside the build.gradle.kts:

ObfuscatedApi {
    outputPath = Paths.get(projectDir.path,     
                           "src/main/kotlin").absolutePathString()
    packagename = "com.some.packagename"
    mappingFilePath = "/path/to/mapping/file"
}

Tip: To stay up-to-date and automatically react to each update of the plugin or the wrapped library,  the plugin can be executed after the Gradle sync task completes. To do so, in the Gradle tab (in Android Studio or IntelliJ Idea, right-click on plugin’s task and select “Execute After Sync”:

Summary

In conclusion, we successfully met all our requirements by implementing an obfuscated code as an API solution. The Obfuscated Code as an API has proven to be the optimal balance between maintenance and development efforts. Moreover, it allows us to make internal changes within the obfuscated code while generating the necessary code through the plugin, all without requiring any modifications to the API layer for our partners.

Andrey Michailov
Read more by this author
You Might Also Like
Data Pipeline Migration from AWS to GCP
A Novel Approach to Thresholding and Classifying Large-Scale Data
Apache Druid’s Lookups as Code

Newsletter Sign-Up

Get our mobile expertise straight to your inbox.

Explore More

Playful Precision: Perfecting Your Media Mix With Powerful In-App Video Ads for Next-Gen
The A to Zs of Mobile App Monetization
Harnessing Mobile Gaming Apps For Attention & Engagement