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

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. Half the breaches we see start with "we stored the refresh token in AsyncStorage."

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 built into the SDK. It wraps iOS Keychain and Android Keystore. You don't install anything. You don't touch native code. 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 library every serious production app we audit has installed. 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. It's fine. It's not as good as Keychain/SecureStore — you're asking JavaScript-adjacent code to do the encryption instead of leaning on OS primitives with hardware backing.

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: the default on older versions of the library was WHEN_UNLOCKED (without THIS_DEVICE_ONLY). That backs up. If the user restores their iPhone, the token travels. You don't want that 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 man-in-the-middle attacks using compromised or user-installed certificates. react-native-ssl-pinning and react-native-ssl-public-key-pinning are the two options most teams use.

Pin the leaf certificate or, better, the public key. Ship the pin 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 two certificates (primary + backup) 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 if regulated
Bare React Native react-native-keychain Pin if regulated
Stuck in AsyncStorage Migrate to above One-shot migration on launch
Need biometric-gated access react-native-keychain w/ accessControl Separate entries per sensitivity
Regulated industry (finance/health/gov) Keychain/SecureStore + SSL pinning Document rotation procedure
Consumer app, no regulatory pressure 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. About half the apps we see still have auth material somewhere they shouldn't. 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 with hardware backing on most modern devices. 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 in Keychain, protected by the Secure Enclave on supporting devices. On Android it uses the Keystore system with hardware-backed keys where available. The library is a thin bridge; the security guarantees are the OS's.

Do I need SSL pinning for a React Native app?

If you're in regulated verticals (finance, health, government) or handling auth tokens for high-value accounts, yes. SSL pinning blocks man-in-the-middle attacks via compromised CAs or user-installed root certs. For most consumer apps it's overkill and a support headache — rotation mistakes ship more outages than attacks.

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?

On managed Expo, no — native module. On Expo with a dev client or after expo prebuild, yes. If you're on managed Expo and don't want to eject, 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 two certificates — the current one and the next one. Ship the next 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). Recognize that determined attackers bypass detection; it raises cost, doesn't close attack surface.