Encrypted Key-Value Store in Kotlin Multiplatform
Tags: kotlin, android, ios, security
Published: 2021-06-07 18:18:08

## Intro
At some point you will probably need to store a user's auth token on the device, so they won't need to auth whenever they use your app. This is a solved problem on Android with EncryptedSharedPreferences and iOS with the Keychain Services API. But if you're creating a mobile app using Kotlin Multiplatform, the KMP solution might not be as clear. Today we'll go over how to store encrypted key-value data with KMP without having to reinvent the wheel.

## Enter Multiplatform Settings
[Multiplatform Settings](https://github.com/russhwolf/multiplatform-settings) is a solid multiplatform key-value store, created by Touchlab's own Russell Wolf, used extensively at [Touchlab](https://github.com/touchlab/KaMPKit), as well as in Jetbrains' [KMM Production Sample](https://github.com/Kotlin/kmm-production-sample). There is a [Settings](https://github.com/russhwolf/multiplatform-settings/blob/95bcdca9fc549e3f348a3481a1d705eef240a0f8/multiplatform-settings/src/commonMain/kotlin/com/russhwolf/settings/Settings.kt) interface that is implemented for Android, iOS, MacOS, and JVM platforms. At first glance, it doesn't look like Multiplatform Settings offers any encrypted storage. But thanks to the abstraction that Android has built into `SharedPreferences` and Multiplatform Settings' [support](https://github.com/russhwolf/multiplatform-settings/blob/95bcdca9fc549e3f348a3481a1d705eef240a0f8/multiplatform-settings/src/appleMain/kotlin/com/russhwolf/settings/KeychainSettings.kt) for saving to the iOS keychain, our work is pretty easy.

## KMP Encrypted Settings on Android
Let's first take a look at the default non-encrypted AndroidSettings implementation. Usually we'll use the builder, but the public constructor is really helpful here.

``` kotlin
public class AndroidSettings @JvmOverloads public constructor(    
    private val delegate: SharedPreferences,
    private val commit : Boolean = false
) : ObservableSettings
```

[EncryptedSharedPreferences](https://developer.android.com/reference/androidx/security/crypto/EncryptedSharedPreferences) implements the SharedPreferences interface. So all we need to do is create one, and initialize an instance of AndroidSettings with it: 
``` kotlin
AndroidSettings(EncryptedSharedPreferences.create(
    get(),
    "MyEncryptedSettings",
    MasterKey.Builder(get())
        .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
        .build(),
    EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
    EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
), false)
```
Because the `SharedPreferences` delegate instance is a constructor argument rather than hardcoded, we can swap it out during testing, or if we want to provide an alternate implementation like we have here.

## KMP Encrypted Settings on iOS
Multiplatform Settings already has an encrypted implementation of Settings in the form of [KeychainSettings](https://github.com/russhwolf/multiplatform-settings/blob/95bcdca9fc549e3f348a3481a1d705eef240a0f8/multiplatform-settings/src/appleMain/kotlin/com/russhwolf/settings/KeychainSettings.kt). It uses the [iOS platform Security API](https://developer.apple.com/documentation/security/1401659-secitemadd)(`SecItemAdd()`, `SecItemDelete()`, etc. We can use it thus:
``` kotlin
KeychainSettings(
    service = "MyEncryptedSettings"
)
```
That was easy! It is marked with an `@ExperimentalSettingsImplementation` annotation, but I have been using it to build a client project that will soon be used by millions, and have not had any issues. If you do, please open an [issue](https://github.com/russhwolf/multiplatform-settings/issues).

## Providing the Right Implementation
If you plan on using both an unencrypted Settings and an encrypted Settings in shared code, you have a couple options. You can create a special wrapper class to disambiguate unencrypted and encrypted settings instances like this:

{% gist https://gist.github.com/brady-aiello/197c3dbb5a0abc4b19d3df45db4f81b5 %}

{% gist https://gist.github.com/brady-aiello/d3617ad260d5e387784b9155c155b40d file=SharedSettingsHelper.kt %}

Otherwise, you will need use a named instance in your preferred dependency injection or service locator library. Here is an example using Koin, a multiplatform service locator:

{% gist https://gist.github.com/brady-aiello/d3617ad260d5e387784b9155c155b40d file=Koin.kt %}
{% gist https://gist.github.com/brady-aiello/d3617ad260d5e387784b9155c155b40d file=KoinAndroid.kt %}
{% gist https://gist.github.com/brady-aiello/d3617ad260d5e387784b9155c155b40d file=KoinIOS.kt %}

Notice that the name of `SharedSettingsHelper`'s constructor `parameterencryptedSettings` matches the name specified in our Koin setup; it must be exact for Koin to know which one you want. If you're using platform-specific DI / service locator solutions, they will also have options for providing named instances, like Dagger's support of the Javax `@Named` annotation.

Once you have it all working, you can inspect the encrypted data storage on Android, by going to Android Studio, opening Device File Explorer, and navigating data → data → (Your Package Name) → shared_prefs → (Your Encrypted Settings Name) and you may see something like this:

![Encrypted Android Settings](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/m72bq0chy9rjkinjupdk.png)

Sweet! The keys and values are encrypted! Now you can read and write encrypted key-value data from common Kotlin code, maximizing code sharing.

## Conclusion
If you're using KMP, you don't need to create your own multiplatform encrypted settings solution. In fact, if you're already using [Multiplatform Settings](https://github.com/russhwolf/multiplatform-settings), you don't even need another multiplatform dependency. How are you doing encrypted key-value stores in Kotlin Multiplatform? Do you like this approach? Let me know in the comments, or [Twitter](https://twitter.com/AielloBrady). If you'd like to get into KMM and help steer its future, we're [hiring](https://touchlab.co/careers-3/)!