1. keychain概述
1.1 keychain是什么
苹果官网对钥匙串的描述
iOS keychain 是一个相对独立的空间,是用SQLite进行存储的,可以加密我们保存的数据,并且使用keychain service API增删改查。
keychain的是以item为单位存储的。data是数据本身,attributes就是数据库中的键。
1.2 keychain的优点
相对于NSUserDefaults、plist文件保存等一般方式,keychain有以下优点
-
keychain钥匙串中的信息不会因为卸载/重装app而丢失
-
keychain保存更为安全
1.3 keychain的用途
-
保存密码、证书、设备唯一码
-
历史数据
例子:keychain保存did和登录历史数据。
keychain适合保存一些比较小的数据量的数据,如果要保存大的数据,可以考虑文件的形式存储在磁盘上,在keychain里面保存解密这个文件的密钥。
2. keychain的基本使用
2.1 keychain item 的类型和主键
kSecClass可以有以下取值
-
kSecClassGenericPassword //普通密码
-
kSecClassInternetPassword //互联网密码
-
kSecClassCertificate//证书
-
kSecClassKey//加密的内容
-
kSecClassIdentity //身份相关的
每种类型的Keychain item都有不同的键作为主要的Key也就是唯一标示符用于搜索,更新和删除
类型为GenericPassword的item必须使用kSecAttrAccount和kSecAttrService作为主要的key
item类型 | 可指定的attributes |
kSecClassInternetPassword | kSecAttrAccess (OS X only) kSecAttrAccessControl kSecAttrAccessGroup (iOS; also OS X if kSecAttrSynchronizable and/or kSecUseDataProtectionKeychain set) kSecAttrAccessible (iOS; also OS X if kSecAttrSynchronizable and/or kSecUseDataProtectionKeychain set) kSecAttrCreationDate kSecAttrModificationDate kSecAttrDescription kSecAttrComment kSecAttrCreator kSecAttrType kSecAttrLabel kSecAttrIsInvisible kSecAttrIsNegative kSecAttrAccount kSecAttrSecurityDomain kSecAttrServer kSecAttrProtocol kSecAttrAuthenticationType kSecAttrPort kSecAttrPath kSecAttrSynchronizable |
kSecClassGenericPassword | kSecAttrAccess (OS X only) kSecAttrAccessControl kSecAttrAccessGroup (iOS; also OS X if kSecAttrSynchronizable and/or kSecUseDataProtectionKeychain set) kSecAttrAccessible (iOS; also OS X if kSecAttrSynchronizable and/or kSecUseDataProtectionKeychain set) kSecAttrCreationDate kSecAttrModificationDate kSecAttrDescription kSecAttrComment kSecAttrCreator kSecAttrType kSecAttrLabel kSecAttrIsInvisible kSecAttrIsNegative kSecAttrAccount kSecAttrService kSecAttrGeneric kSecAttrSynchronizable |
kSecClassCertificate | kSecAttrAccessible (iOS only) kSecAttrAccessControl (iOS only) kSecAttrAccessGroup (iOS only) kSecAttrCertificateType kSecAttrCertificateEncoding kSecAttrLabel kSecAttrSubject kSecAttrIssuer kSecAttrSerialNumber kSecAttrSubjectKeyID kSecAttrPublicKeyHash kSecAttrSynchronizable |
attributes | 取值及意义 |
kSecAttrAccessible(keychain item保护等级) | 1. kSecAttrAccessibleWhenUnlocked //keychain项受到保护,只有在设备未被锁定时才可以访问 2. kSecAttrAccessibleAfterFirstUnlock //keychain项受到保护,直到设备启动并且用户第一次输入密码 3. kSecAttrAccessibleAlways //keychain未受保护,任何时候都可以访问 (Default) 4.kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly //keychain项受到保护,要求在使用时用户设定了密码,而且不可以转移到其他设备 5. kSecAttrAccessibleWhenUnlockedThisDeviceOnly //keychain项受到保护,只有在设备未被锁定时才可以访问,而且不可以转移到其他设备 6. kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly //keychain项受到保护,直到设备启动并且用户第一次输入密码,而且不可以转移到其他设备 7.kSecAttrAccessibleAlwaysThisDeviceOnly //keychain未受保护,任何时候都可以访问,但是不能转移到其他设备 |
2.2 keychain的增删改查
keychain的增删改查API
增删改查
SecItemAdd
SecItemDelete
SecItemUpdate
SecItemCopyMatching
OSStatus SecItemAdd(CFDictionaryRef attributes, CFTypeRef * __nullable CF_RETURNS_RETAINED result)
OSStatus SecItemDelete(CFDictionaryRef query)
OSStatus SecItemUpdate(CFDictionaryRef query, CFDictionaryRef attributesToUpdate)
OSStatus SecItemCopyMatching(CFDictionaryRef query, CFTypeRef * __nullable CF_RETURNS_RETAINED result)
增
要增加一个keychainItem,要指定其kSecClass(item类型),attributes(属性)和data(存储的数据)
Keychain内部不允许添加重复的Item。如果已存在同样的item,调用SecItemAdd会返回错误码
增加一个keychain item的例子
NSMutableDictionary *keychainQuery = [NSMutableDictionary dictionaryWithDictionary:@{
(__bridge id)kSecClass : (__bridge id)kSecClassGenericPassword,
(__bridge id)kSecAttrService : @"kuaishou.com",
(__bridge id)kSecAttrAccount : @"your user name",
(__bridge id)kSecAttrAccessible : (__bridge id)kSecAttrAccessibleWhenUnlocked
(__bridge
id)kSecValueData : @"your password"
}];
OSStatus status = SecItemAdd((__bridge CFDictionaryRef)keychainQuery, NULL);
删
要删除一个keychainItem,要指定其kSecClass(item类型),attributes(属性)。
要注意的是尽量用多个字段确定这个item,防止删除了其他item。
NSDictionary *query = @{
(__bridge id)kSecClass : (__bridge id)kSecClassGenericPassword,
(__bridge id)kSecAttrService : @"kuaishou.com",
(__bridge id)kSecAttrAccount : @"your user name"
};
OSStatus status = SecItemDelete((__bridge CFDictionaryRef)query);
改
改query这个item的某些属性,要改的属性定义在update中
NSDictionary *query = @{
(__bridge id)kSecClass : (__bridge id)kSecClassGenericPassword,
(__bridge id)kSecAttrAccount : @"kuaishou.com",
(__bridge id)kSecAttrService : @"your user name",
};
NSDictionary *update = @{
(__bridge id)kSecValueData : [@"654321" dataUsingEncoding:NSUTF8StringEncoding],
};
OSStatus status = SecItemUpdate((__bridge CFDictionaryRef)query, (__bridge CFDictionaryRef)update);
查
查询除了要指定kSecClass,kSecAttrAccount,kSecAttrService,还要指定查询结果的条数限制和查询返回的数据
指定查询结果的条数限制 kSecMatchLimit | 1.kSecMatchLimitOne //返回搜索到的第一个item(default) 2.kSecMatchLimitAll //返回搜索到的所有item |
指定查询返回的数据 kSecReturnData kSecReturnAttributes kSecReturnRef kSecReturnPersistentRef | 如果指定为kCFBooleanTrue,返回分别是 1.对于key或者password item,返回它们的加密的data 2.返回item不加密的attributes 3.返回item的引用 4.返回item的持久引用 |
NSDictionary *query = @{
(__bridge id)kSecClass : (__bridge id)kSecClassGenericPassword,
(__bridge id)kSecAttrService : @"kuaishou.com",
(__bridge id)kSecAttrAccount : @"your user name",
(__bridge id)kSecReturnData : @YES,
(__bridge id)kSecMatchLimit : (__bridge id)kSecMatchLimitOne,
};
CFTypeRef dataTypeRef = NULL;//存返回的结果
OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &dataTypeRef);
3.钥匙串共享
同一个开发者账号下(teamID),各个应用之间可以共享item。keychain通过keychain-access-groups来进行访问权限的控制。
在Xcode的Capabilities选项中打开Keychain Sharing即可。
每个group命名开头必须是开发者账号的teamId。
如果有多个sharedGroup,在添加的时候如果不指定,默认是第一个group。
(__bridge id)kSecAttrAccessGroup : @"your_teamId..xxx.xxx"
group也可以置为nil,这样默认也会以自己的bundleID作为Group。
.新建一个Plist文件,在Plist中的数组中添加可以访问的条目的名字(如KeychainAccessGroups.plist),并在Build-setting中进行entitlemment配置
4.keychain三方库介绍
直接调钥匙串API比较麻烦,大多数场景的需求都是键值对形式的调用。
4.1.KeychainItemWrapper,苹果原生封装的库
接口
- (id)initWithIdentifier: (NSString *)identifier accessGroup:(NSString *)accessGroup;
- (id)initWithIdentifier: (NSString *)identifier accessGroup:(NSString *)accessGroup accessible:(CFStringRef)accessible;
- (void)setObject:(id)inObject forKey:(id)key onError:(void(^)(SInt32 result))onError;
- (void)resetKeychainItem;
KeychainItemWrapper创建对象时会创建一个字典,用于记录钥匙串的信息,首先会指定查询的query
(__bridge id)kSecClass : (__bridge id)kSecClassGenericPassword,
(__bridge id)kSecAttrGeneric : identifier,
(__bridge id)kSecReturnAttributes:(__bridge id)kCFBooleanTrue
调用SecItemCopyMatching查找是否存在相关字段,
不存在,这个字典的attributes就赋值为空字符串,
存在就再调用SecItemCopyMatching查找data 的值存在字典中
再查询的时候就直接从这个字典取值
setObject存之前,会调用SecItemCopyMatching查找是否存过,已经存在会比较新的data和旧的data是否相同,不同则调用SecItemUpdate更新,这也是苹果推荐的做法
缺点:初始化可能会调用两次SecItemCopyMatching查询,初始化后再setObject,就有4步操作了,可能会比较耗时
使用默认kSecAttrAccessible, KeychainItemWrapper会出现很多-25308的错误//User interaction is not allowed
KeychainItemWrapper写成一个单例,能得到优化
4.2.JNKeychain
接口
+ (BOOL)saveValue:(id)value forKey:(NSString*)key;
+ (BOOL)saveValue:(id)value forKey:(NSString*)key forAccessGroup:(NSString *)group;
+ (BOOL)saveValue:(id)value forKey:(NSString*)key forAccessGroup:(NSString *)group accessible:(CFStringRef)accessible;
+ (BOOL)deleteValueForKey:(NSString *)key;
内部指定item如下
(__bridge id)kSecClass : (__bridge id)kSecClassGenericPassword,
(__bridge id)kSecAttrService : key,
(__bridge id)kSecAttrAccount : key,
(__bridge id)kSecValueData : value的data形式
为求简单先调一遍SecItemDelete再SecItemAdd
缺点:JNKeychain如果存储的kSecAttrAccessible变了,要先deleteValueForKey之前的kSecAttrAccessible
4.3..SFHFKeychainUtils
接口
+ (NSString *) getPasswordForUsernameOld: (NSString *) username
andServiceName: (NSString *) serviceName
error: (NSError **) error;
+ (NSString *) getPasswordForUsernameV2: (NSString *) username
andServiceName: (NSString *) serviceName
error: (NSError **) error;
+ (BOOL) storeUsername: (NSString *) username
andPassword: (NSString *) password
forServiceName: (NSString *) serviceName
updateExisting: (BOOL) updateExisting
error: (NSError **) error;
+ (BOOL) deleteItemForUsername: (NSString *) username
andServiceName: (NSString *) serviceName
error: (NSError **) error
withAccessible: (BOOL) hasAccessible;
SFHFKeychainUtils和KeychainItemWrapper类似,存密码之前,会调用SecItemCopyMatching查找是否存过,已经存在会比较新的data和旧的data是否相同,不同则调用SecItemUpdate更新。
但不需要初始化,因此存密码效率更高。
缺点:不能指定kSecAttrAccessible属性,默认kSecAttrAccessibleAlways
5. 注意的点
5.1.同样的secClass、account、service,但是存储的kSecAttrAccessible变了,需要deleteValueForKey之前的kSecAttrAccessible对应的item,否则不能存储新的item
5.2.KeychainItemWrapper主线程调用,低端机上长卡顿
5.3 Kechain item字典内添加自定义key时会出现参数不合法的错误。
5.4设备越狱后相当于对苹果做签名检查的地方打了个补丁,伪造一个证书的app也能正常使用,并且加上Keychain Dumper这些工具获取Keychain内的信息会非常容易。