# Sensitive data encrypted at rest

- **Pattern:** `ab-001885` (`mobile-offline-storage.storage-security.encryption-at-rest`)
- **Severity:** critical
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-001885
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-001885)

## Why it matters

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.

## Severity rationale

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.

## Remediation

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.

```tsx
// 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.

## Detection

- **ID:** `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:

  ```tsx
  // 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:

  ```tsx
  // 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:

  ```tsx
  // ❌ DON'T DO THIS
  await AsyncStorage.setItem('authToken', token); // Insecure!

  // ✅ DO THIS INSTEAD
  await SecureStore.setItemAsync('authToken', token); // Secure
  ```

## External references

- cwe CWE-312
- cwe CWE-311
- owasp A02
- gdpr Art. 32
- nist SC-28

Taxons: cryptography-and-secrets

HTML version: https://auditbuffet.com/patterns/ab-001885
