In Early September 2020 Google released the first Alpha of a new Jetpack library named DataStore. DataStore is a replacement for SharedPreferences. While there are some similarities to SharedPreferences, DataStore offers far greater flexibility. In this series of posts we’ll take a detailed look at this new library.
NOTE: There is an update to this series of articles that covers how to use newer releases of DataStore. It also fixes a bug in the Crypto class.
By default, when persisting data to device storage the DataStore library does not encrypt it. Initially this may feel like a regression on EncryptedSharedPreferences
that ships with the Jetpack Security library, it is fairly easy to add it. Moreover we have much greater control over the encryption so we can use more secure encryption of the requirements of our app demand it.
I’m not going to go in to a deep discussion of cryptography here. To keep the focus on the matter in hand, we’ll use a Cipher
– which is a symmetrical encryption technique.
Cipher
The Cipher
will use an AES algorithm, with a CBC block mode, and PKCS7 padding. As I mentioned previously, a deep discussion of this is outside of the scope of this article. We’ll focus instead on how it is actually used for encryption purposes. The Cipher
uses a SecretKey
which, as its name suggests, is a secret, symmetrical key which gets store in the AndroidKeyStore
– which is secure storage, managed by the Android Framework. The other component for the Cipher
is an Initialisation Vector (we’ll refer to this as the IV henceforth).
The IV is essentially the seed value for the CBC block mode. It encrypts the first block with the IV. It then encrypts the next block using a new vector which is a function of the encryption of the first block. If we change the IV each time we encrypt any given data, then the encrypted result will change each time. This makes it harder to detect patterns which might make it possible to determine the SecretKey
. It essentially adds entropy to the encryption. However, when we decrypt the data we must use the same IV that was used for encryption.
It may seem like the IV should remain secret, but that isn’t necessarily the case. Common wisdom is that the IV is public knowledge. That is provided that the same IV is never re-used for the same SecretKey
. For the remainder of this discussion we’ll consider the IV to be something that can be shared publicly and changed each time we encrypt and the SecretKey
as being a secret.
CipherProvider
The first component that we’ll look at is CipherProvider
. This is responsible for providing Cipher
objects which are capable of encrypting or decrypting data:
interface CipherProvider { val encryptCipher: Cipher fun decryptCipher(iv: ByteArray): Cipher }
The Cipher
returned by encryptCipher()
will have a unique IV, and the IV used for encryption needs to be passed in to decryptCipher()
to obtain a Cipher
capable of decoding the data that was encrypted using that IV.
The AES/CBC/PKCS7
implementation of this is:
class AesCipherProvider @Inject constructor( @Named(SecurityModule.KEY_NAME) private val keyName: String, private val keyStore: KeyStore, @Named(SecurityModule.KEY_STORE_NAME) private val keyStoreName: String ) : CipherProvider { override val encryptCipher: Cipher get() = Cipher.getInstance(TRANSFORMATION).apply { init(Cipher.ENCRYPT_MODE, getOrCreateKey()) } override fun decryptCipher(iv: ByteArray): Cipher = Cipher.getInstance(TRANSFORMATION).apply { init(Cipher.DECRYPT_MODE, getOrCreateKey(), IvParameterSpec(iv)) } private fun getOrCreateKey(): SecretKey = (keyStore.getEntry(keyName, null) as? KeyStore.SecretKeyEntry)?.secretKey ?: generateKey() private fun generateKey(): SecretKey = KeyGenerator.getInstance(ALGORITHM, keyStoreName) .apply { init(keyGenParams) } .generateKey() private val keyGenParams = KeyGenParameterSpec.Builder( keyName, KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT ).apply { setBlockModes(BLOCK_MODE) setEncryptionPaddings(PADDING) setUserAuthenticationRequired(false) setRandomizedEncryptionRequired(true) }.build() companion object { const val ALGORITHM = KeyProperties.KEY_ALGORITHM_AES const val BLOCK_MODE = KeyProperties.BLOCK_MODE_CBC const val PADDING = KeyProperties.ENCRYPTION_PADDING_PKCS7 const val TRANSFORMATION = "$ALGORITHM/$BLOCK_MODE/$PADDING" } }
The constructor arguments are the unique key that identifies the SecretKey
that will be stored in the AndroidKeyStore
, the KeyStore
itself, and the name of the KeyStore
.
The companion object contains some constants that identify the algorithm, block mode, and padding. It also contains them all concatenated in to the full transformation string.
Both encryptCipher.get()
and decryptCipher()
create a new Cipher
instance for the transformation. Each then initialises it for either encryption or decryption mode. init()
generates a new IV for encryption mode. It requires the IV used for encryption for decrypt mode.
Both os these methods call getOrCreateKey()
to obtain the SecretKey
from the KeyStore
, or generate a new one if it does not exist. If we generate a new key by invoking generateKey()
, the KeyGenerator.generateKey()
method generates the key and stores it in the KeyStore
for us.
Crypto
The next component is the Crypto
class which is responsible for encrypting and decrypting data:
interface Crypto { fun encrypt(rawBytes: ByteArray, outputStream: OutputStream) fun decrypt(inputStream: InputStream): ByteArray } class CryptoImpl @Inject constructor(private val cipherProvider: CipherProvider) : Crypto { override fun encrypt(rawBytes: ByteArray, outputStream: OutputStream) { val cipher = cipherProvider.encryptCipher val encryptedBytes = cipher.doFinal(rawBytes) with(outputStream) { write(cipher.iv.size) write(cipher.iv) write(encryptedBytes.size) write(encryptedBytes) } } override fun decrypt(inputStream: InputStream): ByteArray { val ivSize = inputStream.read() val iv = ByteArray(ivSize) inputStream.read(iv) val encryptedDataSize = inputStream.read() val encryptedData = ByteArray(encryptedDataSize) inputStream.read(encryptedData) val cipher = cipherProvider.decryptCipher(iv) return cipher.doFinal(encryptedData) } }
The encrypt()
method obtains an encryption Cipher
from cipherProvider
, then uses this to encrypt the raw data. It then obtains the IV from the Cipher
, and then writes both this and the encrypted data to the OutputStream
.
The decrypt()
method does the reverse. It first reads both the IV and encrypted data from the InputStream
. Then it obtains a decryption Cipher
from cipherProvider
for the IV. Finally it decrypts the data using this and returns the result.
This persists the IV along with the encrypted data so that we have the IV available at decryption time. It is fine to share the IV in this way as it is not secret – as we discussed earlier.
SecurityModule
For completeness, here is the Hilt
module that is capable of providing an instance of Crypto
:
@Module(includes = [SecurityModule.Declarations::class]) @InstallIn(ApplicationComponent::class) object SecurityModule { const val KEY_NAME = "Key Name" const val KEY_STORE_NAME = "Key Store Name" private const val ANDROID_KEY_STORE_TYPE = "AndroidKeyStore" private const val SIMPLE_DATA_KEY_NAME = "SimpleDataKey" @Provides fun provideKeyStore(): KeyStore = KeyStore.getInstance(ANDROID_KEY_STORE_TYPE).apply { load(null) } @Provides @Named(KEY_NAME) fun providesKeyName(): String = SIMPLE_DATA_KEY_NAME @Provides @Named(KEY_STORE_NAME) fun providesKeyStoreName(): String = ANDROID_KEY_STORE_TYPE @Module @InstallIn(ApplicationComponent::class) interface Declarations { @Binds fun bindsCipherProvider(impl: AesCipherProvider): CipherProvider @Binds fun bindsCrypto(impl: CryptoImpl): Crypto } }
I have used String Constants throughout to ensure consistency. The concrete implementations of both CipherProvider
are hidden through simple bindings.
SecureSimpleDataSerializer
We can now create a DataStore Serializer
around this:
class SecureSimpleDataSerializer(private val crypto: Crypto) : Serializer{ override fun readFrom(input: InputStream): SimpleData { return if (input.available() != 0) { try { SimpleData.ADAPTER.decode(crypto.decrypt(input)) } catch (exception: IOException) { throw CorruptionException("Cannot read proto", exception) } } else { SimpleData("") } } override fun writeTo(t: SimpleData, output: OutputStream) { crypto.encrypt(SimpleData.ADAPTER.encode(t), output) } }
This is really not much different to the Serializer
we created previously. The only addition is the crypto
instance that gets injected, and we use that to encrypt and decrypt the raw data.
The Serializer
is specific to each proto model we have defined. By keeping the Crypto
logic separate, we can create additional proto models relatively cheaply as the cryptography is handled by this external component and can be reused across any and all proto models that require it.
DataStoreModule
The only thing remaining is to add this where we create our DataStore instance:
@Module @InstallIn(ApplicationComponent::class) object DataStoreModule { @Provides fun providesDataStore( @ApplicationContext context: Context, crypto: Crypto ): DataStore= context.createDataStore( fileName = "DataStoreTest.pb", serializer = SecureSimpleDataSerializer(crypto) ) }
Our UI remains completely unchanged, as does the behaviour as far as the end user is concerned. However, bear in mind that any data previously persisted without the encryption will not be read correctly. So, if you are updating an app to include this kind of functionality, you will need some kind of migration strategy in place.
The data is now more secure. Grabbing the unencrypted persisted data from the device and viewing it in a hex editor shosw that text strings are clearly visible and readable:
However, the encrypted data looks like this:
The encrypted data is slightly larger, but please bear in mind that it also includes the IV which is 16 bytes.
Conclusion
I have deliberately chosen a relatively simple encryption mechanism here to keep the focus on how to apply it to DataStore. However, the concept of encapsulating responsibilities for clean separation of concerns is a useful one. CipherProvide
is solely responsible for creating Cipher
instances using a SecretKey
that gets stored in the AndroidKeyStore
. Crypto
is responsible for encrypting and decrypting data and uses Cipher
s obtained from CipherProvider
. It has no knowledge that they are AES/CBC/PKCS7
Cipher
s. But holds responsibility for persisting the IV. SecureSimpleDataSerializer
is responsible for persisting persisting the data from the proto model, but defers the cryptography to the Cropto
instance. It is agnostic of the IV and the persistence of it.
While this modular approach makes for maintainable code, it also makes it easy to change or strengthen the encryption without affecting anything else. Hopefully it is clear that changes to the internals of the Crypto
or CipherProvider
implementations have no effect on the Serializer
.
In the final article in this series, we’ll take a deeper look at some of the benefits provided by using protobuf over the old key / value pair model.
The source code for this article is available here.
© 2020 – 2021, Mark Allison. All rights reserved.
Copyright © 2020 Styling Android. All Rights Reserved.
Information about how to reuse or republish this work may be available at http://blog.stylingandroid.com/license-information.
Never learnt about encryption in android. This is really helpul. Thanks