Nowadays, one issue that flies under the radar for Android developers is app security. Although the Android OS possesses a layer of security that’s sufficient most of the time, it can be enhanced by adding an extra layer of security using some advanced mechanisms and features.
Today’s article is going to cover some of these best practices and mechanisms to better secure your apps.
We begin by showing the way to secure network communications by using network security configuration files and certificate authority customization. We’ll then shed some light on inter-app communications, where we can secure incoming and outgoing intents, applications components like Content Provider, and more.
This article presumes you have some experience in building Android apps with Kotlin, and also some general knowledge about interacting with other apps and doing network communication. Finally, the reader should have some experience with security principles like HTTPS, Certificate Authorities, and so on.
Network Communications
Most Android devices connect to the internet through wifi, which is oftentimes accessed publicly. This can expose devices and applications to a variety of attacks that affect user information. To confront this, we should secure our network’s communication in order for the user’s sensitive information to be transmitted securely.
One important thing to do is to connect to secure endpoints—by that, I mean HTTPS or non-cleartext traffic domains.
Now we’ll explore the network security configuration that will prevent us from connecting to unsecured connections.
Network Security Configuration
With Android 7.0 Nougat, the platform introduced the Network Security Configuration file, which helps you customize the security configuration in a declarative based scripting style using XML without modifying the app code. It offers a lot of flexibility for developers with features like:
- Prevent connecting to cleartext traffic (non-secure HTTP)
- Customize connection configuration by the domain name.
- Testing connection purposes with debug overrides
- Advanced security features like certificate pinning
To start using the Network Security Configuration file, you first need to create an XML file inside the res/xml
folder. You can name it whatever you want. Next, set your application to use the file you just created as configuration in-network calls. This is achieved by setting a tag in the manifest file to point to our configuration file:
<?xml version="1.0" encoding="utf-8"?>
<manifest ... >
<application
android:networkSecurityConfig="@xml/config"
...>
...
</application>
</manifest>
In the config file, you can set different tags for different purposes. In the root tag called network-security-config, you can add the following tags:
- base-config: This tag is used for the default configuration of all the connections unless it’s not overridden by the domain-config tag.
You can set within it a parameter called clearTextTrafficPermitted
to indicate whether you want to allow clear text traffic (unsecured HTTP) or not by setting it to true or false.
This tag can also contain trust-anchors tags, you can use this tag to indicate that your app may want to trust a custom set of Certificate Authorities instead of the platform default (we will talk more about this later in Custom Trusted Certificate Authorities section).
domain-config: This sort of configuration is used for connecting to specific destinations. It can contain multiple domain tags, by which you indicate the destination.
debug-overrides: These overrides happen when android:debuggable is set to true—usually it’s the non-release build’s apk generated for testing purposes. This tag can contain multiple trust-anchors. This is significantly safer than using conditional code.
Here is an example that shows all the tags in one file:
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<!-- Don't allow clear text traffic for all domains unless is not in the domain-config -->
<base-config cleartextTrafficPermitted="false" />
<!-- Special configuration to some domains -->
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="true">localhost</domain>
<trust-anchors>
<!-- Trust a debug certificate in addition to the system certificates -->
<certificates src="system" />
<certificates src="@raw/debug_certificate" />
</trust-anchors>
</domain-config>
<!-- Special configuration if the app is in a debuggable mode -->
<debug-overrides>
<trust-anchors>
<certificates src="@raw/my_ca"/>
</trust-anchors>
</debug-overrides>
</network-security-config>
Custom Trusted Certificate Authorities
Under some circumstances, you might want to trust certificate authorities (CAs) other than the platform default, or you might want to customize the set of trusted CAs by your app.
This could be the case if you want to trust additional CAs that aren’t included in the system or connect to a host with self-signed certificates (like in a private company)—or even just to limit the set of CAs that your app trusts.
You can achieve all of that using the trust-anchors tag in the network security configuration file in one of the config tags (domain, base or debug). Here’s an example:
<domain-config>
<domain includeSubdomains="true">localhost</domain>
<trust-anchors>
<certificates src="@raw/my_certificate" />
</trust-anchors>
</domain-config>
This means that for the localhost domain we are telling the Android OS to enables the certificate file named my_certificate stored in the res/raw directory. The file should be in PEM or DER format and needs to be in res/raw. you can allow more certificates by adding certificates a tag. Here’s another example that shows how to allow both system-based and other CAs:
<base-config>
<trust-anchors>
<certificates src="@raw/othercas"/>
<certificates src="system"/>
</trust-anchors>
</base-config>
Certificate Pinning
The Network Security Configuration file supports more advanced features like certificate pinning. This is an additional layer of security that can be used when a CA issues a fraudulent certificate, which will make your app vulnerable to a man-in-the-middle (MiTM) attack. To prevent such a problem, you need to use certificate pinning (check the resource section for more information).
To use this feature in your app, you need to specify the set of public key hashes for the certificates that are to be trusted. Then, during the HTTPS handshake, we see if the certificate chain contains at least one of the pinned public key hashes that we specified earlier. For that we use two tags:
- pin-set includes the certificate to be pinned. It can be specified in both domain and base configuration tags, and can contain one or more pin tag. Additionally, they can have an expiration date set after the point at which pinning can expire, so the app keeps working even if the user doesn’t update it for a long time.
- pin tag is specified in the pin-set tag that holds the public key SHA-256 for the certificate to be pinned.
An example of that:
<domain-config>
<domain includeSubdomains="true">website.net</domain>
<pin-set expiration="2020-04-16">
<pin digest="SHA256">public_key_primary</pin>
<pin digest="SHA-256">public_key_backup</pin>
</pin-set>
</domain-config>
One thing to add is that you should include an additional pin (a backup one) in case the primary certificate gets compromised or expires. You should also set an expiration date for the pins to support old apps.
Inter-App Communication
Inter-app communication is the communication that allows your Android app to interact with other applications. This sort of communication takes into account the security of intents and the app components.
1- Securing Outgoing Intents
In Android applications, sometimes we launch intents to other applications in the device. These intents can be explicit or implicit—the explicit ones are handled via your code, but the implicit ones are handled by other applications. And for that, the outgoing intents have to be secured in some manner
The issue here is that the user may set an app as the default for some actions, which can cause a problem when these actions involve sensitive user information. In these cases, the user is no longer given an option to choose a trusted app with this sensitive information.
To solve this, we’ll use something called an app chooser, which basically starts whenever you want to launch an implicit intent. An app chooser dialog pops up even if the user has previously set a default app. Here is a code sample:
val intent = Intent(Intent.ACTION_SEND)
val activityList = packageManager.queryIntentActivities(intent, PackageManager.MATCH_ALL)
when {
activityList.size > 1 -> {
val chooser = Intent.createChooser(intent, "Choose an App")
startActivity(chooser)
}
intent.resolveActivity(packageManager) != null -> startActivity(intent)
else -> Toast.makeText(this, "No App to launch with", Toast.LENGTH_LONG).show()
}
2- Securing Incoming Intents
Say you don’t want to expose your activity or your content provider (or any component) to other applications, and you want to allow only the apps you control or own.
For that, you can use signature-based permission that lets only your apps that are signed with the same signing key share data among them. The cool thing is that this type of permission doesn’t require user confirmation and are granted at install time.
The way to do that is to define permission in the manifest file:
<permission android:name=”packageName.HelloWorldPermission”
android:protectionLevel=”signature” />
And then apply this permission to the component you want to make private to the apps you own:
<provider android:name=”android.support.v4.content.FileProvider”
...
android:permission=”packageName.HelloWorldPermission”/>
3- Securing components
Finally, if you want to prevent access from any other application to your components, you can use the mechanism provided by the Android OS.
We generally use it to prevent access to the content provider that holds sensitive application data (you can also apply it to activities and other components).
You simply specify android:exported="false"
in the manifest inside the component you want to disallow access to. Bear in mind that the android:exported
tag is set to true by default.
Here is an implementation sample:
<provider android:name="StudentsProvider"
android:authorities="com.example.StudentsProvider"
....
android:exported="false"/>
Resources
The official Android documentation provides a lot of rules and best practices that can help improve your application. Here are the best practices you need to follow.
You can take a look at this checklist that includes a set of criteria that ensure the security of user information.
For more information about the certificate pinning check out this article.
And finally, here’s the documentation for the Network Security Configuration, which covers all the available features and principles.
Conclusion
I hope you enjoyed this look at various techniques and methods for securing your Android apps. Android provides a security layer by default that you can build on top of in order to gain more security and ensure user safety. Whether you’re developing an Android application for billions of users, or for a select few, the security of your customers should be your first priority in order to prevent the loss or exposure of sensitive user data.
Top comments (0)