iOS keychain 主要是用来保存一些用户敏感数据。比如用户密码,token。keychain是用SQLite进行存储的。用苹果的话来说是一个专业的数据库,加密我们保存的数据,可以通过metadata(attributes)进行高效的搜索。keychain适合保存一些比较小的数据量的数据,如果要保存大的数据,可以考虑文件的形式存储在磁盘上,在keychain里面保存解密这个文件的密钥。
keychain的类型
- kSecClassGenericPassword
- kSecClassInternetPassword
- kSecClassCertificate
- kSecClassKey
- kSecClassIdentity
这5个类型只是对应于不同的item,存储的属性有区别,使用上都是一样的。
不同类型对应的属性:
既然苹果是采用SQLite去存储的,那么以上这些不同item的attribute可以理解是数据库里面表的字段。那么对keychain的操作其实也就是普通数据库的增删改查了。这样也许就会觉得那些API也没那么难用了。
增
NSDictionary *query = @{(__bridge id)kSecAttrAccessible : (__bridge id)kSecAttrAccessibleWhenUnlocked,
(__bridge id)kSecClass : (__bridge id)kSecClassGenericPassword,
(__bridge id)kSecValueData : [@"1234562" dataUsingEncoding:NSUTF8StringEncoding],
(__bridge id)kSecAttrAccount : @"account name",
(__bridge id)kSecAttrService : @"loginPassword",
};
CFErrorRef error = NULL;
OSStatus status = SecItemAdd((__bridge CFDictionaryRef)query, nil);
以这个添加kSecClassGenericPassword item为例,在字典里面我们设置了以下几个属性:获取权限为当设备处于未锁屏状态,item的类型为kSecClassGenericPassword,item的value为@"123456", item的账户名为@"account name", item的service为@"loginPassword"。最后,调用SecItemAdd进行插入。使用上有点像CoreData。
删
NSDictionary *query = @{
(__bridge id)kSecClass : (__bridge id)kSecClassGenericPassword,
(__bridge id)kSecAttrService : @"loginPassword",
(__bridge id)kSecAttrAccount : @"account name"
};
OSStatus status = SecItemDelete((__bridge CFDictionaryRef)query);
删除同样也是指定之前存的item的属性,最后调用SecItemDelete这个方法。这边要注意的是劲量用多个字段确定这个item,(虽然平常开发都可能是唯一)防止删除了其他item;比如我们把kSecAttrAccount这个属性去掉,那么将会删除所有的kSecAttrService对应value为@"loginPassword"的item;
改
NSDictionary *query = @{(__bridge id)kSecClass : (__bridge id)kSecClassGenericPassword,
(__bridge id)kSecAttrAccount : @"account name",
(__bridge id)kSecAttrService : @"loginPassword",
};
NSDictionary *update = @{
(__bridge id)kSecValueData : [@"654321" dataUsingEncoding:NSUTF8StringEncoding],
};
OSStatus status = SecItemUpdate((__bridge CFDictionaryRef)query, (__bridge CFDictionaryRef)update);
苹果推荐我们用SecItemUpdate去修改一个已经存在的item,可能我们喜欢先调用SecItemDelete方法去删除,再添加一个新的。这个主要目的是防止新添的item丢失了原来的部分属性。这个方法需要两个入参,一个字典是用来指定要更新的item,另一个字典是想要更新的某个属性的value,最后调用SecItemUpdate。
查
NSDictionary *query = @{(__bridge id)kSecClass : (__bridge id)kSecClassGenericPassword,
(__bridge id)kSecReturnData : @YES,
(__bridge id)kSecMatchLimit : (__bridge id)kSecMatchLimitOne,
(__bridge id)kSecAttrAccount : @"account name",
(__bridge id)kSecAttrService : @"loginPassword",
};
CFTypeRef dataTypeRef = NULL;
OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &dataTypeRef);
if (status == errSecSuccess) {
NSString *pwd = [[NSString alloc] initWithData:(__bridge NSData * _Nonnull)(dataTypeRef) encoding:NSUTF8StringEncoding];
NSLog(@"==result:%@", pwd);
}
查和前面几个操作类似,首先同样是指定属性定位到这个item,最后调用SecItemCopyMatching方法。既然是数据库查询,肯定会有记录的条数的问题。本例中使用了kSecMatchLimitOne,表示返回结果集的第一个,当然这个也是默认的。如果是查询出多个,kSecMatchLimitAll可以使用这个,那么返回的将是个数组。SecItemCopyMatching方法的入参dataTypeRef,是一个返回结果的引用,会根据不同的item,返回对应不同的类型(如NSCFData, NSCFDictionary, NSCFArray等等)。
刚刚上面是返回存储的value的引用,如果我们想看看这个item所有的属性怎么办?我们可以使用kSecReturnRef
NSDictionary *query = @{(__bridge id)kSecClass : (__bridge id)kSecClassGenericPassword,
(__bridge id)kSecReturnRef : @YES,
(__bridge id)kSecReturnData : @YES,
(__bridge id)kSecMatchLimit : (__bridge id)kSecMatchLimitOne,
(__bridge id)kSecAttrAccount : @"account name",
(__bridge id)kSecAttrService : @"noraml",
};
CFTypeRef dataTypeRef = NULL;
OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &dataTypeRef);
if (status == errSecSuccess) {
NSDictionary *dict = (__bridge NSDictionary *)dataTypeRef;
NSString *acccount = dict[(id)kSecAttrAccount];
NSData *data = dict[(id)kSecValueData];
NSString *pwd = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSString *service = dict[(id)kSecAttrService];
NSLog(@"==result:%@", dict);
}
这样,我们就得到了这个item的所有属性。
Sharing Items
通一个开发者账号下,各个应用之间可以共享item。keychain通过keychain-access-groups
来进行访问权限的控制。在Xcode的Capabilities选项中打开Keychain Sharing即可。
每个group命名开头必须是开发者账号的teamId。不同开发者账号的teamId是唯一的,所以苹果限制了只有同一个开发者账号下的应用才可以进行共享。如果有多个sharedGroup,在添加的时候如果不指定,默认是第一个group。
添加:
NSDictionary *query = @{(__bridge id)kSecAttrAccessible : (__bridge id)kSecAttrAccessibleWhenUnlocked,
(__bridge id)kSecClass : (__bridge id)kSecClassGenericPassword,
(__bridge id)kSecValueData : [@"1234562" dataUsingEncoding:NSUTF8StringEncoding],
(__bridge id)kSecAttrAccount : @"account name",
(__bridge id)kSecAttrAccessGroup : @"XEGH3759AB.com.developer.test",
(__bridge id)kSecAttrService : @"noraml1",
(__bridge id)kSecAttrSynchronizable : @YES,
};
CFErrorRef error = NULL;
OSStatus status = SecItemAdd((__bridge CFDictionaryRef)query, nil);
取:
NSDictionary *query = @{(__bridge id)kSecClass : (__bridge id)kSecClassGenericPassword,
(__bridge id)kSecReturnRef : @YES,
(__bridge id)kSecReturnData : @YES,
(__bridge id)kSecMatchLimit : (__bridge id)kSecMatchLimitAll,
(__bridge id)kSecAttrAccount : @"account name",
(__bridge id)kSecAttrAccessGroup : @"XEGH3759AB.com.developer.test",
(__bridge id)kSecAttrService : @"noraml1",
};
CFTypeRef dataTypeRef = NULL;
OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &dataTypeRef);
只需要添加一个kSecAttrAccessGroup属性即可。