MyFamilyTracker
Real-time family location sharing — Firebase Realtime DB for sub-second propagation, WorkManager + ForegroundService for OS-compliant background collection, geofencing via Google Maps API.
Learn how to use Android Keystore to protect API keys, tokens, and passwords in your Kotlin apps, with step‑by‑step code, best‑practice patterns, and common pitfalls to avoid.
On this page
Hardcoding secrets in source code is a ticking time bomb. Every leaked API key or token can lead to data breaches, unauthorized billing, or worse. Android Keystore gives you a hardware‑backed vault that keeps cryptographic material out of reach from malware and rooted devices.
Mobile apps constantly need to store sensitive values: backend API keys, JWT tokens, encryption keys for local data, or third‑party SDK credentials. Storing these in plain SharedPreferences, files, or even encrypted with a static password is insecure because the protection key itself becomes discoverable through decompilation or runtime inspection.
Android Keystore solves this by never exposing the raw key material to the application layer. Keys are generated, used, and deleted inside the Trusted Execution Environment (TEE) or Secure Element, depending on device capabilities. The app only receives encrypted blobs or can request signatures/encryptions without ever seeing the key.
[!NOTE]
On devices lacking a TEE, Keystore falls back to software‑based encryption, but the key still remains inaccessible via the Android API, raising the bar for attackers.
| Threat | Plain SharedPreferences | Encrypted File (static password) | Android Keystore |
|---|---|---|---|
| Decompiled APK | Key visible | Password visible → key derivable | Key never exposed |
| Rooted device | Key readable | Password may be extracted | Key material stays in TEE |
| Malware with root | Direct read | Possible if password sniffed | Requires hardware compromise |
| Backup extraction | Exported | Exported (if password stored) | Not exportable without user auth |
The table shows why Keystore is the only option that protects the key itself, not just the data it encrypts.
Below is a minimal, production‑ready wrapper that generates an AES key, encrypts a string, and later decrypts it. All cryptographic operations happen inside Keystore; the app only handles ciphertext.
/**
* KeystoreHelper – singleton wrapper for AES‑GCM encryption.
* Uses Android Keystore to store a symmetric key that never leaves the TEE.
*/
object KeystoreHelper {
private const val KEY_ALIAS = "com.sudarshantechlabs.app.master_key"
private const val TRANSFORMATION = "AES/GCM/NoPadding"
private val keyStore: KeyStore by lazy {
KeyStore.getInstance("AndroidKeyStore").apply { load(null) }
}
private val keyGenerator: KeyGenerator by lazy {
KeyGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_AES,
"AndroidKeyStore"
).apply {
init(
KeyGenParameterSpec.Builder(KEY_ALIAS,
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.setRandomizedEncryptionRequired(true)
.setUserAuthenticationRequired(false) // set true if you want biometric lock
.build()
)
}
}
/** Call once at app start to ensure the key exists. */
fun init() {
if (!keyStore.containsAlias(KEY_ALIAS)) {
keyGenerator.generateKey()
}
}
/** Encrypt a plaintext string, returning Base64‑encoded ciphertext || iv. */
fun encrypt(plainText: String): String {
val secretKey = keyStore.getKey(KEY_ALIAS, null) as SecretKey
val cipher = Cipher.getInstance(TRANSFORMATION).apply {
init(Cipher.ENCRYPT_MODE, secretKey)
}
val iv = cipher.iv ?: throw IllegalStateException("IV missing")
val cipherText = cipher.doFinal(plainText.toByteArray(Charsets.UTF_8))
// Prepend IV for storage; both are needed for decryption
val combined = iv + cipherText
return Base64.encodeToString(combined, Base64.NO_WRAP)
}
/** Decrypt a ciphertext produced by [encrypt]. */
fun decrypt(combinedBase64: String): String {
val combined = Base64.decode(combinedBase64, Base64.NO_WRAP)
val ivSize = 12 // GCM recommended IV length
val iv = combined.copyOfRange(0, ivSize)
val cipherText = combined.copyOfRange(ivSize, combined.size)
val secretKey = keyStore.getKey(KEY_ALIAS, null) as SecretKey
val cipher = Cipher.getInstance(TRANSFORMATION).apply {
init(Cipher.DECRYPT_MODE, secretKey, IvParameterSpec(iv))
}
val plainBytes = cipher.doFinal(cipherText)
return String(plainBytes, Charsets.UTF_8)
}
}Usage example
fun main() {
KeystoreHelper.init()
val token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
val encrypted = KeystoreHelper.encrypt(token)
// Store `encrypted` in SharedPreferences, Room, or a file
val decrypted = KeystoreHelper.decrypt(encrypted)
assert(token == decrypted)
}[!TIP]
Store only the ciphertext (or the Base64 string) in SharedPreferences or Room. Never persist the IV separately unless you prepend it as shown; losing the IV makes decryption impossible.
init()Application.onCreate()keyStore.deleteEntry(KEY_ALIAS)Even with a strong API, misuse can weaken security. Below are concrete do’s and don’ts derived from real‑world audits.
setUserAuthenticationRequired(true)setUserAuthenticationValidityDurationSecondsPURPOSE_SIGNKeyPermanentlyInvalidatedExceptionCipher[!WARNING]
Never rely on Keystore alone to protect data if the device is compromised via a firmware exploit. Treat it as a layer in defense‑in‑depth, not a silver bullet.
A typical architecture places the Keystore helper inside a
RepositoryViewModel@Composable
fun SecretTokenScreen(viewModel: SecretTokenViewModel = hiltViewModel()) {
val tokenState by viewModel.token.collectAsState()
val loading by viewModel.isLoading.collectAsState()
Column(modifier = Modifier.padding(16.dp)) {
TextField(
value = tokenState ?: "",
onValueChange = { viewModel.updateToken(it) },
label = { Text("API Token") },
isError = tokenState.isNullOrEmpty(),
placeholder = { Text("Enter token") }
)
Spacer(modifier = Modifier.height(8.dp))
if (loading) {
CircularProgressIndicator(modifier = Modifier.align(Alignment.CenterHorizontally))
} else {
Button(
onClick = { viewModel.saveToken() },
enabled = tokenState.isNotBlank()
) {
Text("Save Securely")
}
}
}
}@HiltViewModel
class SecretTokenViewModel @Inject constructor(
private val repository: SecretRepository
) : ViewModel() {
private val _token = MutableStateFlow<String?>(null)
val token: StateFlow<String?> = _token.asStateFlow()
private val _isLoading = MutableStateFlow(false)
val isLoading: StateFlow<Boolean> = _isLoading.asStateFlow()
fun updateToken(newToken: String) {
_token.value = newToken
}
fun saveToken() {
val token = _token.value ?: return
viewModelScope.launch {
_isLoading.value = true
try {
repository.storeToken(token)
_token.value = null // clear UI after success
} finally {
_isLoading.value = false
}
}
}
}The repository simply delegates to
KeystoreHelperclass SecretRepository @Inject constructor() {
fun storeToken(token: String) {
val encrypted = KeystoreHelper.encrypt(token)
// PreferenceDao is a thin wrapper around DataStore or SharedPreferences
preferenceDao.saveEncryptedToken(encrypted)
}
fun getToken(): String? {
val encrypted = preferenceDao.getEncryptedToken()
return encrypted?.let { KeystoreHelper.decrypt(it) }
}
}[!IMPORTANT]
Always clear plaintext secrets from memory as soon as they are no longer needed. In Kotlin, overwrite mutableorcodeByteArraywith zeros after use, or usecodeCharArrayfor arrays.codeapply { fill(0) }
KeyPermanentlyInvalidatedExceptionBy following the patterns above, you’ll move from “hoping no one decompiles my APK” to “knowing that even a rooted phone cannot pull my encryption keys out of the device’s secure vault.” Start integrating Keystore today, and sleep a little easier knowing your users’ secrets are guarded by hardware, not hope.
Sudarshan Chaudhari
AI Systems Builder / Product Engineer
Bangkok, Thailand
Solo Android developer with 13+ years in QA, building Android apps, AI automation systems, and developer tools at SudarshanTechLabs.
Related Posts
Related Apps
Real-time family location sharing — Firebase Realtime DB for sub-second propagation, WorkManager + ForegroundService for OS-compliant background collection, geofencing via Google Maps API.
Building something? Available for Android dev and QA consulting.
Work with meComments — powered by Giscus
Real-time family location sharing — Firebase Realtime DB for sub-second propagation, WorkManager + ForegroundService for OS-compliant background collection, geofencing via Google Maps API.
ReadPrivate dream journal — structured entry capture, pattern tagging, and optional Claude-powered insight generation. All data stays on-device by default.
ReadWorkout tracker — exercise logging with set/rep/weight history, goal progression, and local Room DB persistence. No account, no cloud sync required.
Read