Module Overview
Estimated Time: 4 hours | Difficulty: Advanced | Prerequisites: Authentication, Storage modules
- Secure storage solutions
- Certificate pinning
- Code obfuscation
- Jailbreak/root detection
- Data encryption
- Security auditing
Security Threat Model
Copy
┌─────────────────────────────────────────────────────────────────────────────┐
│ 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:Copy
npx expo install expo-secure-store
Copy
// 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:Copy
npm install react-native-keychain
cd ios && pod install
Copy
// 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
Copy
npm install react-native-mmkv
Copy
// 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
Copy
npm install react-native-ssl-pinning
Copy
// 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
Copy
// 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
Copy
// 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
Copy
# 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(...);
}
Copy
// android/app/build.gradle
android {
buildTypes {
release {
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
Jailbreak/Root Detection
Copy
npm install jail-monkey
Copy
// 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
Copy
// 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
Copy
// 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
Copy
// 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
Copy
// 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
Copy
// 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
Copy
// 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