What is Tink and how it powers Encryption in Preferences?
Tink is a multi-language cryptographic library from Google, designed to make common encryption tasks safer and simpler. Within EncryptedSharedPreferences, Tink handles both key/value encryption:
- DeterministicAead (AES-SIV) for keys: ensures the same plaintext key always yields the same ciphertext for stable lookups.
- Aead (AES-GCM) for values: provides random nonces each time, ensuring data confidentiality and integrity.
Because Tink manages the encryption/decryption logic under the hood, no plaintext is ever saved on disk.
1. Encrypting Your SharedPreferences
EncryptedSharedPreferences from Jetpack Security’s androidx.security:security-crypto library works similarly to regular SharedPreferences but encrypts each key/value before writing to disk. Under the hood:
- Keys get deterministically encrypted with a
DeterministicAead(AES256_SIV). - Values get AES256 GCM encryption via an
Aeadinstance.
After exploring library’s source code I understood below things,
Aead – Interface for Authenticated Encryption with Associated Data (AEAD).
DeterministicAead – Interface for Deterministic Authenticated Encryption with Associated Data (Deterministic AEAD).
It uses Tink to retrieve or generate the necessary keysets, storing them in your SharedPreferences file. The MasterKey you provide references an Android Keystore, ensuring any master encryption keys are securely handled at the OS level.
1.1. Setup: Dependencies & MasterKey :
dependencies {
implementation 'androidx.security:security-crypto:1.1.0-alpha06'
}
Build or retrieve a MasterKey :
val masterKey = MasterKey.Builder(context)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build()
Internally, MasterKey.Builder sets up an AES256-GCM key in the Android Keystore. The final alias is then used when creating the Tink keysets for both deterministic and non-deterministic encryption schemes.
1.2. Encryption Flow :
Representation of how plaintext is transformed into ciphertext on write, and back on read:
flowchart TB
A[Plaintext Key/Value] --> B(DeterministicAead / Aead)
B --> C(".putString(encryptedValue)")
C --> D(Saved on Disk in Encrypted Form)
D --> E(".getString(encryptedValue)")
E --> F(Decryption using MasterKey-based Keysets)
F --> G[Plaintext Key/Value Returned]This is how we use regular shared preferences, we just call getSharedPreferences using context.
class GameDifficultyPref(
context: Context
) {
private val sharedPref = context.getSharedPreferences(
PREFERENCE_KEY_GAME_DIFFICULTY,
Context.MODE_PRIVATE
)
}
Now lets build the Encryption for SharedPreferences, the below implementation allows us to reuse it’s functionality across all preferences in your code while simply passing name which key for the preference.
1.3. Apply Encryption :
import android.content.SharedPreferences
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKey
class GetEncryptedSharedPreferences(
private val context: Context
) {
operator fun invoke(
preferenceName: String
): SharedPreferences {
val masterKey = MasterKey.Builder(context)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build()
return EncryptedSharedPreferences.create(
/* context = */ context,
/* fileName = */ preferenceName,
/* masterKey = */ masterKey,
/* prefKeyEncryptionScheme = */ EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
/* prefValueEncryptionScheme = */ EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
}
}
This returns an EncryptedSharedPreferences instance using MasterKey approach.
Example :
A usage example that reads/writes a game difficulty. Instead of normal getSharedPreferences, it calls GetEncryptedSharedPreferences(context)(preferenceName).
import android.content.Context
class GameDifficultyPref(
context: Context
) {
private val sharedPref = GetEncryptedSharedPreferences(context).invoke(
preferenceName = PREFERENCE_KEY_GAME_DIFFICULTY
)
...
}
Internally wraps our preference with a new EncryptedSharedPreferences instance.
- Keys are encrypted with AES256-SIV which is a deterministic scheme so the same key always encrypts to the same ciphertext necessary for consistent lookups.
- Values are encrypted with AES256-GCM providing authenticity/integrity checks on the data.
2. Migration from Unencrypted Preferences
If you have an old SharedPreferences implementation and want to encrypt them:
- Read existing values from that preference.
- Write them to your new
EncryptedSharedPreferences. - Clear the old values.
fun migratePrefs(context: Context) {
val old = context.getSharedPreferences("previous_prefs_key", Context.MODE_PRIVATE)
val oldValue = old.getString("game_difficulty_result", null)
if (oldValue != null) {
val securePrefs = GetEncryptedSharedPreferences(context)("new_prefs_key")
securePrefs.edit()
.putString("game_difficulty_result", oldValue)
.apply()
old.edit().clear().apply()
}
}
This ensures the user’s existing data is seamlessly encrypted going forward.
3. Testing encrypted shared preferences
When you attempt to use EncryptedSharedPreferences in a test, you’ll encounter a KeyStoreException: AndroidKeyStore not found.
AndroidKeyStore not found
java.security.KeyStoreException: AndroidKeyStore not found
at java.base/java.security.KeyStore.getInstance(KeyStore.java:872)
Robolectric lacks real Android Keystore support, so the library’s code cannot generate or retrieve the master key.
Solution: Instrumentation Tests
By running instrumentation tests (in androidTest/), you rely on a real device/emulator environment, which does provide the AndroidKeyStore. This way, MasterKey.Builder() and EncryptedSharedPreferences.create() function without exceptions.
Setup : We need to get context and instantiate our preferences class
import android.content.Context
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class GetEncryptedSharedPreferencesTest {
private lateinit var context: Context
private lateinit var getEncryptedSharedPreferences: GetEncryptedSharedPreferences
@Before
fun setup() {
context = ApplicationProvider.getApplicationContext()
getEncryptedSharedPreferences = GetEncryptedSharedPreferences(context)
}
}
Test case 1 :
Confirm function can write a string into Preference and read it back.
Proves encryption logic doesn’t block normal usage both the key and value are stored.
@Test
fun canStoreAndRetrieveData() {
val prefs = getEncryptedSharedPreferences("preference_game_key")
// Put a string value
prefs.edit().putString("game_difficulty_result", GameDifficulty.HARD.name).apply()
// Retrieve it
val result = prefs.getString("game_difficulty_result", null)
assertEquals(GameDifficulty.HARD.name, result)
}
Test case 2 :
Writes MEDIUM as the difficulty.
Reads it back, ensuring we get MEDIUM.
Demonstrates it handles the master key behind the scenes with no extra steps needed.
@Test
fun masterKeyIsCreatedAutomaticallyByLibrary() {
// the library calls MasterKey.Builder behind the scenes.)
val prefs = getEncryptedSharedPreferences("preference_game_key")
prefs.edit().putString("game_difficulty_result", GameDifficulty.MEDIUM.name).apply()
assertEquals("MEDIUM", prefs.getString("game_difficulty_result", null))
}
Let’s write tests for actual preference feature which depends on encrypting the preferences.
import android.content.Context
class GamePref(
context: Context
) {
private val sharedPref = GetEncryptedSharedPreferences(context).invoke(
preferenceName = PREFERENCE_KEY_GAME_DIFFICULTY
)
fun getGameDifficultyPref(): GameDifficulty {
val difficulty = sharedPref.getString(PREFERENCE_RESULT_GAME_DIFFICULTY, defaultDifficulty)
return GameDifficulty.valueOf(difficulty ?: defaultDifficulty)
}
fun updateGameDifficultyPref(
gameDifficulty: GameDifficulty
) {
sharedPref
.edit()
.putString(PREFERENCE_RESULT_GAME_DIFFICULTY, gameDifficulty.name)
.apply()
}
companion object {
private const val PREFERENCE_KEY_GAME_DIFFICULTY = "preference_game_key"
private const val PREFERENCE_RESULT_GAME_DIFFICULTY = "game_difficulty_result"
private val defaultDifficulty = GameDifficulty.EASY.name
}
}
Setup :
import android.content.Context
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class GamePrefTest {
private lateinit var context: Context
private lateinit var gamePref: GamePref
@Before
fun setup() {
context = ApplicationProvider.getApplicationContext()
gamePref = GamePref(context)
}
}
Test case 1 :
No updates yet, the default should be EASY
@Test
fun defaultDifficultyShouldBeEasy() {
val actualDifficulty = gamePref.getGameDifficultyPref()
assertEquals(GameDifficulty.EASY, actualDifficulty)
}
Test case 2 :
Update and verify preference difficulty to HARD
@Test
fun canStoreAndRetrieveGameDifficulty() {
gamePref.updateGameDifficultyPref(GameDifficulty.HARD)
val storedDifficulty = gamePref.getGameDifficultyPref()
assertEquals(GameDifficulty.HARD, storedDifficulty)
}
Test case 3 :
Update and verify preferences repeatedly
@Test
fun updateDifficultyMultipleTimes() {
// Start and verify with MEDIUM
gamePref.updateGameDifficultyPref(GameDifficulty.MEDIUM)
var currentDiff = gamePref.getGameDifficultyPref()
assertEquals(GameDifficulty.MEDIUM, currentDiff)
// Update and verify preference to HARD
gamePref.updateGameDifficultyPref(GameDifficulty.HARD)
currentDiff = gamePref.getGameDifficultyPref()
assertEquals(GameDifficulty.HARD, currentDiff)
// Update and verify preference set EASY
gamePref.updateGameDifficultyPref(GameDifficulty.EASY)
currentDiff = gamePref.getGameDifficultyPref()
assertEquals(GameDifficulty.EASY, currentDiff)
}
4. Internals of EncryptedSharedPreferences
Information outlined here is referring internal implementation of actual code in library.
4.1. Sequence of Operations
First create() function is called from our encrypted shared preference, internally being called this function which actually calls standard shared preference which is referred below at applicationContext.getSharedPreferences
public static SharedPreferences create(
@NonNull String fileName,
@NonNull String masterKeyAlias,
@NonNull Context context,
...
) throws GeneralSecurityException, IOException {
...
// here is standard SharedPreferences call
SharedPreferences realPrefs =
applicationContext.getSharedPreferences(fileName, Context.MODE_PRIVATE);
return new EncryptedSharedPreferences(
fileName,
masterKeyAlias,
realPrefs,
aead,
daead
);
}
realPrefs is just a normal SharedPreferences instance that references an XML file in /data/data/<our_package>/shared_prefs/<fileName>.xml.
4.2. Returns instance of EncryptedSharedPreferences
flowchart LR
subgraph creation
A["create()"] --> B["2 Tink keysets: Key + Value"]
B --> C[EncryptedSharedPreferences constructor]
C --> D("Stores references to realPrefs + aead + daead")
endflowchart LR
subgraph writing
E(".putString(userKey, someValue)") --> F("encryptKey -> encryptedKey")
F --> G("encryptValue -> encryptedValue")
G --> H("call realPrefs.putString(encryptedKey, encryptedValue)")
H --> I(".apply/.commit -> normal SP disk write")
endWriting sequence : once .putString("userKey", "someValue") is called
- Key is deterministically encrypted with AES-SIV →
encryptedKey. - Value is non-deterministically encrypted with AES-GCM →
encryptedValue. - Then calls
realPrefs.edit().putString(encryptedKey, encryptedValue). - Finally
.apply()or.commit()writes ciphertext to the file normalSharedPreferencesuses.
4.3. Underlying Data Structures
In-Memory Map:
realPrefsuses aMap<String, Object>to store(key → value)pairs.- In
EncryptedSharedPreferences, these keys/values end up being base64-encoded ciphertext strings.
File Storage: The result is an XML file with ciphertext pairs, e.g
<map>
<string name="ENCRYPTED_KEY_ABC..">"ENCRYPTED_VALUE_XYZ.."</string>
</map>
So the key in the map is also cryptographically scrambled, ensuring an attacker can’t guess the real preference name.
Reference :
Android documentation – Save simple data with SharedPreferences
Android documentation – SharedPreferences methods
GitHub project with code examples
I’ve written this article to the best of my knowledge based on the current documentation and best practices. However, if you spot any errors, missing details, or have suggestions for improvement, please let me know so I can correct them. The goal is to keep this information as accurate and helpful as possible!
Author
Raj
Hi ! I’m a Software Engineer at MedKitDoc & Technical writer.
I lead organization Developers Breach focusing on contributing to Open Source and Student Projects.

