iOS 中的 Keychain:什么是它、架构以及如何处理机密

作者: IT Sectr 发布日期: 2026-04-04 阅读时间: 9 分钟

Keychain(钥匙串)—— iOS 中受保护的安全存储,旨在安全存储密码、加密密钥、证书和机密笔记。根据 Apple Security Documentation (2025),Keychain 在配备 A7 及更新芯片的所有设备上通过 Secure Enclave 使用硬件加密。理解 iOS Keychain 的架构对于每个开发人员正确存储应用程序的令牌和机密至关重要。

要点

  • iOS Keychain — 一个加密的 SQLite 数据库,用于存储机密,通过 Secure Enclave 提供硬件保护。
  • Protection Class 决定数据何时可用:设备解锁时、首次解锁后或始终可用。
  • Access Control List(ACL)— 限制对 Keychain 元素访问的机制,包括生物识别认证。
  • SecItemAdd 和 SecItemCopyMatching — Security framework 中用于写入和读取元素的主要 API。
  • kSecAttrSynchronizable — 允许通过 iCloud 同步 Keychain 以在用户所有设备上访问的标志。

什么是 iOS 中的 Keychain?

iOS Keychain — 一种内置于 Apple 操作系统的安全机密数据存储机制。与 UserDefaults 或普通文件不同,Keychain 在硬件级别加密所有元素,并基于安全策略提供细粒度的访问控制。

Keychain 在 iOS 2.0 中引入,此后经历了重大变化:iOS 7 增加了通过 Secure Enclave 对硬件密钥的支持,iOS 9 — 通过 Access Groups 在应用程序间分离 Keychain,iOS 13 — 通过 LAContext 支持生物识别绑定。根据 Apple WWDC Session (2024),App Store 前 100 名中超过 90% 的 iOS 应用程序使用 Keychain 存储认证令牌。

在架构上,Keychain 是一个加密的 SQLite 数据库,位于应用程序沙箱之外。每个元素(SecItem)使用单独的密钥加密,该密钥又受到 Secure Enclave 硬件密钥的保护。系统服务 Securityd 根据应用程序的权限(entitlements)和请求的保护类别管理对 Keychain 的访问。

Keychain 相较于其他存储方式的重要优势:数据由操作系统自动加密和解密。开发人员无需手动实现加密 — 只需使用正确的参数调用 SecItemAdd 即可。iOS 保证其他应用程序无法读取 Keychain 中的数据(在正确配置 Access Groups 的情况下)。

Keychain 架构

Keychain 架构包含多个层级:物理层(Secure Enclave)、系统层(Security.framework)、应用层(SecItem* API)和逻辑层(Access Groups、Protection Classes)。理解每个层级有助于正确设计机密存储。

SecItemAdd 和 SecItemCopyMatching

用于处理 Keychain 的主要 API 是 Security framework 的函数:SecItemAdd 用于添加,SecItemCopyMatching 用于读取,SecItemUpdate 用于更新,SecItemDelete 用于删除。每个函数接受一个 query 字典,该字典描述要查找或存储的元素的属性。

关键 query 属性:kSecClass — 元素类型(kSecClassGenericPassword、kSecClassKey、kSecClassCertificate),kSecAttrAccount — 类内的唯一标识符,kSecValueData — 存储的数据(Data),kSecAttrAccessible — 保护类别。带有 kSecReturnData 标志的 SecItemCopyMatching 返回元素的数据,kSecMatchLimit 则返回结果数量。

重要提示:所有函数都返回 OSStatus。成功操作返回 errSecSuccess(0)。错误:errSecItemNotFound(-25300)— 未找到元素,errSecDuplicateItem(-25299)— 元素已存在,errSecAuthFailed(-25293)— 生物识别认证失败。开发人员必须正确处理每种状态。

保护类别(Protection Class)

Protection Class — 属性 kSecAttrAccessible,它决定 Keychain 中的数据何时可读。iOS 支持六种保护类别,具有不同的可用性和安全级别。

大多数场景推荐的类别是 kSecAttrAccessibleWhenUnlockedThisDeviceOnly:数据仅在设备解锁时可用,且不会复制到 iCloud Backup。对于重启后必须可用的数据(但仅在首次解锁后),请使用 kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly。对于每次访问都需要生物识别认证的敏感数据,请将 kSecAttrAccessibleWhenUnlockedThisDeviceOnly 与要求 biometry 的 ACL 结合使用。

不带 ThisDeviceOnly 后缀的类别(kSecAttrAccessibleWhenUnlocked、kSecAttrAccessibleAfterFirstUnlock)允许复制到 iCloud Backup。这对用户方便,但会降低安全性 — 数据可以从备份中恢复。对于认证令牌,始终使用 ThisDeviceOnly。

访问控制列表(ACL)

Access Control List(ACL)— 一种基于用户身份验证限制对 Keychain 元素进行操作的机制。ACL 通过 SecAccessControlCreateWithFlags 定义,并在保存元素时传递给 kSecAttrAccessControl 属性。

支持的标志:kSecAccessControlUserPresence — 任何身份验证(Face ID、Touch ID 或密码),kSecAccessControlBiometryCurrentSet — 仅生物识别(当前注册的指纹或面部),kSecAccessControlDevicePasscode — 仅密码。ACL 适用于每个操作:读取、更新和删除元素也需要身份验证。

在 iOS 15+ 中,新增了 kSecAccessControlWatch 标志 — 针对 Apple Watch,允许通过配对手表进行身份验证。ACL 可以组合:例如,kSecAccessControlUserPresence 或 kSecAccessControlBiometryAny 带可选密码(.or 方向)。

Keychain 中的数据类型

iOS Keychain 支持四种基本元素类别(kSecClass),每种适用于其自身的数据类型。正确的类别选择简化了元素的组织和搜索。

kSecClassGenericPassword — 通用密码:最常用的类别。存储任意二进制数据(Data)并带唯一密钥(kSecAttrAccount)。适用于令牌、API 密钥、PIN 码。使用时无需额外 entitlements。

kSecClassInternetPassword — 互联网密码:存储与网络资源相关的数据。附加属性:kSecAttrServer(服务器域名)、kSecAttrProtocol(https、ftp)、kSecAttrPort、kSecAttrAuthenticationType。iOS 可以通过 AutoFill 自动填充此类密码。

kSecClassKey — 加密密钥:用于存储加密密钥(AES、RSA、EC)。密钥存储为 SecKeyRef 而非 Data。kSecClassCertificate — X.509 证书,用于存储和验证数字证书。这两个类别都需要理解加密操作并正确配置属性。

实际上,移动应用中 95% 的 Keychain 使用场景由 kSecClassGenericPassword(用于存储认证令牌)和 kSecClassKey(用于存储私钥加密密钥)覆盖。kSecClassCertificate 很少使用 — 通常用于具有自有 PKI 的企业应用。

代码示例:在 Swift 中使用 Keychain

让我们来看使用 Security framework 在 Swift 中处理 Keychain 的实践示例。每个示例包括错误处理和正确的 Protection Class 配置。

存储和读取令牌

基本示例使用 WhenUnlockedThisDeviceOnly 保护将认证令牌存储在 Keychain 中。密钥(kSecAttrAccount)是服务标识符,数据(kSecValueData)是 Data 格式的令牌。

swift
import Security

enum KeychainError: Error {
    case unexpectedStatus(OSStatus)
}

func saveToken(token: String, service: String) throws {
    let data = Data(token.utf8)
    let query: [String: Any] = [
        kSecClass as String: kSecClassGenericPassword,
        kSecAttrService as String: service,
        kSecAttrAccount as String: "auth_token",
        kSecValueData as String: data,
        kSecAttrAccessible as String:
            kSecAttrAccessibleWhenUnlockedThisDeviceOnly
    ]
    SecItemDelete(query as CFDictionary)
    let status = SecItemAdd(query as CFDictionary, nil)
    guard status == errSecSuccess else {
        throw KeychainError.unexpectedStatus(status)
    }
}

func readToken(service: String) throws -> String {
    let query: [String: Any] = [
        kSecClass as String: kSecClassGenericPassword,
        kSecAttrService as String: service,
        kSecAttrAccount as String: "auth_token",
        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.unexpectedStatus(status)
    }
    return String(decoding: data, as: UTF8.self)
}

带生物识别绑定的存储

该示例演示了使用 SecAccessControlCreateWithFlags 将密钥绑定到生物识别。每次访问该元素都需要 Face ID 或 Touch ID。

swift
import LocalAuthentication

func saveWithBiometry(data: Data, key: String) throws {
    let accessControl = SecAccessControlCreateWithFlags(
        nil,
        kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
        .biometryCurrentSet,
        nil
    )

    let query: [String: Any] = [
        kSecClass as String: kSecClassGenericPassword,
        kSecAttrAccount as String: key,
        kSecValueData as String: data,
        kSecAttrAccessControl as String: accessControl as Any
    ]

    SecItemDelete(query as CFDictionary)
    let status = SecItemAdd(query as CFDictionary, nil)
    guard status == errSecSuccess else {
        throw KeychainError.unexpectedStatus(status)
    }
}

应用程序间的 Keychain 共享

该示例展示了配置 Access Group 以实现同一开发者的应用程序之间共享 Keychain 访问。需要 entitlement keychain-access-groups。

swift
// Capabilities: 已启用 Keychain Sharing
// App IDs: group.com.example.shared

func saveSharedToken(token: Data) {
    let query: [String: Any] = [
        kSecClass as String: kSecClassGenericPassword,
        kSecAttrAccount as String: "shared_token",
        kSecValueData as String: token,
        kSecAttrAccessGroup as String:
            "group.com.example.shared",
        kSecAttrAccessible as String:
            kSecAttrAccessibleWhenUnlockedThisDeviceOnly
    ]
    SecItemAdd(query as CFDictionary, nil)
}

Keychain 最佳实践

正确使用 iOS Keychain 需要遵守若干关键规则,以防止典型漏洞和数据丢失。

对所有认证机密使用 ThisDeviceOnly:kSecAttrAccessibleWhenUnlockedThisDeviceOnly 确保令牌不会进入 iCloud Backup。如果攻击者获得备份访问权限,带有此标志的 Keychain 数据将不可用。例外情况是需要在用户所有设备上可用的数据(例如,自有服务的加密密钥),对于这些数据,请使用带 kSecAttrSynchronizable 的 kSecAttrAccessibleWhenUnlocked。

不要存储原始密码 — 存储哈希或会话令牌。Apple Security Guide (2025) 建议永远不要将用户的密码以明文形式存储在 Keychain 中。而是存储通过 OAuth 2.0 成功认证后从服务器获取的刷新令牌。密码仅用于获取令牌,并立即从内存中删除。

正确处理 Keychain 错误:每次对 Keychain 的操作都会返回 OSStatus,需要进行检查。特别注意 errSecItemNotFound(令牌已过期或已删除)和 errSecAuthFailed(生物识别认证失败)。在前一种情况下,应用程序应请求新的认证,在后一种情况下 — 向用户显示替代方式(密码)。切勿忽略 errSecItemNotFound 状态 — 这会在尝试读取 nil 时导致应用程序崩溃。

在真实设备上测试 Keychain:模拟器没有 Secure Enclave,不支持生物识别 ACL。始终测试以下场景:首次启动、从备份恢复、更改设备密码、删除和重新安装应用程序。在真实设备上,Keychain 在删除应用程序时会被保留,但仅当未使用 kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly 标志时,该标志会在移除密码时被清除。

最小化 Keychain 操作次数:每次读取或写入操作都是对系统服务 Securityd 的调用,可能会阻塞线程。在会话期间将读取的令牌缓存在内存中,仅在应用程序重启或认证错误(来自服务器的 401)时重新访问 Keychain。iOS 会在设备锁定时自动锁定 Keychain,因此需通过 LAContext 计划读取并请求生物识别。

常见问题

我可以将数据保存在 Keychain 中并在重启后读取吗?

可以,请使用保护类别 kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly 或 kSecAttrAccessibleAfterFirstUnlock。数据将在重启后设备首次解锁时可用。要在应用启动时自动访问(无需等待解锁),请使用 kSecAttrAccessibleAlways,但这会降低安全性。

如何在用户退出时清除 Keychain?

调用带有包含每种数据类型 kSecClass 的 query 的 SecItemDelete。要完全清除应用程序的所有元素,请执行:SecItemDelete([kSecClass as String: kSecClassGenericPassword] as CFDictionary)。对 kSecClassKey、kSecClassCertificate 和 kSecClassInternetPassword 重复调用。

kSecAttrAccessible 和 kSecAttrAccessControl 有什么区别?

kSecAttrAccessible 决定数据何时可用(解锁时、首次解锁后等)。kSecAttrAccessControl 决定谁可以访问(生物识别、密码、任何身份验证)。它们组合使用:先 Protection Class,后 ACL。例如,数据仅在解锁时 AND 仅在 Face ID 之后可用。

为什么 SecItemCopyMatching 返回 errSecItemNotFound?

原因:元素从未保存、移除密码时元素被删除(如果使用了 kSecAttrAccessibleWhenPasscodeSet)、应用被重新安装(Keychain 保留但不会从备份恢复到新设备)、Access Group 或开发者团队标识符已更改。请检查 kSecAttrService 和 kSecAttrAccount。

如何检查设备是否支持 Keychain 中的生物识别?

使用来自 LocalAuthentication 的 LAContext:调用 context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: nil)。如果返回 true — 设备支持 Touch ID 或 Face ID。对于 Keychain ACL,使用标志 biometryCurrentSet(仅当前生物识别数据)或 biometryAny(之前注册的任何数据)。

总结

  • iOS Keychain — 受硬件保护的机密存储空间,通过 Secure Enclave 加密并通过 ACL 控制访问。
  • Security framework 提供 SecItemAdd、SecItemCopyMatching、SecItemUpdate 和 SecItemDelete 函数来处理元素。
  • Protection Class(kSecAttrAccessible)根据场景选择:WhenUnlockedThisDeviceOnly — 令牌的标准选择。
  • 带生物识别的 ACL(kSecAttrAccessControl)为每次读取操作添加 Face ID 或 Touch ID 要求。
  • kSecClassGenericPassword 覆盖 95% 的场景 — 存储令牌、API 密钥、PIN 码和笔记。
  • ThisDeviceOnly 防止机密复制到 iCloud Backup — 认证令牌的必需品。
  • 正确处理 OSStatus 和在真实设备上测试 — 可靠使用 Keychain 的必备实践。

我们将开发一款交钥匙移动应用程序

IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。

讨论项目

另请阅读