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.
Multiplatform Settings is a solid multiplatform key-value store, created by Touchlab's own Russell Wolf, used extensively at Touchlab, as well as in Jetbrains' KMM Production Sample. There is a Settings 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 for saving to the iOS keychain, our work is pretty easy.
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.
public class AndroidSettings @JvmOverloads public constructor( private val delegate: SharedPreferences, private val commit : Boolean = false ) : ObservableSettings
EncryptedSharedPreferences implements the SharedPreferences interface. So all we need to do is create one, and initialize an instance of AndroidSettings with it:
AndroidSettings(EncryptedSharedPreferences.create( get(), "MyEncryptedSettings", MasterKey.Builder(get()) .setKeyScheme(MasterKey.KeyScheme.AES256_GCM) .build(), EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM ), false)
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.
Multiplatform Settings already has an encrypted implementation of Settings in the form of KeychainSettings. It uses the iOS platform Security API(
SecItemDelete(), etc. We can use it thus:
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.
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:
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:
Notice that the name of
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
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:
Sweet! The keys and values are encrypted! Now you can read and write encrypted key-value data from common Kotlin code, maximizing code sharing.
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, 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. If you'd like to get into KMM and help steer its future, we're hiring!