Skip to content
Shushant TiwariMobile engineer & maker
← All stories

Automating Native String Obfuscation with a Kotlin Gradle Plugin

A Kotlin Gradle task for native header generation, with a clear look at what obfuscation can—and cannot—protect.

Shushant TiwariMobile engineer. Building tools that help us see.
Contents

A custom Gradle task can automate native header generation, but putting a value in C++ does not make it a confidential secret. This article describes the implementation visible in the public source and its limits.

The problem: repeatable build tooling

Android projects that use JNI may need different constants for different environments. Hand-editing C++ headers is repetitive and can produce configuration mistakes. I built a Gradle plugin to generate those headers as part of the build lifecycle.

The useful engineering problem is keeping configuration, generation, and native access consistent. Any value and recovery logic shipped inside an app can be recovered by reverse engineering or runtime inspection. Native string obfuscation can add effort; it cannot guarantee secrecy.

What the current implementation does

EncryptionPlugin registers an encryptionConfig extension and an encryptStrings task. After project evaluation, an existing preBuild task is made dependent on that generation task.

RsaKeyLoader reads a private key from a JKS keystore and returns a SHA-256 fingerprint of its encoded bytes. Despite the class name, this step does not perform RSA encryption.

EncryptedHeaderBuilder applies a repeating XOR transformation to each string, Base64-encodes the result, and emits C++ #define constants. It also generates native Base64 decoding and XOR recovery code. Random function and variable names are generated for the key-access function.

This is reversible obfuscation, not implemented RSA/AES protection. Base64 is an encoding. The recovery material is included in the generated native code, so it must not be treated as an app-held confidential credential.

Configuration boundaries

The current public source uses ordinary Kotlin properties under encryptionConfig. The earlier encryption { secrets.set(…) } example did not match that source. The secrets map is currently declared as an immutable empty map, so assigning it through the DSL is not supported by this implementation.

The source provides a secretsUrl path for reading a JSON string-to-string map during the build. A successful remote result replaces the fallback map; it is not merged with it. Network failures fall back to the local map, and an empty result causes the generation task to fail.

The following is an illustrative configuration for the current source, after the plugin has been made available through your build. It does not establish a verified public plugin version or a tested installation:

plugins {
    id("dev.shushant.ndk-encryption")
}
encryptionConfig {
    outputFile = "src/main/cpp/Secrets.h"
    keystorePropertiesFile = "local.properties"
    secretsUrl = "https://example.com/build-config.json"
    generateNativeCpp = false
}

The keystore properties loader expects signing.storeFile, signing.storePassword, signing.keyAlias, and signing.keyPassword. Supply real values through your own protected build configuration. Do not commit credentials, keystores, or generated sensitive headers to a public repository.

A remote build-time endpoint does not solve runtime secrecy: its returned values are still embedded in the application. The current request code does not configure an authenticated secret-manager integration, so do not interpret secretsUrl as one.

Generated native access

The generated header contains Base64 strings, a randomly named key-access function, a Base64 decoder named bd, and a recovery function named dt. A generated constant can be passed to dt to recover its value. Function-name randomization changes identifiers; it does not provide a cryptographic security boundary.

What this adds — and what it leaves open

The plugin demonstrates Gradle extension design, task registration, build lifecycle integration, and Kotlin-to-C++ code generation. It can reduce manual header-editing work and centralize build configuration.

Confidential server credentials should remain on the server. Application security still requires an appropriate authentication design, limited permissions, and controls suited to the threat model. Obfuscation is only one possible layer.

The task explicitly reports that it is not compatible with Gradle configuration caching. Release-specific plugin resolution, native compilation, character encoding behavior, and integration tests need validation before adoption. This source review does not establish a production-ready or plug-and-play release.

Future work

Potential next steps include a configurable local input map, declared task inputs and outputs, configuration-cache support, native round-trip tests, and a clearly documented distribution path. Any future cryptographic design should state its key-management and runtime recovery boundaries explicitly.

Source and correction note

Source: https://github.com/dev-shushant/gradle-localization-plugin

Correction, 9 October 2026: this revision replaces the earlier RSA-encryption, hexadecimal-output, and plug-and-play claims with the behavior verified in the public source. Earlier version-specific installation examples have been removed rather than presented as validated. The original story URL is preserved.

Originally published on Medium on 22 Apr 2025. Republished here by the author.

Examples and opinions reflect the original publication date.

Keep the conversation going.

Have a thought to add?Join the conversation on Medium.