Migrate from EncryptedSharedPreferences to Tink and DataStore

Migrate from EncryptedSharedPreferences to Tink + DataStore
Hello. My name is Osugi, and I’m part of the Toyota Woven City Payment Development Group.
Our team develops the payment system used in Woven by Toyota’s Toyota Woven City, covering a wide range of payment-related functions, from backend to Web frontend and mobile applications.
In this post, I've summarized the story of how I replaced an Android app that implemented EncryptedSharedPreferences, which has now been officially deprecated.
Introduction
EncryptedSharedPreferences has been deprecated since v1.1.0-alpha07, with an official recommendation to replace it with Android KeyStore.
Investigating Alternatives to EncryptedSharedPreferences
With EncryptedSharedPreferences being deprecated, we began exploring options for both data persistence and encryption.
Choosing a Data Persistence Method
In our app's use case, EncryptedSharedPreferences had only been used to store configuration data, so using SharedPreferences alone would have been sufficient. However, since we had this opportunity to refactor, we decided to follow the official recommendation and adopted DataStore as our persistence means.
Choosing an Encryption Library
Following the official recommendation mentioned earlier, we initially planned to use Android KeyStore. However, we found that not only are there functional limitations depending on the API level, but achieving a high level of security using StrongBox also depends on the device specifications. This meant that simply implementing it in code might not guarantee the intended level of security. In our case, since the app was designed to run on devices managed via MDM, and we had already selected devices that support StrongBox, this limitation was not an issue.
During our research on encryption libraries, we also came across Tink, a cryptographic library provided by Google. Looking at Tink’s repository, we found that it uses Android KeyStore to store its master key.
To compare Android KeyStore and Tink in terms of maintainability and performance, we created a sample implementation.
Comparing Encryption Library Implementations
Below is a summary of sample code using Android KeyStore with StrongBox and TEE, as well as using Tink.
We found that both were relatively easy to implement at a basic level. That said, Android KeyStore has some challenges:
- Key generation settings must be adjusted depending on the encryption algorithm
- Developers are responsible for managing initialization Vectors (IVs)
- There are very few sample implementations available
Tink, on the other hand, wraps these aspects nicely, making implementation smoother.
Sample Implementation of Encryption and Decryption Using Android KeyStore
class AndroidKeyStoreClient(
private val useStrongKeyBox: Boolean = false
) {
private val keyStoreAlias = "key_store_alias"
private val KEY_STORE_PROVIDER = "AndroidKeyStore"
private val keyStore by lazy {
KeyStore.getInstance(KEY_STORE_PROVIDER).apply {
load(null)
}
}
private val cipher by lazy {
Cipher.getInstance("AES/GCM/NoPadding")
}
private fun generateSecretKey(): SecretKey {
val keyStore = keyStore.getEntry(keyStoreAlias, null)
if (keyStore != null) {
return (keyStore as KeyStore.SecretKeyEntry).secretKey
}
return KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, KEY_STORE_PROVIDER)
.apply {
init(
KeyGenParameterSpec.Builder(
keyStoreAlias,
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
).setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.setIsStrongBoxBacked(useStrongKeyBox)
.setKeySize(256)
.build()
)
}.generateKey()
}
fun encrypt(inputByteArray: ByteArray): Result<String> {
return runCatching {
val secretKey = generateSecretKey().getOrThrow()
cipher.init(Cipher.ENCRYPT_MODE, secretKey)
val encryptedData = cipher.doFinal(inputByteArray)
cipher.iv.joinToString("|") + ":iv:" + encryptedData.joinToString("|")
}
}
fun decrypt(inputEncryptedString: String): Result<ByteArray> {
return runCatching {
val (ivString, encryptedString) = inputEncryptedString.split(":iv:", limit = 2)
val iv = ivString.split("|").map { it.toByte() }.toByteArray()
val encryptedData = encryptedString.split("|").map { it.toByte() }.toByteArray()
val secretKey = generateSecretKey()
val gcmParameterSpec = GCMParameterSpec(128, iv)
cipher.init(Cipher.DECRYPT_MODE, secretKey, gcmParameterSpec)
cipher.doFinal(encryptedData)
}
}
}
Sample Implementation of Encryption and Decryption Using Tink
class TinkClient(
context: Context
) {
val keysetName = "key_set"
val prefFileName = "pref_file"
val packageName = context.packageName
var aead: Aead
init {
AeadConfig.register()
aead = buildAead(context)
}
private fun buildAead(context: Context): Aead {
return AndroidKeysetManager.Builder()
.withKeyTemplate(KeyTemplates.get("AES256_GCM"))
.withSharedPref(
context,
"$packageName.$keysetName",
"$packageName.$prefFileName"
)
.withMasterKeyUri("android-keystore://tink_master_key")
.build()
.keysetHandle
.getPrimitive(RegistryConfiguration.get(), Aead::class.java)
}
fun encrypt(inputByteArray: ByteArray): Result<String> {
return runCatching {
val encrypted = aead.encrypt(inputByteArray, null)
Base64.getEncoder().encodeToString(encrypted)
}
}
fun decrypt(inputEncryptedString: String): Result<ByteArray> {
return runCatching {
val encrypted = Base64.getDecoder().decode(inputEncryptedString)
aead.decrypt(encrypted, null)
}
}
}
Performance Benchmarking of Encryption Libraries
We measured the encryption processing time of Android KeyStore and Tink. For Android KeyStore, we evaluated two execution environments: StrongBox and TEE.
In the test code, a common encryption algorithm (AES_GCM) was set and the process of repeatedly encrypting 10KB of data was measured using Microbenchmark. By using Microbenchmark, measurements were taken on an actual Google Pixel Tablet using a thread other than the UI thread.
The test code is shown below:
import androidx.benchmark.junit4.BenchmarkRule
import androidx.benchmark.junit4.measureRepeated
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class ExampleBenchmark {
@get:Rule
val benchmarkRule = BenchmarkRule()
@Test
fun benchmarkTinkEncrypt() {
val context = InstrumentationRegistry.getInstrumentation().context
val client = TinkClient(context)
val plainText = ByteArray(1024 * 10)
benchmarkRule.measureRepeated {
client.encrypt(plainText).getOrThrow()
}
}
@Test
fun benchmarkStrongBoxEncrypt() {
val context = InstrumentationRegistry.getInstrumentation().context
val client = AndroidKeyStoreClient(context, true)
val plainText = ByteArray(1024 * 10)
benchmarkRule.measureRepeated {
client.encrypt(plainText).getOrThrow()
}
}
@Test
fun benchmarkTeeEncrypt() {
val context = InstrumentationRegistry.getInstrumentation().context
val client = AndroidKeyStoreClient(context, false)
val plainText = ByteArray(1024 * 10)
benchmarkRule.measureRepeated {
client.encrypt(plainText).getOrThrow()
}
}
}
Here are the benchmark results:
Encryption Backend | Average Encryption Time (ms) | Number of Allocations |
---|---|---|
Android KeyStore (StrongBox) | 209 | 4646 |
Android KeyStore (TEE) | 7.07 | 4786 |
Tink | 0.573 | 38 |
Compared to Tink, which performs encryption in software, both Android KeyStore (StrongBox) and Android KeyStore (TEE) take significantly longer to process due to hardware access. Although the device we used in this test is relatively high-spec for an Android device, using Android KeyStore—particularly StrongBox—may require careful consideration of the user experience (UX).
Notes
Incidentally, the actual execution environment used for key generation with Android KeyStore can be determined using the code below:
val secretKey = generateSecretKey()
val kf = SecretKeyFactory.getInstance(KeyProperties.KEY_ALGORITHM_AES, KEY_STORE_PROVIDER)
val ki = kf.getKeySpec(secretKey, KeyInfo::class.java) as KeyInfo
val securityLevelString = when (ki.securityLevel) {
KeyProperties.SECURITY_LEVEL_STRONGBOX -> "STRONGBOX"
KeyProperties.SECURITY_LEVEL_TRUSTED_ENVIRONMENT -> "TEE"
KeyProperties.SECURITY_LEVEL_SOFTWARE -> "SOFTWARE"
else -> "UNKNOWN"
}
Log.d("KeyStoreSecurityLevel", "Security Level: ${ki.securityLevel}")
Conclusion
Since EncryptedSharedPreferences has been deprecated, we evaluated technologies as potential replacements. Following the official recommendation, we adopted DataStore as our data persistence solution. For encryption, after comparing Android KeyStore and Tink, we found that Tink was easier to use as it abstracts the key generation and encryption processes. It also offered better performance and met our security requirements, making it our preferred choice. It's worth noting that using Android KeyStore requires accounting for device-specific behavior. As such, it's important to carefully weigh both performance and security needs when choosing an encryption approach.
関連記事 | Related Posts
We are hiring!
【ソフトウェアエンジニア(リーダークラス)】共通サービス開発G/東京・大阪
共通サービス開発グループについてWebサービスやモバイルアプリの開発において、必要となる共通機能=会員プラットフォームや決済プラットフォームなどの企画・開発を手がけるグループです。KINTOの名前が付くサービスやKINTOに関わりのあるサービスを同一のユーザーアカウントに対して提供し、より良いユーザー体験を実現できるよう、様々な共通機能や顧客基盤を構築していくことを目的としています。
【フロントエンドエンジニア(リードクラス)】プロジェクト推進G/東京
配属グループについて▶新サービス開発部 プロジェクト推進グループ 中古車サブスク開発チームTOYOTAのクルマのサブスクリプションサービスである『 KINTO ONE 中古車 』のWebサイトの開発、運用を中心に、その他サービスの開発、運用も行っています。“とりあえずやってみる”から始まる開発文化。