Hello Readers, CoolMonkTechie heartily welcomes you in this article (An Overview Of Application Security Best Practices).
In this article, we will learn about the best practices of Application Security in Android. By making our application more secure in android, we help preserve user trust and device integrity. This article explains about few best practices that have a significant, positive impact on our application’s security.
To understand the Application Security Best Practices, we cover the below topics as below :
- Enforce secure communication with other applications
- Provide the right permissions
- Store data safely
- Keep the services and related dependencies up-to-date
A famous quote about learning is :
“Being a student is easy. Learning requires actual work.”
So Let’s begin.
1. Enforce secure communication with other applications
When we safeguard the data that we want to exchange between our application and other applications, or between our application and a website, we improve our application’s stability and protect the data that we want to send and receive.
1.1 Use implicit intents and non-exported content providers
1.1.1 Show an application chooser
Use implicit intents to show application chooser that provides option to user to launch at least two possible applications on the device for the requested action. This allows users to transfer sensitive information to the application that they trust.
val intent = Intent(Intent.ACTION_SEND)
val possibleActivitiesList: List<ResolveInfo> =
packageManager.queryIntentActivities(intent, PackageManager.MATCH_ALL)
// Verify that an activity in at least two applications on the user's device
// can handle the intent. Otherwise, start the intent only if an application
// on the user's device can handle the intent.
if (possibleActivitiesList.size > 1) {
// Create intent to show chooser.
// Title is something similar to "Share this photo with".
val chooser = resources.getString(R.string.chooser_title).let { title ->
Intent.createChooser(intent, title)
}
startActivity(chooser)
} else if (intent.resolveActivity(packageManager) != null) {
startActivity(intent)
}
1.1.2 Apply signature-based permissions
Apply signature-based permissions while sharing data between two applications that is controlled by us. These permissions do not need user confirmation, but instead it checks that the applications accessing the data are signed using the same signing key. Hence offer more streamlined and secure user experience.
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.myapp">
<permission android:name="my_custom_permission_name"
android:protectionLevel="signature" />
1.1.3 Disallow access to our application’s content providers
Unless we intend to send data from our application to a different application that we don’t own, we should explicitly disallow other developers’ apps from accessing the ContentProvider
objects that our application contains. This setting is particularly important if our application can be installed on devices running Android 4.1.1 (API level 16) or lower, as the android:exported
attribute of the <provider>
element is true
by default on those versions of Android.
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.myapp">
<application ... >
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="com.example.myapp.fileprovider"
...
android:exported="false">
<!-- Place child elements of <provider> here. -->
</provider>
...
</application>
</manifest>
1.2 Ask for credentials before showing sensitive information
When we are requesting the credentials so that we can access sensitive information or premium content in our application, ask for either a PIN/password/pattern or a biometric credential, such as using face recognition or fingerprint recognition.
1.3 Apply network security measures
Ensure network security with Security with HTTPS and SSL — For any kind of network communication we must use HTTPS (instead of plain http) with proper certificate implementation. This section describes how we can improve our application’s network security.
1.3.1 Use SSL traffic
If our application communicates with a web server that has a certificate issued by a well-known, trusted CA, the HTTPS request is very simple:
val url = URL("https://www.google.com")
val urlConnection = url.openConnection() as HttpsURLConnection
urlConnection.connect()
urlConnection.inputStream.use {
...
}
1.3.2 Add a network security configuration
If our application uses new or custom CAs, we can declare our network’s security settings in a configuration file. This process allows us to create the configuration without modifying any application code.
To add a network security configuration file to our application, we can follow these steps:
- Declare the configuration in our application’s manifest:
<manifest ... >
<application
android:networkSecurityConfig="@xml/network_security_config"
... >
<!-- Place child elements of <application> element here. -->
</application>
</manifest>
2. We add an XML resource file, located at res/xml/network_security_config.xml
.
Specify that all traffic to particular domains should use HTTPS by disabling clear-text:
<network-security-config>
<domain-config cleartextTrafficPermitted="false">
<domain includeSubdomains="true">secure.example.com</domain>
...
</domain-config>
</network-security-config>
During the development process, we can use the <debug-overrides>
element to explicitly allow user-installed certificates. This element overrides our application’s security-critical options during debugging and testing without affecting the application’s release configuration.
The following snippet shows how to define this element in our application’s network security configuration XML file:
<network-security-config>
<debug-overrides>
<trust-anchors>
<certificates src="user" />
</trust-anchors>
</debug-overrides>
</network-security-config>
1.3.3 Create own trust manager
Our SSL checker shouldn’t accept every certificate. We may need to set up a trust manager and handle all SSL warnings that occur if one of the following conditions applies to our use case:
- We’re communicating with a web server that has a certificate signed by a new or custom CA.
- That CA isn’t trusted by the device we’re using.
- You cannot use a network security configuration.
1.4 Use WebView objects carefully
Whenever possible, we load only allowlisted content in WebView
objects. In other words, the WebView
objects in our application shouldn’t allow users to navigate to sites that are outside of our control. In addition, we should never enable JavaScript interface support unless we completely control and trust the content in our application’s WebView
objects.
1.4.1 Use HTML message channels
If our application must use JavaScript interface support on devices running Android 6.0 (API level 23) and higher, use HTML message channels instead of communicating between a website and your app, as shown in the following code snippet:
val myWebView: WebView = findViewById(R.id.webview)
// channel[0] and channel[1] represent the two ports.
// They are already entangled with each other and have been started.
val channel: Array<out WebMessagePort> = myWebView.createWebMessageChannel()
// Create handler for channel[0] to receive messages.
channel[0].setWebMessageCallback(object : WebMessagePort.WebMessageCallback() {
override fun onMessage(port: WebMessagePort, message: WebMessage) {
Log.d(TAG, "On port $port, received this message: $message")
}
})
// Send a message from channel[1] to channel[0].
channel[1].postMessage(WebMessage("My secure message"))
2. Provide the right permissions
Application should request only the minimum number of permissions necessary to function properly.
2.1 Use intents to defer permissions
It should not add a permission to complete an action that could be completed in another application. Instead, we use an intent to defer the request to a different application that already has the necessary permission.
For example, If an application requires to create a contact to a contact application, it delegates the responsibility of creating the contact to a contacts application, which has already been granted the appropriate WRITE_CONTACTS permission.
// Delegates the responsibility of creating the contact to a contacts application,
// which has already been granted the appropriate WRITE_CONTACTS permission.
Intent(Intent.ACTION_INSERT).apply {
type = ContactsContract.Contacts.CONTENT_TYPE
}.also { intent ->
// Make sure that the user has a contacts application installed on their device.
intent.resolveActivity(packageManager)?.run {
startActivity(intent)
}
}
In addition, if our application needs to perform file-based I/O – such as accessing storage or choosing a file – it doesn’t need special permissions because the system can complete the operations on our application’s behalf. Better still, after a user selects content at a particular URI, the calling application gets granted permission to the selected resource.
2.2 Share data securely across applications
We can follow these best practices in order to share our application’s content with other applications in a more secure manner:
- Enforce read-only or write-only permissions as needed.
- Provide clients one-time access to data by using the
FLAG_GRANT_READ_URI_PERMISSION
andFLAG_GRANT_WRITE_URI_PERMISSION
flags. - When sharing data, we use “content://” URIs, not “file://” URIs. Instances of
FileProvider
do this for us.
The following code snippet shows how to use URI permission grant flags and content provider permissions to display an application’s PDF file in a separate PDF Viewer application:
// Create an Intent to launch a PDF viewer for a file owned by this application.
Intent(Intent.ACTION_VIEW).apply {
data = Uri.parse("content://com.example/personal-info.pdf")
// This flag gives the started application read access to the file.
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}.also { intent ->
// Make sure that the user has a PDF viewer application installed on their device.
intent.resolveActivity(packageManager)?.run {
startActivity(intent)
}
}
3. Store data safely
Although our application might require access to sensitive user information, our users will grant our application access to their data only if they trust that we’ll safeguard it properly.
3.1 Store private data within internal storage
We need to store all private user data within the device’s internal storage, which is sandboxed per application. Our application doesn’t need to request permission to view these files, and other applications cannot access the files. As an added security measure, when the user uninstalls an app, the device deletes all files that the app saved within internal storage.
We consider working with EncryptedFile objects if storing data is particularly sensitive or private. These objects are available from Security library instead of File objects.
For Example, one way to write data to storage demonstrates in the below code snippet:
// Although you can define your own key generation parameter specification, it's
// recommended that you use the value specified here.
val keyGenParameterSpec = MasterKeys.AES256_GCM_SPEC
val mainKeyAlias = MasterKeys.getOrCreate(keyGenParameterSpec)
// Create a file with this name, or replace an entire existing file
// that has the same name. Note that you cannot append to an existing file,
// and the file name cannot contain path separators.
val fileToWrite = "my_sensitive_data.txt"
val encryptedFile = EncryptedFile.Builder(
File(DIRECTORY, fileToWrite),
applicationContext,
mainKeyAlias,
EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB
).build()
val fileContent = "MY SUPER-SECRET INFORMATION"
.toByteArray(StandardCharsets.UTF_8)
encryptedFile.openFileOutput().apply {
write(fileContent)
flush()
close()
}
Another example shows the inverse operation, reading data from storage:
// Although you can define your own key generation parameter specification, it's
// recommended that you use the value specified here.
val keyGenParameterSpec = MasterKeys.AES256_GCM_SPEC
val mainKeyAlias = MasterKeys.getOrCreate(keyGenParameterSpec)
val fileToRead = "my_sensitive_data.txt"
val encryptedFile = EncryptedFile.Builder(
File(DIRECTORY, fileToRead),
applicationContext,
mainKeyAlias,
EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB
).build()
val inputStream = encryptedFile.openFileInput()
val byteArrayOutputStream = ByteArrayOutputStream()
var nextByte: Int = inputStream.read()
while (nextByte != -1) {
byteArrayOutputStream.write(nextByte)
nextByte = inputStream.read()
}
val plaintext: ByteArray = byteArrayOutputStream.toByteArray()
3.2 Store data in external storage based on use case
We consider external storage for large, non-sensitive files that are specific to our application, as well as files that our application shares with other applications. The specific APIs that we use depend on whether our application is designed to access app-specific files or access shared files.
3.2.1 Check availability of storage volume
When user interacts with a removable external storage device from the application, then he might remove the storage device while our app is trying to access it. We need to include logic to verify that the storage device is available.
3.2.2 Access application-specific files
If a file doesn’t contain private or sensitive information but provides value to the user only in our application, we store the file in an application-specific directory on external storage.
3.2.3 Access shared files
If our application needs to access or store a file that provides value to other applications, we can use one of the following APIs depending on our use case:
- Media files: To store and access images, audio files, and videos that are shared between apps, use the Media Store API.
- Other files: To store and access other types of shared files, including downloaded files, use the Storage Access Framework.
3.2.4 Check validity of data
If our application uses data from external storage, make sure that the contents of the data haven’t been corrupted or modified. Our application should also include logic to handle files that are no longer in a stable format.
We take an example of hash verifier in below code snippet:
val hash = calculateHash(stream)
// Store "expectedHash" in a secure location.
if (hash == expectedHash) {
// Work with the content.
}
// Calculating the hash code can take quite a bit of time, so it shouldn't
// be done on the main thread.
suspend fun calculateHash(stream: InputStream): String {
return withContext(Dispatchers.IO) {
val digest = MessageDigest.getInstance("SHA-512")
val digestStream = DigestInputStream(stream, digest)
while (digestStream.read() != -1) {
// The DigestInputStream does the work; nothing for us to do.
}
digest.digest().joinToString(":") { "%02x".format(it) }
}
}
3.3 Store only non-sensitive data in cache files
To provide quicker access to non-sensitive application data, we store it in the device’s cache. For caches larger than 1 MB in size, we use getExternalCacheDir()
otherwise, use getCacheDir()
. Each method provides the File
object that contains our application’s cached data.
Let’s take one example code snippet that shows how to cache a file that application recently downloaded:
val cacheFile = File(myDownloadedFileUri).let { fileToCache ->
File(cacheDir.path, fileToCache.name)
}
If we use use getExternalCacheDir()
to place our application’s cache within shared storage, the user might eject the media containing this storage while our application run. We should include logic to gracefully handle the cache miss that this user behavior causes.
3.4 Use SharedPreferences in private mode
When we are using getSharedPreferences()
to create or access our application’s SharedPreferences
objects, use MODE_PRIVATE
. That way, only our application can access the information within the shared preferences file.
Moreover, EncryptedSharedPreferences
should be used for more security which wraps the SharedPreferences
class and automatically encrypts keys and values.
4. Keep services and dependencies up-to-date
Most applications use external libraries and device system information to complete specialized tasks. By keeping our app’s dependencies up to date, we make these points of communication more secure.
4.1 Check the Google Play services security provider
If our application uses Google Play services, make sure that it’s updated on the device where our application is installed. This check should be done asynchronously, off of the UI thread. If the device isn’t up-to-date, our application should trigger an authorization error.
4.2 Update all application dependencies
Before deploying our application, make sure that all libraries, SDKs, and other dependencies are up to date:
- For first-party dependencies, such as the Android SDK, we use the updating tools found in Android Studio, such as the SDK Manager.
- For third-party dependencies, we check the websites of the libraries that our app uses, and install any available updates and security patches.
That’s all about in this article.
Related Other Articles / Posts
Conclusion
In this article, we understood about the best practices of Application Security in Android. This article explained about few best practices that every mobile app developer must follow to secure the application from vulnerability. This helps us to develop the highly secure applications required to prevent valuable user information of our application and maintain the trust of our client.
Thanks for reading! I hope you enjoyed and learned about the best practices of Application Security concepts in Android. Reading is one thing, but the only way to master it is to do it yourself.
Please follow and subscribe to the blog and support us in any way possible. Also like and share the article with others for spread valuable knowledge.
You can find other articles of CoolMonkTechie as below link :
You can also follow official website and tutorials of Android as below links :
If you have any comments, questions, or think I missed something, leave them below in the comment box.
Thanks again Reading. HAPPY READING !!???