Security & CVEs · 9 min read

React Native Keychain and secure storage: what to use, what to skip

By React Native Rescue · Published April 15, 2026 · Updated September 13, 2026

TL;DR Use react-native-keychain (bare) or Expo SecureStore (managed) for tokens, credentials, and anything sensitive. Never AsyncStorage. SSL pinning is a network-layer complement, not a substitute. Privileged API keys are a different problem: they don't belong in the app at all.

The storage question in React Native sounds like a tooling question. It's a security question dressed as a tooling question.

AsyncStorage is the default option every tutorial reaches for. It's plaintext. It lives on disk. On a jailbroken or rooted device it's a file. On a backed-up device, the backup often includes it. And it's where most teams still put the access token the moment they ship.

Here's the field guide.

The three options, ranked by when you should use them

1. Expo SecureStore — the right default for managed Expo apps

If you're on managed Expo, start here. SecureStore is part of the Expo SDK: install it with npx expo install expo-secure-store. It stores values in the iOS Keychain and in SharedPreferences encrypted with the Android Keystore. You don't touch native code. Expo's docs note two limits: large values can be rejected, and on Android the stored entries should be excluded from Auto Backup because they can't be decrypted after a restore. The API is four functions and they all do the right thing.

import * as SecureStore from 'expo-secure-store';

await SecureStore.setItemAsync('refreshToken', token);
const token = await SecureStore.getItemAsync('refreshToken');
await SecureStore.deleteItemAsync('refreshToken');

Works. Ship it. The only time to reach past SecureStore on managed Expo is when you need something it doesn't expose — biometric-gated access, access groups, item sharing across apps, or fine-grained accessibility flags.

2. react-native-keychain — the right default for bare React Native

On bare React Native, react-native-keychain is the one. It's the most common choice in the apps we audit. Same underlying primitives (iOS Keychain, Android Keystore), richer API surface.

import * as Keychain from 'react-native-keychain';

// Store
await Keychain.setGenericPassword('user', token, {
  accessible: Keychain.ACCESSIBLE.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
  accessControl: Keychain.ACCESS_CONTROL.BIOMETRY_ANY,
});

// Retrieve
const credentials = await Keychain.getGenericPassword();
if (credentials) {
  const { username, password } = credentials;
}

// Delete
await Keychain.resetGenericPassword();

The interesting part is the options object. Get these wrong and you get security theater. Get them right and the data is genuinely protected.

3. Encrypted AsyncStorage libs — only when you can't use the above

Libraries like react-native-encrypted-storage exist because some teams need a drop-in replacement for AsyncStorage with encryption on top. Check what they wrap before you adopt one. react-native-encrypted-storage, for example, wraps iOS Keychain and Android's EncryptedSharedPreferences, which Jetpack deprecated in androidx.security:security-crypto 1.1.0 in favor of using Android Keystore directly, and its last npm release was in November 2022.

Use it only when you've got a concrete reason you can't use Keychain or SecureStore. Migrating away from AsyncStorage gradually is a legit reason. "I heard it was simpler" is not.

The options that actually matter on react-native-keychain

Most Keychain bugs we find in audits are misconfigured options, not missing ones. The library is correct. The config is wrong.

accessible — when the item is readable

Controls when iOS will unlock the item. Most teams want WHEN_UNLOCKED_THIS_DEVICE_ONLY. It means: only when the device is unlocked, and don't back this up to iCloud / restore to another device.

The footgun: accessibility values without THIS_DEVICE_ONLY let the item move to a new device through a backup restore, so the token travels with it. Check the default in the library version you use instead of relying on it, and set the value explicitly for auth material.

accessControl — who can read the item

This is the biometric gate. Options like BIOMETRY_ANY, BIOMETRY_CURRENT_SET, and USER_PRESENCE mean the OS requires Face ID / Touch ID / PIN before releasing the value.

The tradeoff: every getGenericPassword() becomes a biometric prompt. Put access-token refresh behind biometry and your app prompts constantly. Gate the long-lived credential (refresh token, keychain-wrapped password) with biometry; keep short-lived access tokens in memory or in a separate, non-gated entry.

service — namespace your entries

If your app stores more than one secret, use service to namespace them. Otherwise every call reads and writes the same slot and you get weird bugs where logging out of one identity also wipes credentials for another.

What never to store in AsyncStorage

Not a complete list. A starting list.

AsyncStorage is fine for UI state, last-viewed IDs, cached non-sensitive API responses, feature flags, onboarding progress. Anything an attacker with file-system access wouldn't care about.

The network-side complement: SSL pinning

Secure storage protects data at rest. SSL pinning protects it in transit — specifically against interception with a certificate the device trusts but you didn't authorize, such as a mis-issued one. On Android 7 and later, apps already ignore user-installed CAs by default. If you pin, start with platform configuration (Android Network Security Config, iOS NSPinnedDomains) before a library such as react-native-ssl-public-key-pinning.

Pin public keys rather than certificates. Ship the pins in the app bundle. If the server presents a cert whose key doesn't match, the request fails closed.

The honest downside: pinning creates a rotation problem. If your server's certificate chain changes and you didn't ship a new app with an updated pin, every user gets a hard failure. We've seen this take down apps for a full day because nobody in the rotation playbook remembered the mobile app.

Pragmatic stance:

If you pin, pin at least two public keys, including a backup key fully in your control, and document the rotation procedure in the same place as your DNS and TLS cert rotation playbook. The mobile app has to ship before the server cut-over.

Decision table

You are... Use Also consider
Managed Expo Expo SecureStore Pin only if your threat model calls for it
Bare React Native react-native-keychain Pin only if your threat model calls for it
Stuck in AsyncStorage Migrate to above One-shot migration on launch
Need biometric-gated access react-native-keychain w/ accessControl Separate entries per sensitivity
High-value app (banking, payments) Keychain/SecureStore; pinning if the threat model calls for it Document rotation procedure
Most other apps Keychain/SecureStore Skip pinning; fix storage first

Migrating off AsyncStorage without breaking users

If you're reading this and you have tokens in AsyncStorage today, the migration is a one-shot, version-gated function on app launch.

const MIGRATION_VERSION = 'v1-2026-05';

async function migrateToSecureStorage() {
  const done = await AsyncStorage.getItem('migration:secureStorage');
  if (done === MIGRATION_VERSION) return;

  const token = await AsyncStorage.getItem('refreshToken');
  if (token) {
    await Keychain.setGenericPassword('user', token, {
      accessible: Keychain.ACCESSIBLE.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
    });
    await AsyncStorage.removeItem('refreshToken');
  }

  await AsyncStorage.setItem('migration:secureStorage', MIGRATION_VERSION);
}

Run it in your app-launch bootstrap, before any authenticated request fires. Version the marker so it doesn't re-run. Drop the AsyncStorage read path in the next release — leaving it behind is how legacy code wins.

One thing we see every audit: teams add react-native-keychain, migrate the new flow, and never go back to clean up the AsyncStorage reads. A month later someone adds a new feature that reaches for the old AsyncStorage key because the example in the internal wiki shows it. Delete the example.

Common mistakes, ranked by frequency

  1. Tokens in AsyncStorage. The default mistake. Most prevalent.
  2. Keychain with wrong accessible flag. Allowing iCloud backup of auth material.
  3. Biometric prompt on every token refresh. Destroys UX; users turn off biometry.
  4. SSL pinning without a rotation plan. The cert changes, the app breaks, the team gets paged.
  5. Storing OAuth client secrets in the app bundle. They're extractable from any compiled binary. Move the flow.
  6. "JavaScript-layer encryption" of AsyncStorage with a hardcoded key. The key is in the bundle. This is not encryption.
  7. Leftover debug logging of secrets. console.log(token) in production. Check your Sentry / Crashlytics breadcrumbs.

What this looks like in an audit

When we run a React Native security audit, secure storage is the first thing we check. It takes 30 minutes and it's the highest-signal finding we ever deliver. Auth material in the wrong place is one of the most common findings. Usually it's one of: AsyncStorage, a plaintext config file, a hardcoded OAuth client secret, or a debug log that still prints tokens.

Fix this layer and you've closed the most common path to a serious breach. Then worry about pinning, obfuscation, jailbreak detection — the layers above.

Want us to audit your React Native secure storage?

We run fixed-price security audits that include storage review, dependency CVE scan, and SSL pinning assessment. Two weeks, senior engineers, written report.

See the Security Audit →

Frequently Asked Questions

What's the difference between react-native-keychain and AsyncStorage?

AsyncStorage is plaintext key-value storage. react-native-keychain wraps iOS Keychain and Android Keystore — OS-level secure storage that can use hardware-backed keys where the device supports them. Don't store credentials, tokens, or PII in AsyncStorage. Use Keychain (or Expo SecureStore) for anything sensitive.

Should I use react-native-keychain or Expo SecureStore?

On managed Expo, use SecureStore — it's built-in and covers the same underlying Keychain/Keystore APIs for the common case. On bare React Native, or when you need advanced features (biometric auth, access control, sharing across apps), use react-native-keychain.

Is react-native-keychain actually encrypted?

Yes — but the encryption is the OS's, not the library's. On iOS it stores items in the Keychain, an encrypted database whose availability depends on the accessibility setting and device passcode. On Android it relies on the Keystore system, which can keep key material in secure hardware where available. The library is a bridge; the protection comes from the OS, and it protects user data on the device, not secrets shipped inside the app.

Do I need SSL pinning for a React Native app?

Usually not. Apple says pinning is not necessary in most cases and should be avoided, and Android does not recommend it, because certificate changes can lock users out. Consider it for high-value apps whose threat model includes interception with a trusted-but-rogue certificate, and remember it can be bypassed on a device the attacker controls.

What if I already stored tokens in AsyncStorage?

Write a one-shot migration on app launch: read from AsyncStorage, write to Keychain/SecureStore, wipe AsyncStorage. Version the migration so it only runs once. Drop the AsyncStorage read path in the next release so legacy code doesn't win.

Does react-native-keychain work on Expo?

Not in Expo Go, because it's a native module. In an Expo development build or after npx expo prebuild, yes. If you want to stay in Expo Go, SecureStore covers the common case. If you need what only react-native-keychain offers, you're probably already past managed workflow anyway.

How do I handle SSL pinning certificate rotation?

Pin at least two public keys: the current one and a backup key you control. Ship the backup pin in a release before the server cuts over. Document the rotation in the same playbook as your DNS/TLS rotation. The most common outage isn't the attack — it's forgetting the mobile app in the rotation sequence.

Should I also use jailbreak / root detection?

Layer four or five on the defense stack. Fix storage and transport first. Add detection if your threat model justifies it (finance, enterprise), preferably backed by server-verified attestation such as Play Integrity or App Attest. OWASP notes that local root detection is not very effective by itself and can be bypassed; it raises cost, it doesn't close attack surface.