If you ever cared about data security, you probably already know it's often a matter of security-convenience balance. When trying to use sophisticated security solutions, we will be missing out on the ease of use or development of the software. In the end, it's always an economic trade-off that we have to make: how crucial are the information, how much time can we devote to its protection, and how motivated attackers will be trying to gain access to the information you'd like to protect from the public. It's not a secret that any native android app could be decompiled, which may lead to some key information leaks. Intrinsically, nothing on the client-side is unbreakable, but we can certainly raise the bar.
'The lost art of keeping a secret'
CheckPoint Research published report about mobile apps security which shows that misconfiguration of third party services exposed data of over 100 milion users. The main issue listed in the report is "Exposed Realtime DB" which basically means that the services like Firebase Firestore are not configured properly to stop unauthorized access. However, there is one more vulnerability revealed which I would like to focus on in this post - embedded secret keys for third-party services, including realtime databases, push notification services and cloud storage. The main problems described in the report are insufficient code obfuscation or the use of easily reversible encryption functions like Base64.
Example of leaked secret keys from decompiled .apk
'Do not use a cannon to kill a mosquito'
The secret of keeping secret keys? Not store them on the end-user device at all. There are techniques well described on hackernoon by Skip Hovsmith: Mobile API Security Techniques - it will be very useful in case you're looking for top-level security for communication between your app and third-party services.
Anyway, following the Confucius famous quote, some cases require a more simple, but still effective way of securing secrets in the app. I'll consider two cases:
- Open-source repository. The app is not intended to be distributed as .apk file, but the code should be public.
- A native app distributed as usual, with no dedicated backend, but using third-party services.
Case 1: Hide secret keys in open repository
One of the ways to avoid secret keys to check-in to the public repository is to use Secrets Gradle Plugin for Android from Google developers. It allows using local.properties
file, by default excluded from source control, as a store for API keys, client IDs, secrets, etc.
This solution will not secure your secrets in case of decompiling the .apk file. That's why it should be used only in case of sharing source code, without compiled .apk included. If you plan to publish your .apk, take a look at the 2nd case below.
The first thing to do is to apply the plugin in module-level gradle.build
:
plugins {
id("com.google.android.secrets-gradle-plugin") version "1.1.0"
}
In root-level local.properties
file we're able to add some API key:
googleMapsKey=hC1EgowMbPBA6uObpIwi
After building the project, this API key will be accessible as a BuildConfig property. In this example, using kotlin syntax, we can access this property:
val mapsApiKey = BuildConfig.googleMapsKey
and also in .xml, for example, AndroidManifest.xml:
<meta-data
android:name="com.google.android.geo.API_KEY"
android:value="${googleMapsKey}" />
Last, but not least thing to double-check is .gitignore
configuration. It needs to contain the following entry:
local.properties
Case 2: Obfuscate secret keys using NDK and CMake
As Secrets Gradle Plugin for Android isn't secure in the case when the developer publishes .apk (even if it's through the Google Play store), there's a need to find another solution. It takes more time and effort to decompile native C/C++ code and usually makes it unprofitable for potential attackers. Even if it's not 100% secure it's still a much better solution when publishing compiled apps.
To use NDK in Android Studio it's necessary to install the following tools with SDK Manager: NDK (Side by side)
and CMake
.
When the tools are ready, we can create C++ file under app/src/main/cpp/
directory. Let's call it secret-keys.cpp
:
#include <jni.h>
#include <string>
extern "C" JNIEXPORT jstring JNICALL
Java_dev_michalkasza_smartlock_utils_SecretKeys_mapsKey(JNIEnv* env, jobject object) {
std::string maps_key = "hC1EgowMbPBA6uObpIwi";
return env->NewStringUTF(maps_key.c_str());
}
The important part here is the name of the function Java_dev_michalkasza_smartlock_SecretKeys_mapsKey. It uses a strict pattern:
dev_michalkasza_smartlock_utils
- package name that contains an object which will interact with C++ codeSecretKeys
- the name of the object (in this case) which will refer to the secret keymapsKey
- the name of the method that will be used to access maps_key string
At this point, very important thing is to include secret-keys.cpp
to .gitignore file.
The next file to create in the app
directory is CMakeLists.txt
which contains a set of directives and instructions describing the project's source files and targets, including our secret-keys.cpp
. I used the template from the Google android developer guide:
# Sets the minimum version of CMake required to build your native library.
# This ensures that a certain set of CMake features is available to
# your build.
cmake_minimum_required(VERSION 3.4.1)
# Specifies a library name, specifies whether the library is STATIC or
# SHARED, and provides relative paths to the source code. You can
# define multiple libraries by adding multiple add_library() commands,
# and CMake builds them for you. When you build your app, Gradle
# automatically packages shared libraries with your APK.
add_library( # Specifies the name of the library.
secret-keys
# Sets the library as a shared library.
SHARED
# Provides a relative path to your source file(s).
src/main/cpp/secret-keys.cpp )
# Searches for a specified prebuilt library and stores the path as a
# variable. Because CMake includes system libraries in the search path by
# default, you only need to specify the name of the public NDK library
# you want to add. CMake verifies that the library exists before
# completing its build.
find_library( # Defines the name of the path variable that stores the
# location of the NDK library.
log-lib
# Specifies the name of the NDK library that
# CMake needs to locate.
log )
# Links your native library against one or more other native libraries.
target_link_libraries( # Specifies the target library.
secret-keys
# Links the log library to the target library.
${log-lib} )
Next step is to add CMakeLists file to the build configuration in app-level build.gradle
under android
block:
externalNativeBuild {
cmake {
path "CMakeLists.txt"
}
}
The last step to access the secret keys is to create a Kotlin object following the package name and class name configuration in secret-keys.cpp
. In this case, it will be SecretKeys
object under dev.michalkasza.smartlock.utils
package. System.loadLibrary("secret-keys")
in init block will load our .cpp, as its path is already defined in CMakeLists.txt. As soon as it's loaded, we can define an external function that will allow accessing maps_key
from any place in the app.
object SecretKeys {
init {
System.loadLibrary("secret-keys")
}
external fun mapsKey(): String
}
From this moment maps secret key is accessible through the SecretKeys object:
SecretKeys.mapsKey()
Conclusion
As shown in these two examples, obstructing access to keys is not a difficult thing and should be standard in mobile applications development. By securing sensitive data, we not only protect ourselves against the uncontrolled use of services registered in our name, but we also increase the security of users of the app.