Storing auth tokens, API keys, or PII in AsyncStorage writes plaintext to the device filesystem with no access controls beyond the app sandbox. On a rooted or jailbroken device — a realistic threat model, not an edge case — any process can read AsyncStorage without authentication. CWE-311 (Missing Encryption of Sensitive Data) and CWE-312 (Cleartext Storage of Sensitive Information) directly describe this defect. OWASP Mobile Top 10 M9 and GDPR Art. 32 both mandate encryption for personal data at rest. A compromised device that exposes a plaintext refresh token enables full account takeover; the attacker does not need the user's password.
Critical because plaintext token storage in AsyncStorage exposes credentials to any process on a rooted device, enabling account takeover without requiring the user's password.
Move all tokens, API keys, and PII out of AsyncStorage and into platform-secure storage (iOS Keychain, Android Keystore) via expo-secure-store or react-native-keychain. Never call AsyncStorage.setItem with a token or secret.
// npm install expo-secure-store
import * as SecureStore from 'expo-secure-store';
export const Auth = {
async saveTokens(access: string, refresh: string) {
await Promise.all([
SecureStore.setItemAsync('access_token', access),
SecureStore.setItemAsync('refresh_token', refresh),
]);
},
async getAccessToken() {
return SecureStore.getItemAsync('access_token');
},
async clearTokens() {
await Promise.all([
SecureStore.deleteItemAsync('access_token'),
SecureStore.deleteItemAsync('refresh_token'),
]);
},
};
Search your codebase for AsyncStorage.setItem calls and audit every stored key against your sensitive-data inventory. Any key storing a token, credential, or PII field must migrate to SecureStore.
ID: mobile-offline-storage.storage-security.encryption-at-rest
Severity: critical
What to look for: Examine what data is stored locally and how. Look for encryption of sensitive data (auth tokens, passwords, PII). Check for use of encrypted storage libraries like expo-secure-store or encrypted SQLite/MMKV.
Pass criteria: Enumerate all sensitive data types stored locally (tokens, API keys, PII). Sensitive data (tokens, API keys, PII) must be encrypted at rest using a platform-provided secure storage or encryption library. AsyncStorage (if used) must only contain non-sensitive data. At least 1 secure storage library (expo-secure-store, Keychain, Keystore) must be used for credentials. Report even on pass: "Found X sensitive data items, all stored via secure storage. Y non-sensitive items in AsyncStorage."
Fail criteria: Sensitive data is stored in plain text in AsyncStorage, shared preferences, or local files without encryption.
Skip (N/A) when: Never — encryption is essential for security.
Cross-reference: The Security Headers audit (security-headers) checks server-side security that complements client-side encryption at rest.
Detail on fail: "Authentication tokens stored in plain text in AsyncStorage. Rooted devices or app backups could expose user credentials."
Remediation: Use secure storage for sensitive data:
// Install: npm install expo-secure-store
import * as SecureStore from 'expo-secure-store';
export async function storeAuthToken(token: string) {
try {
await SecureStore.setItemAsync('authToken', token);
} catch (error) {
console.error('Failed to store token:', error);
}
}
export async function getAuthToken(): Promise<string | null> {
try {
return await SecureStore.getItemAsync('authToken');
} catch (error) {
console.error('Failed to retrieve token:', error);
return null;
}
}
export async function deleteAuthToken() {
try {
await SecureStore.deleteItemAsync('authToken');
} catch (error) {
console.error('Failed to delete token:', error);
}
}
For React Native (non-Expo), use react-native-keychain:
// Install: npm install react-native-keychain
import * as Keychain from 'react-native-keychain';
export async function storeAuthToken(token: string) {
await Keychain.setGenericPassword('authToken', token);
}
export async function getAuthToken(): Promise<string | null> {
const credentials = await Keychain.getGenericPassword();
return credentials ? credentials.password : null;
}
Never store sensitive data in AsyncStorage:
// ❌ DON'T DO THIS
await AsyncStorage.setItem('authToken', token); // Insecure!
// ✅ DO THIS INSTEAD
await SecureStore.setItemAsync('authToken', token); // Secure