iOS数据安全:XML库、钥匙串与数据保护API全解析
1. 替代XML库的问题
在各种iOS项目中,你可能会遇到替代XML库。这些库通常因其比NSXMLParser更好的性能特性以及对XPath等功能的支持而被选用。当检查使用替代XML库的代码时,需遵循以下步骤确保安全:
1.
禁用外部实体扩展
:使用该库的标准方法禁用外部实体扩展。
2.
输入清理
:确认任何集成外部提供输入的XPath查询都先对输入进行清理,就像防止跨站脚本攻击那样。
3.
参数化查询
:XPath查询应像SQL查询一样进行参数化,但具体方法可能因涉及的第三方库而异。
2. 钥匙串的使用
钥匙串用于存储小段敏感数据,如密码、个人数据等。它使用设备密钥(若有用户密码则结合使用)进行加密。钥匙串的API主要包含四个操作:
-
SecItemAdd
:向钥匙串添加项目。
-
SecItemUpdate
:更新现有项目。
-
SecItemCopyMatching
:检索项目。
-
SecItemDelete
:从钥匙串中删除项目。
2.1 用户备份中的钥匙串
用户对设备进行完整备份时,有两种与安全相关的选项:
-
未加密备份
:只能恢复到接收备份的同一设备。
-
加密备份
:用户可选择密码短语对备份数据进行加密,可恢复到任何设备(标记为ThisDeviceOnly的项目除外),并备份钥匙串的全部内容。
如果不想让钥匙串项目存储在备份中,可以使用钥匙串的数据保护属性。
2.2 钥匙串保护属性
钥匙串保护属性指定了钥匙串数据何时可以存储在内存中并被操作系统或应用程序请求。添加密码或个人数据等项目到钥匙串时,指定保护属性很重要,因为这明确说明了数据何时可用。不指定保护属性应被视为一个错误。
可以通过
SecItemAdd
方法在首次将项目存储到钥匙串时指定属性,需要传入
kSecAttrAccessible
的预定义值之一,主要有三种访问类型:
| 钥匙串保护属性 | 含义 |
| — | — |
| kSecAttrAccessibleAfterFirstUnlock | 设备启动后,直到用户首次输入密码,密钥不可访问。 |
| kSecAttrAccessibleAlways | 只要设备启动,密钥始终可访问。此属性在iOS 9中已弃用,因为它相比kSecAttrAccessibleAfterFirstUnlock没有实际优势。 |
| kSecAttrAccessibleAlwaysThisDeviceOnly | 密钥始终可访问,但不能移植到其他iOS设备。 |
| kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly | 与上一个密钥相同,但此密钥仅保留在本设备上。 |
| kSecAttrAccessibleWhenUnlocked | 设备解锁后(即用户输入密码后),密钥可访问。 |
| kSecAttrAccessibleWhenUnlockedThisDeviceOnly | 与上一个密钥相同,但此密钥仅保留在本设备上(完整加密备份除外)。 |
| kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly | 与上一个密钥相同,但此密钥仅对设置了密码的用户可用,如果密码被取消设置,将从设备中移除,且不会包含在任何备份中。 |
目前,默认属性是
kSecAttrAccessibleWhenUnlocked
,这是一个合理的限制性默认值。但由于苹果的公开文档对此默认属性的定义存在分歧,所以应在所有钥匙串项目上显式设置此属性。对于自己的代码,可考虑使用
kSecAttrAccessibleWhenUnlockedThisDeviceOnly
;检查第三方源代码时,确保使用了限制性保护属性。
2.3 基本钥匙串使用
钥匙串项目有几种类型,如下表所示:
| 项目类 | 含义 |
| — | — |
| kSecClassGenericPassword | 普通密码 |
| kSecClassInternetPassword | 专门用于互联网服务的密码 |
| kSecClassCertificate | 加密证书 |
| kSecClassKey | 加密密钥 |
| kSecClassIdentity | 由公共证书和私钥组成的密钥对 |
除非处理证书,否则
kSecClassGenericPassword
通常可用于大多数敏感数据。以下是一些常用方法的示例:
添加项目到钥匙串
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
NSData *passwordData = [@"mypassword" dataUsingEncoding:NSUTF8StringEncoding];
[dict setObject:(__bridge id)kSecClassGenericPassword forKey:(__bridge id)kSecClass];
[dict setObject:@"Conglomco login" forKey:(__bridge id)kSecAttrLabel];
[dict setObject:@"This is your password for the Conglomco service." forKey:(__bridge id)kSecAttrDescription];
[dict setObject:@"dthiel" forKey:(__bridge id)kSecAttrAccount];
[dict setObject:@"com.isecpartners.SampleKeychain" 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");
}
更新钥匙串项目
NSString *newPassword = @"";
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
[dict setObject:(__bridge id)kSecClassGenericPassword forKey:(__bridge id)kSecClass];
[dict setObject:@"dthiel" forKey:(__bridge id)kSecAttrAccount];
[dict setObject:@"com.isecpartners.SampleKeychain" forKey:(__bridge id)kSecAttrService];
NSDictionary *updatedAttribute = [NSDictionary dictionaryWithObject:[newPassword dataUsingEncoding:NSUTF8StringEncoding] forKey:(__bridge id)kSecValueData];
OSStatus error = SecItemUpdate((__bridge CFDictionaryRef)dict, (__bridge CFDictionaryRef)updatedAttribute);
查询钥匙串
[dict setObject:(__bridge id)kSecClassGenericPassword forKey:(__bridge id)kSecClass];
[dict setObject:@"dthiel" forKey:(__bridge id)kSecAttrAccount];
[dict setObject:@"com.isecpartners.SampleKeychain" forKey:(__bridge id)kSecAttrService];
[dict setObject:(id)kCFBooleanTrue forKey:(__bridge id)kSecReturnAttributes];
NSDictionary *result = nil;
OSStatus error = SecItemCopyMatching((__bridge CFDictionaryRef)dict, (void *)&result);
NSLog(@"Yay %@", result);
删除钥匙串项目
NSMutableDictionary *searchDictionary = [NSMutableDictionary dictionary];
[searchDictionary setObject:(__bridge id)kSecClassGenericPassword forKey:(__bridge id)kSecClass];
[searchDictionary setObject:@"dthiel" forKey:(__bridge id)kSecAttrAccount];
[searchDictionary setObject:@"com.isecpartners.SampleKeychain" forKey:(__bridge id)kSecAttrService];
OSStatus error = SecItemDelete((__bridge CFDictionaryRef)searchDictionary);
2.4 钥匙串包装器
使用钥匙串时,可能需要编写一些包装函数以方便使用,因为大多数应用程序只使用钥匙串API功能的一部分。有许多第三方预编写的钥匙串包装器,例如Lockbox,它提供了一组用于存储字符串、日期、数组和集合的类方法。
使用Lockbox设置钥匙串项目
#import "Lockbox.h"
NSString *keyname = @"KeyForMyApp";
NSString *secret = @"secretstring";
[Lockbox setString:secret forKey:keyname accessibility:kSecAttrAccessibleWhenUnlocked];
使用Lockbox从钥匙串中检索字符串
NSString *result = [Lockbox stringForKey:secret];
无论选择或编写哪种包装器,都要确保它能够设置
kSecAttrAccessible
属性,因为许多可用的示例代码都忽略了这个功能。
2.5 共享钥匙串
iOS允许同一开发者的多个应用程序通过钥匙串访问组共享钥匙串数据。例如,在线市场的“买家”应用和“卖家”应用可以让用户在两个应用之间共享相同的用户名和密码。
要使用钥匙串访问组,应用程序必须共享相同的捆绑种子ID,这只能在创建新的App ID时指定。应用程序要利用访问组,需要创建一个Entitlements属性列表,其中包含一个名为
keychain-access-groups
的数组,每个共享钥匙串项目都有一个字符串条目。
2.6 iCloud同步
iOS 7引入了一种机制,允许钥匙串项目与iCloud同步,使用户可以在多个设备之间共享钥匙串项目。默认情况下,应用程序创建的钥匙串项目未启用此功能,但可以通过将
kSecAttrSynchronizable
设置为
true
来启用。
[query setObject:(id)kCFBooleanTrue forKey:(id)kSecAttrSynchronizable];
由于此项目现在可能在多个钥匙串之间同步,对项目的更新(包括删除)也会传播到所有其他位置。因此,要确保应用程序能够处理系统对钥匙串项目的移除或更改。同时,使用此选项时不能指定不兼容的
kSecAttrAccessible
属性,例如
kSecAttrAccessibleWhenUnlockedThisDeviceOnly
就不适用,因为
ThisDeviceOnly
指定该项目永远不应备份到iCloud、笔记本电脑或桌面,或任何其他同步提供商。
3. 数据保护API
作为额外的保护层,苹果引入了数据保护API(不要与微软的数据保护API混淆),它允许开发者指定文件解密密钥何时可用,从而控制对文件本身的访问,类似于钥匙串项目的
kSecAttrAccessible
属性。数据保护API使用用户的密码结合类密钥来加密每个受保护文件的特定密钥,并在文件不可访问时(即设备锁定时)在内存中丢弃类密钥。当启用PIN时,密码设置屏幕将显示数据保护已启用。
3.1 保护级别
开发者可以使用数据保护API请求几种保护级别,这些级别大致类似于为钥匙串项目设置的
kSecAttrAccessible
属性。下面来详细探讨这些级别:
| 保护级别 | 描述 |
|---|---|
| CompleteUntilFirstUserAuthentication | 这是iOS 5及更高版本的默认文件保护属性。除非明确指定其他属性,否则它将应用于所有适用的文件。其功能类似于FileProtectionComplete,但文件在用户首次重启设备并解锁后始终可用。如果有人在运行的设备上获得远程代码执行权限或存在沙盒绕过漏洞,此级别提供的保护有限,但它可以防止一些需要重启的攻击。 |
| Complete |
如果可以使用,这是最安全的文件保护类。完全保护确保在短暂延迟后,锁定设备会从内存中丢弃类密钥,使文件内容不可读。此保护级别通过NSFileManager的
NSFileProtectionComplete
属性和NSData对象的
NSDataWritingFileProtectionComplete
标志来表示。
|
| CompleteUnlessOpen |
此保护级别稍微复杂一些。使用NSFileManager时设置
NSFileProtectionCompleteUnlessOpen
标志,操作NSData存储时设置
NSDataWritingFileProtectionCompleteUnlessOpen
。它并非如名称所示,在文件被应用程序打开时禁用文件保护。实际上,它确保在设备锁定后,打开的文件仍可写入,并允许新文件写入磁盘。设备锁定时,任何具有此类别的现有文件除非事先已打开,否则无法打开。
|
以下是设置不同保护级别的代码示例:
设置NSData对象的Complete保护级别
NSData *data = [request responseData];
if (data) {
NSError *error = nil;
NSString *downloadFilePath = [NSString stringWithFormat:@"%@mydoc.pdf", NSTemporaryDirectory()];
[data writeToFile:downloadFilePath options:NSDataWritingFileProtectionComplete error:&error];
}
使用NSFileManager设置Complete保护级别
NSArray *searchPaths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *applicationDocumentsDirectory = [searchPaths lastObject];
NSString *filePath = [applicationDocumentsDirectory stringByAppendingPathComponent:@"mySensitivedata.txt"];
NSError *error = nil;
NSDictionary *attr = [NSDictionary dictionaryWithObject:NSFileProtectionComplete forKey:NSFileProtectionKey];
[[NSFileManager defaultManager] setAttributes:attr ofItemAtPath:filePath error:&error];
设置SQLite数据库的保护属性
NSString *databasePath = [documentsDirectory stringByAppendingPathComponent:@"MyNewDB.sqlite"];
sqlite3_open_v2([databasePath UTF8String], &handle, SQLITE_OPEN_CREATE|SQLITE_OPEN_READWRITE_SQLITE_OPEN_FILEPROTECTION_COMPLETEUNLESSOPEN,NULL);
3.2 CompleteUnlessOpen保护级别工作流程
CompleteUnlessOpen
保护级别的工作流程如下:
graph LR
A[生成文件密钥fK] --> B[生成额外密钥对]
B --> C[生成文件公钥fpubK和文件私钥fprvK]
C --> D[计算共享密钥ss]
D --> E[使用SHA-1哈希加密文件密钥]
E --> F[存储加密文件密钥和文件公钥到文件元数据]
F --> G[丢弃文件私钥]
G --> H[关闭文件时擦除未加密文件密钥]
具体步骤如下:
1. 与所有文件一样,生成一个文件密钥
fK
来加密文件内容。
2. 生成一个额外的密钥对,以产生文件公钥
fpubK
和文件私钥
fprvK
。
3. 使用文件私钥
fprvK
和“受保护除非打开”类公钥
cpubK
计算共享密钥
ss
。
4. 对该共享密钥进行SHA - 1哈希计算,使用哈希结果加密文件密钥。
5. 将加密后的文件密钥存储在文件的元数据中,同时存储文件公钥。
6. 系统丢弃文件私钥。
7. 关闭文件时,从内存中擦除未加密的文件密钥。
综上所述,在iOS开发中,无论是处理替代XML库、使用钥匙串存储敏感数据,还是利用数据保护API保护文件,都需要仔细考虑安全问题。通过正确设置各种属性和使用相应的API,可以有效提高应用程序的数据安全性,保护用户的敏感信息不被非法获取。
超级会员免费看
1924

被折叠的 条评论
为什么被折叠?



