expo-secure-store & expo-crypto
Secrets, hashing, and random bytes on device - the expo-secure-store and expo-crypto cookbook for Expo SDK 57 apps that vault refresh tokens, derive fingerprints, and generate nonces.
Search across all documentation pages
Secrets, hashing, and random bytes on device - the expo-secure-store and expo-crypto cookbook for Expo SDK 57 apps that vault refresh tokens, derive fingerprints, and generate nonces.
Quick-reference recipe card - copy-paste ready.
npx expo install expo-secure-store expo-cryptoimport * as SecureStore from "expo-secure-store";
const REFRESH_KEY = "auth.refresh_token";
export async function saveRefreshToken(token: string) {
await SecureStore.setItemAsync(REFRESH_KEY, token, {
keychainAccessible: SecureStore.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
});
}
export async function loadRefreshToken(): Promise<string | null> {
return SecureStore.getItemAsync(REFRESH_KEY);
}
export async function clearAuthSecrets() {
await SecureStore.deleteItemAsync(REFRESH_KEY);
}import * as Crypto from "expo-crypto";
export async function sha256Hex(input: string) {
return Crypto.digestStringAsync(
Crypto.CryptoDigestAlgorithm.SHA256,
input
);
}
export function randomNonce(bytes = 16) {
return Crypto.getRandomBytes(bytes);
}When to reach for this:
code_verifier generation for OAuth - see auth-session.When to avoid:
Token vault adapter, logout purge, and hashed cache keys with secure random IDs.
npx expo install expo-secure-store expo-crypto// src/auth/secureTokenVault.ts
import * as SecureStore from "expo-secure-store";
const keys = {
refresh: (userId: string) => `auth.refresh_token.${userId}`,
device: "auth.device_binding",
} as const;
export const tokenVault = {
async saveRefresh(userId: string, token: string) {
await SecureStore.setItemAsync(keys.refresh(userId), token, {
keychainAccessible: SecureStore.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
});
},
async loadRefresh(userId: string) {
return SecureStore.getItemAsync(keys.refresh(userId));
},
async clearUser(userId: string) {
await SecureStore.deleteItemAsync(keys.refresh(userId));
},
async clearAllKnownUsers(userIds: string[]) {
await Promise.all(userIds.map((id) => tokenVault.clearUser(id)));
await SecureStore.deleteItemAsync(keys.device);
},
};// src/cache/cacheKey.ts
import * as Crypto from "expo-crypto";
export async function cacheKeyForUrl(url: string) {
const hash = await Crypto.digestStringAsync(
Crypto.CryptoDigestAlgorithm.SHA256,
url
);
return `cache:${hash.slice(0, 16)}`;
}// src/auth/logout.ts
import { tokenVault } from "./secureTokenVault";
export async function logout(userId: string, allUserIds: string[]) {
await tokenVault.clearAllKnownUsers(allUserIds);
// Also: queryClient.clear(), router.replace('/sign-in') - see auth-session
}What this demonstrates:
WHEN_UNLOCKED_THIS_DEVICE_ONLY - excludes iCloud Keychain sync.| Constraint | Detail |
|---|---|
| Item size | ~2048 bytes - store compact tokens only |
keychainAccessible | WHEN_UNLOCKED_THIS_DEVICE_ONLY recommended for refresh tokens |
| Web | Limited - use web-specific auth storage patterns |
| Expo Go | Works for dev - test Keychain on dev client before release |
await SecureStore.setItemAsync("pin.step_up", "verified", {
requireAuthentication: true, // iOS: Face ID to read
authenticationPrompt: "Confirm identity to view account",
});requireAuthentication is iOS-only biometric gate - pair with ../auth-session/biometrics-with-localauthentication/biometrics-with-localauthentication.md.| API | Purpose |
|---|---|
digestStringAsync | SHA-1/256/512 hex digests |
getRandomBytes | CSPRNG bytes for nonces |
getRandomBytesAsync | Async variant for large buffers |
randomUUID | UUID v4 strings |
import * as Crypto from "expo-crypto";
const verifier = Crypto.getRandomBytes(32);
const uuid = Crypto.randomUUID();getRandomBytes for PKCE verifiers - never Math.random().Refresh token → expo-secure-store
Access token → memory or short SecureStore session
User preferences → AsyncStorage / MMKV
Photos → expo-file-system paths// Minimum logout sequence
await SecureStore.deleteItemAsync("auth.refresh_token");
queryClient.clear();
router.replace("/sign-in");userId - auth.refresh_token.${userId}.Math.random for OAuth verifier - Predictable. Fix: Crypto.getRandomBytes.deleteItemAsync list.
| Alternative | Use When | Don't Use When |
|---|---|---|
expo-secure-store | Tokens, small secrets | Multi-KB JSON documents |
| Encrypted MMKV | Large encrypted KV with custom key | Refresh tokens without hardware backing |
| Server-only sessions | Web-style cookie apps | Mobile offline refresh needs |
expo-crypto digests | Cache keys, fingerprints | Encrypting large files at rest |
npx expo install expo-secure-store expo-cryptoNo config plugin required for basic SecureStore on SDK 57.
Stack versions: This page was written for React 19.2.3, React Native 0.86.0, and Expo SDK 57 (
expo~57.0.4).
Reviewed by Chris St. John·Last updated Jul 19, 2026