keychain存储详解
Keychain是唯一一个存储安全可靠且不受应用卸载影响的存储方式
Keychain由四大项组成:
1. 条目类别kSecClass,条目类别是一个条目最基本的属性,每个存入keychain的条目,都需要为它定义一个类别,实际就是你用该条目存储什么样的数据
条目类别的具体值有以下这些:
kSecClassInternetPassword
kSecClassGenericPassword
kSecClassCertificate
kSecClassKey
kSecClassIdentity
— 如:我们用该条目存储普通密码,所以设置成kSecClassGenericPassword
[dictionary setObject:(id)kSecClassGenericPassword forKey:
(id)kSecClass];
2. 条目id:kSecAttrGeneric
条目id就是你存储在keychain的字段值,存储条目,必须设置条目id,条目id必须是NSData,不能是NSString;
NSString *password = @"MyPasswordForFtp";
NSData *itemID= [password dataUsingEncoding:NSUTF8StringEncoding];
[query setObject:itemID forKey:(id)kSecValueData];
3. 条目所属服务: kSecAttrService,
4. 条目所属账户名: kSecAttrAccount
— 其中kSecAttrService和kSecAttrAccount在整个keychain里必须唯一,不能重名,就行数据库中的主键,标识了你存储的条目,所以,一般kSecAttrService就是你的bundleID
Keychain存储的的系统库和API:
— Security.framework,该库内使用的是CoreFoundation下的对象,所以在使用的时候避免不了要使用__bridge桥接方法
— keychain操作的几个API大概的中心思想是,把key-value创建的字典(其中key主要是security库中特定的几个条目key)作为主体传入API(增删改查),内部通过特定的条目kSecAttrService,kSecAttrAccount等定位查询keychain存储的数据是否有匹配项,然后再通过key-value更新keychain中存储的条目
— 重点,先设置条目类别kSecClass, [queryDic setObject:(id)kSecClassGenericPassword forKey:(id)kSecClass];
— 再设置kSecAttrService,kSecAttrAccount,这样就能定位到具体条目了,
[queryDic setObject:account forKey:(id)kSecAttrAccount];
[queryDic setObject:accessGroup forKey:(id)kSecAttrAccessGroup];
1. keychain查询
OSStatus SecItemCopyMatching(CFDictionaryRef query, CFTypeRef * __nullable CF_RETURNS_RETAINED result)
1.1 使用SecItemCopyMatching进行查询,查询时,我们需要指明要查询条目的类别(kSecClass),条目的id(kSecAttrGeneric),条目所属的服务和账户(kSecAttrService,kSecAttrAccount)。此外,我们还可以设置其他一些查询条件,比如返回条目的数量(kSecMatchLimit),返回条目的数据类型,比如:
- kSecReturnData:返回条目所存储的数据,返回值类型是CFDataRef
- kSecReturnAttributes:返回该条目的属性,返回值是字典类型CFDictionaryRef
- kSecReturnRef:返回条目的引用,根据条目所属类别,返回值类型可能是:SecKeychainItemRef, SecKeyRef,SecCertificateRef, SecIdentityRef.
- kSecReturnPersistentRef:返回条目的引用,返回值类型是CFDataRef
// 设置查询条件字典
NSMutableDictionary *queryDic = [NSMutableDictionary dictionary];
//设置条目类别
[queryDic setObject:(id)kSecClassGenericPassword forKey:(id)kSecClass];
//设置条目所属的服务和账户,为了避免重名,我们使用常见的反转域名规则,比如com.mykeychain.password
NSString *account = @"com.mykeychain.password";
NSString *service = @"com.mykeychain.password";
[queryDic setObject:account forKey:(id)kSecAttrAccount];
[queryDic setObject:service forKey:(id)kSecAttrService];
//设置查询条件,只返回一个条目
[queryDic setObject:(id)kSecMatchLimitOne forKey:(id)kSecMatchLimit];
//设置查询条件,返回条目存储的数据(kSecReturnData==True)
[queryDic setObject:(id)kCFBooleanTrue forKey:(id)kSecReturnData];
//开始查询
CFTypeRef result;
OSStatus status = SecItemCopyMatching((CFDictionaryRef)dictionary , &result);
if (queryStatus == errSecSuccess) { //查询到数据,CF和OC的桥接
NSData *passwordData = (__bridge_transfer NSData*)result;
NSString *password = [[NSString alloc]initWithData:passwordData encoding:NSUTF8StringEncoding];
return password?:@"";
}
2. keychain添加数据
OSStatus SecItemAdd(CFDictionaryRef attributes, CFTypeRef * __null
2.1 进行条目添加,我们需要指明新条目的类别(kSecClass),新条目的id(kSecAttrGeneric),新条目所属的服务和账户(kSecAttrService,kSecAttrAccount),此外,还需要指明该条目所存储的数据(kSecValueData),否则,没有数据的条目就没有任何存入keychain的意义了
// 设置条件字典
NSMutableDictionary *queryDic = [NSMutableDictionary dictionary];
//设置条目类别
[queryDic setObject:(id)kSecClassGenericPassword forKey:(id)kSecClass];
//设置条目所属的服务和账户,为了避免重名,我们使用常见的反转域名规则,比如com.mykeychain.password
NSString *account = @"com.mykeychain.password";
NSString *service = @"com.mykeychain.password";
[queryDic setObject:account forKey:(id)kSecAttrAccount];
[queryDic setObject:service forKey:(id)kSecAttrService];
//设置条目id,必须是NSData类型,不能是NSString
NSString *password = @"1234567";
NSData *itemID= [password dataUsingEncoding:NSUTF8StringEncoding];
[queryDic setObject:itemID forKey:(id)kSecValueData];
//新增条目
OSStatus status = SecItemAdd((__bridge CFDictionaryRef)query, NULL);
if (status == errSecSuccess) {
NSLog(@"——新增成功");
}
3. keychain删除条目
OSStatus SecItemDelete(CFDictionaryRef query)
3.1 删除前先查询,以上已有查询示例,通常不需要获取查询结果的时候(比如删除),只需要定义基本查询条件即可
// 设置查询条件字典
NSMutableDictionary *queryDic = [NSMutableDictionary dictionary];
//设置条目类别
[queryDic setObject:(id)kSecClassGenericPassword forKey:(id)kSecClass];
//设置条目所属的服务和账户,为了避免重名,我们使用常见的反转域名规则,比如com.mykeychain.password
NSString *account = @"com.mykeychain.password";
NSString *service = @"com.mykeychain.password";
[queryDic setObject:account forKey:(id)kSecAttrAccount];
[queryDic setObject:service forKey:(id)kSecAttrService];
OSStatus queryStatus = SecItemCopyMatching((__bridge CFDictionaryRef)query, NULL);
if (queryStatus == errSecSuccess) { //查询到数据
OSStatus queryStatus = SecItemDelete((__bridge CFDictionaryRef)query);
if (queryStatus == errSecSuccess) {
NSLog(@"%@---删除成功",account);
}
}
4. keychain更新条目
OSStatus SecItemUpdate(CFDictionaryRef query, CFDictionaryRef attributesToUpdate)
4.1 更新前先查询,以上已有查询示例,通常不需要获取查询结果的时候(比如更新),只需要定义基本查询条件即可,查到条目后,更新条目id即可,
// 设置查询条件字典
NSMutableDictionary *queryDic = [NSMutableDictionary dictionary];
//设置条目类别
[queryDic setObject:(id)kSecClassGenericPassword forKey:(id)kSecClass];
//设置条目所属的服务和账户,为了避免重名,我们使用常见的反转域名规则,比如com.mykeychain.password
NSString *account = @"com.mykeychain.password";
NSString *service = @"com.mykeychain.password";
[queryDic setObject:account forKey:(id)kSecAttrAccount];
[queryDic setObject:service forKey:(id)kSecAttrService];
OSStatus queryStatus = SecItemCopyMatching((__bridge CFDictionaryRef)query, NULL);
if (queryStatus == errSecSuccess) { //查询到数据
//设置更新的条目id字典
NSMutableDictionary *passwordDic = [NSMutableDictionary dictionary];
NSData *itemID= [password dataUsingEncoding:NSUTF8StringEncoding];
[passwordDic setObject:itemID forKey:(id)kSecValueData];
OSStatus status = SecItemUpdate((__bridge CFDictionaryRef)query, (__bridge CFDictionaryRef)passwordDic);
if (status == errSecSuccess) {
NSLog(@"%s——更新成功",__func__);
}
}
OSStatus的常见code
- -50,可能是没有设置条目类别kSecClass,或者条目ID的数据类型不对等
其他对应枚举值,仔细查找或自行百度
CF_ENUM(OSStatus)
{
errSecSuccess = 0, /* No error. /
errSecUnimplemented = -4, / Function or operation not implemented. /
errSecDiskFull = -34, / The disk is full. /
errSecDskFull = -34,
errSecIO = -36, / I/O error. /
errSecOpWr = -49, / File already open with write permission. /
errSecParam = -50, / One or more parameters passed to a function were not valid. /
errSecWrPerm = -61, / Write permissions error. /
errSecAllocate = -108, / Failed to allocate memory. /
errSecUserCanceled = -128, / User canceled the operation. /
errSecBadReq = -909, / Bad parameter or invalid state for operation. */
ARC中的CF和OC类型相互转换
1. 在使用查询方法并获取结果的时候,需要传入CFTypeRef 类型的地址参数
SecItemCopyMatching(CFDictionaryRef query, CFTypeRef * __nullable CF_RETURNS_RETAINED result)
1.1 在MRC下,我们可以直接使用NSData *result = nil;
SecItemCopyMatching(CFDictionaryRef query, (CFTypeRef *)&result), 但是ARC下不允许如此使用,所以就得老实传入CFTypeRef类型指针
1.2 CFTypeRef result;SecItemCopyMatching(CFDictionaryRef query, &result),得到的结果又必须转为NSData类型,这个时候就需要CF和OC类型的转换了
1.3 ARC模式下CF类型与OC类型之间的转换有所了解,最常用的有两个转换关键字,
(__bridge type)expression 和 (__bridge_transfer Objective-C type)expression。(__bridge type)expression //type 为id 或者 void* , expression为带有CF前缀类型的变量或者 void* 变量。
如下所示
1、id obj = [[NSObject alloc] init];
void *p = (__bridge void *)obj;
id o = (__bridge id)p;
2、CGImageRef cgimage
self.layer.contents = (__bridge id)cgimage;
(__bridge_transfer Objective-C type)expression //type为OC类型 expression 可以是带有CF前缀的类型变量 。
3、
CFTypeRef result;
NSData passwordData = (__bridge_transfer NSData)result;
搞定了