[ad_1]

Many Android apps require some secrets, such as API keys, to function. In most cases, these keys are for Google services and can be protected based on your app signing key (this can be configured in the Google Cloud Console), so it’s safe to compromise. However, in some cases you may need to add keys or other secrets that are unprotected and need to be hidden. These articles will show you how.
There are two approaches to client secrets.
- Bundle secrets with app binaries
- Get from remote storage (server)
This article describes the first type.
The first solution most developers use is to store static files that hold the keys within their code.
Keys can also be added at compile time. Build configuration Or the following resources (similar to the google-services plugin) build.gradle composition.
android {
defaultConfig {
buildConfigField("String", "API_KEY", "\"SECRET_API_KEY\"")
resValue("string", "api_key", "\"SECRET_API_KEY\"")
}
buildFeatures {
buildConfig = true
}
}
These keys can then be accessed via:
class MyApp : Application() {override fun onCreate() {
super.onCreate()
Log.e("TEST", "Static field: $API_KEY")
Log.e("TEST", "BuildConfig field: ${BuildConfig.API_KEY}")
Log.e("TEST", "Res field: ${getString(R.string.api_key)}")
}
companion object {
const val API_KEY = "SECRET_API_KEY"
}
}
The main problem with this approach is that these values are easily discoverable in the results app.
During compilation, R8 optimizes the use of static string concatenation into a single string. Therefore, the compiled code inserts the value into the log message.
Resource values can also be easily found in the Results app.
This approach can be improved a bit by splitting the key into several parts and placing them in different parts of the application. You can then combine these parts at runtime. Parts can also be processed. For example: XOREditing it with some constants makes the process of discovering the actual key more difficult for an attacker, but it is still possible.
The first approach hides keys in the Kotlin/Java source code. You can go down a level and hide keys in native code (C/C++) compiled into .so libraries.
Let’s start by building the .so library.You need to add the following configuration to your module build.gradle.
android {
externalNativeBuild {
cmake {
path = file("CMakeLists.txt")
}
}
}
CMake configuration ({module}/CMakeLists.txt) is:
project(secrets)cmake_minimum_required(VERSION 3.4.1)
add_library( # Specifies the name of the library.
secrets
# Sets the library as a shared library.
SHARED
# Provides a relative path to your source file(s).
src/main/cpp/secrets.h
src/main/cpp/secrets.cpp
)
The library source code is represented as .cpp and .h files.
#include "jni.h"
#include <string>#define API_KEY "SECRET_API_KEY"
#define API_KEY_LENGTH strlen(API_KEY)
void getApiKey(char* buffer);
extern "C" JNIEXPORT jstring JNICALL
Java_com_kurantsov_NativeSecrets_getApiKeyFromNative(
JNIEnv *env,
jobject thiz
) {
char key_buffer[API_KEY_LENGTH + 1] ;
key_buffer[API_KEY_LENGTH] = '\0';
getApiKey(key_buffer);
return env->NewStringUTF(key_buffer);
}
#include "secrets.h"void getApiKey(char* buffer) {
for (int i = 0; i < API_KEY_LENGTH; i++) {
buffer[i] = API_KEY[i];
}
}
To retrieve data from native libraries, you can use JNI (Java Native Interface).
Let’s define a Kotlin class that calls a native library using JNI.
package com.kurantsovobject NativeSecrets {
init {
System.loadLibrary("secrets")
}
external fun getApiKeyFromNative(): String
}
In the above snippet, the initialization block loads the native library and defines an object with methods. getApiKeyFromNative marked with external. This means that the implementation of this method is in native code.
The interaction between Kotlin methods and native functions follows a special naming convention. Native functions must have names of the form Java_{ fully qualified method name (contains ‘.’). ‘_’} replaced.
This approach makes it slightly harder for an attacker to discover the actual value of the secret, but it is still possible. You can easily find this string by opening the .so file in her hex editor.
You can make your native library a little more “safe” and dynamic by making some changes to the initial secret and implementing logic to undo changes in the native code. In the following example, we changed the initial string using: XOR Perform an operation on each byte and perform the same operation in native code.
To achieve this you need to add the following changes: build.gradle:
android {
defaultConfig {
val xorValue = 0xAA
val API_KEY = "SECRET_API_KEY"
val apiKeyBytes = API_KEY.toByteArray().map {
it.toInt() xor xorValue
}
val apiKeyDefinitionString =
apiKeyBytes.joinToString(prefix = "[${apiKeyBytes.size}]{", postfix = "}")
externalNativeBuild {
cmake {
cppFlags(
"-DAPI_KEY_LENGTH=${apiKeyBytes.size}",
"-DAPI_KEY_BYTES_DEFINITION=\"$apiKeyDefinitionString\"",
"-DXOR_VALUE=$xorValue",
)
}
}
}
}
The header remains mostly the same, except the original definition has been removed.
#include "jni.h"void getApiKey(char* buffer);
extern "C" JNIEXPORT jstring JNICALL
Java_com_kurantsov_NativeSecrets_getApiKeyFromNative(
JNIEnv *env,
jobject thiz
) {
char key_buffer[API_KEY_LENGTH + 1] ;
key_buffer[API_KEY_LENGTH] = '\0';
getApiKey(key_buffer);
return env->NewStringUTF(key_buffer);
}
cpp file is:
#include "secrets.h"void getApiKey(char* buffer) {
int api_key_bytes API_KEY_BYTES_DEFINITION;
for (int i = 0; i < API_KEY_LENGTH; i++) {
buffer[i] = api_key_bytes[i] ^ XOR_VALUE;
}
}
A complete example can be found on my GitHub (https://github.com/ArtsemKurantsou/Secrets-in-Android).
[ad_2]
Source link