Blog Infos
Author
Published
Topics
Published
Topics

As we saw previously, the data your app stores to external storage can be read by other apps if they have the right permissions. Extra encryption, therefore, is essential for private data.¹

Data stored to internal storage is better protected using scoped storage. But under some circumstances, even that isn’t completely safe. Rooted devices, for example, could circumvent Android’s usual protection and make internal data available to other apps². Likewise backup or transfer systems may be able to be exploited.

So in both cases it’s a good idea to use another level of encryption for particularly sensitive data, despite Android itself also encrypting its file system at a low-level.

This article, therefore, shows you how to write encrypted files and encrypted SharedPreferences using Kotlin + Compose + ViewModels.

The code for the sample app introduced here, is available in my GitHub.

Introducing Jetpack Security

The easiest way to introduce file security is using the Jetpack Security library. Install it via Gradle as below:

implementation("androidx.security:security-crypto:1.0.0")

Before we perform any encryption operations, the first thing we need is an encryption key. Jetpack Security makes this incredibly easy (trust me, the code below hides a vast amount of complexity):

private val mainKeyAlias by lazy {
// 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
MasterKeys.getOrCreate(keyGenParameterSpec)
}

This goes inside the ViewModel; I’ve made it a lazy variable so that it only gets created on first use.

Encrypt your SharedPreferences

Jetpack Security’s EncryptedSharedPreferences object is a near drop-in replacement for the standard Android SharedPreferences. Here I’ve used lazy instantiation so it’s only created when needed:

private val sharedPreferences by lazy {
//The name of the file on disk will be this, plus the ".xml" extension.
val sharedPrefsFile = "sharedPrefs"
//Create the EncryptedSharedPremises using the key above
EncryptedSharedPreferences.create(
sharedPrefsFile,
mainKeyAlias,
getApplication(),
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
}

Note that getApplication() here (which gives us our global Application object) is provided automatically by the fact that this is an AndroidViewModel.

Writing to the EncryptedSharedPreferences is done exactly as with SharedPreferences:

fun writeToSharedPrefs(value: String) {
with (sharedPreferences.edit()) {
putString("test", value)
apply()
}
}

Here we’re setting the test key to given value.

Likewise reading from EncryptedSharedPreferences is the same as for SharedPreferences:

fun readFromSharedPrefs(): String? {
return sharedPreferences.getString("test", "")
}

That second argument to getString is the default to be returned if the key doesn’t exist.

Encrypting whole files

Reading and writing encrypted files isn’t much more complex than reading and writing encrypted SharedPreferences.

Using the master key we generated earlier, we can create our EncryptedFile object as below:

private val encryptedFile by lazy {
//This is the app's internal storage folder
val baseDir = getApplication<Application>().filesDir
//The encrypted file within the app's internal storage folder
val fileToWrite = File(baseDir, "encrypted-file.txt")
//Create the encrypted file
EncryptedFile.Builder(
fileToWrite,
getApplication(),
mainKeyAlias,
EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB
).build()
}

This denotes an EncryptedFile in our app’s internal storage (i.e. Application.filesDir) and encrypted using AES-256.

Then, we can write to this file using EncryptedFile.openFileOutput() and with syntax very similar to the old Java File API:

fun writeToEncryptedFile(content: String) {
//Open the file for writing, and write our contents to it.
//Note how Kotlin's 'use' function correctly closes the resource after we've finished,
//regardless of whether or not an exception was thrown.
encryptedFile.openFileOutput().use {
it.write(content.toByteArray(StandardCharsets.UTF_8))
it.flush()
}
}

Note how this code uses Kotlin’s .use { } extension function to ensure the file we’ve opened gets closed correctly, even if an exception is thrown. How neat is that! I love Kotlin.

Reading from the file is also straightforward using EncryptedFile.openFileInput(), and very similar to the File API:

fun readFromEncryptedFile(): String {
//We will read up to the first 32KB from this file. If your file may be larger, then you
//can increase this value, or read it in chunks.
val fileContent = ByteArray(32000)
//The number of bytes actually read from the file.
val numBytesRead: Int
//Open the file for reading, and read all the contents.
//Note how Kotlin's 'use' function correctly closes the resource after we've finished,
//regardless of whether or not an exception was thrown.
encryptedFile.openFileInput().use {
numBytesRead = it.read(fileContent)
}
return String(fileContent, 0, numBytesRead)
}

Every developer has their own way of handling file reads, and yours may vary. Beware that the above code only reads the first 32KB of the file. For production code, you will also want to ensure reads don’t hold up the main thread.

Note that calling EncryptedFile.openFileInput() will throw an IOException if the file doesn’t exist.

(By the way, did you note how we were able to create numBytesRead as a val, and provide its value later? That’s Kotlin being awesome again. In many other languages, numBytesRead would have to be a var and given an initial value.)

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

Jobs

The encrypted contents

The EncryptedSharedPreferences file created above looks like this:

As you can see, both the key and the value we wrote to the file are encrypted. So it’s not even possible for an attacker to see which keys have values, let alone what those values are.

The EncryptedFile we wrote is completely unreadable without being decrypted:

Some caveats

For the most part, adding encryption using EncryptedFile and EncryptedSharedPreferences is a direct and simple replacement for the File and SharedPreferences APIs. There are some caveats, however:

  • There’s no point backing up encrypted files. The encryption key is stored in the phone’s hardware, so if you restore a phone from backup, the relevant key won’t be present.
  • Whilst this protects data from root access, the key is stored in a way that rooted apps might be able to get access to.
  • Obviously the overheads of encryption and decryption mean that reads and writes are slower. In my experience the difference isn’t noticeable unless you’re writing huge amounts of data.

Otherwise, there’s no reason not to use encryption, and it’s as simple as above.

The code for this sample app is in my GitHub.

Tom Colvin is CTO of Apptaura, the app development specialists; and Conseal Security, the mobile app security testing experts. Get in touch if I can help with any mobile security or development projects!

[1] Of course, best practice is to use internal storage for private data.

[2] Note that apps on rooted devices may be able to access your encryption key. If they can, obviously that negates the effect of encryption entirely. But since they can’t always, it’s still worth using this kind of encryption.

Previously posted on proandroiddev.com

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
It’s one of the common UX across apps to provide swipe to dismiss so…
READ MORE
blog
Hi, today I come to you with a quick tip on how to update…
READ MORE
blog
Automation is a key point of Software Testing once it make possible to reproduce…
READ MORE
blog
Drag and Drop reordering in Recyclerview can be achieved with ItemTouchHelper (checkout implementation reference).…
READ MORE

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.

Menu