Skip to main content

Implementing Security Best Practices

1. Using Keychain for Sensitive Data

Property Description Notes
SecKeychainItemCreateFromContent Creates a keychain item from content Used to store sensitive data securely
SecKeychainItemCopyContent Retrieves content from a keychain item Decrypts and returns stored data
SecKeychainItemDelete Deletes a keychain item Removes sensitive data from storage

Example: Storing and Retrieving Data in Keychain

import Security

let query: [String: Any] = [
    kSecClass as String: kSecClassGenericPassword,
    kSecAttrAccount as String: "user@example.com",
    kSecAttrService as String: "MyApp",
    kSecValueData as String: "secretKey".data(using: .utf8)!
]

// Store data
let status = SecItemAdd(query as CFDictionary, nil)

if status == errSecSuccess {
    print("Data stored successfully")
}

// Retrieve data
let retrieveQuery: [String: Any] = [
    kSecClass as String: kSecClassGenericPassword,
    kSecAttrAccount as String: "user@example.com",
    kSecAttrService as String: "MyApp",
    kSecReturnData as String: true,
    kSecMatchLimit as String: kSecMatchLimitOne
]

var retrievedData: AnyObject?
let retrieveStatus = SecItemCopyMatching(retrieveQuery as CFDictionary, &retrievedData)

if retrieveStatus == errSecSuccess {
    if let data = retrievedData as? Data,
       let string = String(data: data, encoding: .utf8) {
        print("Retrieved data: \(string)")
    }
}
Note: Keychain is the most secure way to store sensitive data in iOS apps. It's encrypted and protected by the device's passcode.
Warning: Never store sensitive data in UserDefaults or plain text files. Always use Keychain for such data.

2. Implementing Certificate Pinning

Certificate pinning is a security technique that ensures your app only accepts connections from specific certificates or public keys, mitigating man-in-the-middle (MITM) attacks.

Property Description Notes
ServerTrustEvaluator Defines how to evaluate server trust during TLS handshake Used in conjunction with Alamofire or custom URLSession configurations
PublicKeyPin Represents a pinned public key for validation Can be derived from certificate or directly from key data
CertificatePin Represents a pinned certificate for validation Used to validate the full certificate chain

Example: Using Certificate Pinning with URLSession

import Foundation

class PinningDelegate: NSObject, URLSessionDelegate {
    func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
        guard challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust else {
            completionHandler(.performDefaultHandling, nil)
            return
        }

        let serverTrust = challenge.protectionSpace.serverTrust!
        let pinnedCertificates = [/* Array of pinned certificates */]
        
        if validateCertificate(serverTrust, against: pinnedCertificates) {
            completionHandler(.useCredential, URLCredential(trust: serverTrust))
        } else {
            completionHandler(.rejectProtectionSpace, nil)
        }
    }

    private func validateCertificate(_ serverTrust: SecTrust, against pinnedCertificates: [Data]) -> Bool {
        // Implementation logic here
        return true
    }
}

let session = URLSession(configuration: .default, delegate: PinningDelegate(), delegateQueue: nil)
Note: Certificate pinning should be used in conjunction with other security measures like secure network configuration and proper error handling.
Warning: Incorrectly implemented certificate pinning can lead to app breakage if the server's certificate changes or becomes invalid.

3. Using App Transport Security

Property Value Description
NSAppTransportSecurity Dictionary<String, Any> Root key for App Transport Security configuration in Info.plist

Example: Basic ATS Configuration

<key>NSAppTransportSecurity</key>
<dict>
    <key>NSAllowsArbitraryLoads</key>
    <false/>
</dict>
Note: ATS is enabled by default in iOS 9+ and macOS 10.11+. Disabling it entirely is discouraged.
ATS Setting Default Value Description
NSAllowsArbitraryLoads false Allows loading of HTTP content. Setting to true disables ATS entirely.
NSAllowsArbitraryLoadsInWebContent false Allows arbitrary loads in web views.
NSAllowsLocalNetworking false Allows local networking (e.g., localhost) to bypass ATS.
NSExceptionDomains nil Dictionary of domains to allow with exceptions.

Example: Exception for Specific Domain

<key>NSAppTransportSecurity</key>
<dict>
    <key>NSExceptionDomains</key>
    <dict>
        <key>example.com</key>
        <dict>
            <key>NSExceptionAllowsInsecureHTTPLoads</key>
            <true/>
            <key>NSExceptionRequiresForwardSecrecy</key>
            <false/>
        </dict>
    </dict>
</dict>
Warning: Setting NSAllowsArbitraryLoads to true disables all ATS protections and should be avoided in production apps.

ATS Best Practices

  • Enable ATS by default for all apps.
  • Use NSExceptionDomains to allow specific domains only when necessary.
  • Avoid setting NSAllowsArbitraryLoads to true.
  • Ensure all domains used in your app support HTTPS.

4. Encrypting Data at Rest

Property Description Notes
AES-256 Encryption Advanced Encryption Standard with 256-bit key length Recommended for sensitive data storage
Keychain Services Secure storage for small amounts of data (e.g., passwords) Automatically encrypted by the OS
Data Protection API Enables automatic encryption of files in the app sandbox Uses device-specific keys

Example: Using Data Protection API

import Foundation

let fileURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!.appendingPathComponent("secureData.plist")

let data = "Sensitive information".data(using: .utf8)!

// Set protection attributes to encrypt at rest
let attributes = [FileAttributeKey.protectionKey: FileProtectionType.complete]
try FileManager.default.setAttributes(attributes, ofItemAtPath: fileURL.path)
Note: Files protected with FileProtectionType.complete are encrypted using the device's hardware keychain and can only be accessed when the device is unlocked.

Example: Storing Secure Data in Keychain

import Security

let query: [String: Any] = [
    kSecClass as String: kSecClassGenericPassword,
    kSecAttrAccount as String: "userToken",
    kSecAttrService as String: "MyApp",
    kSecValueData as String: "encryptedToken".data(using: .utf8)!
]

// Save to keychain
let status = SecItemAdd(query as CFDictionary, nil)

if status == errSecSuccess {
    print("Token stored securely")
} else {
    print("Failed to store token")
}
Warning: Never store sensitive data in plain text or unencrypted files. Always use secure storage mechanisms like Keychain or Data Protection API.

5. Using Secure Coding Practices

Practice Description Implementation
Input Validation Validate all user inputs to prevent injection attacks
func validateEmail(_ email: String) -> Bool {
    let emailRegEx = "^[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,6}$"
    let emailPredicate = NSPredicate(format: "SELF MATCHES %@", emailRegEx)
    return emailPredicate.evaluate(with: email)
}
Secure Data Storage Use Keychain for sensitive data, not UserDefaults
let query: [String: Any] = [
    kSecClass as String: kSecClassGenericPassword,
    kSecAttrAccount as String: "userKey",
    kSecValueData as String: data
]
SecItemAdd(query as CFDictionary, nil)
Encryption Encrypt sensitive data at rest and in transit
let data = "Sensitive Info".data(using: .utf8)!
let encryptedData = try! AES256.encrypt(data, key: key)
Error Handling Avoid leaking sensitive information in error messages
do {
    try riskyOperation()
} catch {
    // Log generic error, don't expose details
    logger.log("An error occurred")
}

Example: Secure API Call

func secureAPICall() {
    let url = URL(string: "https://api.example.com/data")!
    var request = URLRequest(url: url)
    request.httpMethod = "POST"
    request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
    
    // Validate response before processing
    URLSession.shared.dataTask(with: request) { data, response, error in
        guard let httpResponse = response as? HTTPURLResponse,
              (200...299).contains(httpResponse.statusCode) else {
            print("API Error")
            return
        }
        // Process data securely
    }.resume()
}
Note: Always use HTTPS and validate server certificates to prevent man-in-the-middle attacks.
Warning: Never store passwords or tokens in plain text. Always use secure storage mechanisms like Keychain Services.

6. Preventing Screen Capture

Method Description Notes
UIScreen.main.isCaptured Checks if the screen is currently being captured DEPRECATED in iOS 17+
UIWindowScene.screenCaptureDisabled Disables screen capture for the scene Available from iOS 17+

Example: Disable Screen Capture

import UIKit

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        
        if #available(iOS 17.0, *) {
            // Disable screen capture for this scene
            if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene {
                windowScene.screenCaptureDisabled = true
            }
        } else {
            // Fallback for older versions
            UIScreen.main.isCaptured = true
        }
    }
}
Note: Screen capture prevention is not 100% reliable and can be bypassed by determined users.

7. Implementing Biometric Authentication

Property Description Notes
LAContext Class for evaluating biometric authentication Used to check if biometrics are available and evaluate authentication
evaluatePolicy Method to evaluate biometric authentication Requires a policy type and localized reason string
biometryType Property indicating type of biometric available Returns .faceID, .touchID, or .none

Example: Evaluating Face ID Authentication

import LocalAuthentication

let context = LAContext()
var error: NSError?

if context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) {
    let reason = "Authenticate to access the app"
    
    context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: reason) { success in
        DispatchQueue.main.async {
            if success {
                print("Authentication successful")
            } else {
                print("Authentication failed")
            }
        }
    }
} else {
    print("Biometric authentication not available: \(error?.localizedDescription ?? "Unknown error")")
}
Note: Ensure you have the appropriate permissions in your app's Info.plist for biometric usage.
Warning: Always handle authentication failures gracefully and provide alternative access methods if needed.
Policy Type Description Support
.deviceOwnerAuthenticationWithBiometrics Face ID or Touch ID authentication NEW in iOS 11+
.deviceOwnerAuthentication Passcode or biometric authentication Available in iOS 8+

8. Using Secure Random Generators

Method Description Security Level Notes
SecRandomCopyBytes Generates cryptographically secure random bytes using the system's secure random number generator. NEW Available in iOS 4.0+ and macOS 10.6+. Recommended for generating secure keys or tokens.
CryptoKit.Random Swift wrapper for secure random generation, part of the CryptoKit framework introduced in iOS 13. NEW Provides high-level access to secure randomness with type safety.
UInt.random(in:) Generates a random integer within a specified range using Swift’s built-in random number generator. DEPRECATED Not suitable for cryptographic use. Use SecRandomCopyBytes or CryptoKit instead.

Example: Generating a Secure Random Number

import Security

func generateSecureRandomNumber() -> UInt32 {
    var random: UInt32 = 0
    let status = SecRandomCopyBytes(kSecRandomDefault, MemoryLayout<UInt32>.size, &random)
    
    if status != errSecSuccess {
        fatalError("Failed to generate secure random number")
    }
    
    return random
}

Example: Using CryptoKit for Secure Randomness

import CryptoKit

let randomBytes = try! ChaChaPoly.Nonce()
let randomInt = Int.random(in: 1...1000)
Note: When generating random numbers for cryptographic purposes (e.g., session tokens, keys), always use SecRandomCopyBytes or CryptoKit. Avoid using Swift’s built-in random functions like Int.random(in:) as they are not cryptographically secure.

9. Validating User Input

Validation Type Description Implementation Notes
Input Length Ensures input does not exceed maximum allowed length if userInput.count <= 255 { ... } Prevents buffer overflow and database size issues
Character Set Validates that input contains only allowed characters
let allowedChars = CharacterSet.alphanumerics
if userInput.rangeOfCharacter(from: allowedChars.inverted) == nil { ... }
Use CharacterSet for strict validation
Regex Pattern Matches input against a defined pattern (e.g., email format)
let emailRegex = "^[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,}$"
let emailPredicate = NSPredicate(format: "SELF MATCHES %@", emailRegex)
if emailPredicate.evaluate(with: userInput) { ... }
Use NSPredicate for complex patterns
URL Encoding Ensures URLs are properly encoded to prevent injection
let encodedURL = userInput.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)
if let safeURL = URL(string: encodedURL ?? "") { ... }
Use addingPercentEncoding for safe URL handling

Example: Validating Email Input

func isValidEmail(_ email: String) -> Bool {
    let emailRegex = "^[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,}$"
    let emailPredicate = NSPredicate(format: "SELF MATCHES %@", emailRegex)
    return emailPredicate.evaluate(with: email)
}

let userInput = "user@example.com"
if isValidEmail(userInput) {
    print("Valid email")
} else {
    print("Invalid email")
}
Note: Always validate input on both client and server side to ensure security.
Warning: Never rely solely on client-side validation. Server-side validation is critical for security.

10. Implementing Jailbreak Detection

Example: Basic Jailbreak Detection Check

import UIKit

func isJailbroken() -> Bool {
    // Check for common jailbreak indicators
    let pathsToCheck = [
        "/Applications/BlackKey.app",
        "/Applications/FakeCarrier.app",
        "/Library/MobileSubstrate/DynamicLibraries/LiveClock.plist",
        "/System/Library/LaunchDaemons/com.ikey.bbot.plist",
        "/etc/ssh/sshd_config"
    ]
    
    for path in pathsToCheck {
        if FileManager.default.fileExists(atPath: path) {
            return true
        }
    }
    
    // Check for Cydia URL scheme
    if UIApplication.shared.canOpenURL(URL(string: "cydia://package/com.example.app")!) {
        return true
    }
    
    // Check for debug symbols in executable
    let image = UnsafeMutablePointer<UInt8>.allocate(capacity: 1)
    defer { image.deallocate() }
    
    if image.pointee == 0x7F {
        // This is a basic check, more complex checks needed for production
        return false
    }
    
    return false
}

// Usage example
if isJailbroken() {
    print("Device is jailbroken")
} else {
    print("Device is not jailbroken")
}
Method Description Notes
fileExists(atPath:) Checks if a file or directory exists at the specified path Common jailbreak files are checked here
canOpenURL(_:) Checks if the app can open a URL Cydia URLs are often used to detect jailbreaks
Note: Jailbreak detection is not foolproof and can be bypassed by determined attackers. It should be used as part of a broader security strategy.
Warning: Some jailbreak detection methods may cause false positives on non-jailbroken devices or be detected by security tools.

Key Points

  • Jailbreak detection should be performed early in the app lifecycle
  • Multiple checks should be used to increase accuracy
  • Detection methods can be bypassed by advanced attackers
  • Implementing jailbreak detection is a security best practice but not a guarantee of security

Was this article helpful?