Skip to main content
Security Best Practices

Module Overview

Estimated Time: 4 hours | Difficulty: Advanced | Prerequisites: Authentication, Storage modules
Security is critical for mobile applications, especially those handling sensitive user data. This module covers comprehensive security practices for React Native apps, from secure storage to preventing reverse engineering. What You’ll Learn:
  • Secure storage solutions
  • Certificate pinning
  • Code obfuscation
  • Jailbreak/root detection
  • Data encryption
  • Security auditing

Security Threat Model

┌─────────────────────────────────────────────────────────────────────────────┐
│                    Mobile App Security Threats                               │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                              │
│   Data at Rest                          Data in Transit                      │
│   ─────────────                         ────────────────                     │
│   • Insecure storage                    • Man-in-the-middle attacks         │
│   • Unencrypted databases               • SSL stripping                     │
│   • Exposed credentials                 • Certificate spoofing              │
│   • Backup vulnerabilities              • Insecure protocols                │
│                                                                              │
│   Code & Binary                         Runtime                              │
│   ─────────────                         ───────                              │
│   • Reverse engineering                 • Jailbreak/root exploits           │
│   • Code injection                      • Memory tampering                  │
│   • Hardcoded secrets                   • Debugger attachment               │
│   • Unobfuscated code                   • Dynamic instrumentation           │
│                                                                              │
│   Authentication                        Input/Output                         │
│   ──────────────                        ────────────                         │
│   • Weak credentials                    • Injection attacks                 │
│   • Session hijacking                   • Deep link exploitation            │
│   • Token theft                         • Clipboard leakage                 │
│   • Biometric bypass                    • Screenshot capture                │
│                                                                              │
└─────────────────────────────────────────────────────────────────────────────┘

Secure Storage

expo-secure-store

For sensitive data like tokens and credentials:
npx expo install expo-secure-store
// services/secureStorage.ts
import * as SecureStore from 'expo-secure-store';

const KEYS = {
  ACCESS_TOKEN: 'access_token',
  REFRESH_TOKEN: 'refresh_token',
  USER_CREDENTIALS: 'user_credentials',
  BIOMETRIC_KEY: 'biometric_key',
} as const;

export const secureStorage = {
  // Store sensitive data
  async setItem(key: string, value: string): Promise<void> {
    try {
      await SecureStore.setItemAsync(key, value, {
        keychainAccessible: SecureStore.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
      });
    } catch (error) {
      console.error('SecureStore setItem error:', error);
      throw new Error('Failed to store secure data');
    }
  },

  // Retrieve sensitive data
  async getItem(key: string): Promise<string | null> {
    try {
      return await SecureStore.getItemAsync(key);
    } catch (error) {
      console.error('SecureStore getItem error:', error);
      return null;
    }
  },

  // Delete sensitive data
  async removeItem(key: string): Promise<void> {
    try {
      await SecureStore.deleteItemAsync(key);
    } catch (error) {
      console.error('SecureStore removeItem error:', error);
    }
  },

  // Token management
  async setTokens(accessToken: string, refreshToken: string): Promise<void> {
    await Promise.all([
      this.setItem(KEYS.ACCESS_TOKEN, accessToken),
      this.setItem(KEYS.REFRESH_TOKEN, refreshToken),
    ]);
  },

  async getTokens(): Promise<{ accessToken: string | null; refreshToken: string | null }> {
    const [accessToken, refreshToken] = await Promise.all([
      this.getItem(KEYS.ACCESS_TOKEN),
      this.getItem(KEYS.REFRESH_TOKEN),
    ]);
    return { accessToken, refreshToken };
  },

  async clearTokens(): Promise<void> {
    await Promise.all([
      this.removeItem(KEYS.ACCESS_TOKEN),
      this.removeItem(KEYS.REFRESH_TOKEN),
    ]);
  },

  // Clear all secure data
  async clearAll(): Promise<void> {
    await Promise.all(
      Object.values(KEYS).map(key => this.removeItem(key))
    );
  },
};

react-native-keychain (CLI)

For React Native CLI projects:
npm install react-native-keychain
cd ios && pod install
// services/keychain.ts
import * as Keychain from 'react-native-keychain';

export const keychainService = {
  // Store credentials with biometric protection
  async setCredentials(
    username: string,
    password: string,
    options?: { biometricProtection?: boolean }
  ): Promise<boolean> {
    try {
      const result = await Keychain.setGenericPassword(username, password, {
        service: 'com.myapp.credentials',
        accessControl: options?.biometricProtection
          ? Keychain.ACCESS_CONTROL.BIOMETRY_CURRENT_SET_OR_DEVICE_PASSCODE
          : undefined,
        accessible: Keychain.ACCESSIBLE.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
      });
      return !!result;
    } catch (error) {
      console.error('Keychain setCredentials error:', error);
      return false;
    }
  },

  // Retrieve credentials
  async getCredentials(): Promise<{ username: string; password: string } | null> {
    try {
      const credentials = await Keychain.getGenericPassword({
        service: 'com.myapp.credentials',
      });
      if (credentials) {
        return {
          username: credentials.username,
          password: credentials.password,
        };
      }
      return null;
    } catch (error) {
      console.error('Keychain getCredentials error:', error);
      return null;
    }
  },

  // Check biometric availability
  async getBiometryType(): Promise<Keychain.BIOMETRY_TYPE | null> {
    try {
      return await Keychain.getSupportedBiometryType();
    } catch (error) {
      return null;
    }
  },

  // Reset credentials
  async resetCredentials(): Promise<boolean> {
    try {
      return await Keychain.resetGenericPassword({
        service: 'com.myapp.credentials',
      });
    } catch (error) {
      console.error('Keychain resetCredentials error:', error);
      return false;
    }
  },
};

Encrypted Storage with MMKV

npm install react-native-mmkv
// services/encryptedStorage.ts
import { MMKV } from 'react-native-mmkv';

// Create encrypted storage instance
const storage = new MMKV({
  id: 'secure-storage',
  encryptionKey: 'your-encryption-key', // In production, derive from secure source
});

export const encryptedStorage = {
  set(key: string, value: string): void {
    storage.set(key, value);
  },

  get(key: string): string | undefined {
    return storage.getString(key);
  },

  setObject<T>(key: string, value: T): void {
    storage.set(key, JSON.stringify(value));
  },

  getObject<T>(key: string): T | null {
    const value = storage.getString(key);
    if (value) {
      try {
        return JSON.parse(value) as T;
      } catch {
        return null;
      }
    }
    return null;
  },

  delete(key: string): void {
    storage.delete(key);
  },

  clearAll(): void {
    storage.clearAll();
  },

  getAllKeys(): string[] {
    return storage.getAllKeys();
  },
};

Certificate Pinning

Prevent man-in-the-middle attacks by pinning SSL certificates:

Using react-native-ssl-pinning

npm install react-native-ssl-pinning
// services/api/pinnedClient.ts
import { fetch as pinnedFetch } from 'react-native-ssl-pinning';

const API_BASE_URL = 'https://api.example.com';

// Certificate pins (SHA-256 hashes)
const CERTIFICATE_PINS = {
  'api.example.com': [
    'sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=',
    'sha256/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=', // Backup pin
  ],
};

export async function secureFetch(
  endpoint: string,
  options: RequestInit = {}
): Promise<Response> {
  const url = `${API_BASE_URL}${endpoint}`;
  
  try {
    const response = await pinnedFetch(url, {
      method: options.method || 'GET',
      headers: options.headers as Record<string, string>,
      body: options.body as string,
      sslPinning: {
        certs: ['cert1', 'cert2'], // Certificate names in assets
      },
      timeoutInterval: 30000,
    });

    return new Response(JSON.stringify(response.json()), {
      status: response.status,
      headers: response.headers,
    });
  } catch (error) {
    if (error.message?.includes('SSL')) {
      // Certificate pinning failed - possible MITM attack
      console.error('SSL Pinning failed - possible security threat');
      throw new Error('Security verification failed');
    }
    throw error;
  }
}

Using Axios with Certificate Pinning

// services/api/secureAxios.ts
import axios, { AxiosInstance } from 'axios';
import { Platform } from 'react-native';

// For Expo, use a custom native module or proxy
// For CLI, configure in native code

// iOS: Add to Info.plist
// <key>NSAppTransportSecurity</key>
// <dict>
//   <key>NSPinnedDomains</key>
//   <dict>
//     <key>api.example.com</key>
//     <dict>
//       <key>NSIncludesSubdomains</key>
//       <true/>
//       <key>NSPinnedLeafIdentities</key>
//       <array>
//         <dict>
//           <key>SPKI-SHA256-BASE64</key>
//           <string>YOUR_PIN_HERE</string>
//         </dict>
//       </array>
//     </dict>
//   </dict>
// </dict>

export function createSecureClient(): AxiosInstance {
  const client = axios.create({
    baseURL: 'https://api.example.com',
    timeout: 30000,
    headers: {
      'Content-Type': 'application/json',
    },
  });

  // Add request interceptor for additional security headers
  client.interceptors.request.use(config => {
    // Add timestamp to prevent replay attacks
    config.headers['X-Request-Timestamp'] = Date.now().toString();
    
    // Add request signature (implement your signing logic)
    // config.headers['X-Request-Signature'] = signRequest(config);
    
    return config;
  });

  return client;
}

Code Obfuscation

JavaScript Obfuscation

// metro.config.js
const { getDefaultConfig } = require('expo/metro-config');

const config = getDefaultConfig(__dirname);

// Enable minification in production
config.transformer.minifierConfig = {
  keep_classnames: false,
  keep_fnames: false,
  mangle: {
    toplevel: true,
  },
  output: {
    ascii_only: true,
    quote_style: 3,
    wrap_iife: true,
  },
  sourceMap: {
    includeSources: false,
  },
  toplevel: true,
  compress: {
    reduce_funcs: true,
    drop_console: true, // Remove console.log in production
    drop_debugger: true,
  },
};

module.exports = config;

Android ProGuard

# android/app/proguard-rules.pro

# React Native
-keep class com.facebook.react.** { *; }
-keep class com.facebook.hermes.** { *; }

# Keep native methods
-keepclassmembers class * {
    @com.facebook.react.uimanager.annotations.ReactProp <methods>;
}

# Obfuscate everything else
-repackageclasses ''
-allowaccessmodification
-optimizations !code/simplification/arithmetic

# Remove logging
-assumenosideeffects class android.util.Log {
    public static *** d(...);
    public static *** v(...);
    public static *** i(...);
}
// android/app/build.gradle
android {
    buildTypes {
        release {
            minifyEnabled true
            shrinkResources true
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
}

Jailbreak/Root Detection

npm install jail-monkey
// services/security/deviceSecurity.ts
import JailMonkey from 'jail-monkey';
import { Platform, Alert } from 'react-native';

interface SecurityCheckResult {
  isSecure: boolean;
  issues: string[];
}

export const deviceSecurity = {
  // Comprehensive security check
  async checkDeviceSecurity(): Promise<SecurityCheckResult> {
    const issues: string[] = [];

    // Check for jailbreak/root
    if (JailMonkey.isJailBroken()) {
      issues.push('Device is jailbroken/rooted');
    }

    // Check for debugging
    if (JailMonkey.isDebuggedMode()) {
      issues.push('App is being debugged');
    }

    // Check for mock location (Android)
    if (Platform.OS === 'android' && JailMonkey.hookDetected()) {
      issues.push('Hooking framework detected');
    }

    // Check if running on emulator
    if (await this.isEmulator()) {
      issues.push('Running on emulator');
    }

    return {
      isSecure: issues.length === 0,
      issues,
    };
  },

  // Check if running on emulator
  async isEmulator(): Promise<boolean> {
    // JailMonkey doesn't have direct emulator detection
    // Implement custom check based on device properties
    if (Platform.OS === 'android') {
      // Check for common emulator indicators
      const DeviceInfo = require('react-native-device-info');
      const isEmulator = await DeviceInfo.isEmulator();
      return isEmulator;
    }
    return false;
  },

  // Handle security violations
  handleSecurityViolation(issues: string[]): void {
    // Log security event (to your analytics/security service)
    console.warn('Security violation detected:', issues);

    // Show warning to user
    Alert.alert(
      'Security Warning',
      'This device may be compromised. Some features may be restricted.',
      [
        {
          text: 'I Understand',
          style: 'default',
        },
      ]
    );
  },

  // Enforce security policy
  async enforceSecurityPolicy(
    options: { allowJailbroken?: boolean; allowDebug?: boolean } = {}
  ): Promise<boolean> {
    const { isSecure, issues } = await this.checkDeviceSecurity();

    if (!isSecure) {
      // Filter issues based on policy
      const criticalIssues = issues.filter(issue => {
        if (issue.includes('jailbroken') && options.allowJailbroken) return false;
        if (issue.includes('debugged') && options.allowDebug) return false;
        return true;
      });

      if (criticalIssues.length > 0) {
        this.handleSecurityViolation(criticalIssues);
        return false;
      }
    }

    return true;
  },
};

Security Provider Component

// providers/SecurityProvider.tsx
import { createContext, useContext, useEffect, useState, ReactNode } from 'react';
import { deviceSecurity } from '@/services/security/deviceSecurity';

interface SecurityContextType {
  isSecure: boolean;
  isChecking: boolean;
  securityIssues: string[];
  recheckSecurity: () => Promise<void>;
}

const SecurityContext = createContext<SecurityContextType | undefined>(undefined);

export function SecurityProvider({ children }: { children: ReactNode }) {
  const [isSecure, setIsSecure] = useState(true);
  const [isChecking, setIsChecking] = useState(true);
  const [securityIssues, setSecurityIssues] = useState<string[]>([]);

  const checkSecurity = async () => {
    setIsChecking(true);
    try {
      const result = await deviceSecurity.checkDeviceSecurity();
      setIsSecure(result.isSecure);
      setSecurityIssues(result.issues);

      if (!result.isSecure) {
        deviceSecurity.handleSecurityViolation(result.issues);
      }
    } catch (error) {
      console.error('Security check failed:', error);
    } finally {
      setIsChecking(false);
    }
  };

  useEffect(() => {
    checkSecurity();
  }, []);

  return (
    <SecurityContext.Provider
      value={{
        isSecure,
        isChecking,
        securityIssues,
        recheckSecurity: checkSecurity,
      }}
    >
      {children}
    </SecurityContext.Provider>
  );
}

export function useSecurity() {
  const context = useContext(SecurityContext);
  if (!context) {
    throw new Error('useSecurity must be used within SecurityProvider');
  }
  return context;
}

Data Encryption

Encrypting Sensitive Data

// services/security/encryption.ts
import CryptoJS from 'crypto-js';
import * as SecureStore from 'expo-secure-store';

const ENCRYPTION_KEY_ID = 'app_encryption_key';

export const encryption = {
  // Generate or retrieve encryption key
  async getEncryptionKey(): Promise<string> {
    let key = await SecureStore.getItemAsync(ENCRYPTION_KEY_ID);
    
    if (!key) {
      // Generate new key
      key = CryptoJS.lib.WordArray.random(256 / 8).toString();
      await SecureStore.setItemAsync(ENCRYPTION_KEY_ID, key);
    }
    
    return key;
  },

  // Encrypt data
  async encrypt(data: string): Promise<string> {
    const key = await this.getEncryptionKey();
    const encrypted = CryptoJS.AES.encrypt(data, key).toString();
    return encrypted;
  },

  // Decrypt data
  async decrypt(encryptedData: string): Promise<string> {
    const key = await this.getEncryptionKey();
    const decrypted = CryptoJS.AES.decrypt(encryptedData, key);
    return decrypted.toString(CryptoJS.enc.Utf8);
  },

  // Encrypt object
  async encryptObject<T>(data: T): Promise<string> {
    const jsonString = JSON.stringify(data);
    return this.encrypt(jsonString);
  },

  // Decrypt object
  async decryptObject<T>(encryptedData: string): Promise<T | null> {
    try {
      const jsonString = await this.decrypt(encryptedData);
      return JSON.parse(jsonString) as T;
    } catch {
      return null;
    }
  },

  // Hash sensitive data (one-way)
  hash(data: string): string {
    return CryptoJS.SHA256(data).toString();
  },

  // Generate secure random string
  generateSecureRandom(length: number = 32): string {
    return CryptoJS.lib.WordArray.random(length).toString();
  },
};

Biometric Authentication

// services/security/biometrics.ts
import * as LocalAuthentication from 'expo-local-authentication';
import { Platform } from 'react-native';

export const biometrics = {
  // Check if biometrics are available
  async isAvailable(): Promise<boolean> {
    const compatible = await LocalAuthentication.hasHardwareAsync();
    const enrolled = await LocalAuthentication.isEnrolledAsync();
    return compatible && enrolled;
  },

  // Get available biometric types
  async getAvailableTypes(): Promise<LocalAuthentication.AuthenticationType[]> {
    return LocalAuthentication.supportedAuthenticationTypesAsync();
  },

  // Authenticate with biometrics
  async authenticate(
    options: {
      promptMessage?: string;
      cancelLabel?: string;
      fallbackLabel?: string;
      disableDeviceFallback?: boolean;
    } = {}
  ): Promise<{ success: boolean; error?: string }> {
    try {
      const result = await LocalAuthentication.authenticateAsync({
        promptMessage: options.promptMessage || 'Authenticate to continue',
        cancelLabel: options.cancelLabel || 'Cancel',
        fallbackLabel: options.fallbackLabel || 'Use Passcode',
        disableDeviceFallback: options.disableDeviceFallback || false,
      });

      if (result.success) {
        return { success: true };
      }

      return {
        success: false,
        error: result.error || 'Authentication failed',
      };
    } catch (error) {
      return {
        success: false,
        error: error instanceof Error ? error.message : 'Unknown error',
      };
    }
  },

  // Get biometric type name
  getBiometricTypeName(type: LocalAuthentication.AuthenticationType): string {
    switch (type) {
      case LocalAuthentication.AuthenticationType.FINGERPRINT:
        return Platform.OS === 'ios' ? 'Touch ID' : 'Fingerprint';
      case LocalAuthentication.AuthenticationType.FACIAL_RECOGNITION:
        return Platform.OS === 'ios' ? 'Face ID' : 'Face Recognition';
      case LocalAuthentication.AuthenticationType.IRIS:
        return 'Iris';
      default:
        return 'Biometric';
    }
  },
};

Biometric Login Component

// components/BiometricLogin.tsx
import { View, Text, Pressable, StyleSheet, Alert } from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import { useEffect, useState } from 'react';
import { biometrics } from '@/services/security/biometrics';
import * as LocalAuthentication from 'expo-local-authentication';

interface BiometricLoginProps {
  onSuccess: () => void;
  onFallback: () => void;
}

export function BiometricLogin({ onSuccess, onFallback }: BiometricLoginProps) {
  const [isAvailable, setIsAvailable] = useState(false);
  const [biometricType, setBiometricType] = useState<string>('Biometric');

  useEffect(() => {
    checkBiometrics();
  }, []);

  const checkBiometrics = async () => {
    const available = await biometrics.isAvailable();
    setIsAvailable(available);

    if (available) {
      const types = await biometrics.getAvailableTypes();
      if (types.length > 0) {
        setBiometricType(biometrics.getBiometricTypeName(types[0]));
      }
    }
  };

  const handleBiometricAuth = async () => {
    const result = await biometrics.authenticate({
      promptMessage: `Login with ${biometricType}`,
    });

    if (result.success) {
      onSuccess();
    } else {
      Alert.alert('Authentication Failed', result.error || 'Please try again');
    }
  };

  if (!isAvailable) {
    return null;
  }

  return (
    <View style={styles.container}>
      <Pressable
        style={({ pressed }) => [
          styles.button,
          pressed && styles.buttonPressed,
        ]}
        onPress={handleBiometricAuth}
      >
        <Ionicons
          name={biometricType.includes('Face') ? 'scan' : 'finger-print'}
          size={32}
          color="#3b82f6"
        />
        <Text style={styles.buttonText}>Login with {biometricType}</Text>
      </Pressable>

      <Pressable onPress={onFallback}>
        <Text style={styles.fallbackText}>Use password instead</Text>
      </Pressable>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    alignItems: 'center',
    gap: 16,
  },
  button: {
    flexDirection: 'row',
    alignItems: 'center',
    gap: 12,
    backgroundColor: '#eff6ff',
    paddingVertical: 16,
    paddingHorizontal: 24,
    borderRadius: 12,
    borderWidth: 1,
    borderColor: '#bfdbfe',
  },
  buttonPressed: {
    backgroundColor: '#dbeafe',
  },
  buttonText: {
    fontSize: 16,
    fontWeight: '600',
    color: '#3b82f6',
  },
  fallbackText: {
    fontSize: 14,
    color: '#6b7280',
    textDecorationLine: 'underline',
  },
});

Preventing Screenshot/Screen Recording

// hooks/useScreenSecurity.ts
import { useEffect } from 'react';
import { Platform } from 'react-native';

// For Expo, use expo-screen-capture
import * as ScreenCapture from 'expo-screen-capture';

export function useScreenSecurity(preventCapture: boolean = true) {
  useEffect(() => {
    if (!preventCapture) return;

    const enableProtection = async () => {
      if (Platform.OS === 'android') {
        await ScreenCapture.preventScreenCaptureAsync();
      }
      // iOS doesn't support preventing screenshots programmatically
      // but you can detect them
    };

    const disableProtection = async () => {
      if (Platform.OS === 'android') {
        await ScreenCapture.allowScreenCaptureAsync();
      }
    };

    enableProtection();

    return () => {
      disableProtection();
    };
  }, [preventCapture]);

  // Listen for screenshot events (iOS)
  useEffect(() => {
    const subscription = ScreenCapture.addScreenshotListener(() => {
      console.warn('Screenshot detected!');
      // Log security event, show warning, etc.
    });

    return () => subscription.remove();
  }, []);
}

// Usage in sensitive screens
function SensitiveDataScreen() {
  useScreenSecurity(true);

  return (
    <View>
      <Text>Sensitive information here</Text>
    </View>
  );
}

Security Checklist

Storage Security

  • Use SecureStore for tokens
  • Encrypt sensitive data
  • Clear data on logout
  • No hardcoded secrets

Network Security

  • Use HTTPS only
  • Implement certificate pinning
  • Validate SSL certificates
  • Add request signing

Code Security

  • Enable ProGuard/R8
  • Remove debug logs
  • Obfuscate JavaScript
  • No sensitive data in logs

Runtime Security

  • Detect jailbreak/root
  • Prevent debugging
  • Screen capture protection
  • Clipboard security

Security Audit Script

// scripts/security-audit.ts
import { execSync } from 'child_process';

interface AuditResult {
  category: string;
  check: string;
  status: 'pass' | 'fail' | 'warning';
  message: string;
}

const results: AuditResult[] = [];

// Check for vulnerable dependencies
function checkDependencies() {
  try {
    execSync('npm audit --json', { encoding: 'utf-8' });
    results.push({
      category: 'Dependencies',
      check: 'Vulnerability scan',
      status: 'pass',
      message: 'No vulnerabilities found',
    });
  } catch (error) {
    results.push({
      category: 'Dependencies',
      check: 'Vulnerability scan',
      status: 'fail',
      message: 'Vulnerabilities detected - run npm audit',
    });
  }
}

// Check for hardcoded secrets
function checkHardcodedSecrets() {
  const patterns = [
    /api[_-]?key\s*[:=]\s*['"][^'"]+['"]/gi,
    /secret\s*[:=]\s*['"][^'"]+['"]/gi,
    /password\s*[:=]\s*['"][^'"]+['"]/gi,
  ];
  
  // Scan source files for patterns
  // Add results based on findings
}

// Run all checks
checkDependencies();
checkHardcodedSecrets();

// Output results
console.log('\n🔒 Security Audit Results\n');
results.forEach(result => {
  const icon = result.status === 'pass' ? '✅' : result.status === 'fail' ? '❌' : '⚠️';
  console.log(`${icon} [${result.category}] ${result.check}: ${result.message}`);
});

Next Steps

Module 32: Offline-First Architecture

Learn to build apps that work seamlessly without internet connectivity