写在前面:
- 最近因为做内购,需要保存交易过程中的一些信息,所以用到了KeyChain。在此做一下简单的总结和记录。
KeyChain是什么
-
keychain是一个加密的容器,通常用来保存密码,证书,和一些需要加密的key。对于iOS应用来说,每个APP有独立的keychain。苹果自己用keychain来保存Wi-Fi网络密码,VPN凭证等等。它是一个在所有app之外的sqlite数据库。
-
通俗点来说 keychain相当于银行的一个金库,keychain Item就相当于金库里的一个个保险柜,保存在keychain Item里面的密码就相当于保险柜里面存放的金条。
-
注意 :keyChain的访问权限依赖于provisioning file。所以,如果要在应用更新的时候,仍然能够访问之前保存的密码,要保证provisioning file是同一个文件。
KeyChain 结构组成
-
keyChain是通过字典来描述的,是一组key-value的对。用来描述这个keyChain是为什么样的应用保存什么样的数据,有什么样的访问权限等等。
-
Keychain内部可以保存很多的信息。每条信息作为一个单独的keychain item,keychain item一般为一个字典,每条keychain item包含一条data和很多attributes。
如图 keychainItem.png
1.组成部分由 {N个标签(属性) + 一个重要数据} 组成!
2.@{@"属性key1":@"属性值1",@"属性keyN":@"属性值N",@"valueData":@数据}
-- kSecClass 表示存储数据的类型,kSecClassGenericPassword代表密码
-- kSecAttrAccount 表示的是为IamUser这个账号存储的密码
-- kSecAttrService 表示是为App Store 这个歌APP存储的账号
-- 其余两个在查询的时候使用,知道如果要查询都设为ture就可以了
-
如果把keychain item的类型指定为需要保护的类型比如password或者private key,item的data会被加密并且保护起来,如果把类型指定为不需要保护的类型,比如certificates,item的data就不会被加密。
item可以指定为以下几种类型:
kSecClassGenericPassword //指定通用密码项目
kSecClassInternetPassword //指定Internet密码项目
kSecClassCertificate //指定证书项目
kSecClassKey //指定关键项目
kSecClassIdentityOSX_AVAILABLE_STARTING(MAC_10_7, __IPHONE_2_0);//指定标识项 -
注意:keychain虽然是可以保存15000条item,每条50个attributes,但是苹果工程师建议最好别放那么多,存几千条密码,几千字节没什么问题。
代码实现讲解
- 1.对于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)
--p 这些API的关键在于1.是理解和配置好这个操作字典 2.注意返回的OSStatus 状态 3.CF对象与OC 之间的bridge - 2.查找走一波
-- (关键)先配置一个操作字典:
kSecAttrService(属性),kSecAttrAccount(属性) 这些属性or标签是查找的依据
kSecReturnData(值为@YES 表明返回类型为data),kSecClass(值为kSecClassGenericPassword 表示重要数据为“一般密码”类型) 这些限制条件是返回结果类型的依据
-- 然后用查找的API 得到查找状态和返回数据(密码)
-- 最后如果状态成功那么将数据(密码)转换成string 返回
//用原生的API 实现查询密码
- (NSString *)passwordForService:(nonnull NSString *)service account:(nonnull NSString *)account{
//生成一个查询用的 可变字典
NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithCapacity:4];
[dict setObject:(__bridge id)kSecClassGenericPassword forKey:(__bridge id)kSecClass]; //表明为一般密码可能是证书或者其他东西
[dict setObject:service forKey:(__bridge id)kSecAttrService]; //输入service
[dict setObject:account forKey:(__bridge id)kSecAttrAccount]; //输入account
[dict setObject:@YES forKey:(__bridge id)kSecReturnData]; //返回Data
//查询
OSStatus status = -1;
CFTypeRef result = NULL;
status = SecItemCopyMatching((__bridge CFDictionaryRef)dict,&result);//核心API 查找是否匹配 和返回密码!
if (status != errSecSuccess) { //判断状态
return nil;
}
//返回数据
NSString *password = [[NSString alloc] initWithData:(__bridge_transfer NSData *)result encoding:NSUTF8StringEncoding];//转换成string
return password;
}
- 添加或更新:
-- 说明:当添加的时候我们一般需要判断一下当前钥匙串里面是否已经存在我们要添加的钥匙。如果已经存在我们就更新好了,不存在再添加,所以这两个操作一般写成一个函数搞定吧。
-- 过程关键:
1.检查是否已经存在 构建的查询用的操作字典:kSecAttrService,kSecAttrAccount,kSecClass(标明存储的数据是什么类型,值为kSecClassGenericPassword 就代表一般的密码)
2.添加用的操作字典: kSecAttrService,kSecAttrAccount,kSecClass,kSecValueData
3.更新用的操作字典1(用于定位需要更改的钥匙):kSecAttrService,kSecAttrAccount,kSecClass; 操作字典2(新信息)kSecAttrService,kSecAttrAccount,kSecClass ,kSecValueData
//用原生的API 添加一条钥匙
-(BOOL)addItemWithService:(NSString *)service account:(NSString *)account password:(NSString *)password{
//先查查是否已经存在
//构造一个操作字典用于查询
NSMutableDictionary *searchDict = [[NSMutableDictionary alloc]initWithCapacity:4];
[searchDict setObject:service forKey:(__bridge id)kSecAttrService]; //标签service
[searchDict setObject:account forKey:(__bridge id)kSecAttrAccount]; //标签account
[searchDict setObject:(__bridge id)kSecClassGenericPassword forKey:(__bridge id)kSecClass];//表明存储的是一个密码
OSStatus status = -1;
CFTypeRef result =NULL;
status = SecItemCopyMatching((__bridge CFDictionaryRef)searchDict, &result);
if (status == errSecItemNotFound) { //没有找到则添加
NSData *passwordData = [password dataUsingEncoding:NSUTF8StringEncoding]; //把password 转换为 NSData
[searchDict setObject:passwordData forKey:(__bridge id)kSecValueData]; //添加密码
status = SecItemAdd((__bridge CFDictionaryRef)searchDict, NULL); //!!!!!关键的添加API
}else if (status == errSecSuccess){ //成功找到,说明钥匙已经存在则进行更新
NSData *passwordData = [password dataUsingEncoding:NSUTF8StringEncoding]; //把password 转换为 NSData
NSMutableDictionary *dict = [[NSMutableDictionary alloc] initWithDictionary:searchDict];
[dict setObject:passwordData forKey:(__bridge id)kSecValueData]; //添加密码
status = SecItemUpdate((__bridge CFDictionaryRef)searchDict, (__bridge CFDictionaryRef)dict);//!!!!关键的更新API
}
return (status == errSecSuccess);
}
4.删除
-- 说明:删除同样也是先配置好 一个字典,然后调用删除API
- (BOOL)deleteObjectForService:(NSString *)serviceName account:(NSString *)account{
//构造一个操作字典用于删除
NSMutableDictionary *deleteDict = [[NSMutableDictionary alloc]initWithCapacity:4];
[deleteDict setObject:service forKey:(__bridge id)kSecAttrService]; //标签service
[deleteDict setObject:account forKey:(__bridge id)kSecAttrAccount]; //标签account
[deleteDict setObject:(__bridge id)kSecClassGenericPassword forKey:(__bridge id)kSecClass];//表明存储的是一个密码
OSStatus status = SecItemDelete((__bridge CFDictionaryRef)dict);//核心API 删除数据!
return (status == errSecSuccess);
}
网友评论