Business
blog image

Hiding Sensitive Data in Android App

How often do we worry about our sensitive data shipped with the apk without security?

In this blog post, I am going to talk about hiding sensitive data in your Android app. I will explore this topic by answering the following questions-What, Why, and How?

The What?

What do we need to hide in our code? There are many examples of sensitive string data that need hiding-

  1. API keys from third-party libraries. Example- Google Maps API key
  2. URLs to make server API calls. Example- Base URLs to our private server
  3. Default login credentials (if your app allows a demo login to showcase various features before users can subscribe to them). Example- Username and password

The Why?

Why do we need to hide the data when Android already uses Proguard to protect the classes?

There are many ways in which you can store sensitive string constants in your Android application to use in web service calls to your server like:

  1. Store as string constants in some .kt/.java file
  2. Store as string resource in strings.xml
  3. Store inside Build.Config from the Android Gradle plugin
  4. Directly use the strings as literals in the code

However, if your application is reverse-engineered (decompiled), the string constants can be easily intercepted and misused.

Strings stored using the above mechanisms can be intercepted in a decompiled application like so-

For brevity, I would be taking a base URL for interacting with a server as an example throughout the post. Let’s consider following the literal as an example-https://domainname/functiontype/.

1. The URL intercepted from a decompiled constants class-

1/* compiled from: AppConstants.kt */
2public final class AppConstants {
3    @NotNull
4    private static final String BASE_URL = "https://domainname/functiontype/";
5    public static final Companion Companion = new Companion();
6
7    @Metadata(bv = {1, 0, 3}, d1 = {"\u0000\u0014\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0002\b\u0002\n\u0002\u0010\u000e\n\u0002\b\u0003\b†\u0003\u0018\u00002\u00020\u0001B\u0007\b\u0002¢\u0006\u0002\u0010\u0002R\u0014\u0010\u0003\u001a\u00020\u0004X†D¢\u0006\b\n\u0000\u001a\u0004\b\u0005\u0010\u0006¨\u0006\u0007"}, d2 = {"Lcom/novumlogic/ndktest/AppConstants$Companion;", "", "()V", "BASE_URL", "", "getBASE_URL", "()Ljava/lang/String;", "app_debug"}, k = 1, mv = {1, 1, 13})
8    /* compiled from: AppConstants.kt */
9    public static final class Companion {
10        private Companion() {
11        }
12
13        @NotNull
14        public final String getBASE_URL() {
15            return AppConstants.BASE_URL;
16        }
17    }
18}

2. The URL intercepted from decompiled strings.xml:

1.....
2<string name="abc_shareactionprovider_share_with_application">Share with %s</string>
3<string name="abc_toolbar_collapse_description">Collapse</string>
4<string name="app_name">Ndktest</string>
5<string name="base_url">https://domainname/functiontype/</string>
6<string name="search_menu_title">Search</string>
7......

3. The URL intercepted from a decompiled BuildConfig class-

1public final class BuildConfig {
2    public static final String APPLICATION_ID = "pacakge.name";
3    public static final String BUILD_TYPE = "debug";
4    public static final boolean DEBUG = Boolean.parseBoolean("true");
5    public static final String FLAVOR = "";
6    public static final int VERSION_CODE = 1;
7    public static final String VERSION_NAME = "1.0";
8    public static final String baseURL = "https://domainname/functiontype/";
9}

4. The URL intercepted from decompiled from the code using it as a string literal-

1@NotNull
2public final Retrofit getInstance() {
3    Retrofit access$getINSTANCE$cp = ApiHelper.INSTANCE;
4    if (access$getINSTANCE$cp != null) {
5        return access$getINSTANCE$cp;
6    }
7    Retrofit access$getINSTANCE$cp2;
8    synchronized (this) {
9        access$getINSTANCE$cp2 = ApiHelper.INSTANCE;
10        if (access$getINSTANCE$cp2 == null) {
11            access$getINSTANCE$cp2 = new Builder().baseUrl("https://domainname/functiontype/").addConverterFactory(GsonConverterFactory.create()).build();
12            ApiHelper.INSTANCE = access$getINSTANCE$cp2;
13        }
14    }
15    Intrinsics.checkExpressionValueIsNotNull(access$getINSTANCE$cp2, "synchronized(this) {\n   …ANCE = it }\n            }");
16    return access$getINSTANCE$cp2;
17}

Thus, the common ways to store strings in Android applications fail to provide any secure way to store untraceable string data.

The How?

So, how do we secure the strings? One way to secure the string constants without exposing them to hackers in the decompiled version is by using Android NDK.

Using C/C++ JNI native code to hide sensitive string data with NDK and CMake

Note: This method does not guarantee full proof security against reverse engineering (Although the .so files are very hard to decompile, it is not impossible to do so) but it adds a layer of security.

The idea behind this way of securing sensitive data is to store the sensitive strings in C/C++ classes and fetch these strings by function calling from Java/Kotlin classes.

The code for C/C++ classes is stored inside generated .so files which are harder to decrypt than the Java/Kotlin classes from the decompiled version.

Getting started

1. Install CMake from Android SDK

Go to Tools -> SDK Manager -> SDK Tools -> Check the CMake checkbox to install the latest CMake.

Install Latest CMake
Install Latest CMake

2. Install Android Native Development Toolkit (NDK)

You can tick NDK from the settings dialog above along with CMake to install NDK. Otherwise, you can download NDK from the official site. I already have NDK downloaded on my computer. I am therefore using the path to its folder in the local.properties file.

local.properties file to store path to android NDK
local.properties file to store path to android NDK

3. Create a folder “cpp” under app/src/main. Create a C/C++ file to store and access your base URL from- native-lib.cpp under cpp f`older.

native-lib.cpp
native-lib.cpp

1#include <jni.h>
2#include <string>
3
4extern "C"
5jstring
6Java_com_package_name_ApiHelper_baseUrlFromJNI(
7        JNIEnv* env,
8        jobject /* this */) {
9    std::string baseURL = "https://domainname/functiontype/";
10    return env->NewStringUTF(baseURL.c_str());
11}

The above is a simple C++ function to return the base URL from a method named stringFromJNI().

Things to note

For making the function available in Java/Kotlin code, the format of the function name is Java_package_name_activity_name_function_name

  • activity_name is the name of the activity class inside which this native function is to be used.
  • package_name is the name of the application’s package name where the activity resides.
  • function_name is the name of the function that will be called from Kotlin/Java classes to fetch the URL.

4. Create a CMake file named CMakeLists.txt (the file name has to be the same) to allow your app to build the native C++ code into a library.

1# Sets the minimum version of CMake required to build the native
2# library. You should either keep the default value or only pass a
3# value of 3.4.0 or lower.
4
5cmake_minimum_required(VERSION 3.4.1)
6
7# Creates and names a library, sets it as either STATIC
8# or SHARED, and provides the relative paths to its source code.
9# You can define multiple libraries, and CMake builds it for you.
10# Gradle automatically packages shared libraries with your APK.
11
12add_library( # Sets the name of the library.
13             native-lib
14
15             # Sets the library as a shared library.
16             SHARED
17
18             # Provides a relative path to your source file(s).
19             # Associated headers in the same location as their source
20             # file are automatically included.
21             src/main/cpp/native-lib.cpp )

The add_library The CMake command is used to instruct CMake to build a native library with the name specified (“native-lib”) from the native code from the path to the .cpp file specified (src/main/cpp/native-lib.cpp).

Along with creating CMakeLists.txt, add the following to the app’s build.gradle file to specify the path to CMakeLists.txt for CMake to compile and build the native code using NDK.

1externalNativeBuild {
2    cmake {
3        path "CMakeLists.txt"
4    }
5}

5. The next step is to make the native code available for use in Java/Kotlin activity. To enable this you must include the following in your Activity.

Note: For this example, I am using retrofit to illustrate the use of network calling.

Kotlin code-

1class ApiHelper {
2    companion object {
3        init {
4            System.loadLibrary("native-lib")
5        }
6
7        @JvmStatic
8        external fun baseUrlFromJNI(): String
9
10        @Volatile
11        private var INSTANCE: Retrofit? = null
12
13        fun getInstance(): Retrofit {
14            return INSTANCE ?: synchronized(this) {
15                INSTANCE ?: Retrofit.Builder()
16                        .baseUrl(baseUrlFromJNI())
17                        .addConverterFactory(GsonConverterFactory.create())
18                        .build().also { INSTANCE = it }
19            }
20        }
21    }
22}

Java code-

1public class ApiHelperJava {
2    public static native String baseUrlFromJNI();
3    private static Retrofit INSTANCE = null;
4
5    static {
6        System.loadLibrary("native-lib");
7    }
8
9    public static Retrofit getInstance() {
10        if (INSTANCE == null) {
11            INSTANCE = new Retrofit.Builder().baseUrl(baseUrlFromJNI())
12                    .addConverterFactory(GsonConverterFactory.create())
13                    .build();
14        }
15        return INSTANCE;
16    }
17}

  • The loadLibrary() a function is called from the Activity’s init block of companion object (Kotlin)/static init block (Java) because in this example we are required to access the native code in the launcher activity’s onCreate() method.
  • Note that the access modifiers- native in Kotlin and external in Java are used respectively to be able to access functions from native.
  • Also, note that the external function in Kotlin is annotated with @JvmStatic to enable static access to it. (i.e. direct usage of ApiHelper.baseUrlFromJni())

6. Build and Run the project.

That’s it!

Try to decompile your built apk and check if you can find the URL defined anywhere in the decompiled version. All you can see is libnative-lib.so files in the lib directory which is very hard to decipher.

Heading 1

Heading 2

Heading 3

Heading 4

Heading 5
Heading 6

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.

Block quote

Ordered list

  1. Item 1
  2. Item 2
  3. Item 3

Unordered list

  • Item A
  • Item B
  • Item C

Text link

Bold text

Emphasis

Superscript

Subscript