DataStore / Jetpack / Security

DataStore: Security

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 Ciphers obtained from CipherProvider. It has no knowledge that they are AES/CBC/PKCS7 Ciphers. 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.

1 Comment

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.