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)
    }
}
  • kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly keeps 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 — SecItemAdd fails with errSecDuplicateItem if 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>
DataWhere it should liveWhy
Auth tokensKeychainEncrypted, access-controlled, excluded from backups
PII cacheFile with .completeFileProtectionPrevents reading while the device is locked
AnalyticsPrivacy manifest declarationRequired for App Store submission
Feature flagsUserDefaultsNon-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.

Release engineering: TestFlight, CI and App Store Connect Localization, accessibility and system integration

Last refreshed 2026-09-18.