iOS Security Best Practices 2025: Complete Developer Guide

By Harjot Singh Panesar | January 9, 2026 | 16 min read

Security isn't optional - it's foundational. A single data breach can destroy user trust and expose your company to legal liability. After building secure iOS apps for enterprise clients handling sensitive data, I've compiled the essential security practices every iOS developer must implement.

1. Secure Data Storage

The Keychain: Your First Line of Defense

Never store sensitive data in UserDefaults, plist files, or plain Core Data. The iOS Keychain provides hardware-backed encryption:

import Security

class KeychainManager {
    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
        ]

        // Delete existing item first
        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 result: AnyObject?
        let status = SecItemCopyMatching(query as CFDictionary, &result)

        return status == errSecSuccess ? result as? Data : nil
    }
}

Security Alert: Never store API keys, passwords, or tokens in your source code. Use the Keychain or secure environment variables during build time.

Keychain Access Levels

Accessibility Level When to Use
kSecAttrAccessibleWhenUnlockedThisDeviceOnly Most sensitive data - only accessible when device is unlocked, not backed up
kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly Background tasks needing credentials after first unlock
kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly Requires device passcode, deleted if passcode removed

Encrypting Local Databases

For apps storing significant local data, encrypt your database:

  • SQLCipher - AES-256 encryption for SQLite databases
  • Core Data with encryption - Use NSFileProtection or custom encryption
  • Realm Encryption - Built-in 64-byte encryption key support
// SQLCipher example
let db = try Connection("/path/to/db.sqlite3")
try db.key("your-256-bit-key")

// Realm encryption
var config = Realm.Configuration()
config.encryptionKey = getKeyFromKeychain() // 64-byte key
let realm = try Realm(configuration: config)

2. Secure Network Communication

App Transport Security (ATS)

Always use HTTPS. ATS is enabled by default - don't disable it:

// Info.plist - DON'T DO THIS
<key>NSAppTransportSecurity</key>
<dict>
    <key>NSAllowsArbitraryLoads</key>
    <true/> // NEVER in production!
</dict>

Certificate Pinning

Prevent man-in-the-middle attacks by pinning your server's certificate:

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

    init(certificateNames: [String]) {
        pinnedCertificates = certificateNames.compactMap { name in
            guard let url = Bundle.main.url(forResource: name, withExtension: "cer"),
                  let data = try? Data(contentsOf: url) else { return nil }
            return data
        }
        super.init()
    }

    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
        }

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

        let serverCertData = SecCertificateCopyData(serverCertificate) as Data

        // Check if server cert matches pinned cert
        if pinnedCertificates.contains(serverCertData) {
            completionHandler(.useCredential, URLCredential(trust: serverTrust))
        } else {
            completionHandler(.cancelAuthenticationChallenge, nil)
        }
    }
}

Pro Tip: Pin the public key instead of the certificate. This survives certificate rotation without app updates.

3. Authentication Security

Biometric Authentication

Implement Face ID and Touch ID properly:

import LocalAuthentication

class BiometricAuth {
    func authenticate() async throws -> Bool {
        let context = LAContext()
        var error: NSError?

        guard context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) else {
            throw BiometricError.notAvailable
        }

        return try await context.evaluatePolicy(
            .deviceOwnerAuthenticationWithBiometrics,
            localizedReason: "Authenticate to access your account"
        )
    }
}

// Combine with Keychain for maximum security
func getSecureToken() async throws -> String? {
    guard try await BiometricAuth().authenticate() else {
        throw AuthError.biometricFailed
    }

    // Only accessible after biometric auth
    let query: [String: Any] = [
        kSecClass as String: kSecClassGenericPassword,
        kSecAttrAccount as String: "authToken",
        kSecReturnData as String: true,
        kSecUseAuthenticationContext as String: LAContext()
    ]

    // ... retrieve from Keychain
}

Secure Token Management

  • Use short-lived access tokens with refresh tokens
  • Store refresh tokens in Keychain with biometric protection
  • Implement token rotation on each refresh
  • Clear all tokens on logout
  • Handle token expiration gracefully

4. Code Security

Prevent Reverse Engineering

While no protection is foolproof, make it harder:

  • Enable Bitcode - Apple recompiles your app
  • Strip debug symbols - In release builds
  • Obfuscate sensitive strings - Don't hardcode secrets
  • Detect jailbroken devices - For high-security apps
// Jailbreak detection (basic)
func isJailbroken() -> Bool {
    #if targetEnvironment(simulator)
    return false
    #else
    let paths = [
        "/Applications/Cydia.app",
        "/Library/MobileSubstrate/MobileSubstrate.dylib",
        "/bin/bash",
        "/usr/sbin/sshd",
        "/etc/apt"
    ]

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

    // Check if app can write outside sandbox
    let testPath = "/private/jailbreak_test.txt"
    do {
        try "test".write(toFile: testPath, atomically: true, encoding: .utf8)
        try FileManager.default.removeItem(atPath: testPath)
        return true
    } catch {
        return false
    }
    #endif
}

Secure Coding Practices

  • Validate all input data
  • Use parameterized queries for databases
  • Sanitize data before displaying in web views
  • Avoid using NSLog for sensitive data in production
  • Implement proper error handling without exposing details

5. Protecting Sensitive UI

Prevent Screenshots of Sensitive Data

// Blur content when app goes to background
class AppDelegate: UIResponder, UIApplicationDelegate {
    var blurView: UIVisualEffectView?

    func applicationWillResignActive(_ application: UIApplication) {
        let blur = UIBlurEffect(style: .light)
        blurView = UIVisualEffectView(effect: blur)
        blurView?.frame = UIScreen.main.bounds
        UIApplication.shared.windows.first?.addSubview(blurView!)
    }

    func applicationDidBecomeActive(_ application: UIApplication) {
        blurView?.removeFromSuperview()
        blurView = nil
    }
}

Secure Text Fields

// Disable autocomplete for sensitive fields
textField.textContentType = .oneTimeCode // Prevents keyboard caching
textField.isSecureTextEntry = true
textField.autocorrectionType = .no
textField.autocapitalizationType = .none
textField.spellCheckingType = .no

6. Runtime Protection

Detect Debugging

func isDebuggerAttached() -> Bool {
    var info = kinfo_proc()
    var mib: [Int32] = [CTL_KERN, KERN_PROC, KERN_PROC_PID, getpid()]
    var size = MemoryLayout<kinfo_proc>.stride

    let result = sysctl(&mib, UInt32(mib.count), &info, &size, nil, 0)

    if result != 0 {
        return false
    }

    return (info.kp_proc.p_flag & P_TRACED) != 0
}

Secure Memory Handling

// Clear sensitive data from memory
extension Data {
    mutating func secureZero() {
        guard !isEmpty else { return }
        withUnsafeMutableBytes { bytes in
            memset_s(bytes.baseAddress!, bytes.count, 0, bytes.count)
        }
    }
}

// Usage
var sensitiveData = "password".data(using: .utf8)!
// ... use the data
sensitiveData.secureZero() // Clear from memory

7. Third-Party Library Security

  • Audit all dependencies for known vulnerabilities
  • Keep dependencies updated
  • Use Swift Package Manager or CocoaPods with locked versions
  • Review library permissions and network calls
  • Avoid libraries that are no longer maintained

8. OWASP Mobile Top 10

Protect against the most common mobile vulnerabilities:

Vulnerability Protection
Improper Platform Usage Use iOS security features (Keychain, ATS, biometrics)
Insecure Data Storage Encrypt sensitive data, use Keychain
Insecure Communication HTTPS + certificate pinning
Insecure Authentication OAuth 2.0, biometrics, secure token storage
Insufficient Cryptography Use Apple's CryptoKit, avoid custom crypto
Insecure Authorization Server-side validation, principle of least privilege
Client Code Quality Input validation, secure coding practices
Code Tampering Jailbreak detection, code signing validation
Reverse Engineering Obfuscation, no hardcoded secrets
Extraneous Functionality Remove debug code, disable logging in production

Security Checklist

Pre-Release Security Audit

  • All sensitive data stored in Keychain
  • No hardcoded API keys or secrets
  • HTTPS enforced with certificate pinning
  • Biometric authentication implemented correctly
  • Debug logging disabled in release builds
  • Input validation on all user data
  • Sensitive screens protected from screenshots
  • All third-party libraries audited
  • Jailbreak detection (if required)
  • Privacy manifest (PrivacyInfo.xcprivacy) completed

Need a Security Audit for Your iOS App?

I can review your app's security implementation and identify vulnerabilities before they become problems.

Request Security Review

Related Articles