iOS Keychain vs. Android Keystore
Deep Dive for Mobile Engineers, Architects & Security Professionals
Based on insights shared by the Talsee community, guests from Tide (and a little help of AI).
📌 Overview
Storing sensitive data securely on mobile devices is not optional—it’s a foundational part of secure app design. Whether you're protecting access tokens, private keys, or biometric credentials, both iOS and Android provide secure storage APIs:
iOS Keychain: Apple’s encrypted container for small secrets.
Android Keystore System: Cryptographic framework with hardware-backed protection.
This article compares both in depth, explores their limitations, gives code samples, and explains real-world attack surfaces and defenses.

🧠 Why Secure Storage Matters
Let’s begin with the “why.” Many app developers underestimate threats like token extraction, file tampering, or insecure credential caching. But without secure storage, all other defenses become brittle.
We outline real-world attack scenarios and the necessity of relying on OS-level cryptographic APIs rather than home-grown encryption or local file storage.
Token theft
Reverse engineering is used to obtain the JWT or OAuth token.
Identity theft, unauthorized access
Device rooting
User/root attacker extracts auth credentials
Fraud, session hijacking
Insecure fallback
Weak encryption when keystore unavailable
GDPR/PCI violations
🔄 Architecture Summary
Now that we understand the stakes, let’s compare the core architecture of both systems.
This chapter maps out how the iOS Keychain and Android Keystore are designed, how they differ in scope, and what developers can rely on when targeting modern (and older) devices.
Primary Use
Secure key management + crypto operations
Secure key management + crypto operations
Hardware-backed
✅ Secure Enclave (since iPhone 5s)
✅ TEE or StrongBox (if supported)
Item Export
❌ No (keys stay in)
❌ No (keys stay in)
Biometric Integration
✅ LAContext + SecAccessControl
✅ BiometricPrompt + setUserAuthenticationRequired()
App-to-app sharing
✅ via Access Groups
❌ Isolated per app
Large data storage
🚫No (encrypt large data with key)
🚫No (encrypt large data with key)
Enforced key invalidation
✅ With .biometryCurrentSet ACL
✅ With biometric invalidation settings via .setInvalidatedByBiometricEnrollment(true)
📦 Use-Case Matrix with Limitations
Each platform has strong suits and weak spots. To help you design for both Android and iOS, this section introduces a detailed use-case vs capability matrix showing which platform supports what, and under what conditions.
Store API access tokens
✅ Fully supported
✅ Via encrypting with generated key
Use symmetric keys for speed
Store refresh tokens
✅ With unlock control
✅ With biometric or PIN auth
Consider short-lived tokens
Store biometric-gated keys
✅ With Secure Enclave
✅ StrongBox or TEE, if available
Device-specific availability
Encrypt local DB/files
✅ Encrypt key in Keychain
✅ AES key stored in Keystore
Use wrapper libraries (e.g. SQLCipher)
Store session data or cache
🚫 Use disk or memory
🚫Use disk or memory
Use memory-only or encrypted files
Cross-device key sync
✅ With iCloud Keychain
❌ Not supported
iCloud Keychain opt-in only
Shared credentials across apps
✅ Via Access Groups
✅ Via AccountManager
Let’s take a closer look at the four most common use cases from the list above.
🧪 1. Secure Token Storage Example
The most common use case in mobile apps is storing authentication tokens securely — whether it’s a short-lived access token or a long-lived refresh token.
Here we dive into hands-on examples that demonstrate the correct way to store tokens with biometric enforcement, both on iOS and Android.
iOS: Store Auth Token with Face ID Protection
import Security
// The token to be stored. In a real app, this wouldn't be hardcoded.
let token = "sensitive_token"
// 1. Create an access control object that requires the current set of biometrics.
// This policy is enforced when you later try to *read* the item.
let access = SecAccessControlCreateWithFlags(
nil,
kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
.biometryCurrentSet, // The key is invalidated if biometrics change.
nil
)!
// 2. Define the attributes for the new keychain item.
// An LAContext is NOT needed to add an item, only to retrieve it.
let attributes: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: "com.example.app",
kSecAttrAccount as String: "authToken",
kSecValueData as String: token.data(using: .utf8)!,
kSecAttrAccessControl as String: access // Apply the access control policy.
]
// To ensure this code can be re-run, delete any existing item first.
SecItemDelete(attributes as CFDictionary)
// 3. Add the item to the Keychain.
let status = SecItemAdd(attributes as CFDictionary, nil)
if status == errSecSuccess {
print("✅ Token stored successfully. Future access will require biometrics.")
} else {
print("❌ Error storing token: \(status)")
}
biometryCurrentSet: Invalidate if Face ID/Touch ID enrollment changes.
ThisDeviceOnly: Data won't migrate to other devices or backups.
Android: Encrypt Token with Keystore-Backed AES Key
// auth_token_key - The token to be stored. In a real app, this wouldn't be hardcoded.
val keyGenSpec = KeyGenParameterSpec.Builder(
"auth_token_key",
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
).apply {
setBlockModes(KeyProperties.BLOCK_MODE_GCM)
setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
setUserAuthenticationRequired(true)
setUserAuthenticationValidityDurationSeconds(60)
}.build()
val keyGenerator = KeyGenerator.getInstance("AES", "AndroidKeyStore")
keyGenerator.init(keyGenSpec)
val secretKey = keyGenerator.generateKey()
// Use AES encryption with this key
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
cipher.init(Cipher.ENCRYPT_MODE, secretKey)
val iv = cipher.iv
val encrypted = cipher.doFinal("token_value".toByteArray())
setUserAuthenticationRequired(true): Tied to biometric/PIN for decryption.
GCM: Provides encryption + integrity via MAC.
🔒 2. Biometric Invalidation Edge Case
While biometric enforcement is a powerful tool, it introduces complexity. What happens when the user adds a new fingerprint or resets Face ID?
We explain how each platform handles changes in biometric configuration — and how to build apps that detect invalidation and gracefully recover when cryptographic material is no longer accessible.
iOS behavior:
Use .biometryCurrentSet to force key/token invalidation if fingerprint/face data changes.
The Keychain item is not deleted, but it becomes permanently inaccessible because the underlying encryption key has been discarded by the Secure Enclave. An attempt to read the item will fail with an authentication error, typically errSecUserCanceled (if the system prompt is dismissed) or errSecAuthFailed, not errSecItemNotFound. The item is still technically present but cannot be decrypted.
Android behavior:
When biometrics change, KeyPermanentlyInvalidatedException is thrown.
The Omission: Key invalidation on biometric change only occurs for keys that were generated with setUserAuthenticationRequired(true). If a key is created in the Android Keystore without this flag, it is not tied to the user's authentication state and will not be invalidated if fingerprints or faces are changed. This is a vital detail for developers deciding which level of security to apply to different keys.
You must catch the exception and regenerate the key.
try {
cipher.init(Cipher.DECRYPT_MODE, key)
} catch (e: KeyPermanentlyInvalidatedException) {
// Biometrics changed — regenerate key
}
⚙️ 3. Key Rotation Strategy
Security isn’t static — and neither should your encryption keys be. Whether for compliance or good hygiene, apps should rotate keys regularly or on key events like logout.
This section shows how to build key rotation strategies on both platforms using built-in tools — including setting expiration dates and regenerating keys securely.
Don't reuse the same key indefinitely. Instead:
Rotate on logout / login
Set short validity periods
The iOS Keychain Services API has no built-in mechanism for key expiration equivalent to Android's setKeyValidityEnd. To implement key rotation on iOS, a you must manually store metadata (like a creation timestamp) along with the Keychain item and write application-level logic to check this timestamp and perform the rotation.
Invalidate when permissions or user state changes
Android: Set key expiration
setKeyValidityStart(Date())
setKeyValidityEnd(Calendar.getInstance().apply {
add(Calendar.DAY_OF_MONTH, 30)
}.time)
📂 4. Secure File Encryption Pattern
Sometimes you need to protect more than just a 256-bit token. This chapter covers how to encrypt larger content — such as local database files — by using AES keys stored in the Keychain or Keystore.
We introduce a hybrid encryption strategy: store small symmetric keys securely, then use those to encrypt larger payloads.
iOS Concept:
let aesKey = generateAESKey()
storeInKeychain(key: aesKey)
encryptFile(data: fileData, with: aesKey)
This stores the key inside the regular memory of the application for the brief period making it vulnerable. As a better approach you could use SecKeyCreateRandomKey that makes sure the entire process is done inside the Secure Enclave.
Android Concept:
val aesKey = getKeyFromKeystore("file_key")
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
cipher.init(Cipher.ENCRYPT_MODE, aesKey)
val encryptedFile = cipher.doFinal(fileData)
🔁 Never store encryption keys in plain preferences or files.
Dev & Testing Tools best practices
Implementing secure storage is just the first step. Validating it is where true security lies.
This chapter introduces security testing tools used in both pentesting and automated CI/CD pipelines. Whether you’re red-teaming your own apps or building test automation, these tools will help uncover vulnerabilities in storage logic, fallback behavior, and rooted/jailbroken environments.
Frida
Both
Dynamic analysis, hook crypto APIs
MobSF
Both
Static/dynamic app security scanning
Keychain Dumper
iOS
Inspect Keychain entries on jailbroken devices
Objection
Both
Runtime inspection & patching
Talsec SDK
Both
Runtime protection, root detection, jailbreak, device binding, device lock, secure HW presence check, app integrity
🔍 6. Common Pitfalls & Prevention
Even experienced developers make mistakes: storing secrets in preferences, misconfiguring biometric policies, or assuming parity across devices.
Here we list the most common issues seen in audits and how to proactively address them.
Storing tokens in UserDefaults / SharedPreferences
Easily extractable
Always use Keychain/Keystore
Not checking biometric state changes
Crashes or bypass
Use .biometryCurrentSet or handle KeyPermanentlyInvalidatedException
Using software-only Keystore on Android
Weak security on budget devices
Check with isInsideSecureHardware() + learn trends in your app using https://my.talsec.app/
Storing large files in Keychain/Keystore
Performance & failure
Store AES key, encrypt files separately
🚧 Security vs Usability
Security always competes with usability — especially in mobile UX. This chapter explores trade-offs like biometric lockouts, token persistence, and device migration.
We explain how to tune secure storage behavior based on your risk model and user expectations.
Biometric friction
Re-auth required every 30 sec
Increase setUserAuthenticationValidityDurationSeconds
Compatibility issues
Older Androids lacking TEE
Either allow and accept the risk, or limit to nonsensitive operations
Shared iCloud Keychain
iOS devices sync sensitive tokens
Use ThisDeviceOnly for high-security needs
🎯 Final Recommendations
We close with a concise checklist that distills everything into a go-to reference for engineers, architects, and product leads.
✅ Use hardware-backed storage when available ✅ Treat tokens and credentials like passwords ✅ Implement key rotation policies ✅ Test with rooted/jailbroken devices ✅ Regularly audit your secure storage logic ✅ Don’t assume biometric == secure without checking hardware ✅ Monitoring cryptographic exceptions is crucial for detecting security attacks, debugging user issues, and maintaining overall application health.
📚 Further Reading & Tools
Last updated
Was this helpful?