一、简介
iOS 设备中的 keychain 为用户安全储存一些敏感数据,比如用户密码,认证令牌等。苹果自己用 keychain 来保存 Wi-Fi 密码,证书等等。Keychain 内的数据可以通过开启 keychain-sharing 功能,用于在同一 keychain group 下具有相同 TeamID 的 Apps 之间共享。
Keychain 所保存数据都加密过,内部是使用 sqlite 数据库来储存,位于 /private/var/Keychains/keychain-x.db。由于 keychain 的储存位置在 App 沙盒之外,所以用户删除 App 时,keychain 储存的数据并不会被删除。由于这种情形可能会导致一些用户隐私泄露问题,苹果表示不保证 keychain 数据在 App 删除之后一直存在,这种情况只是 keychain 储存数据的一些实现细节,而且苹果也曾在 iOS 10.3 的一些 Beta 版本修改过这一特性,最终可能是考虑实际影响比较大,并没有发布到正式版本。
其中一个常用的使用场景是储存用户登录命令牌,以期望用户卸载 App 再安装的时候能够直接登录。每当请求用户数据时,都需要带上登录命令牌,其中需要注意 keychain 数据的 IO 需要加解密,相对较耗性能,所以应尽量避免频繁直接读写 keychain 数据。
二、Keychain 基础
Keychain 中所储存的每条数据,官方称为 SecItem,对 SecItem 的操作需要有对应的 query 信息,这些 query 信息需要使用系统提供的 key,下面主要介绍以下几种常用类型:
-
表示储存数据类型的 key,一共包含两个,内容种类与数据储存类型
1)储存内容的种类: kSecClass,可选值有:kSecClassGenericPassword(普通密码)
kSecClassInternetPassword(互联网密码)
kSecClassCertificate(证书)
kSecClassKey(安全密钥)
kSecClassIdentity(标识符)通常我们使用的值是 kSecClassGenericPassword
3)对于 kSecClass 的值为 kSecClassGenericPassword 时,对应储存数据类型为 NSData,储存数据类型的 key 为 kSecValueData。对于其他的类型的操作有不同的 API 支持,目前只针对这种类型进行介绍。
-
Keychain 数据共享组:kSecAttrAccessGroup
kSecAttrAccessGroup(允许访问的Group):如果不设置则自动使用plist里设置的第一个,通常plist里会配置成TeamID+BundleID,既是appID前缀加应用打包设置的BundleID。
-
一种是 SecItem 详细属性的 key,主要包含 kSecAttrGeneric、kSecAttrAccount、kSecAttrService、kSecAttrAccessGroup
kSecAttrGeneric(一般属性):可以不设置,也可以设置,但不影响唯一性。
kSecAttrAccount(账号)
值是一个String,标志唯一的账号。(不设置则视为@””)。
kSecAttrService(服务):
值是一个String,标志唯一的服务。相当于与kSecAttrAccount一同看成一个联合主键,标志唯一的SecItem。存不同的内容应该使用不同的服务或账号。(不设置则视为@””) -
设置 SecItem 访问控制(添加时使用): kSecAttrAccessible ,如果增加用户授权设置的话则用 kSecAttrAccessControl(iOS 8 新增)
1)kSecAttrAccessible 可选值kSecAttrAccessibleWhenUnlocked (系统默认):设备解锁时
kSecAttrAccessibleAfterFirstUnlock:设备第一次解锁时
kSecAttrAccessibleAlways(不建议使用):任何时候
kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly:当前设备,设置密码时
kSecAttrAccessibleWhenUnlockedThisDeviceOnly:当前设备,解锁时
kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly:当前设备,首次解锁时
kSecAttrAccessibleAlwaysThisDeviceOnly:当前设备,任何时候
2)kSecAttrAccessControl 值为一个 SecAccessControlRef 对象,SecAccessControlRef 主要包含两个主要参数,一个是上面讲到的 kSecAttrAccessible,另外一个是 SecAccessControlCreateFlags 用于设置用户验证相关的权限
-
备份至 iCloud :kSecAttrSynchronizable
如果需要备份,则在添加 SecItem 时,对应的值设置为 @YES 。如果想同步到其他设备上也能使用,请避免使用DeviceOnly 设置或者其他设备相关控制权限。
三、使用
Keychain 数据的增删改查的主要 API:
```
/** 添加 */
OSStatus SecItemAdd(CFDictionaryRef attributes, CFTypeRef * __nullable CF_RETURNS_RETAINED result);
/** 查询 */
OSStatus SecItemCopyMatching(CFDictionaryRef query, CFTypeRef * __nullable CF_RETURNS_RETAINED result);
/** 更新 */
OSStatus SecItemUpdate(CFDictionaryRef query, CFDictionaryRef attributesToUpdate);
/** 删除 */
OSStatus SecItemDelete(CFDictionaryRef query);
```
query 信息
- (NSMutableDictionary *)queryWithAccount:(NSString *)account service:(NSString *)service {
NSMutableDictionary *dictionary = [NSMutableDictionary dictionaryWithCapacity:3];
// 设置储存的数据类型为「通用密码」
[dictionary setObject:(__bridge id)kSecClassGenericPassword forKey:(__bridge id)kSecClass];
// account 与 service 可以认为是联合主键
[dictionary setObject:service forKey:(__bridge id)kSecAttrService];
[dictionary setObject:account forKey:(__bridge id)kSecAttrAccount];
// kSecAttrAccessGroup 不设置则自动使用plist里设置的第一个
[dictionary setObject:@"com.xxxx.xx.accessgroup" forKey:(__bridge id)kSecAttrAccessGroup];
// 需要注意,如果设置为 YES, 怎在添加 SecItem 时,kSecAttrAccessible 就不要设置为 DeviceOnly
[dictionary setObject:@YES forKey:(__bridge id)(kSecAttrSynchronizable)];
return dictionary;
}
`
新增 SecItem
NSMutableDictionary* query = [self queryWithAccount: @"account" service:@"service"];
// 需要保存的内容
[query setObject:[@"password_xxx" dataUsingEncoding:NSUTF8StringEncoding] forKey:(__bridge id)kSecValueData];
// 设置访问权限(如果需要本地验证,则创建 SecAccessControlRef 对象,使用 kSecAttrAccessControl Key)
[query setObject:(__bridge id)kSecAttrAccessibleWhenUnlocked forKey:(__bridge id)kSecAttrAccessible];
// 设置与否不影响数据的唯一性
[query setObject:self.label forKey:(__bridge id)kSecAttrLabel];
// 插入数据
OSStatus s = SecItemAdd((__bridge CFDictionaryRef)query, NULL);
更新
// 指定需要更新的条目
NSMutableDictionary * searchQuery = [self queryWithAccount: @"account" service:@"service"];
// 需要更新的属性
NSMutableDictionary *update = [[NSMutableDictionary alloc]init];
[update setObject:self.passwordData forKey:(__bridge id)kSecValueData];
[update setObject:(__bridge id)kSecAttrAccessibleWhenUnlocked forKey:(__bridge id)kSecAttrAccessible];
status = SecItemUpdate((__bridge CFDictionaryRef)(update), (__bridge CFDictionaryRef)(query));
查询
CFTypeRef result = NULL;
NSMutableDictionary* query = [self queryWithAccount: @"account" service:@"service"];
// 需要返回值,如果只判断条目是否存在则不需要设置
[query setObject:@YES forKey:(__bridge id)kSecReturnData];
// 设置只需要一条结果
[query setObject:(__bridge id)kSecMatchLimitOne forKey:(__bridge id)kSecMatchLimit];
// 查找
status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &result);
if (statue == errSecSuccess) {
// 获取到的数据
NSData *passwordData = (__bridge_transfer NSData *)result;
}
删除
NSMutableDictionary* query = [self queryWithAccount: @"account" service:@"service"];
status = SecItemDelete((__bridge CFDictionaryRef)query);
// 注:Mac 上 keychain 储存机制不同,需要做额外处理
不同的 keychain 储存类型,以及对应的参数
iOS钥匙串KeyChain相关参数的说明
密匙类型:
键:
CFTypeRef kSecClass
值:
CFTypeRef kSecClassGenericPassword //一般密码
CFTypeRef kSecClassInternetPassword //网络密码
CFTypeRef kSecClassCertificate //证书
CFTypeRef kSecClassKey //密钥
CFTypeRef kSecClassIdentity //身份证书(带私钥的证书)
密钥串项属性
1.kSecClassGenericPassword //一般密码
属性:
kSecAttrAccessible //kSecAttrAccessiblein变量用来指定这条信息的保护程度
kSecAttrAccessGroup //密钥访问组
kSecAttrCreationDate //创建日期(read only)
kSecAttrModificationDate //最后一次修改日期
kSecAttrDescription //描述
kSecAttrComment //注释
kSecAttrCreator //创建者
kSecAttrType //类型
kSecAttrLabel //标签(给用户看)
kSecAttrIsInvisible //是否隐藏
kSecAttrIsNegative //是否具有密码
kSecAttrAccount //账户名
kSecAttrService //所具有服务
kSecAttrGeneric //用户自定义内容
2. kSecClassInternetPassword //网络密码
属性:
kSecAttrAccessible
kSecAttrAccessGroup
kSecAttrCreationDate
kSecAttrModificationDate
kSecAttrDescription
kSecAttrComment
kSecAttrCreator
kSecAttrType
kSecAttrLabel
kSecAttrIsInvisible
kSecAttrIsNegative
kSecAttrAccount
kSecAttrSecurityDomain
kSecAttrServer
kSecAttrProtocol //协议类型 CFNumberRef
kSecAttrAuthenticationType //认证类型 CFNumberRef
kSecAttrPort //网络端口
kSecAttrPath //访问路径
3.kSecClassCertificate //证书
属性:
kSecAttrAccessible
kSecAttrAccessGroup
kSecAttrLabel
kSecAttrCertificateType //证书类型
kSecAttrCertificateEncoding //证书编码类型
kSecAttrSubject //X.500主题名称
kSecAttrIssuer //X.500发行者名称
kSecAttrSerialNumber //序列号
kSecAttrSubjectKeyID //主题ID
kSecAttrPublicKeyHash //公钥Hash值
4.kSecClassKey//密钥
属性:
kSecAttrAccessible //变量用来指定这条信息的保护程度
kSecAttrAccessGroup //密钥存取群
kSecAttrKeyClass //加密密钥类
kSecAttrLabel //标签
kSecAttrApplicationLabel //标签(给程序使用) CFStringRef(通常是公钥的Hash值)
kSecAttrIsPermanent //是否永久保存加密密钥
kSecAttrApplicationTag //标签(私有标签数据)
kSecAttrKeyType //加密密钥类型(算法)
kSecAttrKeySizeInBits //密钥总位数 CFNumberRef
kSecAttrEffectiveKeySize //密钥有效位数 CFNumberRef
kSecAttrCanEncrypt //密钥是否可用于加密 CFBooleanRef
kSecAttrCanDecrypt //密钥是否可用于解密 CFBooleanRef
kSecAttrCanDerive //密钥是否可用于导出其他密钥 CFBooleanRef
kSecAttrCanSign //密钥是否可用于数字签名 CFBooleanRef
kSecAttrCanVerify //密钥是否可用于验证数字签名 CFBooleanRef
kSecAttrCanWrap //密钥是否可用于打包其他密钥 CFBooleanRef
kSecAttrCanUnwrap //密钥是否可用于解包其他密钥 CFBooleanRef
5.kSecClassIdentity //身份证书(带私钥的证书)
属性:
1.证书属性
2.私钥属性
密钥串项属性的值:
属性:
1.kSecAttrAccessible
CFTypeRef kSecAttrAccessibleWhenUnlocked; //解锁可访问,备份
CFTypeRef kSecAttrAccessibleAfterFirstUnlock; //第一次解锁后可访问,备份
CFTypeRef kSecAttrAccessibleAlways; //一直可访问,备份
CFTypeRef kSecAttrAccessibleWhenUnlockedThisDeviceOnly; //解锁可访问,不备份
CFTypeRef kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly; //第一次解锁后可访问,不备份
CFTypeRef kSecAttrAccessibleAlwaysThisDeviceOnly; //一直可访问,不备份
2.kSecAttrProtocol
略...
3.kSecAttrKeyClass //加密密钥类 CFTypeRef
CFTypeRef kSecAttrKeyClassPublic; //公钥
CFTypeRef kSecAttrKeyClassPrivate; //私钥
CFTypeRef kSecAttrKeyClassSymmetric; //对称密钥
返回值类型
可以同时指定多种返回值类型
CFTypeRef kSecReturnData; //返回数据(CFDataRef) CFBooleanRef
CFTypeRef kSecReturnAttributes; //返回属性字典(CFDictionaryRef) CFBooleanRef
CFTypeRef kSecReturnRef; //返回实例(SecKeychainItemRef, SecKeyRef, SecCertificateRef, SecIdentityRef, or CFDataRef) CFBooleanRef
CFTypeRef kSecReturnPersistentRef; //返回持久型实例(CFDataRef) CFBooleanRef
写入值类型
CFTypeRef kSecValueData;
CFTypeRef kSecValueRef;
CFTypeRef kSecValuePersistentRef;