Encryption Tutorial For Android: Getting Started

Ever wondered how you can use data encryption to secure your private user data from hackers? Look no more, in this tutorial you’ll do just that!

With all the recent data breaches and new privacy laws, such as GDPR, your app’s credibility depends on how you manage your user’s data. There are powerful Android APIs focusing on data encryption that are sometimes overlooked when beginning a project. You can put them to great use and think of security from the ground up.

In this tutorial, you’ll secure an app for veterinary clinics that store medical information. During the process, you’ll learn how to:

  • Tighten app permissions
  • Encrypt your data
  • Use the KeyStore

Note: This tutorial assumes that you’re already familiar with the basics of Android development and Android Studio. If Android development is new to you, first read through the Beginning Android Development and Kotlin for Android tutorials.

Getting Started

Download the starter project by clicking the Download Materials button at the top or bottom of this tutorial. Take a moment to familiarize yourself with the structure of the project. Build and run the app to see what you’re working with.

You’ll see a simple sign-up screen. Once you enter a password and choose Signup, you’ll be prompted for that password on subsequent app launches. After that step, you’ll get a list of pets. Most of the app is complete, so you’ll focus on securing it. Tap an entry in the list to reveal the pet’s medical information:

Pet details screen

If on Android 7+, you get a crash with error java.lang.SecurityException: MODE_WORLD_READABLE no longer supported, don’t worry. You’ll fix it soon.

Securing the foundations

To begin encrypting your applications, and securing important data, you first have to prevent leaking data to the rest of the world. When it comes to Android, this usually means protecting your user-based data from being read by any other application, and limiting the location where the applications are installed. Let’s do this first, so you can start encrypting private information.

Using Permissions

When you first start out to build your app, it’s important to think about how much user-data you actually need to keep. These days, the best practice is to avoid storing private data if you don’t have to — especially for our cute little Lightning, who is concerned about his privacy.

Ever since Android 6.0, files and SharedPreferences you save are set with the MODE_PRIVATE constant. That means only your app can access the data. Android 7 doesn’t allow any other option. So first things first, you’ll make sure the project is set up securely.

Open the MainActivity.kt file. You’ll notice there are two deprecation warnings for MODE_WORLD_READABLE and MODE_WORLD_WRITABLE. These allow public access to your files on earlier Android versions. Find the line that sets MODE_WORLD_WRITABLE and replace it with the following:

val preferences = getSharedPreferences("MyPrefs", Context.MODE_PRIVATE)

Then, find the line that sets MODE_WORD_READABLE and replace it with this:

val editor = getSharedPreferences("MyPrefs", Context.MODE_PRIVATE).edit()

Great, you’ve just made your preferences a bit safer! Additionally, if you build and run the application now, you shouldn’t get the crash you previously encountered, due to security violations of Android 7+ versions. You should now enforce a secure location for your app install directory.

Limiting installation directories

One of the bigger problems Android faced in the past few years is not having enough memory to install a lot of applications. This was mostly due to lower storage capacity of devices, but since technology has advanced, and phones had become somewhat cheaper, most devices now pack plenty of storage for a plethora of apps. However, to mitigate insufficient storage, Android allows you to install apps to external storage. This worked pretty well, but over the years, a lot of security concerns have been raised around this approach. Installing applications on external SD cards is a cool way to conserve storage, but also a security flaw, since anyone with the access to the SD card also has access to the application data. And that data could hold sensitive information. This is why it’s encouraged to restrict your app to internal storage.

To do this, ppen the AndroidManifest.xml file and find the line that reads android:installLocation="auto" and replace it with the following:

android:installLocation="internalOnly"

Now, the install location is limited to the device, but you can still back up your app and its data. This means that users can access the contents of the app’s private data folder using adb backup. To disallow backups, find the line that reads android:allowBackup="true" and replace the value with "false".

Following these best practices, you’ve hardened your app to some extent. However, you can bypass these permission measures on a rooted device. The solution is to encrypt the data with a piece of information which potential attackers cannot find.

Securing User Data With a Password

Device lock screen

You’ll encrypt the data with a well-known recommended standard, Advanced Encryption Standard (AES). AES uses substitution–permutation network to encrypt your data with a key. Using this approach, it replaces bytes from one table with the bytes from another, and as such creates permutations of data. To begin using AES, you have to first create the encryption key, so let’s do that.

Creating a Key

As mentioned above, AES uses a key for encryption. That same key is also used to decrypt the data. This is called symmetric encryption. The key can be different lengths, but 256 bits is standard. Directly using the user’s password for encryption is dangerous. It likely won’t be random or large enough. As such the user password is different from the encryption key.

A function called Password-Based Key Derivation Function (PBKDF2) comes to the rescue. It takes a password and, by hashing it with random data many times over, it creates a key. The random data is called salt. This creates a strong and unique key, even if someone else uses the same password.

PBKDF2 diagram

Because each key is unique, if an attacker steals and publishes the key online, it doesn’t expose all the users that used the same password.

Start by generating the salt. Open up the Encryption.kt file, and add the following code to the first encrypt method, where it reads //TODO: Add code here:

val random = SecureRandom()
val salt = ByteArray(256)
random.nextBytes(salt)

Here, you use the SecureRandom class, which makes sure that the output is difficult to predict. That’s called a cryptographically strong random number generator.

Now, you’ll generate a key with the user’s password and the salt. Add the following right under the code you just added:

val pbKeySpec = PBEKeySpec(password, salt, 1324, 256) // 1
val secretKeyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1") // 2
val keyBytes = secretKeyFactory.generateSecret(pbKeySpec).encoded // 3
val keySpec = SecretKeySpec(keyBytes, "AES") // 4

Here’s what is going on inside that code. You:

  1. Put the salt and password into PBEKeySpec, a password-based encryption object. The constructor takes an iteration count (1324). The higher the number, the longer it would take to operate on a set of keys during a brute force attack.
  2. Passed PBEKeySpec into the SecretKeyFactory.
  3. Generated the key as a ByteArray.
  4. Wrapped the raw ByteArray into a SecretKeySpec object.

Note: For the password, most of these functions work with a CharArray instead of String objects. That’s because objects like String are immutable. A CharArray can be overwritten, which allows you to erase the sensitive information from memory after you’re done with it.

Adding an Initialization Vector

You’re almost ready to encrypt the data, but there’s one more thing to do.

AES works in different modes. The standard mode is called cipher block chaining (CBC). CBC encrypts your data one chunk at a time.

CBC is secure because each block of data in the pipeline is XOR’d with the previous block that it encrypted. This dependency on previous blocks makes the encryption strong, but can you see a problem? What about the first block?

If you encrypt a message that starts off the same as another message, the first encrypted block would be the same! That provides a clue for an attacker. To remedy this, you’ll use an initialization vector (IV).

An IV is a fancy term for a block of random data that gets XOR’d with that first block. Remember that each block relies on all blocks processed up until that point. This means that identical sets of data encrypted with the same key will not produce identical outputs.

Create an IV now by adding the following code right after the code you just added:

val ivRandom = SecureRandom() //not caching previous seeded instance of SecureRandom
// 1
val iv = ByteArray(16)
ivRandom.nextBytes(iv)
val ivSpec = IvParameterSpec(iv) // 2

Here, you:

  1. Created 16 bytes of random data.
  2. Packaged it into an IvParameterSpec object.

Note: On Android 4.3 and under, there was a vulnerability with SecureRandom. It had to do with improper initialization of the underlying pseudorandom number generator (PRNG). A fix is available if you support Android 4.3 and under.

Encrypting the Data

Encryption icon

Now that you have all the necessary pieces, add the following code to perform the encryption:

val cipher = Cipher.getInstance("AES/CBC/PKCS7Padding") // 1
cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec)
val encrypted = cipher.doFinal(dataToEncrypt) // 2

Here:

  1. You passed in the specification string “AES/CBC/PKCS7Padding”. It chooses AES with cipher block chaining mode. PKCS7Padding is a well-known standard for padding. Since you’re working with blocks, not all data will fit perfectly into the block size, so you need to pad the remaining space. By the way, blocks are 128 bits long and AES adds padding before encryption.
  2. doFinal does the actual encryption.

Next, add the following:

  map["salt"] = salt
  map["iv"] = iv
  map["encrypted"] = encrypted

You packaged the encrypted data into a HashMap. You also added the salt and initialization vector to the map. That’s because all those pieces are necessary to decrypt the data.

If you followed the steps correctly, you shouldn’t have any errors and the encrypt function is ready to secure some data! It’s OK to store salts and IVs, but reusing or sequentially incrementing them weakens the security. But you should never store the key! Right about now, you built the means of encrypting data, but to read it later on, you still need to decrypt it. Let’s see how to do that.

Decrypting the Data

Decryption icon

Now, you’ve got some encrypted data. In order to decrypt it, you’ll have to change the mode of Cipher in the init method from ENCRYPT_MODE to DECRYPT_MODE. Add the following to the decrypt method in the Encryption.kt file, right where the line reads //TODO: Add code here:

// 1
val salt = map["salt"]
val iv = map["iv"]
val encrypted = map["encrypted"]

// 2
//regenerate key from password
val pbKeySpec = PBEKeySpec(password, salt, 1324, 256)
val secretKeyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1")
val keyBytes = secretKeyFactory.generateSecret(pbKeySpec).encoded
val keySpec = SecretKeySpec(keyBytes, "AES")

// 3
//Decrypt
val cipher = Cipher.getInstance("AES/CBC/PKCS7Padding")
val ivSpec = IvParameterSpec(iv)
cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec)
decrypted = cipher.doFinal(encrypted)

In this code, you did the following:

  1. Used the HashMap that contains the encrypted data, salt and IV necessary for decryption.
  2. Regenerated the key given that information plus the user’s password.
  3. Decrypted the data and returned it as a ByteArray.

Notice how you used the same configuration for the decryption, but have traced your steps back. This is because you’re using a symmetric encryption algorithm. You can now encrypt data as well as decrypt it!

Oh and, did I mention never to store the key? :]

Saving Encrypted Data

Now that the encryption process is complete, you’ll need to save that data. The app is already reading and writing data to storage. You’ll update those methods to make it work with encrypted data.

In the MainActivity.kt file, replace everything inside the createDataSource method with this:

val inputStream = applicationContext.assets.open(filename)
val bytes = inputStream.readBytes()
inputStream.close()

val password = CharArray(login_password.length())
login_password.text.getChars(0, login_password.length(), password, 0)
val map = Encryption().encrypt(bytes, password)
ObjectOutputStream(FileOutputStream(outFile)).use {
  it -> it.writeObject(map)
}

In the updated code, you opened the data file as an input stream and fed the data into the encryption method. You serialized the HashMap using the ObjectOutputStream class and then saved it to storage.

Build and run the app. Notice the pets are now missing from the list:

Empty list

Keys

That’s because the saved data is encrypted. You’ll need to update the code to be able to read the encrypted content. In the loadPets method of the PetViewModel.kt file, remove the /* and */ comment markers. Then, add the following code right where it reads //TODO: Add decrypt call here:

decrypted = Encryption().decrypt(
    hashMapOf("iv" to iv, "salt" to salt, "encrypted" to encrypted), password)

You called the decrypt method using the encrypted data, IV and salt. Now that the input stream comes from a ByteArray rather than File, replace the line that reads val inputStream = file.inputStream() with this one:

val inputStream = ByteArrayInputStream(decrypted)

If you build and run the application now, you should see a couple of friendly faces!

The data is now secure, but there’s another common place for user data to be stored on Android, the SharedPreferences, which you’ve already used.

Securing the SharedPreferences

The app also keeps track of the last access time in the SharedPreferences, so it’s another spot in the app to protect. Storing sensitive information in the SharedPreferences can be insecure, because you could still leak the information from within your app, even with the Context.MODE_PRIVATE flag. You’ll fix that in a bit.

Open up the MainActivity.kt file, and replace the saveLastLoggedInTime method with this code:

//Get password
val password = CharArray(login_password.length())
login_password.text.getChars(0, login_password.length(), password, 0)

//Base64 the data
val currentDateTimeString = DateFormat.getDateTimeInstance().format(Date())
// 1
val map =
    Encryption().encrypt(currentDateTimeString.toByteArray(Charsets.UTF_8), password)
// 2
val valueBase64String = Base64.encodeToString(map["encrypted"], Base64.NO_WRAP)
val saltBase64String = Base64.encodeToString(map["salt"], Base64.NO_WRAP)
val ivBase64String = Base64.encodeToString(map["iv"], Base64.NO_WRAP)

//Save to shared prefs
val editor = getSharedPreferences("MyPrefs", Context.MODE_PRIVATE).edit()
// 3
editor.putString("l", valueBase64String)
editor.putString("lsalt", saltBase64String)
editor.putString("liv", ivBase64String)
editor.apply()

Here, you:

  1. Converted the String into a ByteArray with the UTF-8 encoding and encrypted it. In the previous code you opened a file as binary, but in the case of working with strings, you’ll need to take the character encoding into account.
  2. Converted the raw data into a String representation. SharedPreferences can’t store a ByteArray directly but it can work with String. Base64 is a standard that converts raw data to a string representation.
  3. Saved the strings to the SharedPreferences. You can optionally encrypt both the preference key and the value. That way, an attacker can’t figure out what the value might be by looking at the key, and using keys like “password” won’t work for brute forcing, since that would be encrypted as well.

Now, replace the lastLoggedIn method to get the encrypted bytes back:

//Get password
val password = CharArray(login_password.length())
login_password.text.getChars(0, login_password.length(), password, 0)

//Retrieve shared prefs data
// 1
val preferences = getSharedPreferences("MyPrefs", Context.MODE_PRIVATE)
val base64Encrypted = preferences.getString("l", "")
val base64Salt = preferences.getString("lsalt", "")
val base64Iv = preferences.getString("liv", "")

//Base64 decode
// 2
val encrypted = Base64.decode(base64Encrypted, Base64.NO_WRAP)
val iv = Base64.decode(base64Iv, Base64.NO_WRAP)
val salt = Base64.decode(base64Salt, Base64.NO_WRAP)

//Decrypt
// 3
val decrypted = Encryption().decrypt(
    hashMapOf("iv" to iv, "salt" to salt, "encrypted" to encrypted), password)

var lastLoggedIn: String? = null
decrypted?.let {
  lastLoggedIn = String(it, Charsets.UTF_8)
}
return lastLoggedIn

You did the following:

  1. Retrieved the string representations for the encrypted data, IV and salt.
  2. Applied a Base64 decode on the strings to convert them back to raw bytes.
  3. Passed that data in a HashMap to the decrypt method.

Now that you have storage set up safely, start fresh by navigating to SettingsAppsPetMed 2StorageClear data.

Build and run the app. If everything worked, after signing in you should see the pets back on the screen again. Esther is happy that her private data is encrypted. :]

Pet list screen

Using a Key From a Server

You’ve just completed a great real-world example, but a lot of apps require a good on-boarding experience. Showing a password screen in addition to a login screen might not be a good user experience. For requirements such as this, you have some options.

The first is to take advantage of a login password to derive the key. You can also have the server generate that key instead. The key would be unique, and transmitted securely once the user has authenticated with their credentials.

If you’re going the server route, it’s important to know that since the server generates the key, it has the capacity to decrypt data stored on the device. There is the potential for someone to leak the key.

If none of these solutions work for you, you can take advantage of device security to secure the app.

Using the KeyStore

Android M introduced the ability to work with an AES key using the KeyStore API. This has some added benefits. The KeyStore allows you to operate on a key without revealing it’s secret content. Only the object, and not the private data, is accessible from app space.

Generating a New Random Key

Keys

In the Encryption.kt file, add the following code to the keystoreTest method to generate a random key. This time around, the KeyStore protects the key:

val keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore") // 1
val keyGenParameterSpec = KeyGenParameterSpec.Builder("MyKeyAlias",
    KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
    .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
    .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
    //.setUserAuthenticationRequired(true) // 2 requires lock screen, invalidated if lock screen is disabled
    //.setUserAuthenticationValidityDurationSeconds(120) // 3 only available x seconds from password authentication. -1 requires finger print - every time
    .setRandomizedEncryptionRequired(true) // 4 different ciphertext for same plaintext on each call
    .build()
keyGenerator.init(keyGenParameterSpec)
keyGenerator.generateKey()

Here’s what’s going on inside the code:

  1. You created a KeyGenerator instance and set it to the “AndroidKeyStore” provider.
  2. Optionally, you added .setUserAuthenticationRequired(true) requiring a lock screen to be set up.
  3. You optionally added .setUserAuthenticationValidityDurationSeconds(120) so that the key is available 120 seconds after device authentication.
  4. You called .setRandomizedEncryptionRequired(true). This tells the KeyStore to use a new IV each time. As you learned earlier, that means that if you encrypt identical data a second time, the encrypted output will not be identical. It prevents attackers from obtaining clues about the encrypted data based on feeding in the same inputs.

There are a few other things about the KeyStore options you should know about:

  1. For .setUserAuthenticationValidityDurationSeconds(), you can pass -1 to require fingerprint authentication every time you want access to the key.
  2. Enabling the screen lock requirements will revoke keys as soon as the user removes or changes the lock screen pin or passcode.
  3. Storing a key in the same place as the encrypted data is like putting a key under the doormat. The KeyStore tries to protect the key with strict permissions and kernel level code. On some devices, keys are hardware backed.
  4. You can use .setUserAuthenticationValidWhileOnBody(boolean remainsValid). This makes the key unavailable once the device has detected it is no longer on the person.

Encrypting the Data

Now, you’ll make use of that key that’s stored in the KeyStore. In the Encryption.kt file, add the following to the keystoreEncrypt method, right under the //TODO: Add code here:

// 1
//Get the key
val keyStore = KeyStore.getInstance("AndroidKeyStore")
keyStore.load(null)

val secretKeyEntry =
    keyStore.getEntry("MyKeyAlias", null) as KeyStore.SecretKeyEntry
val secretKey = secretKeyEntry.secretKey

// 2
//Encrypt data
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
cipher.init(Cipher.ENCRYPT_MODE, secretKey)
val ivBytes = cipher.iv
val encryptedBytes = cipher.doFinal(dataToEncrypt)

// 3
map["iv"] = ivBytes
map["encrypted"] = encryptedBytes

Here:

  1. This time, you retrieve the key from the KeyStore.
  2. You encrypted the data using the Cipher object, given the SecretKey.
  3. Like before, you return a HashMap containing the encrypted data and IV needed to decrypt the data.

Decrypting to a Byte Array

Add the following to the keystoreDecrypt method, right under the //TODO: Add code here:

// 1
//Get the key
val keyStore = KeyStore.getInstance("AndroidKeyStore")
keyStore.load(null)

val secretKeyEntry =
    keyStore.getEntry("MyKeyAlias", null) as KeyStore.SecretKeyEntry
val secretKey = secretKeyEntry.secretKey

// 2
//Extract info from map
val encryptedBytes = map["encrypted"]
val ivBytes = map["iv"]

// 3
//Decrypt data
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
val spec = GCMParameterSpec(128, ivBytes)
cipher.init(Cipher.DECRYPT_MODE, secretKey, spec)
decrypted = cipher.doFinal(encryptedBytes)

In this code, you:

  1. Obtained the key again from the KeyStore.
  2. Extracted the necessary info from map.
  3. Initialized the Cipher object using the DECRYPT_MODE constant and decrypted the data to a ByteArray.

Testing the Example

Now that you’ve created the ways to encrypt and decrypt data using the KeyStore API, it’s time to test them out. Add the following to the end of the keystoreTest method:

// 1
val map = keystoreEncrypt("My very sensitive string!".toByteArray(Charsets.UTF_8))
// 2
val decryptedBytes = keystoreDecrypt(map)
decryptedBytes?.let {
  val decryptedString = String(it, Charsets.UTF_8)
  Log.e("MyApp", "The decrypted string is: $decryptedString")
}

In the updated code, you:

  1. Created a test string and encrypted it.
  2. Called the decrypt method on the encrypted output to test that everything worked.

In the onCreate method of the MainActivity.kt file, uncomment the line that reads //Encryption().keystoreTest(). Build and run the app to check that it worked. You should see the decrypted string:

Console success output

Where to Go From Here?

Congratulations, you’ve learned the ways of encrypting and decrypting data on Android!
Happy data

You also learned other ways of working with keys using the Keystore. You can download the final project using the Download Materials button at the top or bottom of this tutorial, if you skipped some steps, to have the fully working project and all the code filled in.

It’s great to know how to properly implement security. Armed with this knowledge, you’ll be able to confirm if third-party security libraries are up to the best practices. However, implementing it all yourself, especially if rushed, can lead to mistakes. If you’re in that boat, consider using an industry approved or time-tested third party.

Conceal is a great choice for a third party encryption library. It gets you up and running without having to worry about the underlying details. The one drawback — when hackers expose a vulnerability in a popular library. This affects all the apps that rely on that third party at the same time. Apps that have a custom implementation are often immune to wide-spread, scripted attacks.

The Account Manager is part of the Android OS and has a corresponding API. It’s a centralized manager for user account credentials so your app doesn’t have to store or work with passwords and logins directly. The most well known example of this is when requesting an OAuth2 token.

Introduced in Android 4.0 (API Level 14), the Keychain API deals with key management. It specifically works with PrivateKey and X509Certificate objects and provides a more secure container than using your app’s data storage. You can use it to install certificates and use private key objects directly.

Your security code will work well to protect your app, as long as someone doesn’t tamper with it. Check out the ProGuard tutorial to learn how to help prevent reverse engineering or tampering with your security-related code.

Now that you’ve secured your data at rest, why not read about how to secure your data in transit? And for those of you looking for advanced cryptography, check out the GCM Mode for AES.

By the way, the sample data characters Esther, Cornelius, Lightning and Birgit are all real! :]

If you have any questions or comments on what you’ve learned, join the forum discussion below!

Add a rating for this content

Contributors

Comments