mcpField
I
ios-security

ios-security

github.com/dpearson2699安全1.2k1.1k 次安装13 周前更新
代码仓库

关于 ios-security

面向 iOS 开发者的安全实践指南,涵盖敏感数据存储、身份验证、加密与 Apple 平台安全要求,并提供代码示例和审查清单。

主要功能

  1. 01Keychain 敏感数据存储指导
  2. 02文件数据保护级别说明
  3. 03CryptoKit 加密与哈希实践
  4. 04Secure Enclave 和生物识别保护
  5. 05ATS 与证书固定建议
  6. 06安全编码及隐私清单检查

适用场景

  1. 01为密码、令牌和 API 密钥选择安全存储方式
  2. 02为敏感文件设置适当的数据保护
  3. 03审查 iOS 应用的加密和生物识别实现
  4. 04检查网络连接、日志及隐私清单配置

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/dpearson2699/swift-ios-skills --skill ios-security

安装后在 Claude Code 中输入/ios-security确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

references/
app-review-guidelines.md
cryptokit-advanced.md
file-storage-patterns.md
privacy-manifest.md

详细介绍

iOS 安全

介绍如何处理敏感数据、验证用户身份、正确加密,以及遵循 Apple 在 iOS 上的安全最佳实践。

目录

钥匙串服务

钥匙串是存储敏感数据的唯一正确位置。切勿将密码、令牌、API 密钥或机密信息存储在 UserDefaults、文件或 Core Data 中。

存储凭据
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)
    }
}
读取凭据
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
}
删除凭据
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 值
值可用时机仅限本设备用途
kSecAttrAccessibleWhenUnlocked设备已解锁否常规凭据
kSecAttrAccessibleWhenUnlockedThisDeviceOnly设备已解锁是敏感凭据
kSecAttrAccessibleAfterFirstUnlock首次解锁后否可在后台访问的令牌
kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly首次解锁后是后台令牌,不备份
kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly已设置密码且设备已解锁是最高安全级别

规则:

  • 对敏感数据使用 ThisDeviceOnly 变体,以防止备份和恢复到其他设备。
  • 对后台操作所需的令牌使用 AfterFirstUnlock。
  • 对最敏感的数据使用 WhenPasscodeSetThisDeviceOnly。移除密码后,项目会被删除。
  • 切勿使用 kSecAttrAccessibleAlways(已弃用且不安全)。
钥匙串访问组

在同一团队的多个应用之间共享钥匙串项目:

let query: [String: Any] = [
    kSecClass as String: kSecClassGenericPassword,
    kSecAttrAccount as String: "shared-token",
    kSecAttrAccessGroup as String: "TEAMID.com.company.shared"
]
@AppStorage 与钥匙串
存储方式用途安全性
@AppStorage / UserDefaults非敏感偏好设置(主题、引导状态、功能标记)静态存储时未加密
钥匙串密码、令牌、API 密钥、机密信息硬件加密,并受访问控制

规则: 如果数据泄露会令人尴尬或造成危险,就应存入钥匙串。其他数据都可以使用 @AppStorage。

// Non-sensitive preference -- @AppStorage is fine
@AppStorage("hasCompletedOnboarding") private var hasOnboarded = false

// Sensitive credential -- MUST use Keychain
// WRONG: @AppStorage("authToken") private var token = ""
// CORRECT: Use saveToKeychain(account:data:service:)

数据保护

iOS 会根据保护级别对文件进行加密:

级别可用时机用途
.complete仅在设备解锁时敏感用户数据
.completeUnlessOpen锁定后,已打开的句柄仍可使用正在进行的下载、录音
.completeUntilFirstUserAuthentication首次解锁后(默认)大多数应用数据
.none始终可用非敏感且系统所需的数据
// 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

任何包含用户敏感数据的文件都应使用 .complete。默认的 .completeUntilFirstUserAuthentication 适用于一般应用数据。

CryptoKit

所有加密操作都应使用 CryptoKit。新代码不要使用 CommonCrypto 或原始 Security 框架。

对称加密(AES-GCM)
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)
}
哈希
let hash = SHA256.hash(data: data)
let hashString = hash.compactMap { String(format: "%02x", $0) }.joined()

// Also available: SHA384, SHA512
HMAC(消息认证)
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)

有关数字签名(P256/ECDSA)、密钥协商(Curve25519)、ChaChaPoly 和 HKDF 密钥派生,请参阅 references/cryptokit-advanced.md。

安全隔区

为了获得最高安全性,请将密钥存储在安全隔区中。密钥永远不会离开硬件。仅支持 P256。

guard SecureEnclave.isAvailable else { return }

let accessControl = SecAccessControlCreateWithFlags(
    nil, kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly,
    [.privateKeyUsage, .biometryCurrentSet], nil
)!
let privateKey = try SecureEnclave.P256.Signing.PrivateKey(accessControl: accessControl)

let signature = try privateKey.signature(for: data)  // May trigger biometric prompt
let isValid = privateKey.publicKey.isValidSignature(signature, for: data)

// Persist: store dataRepresentation in Keychain, restore with:
let restored = try SecureEnclave.P256.Signing.PrivateKey(
    dataRepresentation: privateKey.dataRepresentation
)

生物识别身份验证

本节介绍钥匙串项目和数据访问的生物识别保护。有关面向用户的生物识别登录流程(将 LAContext.evaluatePolicy 用作登录机制),请参阅 authentication 技能。

LocalAuthentication(Face ID / Touch ID)
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"
    )
}
Info.plist 要求

必须在 Info.plist 中包含 NSFaceIDUsageDescription:

<key>NSFaceIDUsageDescription</key>
<string>Authenticate to access your secure data</string>

缺少此键会导致 Face ID 设备发生崩溃。

LAContext 配置
let context = LAContext()
context.localizedFallbackTitle = "Use Passcode"
context.touchIDAuthenticationAllowableReuseDuration = 30
let currentState = context.evaluatedPolicyDomainState // Compare to detect enrollment changes
生物识别 + Keychain

使用生物识别访问权限保护 Keychain 项目:

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)

ATS 默认强制使用 HTTPS。切勿禁用。

ATS 要求
  • TLS 1.2 或更高版本
  • 前向保密密码套件
  • SHA-256 或更高等级的证书
  • 2048 位或更长的 RSA 密钥(或 256 位 ECC 密钥)
例外域(最后手段)

<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 会拒绝该应用。
  • 例外域需要在 App Review 备注中说明理由。
  • 仅对你无法控制的第三方服务使用例外域。

证书固定

为敏感 API 连接固定证书,以防止 MITM 攻击。

URLSession 委托固定
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)
    }
}

规则:

  • 固定公钥哈希,而非证书。证书会轮换,公钥则更稳定。
  • 始终至少包含一个备用固定项。
  • 制定轮换计划。如果所有固定密钥都过期,应用将无法连接。
  • 考虑设置紧急停止开关(通过远程配置禁用证书固定)。

安全编码模式

切勿记录敏感数据
// WRONG
logger.debug("User logged in with token: \(token)")

// CORRECT
logger.debug("User logged in successfully")
清除内存中的敏感数据
var sensitiveData = Data(/* ... */)
defer {
    sensitiveData.resetBytes(in: 0..<sensitiveData.count)
}
验证所有输入
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
}
API 密钥占位符模式

使用 #error 防止意外提交占位符 API 密钥:

// Forces a build error until the real key is configured
#error("Add your API key to Secrets.plist -- see README for setup")
private let apiKey = Secrets.value(for: "API_KEY")
越狱检测

检查已知越狱文件路径(/Applications/Cydia.app、/usr/sbin/sshd 等)以及沙盒逃逸情况。越狱检测并非万无一失,应将其作为一层防护,而非唯一防护。完整实现请参阅 references/cryptokit-advanced.md。

隐私清单

应用和 SDK 必须在 PrivacyInfo.xcprivacy 中声明数据访问情况。必需理由 API 声明及安全相关的数据收集详情,请参阅 references/privacy-manifest.md。提交要求和 合规检查清单请参阅 references/app-review-guidelines.md。

常见错误

  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。

审查清单

  • 密钥存储在 Keychain 中,而非 UserDefaults 或文件中;不硬编码凭据
  • 使用正确的 kSecAttrAccessible 值;非备份数据使用 ThisDeviceOnly
  • 为敏感文件设置文件保护级别(.complete)
  • 使用 CryptoKit 加密(而非 CommonCrypto);使用 256 位对称密钥
  • 密钥存储在 Keychain 或 Secure Enclave 中
  • 生物识别认证支持回退;在 Info.plist 中添加 NSFaceIDUsageDescription
  • 使用正确的 SecAccessControl 标志;配置 LAContext
  • 强制使用 HTTPS;不设置 NSAllowsArbitraryLoads;为敏感 API 配置证书固定
  • 存在 PrivacyInfo.xcprivacy;声明所有必需理由的 API
  • 日志中不包含敏感数据;使用后清除 Data;验证 URL/路径

参考资料

评论

支持 Markdown