Security, privacy and data protection
Store secrets in the Keychain, enforce App Transport Security, declare data collection with privacy manifests, and use entitlements and file protection correctly.
Secrets belong in the Keychain
import Security
enum TokenStore {
private static let service = "com.example.app.tokens"
static func save(_ token: String, account: String) throws {
let data = Data(token.utf8)
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: account,
kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly,
]
SecItemDelete(query as CFDictionary)
var attributes = query
attributes[kSecValueData as String] = data
let status = SecItemAdd(attributes as CFDictionary, nil)
guard status == errSecSuccess else { throw KeychainError.status(status) }
}
static func read(account: String) -> String? {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: account,
kSecReturnData as String: true,
kSecMatchLimit as String: kSecMatchLimitOne,
]
var item: CFTypeRef?
guard SecItemCopyMatching(query as CFDictionary, &item) == errSecSuccess,
let data = item as? Data else { return nil }
return String(data: data, encoding: .utf8)
}
}kSecAttrAccessibleAfterFirstUnlockThisDeviceOnlykeeps the token off backups and prevents it migrating to a new device.- Never store tokens in
UserDefaults: it is a plain plist inside the app container and readable from a backup. - Delete before adding —
SecItemAddfails witherrSecDuplicateItemif a record already exists. - Keychain items survive app deletion on some platforms; provide an explicit sign-out that deletes them.
Transport security and privacy manifests
<!-- Info.plist: do not weaken ATS globally -->
<key>NSAppTransportSecurity</key>
<dict>
<key>NSExceptionDomains</key>
<dict>
<key>legacy.internal.example</key>
<dict>
<key>NSExceptionMinimumTLSVersion</key>
<string>TLSv1.2</string>
<key>NSIncludesSubdomains</key>
<true/>
</dict>
</dict>
</dict>
<!-- PrivacyInfo.xcprivacy: declare why you touch a required-reason API -->
<key>NSPrivacyAccessedAPITypes</key>
<array>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array><string>CA92.1</string></array>
</dict>
</array>| Data | Where it should live | Why |
|---|---|---|
| Auth tokens | Keychain | Encrypted, access-controlled, excluded from backups |
| PII cache | File with .completeFileProtection | Prevents reading while the device is locked |
| Analytics | Privacy manifest declaration | Required for App Store submission |
| Feature flags | UserDefaults | Non-sensitive and cheap |
Permissions, entitlements and review
// Ask only when the feature is used, never on launch
import AVFoundation
func requestCamera() async -> Bool {
switch AVCaptureDevice.authorizationStatus(for: .video) {
case .authorized: return true
case .notDetermined: return await AVCaptureDevice.requestAccess(for: .video)
default: return false
}
}⚠️
Requesting a permission you never use, or without an accurate purpose string in Info.plist, is one of the most common App Store rejections. Ask at the moment of use and handle the denied path with a link to Settings.
FAQ
Is the Keychain enough to protect a secret?
It protects at rest with hardware-backed keys, but anything in memory can be read on a jailbroken device. For high-value secrets use server-side tokens with short lifetimes and refresh rather than long-lived keys on the device.
What is a privacy manifest and do I need one?
A
PrivacyInfo.xcprivacy file declaring the data you collect and the required-reason APIs you call. Third-party SDKs must ship one too, and Apple rejects builds that reference APIs without a declared reason.Related
Release engineering: TestFlight, CI and App Store Connect Localization, accessibility and system integration
Last refreshed 2026-09-18.