Keychain(钥匙串)—— iOS 中受保护的安全存储,旨在安全存储密码、加密密钥、证书和机密笔记。根据 Apple Security Documentation (2025),Keychain 在配备 A7 及更新芯片的所有设备上通过 Secure Enclave 使用硬件加密。理解 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 架构包含多个层级:物理层(Secure Enclave)、系统层(Security.framework)、应用层(SecItem* API)和逻辑层(Access Groups、Protection Classes)。理解每个层级有助于正确设计机密存储。
用于处理 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 — 属性 kSecAttrAccessible,它决定 Keychain 中的数据何时可读。iOS 支持六种保护类别,具有不同的可用性和安全级别。
大多数场景推荐的类别是 kSecAttrAccessibleWhenUnlockedThisDeviceOnly:数据仅在设备解锁时可用,且不会复制到 iCloud Backup。对于重启后必须可用的数据(但仅在首次解锁后),请使用 kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly。对于每次访问都需要生物识别认证的敏感数据,请将 kSecAttrAccessibleWhenUnlockedThisDeviceOnly 与要求 biometry 的 ACL 结合使用。
不带 ThisDeviceOnly 后缀的类别(kSecAttrAccessibleWhenUnlocked、kSecAttrAccessibleAfterFirstUnlock)允许复制到 iCloud Backup。这对用户方便,但会降低安全性 — 数据可以从备份中恢复。对于认证令牌,始终使用 ThisDeviceOnly。
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 方向)。
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 的企业应用。
让我们来看使用 Security framework 在 Swift 中处理 Keychain 的实践示例。每个示例包括错误处理和正确的 Protection Class 配置。
基本示例使用 WhenUnlockedThisDeviceOnly 保护将认证令牌存储在 Keychain 中。密钥(kSecAttrAccount)是服务标识符,数据(kSecValueData)是 Data 格式的令牌。
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。
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)
}
}
该示例展示了配置 Access Group 以实现同一开发者的应用程序之间共享 Keychain 访问。需要 entitlement keychain-access-groups。
// 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)
}
正确使用 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 计划读取并请求生物识别。
常见问题
可以,请使用保护类别 kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly 或 kSecAttrAccessibleAfterFirstUnlock。数据将在重启后设备首次解锁时可用。要在应用启动时自动访问(无需等待解锁),请使用 kSecAttrAccessibleAlways,但这会降低安全性。
调用带有包含每种数据类型 kSecClass 的 query 的 SecItemDelete。要完全清除应用程序的所有元素,请执行:SecItemDelete([kSecClass as String: kSecClassGenericPassword] as CFDictionary)。对 kSecClassKey、kSecClassCertificate 和 kSecClassInternetPassword 重复调用。
kSecAttrAccessible 决定数据何时可用(解锁时、首次解锁后等)。kSecAttrAccessControl 决定谁可以访问(生物识别、密码、任何身份验证)。它们组合使用:先 Protection Class,后 ACL。例如,数据仅在解锁时 AND 仅在 Face ID 之后可用。
原因:元素从未保存、移除密码时元素被删除(如果使用了 kSecAttrAccessibleWhenPasscodeSet)、应用被重新安装(Keychain 保留但不会从备份恢复到新设备)、Access Group 或开发者团队标识符已更改。请检查 kSecAttrService 和 kSecAttrAccount。
使用来自 LocalAuthentication 的 LAContext:调用 context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: nil)。如果返回 true — 设备支持 Touch ID 或 Face ID。对于 Keychain ACL,使用标志 biometryCurrentSet(仅当前生物识别数据)或 biometryAny(之前注册的任何数据)。
总结
我们将开发一款交钥匙移动应用程序
IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。