Skip to content

Instantly share code, notes, and snippets.

@johnkil
Forked from AJIEKCX/EncryptedSettings.kt
Created July 17, 2025 11:48
Show Gist options
  • Save johnkil/689507d0f419782655a206b404092412 to your computer and use it in GitHub Desktop.
Save johnkil/689507d0f419782655a206b404092412 to your computer and use it in GitHub Desktop.
A secure implementation of SharedPreferences for the MultiplatformSettings library
import android.content.SharedPreferences
import com.google.crypto.tink.Aead
import com.google.crypto.tink.KeysetHandle
import com.google.crypto.tink.RegistryConfiguration
import com.google.crypto.tink.TinkProtoKeysetFormat
import com.google.crypto.tink.aead.AeadConfig
import com.google.crypto.tink.aead.PredefinedAeadParameters
import com.google.crypto.tink.integration.android.AndroidKeystore
import com.google.crypto.tink.subtle.Base64
import com.google.crypto.tink.subtle.Hex
import com.russhwolf.settings.Settings
import com.russhwolf.settings.SharedPreferencesSettings
import java.security.GeneralSecurityException
/**
* A secure implementation of [Settings] that stores encrypted key-value pairs
* using Tink cryptography and the Android Keystore for key management.
*
* This class ensures that preferences stored in [SharedPreferences] are encrypted
* with a symmetric AEAD key, which itself is encrypted using a key encryption key (KEK)
* stored in the Android Keystore.
*
* @param sharedPreferences The [SharedPreferences] instance where encrypted data and keysets are stored.
*/
class EncryptedSettings(
sharedPreferences: SharedPreferences,
) : Settings {
private val preferences = SharedPreferencesSettings(sharedPreferences)
/*
Use an empty string as associated data, because the key encryption key is only used to encrypt one keyset.
If the same KEK is used to encrypt multiple keysets, then each keyset should have a different associated data.
*/
private val tinkKeysetAssociatedData: ByteArray = ByteArray(0)
private val aead: Aead
override val keys: Set<String>
get() = preferences.keys
override val size: Int
get() = preferences.size
init {
val sharedPreferencesKeys = sharedPreferences.all.keys
if (sharedPreferencesKeys.isNotEmpty() && !sharedPreferencesKeys.contains(TinkKeysetName)) {
error("Shared preferences are not empty and do not contain $TinkKeysetName. Use another instance of shared preferences.")
}
AeadConfig.register()
val encryptedKeysetExists = preferences.hasKey(TinkKeysetName)
val keysetEncryptionKeyExists: Boolean = AndroidKeystore.hasKey(KeyEncryptionKeyAlias)
if (!keysetEncryptionKeyExists && encryptedKeysetExists) {
// The KEK is missing. This may happen if the phone is restored from a backup.
// You need to decide how to handle this. You may recover from this by creating a new KEK
// and a new encrypted keyset, but then you need to delete all data that had been
// encrypted with the old key.
clearEncryptedPreference()
}
if (keysetEncryptionKeyExists && !encryptedKeysetExists) {
// The KEK exists, but the encrypted keyset is missing. In this example we assume that
// the KEK is only used to encrypt one keyset, so this should not happen.
// You need to decide how to handle this. You may recover from this by creating a new
// encrypted keyset, but then you need to delete all data that had been encrypted with the old
// key.
clearEncryptedPreference()
}
val keysetHandle = if (!AndroidKeystore.hasKey(KeyEncryptionKeyAlias)) {
// Create a new KEK in Android Keystore.
AndroidKeystore.generateNewAes256GcmKey(KeyEncryptionKeyAlias)
// Create a new keyset. In this example, we create an AEAD key of type AES256_GCM.
val keysetHandle = KeysetHandle.generateNew(PredefinedAeadParameters.AES256_GCM)
val encryptedKeyset = TinkProtoKeysetFormat.serializeEncryptedKeyset(
keysetHandle,
AndroidKeystore.getAead(KeyEncryptionKeyAlias),
tinkKeysetAssociatedData
)
// In this example, we store the encrypted keyset in the shared preferences.
preferences.putString(TinkKeysetName, Hex.encode(encryptedKeyset))
keysetHandle
} else {
val encryptedKeyset = Hex.decode(getEncryptedKeyset())
TinkProtoKeysetFormat.parseEncryptedKeyset(
encryptedKeyset,
AndroidKeystore.getAead(KeyEncryptionKeyAlias),
tinkKeysetAssociatedData
)
}
aead = keysetHandle.getPrimitive(RegistryConfiguration.get(), Aead::class.java)
}
override fun clear() {
clearEncryptedPreference()
}
override fun remove(key: String) {
preferences.remove(key)
}
override fun hasKey(key: String): Boolean {
return preferences.hasKey(key)
}
override fun putInt(key: String, value: Int) {
putString(key, value.toString())
}
override fun getInt(key: String, defaultValue: Int): Int {
return getIntOrNull(key) ?: defaultValue
}
override fun getIntOrNull(key: String): Int? {
return getStringOrNull(key = key)?.toIntOrNull()
}
override fun putLong(key: String, value: Long) {
putString(key, value.toString())
}
override fun getLong(key: String, defaultValue: Long): Long {
return getLongOrNull(key) ?: defaultValue
}
override fun getLongOrNull(key: String): Long? {
return getStringOrNull(key = key)?.toLongOrNull()
}
override fun putString(key: String, value: String) {
val encryptedValue = encrypt(value = value)
preferences.putString(key = key, value = encryptedValue)
}
override fun getString(key: String, defaultValue: String): String {
return getStringOrNull(key = key) ?: defaultValue
}
override fun getStringOrNull(key: String): String? {
val encryptedPreferenceValue = preferences.getStringOrNull(key) ?: return null
return decrypt(encrypted = encryptedPreferenceValue)
}
override fun putFloat(key: String, value: Float) {
putString(key = key, value = value.toString())
}
override fun getFloat(key: String, defaultValue: Float): Float {
return getFloatOrNull(key = key) ?: defaultValue
}
override fun getFloatOrNull(key: String): Float? {
return getStringOrNull(key = key)?.toFloatOrNull()
}
override fun putDouble(key: String, value: Double) {
putString(key = key, value = value.toString())
}
override fun getDouble(key: String, defaultValue: Double): Double {
return getDoubleOrNull(key = key) ?: defaultValue
}
override fun getDoubleOrNull(key: String): Double? {
return getStringOrNull(key = key)?.toDoubleOrNull()
}
override fun putBoolean(key: String, value: Boolean) {
putString(key = key, value = value.toString())
}
override fun getBoolean(key: String, defaultValue: Boolean): Boolean {
return getBooleanOrNull(key = key) ?: defaultValue
}
override fun getBooleanOrNull(key: String): Boolean? {
return getStringOrNull(key = key)?.toBooleanStrictOrNull()
}
private fun clearEncryptedPreference() {
AndroidKeystore.deleteKey(KeyEncryptionKeyAlias)
preferences.clear()
}
private fun decrypt(encrypted: String): String? {
val decryptedByteArray = try {
aead.decrypt(
Base64.decode(encrypted),
getEncryptedKeyset()?.encodeToByteArray()
)
} catch (e: GeneralSecurityException) {
null
}
return decryptedByteArray?.decodeToString()
}
private fun encrypt(value: String): String {
val encrypted: ByteArray = aead.encrypt(
value.encodeToByteArray(),
getEncryptedKeyset()?.encodeToByteArray()
)
return Base64.encode(encrypted)
}
private fun getEncryptedKeyset(): String? {
return preferences.getStringOrNull(TinkKeysetName)
}
}
private const val TinkKeysetName = "YOUR_KEYSET_NAME"
private const val KeyEncryptionKeyAlias = "YOUR_ENCRYPTION_KEY_ALIAS"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment