iOS Security Best Practices 2025: Complete Developer Guide
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
NSLogfor 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