一、Keychain 基础
根据苹果的介绍,iOS设备中的Keychain是一个安全的存储容器,可以用来为不同应用保存敏感信息比如用户名,密码,网络密码,认证令牌。苹果自己用keychain来保存Wi-Fi网络密码,VPN凭证等等。它是一个sqlite数据库,位于/private/var/Keychains/keychain-2.db,其保存的所有数据都是加密过的。
开发者通常会希望能够利用操作系统提供的功能来保存凭证(credentials)而不是把它们(凭证)保存到NSUserDefaults,plist文件等地方。保存这些数据的原因是开发者不想用户每次都要登录,因此会把认证信息保存到设备上的某个地方并且在用户再次打开应用的时候用这些数据自动登录。Keychain的信息是存在于每个应用(app)的沙盒之外的。
通过keychain access groups可以在应用之间共享keychain中的数据。要求在保存数据到keychain的时候指定group。把数据保存到keychain的最好方法就是用苹果提供的KeychainItemWrapper。可以到这下载例子工程。第一步就是创建这个类的实例。
KeychainItemWrapper *wrapper = [[KeychainItemWrapper alloc] initWithIdentifier:@”Password” accessGroup:nil];
标识符(Identifier)在后面我们要从keychain中取数据的时候会用到。如果你想要在应用之间共享信息,那么你需要指定访问组(access group)。有同样的访问组 的应用能够访问同样的keychain信息。
KeychainItemWrapper *wrapper = [[KeychainItemWrapper alloc] initWithIdentifier:@”Account Number” accessGroup:@”YOUR_APP_ID_HERE.com.yourcompany.GenericKeychainSuite”];
要把信息保存到keychain中,使用 setObject:forKey: 方法。在这里, (id)kSecAttrAccount 是一个预先定义好的键(key),我们可以用它来保存账号名称。 kSecClass指定了我们要保存的某类信息,在这里是一个通用的密码。kSecValueData可以被用来保存任意的数据,在这里是一个密码。
[wrapper setObject:kSecClassGenericPassword forKey:(id)kSecClass];
[wrapper setObject:@"username" forKey:(id)kSecAttrAccount];
[wrapper setObject:@"password"forKey:(id)kSecValueData];
[wrapper setObject:(id)kSecAttrAccessibleAlwaysThisDeviceOnly forKey:(id)kSecAttrAccessible];
kSecAttrAccessiblein变量用来指定这个应用合适需要访问这个数据。我们需要对这个选项特别注意,并且使用最严格的选项。这个键(key)可以设置6种值。
你可以从如下对苹果的文档的截图看到。
当然,我们应该绝对不要使用kSecAttrAccessibleAlways。一个安全点的选项是kSecAttrAccessibleWhenUnlocked。有些选项是以 ThisDeviceOnly 结尾的,如果选中了这个选项,那么数据就会被以硬件相关的密钥(key)加密,因此不能被传输到或者被其他设备看到。即使它们提供了进一步的安全性,使用它们可能不是一个好主意,除非你有一个更好的理由不允许数据在备份之间迁移。
要从keychain中获取数据,可以用 NSString *accountName = [wrapper objectForKey:(id)kSecAttrAccount];
钥匙串中的条目称为SecItem,但它是存储在CFDictionary中的。SecItemRef类型并不存在。SecItem有五类:通用密码、互联网密码、证书、密钥和身份。在大多数情况下,我们用到的都是通用密码。许多问题都是开发人员尝试用互联网密码造成的。互联网密码要复杂得多,而且相比之下优势寥寥无几,除非开发Web浏览器,否则没必要用它。KeyChainItemWrapper只使用通用密码,这也是我喜欢它的原因之一。iOS应用很少将密钥和身份存储起来,所以我们在本书中不会讨论这方面的内容。只有公钥的证书通常应该存储在文件中,而不是钥匙串中。
最后,我们需要在钥匙串中搜索需要的内容。密钥有很多个部分可用来搜索,但最好的办法是将自己的标识符赋给它,然后搜索。通用密码条目都包含属性kSecAttrGeneric,可以用它来存储标识符。这也是KeyChainItemWrapper的处理方式。
钥匙串中的条目都有几个可搜索的属性和一个加密过的值。对于通用密码条目,比较重要的属性有账户(kSecAttrAccount)、服务(kSecAttrService)和标识符(kSecAttrGeneric)。而值通常是密码。
说明:
每一个keyChain的组成如图,整体是一个字典结构.
1.kSecClass key 定义属于那一种类型的keyChain
2.不同的类型包含不同的Attributes,这些attributes定义了这个item的具体信息
3.每个item可以包含一个密码项来存储对应的密码
安全性:
从Keychain中导出数据的最流行工具是ptoomey3的Keychain dumper。其github地址位于此。现在到这个地址把它下载下来。然后解压zip文件。在解压的文件夹内,我们感兴趣的文件是keychain_dumper这个二进制文件。一个应用能够访问的keychain数据是通过其entitlements文件指定的。keychain_dumper使用一个自签名文件,带有一个*通配符的entitlments,因此它能够访问keychain中的所有条目。 当然,也有其他方法来使得所有keychain信息都被授权,比如用一个包含所有访问组(access group)的entitlements文件,或者使用一个特定的访问组(access group)使得能够访问所有的keychain数据。 例如,工具Keychain-viewer就使用如下的entitlements.
虽然keychain也容易被破解,不过比NSUserDefaults和plist安全得多,只要我们注意不要在keychain中保存明文密码就会在很大程度上提升安全性。
二、Keychain操作
iOS中Security.framework框架提供了四个主要的方法来操作KeyChain:
// 查询
OSStatus SecItemCopyMatching(CFDictionaryRef query, CFTypeRef *result);
// 添加
OSStatus SecItemAdd(CFDictionaryRef attributes, CFTypeRef *result);
// 更新
KeyChain中的ItemOSStatus SecItemUpdate(CFDictionaryRef query, CFDictionaryRef attributesToUpdate);
// 删除
KeyChain中的ItemOSStatus SecItemDelete(CFDictionaryRef query)
三、Keychain使用
基本搜索字典
所有的调用钥匙链服务使用一个字典定义密钥链项的属性你想找到,创建、更新或删除。 所以我们要做的第一件事就是定义一个函数来分配,构建这本字典对我们来说:
static NSString *serviceName = @"com.mycompany.myAppServiceName";
- (NSMutableDictionary *)newSearchDictionary:(NSString *)identifier {
NSMutableDictionary *searchDictionary = [[NSMutableDictionary alloc] init];
[searchDictionary setObject:(id)kSecClassGenericPassword forKey:(id)kSecClass];
NSData *encodedIdentifier = [identifier dataUsingEncoding:NSUTF8StringEncoding];
[searchDictionary setObject:encodedIdentifier forKey:(id)kSecAttrGeneric];
[searchDictionary setObject:encodedIdentifier forKey:(id)kSecAttrAccount];
[searchDictionary setObject:serviceName forKey:(id)kSecAttrService];
return searchDictionary;
}
字典包含三个项目。 第一个与关键 kSecClass 定义的类密钥链项我们会处理。 我想存储密码的钥匙链我使用价值 kSecClassGenericPassword 的价值。
第二项的字典与关键 kSecAttrGeneric 我们将使用确定密钥链项。 它可以是任何值我们选择等 一个€œPassworda€ 或 一个€œLicenseKeya€ 等等。很明显这不是密码的实际价值只是一个标签我们将连接到这个密钥链项我们可以找到它。 理论上我们的应用程序可以存储的密码钥匙链,所以我们需要一种方法来识别这个特殊的人。 标识符必须编码之前添加到字典中
最后的两个属性的组合 kSecAttrAccount 和 kSecAttrService 这个钥匙链应该设置为一些独特的东西。 在这个例子中,我将服务名称设置为一个静态的字符串和重用作为账户名称的标识符。
您可以使用多个属性对于一个给定的项目。 其他的一些属性,我们也可以使用 kSecClassGenericPassword 项目包括一个账户名称、描述等。但是通过仅使用一个属性我们可以简化代码的其余部分。
搜索钥匙链
发现如果我们的密码已经存在于钥匙扣(密码的值是)我们使用 SecItemCopyMatching 函数。 但首先我们添加两个额外的物品基本搜索字典:
- (NSData *)searchKeychainCopyMatching:(NSString *)identifier {
NSMutableDictionary *searchDictionary = [self newSearchDictionary:identifier];
// Add search attributes
[searchDictionary setObject:(id)kSecMatchLimitOne forKey:(id)kSecMatchLimit];
// Add search return types
[searchDictionary setObject:(id)kCFBooleanTrue forKey:(id)kSecReturnData];
NSData *result = nil;
OSStatus status = SecItemCopyMatching((CFDictionaryRef)searchDictionary,
(CFTypeRef *)&result);
[searchDictionary release];
return result;
}
我们添加到字典的第一属性是限制搜索结果的数量得到返回。 我们正在寻找一个条目设置属性 kSecMatchLimit 来 kSecMatchLimitOne 。
下一个属性决定了返回结果。 因为在我们简单的情况下,我们预计只有一个属性返回(密码)我们可以设置属性 kSecReturnData 来 kCFBooleanTrue 。 这意味着我们会拿回一个NSData参考,我们可以直接访问。
如果我们存储和搜索密钥链项与多个属性(例如,如果我们将一个账户名和密码存储在相同的密钥链项)我们需要添加属性 kSecReturnAttributes 和属性的结果将是一个字典。
现在的搜索字典设置我们所说的 SecItemCopyMatching 功能,如果我们的项目存在于钥匙扣密码的值返回给NSData块。 获得实际的解码字符串可以做一些类似:
NSData *passwordData = [self searchKeychainCopyMatching:@"Password"];
if (passwordData) {
NSString *password = [[NSString alloc] initWithData:passwordData
encoding:NSUTF8StringEncoding];
[passwordData release];
}
创建一个项目在钥匙链
添加一个条目前面的例子几乎是一样的,除了我们需要设置密码我们想要存储的值。
- (BOOL)createKeychainValue:(NSString *)password forIdentifier:(NSString *)identifier {
NSMutableDictionary *dictionary = [self newSearchDictionary:identifier];
NSData *passwordData = [password dataUsingEncoding:NSUTF8StringEncoding];
[dictionary setObject:passwordData forKey:(id)kSecValueData];
OSStatus status = SecItemAdd((CFDictionaryRef)dictionary, NULL);
[dictionary release];
if (status == errSecSuccess) {
return YES;
}
return NO;
}
设置密码我们添加属性的值 kSecValueData 我们的搜索字典确保我们编码字符串,然后调用 SecItemAdd 通过字典作为第一个参数。 如果项目已经存在在钥匙链将会失败。
更新密钥链项
更新一个钥匙链类似于唯一不同的是使用一个单独的字典中添加一个条目包含更新的属性。 因为在我们的例子中,我们仅更新一个属性(密码)这很简单:
- (BOOL)updateKeychainValue:(NSString *)password forIdentifier:(NSString *)identifier {
NSMutableDictionary *searchDictionary = [self newSearchDictionary:identifier];
NSMutableDictionary *updateDictionary = [[NSMutableDictionary alloc] init];
NSData *passwordData = [password dataUsingEncoding:NSUTF8StringEncoding];
[updateDictionary setObject:passwordData forKey:(id)kSecValueData];
OSStatus status = SecItemUpdate((CFDictionaryRef)searchDictionary,
(CFDictionaryRef)updateDictionary);
[searchDictionary release];
[updateDictionary release];
if (status == errSecSuccess) {
return YES;
}
return NO;
}
删除一个项目从钥匙链
最后一个(简单的)操作是删除一个条目的钥匙扣使用 SecItemDelete 功能和我们通常搜索字典:
- (void)deleteKeychainValue:(NSString *)identifier {
NSMutableDictionary *searchDictionary = [self newSearchDictionary:identifier];
SecItemDelete((CFDictionaryRef)searchDictionary);
[searchDictionary release];
}
四、KeyChain相关参数
mark- 密钥类型
//密钥类型键
//CFTypeRef kSecClass
//
//值
//CFTypeRef kSecClassGenericPassword //一般密码
//CFTypeRef kSecClassInternetPassword //网络密码
//CFTypeRef kSecClassCertificate //证书
//CFTypeRef kSecClassKey //密钥
//CFTypeRef kSecClassIdentity //身份证书(带私钥的证书)
//
//不同类型的钥匙串项对应的属性不同
//
//一般密码
//kSecClassGenericPassword
//
//对应属性
//kSecAttrAccessible
//kSecAttrAccessGroup
//kSecAttrCreationDate
//kSecAttrModificationDate
//kSecAttrDescription
//kSecAttrComment
//kSecAttrCreator
//kSecAttrType
//kSecAttrLabel
//kSecAttrIsInvisible
//kSecAttrIsNegative
//kSecAttrAccount
//kSecAttrService
//kSecAttrGeneric
//网络密码
//kSecClassInternetPassword
//
//对应属性
//kSecAttrAccessible
//kSecAttrAccessGroup
//kSecAttrCreationDate
//kSecAttrModificationDate
//kSecAttrDescription
//kSecAttrComment
//kSecAttrCreator
//kSecAttrType
//kSecAttrLabel
//kSecAttrIsInvisible
//kSecAttrIsNegative
//kSecAttrAccount
//kSecAttrSecurityDomain
//kSecAttrServer
//kSecAttrProtocol
//kSecAttrAuthenticationType
//kSecAttrPort
//kSecAttrPath
//证书
//kSecClassCertificate
//
//对应属性
//kSecAttrAccessible
//kSecAttrAccessGroup
//kSecAttrCertificateType
//kSecAttrCertificateEncoding
//kSecAttrLabel
//kSecAttrSubject
//kSecAttrIssuer
//kSecAttrSerialNumber
//kSecAttrSubjectKeyID
//kSecAttrPublicKeyHash
//密钥
//kSecClassKey
//
//对应属性
//kSecAttrAccessible
//kSecAttrAccessGroup
//kSecAttrKeyClass
//kSecAttrLabel
//kSecAttrApplicationLabel
//kSecAttrIsPermanent
//kSecAttrApplicationTag
//kSecAttrKeyType
//kSecAttrKeySizeInBits
//kSecAttrEffectiveKeySize
//kSecAttrCanEncrypt
//kSecAttrCanDecrypt
//kSecAttrCanDerive
//kSecAttrCanSign
//kSecAttrCanVerify
//kSecAttrCanWrap
//kSecAttrCanUnwrap
//身份证书(带私钥的证书)
//kSecClassIdentity
//
//对应属性
// 证书属性
// 私钥属性
mark- 属性
//键
//CFTypeRef kSecAttrAccessible; //可访问性 类型透明
//值
// CFTypeRef kSecAttrAccessibleWhenUnlocked; //解锁可访问,备份
// CFTypeRef kSecAttrAccessibleAfterFirstUnlock; //第一次解锁后可访问,备份
// CFTypeRef kSecAttrAccessibleAlways; //一直可访问,备份
// CFTypeRef kSecAttrAccessibleWhenUnlockedThisDeviceOnly; //解锁可访问,不备份
// CFTypeRef kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly;//第一次解锁后可访问,不备份
// CFTypeRef kSecAttrAccessibleAlwaysThisDeviceOnly; //一直可访问,不备份
//CFTypeRef kSecAttrCreationDate; //创建日期 CFDateRef
//CFTypeRef kSecAttrModificationDate; //最后一次修改日期 CFDateRef
//CFTypeRef kSecAttrDescription; //描述 CFStringRef
//CFTypeRef kSecAttrComment; //注释 CFStringRef
//CFTypeRef kSecAttrCreator; //创建者 CFNumberRef(4字符,如’aLXY’)
//CFTypeRef kSecAttrType; //类型 CFNumberRef(4字符,如’aTyp’)
//CFTypeRef kSecAttrLabel; //标签(给用户看) CFStringRef
//CFTypeRef kSecAttrIsInvisible; //是否隐藏 CFBooleanRef(kCFBooleanTrue,kCFBooleanFalse)
//CFTypeRef kSecAttrIsNegative; //是否具有密码 CFBooleanRef(kCFBooleanTrue,kCFBooleanFalse)此项表示当前的item是否只是一个占位项,或者说是只有key没有value。
//CFTypeRef kSecAttrAccount; //账户名 CFStringRef
//CFTypeRef kSecAttrService; //所具有服务 CFStringRef
//CFTypeRef kSecAttrGeneric; //用户自定义内容 CFDataRef
//CFTypeRef kSecAttrSecurityDomain; //网络安全域 CFStringRef
//CFTypeRef kSecAttrServer; //服务器域名或IP地址 CFStringRef
//键
//CFTypeRef kSecAttrProtocol; //协议类型 CFNumberRef
// 值
// CFTypeRef kSecAttrProtocolFTP; //
// CFTypeRef kSecAttrProtocolFTPAccount; //
// CFTypeRef kSecAttrProtocolHTTP; //
// CFTypeRef kSecAttrProtocolIRC; //
// CFTypeRef kSecAttrProtocolNNTP; //
// CFTypeRef kSecAttrProtocolPOP3; //
// CFTypeRef kSecAttrProtocolSMTP; //
// CFTypeRef kSecAttrProtocolSOCKS; //
// CFTypeRef kSecAttrProtocolIMAP; //
// CFTypeRef kSecAttrProtocolLDAP; //
// CFTypeRef kSecAttrProtocolAppleTalk; //
// CFTypeRef kSecAttrProtocolAFP; //
// CFTypeRef kSecAttrProtocolTelnet; //
// CFTypeRef kSecAttrProtocolSSH; //
// CFTypeRef kSecAttrProtocolFTPS; //
// CFTypeRef kSecAttrProtocolHTTPS; //
// CFTypeRef kSecAttrProtocolHTTPProxy; //
// CFTypeRef kSecAttrProtocolHTTPSProxy; //
// CFTypeRef kSecAttrProtocolFTPProxy; //
// CFTypeRef kSecAttrProtocolSMB; //
// CFTypeRef kSecAttrProtocolRTSP; //
// CFTypeRef kSecAttrProtocolRTSPProxy; //
// CFTypeRef kSecAttrProtocolDAAP; //
// CFTypeRef kSecAttrProtocolEPPC; //
// CFTypeRef kSecAttrProtocolIPP; //
// CFTypeRef kSecAttrProtocolNNTPS; //
// CFTypeRef kSecAttrProtocolLDAPS; //
// CFTypeRef kSecAttrProtocolTelnetS; //
// CFTypeRef kSecAttrProtocolIMAPS; //
// CFTypeRef kSecAttrProtocolIRCS; //
// CFTypeRef kSecAttrProtocolPOP3S; //
//键
//CFTypeRef kSecAttrAuthenticationType; //认证类型 CFNumberRef
// 值
// CFTypeRef kSecAttrAuthenticationTypeNTLM; //
// CFTypeRef kSecAttrAuthenticationTypeMSN; //
// CFTypeRef kSecAttrAuthenticationTypeDPA; //
// CFTypeRef kSecAttrAuthenticationTypeRPA; //
// CFTypeRef kSecAttrAuthenticationTypeHTTPBasic; //
// CFTypeRef kSecAttrAuthenticationTypeHTTPDigest; //
// CFTypeRef kSecAttrAuthenticationTypeHTMLForm; //
// CFTypeRef kSecAttrAuthenticationTypeDefault; //
//CFTypeRef kSecAttrPort; //网络端口 CFNumberRef
//CFTypeRef kSecAttrPath; //访问路径 CFStringRef
//CFTypeRef kSecAttrSubject; //X.500主题名称 CFDataRef
//CFTypeRef kSecAttrIssuer; //X.500发行者名称 CFDataRef
//CFTypeRef kSecAttrSerialNumber; //序列号 CFDataRef
//CFTypeRef kSecAttrSubjectKeyID; //主题ID CFDataRef
//CFTypeRef kSecAttrPublicKeyHash; //公钥Hash值 CFDataRef
//CFTypeRef kSecAttrCertificateType; //证书类型 CFNumberRef
//CFTypeRef kSecAttrCertificateEncoding; //证书编码类型 CFNumberRef
//CFTypeRef kSecAttrKeyClass; //加密密钥类 CFTypeRef
// 值
// CFTypeRef kSecAttrKeyClassPublic; //公钥
// CFTypeRef kSecAttrKeyClassPrivate; //私钥
// CFTypeRef kSecAttrKeyClassSymmetric; //对称密钥
//CFTypeRef kSecAttrApplicationLabel; //标签(给程序使用) CFStringRef(通常是公钥的Hash值)
//CFTypeRef kSecAttrIsPermanent; //是否永久保存加密密钥 CFBooleanRef
//CFTypeRef kSecAttrApplicationTag; //标签(私有标签数据) CFDataRef
//CFTypeRef kSecAttrKeyType; //加密密钥类型(算法) CFNumberRef
// 值
// extern const CFTypeRef kSecAttrKeyTypeRSA;
//CFTypeRef kSecAttrKeySizeInBits; //密钥总位数 CFNumberRef
//CFTypeRef kSecAttrEffectiveKeySize; //密钥有效位数 CFNumberRef
//CFTypeRef kSecAttrCanEncrypt; //密钥是否可用于加密 CFBooleanRef
//CFTypeRef kSecAttrCanDecrypt; //密钥是否可用于加密 CFBooleanRef
//CFTypeRef kSecAttrCanDerive; //密钥是否可用于导出其他密钥 CFBooleanRef
//CFTypeRef kSecAttrCanSign; //密钥是否可用于数字签名 CFBooleanRef
//CFTypeRef kSecAttrCanVerify; //密钥是否可用于验证数字签名 CFBooleanRef
//CFTypeRef kSecAttrCanWrap; //密钥是否可用于打包其他密钥 CFBooleanRef
//CFTypeRef kSecAttrCanUnwrap; //密钥是否可用于解包其他密钥 CFBooleanRef
//CFTypeRef kSecAttrAccessGroup; //访问组 CFStringRef
mark- 搜索
//CFTypeRef kSecMatchPolicy; //指定策略 SecPolicyRef
//CFTypeRef kSecMatchItemList; //指定搜索范围 CFArrayRef(SecKeychainItemRef, SecKeyRef, SecCertificateRef, SecIdentityRef,CFDataRef)数组内的类型必须唯一。仍然会搜索钥匙串,但是搜索结果需要与该数组取交集作为最终结果。
//CFTypeRef kSecMatchSearchList; //
//CFTypeRef kSecMatchIssuers; //指定发行人数组 CFArrayRef
//CFTypeRef kSecMatchEmailAddressIfPresent; //指定邮件地址 CFStringRef
//CFTypeRef kSecMatchSubjectContains; //指定主题 CFStringRef
//CFTypeRef kSecMatchCaseInsensitive; //指定是否不区分大小写 CFBooleanRef(kCFBooleanFalse或不提供此参数,区分大小写;kCFBooleanTrue,不区分大小写)
//CFTypeRef kSecMatchTrustedOnly; //指定只搜索可信证书 CFBooleanRef(kCFBooleanFalse或不提供此参数,全部证书;kCFBooleanTrue,只搜索可信证书)
//CFTypeRef kSecMatchValidOnDate; //指定有效日期 CFDateRef(kCFNull表示今天)
//CFTypeRef kSecMatchLimit; //指定结果数量 CFNumberRef(kSecMatchLimitOne;kSecMatchLimitAll)
//CFTypeRef kSecMatchLimitOne; //首条结果
//CFTypeRef kSecMatchLimitAll; //全部结果
mark- 列表
//CFTypeRef kSecUseItemList; //CFArrayRef(SecKeychainItemRef, SecKeyRef, SecCertificateRef, SecIdentityRef,CFDataRef)数组内的类型必须唯一。用户提供用于查询的列表。当这个列表被提供的时候,不会再搜索钥匙串。
mark- 返回值类型
//可以同时指定多种返回值类型
//CFTypeRef kSecReturnData; //返回数据(CFDataRef) CFBooleanRef
//CFTypeRef kSecReturnAttributes; //返回属性字典(CFDictionaryRef) CFBooleanRef
//CFTypeRef kSecReturnRef; //返回实例(SecKeychainItemRef, SecKeyRef, SecCertificateRef, SecIdentityRef, or CFDataRef) CFBooleanRef
//CFTypeRef kSecReturnPersistentRef; //返回持久型实例(CFDataRef) CFBooleanRef
mark- 写入值类型
//CFTypeRef kSecValueData;
//CFTypeRef kSecValueRef;
//CFTypeRef kSecValuePersistentRef;