Skip to main content
โšก Calmops

Mobile Security: Certificate Pinning, Jailbreak Detection, and Secure Storage

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:

  1. Public Key Pinning: Pin the public key, not the certificate. This allows certificate rotation without updating the app.
  2. Certificate Pinning: Pin the entire certificate. More restrictive but requires updates when certificates change.
  3. 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:

  1. Certificate pinning - Prevent MITM attacks by validating server certificates
  2. Jailbreak detection - Detect compromised devices and respond appropriately
  3. Secure storage - Use platform secure storage for sensitive data
  4. Code obfuscation - Make reverse engineering harder
  5. Input validation - Validate all user input
  6. Regular security audits - Test for vulnerabilities

Defense in depth provides the best protection. Combine multiple security measures to create robust mobile applications.

Comments