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.

🛡️ Threat
🔍 Real-world Example
📉 Without Secure 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.

Feature
iOS Keychain
Android Keystore

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.

Use Case
iOS Keychain
Android Keystore
Notes

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.

Tool
Platform
Use Case

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.

🔥 Pitfall
😱 Impact
✅ Fix

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.

Tradeoff
Example
Mitigation

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?