Q&A
How to store sensitive data in a React Native app
Overview
Mobile apps often need to persist secrets: auth tokens, refresh tokens, API keys, biometric flags. The wrong place - AsyncStorage, Redux persistence, or hard-coded constants - leaves them readable by anyone with device or bundle access. The right place is the OS-provided secure store, which encrypts data and is backed by device hardware (Secure Enclave / TEE).
graph TD
A[Sensitive data?] --> B[❌ AsyncStorage: plain text]
A --> C[❌ Hard-coded in bundle: reverse-engineerable]
A --> D[✓ Secure storage]
D --> D1[iOS Keychain]
D --> D2[Android Keystore / EncryptedSharedPreferences]
D --> E[Via react-native-keychain / expo-secure-store]
style D fill:#1e1b4b,color:#fff
Why Not AsyncStorage?
AsyncStorage is unencrypted key-value storage - data sits in plain text in the app's sandbox, readable on rooted/jailbroken devices, in backups, or via debugging tools. It's fine for non-sensitive preferences (theme, last tab) but never for secrets.
Option 1: react-native-keychain (bare RN)
Wraps iOS Keychain and Android Keystore with a simple API:
import * as Keychain from "react-native-keychain";
// store
await Keychain.setGenericPassword("authToken", token, {
accessible: Keychain.ACCESSIBLE.WHEN_UNLOCKED, // only when device unlocked
});
// retrieve
const creds = await Keychain.getGenericPassword();
if (creds) { const token = creds.password; }
// remove
await Keychain.resetGenericPassword();
It can also require biometric authentication (Face ID / fingerprint) to unlock the stored value.
Option 2: expo-secure-store (Expo)
import * as SecureStore from "expo-secure-store";
await SecureStore.setItemAsync("authToken", token);
const token = await SecureStore.getItemAsync("authToken");
await SecureStore.deleteItemAsync("authToken");
Backed by Keychain (iOS) and encrypted storage (Android) under the hood.
What "Hard-Coding Secrets" Gets Wrong
Putting an API key or secret directly in JS/.env bundled into the app does not hide it - the JavaScript bundle ships to the device and can be extracted and inspected. Client apps fundamentally can't keep an embedded secret secret. The right architecture:
- Keep true secrets (third-party API secrets, signing keys) on a backend; the app calls your backend, which holds the secret.
- Store only per-user, revocable tokens on the device (in secure storage).
- Use short-lived tokens and refresh flows so a compromised token has limited value.
Defense in Depth
- Transport: always HTTPS/TLS; consider certificate pinning for high-value apps.
- Token lifetime: short-lived access tokens + refresh tokens; revoke on logout.
- Biometric gating: require Face ID/fingerprint to read the most sensitive items.
- Don't log secrets; scrub them from crash reports and analytics.
- Detect compromised devices (root/jailbreak) for very sensitive apps.
Why This Matters in Interviews
This is a mobile security question testing judgment. The essentials: use platform secure storage (Keychain/Keystore) via react-native-keychain or expo-secure-store; never use AsyncStorage for secrets (it's plain text); and understand that embedding a secret in the app can't truly hide it - real secrets belong on a server, while the device holds only revocable, short-lived tokens. Mentioning biometric gating, HTTPS, and token rotation rounds out a strong answer.
Common Pitfalls
- Storing tokens/keys in
AsyncStorage(unencrypted) or in Redux-persist to disk. - Believing a
.env/hard-coded key in the bundle is hidden (it's extractable). - Putting third-party API secrets in the client instead of behind a backend.
- Logging secrets or including them in crash/analytics payloads.
- Ignoring token expiry/revocation, so a leaked token works indefinitely.
References
- react-native-keychain: https://github.com/oblador/react-native-keychain
- Expo SecureStore: https://docs.expo.dev/versions/latest/sdk/securestore/