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
NSExceptionDomainsto allow specific domains only when necessary. - Avoid setting
NSAllowsArbitraryLoadstotrue. - 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 | |
| Secure Data Storage | Use Keychain for sensitive data, not UserDefaults | |
| Encryption | Encrypt sensitive data at rest and in transit | |
| Error Handling | Avoid leaking sensitive information in error messages | |
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 | |
Use CharacterSet for strict validation |
| Regex Pattern | Matches input against a defined pattern (e.g., email format) | |
Use NSPredicate for complex patterns |
| URL Encoding | Ensures URLs are properly encoded to prevent injection | |
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.