For a React Native app, how does a token kept in the iOS Keychain differ from one protected by the Android Keystore?
answer
- one stores data, one stores keys
- Keychain item holds the secret itself
- Keystore key is non-exportable
- ciphertext sits in SharedPreferences
- Keychain can outlive an uninstall
basics
~20 sThe iOS Keychain stores the token itself as an OS-encrypted item; the Android Keystore stores only a non-exportable key, so libraries encrypt the token with it and keep the ciphertext in app storage. Their lifecycles differ too.
solid answer
~40 sOn iOS a secure-storage library writes the token as a **Keychain item** (a generic-password item); the OS encrypts it and an accessibility class such as when-unlocked decides when it can be read. On Android the **Keystore holds keys, not data**: `expo-secure-store`, for example, generates an AES-256-GCM key in `AndroidKeyStore`, encrypts the token with it and stores the ciphertext in `SharedPreferences`. The app can use the key but never read its bytes. The lifecycles differ: Keychain items can survive an uninstall and come back on reinstall with the same bundle ID, while Keystore keys are deleted with the app, so leftover ciphertext becomes undecryptable.
go deeper
Recall the split: the Keychain stores the secret itself, the Keystore stores a key that encrypts the secret kept elsewhere.
Walk through the Android two-step (Keystore key, ciphertext in SharedPreferences) and name the iOS accessibility classes and what ThisDeviceOnly changes.
Connect the lifecycle table to real bugs: ghost logins after an iOS reinstall, null tokens after an Android restore, and how the app should recover from each.
Discuss when platform differences justify a first-launch wipe, a ThisDeviceOnly class or forced re-login, weighing support load against the risk of a token outliving its owner.
## Two platforms, two different primitives React Native libraries such as `expo-secure-store` and `react-native-keychain` give you one call to save a string securely, but underneath they drive two facilities that are **not equivalent**. Interviewers ask this to see whether a candidate knows what actually happens on each platform, because the differences leak into app behaviour: reinstalls, backups, device migration and biometric changes. ## iOS: the Keychain stores the secret **Keychain Services** is an OS-managed database for small secrets. A library stores the token as a keychain item; `expo-secure-store` uses the generic-password class and puts the key name and a service name on the item. The OS encrypts the item, and an **accessibility class** decides when it can be decrypted: | Accessibility class (Apple name) | Readable when | Migrates to a new device from a backup | |---|---|---| | `kSecAttrAccessibleWhenUnlocked` | the device is unlocked | yes | | `kSecAttrAccessibleAfterFirstUnlock` | after the first unlock since boot | yes | | `kSecAttrAccessibleWhenUnlockedThisDeviceOnly` | the device is unlocked | no | | `kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly` | unlocked, and only while a passcode is set | no | `expo-secure-store` defaults to the when-unlocked class, and marks its always-accessible options as deprecated because they are the least protective. ## Android: the Keystore holds a key, not the token The **Android Keystore** is a container for cryptographic keys. Its defining property is that keys are **non-exportable**: the app can ask the Keystore to encrypt or decrypt with a key, but it never gets the key bytes. So a Keystore-backed library works in two steps: 1. Generate (once) a symmetric key inside `AndroidKeyStore`. `expo-secure-store` creates an AES key of 256 bits used in GCM mode. 2. Encrypt the token with that key and write the **ciphertext** to ordinary app storage; `expo-secure-store` keeps it in a `SharedPreferences` file. The React Native security guide also mentions Jetpack's Encrypted Shared Preferences, which follows the same "Keystore key encrypts the stored value" pattern. ## Where the lifecycles diverge | Event | iOS Keychain item | Android Keystore-backed value | |---|---|---| | App uninstalled, then reinstalled | can **survive** if the bundle ID is the same (Expo's docs say not to rely on it either way) | the key is **deleted** with the app; any restored ciphertext cannot be decrypted | | Device backup restored to a new phone | migrates unless the class is a `ThisDeviceOnly` one | ciphertext without its key is useless; for that reason `expo-secure-store`'s config plugin excludes its data from Android Auto Backup when the app has no custom backup rules | | Biometric enrollment changes, item bound to biometrics | item becomes unreadable | key is permanently invalidated; `expo-secure-store` then returns `null` | ## Choosing a wrapper library `expo-secure-store` and `react-native-keychain` both sit on these same two primitives, so the choice is about surface, not about a stronger vault. Questions worth asking of either: - Does it let you pick the iOS **accessibility class**, including the `ThisDeviceOnly` ones? - Does it expose a **biometric access-control** setting, and how does it report an invalidated item? - What does it do with **undecryptable Android data**: return `null`, throw, or leave the stale entry? - Does it run where you develop: `expo-secure-store` is available in Expo Go, while a third-party native module needs a development build. ## What this means in a React Native codebase - **Expect `null` on Android after edge events.** When `expo-secure-store` finds ciphertext it can no longer decrypt, for example after a reinstall, it deletes the stale entry and returns `null` rather than throwing. Your code must treat a missing token as "sign in again", not as a crash. - **Expect ghosts on iOS.** A reinstalled app can find a Keychain item from the previous install; if that is not what you want, clear credentials on first launch. - **Keep values small.** These are stores for tokens, not documents; Expo notes that some iOS releases historically rejected values above roughly 2048 bytes. - **Test on real devices.** Simulators and emulators do not enforce biometric checks on retrieval the way real devices do. - **Neither is a vault against a compromised OS.** Both raise the cost of extraction; neither makes a rooted or jailbroken device safe.
- Why can an Android app not simply read its own Keystore key and store it somewhere convenient?Keystore keys are non-exportable by design: the app gets a handle to request encrypt and decrypt operations, never the key material. That is what stops a file-reading attacker from taking the key along with the ciphertext.
- What happens on Android if someone restores `expo-secure-store`'s SharedPreferences from a backup onto a fresh install?The ciphertext arrives without the Keystore key that encrypted it, so decryption fails. `expo-secure-store` treats that as out-of-sync data, removes the entry and returns `null`; its config plugin also excludes that data from Auto Backup, when the app has no custom backup rules, to avoid the situation.
saying these in an interview costs you the question
- The Android Keystore stores the token string itself.
- The app can export its Keystore key and cache it in a file.
- Keychain items are always deleted when the app is uninstalled.
- iOS and Android secure storage behave identically across reinstalls.
- A Keychain item marked ThisDeviceOnly still migrates to a new phone.