iOS开发-“安全”存储KeyChain

在开发中,我们或多或少都会有一些需要存储在本地的数据,如果是一些不是很敏感的数据还好,但是比如我们为了让用户使用更加便捷而采用“记住密码”的方式免去让用户每次打开应用都需要输入账号密码登录的麻烦时,用户依托给我们的密码的安全性就变得尤为重要。

这里我们通常会怎么做呢?如果刚刚做iOS开发的同学,可能会直接存储在plist文件或者NSUserDefault中(我以前就这么干过),这种方法固然简单,但是很容易,密码就会被其他人获取到。因为这些数据都是存储在本地沙盒中的,而如果一旦有人破解了APP,就很容易可以拿到沙盒中的数据,那这些被存储的密码也就直接暴露在了想做坏事的人面前。

存储数据在本地沙盒还有一个很明显的特点,就是沙盒当应用被从本地删除之后就会被清空,随之用户保存的数据也就不复存在了,但是如果你有用过手机QQ,你可能会想到,诶!QQ即使我删掉了应用再次重装也不会清空我的账号信息啊。他是怎么做到的呢?没错就是我们这次想要研究的 KeyChain

什么是 KeyChain

OK,看完上边一部分,你应该会问,这个KeyChain是个什么鬼东西,竟然这么厉害。从名字来看,KeyChain就是钥匙串的意思,用过Mac的同学也都知道,Mac中有一个钥匙串

logo

我们这次所说的KeyChain跟他的功能也差不多,KeyChain是iOS提供的一种安全存储参数的方式,最常用来存储一些敏感信息如账号、密码、用户信息、银行资料等信息,KeyChain会以加密的方式存储在设备中。

苹果自己也用KeyChain来保存Wi-Fi密码,VPN凭证等,实际上以一个数据库,路径在/private/var/Keychains/keychain-2.db

KeyChain的结构

每一个KeyChain由多个KeyChain item组成,KeyChain item的结构类似字典,为Key-Value,同时每条KeyChain Item还包含一条data和多个attributes组成。

比如一个银行就是一个KeyChain,银行里可以有多个保险库,对应的就是KeyChain Item,而每个保险库都有自己该存放的东西,比如现金,黄金等,这个就是attributes,而存储的内容就是data。

其中苹果提供了这些类型的keychain item,并且对不同类型的item做了不同的处理,比如password和key类的item就会做加密,而certificates类的就不会。

1
2
3
4
5
extern CFTypeRef kSecClassGenericPassword
extern CFTypeRef kSecClassInternetPassword
extern CFTypeRef kSecClassCertificate
extern CFTypeRef kSecClassKey
extern CFTypeRef kSecClassIdentity OSX_AVAILABLE_STARTING(MAC_10_7, __IPHONE_2_0);

为什么要用 KeyChain

说了这么多好处,你还要问?

苹果爸爸自己都是用这个来保存敏感信息的,并且存储在这里的数据都经过加密,安全性对比起其他方式自然是不用多说。

另外一个就是这个文件的路径并不在应用的沙盒路径下,所以不必担心删除应用后用户数据丧失的事情发生。

还有一个就是KeyChain可以在不同的应用之间共享,但是前提是他们是使用同一个TeamID。

有这么多好处,你还不用?好了,废话不多说快来看看怎么使用它吧。

准备使用KeyChain

导入Security框架

首先导入Security.framework框架。

logo

打开KeyChain Sharing开关

然后打开KeyChain Sharing开关,这里的KeyChain Groups ID就是以后用来共享KeyChain要关键。

logo

代码配置KeyChain

苹果主要提供了四个方法,对常用的增删改查进行处理:

  • SecItemAdd;
  • SecItemUpdate;
  • SecItemCopyMatching;
  • SecItemDelete;

增加

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
NSMutableDictionary *queryDic = [NSMutableDictionary dictionary];
//添加用户
[queryDic setObject:accountStr forKey:(__bridge id)kSecAttrAccount];
//添加密码标识
[queryDic setObject:(__bridge id)kSecClassGenericPassword forKey:(__bridge id)kSecClass];
//token生成data
NSData *tokenData = [tokenStr dataUsingEncoding:NSUTF8StringEncoding];
//添加密码
[queryDic setObject:tokenData forKey:(__bridge id)kSecValueData];
OSStatus status = -1;
CFTypeRef result = NULL;
//保存!
status = SecItemAdd((__bridge CFDictionaryRef)queryDic, NULL); //!!!!!关键的添加API

修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
NSMutableDictionary *queryDic = [NSMutableDictionary dictionary];
//添加用户
[queryDic setObject:accountStr forKey:(__bridge id)kSecAttrAccount];
//添加密码标识
[queryDic setObject:(__bridge id)kSecClassGenericPassword forKey:(__bridge id)kSecClass];
OSStatus status = -1;
CFTypeRef result = NULL;
//token生成data
NSData *tokenData = [tokenStr dataUsingEncoding:NSUTF8StringEncoding];
NSMutableDictionary *dict = [[NSMutableDictionary alloc] initWithDictionary:queryDic];
[dict setObject:tokenData forKey:(__bridge id)kSecValueData]; //添加密码
status = SecItemUpdate((__bridge CFDictionaryRef)queryDic, (__bridge CFDictionaryRef)dict);//!!!!关键的更新API

删除

1
2
3
4
5
6
7
8
9
10
NSMutableDictionary *queryDic = [NSMutableDictionary dictionary];
[queryDic setObject:service forKey:(__bridge id)kSecAttrService]; //标签service
[queryDic setObject:account forKey:(__bridge id)kSecAttrAccount]; //标签account
[queryDic setObject:(__bridge id)kSecClassGenericPassword forKey:(__bridge id)kSecClass];//表明存储的是一个密码
OSStatus status = SecItemDelete((CFDictionaryRef)queryDic);
return (status == errSecSuccess);

查询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//生成一个查询用的 可变字典
NSMutableDictionary *queryDic = [NSMutableDictionary dictionary];
//首先添加获取密码所需的搜索键和类属性:
[queryDic setObject:(__bridge id)kSecClassGenericPassword forKey:(__bridge id)kSecClass]; //表明为一般密码可能是证书或者其他东西
[queryDic setObject:(__bridge id)kCFBooleanTrue forKey:(__bridge id)kSecReturnData]; //返回Data
[queryDic setObject:accountStr forKey:(__bridge id)kSecAttrAccount]; //输入account
//查询
OSStatus status = -1;
CFTypeRef result = NULL;
status = SecItemCopyMatching((__bridge CFDictionaryRef)queryDic,&result);//核心API 查找是否匹配 和返回密码!
if (status != errSecSuccess)
{ //判断状态
return nil;
}
//删除kSecReturnData键; 我们不需要它了:
[queryDic removeObjectForKey:(__bridge id)kSecReturnData];
//将密码转换为NSString并将其添加到返回字典:
NSString *password = [[NSString alloc] initWithBytes:[(__bridge_transfer NSData *)result bytes] length:[(__bridge NSData *)result length] encoding:NSUTF8StringEncoding];
[queryDic setObject:password forKey:(__bridge id)kSecValueData];
NSLog(@"查询 : %@", queryDic);

KeyChain的安全性

本文的标题也加了引号,也就是说KeyChain并不是绝对的安全的,其实所有的东西都没有绝对的安全,另外也可以通过KeyChain dumper导出KeyChain中保存的内容。所以在保存数据到KeyChain中时,最好也要先进行一层加密,而不是直接明文保存到KeyChain中。

另外苹果提供了应用合适的需要访问的条件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
extern const CFStringRef kSecAttrAccessibleWhenUnlocked
API_AVAILABLE(macos(10.9), ios(4.0));
extern const CFStringRef kSecAttrAccessibleAfterFirstUnlock
API_AVAILABLE(macos(10.9), ios(4.0));
extern const CFStringRef kSecAttrAccessibleAlways
API_AVAILABLE(macos(10.9), ios(4.0));
extern const CFStringRef kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly
API_AVAILABLE(macos(10.10), ios(8.0));
extern const CFStringRef kSecAttrAccessibleWhenUnlockedThisDeviceOnly
API_AVAILABLE(macos(10.9), ios(4.0));
extern const CFStringRef kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
API_AVAILABLE(macos(10.9), ios(4.0));
extern const CFStringRef kSecAttrAccessibleAlwaysThisDeviceOnly
API_AVAILABLE(macos(10.9), ios(4.0));

其中需要注意的是,如何情况下最好都不要将他设置为kSecAttrAccessibleAlways,并且钥匙串可以通过iTunes或iCloud同步的方式同步到其他设备,如果你保存的数据高度敏感,则需要使用后缀为ThisDeviceOnly的选项。

KeyChain的清除条件

之前我们也说到了keyChain的保存路径,所以这就决定了一般情况下是不会讲keyChain删掉的,除了一种情况—抹掉所有内容和设置,在iPhone中使用这种方法将彻底抹掉iPhone中的全部内容,包括keychain中的。

参考文档

iOS本地数据存储安全
iOS KeyChain 基础
谨慎使用keychain
KeyChain Service Programming
iOS Keychain总结
iOS Keychain使用说明
iOS开发中使用keyChain保存用户密码
iOS KeyChain 应用间共享数据

千星框架

scifihifi-iphone
SAMKeychain