数据安全(三):本地安全存储Keychain

情形分析

  1. 登录逻辑
    1. 不勾选自动登录,每次启动都要重新登录
    2. 勾选自动登录
      1. 将token存储到本地,以便于下一次启动app时判断
    3. app启动,本地获取缓存的token
      1. 没有
        1. 登录界面
        1. 加载主界面
  2. 通常为了安全起见,用户输入的密码首先要通过MD5/RSA等加密方式加密,然后在传输给后台服务器,即后台接受的是密文

本地存储token用什么比较好?

  1. 用沙盒偏好设置存储(NSUserDefault)
    1. 应用删除时所有数据都会清除
    2. 不安全,越狱后容易获取
  2. 用keychain

keychain

  1. Keychain 在 Mac 上大家都比较熟悉,钥匙串, 主要进行一些敏感信息存储使用 如用户名,密码,网络密码,认证令牌, Wi-Fi网络密码,VPN凭证等. iOS 中 Keychain, 也有相同的功能实现 , 保存的信息存储在设备中, 独立于每个App沙盒之外
  2. 特点
    1. 更安全. 对比 NSUserDefault 存储一些数据, 会更加安全.
    2. 即便 App 被卸载, 存储的信息依旧存在, 再次安装App, 存储是信息依旧可以使用.
    3. 相同的 Team ID 开发, 可实现多个App 共享数据

Keychain 的四个方法介绍

  1. 存储数据的方法

     OSStatus SecItemAdd(CFDictionaryRef attributes, CFTypeRef * __nullable CF_RETURNS_RETAINED result)
    
     @attributes : 是要添加的数据。 
     @result : 这是存储数据后,返回一个指向该数据的引用,如果不是使用该引用时可以传入 NULL 。
    
  2. 根据条件查询数据

     OSStatus SecItemCopyMatching(CFDictionaryRef query, CFTypeRef * __nullable CF_RETURNS_RETAINED result)
    
     @query : 要查询数据的条件。 
     @result: 根据条件查询得到数据的引用。
    
  3. 更新数据

     OSStatus SecItemUpdate(CFDictionaryRef query, CFDictionaryRef attributesToUpdate) 
    __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_2_0);
    
     @query : 要更新数据的查询条件。 
     @attributesToUpdate : 要更新的数据。
    
  4. 删除数据

     OSStatus SecItemDelete(CFDictionaryRef query) 
    __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_2_0);
    
     @query : 删除的数据的查询条件。
    
  5. 总结: 以上四个方法,就是Keychain 的常用的四个方法,也就是 增、删、改、查 。一般应用这四个方法就可以完全满足。

代码举例

  1. 工具封装

     #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
    
  2. 使用

     #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间共享数据

  1. 打开keychain sharing 图2 此时project会多出一个文件entitlements文件,打开一看实际就是一个PLIST文件,这里保存你你需要分享APP的bundle ID 也就是keychain Groups 里面的信息.两个地方都可以管理,所以你有多个APP里只要在这里进行设置添加就可以了.
  2. 另一个app设置
    1. 同样跟第一步一样打开keychain sharing
  3. 将第一个app的bundle ID添加进来,就可以共享第一个app的存储信息了
  4. 如图 图2
代码
 
```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 的安全性

  1. Keychain 并不是十分安全,在越狱的设备上,可以通过一些相应的工具很轻松的 dump 所有的 Keychain 数据,比如Keychain-Dumper,通过 ssh 登录设备,下载 keychain_dumper 至 /tmp 目录,然后 chmox +x keychain_dumper 赋予执行权限,直接 ./keychain_dumper > keychain_content.txt ,即可查看到相应的数据

  2. Keychain 数据可以通过 iTunes 备份,iTunes 备份可以让用户选择是否加密备份,不的加密备份可以恢复到任何设备,而加密的备份不能恢复到其它设备。虽然 ThisDeviceOnly 类型的 Item 不会备份,但是 Item 则会备份
  3. iOS 7 之后,Keychain 数据还可以通过 iCloud 同步跨越多个设备。默认情况下不同步,但是可以通过 [query setObject:(id)kCFBooleanTrue forKey:(id)kSecAttrSynchronizable]; 来设置同步,即使给 ThisDeviceOnly 设置同步,也不会生效
  4. 总而言之,考虑到 iCloud 服务器端、设备越狱等情况、甚至某些 Wi-FI 漏洞攻击,都有可能会泄露 Keychain 数据,最好是加密存储

DEMO地址

Table of Contents