聊聊 iOS 数据保护

文件系统中的文件、keychain中的项,都是加密存储的。当用户解锁设备后,系统通过UDID密钥和用户设定的密码生成一个用于解密的密码密钥,存放在内存中,直到设备再次被锁,开发者可以通过Data Protection API 来设定文件系统中的文件、keychain中的项应该何时被解密。这个就是数据保护的内容。

钥匙串(keychain)

keychain服务提供了一种安全的保存私密信息(密码,序列号,私钥,证书等)的方式,每个iOS程序都有一个独立的keychain存储。相对于NSUserDefaults、文件保存等一般方式,keychain保存更为安全,而且keychain里保存的信息不会因APP被删除而丢失。

基于钥匙串的特点,我们在使用时对于新增的数据项目,要注意数据删除的时机。还有一点需要注意的就是,保护属性的指定。如果没有指定保护属性,这将被视为严重的安全漏洞。

作用

备份

当我们备份设备数据时,系统会将用户数据保存在相应的钥匙串中,并依照相应的安全策略保存。主要分成两类:加密不加密

两者的主要区别在于恢复数据时的范围不同。加密备份的数据可以恢复到任何设备上(ThisDeviceOnly指定的项目除外),而不加密的备份则只能恢复至同一台设备。

共享

keychain支持在多个应用之间共享数据。但现实开发中UIPasteboardNameFind的使用代替了keychain。不过好在Apple在iOS11中已经废弃它。看来Apple想规范开发者对于用户敏感数据的共享使用,同时提醒大家使用更加安全的方式存储用户敏感数据。

使用范例

SecItemAdd

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
NSData *passwordData = [@"myPassword" dataUsingEncoding:NSUTF8StringEncoding];

[dict setObject:(__bridge id)kSecClassGenericPassword forKey:(__bridge id)kSecClass];
[dict setObject:@"Conglomco" forKey:(__bridge id)kSecAttrLabel];
[dict setObject:@"This is your password for the Conglomco service." forKey:(__bridge id)kSecAttrDescription];
[dict setObject:@"chars" forKey:(__bridge id)kSecAttrAccount];
[dict setObject:@"cn.zaker.keychain.sample" forKey:(__bridge id)kSecAttrService];
[dict setObject:passwordData forKey:(__bridge id)kSecValueData];
[dict setObject:(__bridge id)kSecAttrAccessibleWhenUnlocked forKey:(__bridge id)kSecAttrAccessible];

OSStatus error = SecItemAdd((__bridge CFDictionaryRef)dict, NULL);
if (error == errSecSuccess) {
NSLog(@"Yay");
}

SecItemDelete

1
2
3
4
5
6
7
8
9
10
NSMutableDictionary *dict = [NSMutableDictionary dictionary];

[dict setObject:(__bridge id)kSecClassGenericPassword forKey:(__bridge id)kSecClass];
[dict setObject:@"chars" forKey:(__bridge id)kSecAttrAccount];
[dict setObject:@"cn.zaker.keychain.sample" forKey:(__bridge id)kSecAttrService];

OSStatus error = SecItemDelete((__bridge CFDictionaryRef)dict);
if (error == errSecSuccess) {
NSLog(@"Yay");
}

SecItemUpdate

1
2
3
4
5
6
7
8
9
10
11
12
13
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
NSData *newPasswordData = [@"newMyPassword" dataUsingEncoding:NSUTF8StringEncoding];

[dict setObject:(__bridge id)kSecClassGenericPassword forKey:(__bridge id)kSecClass];
[dict setObject:@"chars" forKey:(__bridge id)kSecAttrAccount];
[dict setObject:@"cn.zaker.keychain.sample" forKey:(__bridge id)kSecAttrService];

NSDictionary *updatedAttribute = [NSDictionary dictionaryWithObject:newPasswordData forKey:(__bridge id)kSecValueData];

OSStatus error = SecItemUpdate((__bridge CFDictionaryRef)dict, (__bridge CFDictionaryRef)updatedAttribute);
if (error == errSecSuccess) {
NSLog(@"Yay");
}

SecItemCopyMatching

1
2
3
4
5
6
7
8
9
10
11
12
NSMutableDictionary *dict = [NSMutableDictionary dictionary];

[dict setObject:(__bridge id)kSecClassGenericPassword forKey:(__bridge id)kSecClass];
[dict setObject:@"chars" forKey:(__bridge id)kSecAttrAccount];
[dict setObject:@"cn.zaker.keychain.sample" forKey:(__bridge id)kSecAttrService];
[dict setObject:(id)kCFBooleanTrue forKey:(__bridge id)kSecReturnAttributes];

NSDictionary *result = nil;
OSStatus error = SecItemCopyMatching((__bridge CFDictionaryRef)dict, (void *)&result);
if (error == errSecSuccess) {
NSLog(@"Yay %@", result);
}

常用方法

方法 描述
SecItemAdd 添加数据
SecItemDelete 删除数据
SecItemUpdate 修改数据
SecItemCopyMatching 查找数据

保护属性

钥匙串保护属性 含义
kSecAttrAccessibleAfterFirstUnlock 开机之后密钥不可用,直到用户首次输入密码
kSecAttrAccessibleAlways 密钥在设备开机后依旧可用。在iOS9中已经废弃
kSecAttrAccessibleAlwaysThisDeviceOnly 密钥始终可用,但无法迁移到其他设备
kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly 作用同上
kSecAttrAccessibleWhenUnlocked 只要解锁过设备,则密钥保持可用状态
kSecAttrAccessibleWhenUnlockedThisDeviceOnly 作用同上
kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly 作用同上,只有用户设置密码密钥才可用

kSecAttrAccessibleAlways 将会引入一个很明显的安全问题,因为此种保护属性,只要有人窃取了你的设备,他们就能读取钥匙串的内容。

kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly 这个属性可以完美的解决上述的安全问题。因为越狱时通常需要重启设备。

kSecAttrAccessibleWhenUnlocked 这个属性要求攻击者必须知道用户密码才能提取隐私数据。它很适合做默认属性值。

kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly 是iOS8新增的保护属性。这个属性要求在使用时用户设定了密码,否则使用就失败。

基本用法

项目类 描述
kSecClassGenericPassword 普通密码
kSecClassInternetPassword 专门用于互联网服务的密码
kSecClassCertificate 加密证书
kSecClassKey 加密密钥
kSecClassIdentity 一个密钥对(包括公共证书和私钥)

iCloud同步

这是iOS7引入的一种新机制,可以把钥匙串项目同步到iCloud,允许用户在多个设备之间共享钥匙串项目。

默认情况下,应用程序创建的钥匙串项目会禁用这个机制,但可以把kSecAttrSynchronizable设置为true来启用。

另外请注意,使用此选项时无法指定不兼容的kSecAttrAccessible属性。例如,指定kSecAttrAccessibleWhenUnlockedThisDeviceOnly不起作用,因为ThisDeviceOnly指定的项目不会备份,也不能同步到iCloud、笔记本电脑、台式机或其他同步位置。

数据保护

Apple 推出了数据保护API作为额外的保护层,它允许开发者指定文件解密密钥的生命周期。可以使用这个API控制文件的访问权限,与钥匙串项目中的kSecAttrAccessible 属性类似。

数据保护API使用用户密码和层级密钥来加密保护文件的密钥,而当这些文件不能被访问时,会从内存中删除这个层级密钥。

文件保护过程

文件保护过程

1.文件生成一个文件密钥来加密文件的内容。

2.生成一个额外的密钥对,用于生成文件公钥文件私钥

3.用文件私钥Protected Unless Open等级公钥计算出一个共享密码

4.用共享密码 SHA-1散列值加密文件密钥。

5.加密过的文件密钥会存储在文件的元数据中,元数据中还有文件的公钥。

6.系统丢弃文件私钥

7.关闭文件时从内存中删除未加密的文件密钥。

8.需要再次打开文件时,用Protected Unless Open等级私钥文件公钥计算共享密码

9.计算共享密码的SHA-1散列值,把它当作解密文件的密钥。

DataProtectionClass 权限

如果你的应用在设备进入后台或锁定时不需要写入或读取文件,那你就可以在工程中配置一个NSFileProtectionComplete值来添加权限。这将确保所有受保护的文件数据只能在设备解锁时访问,相当于为所有的应用文件都设置kSecAttrAccessibleWhenUnlocked选项。

从Xcode5开始,新工程会默认启用数据保护权限,但是一些旧工程并不会自动开启。

开启方式如下图:

开启DataProtectionClass权限

使用范例

NSDataWritingFileProtectionComplete

1
2
3
4
5
6
7
NSData *data = [self generateData];
NSError *error = nil;
NSString *path = [NSString stringWithFormat:@"%@_demo.pdf", NSTemporaryDirectory()];
[data writeToFile:path options:NSDataWritingFileProtectionComplete error:&error];
if (error) {
NSLog(@"%@", error);
}

NSFileProtectionComplete

1
2
3
4
5
6
7
NSString *path = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:@"demo.txt"];
NSError *error = nil;
NSDictionary *attr = [NSDictionary dictionaryWithObject:NSFileProtectionComplete forKey:NSFileProtectionKey];
[[NSFileManager defaultManager] setAttributes:attr ofItemAtPath:path error:&error];
if (error) {
NSLog(@"%@", error);
}

SQLITE_OPEN_FILEPROTECTION_COMPLETEUNLESSOPEN

1
2
3
4
5
6
NSString *path = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:@"demo.sqlite"];
sqlite3 *handle = NULL;
sqlite3_open_v2([path UTF8String],
&handle,
SQLITE_OPEN_CREATE | SQLITE_OPEN_READWRITE | SQLITE_OPEN_FILEPROTECTION_COMPLETEUNLESSOPEN,
NULL);

保护等级

CompleteUntilFirstUserAuthentication

这个保护等级是iOS5开始使用的默认值。主要用来防御一些需要重启的攻击。

Complete

官方称这是目前最安全的文件保护等级。在这种情况下,锁屏之后系统就会删除内存中的层级密钥,并把文件改为不可读。

具体使用的范例前文可见。

使用Complete保护之前,考虑是否合适。如果你的应用进程需要持续写/读一个文件,那这种保护模式就不合适了。

CompleteUnlessOpen

如果一个文件当前被一个应用打开,那会暂时禁用该文件的保护。它会确保打开的文件在设备被锁定时依然能够写入,并且允许新建文件到磁盘。不过这个等级保护的文件在锁屏时无法打开,除非锁屏时就已经提前打开。

CommonCrypto加密

CommonCrypto是Apple官方提供的一套加密框架。在使用这套方案时,主要注意以下几个问题即可。

避免弱算法

众所周知,DES就是一种很典型的弱算法。如果你使用弱算法,那应用就很容易遭受密码攻击和暴力破解。

我们在实际使用时,可以考虑使用AES算法、MD5算法或MD5加盐算法等。

破碎的熵

我们通常会使用rand方法来产生随机数,但是系统的这个随机函数并不是真正的随机。它产生的随机数符合正态分布,我们可以使用官方提供的方法SecRandomCopyBytes获得。也可以自己使用算法产生随机数。

这里分享一个笔者以前自己实现的随机数案例。《均匀分布随机函数的实现》

弱密钥

开发者经常将用户的密码当作加密密钥,尤其在移动设备上,这将导致一个非常脆弱、低熵加密的密钥。

那么正确的姿势是使用CCKeyDerivationPBKDF方法来生成。

Touch ID

使用范例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#import <LocalAuthentication/LocalAuthentication.h>

LAContext *context = [[LAContext alloc] init];
NSError *error = nil;
NSString *reason = @"We use this to verify your identify";
if ([context canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics error:&error]) {
[context evaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics localizedReason:reason reply:^(BOOL success, NSError * _Nullable error) {
if (success) {
NSLog(@"Hello, that's your finger!");
} else {
NSLog(@"Couldn't read your fingerprint. Falling back to PIN or somthing.");
}
}];
} else {
NSLog(@"Error : %@ %@", error, [error userInfo]);
}

Touch ID 与普通密码相比的安全性

一个人的指纹是世界上最好的密码之一。它独一无二,高度特异,并且无需记忆。它会伴随人终生,所以你可以一直使用同一个指纹密码。

比起简单的4位数字锁屏密码一万分之一的破解可能性,能找到同样单个指纹的人的概率为五万分之一,这是非常安全的,比起前者我可以不断尝试,Touch ID 指纹的硬性破解几率几乎没有,因为不可能有五万多个人排队来一一解锁你的设备,况且 Touch ID 会拒绝第五次以后的尝试而要求输入密码。所以我们可以添加更加复杂的密码而不是简单的数字密码来进一步提高安全性。

Touch ID 指纹识别的原理

在如此快速的识别过程中,Home 按钮上的一圈金属环起到了感应手指的功能,通知 Touch ID 来读取指纹。Touch ID 置于该按钮中,传感器的厚度只有 170 微米,拥有 500 ppi 高分辨率,可读取极小的指纹细节。传感器可从皮肤皮下层指纹的一些小部分拍摄高分辨率图像,然后会分析该信息,根据三种基本指纹类型(弧形纹、箕形纹或斗形纹)将指纹分类,它会绘制比人眼不可见的纹路细节,确保指纹读取准确无误。

touch id sensor

Touch ID 可从 360 度方向读取指纹,随后创建指纹的某种数学表达式,并将其与已注册的数据进行比较,以确定是否匹配。如果匹配,便可替代密码来解锁设备或通过某个令牌。

此外,根据苹果官方的描述,Touch ID 会向以注册的指纹数据里持续的添加新的特征数据,随着时间的推移,这能不断提高匹配准确度,也能进一步的提高安全性。

touch id ontouch

除了上述的内容,苹果还加入可以一些有效的手段来进一步确保安全,例如设备再重新启动或者保持锁定 48 小时后,就只能通过密码而不是指纹来解锁设备。

Secure Enclave

Apple声明指纹的图案不会被保存,存储于设备中的是指纹特征的数学表达式,而且从这些表达式不可能直接反推出指纹图像。

touch-id-structure

指纹的验证运算独立于主要处理器芯片(例如 A7、A8),芯片内有称为 “Secure Enclave” 的高级安全架构,专用于密码指纹数据,并使用 Secure Enclave 的专用密钥加密,以及每次启动以随机的 UID 进行管理。指纹数据仅能被 Secure Enclave 处理和使用,正因为此架构独立于其它设备部件,仅有 Touch ID 使用它,且不能将它用于匹配其他指纹数据库,所以存储的指纹数据不会由 iOS 或其他应用访问,也不会被存储到 Apple 服务器或备份到 iCloud 等地方。

A7-Secure-Enclave

从硬件上来看指纹特征数据尚不能被 “Secure Enclave” 和 Touch ID 以外的部件访问到,并且两者芯片互相隔离开来,所有软件都没有权利获得指纹数据,仅能得到指纹是否错误的回馈。因为架构的核心秘密仅仅只有苹果公司知道,所以目前几乎没有第三方软件可以访问到这部分敏感信息。

到此,关于 iOS 数据保护的问题暂时告一段落。欢迎大家评论指正。