Claude Skill

mobile-security-react-native

Secure storage, certificate pinning, biometric auth, jailbreak detection, code obfuscation, network security, screenshot prevention for React Native

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download agents-inc-skills-dist_plugins_mobile-security-react-native_skills_mobile-security-react-native-3a51ef5.zip · 17 KB
Part of agents-inc/skills — 130 skills

Install

skills CLI npx skills add https://github.com/agents-inc/skills/tree/main/dist/plugins/mobile-security-react-native/skills/mobile-security-react-native
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install agents-inc-skills@llmmart
Git git clone https://github.com/agents-inc/skills.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole agents-inc/skills collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

React Native Security Patterns

Quick Guide: Defense-in-depth: layer secure storage (expo-secure-store or react-native-keychain), certificate pinning, biometric authentication, jailbreak/root detection, and code obfuscation. Never store secrets in AsyncStorage or JS bundles. Use Hermes bytecode as your first obfuscation layer. iOS Keychain persists across reinstalls; Android Keystore does not. Certificate pins require at least two hashes (primary + backup) on iOS.


<critical_requirements>

CRITICAL: Before Using This Skill

All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering, import type, named constants)

(You MUST NEVER store tokens, passwords, API keys, or PII in AsyncStorage or plain-text files -- use hardware-backed secure storage)

(You MUST use at least two public key hashes for certificate pinning on iOS -- TrustKit/iOS enforces this and will throw if only one is provided)

(You MUST treat jailbreak/root detection as one layer in defense-in-depth -- client-side checks can be bypassed, always validate server-side too)

(You MUST configure both iOS ATS and Android Network Security Config to enforce HTTPS -- never ship with NSAllowArbitraryLoads: true in production)

(You MUST add NSFaceIDUsageDescription to Info.plist when using Face ID -- the OS silently falls back to passcode without it)

</critical_requirements>


Auto-detection: secure storage, SecureStore, expo-secure-store, react-native-keychain, Keychain, Keystore, certificate pinning, SSL pinning, react-native-ssl-public-key-pinning, TrustKit, jailbreak detection, root detection, jail-monkey, biometric authentication, expo-local-authentication, Face ID, Touch ID, fingerprint, code obfuscation, Hermes bytecode, ProGuard, R8, screen capture prevention, App Transport Security, Network Security Config, MITM

When to use:

  • Storing credentials, tokens, or sensitive data on device
  • Implementing certificate pinning to prevent MITM attacks
  • Adding biometric authentication (Face ID, Touch ID, fingerprint)
  • Detecting jailbroken/rooted devices
  • Hardening builds with code obfuscation (Hermes, ProGuard/R8)
  • Preventing screenshot/screen recording of sensitive screens
  • Configuring network security (ATS on iOS, Network Security Config on Android)

When NOT to use:

  • General React Native component architecture (not a security concern)
  • Server-side API security (use your backend security approach)
  • Web-only applications (web security patterns differ fundamentally)

Key patterns covered:

  • Secure storage with expo-secure-store and react-native-keychain
  • Certificate pinning with react-native-ssl-public-key-pinning
  • Biometric authentication with expo-local-authentication and react-native-keychain
  • Jailbreak/root detection with jail-monkey
  • Code obfuscation: Hermes bytecode, Metro transformer, ProGuard/R8
  • Network security: iOS ATS and Android Network Security Config
  • Screenshot and screen recording prevention
  • Defense-in-depth strategy and security layering

Detailed Resources:

  • examples/core.md - Secure storage, certificate pinning, biometric auth
  • examples/hardening.md - Code obfuscation, jailbreak detection, screenshot prevention, network config
  • reference.md - Security checklist, library API reference, pin hash commands



<decision_framework>

Decision Framework

Secure Storage Choice

Need to store credentials/tokens securely?
+-- Using Expo managed workflow?
|   +-- YES -> expo-secure-store (simpler API, Expo-native)
|   +-- NO  -> react-native-keychain (more control, biometric options)
|
+-- Need biometric-gated credential retrieval?
|   +-- YES -> react-native-keychain with ACCESS_CONTROL.BIOMETRY_ANY
|   +-- OR  -> expo-secure-store with requireAuthentication: true
|
+-- Value larger than 2KB?
|   +-- YES -> react-native-keychain (no size limit)
|   +-- NO  -> Either library works
|
+-- Need credential persistence across app reinstalls?
    +-- iOS -> Both persist (Keychain behavior)
    +-- Android -> Neither persists (cleared on uninstall)

Biometric Authentication Choice

Need biometric prompt (no credential storage)?
+-- YES -> expo-local-authentication
|
Need biometric-gated credential storage/retrieval?
+-- YES -> react-native-keychain with accessControl
|
Need to distinguish biometric security level (weak vs strong)?
+-- YES -> expo-local-authentication (provides SecurityLevel enum)

Certificate Pinning Approach

Need SSL pinning?
+-- JS-level (works with all HTTP clients)?
|   +-- YES -> react-native-ssl-public-key-pinning
|
+-- Native-level (defense-in-depth)?
|   +-- iOS -> TrustKit (via CocoaPods)
|   +-- Android -> Network Security Config XML
|
+-- Best practice -> Both JS-level AND native-level

Security Layering

Minimum viable security:
1. Secure storage (expo-secure-store or react-native-keychain)
2. HTTPS enforcement (ATS + Network Security Config)
3. Hermes bytecode (default since RN 0.70)

Standard security (most apps):
+ Certificate pinning
+ Biometric authentication
+ ProGuard/R8 on Android

High security (banking, healthcare, fintech):
+ Jailbreak/root detection
+ JS code obfuscation transformer
+ Screenshot prevention
+ Server-side device attestation
+ Runtime integrity checks

</decision_framework>


<red_flags>

RED FLAGS

High Priority Issues:

  • Storing tokens or credentials in AsyncStorage -- it is a plain-text file, trivially readable on jailbroken/rooted devices
  • Storing API keys or secrets in the JS bundle -- the bundle is extractable from any published app
  • Shipping with NSAllowArbitraryLoads: true in production Info.plist -- disables ATS entirely, allows HTTP
  • Using only one public key hash for certificate pinning on iOS -- TrustKit throws, pinning silently fails
  • Relying solely on jailbreak detection for security -- client-side checks are bypassable with Frida/Objection

Medium Priority Issues:

  • Not checking hasHardwareAsync and isEnrolledAsync before calling authenticateAsync -- crashes or confusing UX on devices without biometrics
  • Missing NSFaceIDUsageDescription in Info.plist -- Face ID silently falls back to passcode without any error
  • Using ACCESSIBLE.ALWAYS for Keychain items -- allows access even when device is locked (deprecated on iOS)
  • Skipping ProGuard/R8 on Android release builds -- native code is trivially decompilable without it
  • Not setting expirationDate on certificate pins -- expired certificates brick the app until users update

Gotchas & Edge Cases:

  • iOS Keychain data persists across app reinstalls (same bundle ID); Android Keystore data does not -- plan token refresh accordingly
  • expo-secure-store has a ~2KB value size limit -- large tokens or data blobs will silently fail or throw
  • z.coerce.boolean() treats string "false" as truthy -- when parsing security config from strings, use explicit comparison
  • Android FLAG_SECURE blocks screenshots reliably but screen recording prevention varies by OS version and manufacturer
  • Hermes bytecode can be decompiled with tools like hbctool -- it raises the bar but is not true encryption
  • react-native-obfuscating-transformer's stringArray option breaks React Native builds -- avoid it
  • Android biometrics have "weak" (2D face) vs "strong" (fingerprint, 3D face) security levels -- financial apps should require strong
  • Certificate pins must be rotated before expiry -- set calendar reminders and use the expirationDate field as a safety net
  • Keychain.ACCESS_CONTROL.BIOMETRY_CURRENT_SET invalidates credentials when biometrics are re-enrolled -- use BIOMETRY_ANY for persistence across biometric changes

</red_flags>


<critical_reminders>

CRITICAL REMINDERS

All code must follow project conventions in CLAUDE.md

(You MUST NEVER store tokens, passwords, API keys, or PII in AsyncStorage or plain-text files -- use hardware-backed secure storage)

(You MUST use at least two public key hashes for certificate pinning on iOS -- TrustKit/iOS enforces this and will throw if only one is provided)

(You MUST treat jailbreak/root detection as one layer in defense-in-depth -- client-side checks can be bypassed, always validate server-side too)

(You MUST configure both iOS ATS and Android Network Security Config to enforce HTTPS -- never ship with NSAllowArbitraryLoads: true in production)

(You MUST add NSFaceIDUsageDescription to Info.plist when using Face ID -- the OS silently falls back to passcode without it)

Failure to follow these rules will expose user credentials, enable MITM attacks, and create false security assumptions.

</critical_reminders>

Files (skills)
  • examples
    • core.md 13.2 KB
      # React Native Security - Core Patterns
      
      > Secure storage, certificate pinning, and biometric authentication. See [SKILL.md](../SKILL.md) for decision guidance and red flags.
      
      **Prerequisites**: Familiarity with React Native development and async/await patterns.
      
      ---
      
      ## Pattern 1: Secure Storage with expo-secure-store
      
      ### Basic Token Storage
      
      ```typescript
      import * as SecureStore from "expo-secure-store";
      
      const AUTH_TOKEN_KEY = "auth-token";
      const REFRESH_TOKEN_KEY = "refresh-token";
      
      // ---- Store ----
      
      async function storeTokens(
        accessToken: string,
        refreshToken: string,
      ): Promise<void> {
        await SecureStore.setItemAsync(AUTH_TOKEN_KEY, accessToken);
        await SecureStore.setItemAsync(REFRESH_TOKEN_KEY, refreshToken);
      }
      
      // ---- Retrieve ----
      
      async function getAccessToken(): Promise<string | null> {
        return SecureStore.getItemAsync(AUTH_TOKEN_KEY);
      }
      
      // ---- Delete ----
      
      async function clearTokens(): Promise<void> {
        await SecureStore.deleteItemAsync(AUTH_TOKEN_KEY);
        await SecureStore.deleteItemAsync(REFRESH_TOKEN_KEY);
      }
      ```
      
      **Why good:** each token has a named constant key, separate store/retrieve/delete functions, hardware-backed encryption on both platforms
      
      ### Biometric-Protected Storage
      
      ```typescript
      import * as SecureStore from "expo-secure-store";
      
      const BIOMETRIC_TOKEN_KEY = "biometric-protected-token";
      
      async function storeBiometricProtectedToken(token: string): Promise<void> {
        await SecureStore.setItemAsync(BIOMETRIC_TOKEN_KEY, token, {
          requireAuthentication: true,
          authenticationPrompt: "Authenticate to save your credentials",
          keychainAccessible: SecureStore.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
        });
      }
      
      async function getBiometricProtectedToken(): Promise<string | null> {
        try {
          return await SecureStore.getItemAsync(BIOMETRIC_TOKEN_KEY, {
            requireAuthentication: true,
            authenticationPrompt: "Authenticate to access your credentials",
          });
        } catch (error) {
          // User cancelled biometric prompt or auth failed
          return null;
        }
      }
      ```
      
      **Why good:** `requireAuthentication` gates reads behind biometric/passcode, `WHEN_UNLOCKED_THIS_DEVICE_ONLY` prevents backup extraction, error handling covers cancelled prompts
      
      ### Bad Example: AsyncStorage for Sensitive Data
      
      ```typescript
      // BAD: AsyncStorage is a plain-text file on device
      import AsyncStorage from "@react-native-async-storage/async-storage";
      
      await AsyncStorage.setItem("auth-token", token); // Readable on rooted devices
      await AsyncStorage.setItem("user-password", password); // NEVER do this
      ```
      
      **Why bad:** AsyncStorage stores data as unencrypted JSON on the filesystem -- trivially readable on jailbroken/rooted devices, no encryption, no access control
      
      ---
      
      ## Pattern 2: Secure Storage with react-native-keychain
      
      ### Credential Storage with Biometric Protection
      
      ```typescript
      import * as Keychain from "react-native-keychain";
      
      const AUTH_SERVICE = "com.myapp.auth";
      
      async function storeCredentials(
        username: string,
        token: string,
      ): Promise<boolean> {
        try {
          await Keychain.setGenericPassword(username, token, {
            service: AUTH_SERVICE,
            accessControl: Keychain.ACCESS_CONTROL.BIOMETRY_ANY_OR_DEVICE_PASSCODE,
            accessible: Keychain.ACCESSIBLE.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
            authenticationType: Keychain.AUTHENTICATION_TYPE.BIOMETRICS,
          });
          return true;
        } catch (error) {
          return false;
        }
      }
      
      async function getCredentials(): Promise<{
        username: string;
        password: string;
      } | null> {
        try {
          const credentials = await Keychain.getGenericPassword({
            service: AUTH_SERVICE,
            authenticationPrompt: {
              title: "Authenticate",
              subtitle: "Verify your identity to access credentials",
              cancel: "Cancel",
            },
          });
          if (credentials === false) return null;
          return { username: credentials.username, password: credentials.password };
        } catch (error) {
          // Biometric auth failed or was cancelled
          return null;
        }
      }
      
      async function clearCredentials(): Promise<void> {
        await Keychain.resetGenericPassword({ service: AUTH_SERVICE });
      }
      ```
      
      **Why good:** `BIOMETRY_ANY_OR_DEVICE_PASSCODE` provides fallback if biometrics unavailable, service name isolates credentials, `WHEN_UNLOCKED_THIS_DEVICE_ONLY` prevents backup extraction, error handling covers all failure paths
      
      ### Checking Biometric Availability
      
      ```typescript
      import * as Keychain from "react-native-keychain";
      
      async function getBiometricInfo(): Promise<{
        available: boolean;
        biometryType: string | null;
      }> {
        const biometryType = await Keychain.getSupportedBiometryType();
        return {
          available: biometryType !== null,
          biometryType, // "TouchID" | "FaceID" | "Fingerprint" | "Face" | "Iris" | null
        };
      }
      ```
      
      ---
      
      ## Pattern 3: expo-secure-store vs react-native-keychain Comparison
      
      | Feature                   | expo-secure-store                     | react-native-keychain                             |
      | ------------------------- | ------------------------------------- | ------------------------------------------------- |
      | **Workflow**              | Expo managed + bare                   | Bare RN (+ Expo dev builds)                       |
      | **API style**             | Key-value (string only)               | Username/password pairs                           |
      | **Value size limit**      | ~2KB                                  | No practical limit                                |
      | **Biometric gating**      | `requireAuthentication` option        | `ACCESS_CONTROL` enum (granular)                  |
      | **Access control**        | Basic (unlock/passcode)               | Fine-grained (biometry types, current set vs any) |
      | **iOS storage**           | Keychain (`kSecClassGenericPassword`) | Keychain (`kSecClassGenericPassword`)             |
      | **Android storage**       | Keystore-encrypted SharedPreferences  | Keystore (RSA or AES)                             |
      | **Persists on reinstall** | iOS: yes, Android: no                 | iOS: yes, Android: no                             |
      | **Sync API**              | `setItem`/`getItem` (sync)            | No sync API                                       |
      | **Internet credentials**  | No                                    | Yes (`setInternetCredentials`)                    |
      
      **Use expo-secure-store when:** Expo managed workflow, simple token storage, values under 2KB.
      
      **Use react-native-keychain when:** Need granular biometric access control, storing larger values, bare RN workflow, or need internet credential separation.
      
      ---
      
      ## Pattern 4: Certificate Pinning
      
      ### JS-Level Pinning with react-native-ssl-public-key-pinning
      
      ```typescript
      import { initializeSslPinning } from "react-native-ssl-public-key-pinning";
      
      const PIN_EXPIRATION = "2026-12-31";
      
      // Call as early as possible in app entry (before any network requests)
      async function setupCertificatePinning(): Promise<void> {
        try {
          await initializeSslPinning({
            "api.example.com": {
              includeSubdomains: true,
              publicKeyHashes: [
                // Primary certificate hash (current cert)
                "CLOmM1/OXvSPjw5UOYbAf9GKOxImEp9hhku9W90fHMk=",
                // Backup certificate hash (next cert -- REQUIRED on iOS)
                "hxqRlPTu1bMS/0DITB1SSu0vd4u/8l8TjPgfaAp63Gc=",
              ],
              expirationDate: PIN_EXPIRATION,
            },
          });
        } catch (error) {
          // Pinning failed to initialize -- block network access or alert
          throw new Error("Certificate pinning initialization failed");
        }
      }
      ```
      
      **Why good:** called at app entry before any requests, two hashes (primary + backup), expiration date prevents permanent bricking, error handling prevents silent failure
      
      See [reference.md](../reference.md) for pin hash generation commands.
      
      ### Certificate Rotation Strategy
      
      1. Generate hash for the NEW certificate before deploying it to the server
      2. Ship an app update that pins BOTH the current and new certificate hashes
      3. Deploy the new certificate to the server
      4. After the app update has propagated, remove the old hash in the next release
      5. Always keep `expirationDate` set -- it acts as a safety valve if rotation is missed
      
      ### Bad Example: Single Pin Without Expiration
      
      ```typescript
      // BAD: single pin + no expiration
      await initializeSslPinning({
        "api.example.com": {
          includeSubdomains: true,
          publicKeyHashes: [
            "CLOmM1/OXvSPjw5UOYbAf9GKOxImEp9hhku9W90fHMk=",
            // Missing backup hash -- iOS will THROW
          ],
          // Missing expirationDate -- app bricks if cert rotates
        },
      });
      ```
      
      **Why bad:** iOS (TrustKit) requires two hashes and throws with only one, no expiration date means the app permanently breaks when the certificate is rotated until users update
      
      ---
      
      ## Pattern 5: Biometric Authentication with expo-local-authentication
      
      ### Full Biometric Flow
      
      ```typescript
      import * as LocalAuthentication from "expo-local-authentication";
      
      type BiometricResult =
        | { success: true }
        | {
            success: false;
            reason: "no-hardware" | "not-enrolled" | "failed" | "cancelled";
          };
      
      async function authenticateWithBiometrics(): Promise<BiometricResult> {
        const hasHardware = await LocalAuthentication.hasHardwareAsync();
        if (!hasHardware) {
          return { success: false, reason: "no-hardware" };
        }
      
        const isEnrolled = await LocalAuthentication.isEnrolledAsync();
        if (!isEnrolled) {
          return { success: false, reason: "not-enrolled" };
        }
      
        const result = await LocalAuthentication.authenticateAsync({
          promptMessage: "Verify your identity",
          cancelLabel: "Cancel",
          disableDeviceFallback: false, // Allow passcode as backup
          fallbackLabel: "Use passcode", // iOS only
        });
      
        if (result.success) {
          return { success: true };
        }
      
        return {
          success: false,
          reason: result.error === "user_cancel" ? "cancelled" : "failed",
        };
      }
      ```
      
      **Why good:** checks hardware and enrollment before prompting (prevents confusing errors), typed result discriminated union, passcode fallback enabled, handles cancellation distinctly from failure
      
      ### Checking Available Biometric Types
      
      ```typescript
      import * as LocalAuthentication from "expo-local-authentication";
      
      async function getAvailableBiometrics(): Promise<string[]> {
        const types = await LocalAuthentication.supportedAuthenticationTypesAsync();
      
        return types.map((type) => {
          switch (type) {
            case LocalAuthentication.AuthenticationType.FINGERPRINT:
              return "Fingerprint";
            case LocalAuthentication.AuthenticationType.FACIAL_RECOGNITION:
              return "Face Recognition";
            case LocalAuthentication.AuthenticationType.IRIS:
              return "Iris"; // Android only
            default:
              return "Unknown";
          }
        });
      }
      ```
      
      ---
      
      ## Pattern 6: Combined Biometric + Secure Storage Flow
      
      The most secure pattern: biometric-gated credential retrieval. The credentials never leave secure storage without biometric verification.
      
      ### With react-native-keychain
      
      ```typescript
      import * as Keychain from "react-native-keychain";
      
      const CREDENTIAL_SERVICE = "com.myapp.credentials";
      
      async function setupBiometricLogin(
        username: string,
        token: string,
      ): Promise<boolean> {
        const biometryType = await Keychain.getSupportedBiometryType();
        if (!biometryType) return false;
      
        try {
          await Keychain.setGenericPassword(username, token, {
            service: CREDENTIAL_SERVICE,
            accessControl: Keychain.ACCESS_CONTROL.BIOMETRY_ANY_OR_DEVICE_PASSCODE,
            accessible: Keychain.ACCESSIBLE.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
          });
          return true;
        } catch {
          return false;
        }
      }
      
      async function biometricLogin(): Promise<{
        username: string;
        token: string;
      } | null> {
        try {
          const credentials = await Keychain.getGenericPassword({
            service: CREDENTIAL_SERVICE,
            authenticationPrompt: {
              title: "Sign In",
              subtitle: "Use biometrics to access your account",
              cancel: "Cancel",
            },
          });
      
          if (credentials === false) return null;
          return { username: credentials.username, token: credentials.password };
        } catch {
          // Biometric auth failed, cancelled, or credentials don't exist
          return null;
        }
      }
      ```
      
      **Why good:** credentials are stored once during initial login, subsequent logins require biometric verification to read them back, the OS handles the biometric prompt natively (no custom UI), `BIOMETRY_ANY_OR_DEVICE_PASSCODE` provides fallback
      
      ### With expo-secure-store
      
      ```typescript
      import * as SecureStore from "expo-secure-store";
      import * as LocalAuthentication from "expo-local-authentication";
      
      const SECURE_TOKEN_KEY = "biometric-session-token";
      
      async function setupBiometricSession(token: string): Promise<boolean> {
        const hasHardware = await LocalAuthentication.hasHardwareAsync();
        const isEnrolled = await LocalAuthentication.isEnrolledAsync();
      
        if (!hasHardware || !isEnrolled) return false;
      
        await SecureStore.setItemAsync(SECURE_TOKEN_KEY, token, {
          requireAuthentication: true,
          authenticationPrompt: "Authenticate to enable biometric login",
        });
      
        return true;
      }
      
      async function biometricSessionRetrieve(): Promise<string | null> {
        try {
          // The OS automatically prompts for biometric when requireAuthentication was set
          return await SecureStore.getItemAsync(SECURE_TOKEN_KEY, {
            requireAuthentication: true,
            authenticationPrompt: "Authenticate to sign in",
          });
        } catch {
          return null;
        }
      }
      ```
      
      **Why good:** `requireAuthentication` on both set and get ensures biometric gating, the OS handles prompt presentation, two-step check (hardware + enrollment) before setup
      
    • hardening.md 13 KB
      # React Native Security - App Hardening
      
      > Code obfuscation, jailbreak detection, screenshot prevention, and network security configuration. See [SKILL.md](../SKILL.md) for decision guidance and red flags.
      
      **Related**: [core.md](core.md) for secure storage, certificate pinning, and biometric auth.
      
      ---
      
      ## Pattern 1: Jailbreak/Root Detection
      
      ### Basic Detection with jail-monkey
      
      ```typescript
      import JailMonkey from "jail-monkey";
      
      interface DeviceSecurityReport {
        isCompromised: boolean;
        isJailbroken: boolean;
        canMockLocation: boolean;
        isDebugMode: boolean;
        isOnExternalStorage: boolean; // Android only
      }
      
      function assessDeviceSecurity(): DeviceSecurityReport {
        const isJailbroken = JailMonkey.isJailBroken();
        const canMockLocation = JailMonkey.canMockLocation();
        const isDebugMode = JailMonkey.isDebuggedMode();
        const isOnExternalStorage = JailMonkey.isOnExternalStorage();
      
        return {
          isCompromised: isJailbroken || isDebugMode,
          isJailbroken,
          canMockLocation,
          isDebugMode,
          isOnExternalStorage,
        };
      }
      ```
      
      **Why good:** checks multiple indicators, combines jailbreak + debug for `isCompromised` flag, structured return for logging/reporting
      
      ### Response Strategy
      
      ```typescript
      import { Alert } from "react-native";
      
      type SecurityAction = "block" | "warn" | "log";
      
      const SECURITY_ACTIONS: Record<string, SecurityAction> = {
        jailbroken: "warn", // Alert user but allow access
        debugMode: "log", // Log only (may be legitimate dev use)
        mockLocation: "block", // Block for location-sensitive features
      };
      
      function handleCompromisedDevice(report: DeviceSecurityReport): void {
        if (report.isJailbroken && SECURITY_ACTIONS.jailbroken === "warn") {
          Alert.alert(
            "Security Notice",
            "This device appears to be modified. Some features may be restricted.",
            [{ text: "I Understand" }],
          );
        }
      
        if (report.canMockLocation && SECURITY_ACTIONS.mockLocation === "block") {
          // Disable location-dependent features
        }
      
        // Always report to server for monitoring
        reportSecurityEvent({
          type: "device-integrity",
          ...report,
          timestamp: Date.now(),
        });
      }
      ```
      
      **Why good:** configurable response per threat type (not blanket block), server-side reporting for monitoring, user-friendly messaging
      
      ### Bad Example: Blocking Without Server Validation
      
      ```typescript
      // BAD: relying solely on client-side detection
      if (JailMonkey.isJailBroken()) {
        // Attacker hooks JailMonkey.isJailBroken() to return false
        exitApp(); // Bypassed trivially with Frida
      }
      ```
      
      **Why bad:** client-side checks are bypassable with hooking frameworks (Frida, Objection), no server-side validation means the check provides false security assurance
      
      ---
      
      ## Pattern 2: Code Obfuscation
      
      ### Layer 1: Hermes Bytecode (Default)
      
      Hermes is enabled by default since React Native 0.70. It compiles JavaScript to optimized bytecode at build time.
      
      **Verify Hermes is enabled:**
      
      ```json
      // app.json (Expo)
      {
        "expo": {
          "jsEngine": "hermes"
        }
      }
      ```
      
      ```groovy
      // android/gradle.properties (bare RN)
      hermesEnabled=true
      ```
      
      Hermes bytecode is NOT encrypted -- tools like `hbctool` can decompile it. It raises the reverse engineering bar but is not a substitute for proper secret management.
      
      ### Layer 2: Metro Obfuscation Transformer
      
      Adds JavaScript-level obfuscation (variable renaming, control flow flattening, dead code injection) on top of Hermes.
      
      ```javascript
      // metro.config.js
      const { getDefaultConfig } = require("@react-native/metro-config");
      
      const config = getDefaultConfig(__dirname);
      
      config.transformer = {
        ...config.transformer,
        babelTransformerPath: require.resolve("react-native-obfuscating-transformer"),
      };
      
      module.exports = config;
      ```
      
      **Obfuscation config (obfuscating-transformer.config.js):**
      
      ```javascript
      module.exports = {
        // Files to obfuscate (regex pattern)
        filter: (filename) => {
          return (
            filename.startsWith("src/") &&
            !filename.includes("__tests__") &&
            !filename.includes(".test.")
          );
        },
        // Safe options -- avoid stringArray (breaks builds)
        compact: true,
        controlFlowFlattening: true,
        controlFlowFlatteningThreshold: 0.5,
        deadCodeInjection: true,
        deadCodeInjectionThreshold: 0.2,
        identifierNamesGenerator: "hexadecimal",
        renameGlobals: false, // Renaming globals breaks RN module system
        // DO NOT enable stringArray -- known to break React Native builds
      };
      ```
      
      **Why good:** filter excludes test files, conservative thresholds balance security vs performance, `renameGlobals: false` prevents module resolution breaks, explicit warning about `stringArray`
      
      ### Layer 3: ProGuard/R8 for Android
      
      R8 (ProGuard replacement) shrinks, obfuscates, and optimizes Android native/Java code.
      
      ```groovy
      // android/app/build.gradle
      android {
          buildTypes {
              release {
                  minifyEnabled true
                  shrinkResources true
                  proguardFiles getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"
              }
          }
      }
      ```
      
      ```proguard
      # android/app/proguard-rules.pro
      
      # Keep React Native bridge classes
      -keep class com.facebook.react.** { *; }
      -keep class com.facebook.hermes.** { *; }
      -keep class com.facebook.jni.** { *; }
      
      # Keep classes used via reflection
      -keepclassmembers class * {
          @com.facebook.react.uimanager.annotations.ReactProp <methods>;
      }
      
      # Keep native methods
      -keepclasseswithmembernames class * {
          native <methods>;
      }
      
      # Keep serializable classes (if using)
      -keepclassmembers class * implements java.io.Serializable {
          static final long serialVersionUID;
          private static final java.io.ObjectStreamField[] serialPersistentFields;
          !static !transient <fields>;
          private void writeObject(java.io.ObjectOutputStream);
          private void readObject(java.io.ObjectInputStream);
      }
      
      # Keep your app's model classes if accessed via reflection
      # -keep class com.yourapp.models.** { *; }
      ```
      
      **Why good:** `minifyEnabled true` activates R8, React Native bridge classes preserved, native methods preserved, reflection-accessed classes protected with `-keep` rules
      
      **Important:** R8 only runs in release builds. Test release builds thoroughly -- crashes from missing classes only appear in production.
      
      ---
      
      ## Pattern 3: Screenshot and Screen Recording Prevention
      
      ### Expo ScreenCapture (Managed Workflow)
      
      ```typescript
      import { useEffect } from "react";
      import { useIsFocused } from "@react-navigation/native";
      import * as ScreenCapture from "expo-screen-capture";
      
      /**
       * Hook to prevent screen capture while a screen is focused.
       * Enables capture again when navigating away or unmounting.
       */
      function usePreventCapture(): void {
        const isFocused = useIsFocused();
      
        useEffect(() => {
          if (isFocused) {
            ScreenCapture.preventScreenCaptureAsync();
          } else {
            ScreenCapture.allowScreenCaptureAsync();
          }
      
          return () => {
            ScreenCapture.allowScreenCaptureAsync();
          };
        }, [isFocused]);
      }
      
      // Usage in a sensitive screen
      function BankingScreen() {
        usePreventCapture();
        return <AccountDetails />;
      }
      ```
      
      **Why good:** per-screen control (not global), cleanup restores capture on navigate away, isFocused integration with navigation lifecycle
      
      ### Screenshot Listener (Detection Instead of Prevention)
      
      ```typescript
      import { useEffect } from "react";
      import * as ScreenCapture from "expo-screen-capture";
      
      function useScreenshotListener(onCapture: () => void): void {
        useEffect(() => {
          const subscription = ScreenCapture.addScreenshotListener(() => {
            onCapture();
          });
      
          return () => subscription.remove();
        }, [onCapture]);
      }
      
      // Usage: alert user and log event
      function SensitiveScreen() {
        useScreenshotListener(() => {
          Alert.alert("Screenshot Detected", "Screenshots of this screen are monitored.");
          reportSecurityEvent({ type: "screenshot-detected", screen: "sensitive" });
        });
      
        return <SensitiveContent />;
      }
      ```
      
      **Why good:** useful when you cannot prevent screenshots (some iOS scenarios) but want to detect and log them, subscription cleanup prevents memory leaks
      
      ### Android FLAG_SECURE (Bare RN)
      
      For bare React Native without Expo, use the native Android `FLAG_SECURE` flag:
      
      ```typescript
      import { Platform, NativeModules } from "react-native";
      
      // Requires a small native module that calls:
      // getWindow().setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE)
      
      function enableSecureMode(): void {
        if (Platform.OS === "android") {
          NativeModules.ScreenSecurity?.enableSecureFlag();
        }
      }
      
      function disableSecureMode(): void {
        if (Platform.OS === "android") {
          NativeModules.ScreenSecurity?.disableSecureFlag();
        }
      }
      ```
      
      **Platform differences:**
      
      | Platform              | Screenshots     | Screen Recording | App Switcher Thumbnail |
      | --------------------- | --------------- | ---------------- | ---------------------- |
      | iOS (ScreenCapture)   | Blocked (blank) | Blocked          | Not affected           |
      | Android (FLAG_SECURE) | Blocked (black) | Blocked (black)  | Black thumbnail        |
      
      ---
      
      ## Pattern 4: Network Security Configuration
      
      ### iOS App Transport Security (ATS)
      
      ATS enforces HTTPS by default since iOS 9. The correct production configuration is to NOT add exceptions.
      
      ```xml
      <!-- ios/YourApp/Info.plist -->
      <!-- PRODUCTION: Do NOT include NSAppTransportSecurity at all (defaults are secure) -->
      <!-- OR explicitly enforce: -->
      <key>NSAppTransportSecurity</key>
      <dict>
        <key>NSAllowArbitraryLoads</key>
        <false/>
      </dict>
      
      <!-- Face ID permission (REQUIRED when using biometrics) -->
      <key>NSFaceIDUsageDescription</key>
      <string>Use Face ID to securely access your account</string>
      ```
      
      **Development-only exception (NEVER ship this):**
      
      ```xml
      <!-- For local development server only -->
      <key>NSAppTransportSecurity</key>
      <dict>
        <key>NSExceptionDomains</key>
        <dict>
          <key>localhost</key>
          <dict>
            <key>NSTemporaryExceptionAllowsInsecureHTTPLoads</key>
            <true/>
          </dict>
        </dict>
      </dict>
      ```
      
      **Why good:** no blanket `NSAllowArbitraryLoads`, development exceptions scoped to localhost only, includes `NSFaceIDUsageDescription` for biometric features
      
      ### Bad Example: Disabling ATS
      
      ```xml
      <!-- BAD: disables ALL transport security -->
      <key>NSAppTransportSecurity</key>
      <dict>
        <key>NSAllowArbitraryLoads</key>
        <true/>
      </dict>
      ```
      
      **Why bad:** allows plain HTTP to any domain, Apple may reject apps with this in production, exposes all network traffic to interception
      
      ### Android Network Security Config
      
      ```xml
      <!-- android/app/src/main/res/xml/network_security_config.xml -->
      <network-security-config>
        <!-- Block all clear text traffic by default -->
        <base-config cleartextTrafficPermitted="false">
          <trust-anchors>
            <certificates src="system" />
          </trust-anchors>
        </base-config>
      
        <!-- Certificate pinning for your API -->
        <domain-config>
          <domain includeSubdomains="true">api.example.com</domain>
          <pin-set expiration="2026-12-31">
            <pin digest="SHA-256">CLOmM1/OXvSPjw5UOYbAf9GKOxImEp9hhku9W90fHMk=</pin>
            <pin digest="SHA-256">hxqRlPTu1bMS/0DITB1SSu0vd4u/8l8TjPgfaAp63Gc=</pin>
          </pin-set>
        </domain-config>
      
        <!-- Debug override: allow local dev server (debug builds only) -->
        <debug-overrides>
          <trust-anchors>
            <certificates src="user" />
          </trust-anchors>
        </debug-overrides>
      </network-security-config>
      ```
      
      **Reference the config in AndroidManifest.xml:**
      
      ```xml
      <!-- android/app/src/main/AndroidManifest.xml -->
      <application
        android:networkSecurityConfig="@xml/network_security_config"
        ...>
      ```
      
      **Why good:** clear text blocked globally, native pinning as defense-in-depth alongside JS-level pinning, debug-overrides allow local dev without compromising release builds, expiration date on pins
      
      ### Expo Configuration (app.json)
      
      ```json
      {
        "expo": {
          "plugins": [
            [
              "expo-build-properties",
              {
                "android": {
                  "enableProguardInReleaseBuilds": true,
                  "enableShrinkResourcesInReleaseBuilds": true
                }
              }
            ]
          ]
        }
      }
      ```
      
      ---
      
      ## Pattern 5: Secure Data Serialization
      
      Never store structured sensitive data as raw JSON strings. Use a wrapper that encrypts before storing and decrypts after retrieval.
      
      ```typescript
      import * as SecureStore from "expo-secure-store";
      
      const SESSION_KEY = "encrypted-session";
      const MAX_SECURE_STORE_BYTES = 2048;
      
      interface SecureSession {
        userId: string;
        accessToken: string;
        expiresAt: number;
      }
      
      async function storeSession(session: SecureSession): Promise<void> {
        const serialized = JSON.stringify(session);
      
        if (new Blob([serialized]).size > MAX_SECURE_STORE_BYTES) {
          throw new Error("Session data exceeds secure storage limit");
        }
      
        await SecureStore.setItemAsync(SESSION_KEY, serialized);
      }
      
      async function getSession(): Promise<SecureSession | null> {
        const raw = await SecureStore.getItemAsync(SESSION_KEY);
        if (!raw) return null;
      
        const session: SecureSession = JSON.parse(raw);
      
        // Check expiration
        if (session.expiresAt < Date.now()) {
          await SecureStore.deleteItemAsync(SESSION_KEY);
          return null;
        }
      
        return session;
      }
      ```
      
      **Why good:** size check before storing prevents silent failures, expiration check on retrieval prevents using stale tokens, typed session interface, automatic cleanup of expired sessions
      
  • reference.md 5.2 KB
    # React Native Security Reference
    
    > Checklists, library API reference, and quick-lookup commands. See [SKILL.md](SKILL.md) for decision frameworks, red flags, and anti-patterns.
    
    ---
    
    ## Security Checklist
    
    ### Before Release
    
    - [ ] No tokens/credentials in AsyncStorage (use secure storage)
    - [ ] No secrets hardcoded in JS bundle (use secure config)
    - [ ] HTTPS enforced (ATS on iOS, Network Security Config on Android)
    - [ ] `NSAllowArbitraryLoads` is NOT `true` in production Info.plist
    - [ ] `NSFaceIDUsageDescription` set (if using biometrics)
    - [ ] Hermes enabled (bytecode compilation)
    - [ ] ProGuard/R8 enabled for Android release builds
    - [ ] Certificate pinning initialized at app entry
    - [ ] At least 2 pin hashes per domain (iOS requirement)
    - [ ] Pin expiration date set (prevent bricking)
    - [ ] Debug-only network exceptions not in release builds
    
    ### For High-Security Apps
    
    - [ ] Jailbreak/root detection implemented
    - [ ] Security events reported to server
    - [ ] Screenshot prevention on sensitive screens
    - [ ] Biometric authentication on sensitive operations
    - [ ] JS code obfuscation transformer configured
    - [ ] Server-side device attestation integrated
    - [ ] Minimum TLS 1.2 enforced
    
    ---
    
    ## Library Quick Reference
    
    ### expo-secure-store
    
    ```typescript
    import * as SecureStore from "expo-secure-store";
    
    // Store
    await SecureStore.setItemAsync(key, value, options?);
    SecureStore.setItem(key, value, options?);          // Sync
    
    // Retrieve
    const val = await SecureStore.getItemAsync(key, options?);
    const val = SecureStore.getItem(key, options?);     // Sync
    
    // Delete
    await SecureStore.deleteItemAsync(key, options?);
    
    // Check biometric support
    SecureStore.canUseBiometricAuthentication();         // boolean
    
    // Options: { keychainService?, keychainAccessible?, requireAuthentication?, authenticationPrompt? }
    // Accessibility: WHEN_UNLOCKED (default), AFTER_FIRST_UNLOCK, WHEN_UNLOCKED_THIS_DEVICE_ONLY, etc.
    // Size limit: ~2KB per value
    ```
    
    ### react-native-keychain
    
    ```typescript
    import * as Keychain from "react-native-keychain";
    
    // Store credentials
    await Keychain.setGenericPassword(username, password, options?);
    
    // Retrieve (triggers biometric if accessControl set)
    const creds = await Keychain.getGenericPassword(options?);
    // Returns { username, password, service, storage } or false
    
    // Delete
    await Keychain.resetGenericPassword(options?);
    
    // Check biometrics
    const type = await Keychain.getSupportedBiometryType();
    // "TouchID" | "FaceID" | "Fingerprint" | "Face" | "Iris" | null
    
    // Key options:
    // accessControl: ACCESS_CONTROL.BIOMETRY_ANY | BIOMETRY_CURRENT_SET | BIOMETRY_ANY_OR_DEVICE_PASSCODE
    // accessible: ACCESSIBLE.WHEN_UNLOCKED | WHEN_UNLOCKED_THIS_DEVICE_ONLY | AFTER_FIRST_UNLOCK
    // authenticationType: AUTHENTICATION_TYPE.BIOMETRICS | DEVICE_PASSCODE_OR_BIOMETRICS
    // service: string (namespace for multiple credential sets)
    ```
    
    ### expo-local-authentication
    
    ```typescript
    import * as LocalAuthentication from "expo-local-authentication";
    
    // Check hardware support
    const hasHardware = await LocalAuthentication.hasHardwareAsync();
    
    // Check enrollment
    const isEnrolled = await LocalAuthentication.isEnrolledAsync();
    
    // Authenticate
    const result = await LocalAuthentication.authenticateAsync({
      promptMessage: "Verify identity",
      cancelLabel: "Cancel",
      disableDeviceFallback: false,
      fallbackLabel: "Use passcode", // iOS only
      requireConfirmation: true, // Android only
      biometricsSecurityLevel: "strong", // Android: "weak" | "strong"
    });
    // result: { success: true } | { success: false, error: string, warning?: string }
    
    // Available types
    const types = await LocalAuthentication.supportedAuthenticationTypesAsync();
    // AuthenticationType.FINGERPRINT (1) | FACIAL_RECOGNITION (2) | IRIS (3)
    
    // Cancel (Android only)
    await LocalAuthentication.cancelAuthenticate();
    ```
    
    ### react-native-ssl-public-key-pinning
    
    ```typescript
    import { initializeSslPinning } from "react-native-ssl-public-key-pinning";
    
    // Initialize at app entry (before any network requests)
    await initializeSslPinning({
      "api.example.com": {
        includeSubdomains: true, // Pin subdomains too
        publicKeyHashes: [
          // Minimum 2 on iOS
          "hash1...", // Primary (current cert)
          "hash2...", // Backup (next cert)
        ],
        expirationDate: "2026-12-31", // Safety valve (optional but recommended)
      },
    });
    ```
    
    ### jail-monkey
    
    ```typescript
    import JailMonkey from "jail-monkey";
    
    JailMonkey.isJailBroken(); // boolean - jailbreak/root detected
    JailMonkey.canMockLocation(); // boolean - mock location enabled
    JailMonkey.isDebuggedMode(); // boolean - debugger attached
    JailMonkey.isOnExternalStorage(); // boolean - Android only
    ```
    
    ---
    
    ## Pin Hash Generation Commands
    
    ```bash
    # From certificate file
    openssl x509 -in cert.pem -pubkey -noout | \
      openssl pkey -pubin -outform DER | \
      openssl dgst -sha256 -binary | \
      openssl enc -base64
    
    # From live server
    openssl s_client -connect api.example.com:443 -servername api.example.com 2>/dev/null | \
      openssl x509 -pubkey -noout | \
      openssl pkey -pubin -outform DER | \
      openssl dgst -sha256 -binary | \
      openssl enc -base64
    
    # Verify a pin against a server
    openssl s_client -connect api.example.com:443 2>/dev/null | \
      openssl x509 -noout -fingerprint -sha256
    ```
    
  • SKILL.md 19.2 KB
    ---
    name: mobile-security-react-native
    description: Secure storage, certificate pinning, biometric auth, jailbreak detection, code obfuscation, network security, screenshot prevention for React Native
    ---
    
    # React Native Security Patterns
    
    > **Quick Guide:** Defense-in-depth: layer secure storage (expo-secure-store or react-native-keychain), certificate pinning, biometric authentication, jailbreak/root detection, and code obfuscation. Never store secrets in AsyncStorage or JS bundles. Use Hermes bytecode as your first obfuscation layer. iOS Keychain persists across reinstalls; Android Keystore does not. Certificate pins require at least two hashes (primary + backup) on iOS.
    
    ---
    
    <critical_requirements>
    
    ## CRITICAL: Before Using This Skill
    
    > **All code must follow project conventions in CLAUDE.md** (kebab-case, named exports, import ordering, `import type`, named constants)
    
    **(You MUST NEVER store tokens, passwords, API keys, or PII in AsyncStorage or plain-text files -- use hardware-backed secure storage)**
    
    **(You MUST use at least two public key hashes for certificate pinning on iOS -- TrustKit/iOS enforces this and will throw if only one is provided)**
    
    **(You MUST treat jailbreak/root detection as one layer in defense-in-depth -- client-side checks can be bypassed, always validate server-side too)**
    
    **(You MUST configure both iOS ATS and Android Network Security Config to enforce HTTPS -- never ship with `NSAllowArbitraryLoads: true` in production)**
    
    **(You MUST add `NSFaceIDUsageDescription` to Info.plist when using Face ID -- the OS silently falls back to passcode without it)**
    
    </critical_requirements>
    
    ---
    
    **Auto-detection:** secure storage, SecureStore, expo-secure-store, react-native-keychain, Keychain, Keystore, certificate pinning, SSL pinning, react-native-ssl-public-key-pinning, TrustKit, jailbreak detection, root detection, jail-monkey, biometric authentication, expo-local-authentication, Face ID, Touch ID, fingerprint, code obfuscation, Hermes bytecode, ProGuard, R8, screen capture prevention, App Transport Security, Network Security Config, MITM
    
    **When to use:**
    
    - Storing credentials, tokens, or sensitive data on device
    - Implementing certificate pinning to prevent MITM attacks
    - Adding biometric authentication (Face ID, Touch ID, fingerprint)
    - Detecting jailbroken/rooted devices
    - Hardening builds with code obfuscation (Hermes, ProGuard/R8)
    - Preventing screenshot/screen recording of sensitive screens
    - Configuring network security (ATS on iOS, Network Security Config on Android)
    
    **When NOT to use:**
    
    - General React Native component architecture (not a security concern)
    - Server-side API security (use your backend security approach)
    - Web-only applications (web security patterns differ fundamentally)
    
    **Key patterns covered:**
    
    - Secure storage with expo-secure-store and react-native-keychain
    - Certificate pinning with react-native-ssl-public-key-pinning
    - Biometric authentication with expo-local-authentication and react-native-keychain
    - Jailbreak/root detection with jail-monkey
    - Code obfuscation: Hermes bytecode, Metro transformer, ProGuard/R8
    - Network security: iOS ATS and Android Network Security Config
    - Screenshot and screen recording prevention
    - Defense-in-depth strategy and security layering
    
    **Detailed Resources:**
    
    - [examples/core.md](examples/core.md) - Secure storage, certificate pinning, biometric auth
    - [examples/hardening.md](examples/hardening.md) - Code obfuscation, jailbreak detection, screenshot prevention, network config
    - [reference.md](reference.md) - Security checklist, library API reference, pin hash commands
    
    ---
    
    <philosophy>
    
    ## Philosophy
    
    Mobile security is **defense-in-depth** -- no single measure is sufficient. Attackers can bypass any individual protection, so layer multiple defenses: secure storage protects data at rest, certificate pinning protects data in transit, biometric authentication protects access, jailbreak detection identifies compromised environments, and code obfuscation raises the cost of reverse engineering.
    
    **Core principles:**
    
    1. **Never trust the client** -- all sensitive operations need server-side validation. Client-side checks are speed bumps, not walls.
    2. **Hardware-backed storage** -- iOS Keychain and Android Keystore provide hardware-level encryption. AsyncStorage is a plain-text file.
    3. **HTTPS everywhere** -- enforce TLS for all network communication. Certificate pinning adds a second layer against compromised CAs.
    4. **Minimal data exposure** -- store the least sensitive data possible on device. Prefer short-lived tokens over long-lived credentials.
    5. **Fail secure** -- when security checks fail (biometric, jailbreak), deny access by default rather than falling back to insecure paths.
    
    **Mental model:**
    
    Think of mobile security as concentric rings. Each ring (secure storage, pinning, biometrics, obfuscation, jailbreak detection) independently slows attackers. The combination creates a security posture that makes exploitation impractical for most threat models.
    
    **Platform differences that matter:**
    
    | Concern               | iOS                                        | Android                                                        |
    | --------------------- | ------------------------------------------ | -------------------------------------------------------------- |
    | Secure storage        | Keychain (persists across reinstalls)      | Keystore + SharedPreferences (cleared on uninstall)            |
    | Biometrics            | Face ID / Touch ID                         | Fingerprint / Face Unlock (weak vs strong)                     |
    | Network security      | ATS (default HTTPS since iOS 9)            | Network Security Config (clear text blocked API 28+)           |
    | Code protection       | Hermes bytecode (no ProGuard for JS)       | Hermes bytecode + ProGuard/R8 for native/Java                  |
    | Screenshot prevention | Effective (screen recording + screenshots) | FLAG_SECURE (effective for screenshots, partial for recording) |
    
    </philosophy>
    
    ---
    
    <patterns>
    
    ## Core Patterns
    
    ### Pattern 1: Secure Storage
    
    Two main libraries: **expo-secure-store** (Expo-managed, simpler API, 2KB value limit) and **react-native-keychain** (bare RN, biometric-protected credentials, no size limit).
    
    **expo-secure-store** uses iOS Keychain and Android Keystore-encrypted SharedPreferences. Values are strings with a ~2KB limit. Supports biometric gating via `requireAuthentication`.
    
    ```typescript
    import * as SecureStore from "expo-secure-store";
    
    const AUTH_TOKEN_KEY = "auth-token";
    
    // Store securely with biometric protection
    await SecureStore.setItemAsync(AUTH_TOKEN_KEY, token, {
      requireAuthentication: true,
      authenticationPrompt: "Authenticate to save credentials",
    });
    
    // Retrieve (prompts biometric if requireAuthentication was set)
    const stored = await SecureStore.getItemAsync(AUTH_TOKEN_KEY);
    ```
    
    **react-native-keychain** provides credential storage with granular access control and biometric gating via `ACCESS_CONTROL` options.
    
    ```typescript
    import * as Keychain from "react-native-keychain";
    
    await Keychain.setGenericPassword("user@example.com", token, {
      accessControl: Keychain.ACCESS_CONTROL.BIOMETRY_ANY_OR_DEVICE_PASSCODE,
      accessible: Keychain.ACCESSIBLE.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
    });
    ```
    
    **Why good:** hardware-backed encryption, biometric gating prevents unauthorized reads, `WHEN_UNLOCKED_THIS_DEVICE_ONLY` prevents extraction from backups
    
    See [examples/core.md](examples/core.md) for full secure storage patterns with error handling and library comparison.
    
    ---
    
    ### Pattern 2: Certificate Pinning
    
    Pin your server's public key hashes to prevent MITM attacks even when a device's CA store is compromised. Use **react-native-ssl-public-key-pinning** for a JS-level approach that works with all HTTP clients.
    
    ```typescript
    import { initializeSslPinning } from "react-native-ssl-public-key-pinning";
    
    const PIN_EXPIRATION_DATE = "2026-12-31";
    
    await initializeSslPinning({
      "api.example.com": {
        includeSubdomains: true,
        publicKeyHashes: [
          "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", // Primary
          "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=", // Backup (REQUIRED on iOS)
        ],
        expirationDate: PIN_EXPIRATION_DATE,
      },
    });
    ```
    
    **Why good:** intercepts all fetch/XMLHttpRequest calls globally, no native code changes required, expiration date prevents bricking when certificates rotate
    
    **Gotcha:** iOS requires at least two hashes per domain. Providing only one causes `initializeSslPinning` to throw.
    
    See [examples/core.md](examples/core.md) for rotation strategy and [reference.md](reference.md) for pin hash generation commands.
    
    ---
    
    ### Pattern 3: Biometric Authentication
    
    Two approaches: **expo-local-authentication** (standalone biometric prompt, no credential storage) and **react-native-keychain** (biometric-gated credential retrieval).
    
    ```typescript
    import * as LocalAuthentication from "expo-local-authentication";
    
    async function authenticateUser(): Promise<boolean> {
      const hasHardware = await LocalAuthentication.hasHardwareAsync();
      const isEnrolled = await LocalAuthentication.isEnrolledAsync();
    
      if (!hasHardware || !isEnrolled) return false;
    
      const result = await LocalAuthentication.authenticateAsync({
        promptMessage: "Verify your identity",
        disableDeviceFallback: false,
        cancelLabel: "Cancel",
      });
    
      return result.success;
    }
    ```
    
    **Why good:** checks hardware + enrollment before prompting, disableDeviceFallback: false allows passcode as backup, clean boolean result
    
    **Gotcha:** Face ID requires `NSFaceIDUsageDescription` in Info.plist. Without it, iOS silently falls back to passcode (no error, no Face ID prompt). Face ID is not supported in Expo Go -- use a development build.
    
    See [examples/core.md](examples/core.md) for biometric-gated credential flow combining both libraries.
    
    ---
    
    ### Pattern 4: Jailbreak/Root Detection
    
    Detect compromised devices where security controls (sandboxing, code signing) are disabled. Use **jail-monkey** for detection checks.
    
    ```typescript
    import JailMonkey from "jail-monkey";
    
    function getDeviceSecurityStatus() {
      return {
        isJailbroken: JailMonkey.isJailBroken(),
        canMockLocation: JailMonkey.canMockLocation(),
        isDebugMode: JailMonkey.isDebuggedMode(),
        isOnExternalStorage: JailMonkey.isOnExternalStorage(), // Android only
      };
    }
    ```
    
    **Why good:** checks multiple indicators (not just one file path), includes location mocking and debug detection
    
    **Important:** Jailbreak detection is a speed bump, not a wall. Determined attackers bypass client-side checks with hooking frameworks (Frida, Objection). Always pair with server-side device attestation for high-security apps.
    
    See [examples/hardening.md](examples/hardening.md) for response strategies and server-side validation.
    
    ---
    
    ### Pattern 5: Code Obfuscation
    
    Layer 1: **Hermes bytecode** (enabled by default since RN 0.70) compiles JS to bytecode, making casual reverse engineering difficult. Layer 2: **Metro obfuscation transformer** for additional string encryption and control flow flattening. Layer 3: **ProGuard/R8** for Android native/Java code.
    
    ```javascript
    // metro.config.js -- adding obfuscation transformer
    const { getDefaultConfig } = require("@react-native/metro-config");
    
    const config = getDefaultConfig(__dirname);
    
    config.transformer = {
      ...config.transformer,
      babelTransformerPath: require.resolve("react-native-obfuscating-transformer"),
    };
    
    module.exports = config;
    ```
    
    **Why good:** layered approach -- Hermes handles baseline, transformer adds string/flow obfuscation, ProGuard/R8 covers native code
    
    **Gotcha:** Not all obfuscation options work. The `stringArray` option in react-native-obfuscating-transformer is known to break builds. Test thoroughly.
    
    See [examples/hardening.md](examples/hardening.md) for ProGuard/R8 configuration and obfuscation options.
    
    ---
    
    ### Pattern 6: Network Security Configuration
    
    Enforce HTTPS at the OS level. iOS uses **App Transport Security** (ATS), Android uses **Network Security Config**.
    
    **iOS (Info.plist):** ATS enforces HTTPS by default since iOS 9. Never ship with `NSAllowArbitraryLoads: true`.
    
    **Android (network_security_config.xml):** Clear text blocked by default since API 28. Pin certificates natively for defense-in-depth alongside JS-level pinning.
    
    ```xml
    <!-- android/app/src/main/res/xml/network_security_config.xml -->
    <network-security-config>
      <base-config cleartextTrafficPermitted="false">
        <trust-anchors>
          <certificates src="system" />
        </trust-anchors>
      </base-config>
      <domain-config>
        <domain includeSubdomains="true">api.example.com</domain>
        <pin-set expiration="2026-12-31">
          <pin digest="SHA-256">AAAAAAAAAA...=</pin>
          <pin digest="SHA-256">BBBBBBBBBB...=</pin>
        </pin-set>
      </domain-config>
    </network-security-config>
    ```
    
    **Why good:** OS-level enforcement, clear text blocked globally, native pinning adds second layer beyond JS-level pinning
    
    See [examples/hardening.md](examples/hardening.md) for iOS ATS configuration and debug vs release network policies.
    
    ---
    
    ### Pattern 7: Screenshot and Screen Recording Prevention
    
    Prevent screen capture on sensitive screens (banking, credentials, personal data).
    
    ```typescript
    import { useIsFocused } from "@react-navigation/native";
    import * as ScreenCapture from "expo-screen-capture";
    import { useEffect } from "react";
    
    function usePreventCapture() {
      const isFocused = useIsFocused();
    
      useEffect(() => {
        if (isFocused) {
          ScreenCapture.preventScreenCaptureAsync();
        } else {
          ScreenCapture.allowScreenCaptureAsync();
        }
        return () => {
          ScreenCapture.allowScreenCaptureAsync();
        };
      }, [isFocused]);
    }
    ```
    
    **Why good:** per-screen control (not global), cleanup on unfocus/unmount, works for both screenshots and screen recording on iOS
    
    **Gotcha:** On Android, `FLAG_SECURE` reliably blocks screenshots but screen recording prevention varies by Android version. On iOS, screenshot content is replaced with blank but the user can still trigger the screenshot action.
    
    See [examples/hardening.md](examples/hardening.md) for non-Expo alternatives and listener-based approaches.
    
    </patterns>
    
    ---
    
    <decision_framework>
    
    ## Decision Framework
    
    ### Secure Storage Choice
    
    ```
    Need to store credentials/tokens securely?
    +-- Using Expo managed workflow?
    |   +-- YES -> expo-secure-store (simpler API, Expo-native)
    |   +-- NO  -> react-native-keychain (more control, biometric options)
    |
    +-- Need biometric-gated credential retrieval?
    |   +-- YES -> react-native-keychain with ACCESS_CONTROL.BIOMETRY_ANY
    |   +-- OR  -> expo-secure-store with requireAuthentication: true
    |
    +-- Value larger than 2KB?
    |   +-- YES -> react-native-keychain (no size limit)
    |   +-- NO  -> Either library works
    |
    +-- Need credential persistence across app reinstalls?
        +-- iOS -> Both persist (Keychain behavior)
        +-- Android -> Neither persists (cleared on uninstall)
    ```
    
    ### Biometric Authentication Choice
    
    ```
    Need biometric prompt (no credential storage)?
    +-- YES -> expo-local-authentication
    |
    Need biometric-gated credential storage/retrieval?
    +-- YES -> react-native-keychain with accessControl
    |
    Need to distinguish biometric security level (weak vs strong)?
    +-- YES -> expo-local-authentication (provides SecurityLevel enum)
    ```
    
    ### Certificate Pinning Approach
    
    ```
    Need SSL pinning?
    +-- JS-level (works with all HTTP clients)?
    |   +-- YES -> react-native-ssl-public-key-pinning
    |
    +-- Native-level (defense-in-depth)?
    |   +-- iOS -> TrustKit (via CocoaPods)
    |   +-- Android -> Network Security Config XML
    |
    +-- Best practice -> Both JS-level AND native-level
    ```
    
    ### Security Layering
    
    ```
    Minimum viable security:
    1. Secure storage (expo-secure-store or react-native-keychain)
    2. HTTPS enforcement (ATS + Network Security Config)
    3. Hermes bytecode (default since RN 0.70)
    
    Standard security (most apps):
    + Certificate pinning
    + Biometric authentication
    + ProGuard/R8 on Android
    
    High security (banking, healthcare, fintech):
    + Jailbreak/root detection
    + JS code obfuscation transformer
    + Screenshot prevention
    + Server-side device attestation
    + Runtime integrity checks
    ```
    
    </decision_framework>
    
    ---
    
    <red_flags>
    
    ## RED FLAGS
    
    **High Priority Issues:**
    
    - Storing tokens or credentials in AsyncStorage -- it is a plain-text file, trivially readable on jailbroken/rooted devices
    - Storing API keys or secrets in the JS bundle -- the bundle is extractable from any published app
    - Shipping with `NSAllowArbitraryLoads: true` in production Info.plist -- disables ATS entirely, allows HTTP
    - Using only one public key hash for certificate pinning on iOS -- TrustKit throws, pinning silently fails
    - Relying solely on jailbreak detection for security -- client-side checks are bypassable with Frida/Objection
    
    **Medium Priority Issues:**
    
    - Not checking `hasHardwareAsync` and `isEnrolledAsync` before calling `authenticateAsync` -- crashes or confusing UX on devices without biometrics
    - Missing `NSFaceIDUsageDescription` in Info.plist -- Face ID silently falls back to passcode without any error
    - Using `ACCESSIBLE.ALWAYS` for Keychain items -- allows access even when device is locked (deprecated on iOS)
    - Skipping ProGuard/R8 on Android release builds -- native code is trivially decompilable without it
    - Not setting `expirationDate` on certificate pins -- expired certificates brick the app until users update
    
    **Gotchas & Edge Cases:**
    
    - iOS Keychain data persists across app reinstalls (same bundle ID); Android Keystore data does not -- plan token refresh accordingly
    - expo-secure-store has a ~2KB value size limit -- large tokens or data blobs will silently fail or throw
    - `z.coerce.boolean()` treats string `"false"` as truthy -- when parsing security config from strings, use explicit comparison
    - Android `FLAG_SECURE` blocks screenshots reliably but screen recording prevention varies by OS version and manufacturer
    - Hermes bytecode can be decompiled with tools like `hbctool` -- it raises the bar but is not true encryption
    - react-native-obfuscating-transformer's `stringArray` option breaks React Native builds -- avoid it
    - Android biometrics have "weak" (2D face) vs "strong" (fingerprint, 3D face) security levels -- financial apps should require strong
    - Certificate pins must be rotated before expiry -- set calendar reminders and use the `expirationDate` field as a safety net
    - `Keychain.ACCESS_CONTROL.BIOMETRY_CURRENT_SET` invalidates credentials when biometrics are re-enrolled -- use `BIOMETRY_ANY` for persistence across biometric changes
    
    </red_flags>
    
    ---
    
    <critical_reminders>
    
    ## CRITICAL REMINDERS
    
    > **All code must follow project conventions in CLAUDE.md**
    
    **(You MUST NEVER store tokens, passwords, API keys, or PII in AsyncStorage or plain-text files -- use hardware-backed secure storage)**
    
    **(You MUST use at least two public key hashes for certificate pinning on iOS -- TrustKit/iOS enforces this and will throw if only one is provided)**
    
    **(You MUST treat jailbreak/root detection as one layer in defense-in-depth -- client-side checks can be bypassed, always validate server-side too)**
    
    **(You MUST configure both iOS ATS and Android Network Security Config to enforce HTTPS -- never ship with `NSAllowArbitraryLoads: true` in production)**
    
    **(You MUST add `NSFaceIDUsageDescription` to Info.plist when using Face ID -- the OS silently falls back to passcode without it)**
    
    **Failure to follow these rules will expose user credentials, enable MITM attacks, and create false security assumptions.**
    
    </critical_reminders>
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related