Data Privacy for Android

In this data privacy tutorial for Android with Kotlin, you’ll learn how to protect users’ data. By Kolin Stürt.

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

Hardening User Data

In the previous tutorial, you discovered that the app stores sensitive reports in the clear. You’ll change that now by using MasterKeys to generate a key in the KeyStore. This will encrypt the reports.

As you learned above, the benefit of storing a key in the KeyStore is that it allows the OS to operate on it without exposing the secret contents of that key. Key data do not enter the app space.

For devices that don’t have a security chip, permissions for private keys only allow for your app to access the keys — and only after user authorization. This means that a lock screen must be set up on the device before you can make use of the credential storage. This makes it more difficult to extract keys from a device, called extraction prevention.

The security library contains two new classes, EncryptedFile and EncryptedSharedPreferences. In Encryption.kt, replace the entire encryptFile() with this:

fun encryptFile(context: Context, file: File) : EncryptedFile {
  val keyGenParameterSpec = MasterKeys.AES256_GCM_SPEC
  val masterKeyAlias = MasterKeys.getOrCreate(keyGenParameterSpec) // 1
  return EncryptedFile.Builder(
      file,
      context,
      masterKeyAlias,
      EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB // 2
  ).build()
}

Here’s what’s happening:

  1. You either create a new master key or retrieve one already created.
  2. You encrypt the file using the popular secure AES encryption algorithm.

In ReportDetailActivity.kt, find sendReportPressed(). Replace the two lines right after //TODO: Replace below for encrypting file with the below code block:

val file = File(filesDir.absolutePath, "$reportID.txt") //1
val encryptedFile = encryptFile(baseContext, file) // 2
encryptedFile.openFileOutput().bufferedWriter().use {
  it.write(reportString) //3
}

Here’s what’s happening:

  1. You create a file with a name "$reportID.txt".
  2. You create an EncryptedFile instance using the file object created in the last step.
  3. You use the EncryptedFile instance to write to file all the report data.
Note: Biometrics do come with a few concerns. People can use biometrics maliciously. An example of that is when someone steals and holds your phone up to your face while you’re unconscious, or when law enforcement holds your device to your finger after they handcuff you. Or, if someone cuts off your hand when you’re distracted and sends it to the Mob. Always check to make sure your hand is there!

Awesome! You’ve hardened the data stored on the device. To make the app more secure, you’ll next authenticate your biometric credentials with a server.

Authenticating With Biometrics

You can auto-generate a key in KeyStore that is protected by your biometric credential. The key will encrypt a password for server authentication, and if the device becomes compromised, the password will be encrypted.

In Encryption.kt, add the following to generateSecretKey():

val keyGenParameterSpec = KeyGenParameterSpec.Builder(
    KEYSTORE_ALIAS,
    KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
    .setBlockModes(KeyProperties.BLOCK_MODE_GCM) // 1
    .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
    .setUserAuthenticationRequired(true) // 2
    .setUserAuthenticationValidityDurationSeconds(120) // 3
    .build()
val keyGenerator = KeyGenerator.getInstance(
    KeyProperties.KEY_ALGORITHM_AES, PROVIDER) // 4
keyGenerator.init(keyGenParameterSpec)
keyGenerator.generateKey()

Here’s what’s happening:

  1. You chose GCM, a popular and safe-block mode that the encryption uses.
  2. You require a lock screen to be set up and the key locked until the user authenticates by passing in .setUserAuthenticationRequired(true). Enabling the requirement for authentication also revokes the key when the user removes or changes the lock screen.
  3. You made the key available for 120 seconds from password authentication with .setUserAuthenticationValidityDurationSeconds(120). Passing in -1 requires fingerprint authentication every time you want to access the key.
  4. You create KeyGenerator with the above settings and set it the AndroidKeyStore PROVDER.

There are a few more options worth mentioning:

  • setRandomizedEncryptionRequired(true) enables the requirement that there’s enough randomization. If you encrypt the same data a second time, that encrypted output will still be different. This prevents an attacker from gaining clues about the ciphertext based on feeding in the same data.
  • Another option is .setUserAuthenticationValidWhileOnBody(boolean remainsValid). It locks the key once the device has detected it is no longer on the person.

Because you use the same key and cipher in different parts of the app, add the following helper functions to Encryption.kt, under the companion code block:

private fun getSecretKey(): SecretKey {
  val keyStore = KeyStore.getInstance(PROVIDER)

  // Before the keystore can be accessed, it must be loaded.
  keyStore.load(null)
  return keyStore.getKey(KEYSTORE_ALIAS, null) as SecretKey
}

private fun getCipher(): Cipher {
  return Cipher.getInstance(KeyProperties.KEY_ALGORITHM_AES + "/"
      + KeyProperties.BLOCK_MODE_GCM + "/"
      + KeyProperties.ENCRYPTION_PADDING_NONE)
}

The first function returns the secret key from the keystore. The second one returns a pre-configured Cipher.

Encrypting Data

You’ve stored the key in the KeyStore. Next, you’ll update the login method to encrypt the user’s generated password using the Cipher object, given the SecretKey. In the Encryption class, replace the contents of createLoginPassword() with the following:

val cipher = getCipher()
val secretKey = getSecretKey()
val random = SecureRandom() // 1
val passwordBytes = ByteArray(256)
random.nextBytes(passwordBytes)
cipher.init(Cipher.ENCRYPT_MODE, secretKey)
val ivParameters = cipher.parameters.getParameterSpec(GCMParameterSpec::class.java) // 2
val iv = ivParameters.iv
PreferencesHelper.saveIV(context, iv)
return cipher.doFinal(passwordBytes) // 3

Here’s what’s happening:

  1. You create a random password using SecureRandom.
  2. You gather a randomized initialization vector (IV) required to decrypt the data and save it into the shared preferences.
  3. Your return a ByteArray containing the encrypted data.

Decrypting to a Byte Array

You’ve encrypted the password, so now you’ll need to decrypt it when the user authenticates with a server. Replace the contents of decryptPassword() with below:

val cipher = getCipher()
val secretKey = getSecretKey()
val iv = PreferencesHelper.iv(context) // 1
val ivParameters = GCMParameterSpec(128, iv)
cipher.init(Cipher.DECRYPT_MODE, secretKey, ivParameters) // 2
return cipher.doFinal(password) // 3

Here’s what’s happening:

  1. You retrieve the IV required to decrypt the data.
  2. You initialize Cipher using DECRYPT_MODE.
  3. You return a decrypted ByteArray.

Back in MainActivity.kt, find performLoginOperation(). Replace the call to createDataSource where it says //TODO: Replace with encrypted data source below:

val encryptedInfo = createLoginPassword(this)
createDataSource(it, encryptedInfo)

On sign up, you create a password for the account. Right after the //TODO: Replace below with implementation that decrypts password, replace success = true with the following:

val password = decryptPassword(this,
    Base64.decode(firstUser.password, Base64.NO_WRAP))
if (password.isNotEmpty()) {
  //Send password to authenticate with server etc
  success = true
}

On log in, you retrieve the password to authenticate with a server. The app shouldn’t work without the key. Build and run. Then try to log in. You should encounter the following exception:

No key error present

That’s because no key was created on the previous sign up.

Delete the app to remove the old saved state. Then rebuild and run the app. You should now be able to log in. :]

Report list

You’ll notice most security functions work with ByteArray or CharArray, instead of objects such as String. That’s because String is immutable. There’s no control over how the system copies or garbage collects it.

If you’re working with sensitive strings or data, it’s better — though not foolproof — to store them in a mutable array. Overwrite sensitive arrays when you’re done with them like this:

Arrays.fill(array, 0.toByte())

You’ve created an encrypted password that will only be available once you’ve authenticated with your credentials. Your data is safely guarded.

Guard Dog