ios-security

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

iOS Security

iOS 安全

Guidance for handling sensitive data, authenticating users, encrypting correctly, and following Apple's security best practices on iOS.
本文档指导你在iOS上处理敏感数据、验证用户身份、正确加密以及遵循Apple的安全最佳实践。

Keychain Services

Keychain Services

The Keychain is the ONLY correct place to store sensitive data. Never store passwords, tokens, API keys, or secrets in UserDefaults, files, or Core Data.
Keychain是存储敏感数据的唯一正确位置。切勿将密码、令牌、API密钥或机密信息存储在UserDefaults、文件或Core Data中。

Storing Credentials

存储凭据

swift
func saveToKeychain(account: String, data: Data, service: String) throws {
    let query: [String: Any] = [
        kSecClass as String: kSecClassGenericPassword,
        kSecAttrAccount as String: account,
        kSecAttrService as String: service,
        kSecValueData as String: data,
        kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
    ]

    let status = SecItemAdd(query as CFDictionary, nil)

    if status == errSecDuplicateItem {
        let updateQuery: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrAccount as String: account,
            kSecAttrService as String: service
        ]
        let updates: [String: Any] = [kSecValueData as String: data]
        let updateStatus = SecItemUpdate(updateQuery as CFDictionary, updates as CFDictionary)
        guard updateStatus == errSecSuccess else {
            throw KeychainError.updateFailed(updateStatus)
        }
    } else if status != errSecSuccess {
        throw KeychainError.saveFailed(status)
    }
}
swift
func saveToKeychain(account: String, data: Data, service: String) throws {
    let query: [String: Any] = [
        kSecClass as String: kSecClassGenericPassword,
        kSecAttrAccount as String: account,
        kSecAttrService as String: service,
        kSecValueData as String: data,
        kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
    ]

    let status = SecItemAdd(query as CFDictionary, nil)

    if status == errSecDuplicateItem {
        let updateQuery: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrAccount as String: account,
            kSecAttrService as String: service
        ]
        let updates: [String: Any] = [kSecValueData as String: data]
        let updateStatus = SecItemUpdate(updateQuery as CFDictionary, updates as CFDictionary)
        guard updateStatus == errSecSuccess else {
            throw KeychainError.updateFailed(updateStatus)
        }
    } else if status != errSecSuccess {
        throw KeychainError.saveFailed(status)
    }
}

Reading Credentials

读取凭据

swift
func readFromKeychain(account: String, service: String) throws -> Data {
    let query: [String: Any] = [
        kSecClass as String: kSecClassGenericPassword,
        kSecAttrAccount as String: account,
        kSecAttrService as String: service,
        kSecReturnData as String: true,
        kSecMatchLimit as String: kSecMatchLimitOne
    ]

    var result: AnyObject?
    let status = SecItemCopyMatching(query as CFDictionary, &result)

    guard status == errSecSuccess, let data = result as? Data else {
        throw KeychainError.readFailed(status)
    }
    return data
}
swift
func readFromKeychain(account: String, service: String) throws -> Data {
    let query: [String: Any] = [
        kSecClass as String: kSecClassGenericPassword,
        kSecAttrAccount as String: account,
        kSecAttrService as String: service,
        kSecReturnData as String: true,
        kSecMatchLimit as String: kSecMatchLimitOne
    ]

    var result: AnyObject?
    let status = SecItemCopyMatching(query as CFDictionary, &result)

    guard status == errSecSuccess, let data = result as? Data else {
        throw KeychainError.readFailed(status)
    }
    return data
}

Deleting Credentials

删除凭据

swift
func deleteFromKeychain(account: String, service: String) throws {
    let query: [String: Any] = [
        kSecClass as String: kSecClassGenericPassword,
        kSecAttrAccount as String: account,
        kSecAttrService as String: service
    ]

    let status = SecItemDelete(query as CFDictionary)
    guard status == errSecSuccess || status == errSecItemNotFound else {
        throw KeychainError.deleteFailed(status)
    }
}
swift
func deleteFromKeychain(account: String, service: String) throws {
    let query: [String: Any] = [
        kSecClass as String: kSecClassGenericPassword,
        kSecAttrAccount as String: account,
        kSecAttrService as String: service
    ]

    let status = SecItemDelete(query as CFDictionary)
    guard status == errSecSuccess || status == errSecItemNotFound else {
        throw KeychainError.deleteFailed(status)
    }
}

kSecAttrAccessible Values

kSecAttrAccessible 取值

ValueWhen AvailableDevice-OnlyUse For
kSecAttrAccessibleWhenUnlocked
Device unlockedNoGeneral credentials
kSecAttrAccessibleWhenUnlockedThisDeviceOnly
Device unlockedYesSensitive credentials
kSecAttrAccessibleAfterFirstUnlock
After first unlockNoBackground-accessible tokens
kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
After first unlockYesBackground tokens, no backup
kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly
Passcode set + unlockedYesHighest security
Rules:
  • Use
    ThisDeviceOnly
    variants for sensitive data. Prevents backup/restore to other devices.
  • Use
    AfterFirstUnlock
    for tokens needed by background operations.
  • Use
    WhenPasscodeSetThisDeviceOnly
    for most sensitive data. Item is deleted if passcode is removed.
  • NEVER use
    kSecAttrAccessibleAlways
    (deprecated and insecure).
取值可用时机仅限本机适用场景
kSecAttrAccessibleWhenUnlocked
设备解锁后通用凭据
kSecAttrAccessibleWhenUnlockedThisDeviceOnly
设备解锁后敏感凭据
kSecAttrAccessibleAfterFirstUnlock
首次解锁后后台可访问的令牌
kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
首次解锁后后台令牌,不备份
kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly
设置密码且设备解锁后最高安全级别
规则:
  • 敏感数据使用
    ThisDeviceOnly
    系列取值,防止备份/恢复到其他设备。
  • 后台操作所需的令牌使用
    AfterFirstUnlock
  • 最高敏感数据使用
    WhenPasscodeSetThisDeviceOnly
    ,移除密码时该项目会被删除。
  • 绝不要使用
    kSecAttrAccessibleAlways
    (已弃用且不安全)。

Keychain Access Groups

Keychain 访问组

Share keychain items across apps from the same team:
swift
let query: [String: Any] = [
    kSecClass as String: kSecClassGenericPassword,
    kSecAttrAccount as String: "shared-token",
    kSecAttrAccessGroup as String: "TEAMID.com.company.shared"
]
同一开发团队的应用之间共享Keychain项目:
swift
let query: [String: Any] = [
    kSecClass as String: kSecClassGenericPassword,
    kSecAttrAccount as String: "shared-token",
    kSecAttrAccessGroup as String: "TEAMID.com.company.shared"
]

Data Protection

数据保护

iOS encrypts files based on their protection class:
ClassWhen AvailableUse For
.complete
Only when unlockedSensitive user data
.completeUnlessOpen
Open handles survive lockActive downloads, recordings
.completeUntilFirstUserAuthentication
After first unlock (default)Most app data
.none
AlwaysNon-sensitive, system-needed data
swift
// Set file protection
try data.write(to: url, options: .completeFileProtection)

// Check protection level
let attributes = try FileManager.default.attributesOfItem(atPath: path)
let protection = attributes[.protectionKey] as? FileProtectionType
Use
.complete
for any file containing user-sensitive data. The default
.completeUntilFirstUserAuthentication
is acceptable for general app data.
iOS会根据文件的保护类别对其进行加密:
类别可用时机适用场景
.complete
仅设备解锁后敏感用户数据
.completeUnlessOpen
打开的句柄在设备锁定后仍可用活跃下载、录制内容
.completeUntilFirstUserAuthentication
首次解锁后(默认)大多数应用数据
.none
始终可用非敏感、系统所需数据
swift
// 设置文件保护
try data.write(to: url, options: .completeFileProtection)

// 检查保护级别
let attributes = try FileManager.default.attributesOfItem(atPath: path)
let protection = attributes[.protectionKey] as? FileProtectionType
包含用户敏感数据的文件请使用
.complete
。默认的
.completeUntilFirstUserAuthentication
适用于普通应用数据。

CryptoKit

CryptoKit

Use CryptoKit for all cryptographic operations. Do not use CommonCrypto or the raw Security framework for new code.
所有加密操作请使用CryptoKit。新代码不要使用CommonCrypto或原始Security框架。

Symmetric Encryption (AES-GCM)

对称加密(AES-GCM)

swift
import CryptoKit

let key = SymmetricKey(size: .bits256)

func encrypt(_ data: Data, using key: SymmetricKey) throws -> Data {
    let sealed = try AES.GCM.seal(data, using: key)
    guard let combined = sealed.combined else {
        throw CryptoError.sealFailed
    }
    return combined
}

func decrypt(_ data: Data, using key: SymmetricKey) throws -> Data {
    let box = try AES.GCM.SealedBox(combined: data)
    return try AES.GCM.open(box, using: key)
}
swift
import CryptoKit

let key = SymmetricKey(size: .bits256)

func encrypt(_ data: Data, using key: SymmetricKey) throws -> Data {
    let sealed = try AES.GCM.seal(data, using: key)
    guard let combined = sealed.combined else {
        throw CryptoError.sealFailed
    }
    return combined
}

func decrypt(_ data: Data, using key: SymmetricKey) throws -> Data {
    let box = try AES.GCM.SealedBox(combined: data)
    return try AES.GCM.open(box, using: key)
}

Hashing

哈希

swift
let hash = SHA256.hash(data: data)
let hashString = hash.compactMap { String(format: "%02x", $0) }.joined()

// Also available: SHA384, SHA512
swift
let hash = SHA256.hash(data: data)
let hashString = hash.compactMap { String(format: "%02x", $0) }.joined()

// 还支持:SHA384、SHA512

HMAC (Message Authentication)

HMAC(消息认证)

swift
let key = SymmetricKey(size: .bits256)

// Sign
let signature = HMAC<SHA256>.authenticationCode(for: data, using: key)

// Verify
let isValid = HMAC<SHA256>.isValidAuthenticationCode(signature, authenticating: data, using: key)
For digital signatures (P256/ECDSA), key agreement (Curve25519), ChaChaPoly, and HKDF key derivation, see
references/cryptokit-advanced.md
.
swift
let key = SymmetricKey(size: .bits256)

// 签名
let signature = HMAC<SHA256>.authenticationCode(for: data, using: key)

// 验证
let isValid = HMAC<SHA256>.isValidAuthenticationCode(signature, authenticating: data, using: key)
有关数字签名(P256/ECDSA)、密钥协商(Curve25519)、ChaChaPoly以及HKDF密钥派生的内容,请参阅
references/cryptokit-advanced.md

Secure Enclave

Secure Enclave

For the highest security, store keys in the Secure Enclave. Keys never leave the hardware. Only P256 is supported.
swift
// Check availability first
guard SecureEnclave.isAvailable else {
    // Fall back to software-based keys
    return
}

// Create access control
let accessControl = SecAccessControlCreateWithFlags(
    nil,
    kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly,
    [.privateKeyUsage, .biometryCurrentSet],
    nil
)!

// Create a Secure Enclave key with access control
let privateKey = try SecureEnclave.P256.Signing.PrivateKey(
    accessControl: accessControl
)

// Sign data (may trigger biometric prompt)
let signature = try privateKey.signature(for: data)

// Verify with the public key (no hardware access needed)
let isValid = privateKey.publicKey.isValidSignature(signature, for: data)

// Persist the key for later use
let keyData = privateKey.dataRepresentation
// Store keyData in Keychain, then restore with:
let restored = try SecureEnclave.P256.Signing.PrivateKey(
    dataRepresentation: keyData
)
最高安全级别的密钥请存储在Secure Enclave中,密钥永远不会离开硬件。仅支持P256密钥。
swift
// 先检查可用性
guard SecureEnclave.isAvailable else {
    // 回退到基于软件的密钥
    return
}

// 创建访问控制
let accessControl = SecAccessControlCreateWithFlags(
    nil,
    kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly,
    [.privateKeyUsage, .biometryCurrentSet],
    nil
)!

// 创建带访问控制的Secure Enclave密钥
let privateKey = try SecureEnclave.P256.Signing.PrivateKey(
    accessControl: accessControl
)

// 签名数据(可能触发生物识别验证提示)
let signature = try privateKey.signature(for: data)

// 用公钥验证(无需访问硬件)
let isValid = privateKey.publicKey.isValidSignature(signature, for: data)

// 持久化密钥以便后续使用
let keyData = privateKey.dataRepresentation
// 将keyData存储在Keychain中,之后可通过以下方式恢复:
let restored = try SecureEnclave.P256.Signing.PrivateKey(
    dataRepresentation: keyData
)

Biometric Authentication

生物识别认证

LocalAuthentication (Face ID / Touch ID)

LocalAuthentication(Face ID / Touch ID)

swift
import LocalAuthentication

func authenticateWithBiometrics() async throws -> Bool {
    let context = LAContext()
    var error: NSError?

    guard context.canEvaluatePolicy(
        .deviceOwnerAuthenticationWithBiometrics, error: &error
    ) else {
        // Biometrics not available -- fall back to passcode
        if context.canEvaluatePolicy(.deviceOwnerAuthentication, error: &error) {
            return try await context.evaluatePolicy(
                .deviceOwnerAuthentication,
                localizedReason: "Authenticate to access your account"
            )
        }
        throw AuthError.biometricsUnavailable
    }

    return try await context.evaluatePolicy(
        .deviceOwnerAuthenticationWithBiometrics,
        localizedReason: "Authenticate to access your account"
    )
}
swift
import LocalAuthentication

func authenticateWithBiometrics() async throws -> Bool {
    let context = LAContext()
    var error: NSError?

    guard context.canEvaluatePolicy(
        .deviceOwnerAuthenticationWithBiometrics, error: &error
    ) else {
        // 生物识别不可用 -- 回退到密码验证
        if context.canEvaluatePolicy(.deviceOwnerAuthentication, error: &error) {
            return try await context.evaluatePolicy(
                .deviceOwnerAuthentication,
                localizedReason: "验证身份以访问你的账户"
            )
        }
        throw AuthError.biometricsUnavailable
    }

    return try await context.evaluatePolicy(
        .deviceOwnerAuthenticationWithBiometrics,
        localizedReason: "验证身份以访问你的账户"
    )
}

Info.plist Requirement

Info.plist 要求

You MUST include
NSFaceIDUsageDescription
in Info.plist:
xml
<key>NSFaceIDUsageDescription</key>
<string>Authenticate to access your secure data</string>
Missing this key causes a crash on Face ID devices.
你必须在Info.plist中添加
NSFaceIDUsageDescription
xml
<key>NSFaceIDUsageDescription</key>
<string>验证身份以访问你的安全数据</string>
缺少该键会导致应用在支持Face ID的设备上崩溃。

LAContext Configuration

LAContext 配置

swift
let context = LAContext()

// Allow fallback to device passcode
context.localizedFallbackTitle = "Use Passcode"

// Reuse authentication for 30 seconds
context.touchIDAuthenticationAllowableReuseDuration = 30

// Detect biometry enrollment changes by comparing domain state
let currentState = context.evaluatedPolicyDomainState
// Compare currentState to a previously stored value
swift
let context = LAContext()

// 允许回退到设备密码
context.localizedFallbackTitle = "使用密码"

// 30秒内可复用验证结果
context.touchIDAuthenticationAllowableReuseDuration = 30

// 通过对比域状态检测生物识别注册变更
let currentState = context.evaluatedPolicyDomainState
// 将currentState与之前存储的值进行对比

Biometric + Keychain

生物识别 + Keychain

Protect keychain items with biometric access:
swift
let access = SecAccessControlCreateWithFlags(
    nil,
    kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly,
    .biometryCurrentSet,
    nil
)!

let query: [String: Any] = [
    kSecClass as String: kSecClassGenericPassword,
    kSecAttrAccount as String: "auth-token",
    kSecValueData as String: tokenData,
    kSecAttrAccessControl as String: access,
    kSecUseAuthenticationContext as String: LAContext()
]
SecAccessControl flags:
  • .biometryCurrentSet
    -- Requires biometry, invalidated if enrollment changes. Most secure.
  • .biometryAny
    -- Requires biometry, survives enrollment changes.
  • .userPresence
    -- Biometry or passcode. Most flexible.
用生物识别保护Keychain项目:
swift
let access = SecAccessControlCreateWithFlags(
    nil,
    kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly,
    .biometryCurrentSet,
    nil
)!

let query: [String: Any] = [
    kSecClass as String: kSecClassGenericPassword,
    kSecAttrAccount as String: "auth-token",
    kSecValueData as String: tokenData,
    kSecAttrAccessControl as String: access,
    kSecUseAuthenticationContext as String: LAContext()
]
SecAccessControl 标志:
  • .biometryCurrentSet
    -- 需要生物识别,注册变更后失效,安全性最高。
  • .biometryAny
    -- 需要生物识别,注册变更后仍然有效。
  • .userPresence
    -- 支持生物识别或密码,灵活性最高。

App Transport Security (ATS)

App Transport Security (ATS)

ATS enforces HTTPS by default. Do NOT disable it.
ATS默认强制使用HTTPS,请勿禁用它。

What ATS Requires

ATS 的要求

  • TLS 1.2 or later
  • Forward secrecy cipher suites
  • SHA-256 or better certificates
  • 2048-bit or greater RSA keys (or 256-bit ECC)
  • TLS 1.2或更高版本
  • 前向保密密码套件
  • SHA-256或更高级别的证书
  • 2048位或更长的RSA密钥(或256位ECC密钥)

Exception Domains (Last Resort)

例外域(最后手段)

xml
<!-- Only for legacy servers you cannot upgrade -->
<key>NSAppTransportSecurity</key>
<dict>
    <key>NSExceptionDomains</key>
    <dict>
        <key>legacy-api.example.com</key>
        <dict>
            <key>NSExceptionAllowsInsecureHTTPLoads</key>
            <true/>
            <key>NSExceptionMinimumTLSVersion</key>
            <string>TLSv1.2</string>
        </dict>
    </dict>
</dict>
Rules:
  • NEVER set
    NSAllowsArbitraryLoads
    to true. Apple will reject the app.
  • Exception domains require justification in App Review notes.
  • Use exception domains only for third-party servers you cannot control.
xml
<!-- 仅适用于无法升级的遗留服务器 -->
<key>NSAppTransportSecurity</key>
<dict>
    <key>NSExceptionDomains</key>
    <dict>
        <key>legacy-api.example.com</key>
        <dict>
            <key>NSExceptionAllowsInsecureHTTPLoads</key>
            <true/>
            <key>NSExceptionMinimumTLSVersion</key>
            <string>TLSv1.2</string>
        </dict>
    </dict>
</dict>
规则:
  • 绝不要将
    NSAllowsArbitraryLoads
    设为true,Apple会拒绝此类应用。
  • 例外域需要在应用审核备注中说明理由。
  • 仅对无法控制的第三方服务器使用例外域。

Certificate Pinning

证书绑定

Pin certificates for sensitive API connections to prevent MITM attacks.
对敏感API连接使用证书绑定,以防止中间人攻击。

URLSession Delegate Pinning

URLSession 代理绑定

swift
import CryptoKit

class PinnedSessionDelegate: NSObject, URLSessionDelegate {
    // SHA-256 hash of the certificate's Subject Public Key Info
    private let pinnedHashes: Set<String> = [
        "base64EncodedSHA256HashOfSPKI=="
    ]

    func urlSession(
        _ session: URLSession,
        didReceive challenge: URLAuthenticationChallenge
    ) async -> (URLSession.AuthChallengeDisposition, URLCredential?) {
        guard let trust = challenge.protectionSpace.serverTrust,
              let chain = SecTrustCopyCertificateChain(trust) as? [SecCertificate],
              let certificate = chain.first else {
            return (.cancelAuthenticationChallenge, nil)
        }

        guard let publicKey = SecCertificateCopyKey(certificate),
              let publicKeyData = SecKeyCopyExternalRepresentation(
                  publicKey, nil
              ) as Data? else {
            return (.cancelAuthenticationChallenge, nil)
        }

        let hash = SHA256.hash(data: publicKeyData)
        let hashString = Data(hash).base64EncodedString()

        if pinnedHashes.contains(hashString) {
            return (.useCredential, URLCredential(trust: trust))
        }

        return (.cancelAuthenticationChallenge, nil)
    }
}
Rules:
  • Pin the public key hash, not the certificate. Certificates rotate; public keys are more stable.
  • Always include at least one backup pin.
  • Have a rotation plan. If all pinned keys expire, the app cannot connect.
  • Consider a kill switch (remote config to disable pinning in emergency).
swift
import CryptoKit

class PinnedSessionDelegate: NSObject, URLSessionDelegate {
    // 证书主体公钥信息的SHA-256哈希
    private let pinnedHashes: Set<String> = [
        "base64EncodedSHA256HashOfSPKI=="
    ]

    func urlSession(
        _ session: URLSession,
        didReceive challenge: URLAuthenticationChallenge
    ) async -> (URLSession.AuthChallengeDisposition, URLCredential?) {
        guard let trust = challenge.protectionSpace.serverTrust,
              let chain = SecTrustCopyCertificateChain(trust) as? [SecCertificate],
              let certificate = chain.first else {
            return (.cancelAuthenticationChallenge, nil)
        }

        guard let publicKey = SecCertificateCopyKey(certificate),
              let publicKeyData = SecKeyCopyExternalRepresentation(
                  publicKey, nil
              ) as Data? else {
            return (.cancelAuthenticationChallenge, nil)
        }

        let hash = SHA256.hash(data: publicKeyData)
        let hashString = Data(hash).base64EncodedString()

        if pinnedHashes.contains(hashString) {
            return (.useCredential, URLCredential(trust: trust))
        }

        return (.cancelAuthenticationChallenge, nil)
    }
}
规则:
  • 绑定公钥哈希而非证书,证书会轮换,公钥更稳定。
  • 至少添加一个备用绑定哈希。
  • 制定轮换计划,若所有绑定密钥过期,应用将无法连接。
  • 考虑添加应急开关(远程配置以在紧急情况下禁用绑定)。

Secure Coding Patterns

安全编码模式

Never Log Sensitive Data

切勿记录敏感数据

swift
// WRONG
logger.debug("User logged in with token: \(token)")

// CORRECT
logger.debug("User logged in successfully")
swift
// 错误示例
logger.debug("用户登录,令牌:\(token)")

// 正确示例
logger.debug("用户登录成功")

Clear Sensitive Data From Memory

从内存中清除敏感数据

swift
var sensitiveData = Data(/* ... */)
defer {
    sensitiveData.resetBytes(in: 0..<sensitiveData.count)
}
swift
var sensitiveData = Data(/* ... */)
defer {
    sensitiveData.resetBytes(in: 0..<sensitiveData.count)
}

Validate All Input

验证所有输入

swift
// Validate URL schemes
guard let url = URL(string: input),
      ["https"].contains(url.scheme?.lowercased()) else {
    throw SecurityError.invalidURL
}

// Prevent path traversal
let resolved = url.standardized.path
guard resolved.hasPrefix(allowedDirectory.path) else {
    throw SecurityError.pathTraversal
}
swift
// 验证URL协议
guard let url = URL(string: input),
      ["https"].contains(url.scheme?.lowercased()) else {
    throw SecurityError.invalidURL
}

// 防止路径遍历
let resolved = url.standardized.path
guard resolved.hasPrefix(allowedDirectory.path) else {
    throw SecurityError.pathTraversal
}

Jailbreak Detection

越狱检测

swift
func isDeviceCompromised() -> Bool {
    let paths = [
        "/Applications/Cydia.app",
        "/Library/MobileSubstrate/MobileSubstrate.dylib",
        "/usr/sbin/sshd",
        "/etc/apt",
        "/private/var/lib/apt/"
    ]

    for path in paths {
        if FileManager.default.fileExists(atPath: path) { return true }
    }

    // Check if app can write outside sandbox
    let testPath = "/private/test_jailbreak"
    do {
        try "test".write(toFile: testPath, atomically: true, encoding: .utf8)
        try FileManager.default.removeItem(atPath: testPath)
        return true
    } catch {
        return false
    }
}
Jailbreak detection is not foolproof. Use it as one layer, not the only layer.
swift
func isDeviceCompromised() -> Bool {
    let paths = [
        "/Applications/Cydia.app",
        "/Library/MobileSubstrate/MobileSubstrate.dylib",
        "/usr/sbin/sshd",
        "/etc/apt",
        "/private/var/lib/apt/"
    ]

    for path in paths {
        if FileManager.default.fileExists(atPath: path) { return true }
    }

    // 检查应用是否能写入沙箱外路径
    let testPath = "/private/test_jailbreak"
    do {
        try "test".write(toFile: testPath, atomically: true, encoding: .utf8)
        try FileManager.default.removeItem(atPath: testPath)
        return true
    } catch {
        return false
    }
}
越狱检测并非万无一失,仅将其作为安全防护的一层,而非唯一手段。

Privacy Manifests

隐私清单

Apps and SDKs must declare data access in
PrivacyInfo.xcprivacy
. See
references/privacy-manifest.md
for required-reason API declarations and security-related data collection details. For submission requirements and compliance checklists, see
references/app-review-guidelines.md
.
应用和SDK必须在
PrivacyInfo.xcprivacy
中声明数据访问情况。有关必填原因API声明和安全相关数据收集的详细信息,请参阅
references/privacy-manifest.md
。提交要求和合规清单请参阅
references/app-review-guidelines.md

Common Mistakes

常见错误

  1. Storing secrets in UserDefaults. Tokens, passwords, API keys must go in Keychain.
  2. Hardcoded secrets in source. No API keys or credentials in Swift files.
  3. Disabling ATS globally.
    NSAllowsArbitraryLoads = true
    is a rejection risk.
  4. Logging sensitive data. Never log tokens, passwords, or API keys.
  5. Missing PrivacyInfo.xcprivacy. Required for all apps using required-reason APIs.
  6. Using CommonCrypto instead of CryptoKit. CryptoKit is safer and modern.
  7. Missing NSFaceIDUsageDescription. Crashes on Face ID devices.
  8. Using
    .biometryAny
    when
    .biometryCurrentSet
    is needed.
    The former survives enrollment changes, which may be undesirable for high-security items.
  9. Path traversal vulnerabilities. Always resolve and validate paths.
  10. Missing concurrency annotations. Ensure Keychain wrapper types are Sendable; isolate UI-facing security prompts to
    @MainActor
    .
  1. 在UserDefaults中存储机密信息:令牌、密码、API密钥必须存储在Keychain中。
  2. 在源码中硬编码机密信息:Swift文件中不得包含API密钥或凭据。
  3. 全局禁用ATS
    NSAllowsArbitraryLoads = true
    会导致应用被拒绝。
  4. 记录敏感数据:切勿记录令牌、密码或API密钥。
  5. 缺少PrivacyInfo.xcprivacy:使用必填原因API的所有应用都必须添加该文件。
  6. 使用CommonCrypto而非CryptoKit:CryptoKit更安全、更现代化。
  7. 缺少NSFaceIDUsageDescription:会导致应用在Face ID设备上崩溃。
  8. 在需要
    .biometryCurrentSet
    时使用
    .biometryAny
    :后者在注册变更后仍然有效,对于高安全级别的项目来说可能不符合需求。
  9. 路径遍历漏洞:始终解析并验证路径。
  10. 缺少并发注解:确保Keychain包装类型是Sendable;将面向UI的安全提示隔离到
    @MainActor
    中。

Review Checklist

审核清单

  • Secrets in Keychain, not UserDefaults or files; no hardcoded credentials
  • Correct
    kSecAttrAccessible
    value;
    ThisDeviceOnly
    for non-backup data
  • File protection class set for sensitive files (
    .complete
    )
  • CryptoKit for encryption (not CommonCrypto); 256-bit symmetric keys
  • Keys stored in Keychain or Secure Enclave
  • Biometric auth with fallback;
    NSFaceIDUsageDescription
    in Info.plist
  • Correct
    SecAccessControl
    flags;
    LAContext
    configured
  • HTTPS enforced; no
    NSAllowsArbitraryLoads
    ; cert pinning for sensitive APIs
  • PrivacyInfo.xcprivacy present; all required-reason APIs declared
  • No sensitive data in logs; Data cleared after use; URLs/paths validated
  • 机密信息存储在Keychain中,而非UserDefaults或文件;无硬编码凭据
  • 使用正确的
    kSecAttrAccessible
    取值;非备份数据使用
    ThisDeviceOnly
  • 敏感文件设置了正确的文件保护类别(
    .complete
  • 使用CryptoKit进行加密(而非CommonCrypto);使用256位对称密钥
  • 密钥存储在Keychain或Secure Enclave中
  • 生物识别认证支持回退;Info.plist中包含
    NSFaceIDUsageDescription
  • 使用正确的
    SecAccessControl
    标志;配置了
    LAContext
  • 强制使用HTTPS;未设置
    NSAllowsArbitraryLoads
    ;敏感API使用证书绑定
  • 存在PrivacyInfo.xcprivacy;所有必填原因API已声明
  • 日志中无敏感数据;使用后清除Data;验证URL/路径