backups and restricts access to unlocked device states.
// src/security/SecureStorage.ts
import * as Keychain from 'react-native-keychain';
import { Platform } from 'react-native';
export interface SecureStorageConfig {
service: string;
accessControl: Keychain.AccessControl;
accessible: Keychain.Accessible;
}
export class SecureStorage {
private static defaultConfig: SecureStorageConfig = {
service: 'com.yourapp.secure',
// Biometric or Device Passcode required; keys deleted if biometrics removed
accessControl: Keychain.ACCESS_CONTROL.BIOMETRY_CURRENT_SET_OR_DEVICE_PASSCODE,
// Data accessible only when device is unlocked and not backed up
accessible: Keychain.ACCESSIBLE.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
};
static async setSecret(key: string, value: string): Promise<void> {
try {
await Keychain.setGenericPassword(key, value, {
...SecureStorage.defaultConfig,
});
} catch (error) {
// Log to secure telemetry; do not expose stack traces
console.error('SecureStorage: Write failed', error);
throw new Error('STORAGE_WRITE_ERROR');
}
}
static async getSecret(key: string): Promise<string | null> {
try {
const credentials = await Keychain.getGenericPassword({
...SecureStorage.defaultConfig,
});
if (credentials && credentials.username === key) {
return credentials.password;
}
return null;
} catch (error) {
console.error('SecureStorage: Read failed', error);
return null;
}
}
static async clearSecret(key: string): Promise<void> {
await Keychain.resetGenericPassword({
...SecureStorage.defaultConfig,
});
}
}
2. Certificate Pinning Implementation
Implement pinning at the network client level. Use SHA-256 hashes of public keys. Maintain backup pins to ensure availability during key rotation.
// src/network/PinnedHttpClient.ts
import axios, { AxiosInstance, AxiosRequestConfig } from 'axios';
import { CertificatePinning } from 'react-native-cert-pinning';
export class PinnedHttpClient {
private client: AxiosInstance;
constructor(config: { baseUrl: string; pins: string[]; backupPins?: string[] }) {
this.client = axios.create({
baseURL: config.baseUrl,
timeout: 10000,
});
// Interceptor to enforce pinning before request dispatch
this.client.interceptors.request.use(async (requestConfig) => {
const url = new URL(requestConfig.url!, config.baseUrl);
const isPinned = await this.verifyPin(url.hostname, config.pins, config.backupPins);
if (!isPinned) {
throw new Error('CERTIFICATE_PINNING_FAILED');
}
return requestConfig;
});
}
private async verifyPin(
host: string,
pins: string[],
backupPins?: string[]
): Promise<boolean> {
try {
// react-native-cert-pinning validates the server cert chain against provided pins
await CertificatePinning.check(
host,
443,
[...pins, ...(backupPins || [])],
5000 // Timeout
);
return true;
} catch {
// Pinning failed; block request
return false;
}
}
get<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
return this.client.get(url, config).then(res => res.data);
}
}
3. Runtime Integrity Checks
Integrate checks for jailbreak/root and debugger attachment. These checks should run periodically and before sensitive operations.
// src/security/RuntimeIntegrity.ts
import DeviceInfo from 'react-native-device-info';
import { Platform } from 'react-native';
export class RuntimeIntegrity {
static async isDeviceCompromised(): Promise<boolean> {
const isJailbroken = await DeviceInfo.isEmulator() ? false : await DeviceInfo.is Jailbroken();
// Additional checks for Android root
const isRooted = Platform.OS === 'android'
? await DeviceInfo.isRooted()
: false;
return isJailbroken || isRooted;
}
static async isDebuggerAttached(): Promise<boolean> {
// Implementation depends on native module;
// typically checks ptrace or /proc/self/status on Android
// and sysctl on iOS.
// Return true if debugger detected.
return false;
}
static async validateEnvironment(): Promise<{ secure: boolean; risk: string }> {
const compromised = await this.isDeviceCompromised();
const debugged = await this.isDebuggerAttached();
if (compromised) return { secure: false, risk: 'DEVICE_JAILBROKEN' };
if (debugged) return { secure: false, risk: 'DEBUGGER_ATTACHED' };
return { secure: true, risk: 'NONE' };
}
}
4. Token Management and Rotation
Never store long-lived tokens. Use short-lived access tokens with secure refresh logic. Tokens must be stored in secure storage and cleared on session termination.
// src/auth/TokenManager.ts
import { SecureStorage } from './security/SecureStorage';
import { PinnedHttpClient } from './network/PinnedHttpClient';
export class TokenManager {
private static ACCESS_TOKEN_KEY = 'auth_access';
private static REFRESH_TOKEN_KEY = 'auth_refresh';
static async getValidAccessToken(client: PinnedHttpClient): Promise<string> {
const token = await SecureStorage.getSecret(this.ACCESS_TOKEN_KEY);
// Check expiry and refresh if needed
if (!token || this.isExpired(token)) {
return this.refreshToken(client);
}
return token;
}
private static async refreshToken(client: PinnedHttpClient): Promise<string> {
const refreshToken = await SecureStorage.getSecret(this.REFRESH_TOKEN_KEY);
if (!refreshToken) throw new Error('NO_REFRESH_TOKEN');
try {
const newTokens = await client.post('/auth/refresh', { token: refreshToken });
await SecureStorage.setSecret(this.ACCESS_TOKEN_KEY, newTokens.access);
await SecureStorage.setSecret(this.REFRESH_TOKEN_KEY, newTokens.refresh);
return newTokens.access;
} catch {
// Refresh failed; force re-authentication
await this.logout();
throw new Error('SESSION_EXPIRED');
}
}
static async logout(): Promise<void> {
await SecureStorage.clearSecret(this.ACCESS_TOKEN_KEY);
await SecureStorage.clearSecret(this.REFRESH_TOKEN_KEY);
}
private static isExpired(token: string): boolean {
// Decode JWT payload and check 'exp' claim
// Implementation omitted for brevity
return false;
}
}
Pitfall Guide
-
Hardcoding Secrets in Source Code:
- Mistake: Embedding API keys, signing keys, or encryption secrets directly in TypeScript or native code.
- Reality: Reverse engineering tools extract constants instantly. Use secure remote configuration or hardware-backed key generation where possible. If keys must exist in the binary, use obfuscation combined with runtime decryption, though this is not foolproof.
-
Relying on AsyncStorage for Sensitive Data:
- Mistake: Storing tokens, PII, or session data in
AsyncStorage (SQLite/JSON files).
- Reality: Data is stored in plain text and accessible via file system extraction on rooted devices. Always use
react-native-keychain or native Keystore/Keychain wrappers.
-
Ignoring SDK Supply Chain Risks:
- Mistake: Adding third-party SDKs without auditing their network calls, permissions, and data collection practices.
- Reality: SDKs can bypass app-level security controls, exfiltrate data, or introduce vulnerabilities. Maintain a Software Bill of Materials (SBOM) and restrict SDK permissions via manifest merging.
-
Disabling SSL Pinning in Debug Builds Carelessly:
- Mistake: Using build flags to disable pinning for debugging but accidentally shipping the debug configuration to production.
- Reality: This opens the app to MITM attacks in production. Use distinct build variants and automated CI/CD checks to verify pinning is enabled in release builds.
-
Assuming Obfuscation Prevents Reverse Engineering:
- Mistake: Treating ProGuard/R8 or symbol stripping as a security control.
- Reality: Obfuscation increases effort but does not prevent analysis. Attackers can deobfuscate code or hook runtime functions. Security must rely on cryptographic controls and runtime integrity, not obscurity.
-
Weak Biometric Fallback Logic:
- Mistake: Allowing biometric authentication to unlock sensitive features without verifying the key is still bound to the current biometric set.
- Reality: If a user adds/removes biometrics, keys should be invalidated or require re-authentication. Use
ACCESS_CONTROL.BIOMETRY_CURRENT_SET to ensure key validity aligns with enrolled biometrics.
-
Data Leakage via Screenshots and Backups:
- Mistake: Not preventing screenshots on sensitive screens or allowing app data to be included in cloud backups.
- Reality: Sensitive data can be captured via OS features. Disable screenshots on login/payment screens using native flags (
FLAG_SECURE on Android). Configure android:allowBackup="false" and iOS NSUbiquitousContainers restrictions to prevent backup exfiltration.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| High-Risk Financial/Banking App | RASP + Hardware-Backed Keystore + Strict Pinning + Runtime Integrity | Maximum tamper resistance required; regulatory compliance mandates defense-in-depth. | High dev cost; High compliance value; Lower breach risk. |
| Internal Enterprise App | MDM Integration + Basic Pinning + Secure Storage | Devices are managed; threat model focuses on data leakage rather than reverse engineering. | Low dev cost; Leverages existing MDM infrastructure. |
| Consumer Social/Media App | Obfuscation + Secure Storage + SDK Audit | Balance UX friction with security; focus on protecting user credentials and preventing data scraping. | Medium dev cost; Maintains performance and UX. |
Configuration Template
// config/security.config.ts
export const SecurityConfig = {
storage: {
service: 'com.yourapp.prod',
accessControl: 'BIOMETRY_CURRENT_SET_OR_DEVICE_PASSCODE',
accessible: 'WHEN_UNLOCKED_THIS_DEVICE_ONLY',
},
network: {
baseUrl: 'https://api.yourapp.com',
pinning: {
primary: ['sha256/ABC123...', 'sha256/DEF456...'],
backup: ['sha256/GHI789...'],
timeoutMs: 5000,
},
},
runtime: {
allowJailbreak: false,
allowDebugger: false,
checkIntervalMs: 30000,
actionOnFailure: 'TERMINATE_SESSION',
},
dataLeakage: {
disableScreenshots: ['LoginScreen', 'PaymentScreen', 'ProfileScreen'],
allowBackup: false,
},
};
Quick Start Guide
-
Install Dependencies:
npm install react-native-keychain react-native-cert-pinning react-native-device-info axios
cd ios && pod install
-
Replace Storage Calls:
Locate all instances of AsyncStorage.setItem/getItem. Replace with SecureStorage.setSecret and SecureStorage.getSecret using the provided implementation.
-
Configure Pinning:
Extract public key hashes from your backend certificates. Add them to SecurityConfig.network.pinning. Initialize PinnedHttpClient in your API service layer.
-
Add Runtime Checks:
Call RuntimeIntegrity.validateEnvironment() on app launch and before sensitive transactions. Handle the risk response by blocking UI or logging out.
-
Verify in CI/CD:
Add a build step to verify that SecurityConfig.runtime.allowJailbreak is false and pinning is enabled for release builds. Fail the pipeline if configurations are insecure.