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! By Kolin Stürt.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 2 of 3 of this article. Click here to view the first page.

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