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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Encryption Tutorial For Android: Getting Started
30 mins
- Getting Started
- Securing the foundations
- Using Permissions
- Limiting installation directories
- Securing User Data With a Password
- Creating a Key
- Adding an Initialization Vector
- Encrypting the Data
- Decrypting the Data
- Saving Encrypted Data
- Securing the SharedPreferences
- Using a Key From a Server
- Using the KeyStore
- Generating a New Random Key
- Encrypting the Data
- Decrypting to a Byte Array
- Testing the Example
- Where to Go From Here?
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:
- Created 16 bytes of random data.
- 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.
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
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:
- 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. -
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
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:
- Used the HashMap that contains the encrypted data, salt and IV necessary for decryption.
- Regenerated the key given that information plus the user’s password.
- 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:
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:
- Converted the
String
into aByteArray
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. - Converted the raw data into a
String
representation.SharedPreferences
can’t store aByteArray
directly but it can work withString
. Base64 is a standard that converts raw data to a string representation. - 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:
- Retrieved the string representations for the encrypted data, IV and salt.
- Applied a Base64 decode on the strings to convert them back to raw bytes.
- Passed that data in a
HashMap
to thedecrypt
method.
Now that you have storage set up safely, start fresh by navigating to Settings ▸ Apps ▸ PetMed 2 ▸ Storage ▸ Clear 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. :]