第1章 Keychain基础概念与数据保护等级
1.1 Keychain概述与数据保护模型
iOS Keychain是系统级安全存储容器,专门用于保存敏感数据(如加密密钥、证书、密码)。与UserDefaults不同,Keychain数据经过硬件级加密,且在不同应用之间通过访问组(Access Group)隔离或共享。其核心保护机制基于数据保护等级(Data Protection Class),控制数据在设备锁定状态下的可访问性。
| 数据保护等级 | kSecAttrAccessible值 | 描述 |
|---|---|---|
| 完全保护 | kSecAttrAccessibleWhenUnlocked | 仅设备解锁时可读写 |
| 首次解锁后保护 | kSecAttrAccessibleAfterFirstUnlock | 设备首次解锁后持续可读写 |
| 始终保护 | kSecAttrAccessibleAlways | 始终可读写(已弃用,不推荐) |
| 受限保护 | kSecAttrAccessibleWhenUnlockedThisDeviceOnly | 设备解锁时可读,且无法通过备份迁移 |
| 首次解锁后设备限定 | kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly | 首次解锁后持续可读,禁止备份迁移 |
选择设备限定变体(ThisDeviceOnly)可防止iCloud备份泄露蓝牙加密密钥,是安全最佳实践。
设计建议:存储蓝牙长期加密密钥(如LTK、IRK)时,优先使用
kSecAttrAccessibleWhenUnlockedThisDeviceOnly,在保证后台蓝牙操作安全的同时限制备份范围。
关键限制:
kSecAttrAccessibleAlways在iOS 13+已废弃,部分旧值实际映射到其他等级,请勿在新建项目中使用。
1.2 SecItemAdd与SecItemCopyMatching基础
Keychain操作通过Security框架的C函数完成,核心包括添加、查询、更新、删除。两个最常调用的API:
- SecItemAdd:将包含属性(
kSecClass、kSecAttrAccessible、kSecValueData等)的字典写入Keychain。 - SecItemCopyMatching:根据查询字典检索已存储的条目,返回CFTypeRef(通常为CFDataRef)。
核心参数:
kSecClass指定条目类型,常用kSecClassGenericPassword(通用密码)存储二进制密钥。
// 存储蓝牙密钥示例
NSDictionary *addQuery = @{
(__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
(__bridge id)kSecAttrService: @"com.example.bluetooth.keys",
(__bridge id)kSecAttrAccount: @"paired_device_ltk",
(__bridge id)kSecAttrAccessible: (__bridge id)kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
(__bridge id)kSecValueData: keyData // NSData格式的加密密钥
};
OSStatus status = SecItemAdd((__bridge CFDictionaryRef)addQuery, NULL);
if (status == errSecSuccess) {
// 成功
} else if (status == errSecDuplicateItem) {
// 已存在,需先删除或更新
}
// 读取蓝牙密钥示例
NSDictionary *getQuery = @{
(__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
(__bridge id)kSecAttrService: @"com.example.bluetooth.keys",
(__bridge id)kSecAttrAccount: @"paired_device_ltk",
(__bridge id)kSecReturnData: @YES,
(__bridge id)kSecMatchLimit: (__bridge id)kSecMatchLimitOne
};
CFTypeRef result = NULL;
OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)getQuery, &result);
if (status == errSecSuccess) {
NSData *retrievedKey = (__bridge_transfer NSData *)result;
// 使用密钥
}
1.3 实现流程:存储与读取蓝牙加密密钥
- 定义服务标识与账户:使用反转域名作为
kSecAttrService,设备标识作为kSecAttrAccount。 - 构建添加字典:设置
kSecAttrAccessible为WhenUnlockedThisDeviceOnly。 - 调用SecItemAdd:检查返回
errSecDuplicateItem,若存在则使用SecItemUpdate覆盖。 - 构建查询字典:添加
kSecReturnData: @YES,kSecMatchLimit: kSecMatchLimitOne。 - 调用SecItemCopyMatching:获取结果后转换为NSData。
- 错误处理:处理
errSecItemNotFound(未找到)和errSecNotAvailable(设备锁定)。
1.4 调试与常见错误
| 错误码 | 常见原因 | 解决方案 |
|---|---|---|
errSecDuplicateItem | 相同服务+账户的条目已存在 | 先调用SecItemDelete或SecItemUpdate |
errSecItemNotFound | 查询条件无匹配 | 检查kSecAttrService/kSecAttrAccount拼写 |
errSecNotAvailable | 设备锁定且Accessible等级受限 | 改用AfterFirstUnlock等级或推迟操作 |
errSecAuthFailed | 设备解锁失败或未授权 | 确保应用允许访问Keychain(无需额外权限) |
调试技巧:使用NSLog输出OSStatus,并对照SecBase.h中的定义。
// 快速删除所有测试Keychain条目(调试用)
NSDictionary *deleteQuery = @{
(__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
(__bridge id)kSecAttrService: @"com.example.bluetooth.keys"
};
SecItemDelete((__bridge CFDictionaryRef)deleteQuery);
1.5 性能与稳定性优化
- 批量操作合并:不要频繁单次读/写密钥,优先在配对成功后一次性存储所有蓝牙Key。
- 使用后台访问:蓝牙后台重连时需要读取密钥,选择
AfterFirstUnlockThisDeviceOnly可避免因屏幕锁定导致读取失败。 - 缓存避免重复查询:在内存中维护一个
NSDictionary缓存已读取的Keychain数据,仅在启动或设备解锁后刷新。 - 异步调用:Keychain操作是同步且可能阻塞(约1-5ms),在后台队列执行,避免主线程卡顿。
- 错误重试:对于
errSecNotAvailable,可监听UIApplicationProtectedDataDidBecomeAvailable通知后重试。
性能基准:iOS 16+设备上,一次
SecItemCopyMatching平均耗时0.8ms,SecItemAdd约1.2ms。合理缓存可将后续读取降至0.05ms。