6.
Security Best Practices
Written by Fuad Kamal
Security is a hot topic in mobile development, especially in larger enterprise and corporate environments. Many companies pay top dollar for security experts to run penetration tests and other security tests on their apps. Some companies license scanning tools, such as Veracode. There’s even a relatively new career path for people who have cross-domain knowledge in mobile development and security.
However, security starts with the architecture and code you write to develop your app. Following security best practices ensures your users’ safety, which is the ultimate goal. In this chapter, you’ll examine some of the techniques and best practices for securing and hardening your app.
Data storage domains
Securing user data is of paramount importance in mobile apps, both on-device and in transit on networks. Protecting access to source code, API endpoints and keys is a top security concern for corporations.
But for your users, the most critical security concern is protecting their data. You need to protect their data when transmitted to or from the app, which falls under networking. HTTPS (SSL) handles Network traffic.
You also need to protect data stored locally on mobile devices, which falls under data storage. On-device, encryption is the primary way to secure data. Since Android 5.0, the OS encrypts the contents of the user’s data partition by default.
But sometimes, you want to provide an extra layer of protection for sensitive data. For example, you may want extra protection when using shared storage and handling sensitive information, such as personally identifiable information (PII), financial records or any especially sensitive data. Also, starting with Android 10, full disk encryption is no longer an option, and your app must do some form of file encryption to secure sensitive data written to files.
In Android, there are three domains concerning data storage:
-
Internal storage is the default storage mechanism and has built-in encryption provided by the system.
-
You can also store files on external storage, such as SD Cards or even internal disk space your app considers external. If you need to support this type of storage and the data is sensitive, use the Jetpack Security Library. You’ll read about it in more detail later in this chapter.
-
Finally, content providers are an encapsulation method for data storage that provides mechanisms for apps to manage private, self-only access or access to data provided by other apps, such as getting information from Contacts or Gmail, and for sharing data with other apps. Even if your app doesn’t share data with other apps, you might use content providers to benefit from the abstraction layer. However, if you do, make sure you disallow access to your app’s content providers by setting
android:exported=falsein your app manifest file for the content provider.
Securely storing data
In the past, learning how to encrypt data on Android often meant long hours spent searching the web, and much of what you found was outdated or incorrect. Fortunately, now you can leverage Jetpack Security to easily add an extra layer of security and data protection to your apps.
The Android Keystore stores cryptographic keys in a container to make them more challenging to extract from the device. Once keys are in the keystore, you can use them for cryptographic operations in the trusted execution environment, and the key material isn’t exportable. The Android Keystore also provides options such as Strongbox to store and operate on keys in a secure hardware chip.
StrongBox Keymaster is a Hardware Abstraction Layer, or HAL, that resides in a hardware security module with its own CPU, secure storage and a true random number generator. Keep in mind, since this is a hardware feature, it’s limited to devices that support it, such as the Google Pixel series.
Jetpack Security, or JetSec, uses the Android Keystore to keep encryption keys in hardware, making unauthorized access to the key material difficult. It also provides high-level abstractions for encrypting files and shared preferences to let developers encrypt their data safely without understanding algorithms, block modes and other security field specializations.
Jetpack Security is built on top of Tink, a cross-platform Open Source library from Google that provides cryptographic APIs. Jetpack Security focuses on two primary means of securing data persisted to the device: EncryptedSharedPreferences and EncryptedFiles.
Jetpack Security uses a master key that’s hardware-backed and optionally locked by biometrics like fingerprint or face identification. This master key, in turn, secures key-sets treated per cryptographic algorithm using Tink. Biometric security, such as fingerprint authentication, isn’t part of the Jetpack Security library but rather part of the AndroidX library. They work well together to offer advanced encryption options.
Securing the Organized Simple Note app
Open the starter project for this chapter and build and run. Running the project for this chapter is best done on an Android device and not on an Android emulator due to the use of device hardware for encryption.
The Organized Simple Note app lets you create, edit and delete notes saved in the internal file system. In the options menu items, there’s an option to change the background color. You can also filter the notes by priority or sort them by specific sort order.
Select a new background color, set a sort order and one or more priority filters. Now, quit the app and rerun it.
When the app reloads, the background color you selected persists. That’s because the background color you selected saved to prefs and applied when the app reran. The sort order and priority filters reset to defaults. They weren’t stored in prefs. You’ll enable these features using encrypted shared preferences.
You may have noticed another menu item, Set Encryption Key, below the menu item for changing the background color in the overflow menu. If you tap it now, it’ll open a dialog to enter a numeric key, but currently, saving it does nothing.
The notes themselves write to disk, not to shared preferences. They’re currently not encrypted: You’ll use JetSec to encrypt the files as well.
Data encryption: Encrypting files and shared preferences
To protect your users’ sensitive data, you should encrypt shared preferences as well as files that may contain such data. Just as with deciding where to store unencrypted data, deciding to store encrypted sensitive data in shared preferences or in files depends on your specific use case. If you need to store some small amount of data in key value pairs, then EncryptedSharedPreferences is certainly the easier path to take. If you have a lot of data or complex data, go with encrypted files.
Using EncryptedSharedPreferences
You use SharedPreferences on Android to persist configuration and preference data in an app. The data stores in the form of key-value pairs. JetSec provides an encrypted version of SharedPreferences, named EncryptedSharedPreferences, that provides strong security while maintaining performance for reading the key-value data. Keys and values can both be encrypted.
To create and use EncryptedSharedPreferences, you need to provide three things:
- A master key stored in the Android keystone on your device. The master key encrypts all sub keys used for each cryptographic operation.
- A value encryption scheme such as the fast AES-256 GCM algorithm. This algorithm is also used for the master key.
- If you also want to encrypt the keys, provide a key encryption scheme. Encrypting keys is usually optional, but it’s necessary if the keys themselves hold sensitive data.
For Organized Simple Note, you’ll let the user create a key they’ll later use to encrypt the notes themselves. You’ll store the key the user creates in EncryptedSharedPreferences.
Note that the key acts like a password. In this case, it’s OK to store the encrypted key as secret data for the app. However, don’t take this approach to store user account passwords for backend systems. In that case, store a salted and hashed password on your backend and not the password itself.
Salting is the process of using random data as input to a function that hashes the password. That way, if your backend server ever has a data breach, an attacker can only get access to salted password hashes and not your users’ passwords themselves. You can learn more about salting and hashing in the first section of the Saving Data on Android book from raywenderlich.com.
Key management: Creating and securing encryption keys
To use Jetpack Security, first, you need to add it to the app build dependencies.
Open the Simple Note starter project for this chapter. Then open the project build.gradle and add the following definition to the ext{} block:
security_version = "1.0.0"
Then open the app build.gradle and add the jetpack security dependency to the dependencies block:
implementation("androidx.security:security-crypto:$security_version")
Sync the gradle dependencies.
Next, open MainActivityViewModel.kt. Create a sharedPreferences in the ViewModel that you’ll lazily instantiate by adding the code below:
private val sharedPreferences by lazy {
// 1
val masterKeyAlias = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC)
// 2
val keyEncryptionScheme = EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV
// 3
val valueEncryptionScheme = EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
// 4
EncryptedSharedPreferences.create(
ENCRYPTED_PREFS,
masterKeyAlias,
context,
keyEncryptionScheme,
valueEncryptionScheme
)
}
Hit option-return on Mac or Alt-Enter on PC to pull in the necessary import statements as needed.
In the code above, you:
- Create a master key alias that gets the master key from the Android keystore. JetSec lets you create a master key with just one line of code. By default, it uses the AES256 bit key, which is sufficient for most use cases. Google recommends tying hardware-backed keys to user presence via a biometric credential. The Android Keystore enforces biometrics at a hardware level so that keys aren’t available for use without authorizing the device. Furthermore, for time-bound master keys, the Jetpack biometric prompt is a quick and easy way to show a backward-compatible biometric check on the device.
- Set up an encryption scheme for the preference keys. You use the AES256_SIV scheme for the keys. Encrypting keys is optional but necessary if the keys themselves hold sensitive data.
- Add a value encryption scheme. You use AES256_GCM for the encryption values, the same algorithm used by default for the master key.
- Finally, call
createon the encrypted shared preferences class to create the shared preferences instance. You pass in the values you created, along with a constant name for the preferences in the starter project and a context object from the view model.
MainActivity uses getEncryptionKey() and setEncryptionKey() in the ViewModel when the user interacts with the Set Encryption Key menu item you examined earlier.
The starter project’s ViewModel class has stubs for those methods.
You can interact with the encrypted shared preferences in the same way you do with normal shared preferences, calling get methods to get values and put methods to save values. Replace the contents of getEncryptionKey() with the code below:
fun getEncryptionKey(): String? {
return sharedPreferences.getString(
ENCRYPTED_PREFS_ENCRYPTION_KEY,
null
)
}
In the code above, you make a call to getString on sharedPreferences, passing in a key string named ENCRYPTED_PREFS_ENCRYPTION_KEY, the key for the log key value. You also set the default value on the getString call to null, so if there’s no log key saved in encrypted shared preferences, this method will return null.
Replace the contents of setEncryptionKey() with:
if (current != getEncryptionKey()) {
_snackbar.value = context.getString(R.string.error_current_encryption_key_incorrect)
return
}
//1
if (new.isNullOrBlank()) {
//2
sharedPreferences.edit().putString(
ENCRYPTED_PREFS_ENCRYPTION_KEY,
null
).apply()
_snackbar.value = context.getString(
R.string.message_encryption_key_cleared
)
} else {
//3
sharedPreferences.edit().putString(
ENCRYPTED_PREFS_ENCRYPTION_KEY,
new
).apply()
_snackbar.value = context.getString(
R.string.message_encryption_key_set
)
}
In the code above, you:
- Add a conditional to first check whether a new log key being set is null or blank. If it is, clear the log key by sending null to put the string and then update the
_snackbarLiveData value in the view model. - Just as you would with typical shared preferences, you use
editandapplycalls on the encrypted shared preferences. - If the new log key isn’t null or blank, set the new value using
putString. Apply the change and update the SnackBar.
Now build and run. From the overflow menu, tap Set Encryption Key. Set an encryption key in the dialogue. Then, tap Set Encryption Key again, enter your original key and enter a new key.
Encrypted files
For persisting and securing the primary app data with Jetpack Security, use encrypted files. Both files and shared preferences are abstractions that provide authenticated encryption with associated data, or AEAD. AEAD ensures both the confidentiality and integrity of data.
In the past, encrypting files was challenging because of size limitations. You had to load the entire file into memory. EncryptedFile, the Android class used to create and read encrypted files, uses streaming AES encryption to handle files of all sizes. The only limiting factor is the remaining disk space.
You can treat EncryptedFile similar to a standard file in Android. Once you create the encrypted file object, you can operate on the file with the provided file input/output stream APIs.
The API for getting an encrypted file is similar to getting shared preferences: You need a master key and an encryption scheme. You also need to specify a filename that acts similarly to a key in a key-value pair, uniquely identifying the file in the file system.
Reading an encrypted file
Open InternalFileRepository.kt and add the following function:
private fun getEncryptedEntry(name: String): EncryptedFile {
// 1
val keyGenParameterSpec = MasterKeys.AES256_GCM_SPEC
val masterKeyAlias = MasterKeys.getOrCreate(keyGenParameterSpec)
// 2
val fileEncryptionScheme = EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB
// 3
return EncryptedFile.Builder(
// 4
File(context.filesDir, name.urlEncode()),
// 5
context,
masterKeyAlias,
fileEncryptionScheme
// 6
).build()
}
Here’s a code breakdown:
- As with encrypted preferences, you need a master key to access encrypted files.
keyGenParameterSpecis the same value you used for the master key earlier. - Next, like you did for keys and values for encrypted shared preferences, you add a file encryption scheme. You use the AES256_GCM_HKDF_4KB algorithm to encrypt files.
- Then, you use the builder pattern to return an encrypted file.
- In the builder, you first pass in a file object using the context files directory and the file’s name. You use a helper method from the starter project to URL encode the filename, so you don’t run into problems with any input data the user chooses to use for the filename.
- Next, you pass the builder the context, the master key alias and the file encryption scheme.
- Finally, call build on the builder to finish retrieving the encrypted file.
Next, you need to write data to the encrypted file and then read the encrypted data back in. In addNote() replace this line:
context.openFileOutput(note.fileName, Context.MODE_PRIVATE).use{ output ->
output.write(text.toByteArray())
}
With the following try/catch block:
try {
// 1
val encryptedFile = getEncryptedEntry(note.fileName)
encryptedFile.openFileOutput().use { output ->
output.write(text.toByteArray())
}
} catch (e: Exception) {
// 2
e.printStackTrace()
_snackbar.value = context.getString(R.string.error_unable_to_save_file)
}
Here’s a quick breakdown:
- In the try block, you attempt to write to the file. You obtain a handle to access the file.
openFileOutput()grants output access to the file, and then you pass in a Lambda to actually write to the file. - You handle any exceptions that occur while trying to write to the file. This example uses a general exception, but you might want to use an IO exception instead.
The method to read from an encrypted file is similar to the operation for writing to the file. Update getNote() as follows:
override fun getNote(fileName: String): Note {
val note = Note(fileName, "", 0)
try {
context.openFileInput(fileName).use { stream ->
val text = stream.bufferedReader().use {
it.readText()
}
note.dateModified = Date(noteFile(fileName).lastModified())
note.priority = text.takeLast(1).toInt()
note.noteText = text.dropLast(1)
return note
}
} catch (e:Exception) {
e.printStackTrace()
_snackbar.value = context.getString(R.string.error_unable_to_decrypt)
return note
}
}
In the code above, the main difference is that you wrap the file reading operation in a try-catch block.
Build and run. Try creating a new note, and then try to read it back. Congratulations, now you know how to securely encrypt your users’ data!
Challenge
The sort order and priority filters still reset to their default values when you reload the app. They didn’t store in EncryptedSharedPreferences yet. As a challenge, update the app so that these values persist in EncryptedSharedPreferences.
Key points
- Adding Jetpack Security to your project is quick and easy. The defaults work great out of the box.
- Encrypted files use AES encryption to handle files of all sizes.
- You use an encrypted file like a standard file with minor exceptions.
- Keys for
EncryptedSharedPreferencesare encrypted deterministically. -
EncryptedSharedPreferencesimplementSharedPreferencesfor interoperability.
Where to go from here?
The book Saving Data on Android is an in-depth exploration of the various ways to store data on a device. You can find more information about shared preferences, as well as the various storage scopes and domains on Android, here: https://www.raywenderlich.com/books/saving-data-on-android/
You may also want to review:
- raywenderlich.com course on Jetpack Security: https://www.raywenderlich.com/10135609-jetpack-security
- Data Privacy for Android tutorial on Ray Wenderlich: https://www.raywenderlich.com/6901838-data-privacy-for-android
- The book Real-World Android by Tutorials has an entire section on “Securing your App”: https://www.raywenderlich.com/books/real-world-android-by-tutorials/v1.0/
- Securing your app for work - Enterprise Dev Training: https://www.youtube.com/watch?v=2y9Ol2N1I4k
- Android Keystore Documentation: https://developer.android.com/training/articles/keystore
- EncryptedFile documentation: https://developer.android.com/reference/androidx/security/crypto/EncryptedFile