Shielding Your App: Implementing Best Practices for Android Security

Maulik togadiya
Mindful Engineering
16 min readJul 28, 2023
Photo by N I F T Y A R T ✍🏻 on Unsplash

One of the reasons why you should be pondering about app security is to gain user trust and maintain device integrity. App security is crucial to protect user data, establish trust, comply with guidelines and regulations, defend against malicious attacks, reverse engineering, and avoid financial losses. In this article, we will go through some best practices that should be followed to develop one secured app.

“The best way to secure your app is to assume it’s already been compromised.” — Unknown

1. ProGuard/R8

There are many tools available by using which anyone can easily decompile your app and reverse engineer it and retrieve sensitive information like secrets keys etc.

What is it?

ProGuard is a traditional tool that is used in Android SDK and natively integrated into android studio and it helps us to do the following :

  • Shrink code: Remove unused resources and classes.
  • Obfuscate code: Rename the names of classes, variables.
  • Optimize code: Inline code, eliminating unreachable branches, etc.

Overall, it reduces the size of the app, improves performance, and makes the code harder to reverse engineer 🕵️‍♂️.

Gif by gfycat

How to use it?

You can enable ProGuard by simply adding the below code to your app’s build.gradle file.

android {
buildTypes {
release {
// Enables code shrinking, obfuscation, and optimization for only
// your project's release build type.
minifyEnabled true

// Enables resource shrinking, which is performed by the
// Android Gradle plugin.
shrinkResources true

// Includes the default ProGuard rules files that are packaged with
// the Android Gradle plugin. To learn more, go to the section about
// R8 configuration files.
proguardFiles getDefaultProguardFile(
'proguard-android-optimize.txt'),
'proguard-rules.pro'
}
}
}

After enabling ProGuard there might be a chance that it will break your code. To fix this we can write some custom rules to make sure we remove the set of code from obfuscating.

Let’s see how can we write a custom rule in ProGuard :

Keeping Class files

Suppose we have an Address data class and we want not to obfuscate that class, We can use @Keep annotation as shown below.

@Keep
data class Address(
var id: String?,
val name: String,
val street: String,
val city: String,
val state: String,
val country: String,
)

We can also use

-keep class com.myapplication.data.Address.**

Note : If your app uses Gson for JSON serialization/deserialization, ensure that model classes (POJOs) are not obfuscated. Use @SerializedName annotations or add ProGuard rules to keep model classes intact.

Keep a member of the class

To preserve only the class members and not the class while shrinking, then we use,

-keepclassmembers class com.myapplication.data.Address{
public *;
}

Here it will keep all public modifier members of the Address data class.

Keep all classes package

Let’s say we have a data package that includes all models that I want not to be obfuscated.

-keep class com.myapplication.data.**{*;}

Using any Library on Android

If your app uses libraries that require specific ProGuard rules, consult the library’s documentation for the necessary configuration.

📣 Always make sure you enabled ProGuard before publishing you app.

2. Store Safely All API Keys And Other Sensitive Data

Most of the time developers declare sensitive information like BASE_URL, API keys, etc inside the constant file or string resource in their project but it is not secure at all as it can be easily exposed when someone does reverse engineering.

One of the most effective solutions to ensure safety is to avoid storing sensitive information on the client side. Instead, receiving such data as environmental variables from a secure server through an API is an ideal approach. However, it may not always be feasible in all scenarios. In such cases, implementing other security measures becomes crucial to protect sensitive data stored on the client side.

Note : While there is no foolproof method to guarantee 100% security for safeguarding sensitive information, we can take measures to enhance security and make it more challenging for the app to be reverse-engineered.

Let’s talk about the different ways that a developer is used to store API keys and other sensitive data.

Storing keys inside string file or in constant file ❌

<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--API Key-->
<string name="google_api_key">abcdefgdfgdf2q4qsaf34rtyyui421k</string>
</resources>
object AppConstants {  
// Google Api key
const val GOOGLE_API_KEY = "abcdefgdfgdf2q4qsaf34rtyyui421k"
}

This is one of the common ways that developers are using and it is not secured as keys and sensitive information can easily be exposed after reverse engineering. Never do this 🙅‍♂️

Gif by Tenor

Storing keys inside Gradle Files

android {
compileSdk 33

defaultConfig {
applicationId "com.myapplication"
minSdk 29
targetSdk 33
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"

//API BASE URL
buildConfigField "String", "API_BASE_URL", ENDPOINT // "https://myserver.com/v1

// String resource map api key
resValue "string", "MAPS_API_KEY", MAPS_API_KEY as String
}
}

We stored the API_BASE_URL and MAPS_API_KEY inside the Gradle build configuration, and access them directly after successful compilation. While this approach is considered more secure than storing them in plain string resources or constant files, it’s essential to recognize that it is not completely foolproof against determined attackers.

After the build, it can access through code BuildConfig.API_BASE_URL .

Storing keys inside the gradle.properties file

// inside gradle.properties

MAPS_API_KEY=AIzaSsdfgkLDl2astgrkih-vwVYsdfLpr4-Yw
DEV_ENDPOINT="https://myserver.endpoints.com/v1/"

In this approach, we place keys and sensitive information inside the gradle.properties file, and the compiler will build it accordingly. Additionally, we add this file to .gitignore so we can keep it safe in the local system. While this method is widely used among developers, it's important to acknowledge that it is not entirely foolproof or highly secure against potential threats.

Better way using CMake and C/C++ NDK (Re-commanded) ✅

This is one of the best approaches I found till now and it is using NDK native C/C++ code which is way more secure to decompile and reverse engineer than doing with normal JAVA/Kotlin code. so basically in this approach, we will hide our keys and secret information in C/C++ code.

Gif by tenor

Step 1: Install the required tools

We need to install two tools as mentioned below :

  • NDK (Native Development Kit): Will be used to work with C/C++ code in Android.
  • CMake: Required to build a C/C++ library.

You can find it in android studio Tools SDK Manager then go to the SDK tools tab as shown in the below snapshot. make sure you installed it.

Android Studio SDK manager

Step 2: Create a native-lib.cpp file.

First, create a cpp folder inside app/src/main.

then right-click on the cpp folder and go to New → C/C++ Source File and name it native-lib.cpp. Later we will add the required content inside.

app/src/main/native-lib.cpp file created

Step 3: Create a CMakeLists.txt file.

app/src/main/CMakeLists.txt file created

Under the same app/src/main/cpp folder create the CMakeLists txt file and add the below code content.

Note : make sure you name it txt and not text 😀.

# 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.22.1) #Your CMAKE downloaded version

# Declares and names the project.

project("myapplication")

# 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.
myapplication

# Sets the library as a shared library.
SHARED

# Provides a relative path to your source file(s).
native-lib.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.
myapplication

# Links the target library to the log library
# included in the NDK.
${log-lib})

Step 4: Configuring gradle for CMake.

Add the below line inside your app-level build.gradle’s android block:

android {
...
externalNativeBuild {
cmake {
path file('src/main/cpp/CMakeLists.txt') // path to CmakeLists file
version '3.22.1' // your downloaded CMake version
}
}
}

Rebuild and Sync project.

Step 5: Create Key Utility class KeyUtils.kt.

package com.myapplication.utils

object KeyUtils {

init {
System.loadLibrary("myapplication") // project name mentioned in native-lib.cpp file
}

external fun apiKey() : String

external fun baseUrl() : String
}

In this approach, we load the C++ library code by calling System.loadLibrary("myapplication") inside the init block that we will write in the next step inside the native-lib.cpp file. Additionally, we have created an external Kotlin function that we will link to the C++ code. This Kotlin function will provide our sensitive information throughout the app whenever required.

Step 6: Final step - Storing API keys inside native-lib.cpp file.

Inside your native-lib.cpp file add the below code.

#include <jni.h>
#include <string>

extern "C" JNIEXPORT jstring JNICALL

extern "C"
JNIEXPORT jstring JNICALL
Java_com_myapplication_utils_KeyUtils_apiKey(JNIEnv *env, jobject thiz) {
std::string api_key = "api key goes here ...";

return env->NewStringUTF(api_key.c_str());
}
extern "C"
JNIEXPORT jstring JNICALL
Java_com_myapplication_utils_KeyUtils_baseUrl(JNIEnv *env, jobject thiz) {
std::string base_url = "base url goes here ...";

return env->NewStringUTF(base_url.c_str());
}

Here as you can see we are storing API keys/Base Url inside the function variable(std::string api_key/std::string base_url) and returning it.

Okay Now, Let’s have a closer look at the C++ function name (Java_com_myapplication_utils_KeyUtils_apiKey).

Mainly it will divide into 4 parts Let’s look into from right to Left.

  • apiKey: It refers to the method name we will be using to get Api keys. In our case, it is defined in KeyUtils class with apiKey() method.
  • KeyUtils: It refers to kotlin object which provides all methods to receive sensitive data. In our case, it is KeyUtils which holds two methods apiKey() and baseUrl() and it will interact with the C++ code, and get a reference to your API key/Base Url (which you can use throughout your app).
  • com_myapplication_utils: It refers to the package name of kotlin object keyUtils.In our case, it was com.myapplication.utils but here ( . ) will be converted to underscore (_) so it becomes com_myapplication_utils.
  • Java_ : (Static prefix to add on every C++ method)

Note : Remember to add native-lib.cpp to .gitignore to avoid including sensitive C++ code in version control. Keep it secure on your local development environment while excluding it from the repository.

Now, to get your API key/Base Url from any part of your app, just call:

KeyUtils.apiKey()
KeyUtils.baseUrl()

Note: Although there is a possibility of reverse engineering by reading the ‘libnative-lib.so’ file in an editor and finding sensitive information at the end of the file, the number of people capable of extracting information from the C++ library is lower compared to those who can do it from a Java file. Thus, making it harder for hackers increases the overall security.

As mentioned earlier, the safest approach to secure sensitive information is to avoid storing it on the client side.

3. EncryptedSharedPreferences/Data Store

When it comes to storing simple key/value pair data, developers often opt for SharedPreferences. However, it’s important to note that SharedPreferences are not secure and should not be used to store sensitive user information. It is highly recommended to avoid storing any sensitive data in SharedPreferences.

EncryptedSharedPrefrences are one of the best options to store these data in encrypted form. It comes as a part of the JetPack library in androidx.security.crypto package.

What is it?

  • It is an extended version of SharedPrefrences and is used the same way.
  • Under the hood, it uses an Android key store for encryption and stores keys and values securely.
  • It uses two types of keys: master key and subkeys. the subkeys are used to perform encryption operations on actual data and the master key is used to encrypt all subkeys so ultimately it will add an extra security layer.

Any limitation?

  • It is available to support since API 23 for now.
  • It is Slower than regular(non-encrypted) SharedPrefrence

How to implement it?

  1. Add Security library in your app-level’s build.gradle.
dependencies {
implementation "androidx.security:security-crypto:1.1.0-alpha06"
}

2. Create the instance of Encrypted SharedPreferences.

val spec: KeyGenParameterSpec = KeyGenParameterSpec.Builder(
MasterKey.DEFAULT_MASTER_KEY_ALIAS,
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.setKeySize(MasterKey.DEFAULT_AES_GCM_MASTER_KEY_SIZE)
.build()

val masterKey = MasterKey.Builder(context)
.setKeyGenParameterSpec(spec)
.build()

val prefs = EncryptedSharedPreferences.create(
context,
"AppPreferences",
masterKey,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)

Note : As this initialisation may take little while around (25–100 millis depending upon device) would be better to done it off the main thread.

3. Read and write value

You can write to pref by using the edit function like this :

prefs.edit().putString("key goes here..", "value goes here..").apply()

You retrieve data from a key like this :

prefs.getString("key goes here.., "")

Yes, that’s it. Now your keys and values both are encrypted and this is how it looks when you check the preference file in file explorer.

AppPreferance

Why not DataStore?

You must have a question that why we are not talking about DataStore as it is a modern and flexible alternative to SharedPreferences for persistently storing key-value pairs or structured data. If you look into the preference file generated from DataStore then it is not encrypted and there is no provided way to use Encrypted DataStore. Although, Google says that If you’re currently using SharedPreferences it to store data, consider migrating to DataStore instead. But they don’t provide a secure way of storing data.

Gig from tenor

4. Database Encryption

Nowadays, many apps utilize local databases like Room or SQLite, but developers often overlook encrypting the data stored in the database, leaving it vulnerable. By default, Room stores data in the app’s internal storage, which can be accessed by a root user. To enhance database security, various encryption libraries are available, such as cwac-saferoom for Room, SQLCipher with AES256 encryption, and Realm’s built-in encryption system. Implementing these encryption methods helps protect sensitive data and ensures more secure database storage.

5. Root Detection/Play Integrity API

Google Play Integrity API are security mechanisms provided by Google to help protect Android applications and devices from potential threats. It is a security feature that allows app developers to query the Google Play Store for information about their app’s integrity and installation. It helps protect apps from being tampered with or distributed through unofficial channels.

Gif by Giphy

Below are key features it provides:

  • App Verification: Developers can use this API to check if their app was installed from Google Play or if it was sideloaded from other sources. This helps prevent unauthorized distribution and ensures that the app is coming from a trusted source.
  • Play Protect Certification: The API allows developers to check whether their app is Play Protect certified. Play Protect is Google’s built-in malware protection for Android devices, and being Play Protect certified indicates that the app is considered safe by Google.

Ultimately, It will add an extra layer of security on top of your app.

6. Use App-Specific Storage For Sensitive Data

App-specific storage in Android refers to the private storage space allocated to an individual app on the device. This storage space is secure due to several built-in security mechanisms provided by the Android operating system.

Isolated Storage: It runs in its own sandboxed environment, which means that it has limited access to other apps’ data and resources. App-specific storage ensures that an app’s data is isolated and cannot be accessed directly by other apps, thereby reducing the risk of data leakage and unauthorized access.

No permission needed: The files and directories within the app-specific storage have specific permissions assigned to them. These permissions restrict access to the files to only the app’s UID (User ID) and GID (Group ID). Even other apps running under different UIDs on the same device cannot access these files directly.

As an added security measure, when the user uninstalls an app, the device deletes all files that the app saved within internal storage.

7. Network Security Configuration

Network Security Configuration is a feature introduced in Android 7.0 (API level 24) that allows developers to define network security policies for their apps. It provides a way to specify how an app should communicate securely with servers and manage various aspects of network security. By configuring network security, developers can enhance the security of their app’s network communications and protect sensitive data from potential security threats.

  • When Android 6.0 Marshmallow was released, Google introduced the manifest attribute android:usesCleartextTraffic as a means to protect against accidental use of cleartext traffic.
  • Android 7.0 Nougat extended this attribute by introducing the Android Network Security Configuration feature, which allows developers to be more prescriptive about secure communications.
  • Android 9 and above disabled clear text by default.

What is cleartext?

Clear text refers to data or information that is transmitted or stored in plain, unencrypted form like using HTTP.

How to permit cleartext traffic ?🤔

Manifest file with network security config.

<?xml version="1.0" encoding="utf-8"?>
<manifest ...>
<uses-permission android:name="android.permission.INTERNET" />
<application
...
android:networkSecurityConfig="@xml/network_security_config"
...>
...
</application>
</manifest>

Bad example

<network-security-config>
<domain-config cleartextTrafficPermitted="false">
<domain includeSubdomains="true">your.domain1.com</domain>
<domain includeSubdomains="true">your.domain2.com</domain>
</domain-config>
<base-config cleartextTrafficPermitted="true" />
</network-security-config>

The above configuration ensures that all traffic to ‘your.domain1.com’ and ‘your.domain2.com’ is sent over HTTPS, the default setting for all other traffic sent to different domains allows cleartext transmission. This default setting undermines the main purpose of the Network Security Configuration feature, which is to enhance the privacy and security of all data transmitted through Android devices.

Good example

If it is necessary to send data in cleartext, then do this to only allow encrypted communications to certain domains.

Recommended Implementation of cleartext Permissions :

<network-security-config>
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="true">your.domain1.com</domain>
<domain includeSubdomains="true">your.domain2.com</domain>
</domain-config>
<base-config cleartextTrafficPermitted="false" />
</network-security-config>

Implementing Network Security Configuration helps ensure that an app communicates securely with servers, prevents unauthorized access to sensitive data, and protects against certain types of security attacks. It is particularly useful for apps that handle sensitive user information, use secure authentication mechanisms, or have strict security requirements.

Always use HTTPS (HTTP Secure) for network communication to encrypt data during transit

8. Restrict Access To Google API Service Keys

Many apps commonly use popular Google services such as Place API or Google Maps-related services. Unfortunately, it is a common practice to create keys on the Google Cloud Console (GCP) without applying proper key restrictions. This leaves the keys unsecured, allowing anyone to use them freely and potentially leading to excessive requests that exceed your quota.

This leaves the keys unsecured, allowing anyone to use them freely and potentially leading to excessive requests that exceed your quota which end up you with a long invoice bill in your hand.

Gif from tenor

Solution?

The solution is simple, just restrict your API key in the Google console

Key restrict with the package name and SHA1

Restrict your app to specific platform usage and also add your package name(bundleID) with your SHA-1 certificate fingerprint from your release key.

Also, restrict the number of APIs that the key can access it.

Restrict API that can be used with the same keys

9. Firebase security rules

Many apps now leverage Firebase Realtime Database or Firestore to store data, which is excellent. However, a prevalent issue is that developers often neglect to write proper security rules for these databases.

If you don’t know where to find this rule and update check below sample:

Don’t ❌

rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if true;
}
}
}

If you keep read/write true meaning that every person has permission to read and write to your database.

Do ✅

rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if request.auth!=null;
}
}
}

This will allow only logged users to write in your database.

In some common cases, Let’s assume you are storing or accessing user’s information from Firestore path users/uid.

Here we created a users collection and inside that we have uid as documents for different users, like this:

Users(collection) >> UID(Documents)

Here you must want to restrict that each user can only read and write their own information and we can achieve this by adding the below rule :

match /users/{uid} {
match /{document=**} {
allow read, write : if request.auth.uid == uid;
}
}

10. Keep App And SDKs Updated

Ensure your app and any third-party SDKs are up to date to benefit from security fixes and improvements.

It is essential to regularly update all libraries in your app to leverage improved performance, security enhancements, and new features. By keeping your libraries up to date, you ensure that any security vulnerabilities introduced in previous versions are promptly addressed in subsequent updates. Avoiding security issues is of utmost importance, and updating libraries helps prevent potential security breaches in your app.

Remember, security is an ongoing process. Stay informed about the latest security best practices, follow Android security guidelines, and proactively address security issues to keep your app and users’ data safe.

Feel free to comment if you come across any additional points to mention here. Let’s explore and learn together as a community 🤝. Collaboration and shared knowledge are key to enhancing our understanding and improving our practices. Together, we can continue to discover new insights and best practices for better app security.

--

--