数据安全(三):本地安全存储Keychain
情形分析
- 登录逻辑
- 不勾选自动登录,每次启动都要重新登录
- 勾选自动登录
- 将token存储到本地,以便于下一次启动app时判断
- app启动,本地获取缓存的token
- 没有
- 登录界面
- 有
- 加载主界面
- 没有
- 通常为了安全起见,用户输入的密码首先要通过MD5/RSA等加密方式加密,然后在传输给后台服务器,即后台接受的是密文
本地存储token用什么比较好?
- 用沙盒偏好设置存储(NSUserDefault)
- 应用删除时所有数据都会清除
- 不安全,越狱后容易获取
- 用keychain
keychain
- Keychain 在 Mac 上大家都比较熟悉,钥匙串, 主要进行一些敏感信息存储使用 如用户名,密码,网络密码,认证令牌, Wi-Fi网络密码,VPN凭证等. iOS 中 Keychain, 也有相同的功能实现 , 保存的信息存储在设备中, 独立于每个App沙盒之外
- 特点
- 更安全. 对比 NSUserDefault 存储一些数据, 会更加安全.
- 即便 App 被卸载, 存储的信息依旧存在, 再次安装App, 存储是信息依旧可以使用.
- 相同的 Team ID 开发, 可实现多个App 共享数据
Keychain 的四个方法介绍
-
存储数据的方法
OSStatus SecItemAdd(CFDictionaryRef attributes, CFTypeRef * __nullable CF_RETURNS_RETAINED result) @attributes : 是要添加的数据。 @result : 这是存储数据后,返回一个指向该数据的引用,如果不是使用该引用时可以传入 NULL 。
-
根据条件查询数据
OSStatus SecItemCopyMatching(CFDictionaryRef query, CFTypeRef * __nullable CF_RETURNS_RETAINED result) @query : 要查询数据的条件。 @result: 根据条件查询得到数据的引用。
-
更新数据
OSStatus SecItemUpdate(CFDictionaryRef query, CFDictionaryRef attributesToUpdate) __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_2_0); @query : 要更新数据的查询条件。 @attributesToUpdate : 要更新的数据。
-
删除数据
OSStatus SecItemDelete(CFDictionaryRef query) __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_2_0); @query : 删除的数据的查询条件。
-
总结: 以上四个方法,就是Keychain 的常用的四个方法,也就是 增、删、改、查 。一般应用这四个方法就可以完全满足。
代码举例
-
工具封装
#import <Foundation/Foundation.h> @interface ZHKeyChainManager : NSObject /*! 保存数据 @data 要存储的数据 @identifier 存储数据的标示 */ +(BOOL) keyChainSaveData:(id)data withIdentifier:(NSString*)identifier ; /*! 读取数据 @identifier 存储数据的标示 */ +(id) keyChainReadData:(NSString*)identifier ; /*! 更新数据 @data 要更新的数据 @identifier 数据存储时的标示 */ +(BOOL)keyChainUpdata:(id)data withIdentifier:(NSString*)identifier ; /*! 删除数据 @identifier 数据存储时的标示 */ +(void) keyChainDelete:(NSString*)identifier ; @end #import <Security/Security.h> #import "ZHKeyChainManager.h" @implementation ZHKeyChainManager /*! 创建生成保存数据查询条件 */ +(NSMutableDictionary*) keyChainIdentifier:(NSString*)identifier { NSMutableDictionary * keyChainMutableDictionary = [NSMutableDictionary dictionaryWithObjectsAndKeys: (id)kSecClassGenericPassword,kSecClass, identifier,kSecAttrService, identifier,kSecAttrAccount, kSecAttrAccessibleAfterFirstUnlock,kSecAttrAccessible, nil]; return keyChainMutableDictionary; } /*! 保存数据 */ +(BOOL) keyChainSaveData:(id)data withIdentifier:(NSString*)identifier{ // 获取存储的数据的条件 NSMutableDictionary * saveQueryMutableDictionary = [self keyChainIdentifier:identifier]; // 删除旧的数据 SecItemDelete((CFDictionaryRef)saveQueryMutableDictionary); // 设置新的数据 [saveQueryMutableDictionary setObject:[NSKeyedArchiver archivedDataWithRootObject:data] forKey:(id)kSecValueData]; // 添加数据 OSStatus saveState = SecItemAdd((CFDictionaryRef)saveQueryMutableDictionary, nil); // 释放对象 saveQueryMutableDictionary = nil ; // 判断是否存储成功 if (saveState == errSecSuccess) { return YES; } return NO; } /*! 读取数据 */ +(id) keyChainReadData:(NSString*)identifier{ id idObject = nil ; // 通过标记获取数据查询条件 NSMutableDictionary * keyChainReadQueryMutableDictionary = [self keyChainIdentifier:identifier]; // 这是获取数据的时,必须提供的两个属性 // TODO: 查询结果返回到 kSecValueData [keyChainReadQueryMutableDictionary setObject:(id)kCFBooleanTrue forKey:(id)kSecReturnData]; // TODO: 只返回搜索到的第一条数据 [keyChainReadQueryMutableDictionary setObject:(id)kSecMatchLimitOne forKey:(id)kSecMatchLimit]; // 创建一个数据对象 CFDataRef keyChainData = nil ; // 通过条件查询数据 if (SecItemCopyMatching((CFDictionaryRef)keyChainReadQueryMutableDictionary , (CFTypeRef *)&keyChainData) == noErr){ @try { idObject = [NSKeyedUnarchiver unarchiveObjectWithData:(__bridge NSData *)(keyChainData)]; } @catch (NSException * exception){ NSLog(@"Unarchive of search data where %@ failed of %@ ",identifier,exception); } } if (keyChainData) { CFRelease(keyChainData); } // 释放对象 keyChainReadQueryMutableDictionary = nil; // 返回数据 return idObject ; } /*! 更新数据 @data 要更新的数据 @identifier 数据存储时的标示 */ +(BOOL)keyChainUpdata:(id)data withIdentifier:(NSString*)identifier { // 通过标记获取数据更新的条件 NSMutableDictionary * keyChainUpdataQueryMutableDictionary = [self keyChainIdentifier:identifier]; // 创建更新数据字典 NSMutableDictionary * updataMutableDictionary = [NSMutableDictionary dictionaryWithCapacity:0]; // 存储数据 [updataMutableDictionary setObject:[NSKeyedArchiver archivedDataWithRootObject:data] forKey:(id)kSecValueData]; // 获取存储的状态 OSStatus updataStatus = SecItemUpdate((CFDictionaryRef)keyChainUpdataQueryMutableDictionary, (CFDictionaryRef)updataMutableDictionary); // 释放对象 keyChainUpdataQueryMutableDictionary = nil; updataMutableDictionary = nil; // 判断是否更新成功 if (updataStatus == errSecSuccess) { return YES ; } return NO; } /*! 删除数据 */ +(void) keyChainDelete:(NSString*)identifier { // 获取删除数据的查询条件 NSMutableDictionary * keyChainDeleteQueryMutableDictionary = [self keyChainIdentifier:identifier]; // 删除指定条件的数据 SecItemDelete((CFDictionaryRef)keyChainDeleteQueryMutableDictionary); // 释放内存 keyChainDeleteQueryMutableDictionary = nil ; } @end
-
使用
#import "ViewController.h" #import "ZHKeyChainManager.h" #import "ZHMD5Tool.h" @interface ViewController () @end //测试 static NSString * const Keychain = @"zhonghua"; //自动登录,自存储自己的用户名和密码 NSString * const KEY_USERNAME_PASSWORD = @"com.company.app.usernamepassword"; NSString * const KEY_USERNAME = @"com.company.app.username"; NSString * const KEY_PASSWORD = @"com.company.app.password"; //所有登录该app的账号/密码列表 NSString * const KEY_USERNAME_PASSWORDLIST = @"com.company.app.usernamepasswordlist"; @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; // 存储数据 BOOL save = [ZHKeyChainManager keyChainSaveData:@"思念诉说,眼神多像云朵" withIdentifier:Keychain]; if (save) { NSLog(@"存储成功"); }else { NSLog(@"存储失败"); } // 获取数据 NSString * readString = [ZHKeyChainManager keyChainReadData:Keychain]; NSLog(@"获取得到的数据:%@",readString); // 更新数据 BOOL updata = [ZHKeyChainManager keyChainUpdata:@"长发落寞,我期待的女孩" withIdentifier:Keychain]; if (updata) { NSLog(@"更新成功"); }else{ NSLog(@"更新失败"); } // 读取数据 NSString * readUpdataString = [ZHKeyChainManager keyChainReadData:Keychain]; NSLog(@"获取更新后得到的数据:%@",readUpdataString); // 删除数据 [ZHKeyChainManager keyChainDelete:Keychain]; // 读取数据 NSString * readDeleteString = [ZHKeyChainManager keyChainReadData:Keychain]; NSLog(@"获取删除后得到的数据:%@",readDeleteString); } /*******牛逼的分割线*********/ //单个存储:用于记住密码时,时自动登录 - (void)test { NSMutableDictionary *userNamePasswordKVPairs = [NSMutableDictionary dictionary]; [userNamePasswordKVPairs setObject:@"zhonghua" forKey:KEY_USERNAME]; [userNamePasswordKVPairs setObject:@"123456" forKey:KEY_PASSWORD]; NSLog(@"%@", userNamePasswordKVPairs); //有KV值 // A、将用户名和密码写入keychain [ZHKeyChainManager keyChainSaveData:userNamePasswordKVPairs withIdentifier:KEY_USERNAME_PASSWORD]; // B、从keychain中读取用户名和密码 NSMutableDictionary *readUsernamePassword = (NSMutableDictionary *)[ZHKeyChainManager keyChainReadData:KEY_USERNAME_PASSWORD]; NSString *userName = [readUsernamePassword objectForKey:KEY_USERNAME]; NSString *password = [readUsernamePassword objectForKey:KEY_PASSWORD]; NSLog(@"username = %@", userName); NSLog(@"password = %@", password); //C、将用户名和密码从keychain中删除 [ZHKeyChainManager keyChainDelete:KEY_USERNAME_PASSWORD]; } /*******牛逼的分割线*********/ //用于:缓存登录账号的列表 - (void)test2WithAccount:(NSString *)account AndPassword:(NSString *)pwd { //1. 从钥匙串中拿到数据 NSMutableArray *usernameAndPasswords = (NSMutableArray *)[ZHKeyChainManager keyChainReadData:KEY_USERNAME_PASSWORDLIST]; if (!usernameAndPasswords) { usernameAndPasswords = [NSMutableArray array]; } //2.将该账号和密码存入数组 [usernameAndPasswords addObject:@{ account ? account : @"" : pwd ? pwd: @"" }]; //3. 将用户名和密码写入keychain [ZHKeyChainManager keyChainSaveData:usernameAndPasswords withIdentifier:KEY_USERNAME_PASSWORDLIST]; } - (IBAction)inputDatas:(id)sender { [self test2WithAccount:@"zhonghua" AndPassword:@"123456"]; [self test2WithAccount:@"limeng" AndPassword:@"111111"]; } //4.从keychain中读取用户名和密码 - (IBAction)readaction:(id)sender { NSMutableArray *readUsernamePassword = (NSMutableArray *)[ZHKeyChainManager keyChainReadData:KEY_USERNAME_PASSWORDLIST]; if (!readUsernamePassword) { NSLog(@"没有存储到值呗"); } NSLog(@"存入的账号密码列表%@",readUsernamePassword); } - (IBAction)clearnData:(id)sender { //5. 将用户名和密码从keychain中删除 [ZHKeyChainManager keyChainDelete:KEY_USERNAME_PASSWORDLIST]; } -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{ NSLog(@"简单的MD5加密%@",[ZHMD5Tool md5String:@"123456"]); NSLog(@"复杂的MD5加密%@",[ZHMD5Tool md5String2:@"123456"]); } @end
Keychain App间共享数据
- 打开keychain sharing
此时project会多出一个文件entitlements文件,打开一看实际就是一个PLIST文件,这里保存你你需要分享APP的bundle ID 也就是keychain Groups 里面的信息.两个地方都可以管理,所以你有多个APP里只要在这里进行设置添加就可以了.
- 另一个app设置
- 同样跟第一步一样打开keychain sharing
- 将第一个app的bundle ID添加进来,就可以共享第一个app的存储信息了
- 如图
代码
```javasc ript
#import "ViewController.h"
#import "ZHKeyChainManager.h"
@interface ViewController ()
@end
NSString * const KEY_USERNAME_PASSWORDLIST = @"com.company.app.usernamepasswordlist";
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
}
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
NSMutableArray *readUsernamePassword = (NSMutableArray *)[ZHKeyChainManager keyChainReadData:KEY_USERNAME_PASSWORDLIST];
if (!readUsernamePassword) {
NSLog(@"没有存储到值呗");
}
NSLog(@"存入的账号密码列表%@",readUsernamePassword);
}
@end
```
Keychain 的安全性
-
Keychain 并不是十分安全,在越狱的设备上,可以通过一些相应的工具很轻松的 dump 所有的 Keychain 数据,比如Keychain-Dumper,通过 ssh 登录设备,下载 keychain_dumper 至 /tmp 目录,然后 chmox +x keychain_dumper 赋予执行权限,直接 ./keychain_dumper > keychain_content.txt ,即可查看到相应的数据
- Keychain 数据可以通过 iTunes 备份,iTunes 备份可以让用户选择是否加密备份,不的加密备份可以恢复到任何设备,而加密的备份不能恢复到其它设备。虽然 ThisDeviceOnly 类型的 Item 不会备份,但是 Item 则会备份
- iOS 7 之后,Keychain 数据还可以通过 iCloud 同步跨越多个设备。默认情况下不同步,但是可以通过 [query setObject:(id)kCFBooleanTrue forKey:(id)kSecAttrSynchronizable]; 来设置同步,即使给 ThisDeviceOnly 设置同步,也不会生效
- 总而言之,考虑到 iCloud 服务器端、设备越狱等情况、甚至某些 Wi-FI 漏洞攻击,都有可能会泄露 Keychain 数据,最好是加密存储