iOS Keychain 与 Secure Enclave:原理与核心 API
原理:硬件隔离与数据保护等级
iOS Keychain 并非普通的 SQLite 存储,而是基于 Keychain Stripe 机制加密的安全容器,数据被包裹在由设备硬件密钥保护的 SQLCipher 数据库中。对于蓝牙敏感数据(如配对密钥、身份解析密钥 IRK、会话密钥),Keychain 提供了比 UserDefaults 或 Core Data 高得多的安全性。
kSecAttrAccessible 系列属性决定了 Keychain 数据的生命周期与备份行为,直接影响蓝牙密钥的安全性:
| 属性常量 | 可访问时机 | 备份行为 | 推荐场景 |
|---|---|---|---|
kSecAttrAccessibleWhenUnlocked | 设备解锁后 | 备份中加密 | 蓝牙会话临时密钥 |
kSecAttrAccessibleAfterFirstUnlock | 重启后首次解锁之后 | 备份中加密 | 后台 BLE 扫描令牌 |
kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly | 设备解锁(需设置密码) | 不包含在备份中 | 蓝牙长期配对密钥 / IRK |
kSecAttrAccessibleWhenUnlockedThisDeviceOnly | 设备解锁 | 不包含在备份中 | 一次性配对握手数据 |
kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly 配合生物识别访问控制。密钥不会随 iCloud 备份迁移,即使设备丢失也不会泄露。实现流程与 API 调用链
步骤 1:生成 Secure Enclave 密钥对 —— 用于蓝牙长期身份标识。
let access = SecAccessControlCreateWithFlags(
kCFAllocatorDefault,
kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly,
.privateKeyUsage, // 允许私钥在 Secure Enclave 内使用
nil
)
let attributes: [String: Any] = [
kSecAttrKeyType: kSecAttrKeyTypeECSECPrimeRandom,
kSecAttrKeySizeInBits: 256,
kSecPrivateKeyAttrs: [
kSecAttrIsPermanent: true,
kSecAttrApplicationTag: "com.ble.identity.key".data(using: .utf8)!,
kSecAttrAccessControl: access!,
kSecAttrTokenID: kSecAttrTokenIDSecureEnclave
]
]
var error: Unmanaged?
guard let privateKey = SecKeyCreateRandomKey(attributes as CFDictionary, &error) else {
// 处理错误:检查是否在模拟器上运行或权利不足
throw KeychainError.secureEnclaveNotAvailable(error!.takeRetainedValue() as Error)
}
let publicKey = SecKeyCopyPublicKey(privateKey)
步骤 2:持久化蓝牙对称密钥 —— 加密的 BLE 报文数据。
let keyData = Data(...) // AES-CCM 或 AES-GCM 密钥
let addQuery: [String: Any] = [
kSecClass: kSecClassGenericPassword,
kSecAttrService: "com.ble.session",
kSecAttrAccount: peripheralIdentifier.uuidString,
kSecValueData: keyData,
kSecAttrAccessible: kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly
]
let status = SecItemAdd(addQuery as CFDictionary, nil)
guard status == errSecSuccess else {
throw KeychainError.addFailed(status)
}
步骤 3:查询与使用 —— 检索数据。
let query: [String: Any] = [
kSecClass: kSecClassGenericPassword,
kSecAttrService: "com.ble.session",
kSecAttrAccount: peripheralIdentifier.uuidString,
kSecReturnData: kCFBooleanTrue!,
kSecMatchLimit: kSecMatchLimitOne
]
var result: CFTypeRef?
let status = SecItemCopyMatching(query as CFDictionary, &result)
guard status == errSecSuccess, let keyData = result as? Data else {
throw KeychainError.copyFailed(status)
}
调试与常见错误排查
| 错误码 | 符号常量 | 常见原因 | 蓝牙场景解决方案 |
|---|---|---|---|
| -25300 | errSecItemNotFound | 查询条件过于严格或项目确实不存在 | 检查 kSecAttrService 是否匹配设备 ID |
| -25293 | errSecNotAvailable | 访问控制不满足(设备未解锁/未设置密码) | 确保在 UIApplicationStateActive 下访问 |
| -25299 | errSecDecode | 属性格式错误(例如 kSecAttrApplicationTag 不是 Data) | 严格使用 .data(using: .utf8) 转换 Tag |
| -34018 | errSecMissingEntitlement | App ID 未开启 Keychain Access Groups 权利 | 在 Capabilities 中启用 Keychain Sharing |
kSecAttrTokenID 会直接返回错误。必须使用条件编译来区分模拟器与真机:#if targetEnvironment(simulator)
// 使用软件密钥生成(仅用于开发调试)
let attributes: [String: Any] = [
kSecAttrKeyType: kSecAttrKeyTypeECSECPrimeRandom,
kSecAttrKeySizeInBits: 256
]
#else
// 使用包含 Secure Enclave 的完整属性(见上文)
#endif
部署与稳定性优化
1. 应用程序卸载后的 Keychain 持久化策略
默认情况下,Keychain 数据在应用卸载后保留。这有利于蓝牙设备重新安装时快速恢复配对(无需重新配对),但也可能带来残余数据风险。建议在应用退出、账户切换或擦除配对列表时显式调用 SecItemDelete。
2. 蓝牙扩展与 Keychain 共享
如果 App 包含 Widget 或 Watch 扩展,需要在 Capabilities 中开启 Keychain Sharing 并指定相同的 Access Group。查询时添加 kSecAttrAccessGroup 以支持组件间蓝牙会话共享:
let query: [String: Any] = [
kSecClass: kSecClassGenericPassword,
kSecAttrAccessGroup: "TEAMID.com.example.ble.shared",
// ... 其他查询参数
]
3. 性能与 UI 阻塞
Secure Enclave 的操作(密钥生成、签名)运行在专用协处理器上,延迟约为 10~100ms。应避免在主线程同步调用 SecKeyCreateRandomKey。建议在 App 启动初始化蓝牙管理器时,异步生成或从 Keychain 加载 SE 密钥:
DispatchQueue.global(qos: .userInitiated).async {
let privateKey = try KeychainService.loadOrCreateSecureEnclaveKey()
// 回到主线程更新蓝牙状态
DispatchQueue.main.async {
self.bluetoothManager.identityResolvingKey = privateKey
}
}
4. 数据保护与 Keychain 层级关联
对于 BLE 加密数据,可以使用 kSecUseDataProtectionKeychain 与文件级保护相结合。始终指定明确的 kSecAttrAccessible,避免使用默认值(默认值为 WhenUnlocked,在设备锁定且需要处理后台 BLE 报文时可能失败)。