第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+已废弃,部分旧值实际映射到其他等级,请勿在新建项目中使用。
iOS Keychain 数据保护等级层次 iOS Keychain 硬件加密 · 应用沙箱隔离 数据保护类 辅助存取等级 擦除策略 WhenUnlocked AfterFirstUnlock ThisDeviceOnly 备份限制 选择原则 1. 蓝牙长期密钥 → WhenUnlocked + ThisDeviceOnly 2. 避免使用 Always 等级

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 实现流程:存储与读取蓝牙加密密钥

  1. 定义服务标识与账户:使用反转域名作为kSecAttrService,设备标识作为kSecAttrAccount。
  2. 构建添加字典:设置kSecAttrAccessible为WhenUnlockedThisDeviceOnly。
  3. 调用SecItemAdd:检查返回errSecDuplicateItem,若存在则使用SecItemUpdate覆盖。
  4. 构建查询字典:添加kSecReturnData: @YES,kSecMatchLimit: kSecMatchLimitOne。
  5. 调用SecItemCopyMatching:获取结果后转换为NSData。
  6. 错误处理:处理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。