正在加载,请稍候…

Mobile App Security Best Practices: Pinning, Secure Storage, and Code Protection

Comprehensive guide to mobile application security covering certificate pinning, secure storage, jailbreak/root detection, code obfuscation, and API security for iOS and Android.

Mobile App Security Best Practices: Pinning, Secure Storage, and Code Protection

Mobile application security requires a multi-layered approach. From network communication to local data storage, attackers probe every surface. This guide covers production-grade security measures for both iOS and Android applications.

Network Security: Certificate Pinning

Certificate pinning prevents man-in-the-middle attacks by validating the server's certificate against a known good value.

iOS Certificate Pinning with URLSession

class PinnedURLSessionDelegate: NSObject, URLSessionDelegate {
    private let pinnedCertificates: [Data]

    init(certificates: [Data]) {
        self.pinnedCertificates = certificates
    }

    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
        }

        // Validate the certificate chain
        var error: CFError?
        guard SecTrustEvaluateWithError(serverTrust, &error) else {
            completionHandler(.cancelAuthenticationChallenge, nil)
            return
        }

        // Get the server certificate
        guard let serverCertificate = SecTrustGetCertificateAtIndex(serverTrust, 0) else {
            completionHandler(.cancelAuthenticationChallenge, nil)
            return
        }

        let serverCertData = SecCertificateCopyData(serverCertificate) as Data

        // Check against pinned certificates
        if pinnedCertificates.contains(serverCertData) {
            completionHandler(.useCredential, URLCredential(trust: serverTrust))
        } else {
            completionHandler(.cancelAuthenticationChallenge, nil)
            // Report potential MITM attack
            SecurityLogger.report(event: .certificatePinningFailure)
        }
    }
}

// Usage
let pinnedDelegate = PinnedURLSessionDelegate(
    certificates: [loadCertificateData(named: "api.example.com")]
)
let session = URLSession(
    configuration: .default,
    delegate: pinnedDelegate,
    delegateQueue: nil
)

Android Certificate Pinning with OkHttp

import okhttp3.CertificatePinner
import okhttp3.OkHttpClient

val certificatePinner = CertificatePinner.Builder()
    .add("api.example.com", "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=")
    .add("api.example.com", "sha256/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=") // backup
    .build()

val client = OkHttpClient.Builder()
    .certificatePinner(certificatePinner)
    .connectTimeout(30, TimeUnit.SECONDS)
    .readTimeout(30, TimeUnit.SECONDS)
    .build()

Network Security Config (Android)

<!-- res/xml/network_security_config.xml -->
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <domain-config cleartextTrafficPermitted="false">
        <domain includeSubdomains="true">api.example.com</domain>
        <pin-set expiration="2027-01-01">
            <pin digest="SHA-256">AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</pin>
            <!-- Backup pin -->
            <pin digest="SHA-256">BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=</pin>
        </pin-set>
        <trust-anchors>
            <certificates src="system" />
        </trust-anchors>
    </domain-config>
</network-security-config>

Secure Storage

iOS Keychain

import Security

class KeychainManager {
    static func save(key: String, value: String) throws {
        let data = value.data(using: .utf8)!

        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)

        guard status == errSecSuccess else {
            throw KeychainError.saveFailed(status)
        }
    }

    static func load(key: String) throws -> String {
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrAccount as String: key,
            kSecReturnData as String: true,
            kSecMatchLimit as String: kSecMatchLimitOne,
        ]

        var result: AnyObject?
        let status = SecItemCopyMatching(query as CFDictionary, &result)

        guard status == errSecSuccess,
              let data = result as? Data,
              let value = String(data: data, encoding: .utf8) else {
            throw KeychainError.loadFailed(status)
        }

        return value
    }

    static func delete(key: String) {
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrAccount as String: key,
        ]
        SecItemDelete(query as CFDictionary)
    }
}

Android EncryptedSharedPreferences

import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKey

class SecureStorage(context: Context) {
    private val masterKey = MasterKey.Builder(context)
        .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
        .build()

    private val sharedPreferences = EncryptedSharedPreferences.create(
        context,
        "secure_prefs",
        masterKey,
        EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
        EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM,
    )

    fun saveToken(token: String) {
        sharedPreferences.edit()
            .putString("auth_token", token)
            .apply()
    }

    fun getToken(): String? = sharedPreferences.getString("auth_token", null)

    fun clearToken() {
        sharedPreferences.edit().remove("auth_token").apply()
    }
}

Jailbreak and Root Detection

iOS Jailbreak Detection

class IntegrityChecker {
    static func isDeviceJailbroken() -> Bool {
        // Check for jailbreak files
        let jailbreakPaths = [
            "/Applications/Cydia.app",
            "/Library/MobileSubstrate/MobileSubstrate.dylib",
            "/bin/bash",
            "/usr/sbin/sshd",
            "/etc/apt",
            "/private/var/lib/apt/",
        ]

        for path in jailbreakPaths {
            if FileManager.default.fileExists(atPath: path) {
                return true
            }
        }

        // Try to write to system directory
        let testPath = "/private/jailbreak_test_\(UUID().uuidString)"
        do {
            try "test".write(toFile: testPath, atomically: true, encoding: .utf8)
            try FileManager.default.removeItem(atPath: testPath)
            return true
        } catch {}

        // Check for sandbox violation
        if let _ = try? String(contentsOfFile: "/etc/passwd") {
            return true
        }

        return false
    }
}

Android Root Detection

object RootDetector {
    fun isDeviceRooted(): Boolean {
        return checkSuBinary() || checkBuildTags() || checkRootApps()
    }

    private fun checkSuBinary(): Boolean {
        val paths = arrayOf(
            "/system/bin/su", "/system/xbin/su",
            "/sbin/su", "/data/local/xbin/su",
            "/data/local/bin/su", "/data/local/su",
        )
        return paths.any { File(it).exists() }
    }

    private fun checkBuildTags(): Boolean {
        val buildTags = Build.TAGS
        return buildTags != null && buildTags.contains("test-keys")
    }

    private fun checkRootApps(): Boolean {
        val rootApps = arrayOf(
            "com.noshufou.android.su",
            "com.thirdparty.superuser",
            "eu.chainfire.supersu",
            "com.koushikdutta.superuser",
            "com.zachspong.temprootremovejb",
        )

        val pm = appContext.packageManager
        return rootApps.any { packageName ->
            try {
                pm.getPackageInfo(packageName, 0)
                true
            } catch (e: PackageManager.NameNotFoundException) {
                false
            }
        }
    }
}

API Security

JWT Token Handling

import * as SecureStore from 'expo-secure-store';
import jwtDecode from 'jwt-decode';

class TokenManager {
  private static ACCESS_TOKEN_KEY = 'access_token';
  private static REFRESH_TOKEN_KEY = 'refresh_token';

  static async saveTokens(accessToken: string, refreshToken: string) {
    await Promise.all([
      SecureStore.setItemAsync(this.ACCESS_TOKEN_KEY, accessToken),
      SecureStore.setItemAsync(this.REFRESH_TOKEN_KEY, refreshToken),
    ]);
  }

  static async getAccessToken(): Promise<string | null> {
    const token = await SecureStore.getItemAsync(this.ACCESS_TOKEN_KEY);
    if (!token) return null;

    const decoded = jwtDecode<{ exp: number }>(token);
    const isExpired = decoded.exp * 1000 < Date.now() + 30000; // 30s buffer

    if (isExpired) {
      return this.refreshAccessToken();
    }

    return token;
  }

  static async refreshAccessToken(): Promise<string | null> {
    const refreshToken = await SecureStore.getItemAsync(this.REFRESH_TOKEN_KEY);
    if (!refreshToken) return null;

    try {
      const response = await fetch('/api/auth/refresh', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ refreshToken }),
      });

      const { accessToken, newRefreshToken } = await response.json();
      await this.saveTokens(accessToken, newRefreshToken);
      return accessToken;
    } catch {
      await this.clearTokens();
      return null;
    }
  }

  static async clearTokens() {
    await Promise.all([
      SecureStore.deleteItemAsync(this.ACCESS_TOKEN_KEY),
      SecureStore.deleteItemAsync(this.REFRESH_TOKEN_KEY),
    ]);
  }
}

Request Signing

import CryptoJS from 'crypto-js';

function signRequest(
  method: string,
  path: string,
  body: object | null,
  secretKey: string
): Record<string, string> {
  const timestamp = Date.now().toString();
  const nonce = Math.random().toString(36).substring(2);
  const bodyHash = body
    ? CryptoJS.SHA256(JSON.stringify(body)).toString()
    : '';

  const message = `${method}\n${path}\n${timestamp}\n${nonce}\n${bodyHash}`;
  const signature = CryptoJS.HmacSHA256(message, secretKey).toString();

  return {
    'X-Timestamp': timestamp,
    'X-Nonce': nonce,
    'X-Signature': signature,
  };
}

Code Obfuscation

Android ProGuard/R8

# proguard-rules.pro
-optimizationpasses 5
-dontusemixedcaseclassnames
-dontskipnonpubliclibraryclasses
-dontpreverify
-verbose

# Keep model classes
-keep class com.example.app.models.** { *; }

# Obfuscate everything else
-keepattributes Signature
-keepattributes *Annotation*

# Remove logging in release
-assumenosideeffects class android.util.Log {
    public static *** d(...);
    public static *** v(...);
    public static *** i(...);
}

Conclusion

Mobile app security is not a single feature but a holistic discipline. Implement certificate pinning to prevent network interception. Use platform-native secure storage for sensitive data. Detect compromised devices and respond appropriately. Sign and validate API requests. Apply code obfuscation in release builds. Conduct regular security audits and penetration testing. Combined, these layers create defense-in-depth that protects both your users and your business from the most common mobile attack vectors.