Security Pipeline for Android Development 🔐

Hero image for Security Pipeline for Android Development 🔐

Mobile applications are rapidly becoming the default tool for personal finance, e.g., e-banking, online shopping, etc.; when they get a wide user base and offer access to more resources, they become more attractive to attackers. Without a robust security foundation, a mobile financial platform provides a vast attack surface and the opportunity to compromise individual accounts or the complete infrastructure.

Some attacks attempt to reverse-engineer mobile applications, while others focus on intercepting the communication between the server and the application. If successful, such attacks can result in severe financial and/or reputational damage.

Therefore, security must be integrated and prioritized during the development of Android applications.

Application Data and Credentials

User Data protection

Keeping sensitive user data away from the prying eye is always the priority. The following sections present how to protect user data in data storage as well as from media projection and abusing permissions. It also shows how to hide away credential information in the gradle script and using cryptography.

1. Data Storage

There are many types of data storage for Android e.g., SharedPreferences, Files, and Databases.

The basic SharedPreferences stores the data in plain text, making it vulnerable to actors with root access, when mounting the device without using the Android OS, bypassing permissions that restrict access, or other security exploits. Similarly, files and databases can also be compromised. This poses a security risk for storing sensitive user data such as credentials, tokens, sessions, etc.

💡 The following best practices must be followed:

  • SharedPreferences: to securely store private user data, consider using EncryptedSharedPreferences from the latest security library, part of Android Jetpack.
  • Files: store sensitive application files in the internal storage to prevent other applications from accessing them. To introduce additional security, use EncryptedFile included in the security library, which provides custom implementations of FileInputStream and FileOutputStream for more secure streaming read-write operations to store and access files on the device storage. Store files in external storage only when they are not sensitive. Otherwise, use the aforementioned EncryptedFile class to encrypt the files while storing them in the external storage.

Store files that are meant for the application’s use only, either in dedicated directories within an internal storage volume or different dedicated directories within external storage. Use the directories within internal storage to save sensitive information that other applications should not access. Reference: https://developer.android.com/training/data-storage

  • Databases: do NOT store sensitive data in databases. If necessary, apply encryption using the Android Keystore system. Use a third-party library such as SQLCipher to integrate Room with SQLCipher.

2. Media Projection

A malicious application can bypass the permission models to capture screenshots or record the screen with audio while the user is using an application that requires strict privacy, e.g, banking, mobile wallet, etc. The same can happen while the application window is visible in the Recent Screen, thus leaking sensitive user information.

💡 To prevent any application, malicious or not, from capturing/recording an application screen, consider applying FLAG_SECURE on destination activity or fragment.

3. Application Runtime Permission

Permissions provide the applications access to user data and hardware that can be abused by malicious applications. Therefore, requesting more permissions (install-time or runtime) than the application is supposed to need may deter users from using it and reduce user engagement. Also, third-party libraries can sometimes silently add unintended permissions to the final merged manifest file. They must be reviewed carefully and removed before publishing the application.

💡 The following best practices must be followed:

  • Minimize the number of permissions that the application requests. Generally, if a permission is not required for the application to function, do NOT request it.
  • Ask for runtime permission only when the feature that needs the permission is being used and provide a clear and succinct explanation as to why it is needed.
  • If the permission is not granted by the user, allow them to continue using other features of the application.
  • Permissions asked by third-party libraries can be found in the merged manifest.
    • To view which library asked for the unwanted permission, go through the merger-staging-debug-report.txt file located in the directory app/build/outputs/logs/.
    • Use tools:node="remove" inside the <uses-permission/> tag for the permission that want to remove in the AndroidManifest file. For example:
<uses-permission android:name="android.permission.RECORD_AUDIO" tools:node="remove" />

Credentials protection

1. Credential information

When using Gradle to generate signed APK, creating manifest placeholders, or defining buildConfigFields, hardcoding the credentials such as API secrets, keystore passwords, etc., can expose them via version control or reverse engineering.

💡 The following best practices must be followed:

Create .properties files to keep the passwords, secrets, and other credentials. Then read them from the files in build.gradle. After that, add the files to .gitignore to prevent pushing them to the repository.

// Put this in app module level gradle:
signingConfigs {
    release {
        try {
            storeFile file("myapp.keystore")
            storePassword KEYSTORE_PASSWORD
            keyAlias "thekey"
            keyPassword KEY_PASSWORD
        }
        catch (ex) {
            throw new InvalidUserDataException("You should define KEYSTORE_PASSWORD and KEY_PASSWORD in gradle.properties.")
        }
    }
}

// And create another `gradle.properties` file to keep the password, do NOT push this to the repo.
KEYSTORE_PASSWORD=real_keystore_password
KEY_PASSWORD=real_key_password

2. Keystore API

At lower API levels (< 21), EncryptedSharedPreferences is not available. Therefore, Android provides the Keystore API to securely store the encryption keys to encrypt and decrypt secrets.

💡 The following best practices must be followed:

Encrypt and decrypt the data using a randomly generated AES key along with RSA public/private key pair in the following steps:

Key Generation

  1. Generate a pair of RSA keys.
  2. Generate a random AES key.
  3. Encrypt the AES key using the RSA public key.
  4. Store the encrypted AES key in Preferences.

Encrypting and Storing the data

  1. Retrieve the encrypted AES key from Preferences.
  2. Decrypt the above to obtain the AES key using the private RSA key.
  3. Encrypt the data using the AES key.

Retrieving and decrypting the data

  1. Retrieve the encrypted AES key from Preferences.
  2. Decrypt the above to obtain the AES key using the private RSA key.
  3. Decrypt the data using the AES key.

Networking

1. SSL Certificate Pinning

The Secure Sockets Layer (SSL)-—now technically known as Transport Layer Security (TLS)-—is a standard building block for encrypted communications between clients and servers. An application might misuse SSL, such that malicious entities may be able to intercept application data over the network (man-in-the-middle attack).

Therefore, SSL pinning, which is known as Public Key Pinning, is an attempt to solve these issues. It ensures that the certificate chain used is the one that the application expects by checking a particular public key or certificate that appears in the chain.

💡 The following best practices must be followed:

  • If the minimum SDK is lower than Android N (API 24); pin the certificate by using the library OkHttp3 API.
  • If the minimum SDK is equal or higher than Android N (API 24); pin certificate by adding the Network Security Configuration file.

Android developers can prevent users from blocking or force-updating future server configuration changes, such as changes to another Certificate Authority, by pinning two certificates simultaneously.

2. Payload encryption

Developers must ensure payloads are encrypted when passing users’ sensitive data. By default, transport between client applications and the backend must be secure using TLS/SSL, which means data are encrypted when transmitted across networks. In addition, applications can also implement end-to-end payload encryption as a second layer of security.

💡 The following sequence diagram represents the payload encryption process, which developers can follow:

Payload Encryption

Others

1. WebView

WebView is commonly used to open web content with HTML and Javascript inside the application as a part of an Activity without opening an external browser. Therefore, WebView can introduce the most common web security issues such as cross-site-scripting (more commonly known by developers as XSS). By default, WebView does not execute JavaScript, so XSS is not possible; therefore, remove setJavaScriptEnabled() if it is not required.

Furthermore, if an application accesses sensitive data with a WebView, developers may want to use the clearCache() method to delete any files stored locally, or can also use server-side headers such as no-cache to indicate that the application should not cache particular content.

By replacing of WebView usage from the application, making use of Chrome Custom Tabs for supporting the most feature like a Web browser with more efficiency and better performance.

💡 The following best practices must be followed:

  • Do NOT enable setJavaScriptEnabled() in Settings if it is not required.
  • Make use of Chrome Custom Tabs.

2. Component Hijacking via Intent

The Android framework allows applications with application components to communicate with one another by passing messages or calling Intents, which effectively specify both a procedure to call and the arguments to use.

Applications must declare in a static manifest file the type of Intent each component service would like to receive as well as application and component level permissions. While the security vulnerabilities in outgoing Intents have been well studied and developer tools exist to limit potentially insecure Intents, little has been done to address malicious incoming Intents. Exploits of this nature have been discovered in the firmware of various Android phones, but exploits in third-party applications are not well studied.

Developers must make sure their manifest file has been properly configured to only accept desired Intents, which can limit usability. To trust Intent input by default will allow malicious input to potentially crash or abuse the application.

💡 The following best practices must be followed:

  • Avoid "exported=true" if developers are not intending to share the Service/Activity for other applications.

In projects supporting Android 12 or higher, if the application component includes the LAUNCHER category, set android:exported to true. In most other cases, set android:exported to false.

  • Wrap Intent with PendingIntent to avoid unexpected incoming Intent received and handled on the app.

3. Proguard / Dexguard (Avoid reverse engineering)

Android applications can be reverse-engineered using a host of freely available tools such as Apktool, dex2jar, dexdump, Jadx, JD-GUI, CFR, Procyon, and so on. These tools are used to decompile the APK packages and dump the executable code and, in some cases, reconstruct the source code. Once an attacker has retrieved the source code, they are able to steal parts of it, extract valuable information from it, or modify it.

💡 The best practice is to enable Proguard to obfuscate strings, names of packages, etc. on production build.

buildTypes {
    debug {
        ...
    }

    release {
        ...
        proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        ...
    }
}

4. Logging (API request, user information, etc.)

Other very important elements in the process of developing applications are logs. Logs are especially useful for developers while analyzing the work of algorithms responsible for data processing inside apps. Adding data to logs makes it easy for developers to make sure the results are correct or that the sequence of processing is performed in the correct order.

Unfortunately, as a result of that, logs may also store sensitive data e.g. passwords, or access to keys/tokens. It is dangerous because logs are stored locally on devices. Moreover, on any devices with lower than Android 4.2 OS version, logs are publicly readable and visible to all applications installed on the device. Only since Android 4.2, access has been limited to certain applications.

💡 The following best practices should be followed to manage logging properly:

  • Disable logging tools for the production build.
  • Prefer to use Timber.

      if(BuildConfig.DEBUG){
      	Timber.plant(new Timber.DebugTree());
      }
    
  • Disable logging API request/response with OkHttpClient

      if (BuildConfig.DEBUG) {
      	val logging = HttpLoggingInterceptor()
      	logging.level = HttpLoggingInterceptor.Level.BODY
      	addInterceptor(logging)
      }
    

Best Practices

1. Root detection

On Android, rooting a device brings a lot of possibilities by giving all the rights on the device to the user. To protect against the risks posed by rooted devices, banking apps or other apps with sensitive data need the ability to detect such conditions and respond to them by preventing the execution or restricting the application’s functionalities.

Rooting detection boils down to the below checks (see this article for detailed information):

  • Detecting an unlocked bootloader is the apparent sign of a rooted device.
  • Checking files and packages.
  • Checking the BUILD tag.
  • Checking root permissions, commands, etc.

💡 The following proven solutions should be considered:

  • SafetyNet: is the official rooting detection package for Android provided and maintained by Google. It can detect rooting and many non-standard modifications. However, there are still some issues such as the inability to bypass the Magisk Hide feature and handle complex implementations, quota limitations, and it depends on Play Services (if they are not installed, SafetyNet will not work).
  • RootBeer is a more popular solution and works by checking the presence of some apps/files on the device. The implementation is quite simple and neat, but there might still be false-positive cases, so the team should take this into account during the implementation.
  • Some other paid solutions from Promon SHIELD, Verimatrix, Certero, etc.

These solutions above should be seen only as a helper countermeasure because there is no 💯 percent safe solution.

2. Recent Apps thumbnail hiding

Since privacy is always a concern, some applications – especially digital banking, and wallet application – show sensitive information that must NOT be shown on the Recent Tasks screen when stopping the app by pressing the home button. Instead, they will try to show a custom app logo as an app thumbnail on Recents Screen.

Payload Encryption

💡 The following best practices must be followed:

  • There is no explicit support from Android at the official API level. Only a secure flag was initially introduced to prevent DRM-protected content from appearing in screenshots, video screencaps, or being viewed on non-secure displays such as Recents Screen. This flag does not support showing custom app UI in Recents Screen, only blank.

  • A non-official approach to achieve this requirement besides using the secure flag is HardwareKeyWatcher. Still, it cannot cover all of the cases due to fragmented device configurations and custom ROMs, so the team must fulfill missing cases with other approaches.

To ease the way of implementation, we (Nimble) had built a library 👉 Recent Apps thumbnail hiding 🌟, which supports implementing a custom layout to show an empty screen with the app’s logo when the app is going to Recents Screen.

3. Biometric authentication (Fingerprint, Iris, Face ID)

Users are now favoring biometric authentication as it is a convenient mechanism - such as fingerprint scanning or face ID. Proper use of biometrics increases security too 👉 passwords are easy to steal, and faking biometrics is much more difficult. Therefore, biometric authentication is likely to have a significant role in the future world of payments.

The Android framework includes face, fingerprint, and iris biometric authentication and offers some different classes for biometric authentication:

💡 The following best practices must be followed:

  • All biometric implementations must meet security specifications and have a solid rating for participation in the BiometricPrompt class.
  • Acquire, store, and process biometric data for user authentication carefully by following System Security Best Practices.
  • The difference between auth-per-use vs time-bound encryption keys.

References