You Can Double-Protect Your Keys by Storing Them in NDK and Encrypting Them
There are many ways to secure your key when building an APK. But for now, I want to explain one way to secure your key: store it in NDK and double-secure with an encrypted key.
I believe that this is not a 100% guarantee that your key is secure. But at least make it hard for an attacker to get your key or any sensitive data from your APK.
Read this documentation about NDK from Google.
Add C++ to Module
In Android Studio, you can easily add a C++ module to your app module with a right-click on your app module and a click on Add C++ to Module. And also, you can do it by double-clicking the shift button on your keyboard and typing Add C++ to Module
.
Once you’re done, you can create a CMakeLists.txt and put it under app/src/main/cpp
.
Finally, from these processes, it will do several things:
- Generate CMakeLists.txt. We can leave it as is for CMakeLists.txt.
# CMakeLists.txt
# For more information about using CMake with Android Studio, read the
# documentation: https://d.android.com/studio/projects/add-native-code.html
# Sets the minimum version of CMake required to build the native library.
cmake_minimum_required(VERSION 3.18.1)
# Declares and names the project.
project("encryptedndk")
# Creates and names a library, sets it as either STATIC
# or SHARED, and provides the relative paths to its source code.
# You can define multiple libraries, and CMake builds them for you.
# Gradle automatically packages shared libraries with your APK.
add_library( # Sets the name of the library.
encryptedndk
# Sets the library as a shared library.
SHARED
# Provides a relative path to your source file(s).
encryptedndk.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( # Sets the name of the path variable.
log-lib
# Specifies the name of the NDK library that
# you want CMake to locate.
log )
# Specifies libraries CMake should link to your target library. You
# can link multiple libraries, such as libraries you define in this
# build script, prebuilt third-party libraries, or system libraries.
target_link_libraries( # Specifies the target library.
encryptedndk
# Links the target library to the log library
# included in the NDK.
${log-lib} )
- Generate encryptedndk.cpp. In this file, we will add some code to put our API_TOKEN here.
// encryptedndk.cpp
#include <jni.h>
extern "C" JNIEXPORT jstring JNICALL
// Java_{package_name}_{class_name}_{method_name}
Java_com_adefruandta_encryptedndk_EncryptedNdk_apiTokenNative(JNIEnv *env, jobject object) {
// API_TOKEN is a constant that will be passed from gradle
return env->NewStringUTF(API_TOKEN);
}
- Modify your app/build.gradle
android {
...
defaultConfig {
...
externalNativeBuild {
cmake {
// This is the way we pass our API_TOKEN from gradle to cpp
cppFlags '-DAPI_TOKEN=\\\"This_is_API_TOKEN_from_native\\\"'
}
}
}
// This section is auto updated from Add C++ to module process
externalNativeBuild {
cmake {
path file('src/main/cpp/CMakeLists.txt')
version '3.18.1'
}
}
}
Just try to sync and build the project. Make sure there is no error.
Linking cpp to kotlin class
After we have a cpp file to store our API_TOKEN, we can create a class with the same package name, class name, and method name as in your cpp file.
package com.adefruandta.encryptedndk
object EncryptedNdk {
init {
System.loadLibrary("encryptedndk");
}
external fun apiTokenNative(): String
}
// or if you prefer, use class instead of object
class EncryptedNdk {
companion object {
init {
System.loadLibrary("encryptedndk");
}
}
external fun apiTokenNative(): String
}
Make sure there is an icon on the left side of your method apiTokenNative()
. It means you’ve already succeeded in connecting your cpp to your Kotlin class.
After that, you can do some testing by printing it into your activity or showing the toast.
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Toast.makeText(this, EncryptedNdk.apiTokenNative(), Toast.LENGTH_LONG).show()
}
}
All done, you’ve already successfully put your first API_TOKEN into NDK.
Encrypt API_TOKEN
To have an encrypted API_TOKEN, you need to have an encryption function that you will use before passing it into cppFlags in your Gradle script. Create encryptor.gradle in the root project.
import javax.crypto.Cipher
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
def algorithm = "AES/CBC/PKCS5Padding"
def iv = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] as byte[]
def ivSpec = new IvParameterSpec(iv)
ext.encrypt = { text, key ->
def cipher = Cipher.getInstance(algorithm)
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key.bytes, "AES"), ivSpec)
def cipherText = cipher.doFinal(text.bytes)
return cipherText.encodeBase64()
}
More information on the algorithm can be found here. You can change it to something suitable for your project. Different algorithms have different requirements for your key. In the preceding example, use the AES/CBC/PKCS5Padding algorithm to generate a key of 16 characters (or 16 bytes).
After that, you can use it in your app/build.gradle.
apply from: '../encryptor.gradle'
android {
...
defaultConfig {
...
externalNativeBuild {
cmake {
// encrypt when passing to cppFlags
cppFlags '-DAPI_TOKEN=\\\"' + encrypt("This_is_API_TOKEN_from_native", "1234567890123456") + '\\\"'
}
}
}
}
Just try to run it and see the result. The API_TOKEN will be shown encrypted.
Decrypt API_TOKEN
After you successfully encrypt your API_TOKEN, you need to decrypt it to get the actual value of your API_TOKEN. So you need to have a function on the EncryptedNdk class to decrypt apiTokenNative()
.
object EncryptedNdk {
...
external fun apiTokenNative(): String
fun apiToken(): String = decrypt(apiTokenNative())
// region Decryptor
// The algorithm should be the same with encryptor
private const val algorithm = "AES/CBC/PKCS5Padding"
private val cipher = Cipher.getInstance(algorithm)
private val iv = ByteArray(16)
private val ivSpec = IvParameterSpec(iv)
private val keySpec = SecretKeySpec("1234567890123456".toByteArray(), "AES")
private fun decrypt(
text: String
): String {
cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec)
val plainText = cipher.doFinal(Base64.decode(text, Base64.DEFAULT))
return String(plainText)
}
// endregion
}
And finally, change from EncryptedNdk.apiTokenNative() to EncryptedNdk.apiToken() on MainActivity.kt.
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Toast.makeText(this, EncryptedNdk.apiToken(), Toast.LENGTH_LONG).show()
}
}
It should show the actual API_TOKEN on Toast.
Proguard rule
Because we write the name of the package, the class name, and the method name on the cpp file, we need a proguard rule to keep the package name, the class name, and the method name for your EncryptedNdk class.
-keep class com.adefruandta.encryptedndk.EncryptedNdk {
native <methods>;
}
Final touch
Because you have C++ in your module, when you build your project, it will generate a cxx folder in your module. So you need to ignore it from git. And also, you can add a custom Gradle task to delete the cxx folder when running the clean task.
// app/build.gradle
project.task("cleanCxx") {
delete '.cxx'
}
project.tasks.findByName("clean").finalizedBy("cleanCxx")
I hope this article will help you protect and secure your keys, tokens, or any sensitive data when you store it in your APK. See the repository below for a full example.
Keep safe and stay healthy.