# iOS Keychain vs. Android Keystore

Based on insights shared by the Talsee community, guests from Tide (and a little help of AI).

{% embed url="<https://youtu.be/nB4hULJ4HFg?feature=shared>" %}

### 📌 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.

<figure><img src="https://1548930415-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FNjTFXsqCLQ3RU2oA2uHC%2Fuploads%2FPkbfLVa3ErpOUGYsncQr%2Fhow%20to%20detect%20root.png?alt=media&#x26;token=e74c3ecb-e9b0-4109-9f77-431046e6801b" alt=""><figcaption></figcaption></figure>

### 🧠 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

```swift
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

```kotlin
// 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.

```kotlin
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**

```kotlin
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:

```swift
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               | <p>Use .biometryCurrentSet or handle KeyPermanentlyInvalidatedException</p><p><br></p>        |
| 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

* <https://docs.talsec.app/appsec-articles/articles/how-to-implement-secure-storage-in-flutter>
* <https://docs.talsec.app/appsec-articles/articles/podcast-ios-keychain-vs-android-keystore>
* <https://docs.talsec.app/appsec-articles/articles/owasp-top-10-for-flutter-m3-insecure-authentication-and-authorization-in-flutter>
* <https://docs.talsec.app/appsec-articles/articles/secure-storage-what-flutter-can-do-what-flutter-could-do>
* [Apple Keychain Services Docs](https://developer.apple.com/documentation/security/keychain_services)
* [Android Keystore System Docs](https://developer.android.com/training/articles/keystore)
* [Talsec App Protection SDK](https://www.talsec.app/)
* [Mobile Security Testing Guide (OWASP MSTG)<br>](https://owasp.org/www-project-mobile-security-testing-guide/)
