Mobile Security: Certificate Pinning, Jailbreak Detection, and Secure Storage
TL;DR: This guide covers mobile app security. Learn certificate pinning, jailbreak detection, secure storage, and protecting mobile applications from attacks.
Introduction
Mobile applications handle sensitive data including personal information, credentials, and financial data. Protecting this data requires defense in depthโmultiple security layers that together provide comprehensive protection. This guide covers the essential security practices for mobile applications: certificate pinning, jailbreak detection, secure storage, and additional protective measures.
Understanding mobile threats is the first step in protecting against them. Common attack vectors include man-in-the-middle attacks where attackers intercept network traffic, compromised devices where security controls have been bypassed, and reverse engineering where attackers analyze app code to find vulnerabilities.
Certificate Pinning
Why Certificate Pinning Matters
Certificate pinning protects against man-in-the-middle (MITM) attacks by ensuring the app only trusts specific certificates. Without pinning, any valid certificate from a trusted Certificate Authority can establish a connectionโattackers can use certificates from trusted CAs to intercept traffic.
Implementation Approaches
There are several approaches to certificate pinning:
- Public Key Pinning: Pin the public key, not the certificate. This allows certificate rotation without updating the app.
- Certificate Pinning: Pin the entire certificate. More restrictive but requires updates when certificates change.
- Domain Pinning: Pin specific domains while allowing others.
React Native Implementation
import axios from 'axios';
import { validateDomain } from 'react-native-ssl-public-key';
// Create axios instance with pinning
const api = axios.create({
baseURL: 'https://api.example.com',
});
// Add request interceptor for pinning
api.interceptors.request.use(async (config) => {
try {
// Validate certificate for the domain
const isValid = await validateDomain(
config.url,
'SHA256/PUBLIC_KEY_HASH_BASE64'
);
if (!isValid) {
throw new Error('Certificate pinning validation failed');
}
} catch (error) {
// Handle validation failure
console.error('Security error:', error);
throw error;
}
return config;
});
Native iOS Implementation
import Security
import Alamofire
class CertificatePinningDelegate: NSObject, URLSessionDelegate {
func urlSession(
_ session: URLSession,
didReceive challenge: URLAuthenticationChallenge,
completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void
) {
guard challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust,
let serverTrust = challenge.protectionSpace.serverTrust else {
completionHandler(.cancelAuthenticationChallenge, nil)
return
}
// Define pinned public keys
let pinnedKeys = [
"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgA..."
]
// Get server's certificate chain
if let serverCertificate = SecTrustGetCertificateAtIndex(serverTrust, 0) {
if let serverPublicKey = SecCertificateCopyKey(serverCertificate) {
if let serverPublicKeyData = SecKeyCopyExternalRepresentation(serverPublicKey, nil) as Data? {
let serverKeyHash = serverPublicKeyData.base64EncodedString()
if pinnedKeys.contains(serverKeyHash) {
completionHandler(.useCredential, URLCredential(trust: serverTrust))
return
}
}
}
}
completionHandler(.cancelAuthenticationChallenge, nil)
}
}
Native Android Implementation
import okhttp3.CertificatePinner
import okhttp3.OkHttpClient
class NetworkModule {
fun createPinnedClient(): OkHttpClient {
val certificatePinner = CertificatePinner.Builder()
.add("api.example.com", "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=")
.add("api.example.com", "sha256/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=")
.build()
return OkHttpClient.Builder()
.certificatePinner(certificatePinner)
.build()
}
}
Flutter Implementation
import 'dart:io';
import 'package:http/http.dart' as http;
class PinningClient extends http.BaseClient {
final String expectedPublicKey;
PinningClient({required this.expectedPublicKey});
@override
Future<http.StreamedResponse> send(http.BaseRequest request) async {
final client = HttpClient();
client.connectionTimeout = Duration(seconds: 10);
client.certificateVerifier = (X509Certificate cert, String host, int port) {
// Compare public key
return cert.pem == expectedPublicKey;
};
final streamedResponse = await client.send(request);
return streamedResponse;
}
}
Jailbreak/Root Detection
Understanding the Threat
Jailbroken (iOS) or rooted (Android) devices bypass security controls, making it easier for attackers to analyze app behavior, intercept network traffic, and manipulate app functionality. While many users jailbreak for legitimate reasons, the security implications require consideration.
Detection Techniques
// React Native jailbreak detection
import { Platform } from 'react-native';
const detectJailbreak = async () => {
if (Platform.OS === 'ios') {
// Check for common jailbreak files
const jailbreakPaths = [
'/Applications/Cydia.app',
'/Library/MobileSubstrate/MobileSubstrate.dylib',
'/bin/bash',
'/usr/sbin/sshd',
'/etc/apt',
'/private/var/lib/apt/',
];
// Check if app can write outside sandbox
const canWrite = await checkFilesystemAccess();
return { isJailbroken: jailbreakPaths.length > 0 || canWrite };
}
if (Platform.OS === 'android') {
// Check for root apps
const rootApps = [
'com.topjohnwu.magisk',
'com.smedialab.onekeyroot',
'eu.chainfire.supersu',
];
// Check for su binary
const hasSuBinary = await checkSuBinary();
// Check for test-keys
const hasTestKeys = await checkTestKeys();
return {
isRooted: rootApps.length > 0 || hasSuBinary || hasTestKeys
};
}
return { isJailbroken: false };
};
Handling Detection
const SecurityService = {
checkDeviceSecurity: async () => {
const { isJailbroken } = await detectJailbreak();
if (isJailbroken) {
// Options: warn user, restrict features, or block access
return {
allowed: false,
reason: 'Device security compromised'
};
}
return { allowed: true };
}
};
Secure Storage
Storing Sensitive Data
Never store sensitive data in insecure locations. Use platform-specific secure storage solutions.
React Native Keychain
import * as Keychain from 'react-native-keychain';
// Store sensitive data securely
const storeSecure = async (key, value) => {
try {
await Keychain.setGenericPassword(key, value, {
service: 'my-app-secure-storage',
accessible: Keychain.ACCESSIBLE.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
});
return true;
} catch (error) {
console.error('Storage error:', error);
return false;
}
};
// Retrieve secure data
const getSecure = async (key) => {
try {
const result = await Keychain.getGenericPassword({
service: 'my-app-secure-storage',
});
return result ? result.password : null;
} catch (error) {
console.error('Retrieval error:', error);
return null;
}
};
// Delete secure data
const deleteSecure = async (key) => {
try {
await Keychain.resetGenericPassword({
service: 'my-app-secure-storage',
});
return true;
} catch (error) {
return false;
}
};
iOS Keychain Services
import Security
class KeychainService {
static func save(key: String, data: Data) -> Bool {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key,
kSecValueData as String: data,
kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlockedThisDeviceOnly
]
SecItemDelete(query as CFDictionary)
let status = SecItemAdd(query as CFDictionary, nil)
return status == errSecSuccess
}
static func load(key: String) -> Data? {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key,
kSecReturnData as String: true,
kSecMatchLimit as String: kSecMatchLimitOne
]
var dataTypeRef: AnyObject?
let status = SecItemCopyMatching(query as CFDictionary, &dataTypeRef)
if status == errSecSuccess {
return dataTypeRef as? Data
}
return nil
}
}
Android EncryptedSharedPreferences
import android.content.Context
import android.security.encryption.EncryptedSharedPreferences
import android.security.encryption.MasterKeys
class SecureStorage(context: Context) {
private val masterKey = MasterKeys.getOrCreate(
MasterKey.AES256_GCM_SPEC
)
private val sharedPreferences = EncryptedSharedPreferences.create(
context,
"secure_prefs",
masterKey,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
fun save(key: String, value: String) {
sharedPreferences.edit().putString(key, value).apply()
}
fun get(key: String): String? {
return sharedPreferences.getString(key, null)
}
fun delete(key: String) {
sharedPreferences.edit().remove(key).apply()
}
}
Code Obfuscation
Protecting App Code
Code obfuscation makes reverse engineering harder by renaming classes, removing debug information, and encrypting strings.
React Native uses ProGuard/R8:
// android/app/build.gradle
android {
buildTypes {
release {
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
iOS uses bitcode (deprecated) and Swift obfuscation tools.
Additional Security Measures
Input Validation
// Validate all inputs
const validateInput = (input, schema) => {
// Use Joi, Yup, or Zod for schema validation
return schema.validate(input);
};
Anti-Debugging
// Detect debugging
const detectDebugger = () => {
const isDebugged =
global.Debug ||
window.debugger ||
console._commandLineAPI;
return !!isDebugged;
};
Conclusion
Mobile security requires:
- Certificate pinning - Prevent MITM attacks by validating server certificates
- Jailbreak detection - Detect compromised devices and respond appropriately
- Secure storage - Use platform secure storage for sensitive data
- Code obfuscation - Make reverse engineering harder
- Input validation - Validate all user input
- Regular security audits - Test for vulnerabilities
Defense in depth provides the best protection. Combine multiple security measures to create robust mobile applications.
Comments