Back to Posts

Mobile App Security Best Practices

By Lumina Software
securityreact-nativemobile-developmentbest-practices

Mobile App Security Best Practices

Mobile apps handle sensitive data, process payments, and access personal information. Security isn't optional—it's fundamental. Here's how to secure React Native apps against common threats.

Threat Model

Common Threats

  1. Data interception: Man-in-the-middle attacks
  2. Insecure storage: Sensitive data in plaintext
  3. Code tampering: Reverse engineering and modification
  4. Insecure communication: Unencrypted network traffic
  5. Authentication flaws: Weak or missing authentication
  6. Authorization issues: Insufficient access controls

Secure Data Storage

1. Use Secure Storage

import * as SecureStore from 'expo-secure-store';

class SecureStorage {
  async save(key: string, value: string): Promise<void> {
    await SecureStore.setItemAsync(key, value, {
      requireAuthentication: true, // Require biometrics
      authenticationPrompt: 'Authenticate to access secure data',
    });
  }
  
  async load(key: string): Promise<string | null> {
    try {
      return await SecureStore.getItemAsync(key, {
        requireAuthentication: true,
      });
    } catch (error) {
      console.error('Failed to load secure data:', error);
      return null;
    }
  }
  
  async delete(key: string): Promise<void> {
    await SecureStore.deleteItemAsync(key);
  }
}

2. Encrypt Sensitive Data

import CryptoJS from 'crypto-js';

class EncryptedStorage {
  private encryptionKey: string;
  
  constructor() {
    // Get key from secure storage or keychain
    this.encryptionKey = this.getEncryptionKey();
  }
  
  encrypt(data: string): string {
    return CryptoJS.AES.encrypt(data, this.encryptionKey).toString();
  }
  
  decrypt(encryptedData: string): string {
    const bytes = CryptoJS.AES.decrypt(encryptedData, this.encryptionKey);
    return bytes.toString(CryptoJS.enc.Utf8);
  }
  
  async save(key: string, data: string): Promise<void> {
    const encrypted = this.encrypt(data);
    await SecureStore.setItemAsync(key, encrypted);
  }
  
  async load(key: string): Promise<string | null> {
    const encrypted = await SecureStore.getItemAsync(key);
    if (!encrypted) return null;
    return this.decrypt(encrypted);
  }
}

3. Never Store Sensitive Data in AsyncStorage

// ❌ Bad: Plaintext in AsyncStorage
await AsyncStorage.setItem('token', authToken);
await AsyncStorage.setItem('password', userPassword);

// ✅ Good: Use SecureStore
await SecureStore.setItemAsync('token', authToken);
// Never store passwords - use tokens instead

Secure Network Communication

1. Always Use HTTPS

// ✅ Good: HTTPS only
const API_URL = 'https://api.example.com';

// ❌ Bad: HTTP
const API_URL = 'http://api.example.com'; // Insecure!

2. Certificate Pinning

import { fetch } from 'react-native';

class SecureApiClient {
  async request(url: string, options: RequestInit): Promise<Response> {
    // Certificate pinning configuration
    const pinnedCertificates = [
      'sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=',
      // Your server's certificate fingerprint
    ];
    
    // Use library like react-native-cert-pinner
    return fetch(url, {
      ...options,
      // Certificate pinning handled by library
    });
  }
}

3. Request Signing

import CryptoJS from 'crypto-js';

class SignedApiClient {
  private apiKey: string;
  private secretKey: string;
  
  async request(
    method: string,
    endpoint: string,
    body?: any
  ): Promise<Response> {
    const timestamp = Date.now().toString();
    const nonce = this.generateNonce();
    
    const signature = this.signRequest(method, endpoint, timestamp, nonce, body);
    
    return fetch(`${this.baseUrl}${endpoint}`, {
      method,
      headers: {
        'X-API-Key': this.apiKey,
        'X-Timestamp': timestamp,
        'X-Nonce': nonce,
        'X-Signature': signature,
        'Content-Type': 'application/json',
      },
      body: body ? JSON.stringify(body) : undefined,
    });
  }
  
  private signRequest(
    method: string,
    endpoint: string,
    timestamp: string,
    nonce: string,
    body?: any
  ): string {
    const message = `${method}${endpoint}${timestamp}${nonce}${body ? JSON.stringify(body) : ''}`;
    return CryptoJS.HmacSHA256(message, this.secretKey).toString();
  }
}

Authentication Security

1. Use Secure Token Storage

class AuthManager {
  async saveTokens(accessToken: string, refreshToken: string): Promise<void> {
    // Store in secure storage
    await SecureStore.setItemAsync('access_token', accessToken);
    await SecureStore.setItemAsync('refresh_token', refreshToken);
  }
  
  async getAccessToken(): Promise<string | null> {
    return await SecureStore.getItemAsync('access_token');
  }
  
  async refreshAccessToken(): Promise<string | null> {
    const refreshToken = await SecureStore.getItemAsync('refresh_token');
    if (!refreshToken) return null;
    
    const response = await fetch(`${API_URL}/auth/refresh`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ refresh_token: refreshToken }),
    });
    
    const { access_token } = await response.json();
    await SecureStore.setItemAsync('access_token', access_token);
    return access_token;
  }
  
  async logout(): Promise<void> {
    await SecureStore.deleteItemAsync('access_token');
    await SecureStore.deleteItemAsync('refresh_token');
  }
}

2. Biometric Authentication

import * as LocalAuthentication from 'expo-local-authentication';

class BiometricAuth {
  async isAvailable(): Promise<boolean> {
    const compatible = await LocalAuthentication.hasHardwareAsync();
    const enrolled = await LocalAuthentication.isEnrolledAsync();
    return compatible && enrolled;
  }
  
  async authenticate(): Promise<boolean> {
    const result = await LocalAuthentication.authenticateAsync({
      promptMessage: 'Authenticate to access the app',
      fallbackLabel: 'Use passcode',
      disableDeviceFallback: false,
    });
    
    return result.success;
  }
}

3. Session Management

class SessionManager {
  private sessionTimeout = 15 * 60 * 1000; // 15 minutes
  private lastActivity: number = Date.now();
  
  startSession(): void {
    this.lastActivity = Date.now();
    this.startActivityMonitor();
  }
  
  private startActivityMonitor(): void {
    setInterval(() => {
      if (Date.now() - this.lastActivity > this.sessionTimeout) {
        this.endSession();
      }
    }, 60000); // Check every minute
  }
  
  updateActivity(): void {
    this.lastActivity = Date.now();
  }
  
  async endSession(): Promise<void> {
    await this.authManager.logout();
    // Navigate to login screen
  }
}

Input Validation

1. Validate All Inputs

import { z } from 'zod';

const UserSchema = z.object({
  email: z.string().email(),
  password: z.string().min(8).regex(/[A-Z]/).regex(/[0-9]/),
  age: z.number().int().min(18).max(120),
});

function validateUserInput(input: unknown): User {
  return UserSchema.parse(input); // Throws if invalid
}

2. Sanitize Outputs

import DOMPurify from 'isomorphic-dompurify';

function sanitizeHtml(html: string): string {
  return DOMPurify.sanitize(html);
}

function sanitizeUserInput(input: string): string {
  // Remove potentially dangerous characters
  return input
    .replace(/[<>]/g, '') // Remove HTML tags
    .replace(/javascript:/gi, '') // Remove javascript: protocol
    .trim();
}

Code Protection

1. Obfuscation

// Use tools like:
// - react-native-obfuscator
// - jscrambler
// - proguard (Android)

// Obfuscate sensitive code
const API_KEY = process.env.API_KEY; // Don't hardcode!

2. Root/Jailbreak Detection

import JailMonkey from 'jail-monkey';

class SecurityCheck {
  checkDeviceSecurity(): SecurityStatus {
    return {
      isJailBroken: JailMonkey.isJailBroken(),
      isOnExternalStorage: JailMonkey.isOnExternalStorage(),
      canMockLocation: JailMonkey.canMockLocation(),
    };
  }
  
  shouldBlockAccess(): boolean {
    const status = this.checkDeviceSecurity();
    return status.isJailBroken || status.canMockLocation;
  }
}

3. Debug Detection

class DebugDetection {
  isDebuggingEnabled(): boolean {
    // Check for debugger
    if (__DEV__) {
      return true; // Development mode
    }
    
    // Additional checks for production
    return false;
  }
  
  handleDebuggingDetected(): void {
    // Log security event
    // Optionally block access or show warning
    console.warn('Debugging detected - security risk');
  }
}

Secure Configuration

1. Environment Variables

// ✅ Good: Use environment variables
const API_URL = process.env.EXPO_PUBLIC_API_URL;
const API_KEY = process.env.EXPO_PUBLIC_API_KEY;

// ❌ Bad: Hardcoded secrets
const API_KEY = 'sk_live_1234567890abcdef'; // Never do this!

2. Secure Config Files

// config.ts
export const config = {
  apiUrl: process.env.EXPO_PUBLIC_API_URL || 'https://api.example.com',
  // Never include secrets in config files
  // Use secure storage or environment variables
};

Best Practices Checklist

  • Secure storage: Use SecureStore for sensitive data
  • HTTPS only: Never use HTTP
  • Certificate pinning: Prevent MITM attacks
  • Input validation: Validate all user inputs
  • Output sanitization: Sanitize data before display
  • Token management: Secure token storage and refresh
  • Session timeout: Auto-logout after inactivity
  • Biometric auth: Use when available
  • Root detection: Block on jailbroken devices
  • No hardcoded secrets: Use environment variables
  • Error handling: Don't leak sensitive info in errors
  • Logging: Don't log sensitive data

Security Testing

1. Static Analysis

# Use tools like:
# - ESLint security plugins
# - SonarQube
# - Snyk
npm run lint:security

2. Dependency Scanning

# Check for vulnerable dependencies
npm audit
npx snyk test

3. Penetration Testing

Test for:

  • SQL injection
  • XSS vulnerabilities
  • Insecure data storage
  • Weak authentication
  • Authorization bypass

Conclusion

Mobile app security requires:

  • Secure storage: Encrypt sensitive data
  • Secure communication: HTTPS + certificate pinning
  • Strong authentication: Tokens + biometrics
  • Input validation: Validate and sanitize
  • Code protection: Obfuscation + root detection
  • Secure configuration: No hardcoded secrets

Security is not a feature—it's a requirement. Build security in from the start, and your users' data will be protected.