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.
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
Data Privacy for Android
30 mins
- Getting Started
- Requesting Permissions
- Using IPC
- Opting Out
- Clearing the Cache
- Disabling Logging of Sensitive Data
- Disabling ability to Screenshot
- Exploring Hardware Security Modules
- Implementing Biometrics
- Hardening User Data
- Authenticating With Biometrics
- Encrypting Data
- Decrypting to a Byte Array
- Where to Go From Here?
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:
- You either create a new master key or retrieve one already created.
- 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:
- You create a file with a name
"$reportID.txt"
. - You create an
EncryptedFile
instance using the file object created in the last step. - You use the
EncryptedFile
instance to write to file all the report data.
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:
- You chose GCM, a popular and safe-block mode that the encryption uses.
- 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. - 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. - You create
KeyGenerator
with the above settings and set it theAndroidKeyStore
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:
- You create a random password using
SecureRandom
. - You gather a randomized initialization vector (IV) required to decrypt the data and save it into the shared preferences.
- 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:
- You retrieve the IV required to decrypt the data.
- You initialize
Cipher
usingDECRYPT_MODE
. - 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:
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. :]
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.