article
Protect Secrets in your app - Android NDK - Part 2
Protect Secrets using Android NDK
Configuration
To configure the project to support native code we require a Android.mk
and Application.mk
.
Create a file app/src/main/jni/Application.mk
and add this to it:
APP_ABI := all
The Application.mk specifies project-wide settings for ndk-build. By default, it is located at jni/Application.mk, in your application's project directory.
Application.mk
Then create a file app/src/main/jni/Android.mk
and add:
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
# specify the name of the local module
LOCAL_MODULE := protected
# specify the src files to include in the native lib
LOCAL_SRC_FILES := protected.c
include $(BUILD_SHARED_LIBRARY)
The Android.mk file resides in a subdirectory of your project's jni/ directory, and describes your sources and shared libraries to the build system. It is really a tiny GNU makefile fragment that the build system parses once or more. The Android.mk file is useful for defining project-wide settings that Application.mk, the build system, and your environment variables leave undefined. It can also override project-wide settings for specific modules.
Android.mk
Let's also directly create an empty *.c
file with the jni
headers, which will specify our native code.
Create it as app/src/main/jni/protected.c
and include the header
#include "jni.h"
For the last configuration part, define the path to the Android.mk
in the externalNativeBuild
block via the ndkBuild
. This will connect the native build with the android build, and ensures the native library will be built and included in your app.
android {
externalNativeBuild {
ndkBuild {
path 'src/main/jni/Android.mk'
}
}
}
Sync the project and build. It should compile and install without issues.
Implement JNI Access
Head over to the CustomApplication.kt
and include the native library
init {
System.loadLibrary("protected")
}
After that specify the external specification for the native functions we want to call.
private external fun getSdkKey(): String
private external fun getSdkSecret(): String
Now we can use those from our init
call.
FunSdk.init(getSdkKey(), getSdkSecret())
The last bit is now to specify our native JNI functions, this was a tedious step in the past, but thanks to Android Studio most of it happens automatically.
Open the protected.c
class. It only should contain the jni
header include.
Start typing JN
and Android Studio should propose the auto completion.

Press enter
and do the same for the secret.
JNIEXPORT jstring JNICALL
Java_com_mikepenz_credsshowcase_CustomApplication_getSdkKey(JNIEnv *env, jobject thiz) {
// TODO: implement getSdkKey()
}
JNIEXPORT jstring JNICALL
Java_com_mikepenz_credsshowcase_CustomApplication_getSdkSecret(JNIEnv *env, jobject thiz) {
// TODO: implement getSdkSecret()
}
Android Studio will define the correct name, parameters and return value according to the specification in the CustomApplication
.
Now complete the implementation of the getSdkKey
and getSdkSecret
functions.
JNIEXPORT jstring JNICALL
Java_com_mikepenz_credsshowcase_CustomApplication_getSdkKey(JNIEnv *env, jobject thiz) {
return (*env)->NewStringUTF(env, "amazing-key");
}
JNIEXPORT jstring JNICALL
Java_com_mikepenz_credsshowcase_CustomApplication_getSdkSecret(JNIEnv *env, jobject thiz) {
return (*env)->NewStringUTF(env, "super-secure-secret");
}
Now build and install the app. If you log the values for key
and secret
, they will contain the correct values.
Check SDK Credential security strength
Building the app in release mode again and looking into the bytecode will reveal that the source is no longer containing the key
or secret
.

What it exposes though is the external specification of the native library.
This theoretically allows a potential attacker to rebuild a sample app including the native lib by copying over the *.so
file, and defining a class in the same package and class name. And then just call those 2 functions, retrieving the values.
Compared to the original steps to reverse engineer this is significantly more time consuming and complicated though, and can be hardly automated.
As we built a native library the apk now includes a file libprotected.so
. So let's have a look if we can extract some details from that one.
readelf
First lets try using a tool called readelf.
readelf -a libprotected.so
It gives some pointers but won't expose the string constants yet
Symbol table '.dynsym' contains 15 entries:
Num: Value Size Type Bind Vis Ndx Name
7: 000000000000067c 20 FUNC GLOBAL DEFAULT 10 Java_com_mikepen[...]
So let's try a different approach.
strings
strings libprotected.so
And success, beside all other string constants it includes our key and secret:
...
Java_com_mikepenz_credsshowcase_CustomApplication_getSdkKey
Java_com_mikepenz_credsshowcase_CustomApplication_getSdkSecret
libprotected.so
amazing-key
super-secure-secret
Even though requiring additional effort to extract the key
and secret
from the *.so
file, it's pretty much doable to retrieve the string and secret.
Our sample makes it super obvious to identify key
and secret
, but also for real keys and secrets, they usually follow a specific pattern. E.g. specific length or characters, making it possible to guess. And with minimal time the limited possibilities are quickly tried through.