Android Keystore vs iOS Keychain — how do you actually store a secret securely on-device?
Storing a secret on-device isn't one API call — it's a decision about hardware-backed keys, biometric binding, and what happens if the device is rooted.
LEARN BY DOINGPut your intuition to work3 steps
Android Keystore vs iOS Keychain — how do you actually store a secret securely on-device?
Use the lesson’s examples and explanation to check your idea.
Personal reflection. Nothing is sent to an AI service.#Two very different security models
"Store a secret securely" is doing a lot of work in that question, because Android and iOS solve it in structurally different ways. Android's Keystore is a key management API: you ask it to generate or import a cryptographic key, and from that point on you never see the raw key bytes again — you only ever ask the provider to encrypt, decrypt, sign or verify on your behalf. iOS's Keychain is a secret storage API: it stores arbitrary encrypted values (passwords, tokens, small blobs) directly, indexed by attributes like service and account. Candidates who answer "they're basically the same thing" usually haven't had to build the encryption layer on top of Keystore themselves — Android gives you the safe, iOS gives you the safe and the thing you wanted to put in it.
That distinction matters in practice: on Android, storing a token means generating (or reusing) a Keystore-backed AES key, encrypting the token with it, and persisting the ciphertext somewhere — usually EncryptedSharedPreferences or a Room column, since Keystore itself has nowhere to put your data. On iOS, SecItemAdd does both jobs in one call: the OS encrypts your value at rest under a Data Protection class, and you get it back with SecItemCopyMatching. Both approaches ultimately terminate in the same hardware, though: on modern devices, key operations happen inside a Trusted Execution Environment (TEE) or a discrete Secure Element (Android's StrongBox, iOS's Secure Enclave), so the key material never touches the app's process memory or gets paged to disk in the clear.
#Android: generate the key, never touch the key
Here's a Keystore-backed AES-GCM key that requires a fresh biometric authentication before it can be used, and invalidates itself if the user enrolls a new fingerprint or face:
private const val KEY_ALIAS = "user_session_key"fun getOrCreateSecretKey(): SecretKey {val keyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) }(keyStore.getKey(KEY_ALIAS, null) as? SecretKey)?.let { return it }val keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore")val spec = KeyGenParameterSpec.Builder(KEY_ALIAS,KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT).setBlockModes(KeyProperties.BLOCK_MODE_GCM).setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE).setUserAuthenticationRequired(true).setUserAuthenticationParameters(0, KeyProperties.AUTH_BIOMETRIC_STRONG).setInvalidatedByBiometricEnrollment(true).setIsStrongBoxBacked(true) // falls back with StrongBoxUnavailableException on unsupported devices.build()keyGenerator.init(spec)return keyGenerator.generateKey()}
The actual persisted value never goes through raw SharedPreferences. Jetpack Security wraps exactly this pattern for you:
val masterKey = MasterKey.Builder(context).setKeyScheme(MasterKey.KeyScheme.AES256_GCM).build()val securePrefs = EncryptedSharedPreferences.create(context,"secure_prefs",masterKey,EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM)securePrefs.edit().putString("refresh_token", token).apply()
#iOS: one call encrypts and stores
The Keychain takes an access-control object that ties an item to the current biometric enrollment, mirroring setInvalidatedByBiometricEnrollment:
func saveToken(_ token: String) throws {guard let access = SecAccessControlCreateWithFlags(nil,kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly,.biometryCurrentSet,nil) else { throw KeychainError.accessControlFailed }let query: [String: Any] = [kSecClass as String: kSecClassGenericPassword,kSecAttrService as String: "com.myapp.session",kSecAttrAccount as String: "refresh_token",kSecValueData as String: token.data(using: .utf8)!,kSecAttrAccessControl as String: access]SecItemDelete(query as CFDictionary) // replace any stale item firstlet status = SecItemAdd(query as CFDictionary, nil)guard status == errSecSuccess else { throw KeychainError.saveFailed(status) }}
kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly is the detail people skip: without the ThisDeviceOnly suffix, an item can be restored from an iCloud/iTunes backup onto a different device, which usually isn't what you want for a session token.
#The mistake that undoes all of it
The single most common way senior candidates get this wrong isn't the storage API — it's gating the wrong thing with biometrics. Showing a BiometricPrompt and then setting isUnlocked = true in a view model is not secure: on a rooted or jailbroken device, that boolean lives in ordinary process memory, and a Frida script can flip it without ever touching a sensor. The fix is to pass a CryptoObject (Android) or perform the decryption inside the Secure Enclave-backed key's usage (iOS LAContext + access control) so the cryptographic operation itself fails unless the hardware just confirmed the biometric match. There's no boolean to hook, because there's no boolean in the trust chain at all.
Neither platform's guarantees survive a compromised OS: on a rooted Android device, an attacker with system privileges can potentially coerce the Keystore daemon itself, and jailbreak tooling has, at various points, extracted Keychain items class-protected below the strongest tiers. That's why device attestation (Play Integrity API, DeviceCheck/App Attest) is a separate control, not a replacement — Keystore and Keychain protect the data, attestation tells your backend whether it should trust the environment asking for it in the first place.
🧠 Keystore vs Keychain
0/3 · 0/3 answered1. What is the core structural difference between the Android Keystore and the iOS Keychain?
2. Why is gating decryption with a CryptoObject stronger than checking a boolean 'isAuthenticated' flag after a biometric prompt?
3. What does StrongBox add on supported Android devices?