OC学习57:keyChain钥匙串存储

张建 lol

前言

本文将向大家介绍如何使用 iOS Keychain(钥匙串)保护用户敏感数据iOS Keychain 是一个安全的存储解决方案,可以将 用户名、密码、证书、私钥等 敏感数据存储在设备的安全存储区域中,并且只有经过身份验证的应用程序才能访问这些数据。

为什么不使用 NSUserDefaults 存储敏感数据?

不安全:因为 NSUserDefaults 将数据以 plist文件 形式存储到 沙盒 中的 Library/Preferences 中,如果 沙盒被破解或手机被越狱,那么就能轻松拿到这个文件,就能轻松读取都存储的信息,不安全

如何解决把 密码等敏感数据存储在 NSUserDefaults 不安全的问题?

  1. 通过 加密算法加密,然后存储,例如 不可逆的MD5加密
  • 导入头文件
1
#import <CommonCrypto/CommonDigest.h>
  • 简单的 MD5 加密
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 简单的MD5加密
+ ( NSString *)md5String:( NSString *)str{

const char *myPasswd = [str UTF8String ];

unsigned char mdc[ 16 ];

CC_MD5 (myPasswd, ( CC_LONG ) strlen (myPasswd), mdc);

NSMutableString *md5String = [ NSMutableString string ];

for ( int i = 0 ; i< 16 ; i++) {

[md5String appendFormat : @"%02x" ,mdc[i]];

}

return md5String;

}
  • 复杂的MD5加密
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 复杂一些的MD5加密
+ ( NSString *)md5String:( NSString *)str{

const char *myPasswd = [str UTF8String ];

unsigned char mdc[ 16 ];

CC_MD5 (myPasswd, ( CC_LONG ) strlen (myPasswd), mdc);

NSMutableString *md5String = [ NSMutableString string ];

[md5String appendFormat : @"%02x" ,mdc[ 0 ]];

for ( int i = 1 ; i< 16 ; i++) {

[md5String appendFormat : @"%02x" ,mdc[i]^mdc[ 0 ]];
}
return md5String;
}
  1. 使用 keyChain 来保存密码

更加保险的方法是把密码保存在iOS提供的 keychain 中,并且删除应用后,密码不会删除,下载安装还能使用

什么是 iOS Keychain?

  • Keychain 是一个存储在文件下的简单数据库。Keychain 有任意数量的 钥匙链(item),该钥匙链中包含一组属性。该属性和钥匙链的 类型 相关。创建日期和label对所有的钥匙链是通用的。其他的都是根据钥匙链的类型不同而不同,比如,generic password类型 包含 serviceaccount 属性。

  • 钥匙链可以使用 kSecAttrSynchronizable 同步属性,被标记为该属性的值都可以被放置在iCloud 的钥匙链中,它会被自动同步到相同账号的设备上。

  • 有些钥匙链需要保护起来,比如 密码和私人key,都会被加密;对于那些不需要被保护的钥匙链,比如证书,就不会被加密。

  • 在iOS设备上(手机),当屏幕被解锁时,钥匙链的访问权限就会被打开。

  • iOS Keychain 是一个安全的存储解决方案,可以将敏感数据存储在设备的安全存储区域中。这个安全存储区域被称为 Keychain,它是一种加密的数据库,可以存储 用户名、密码、证书、私钥等 敏感数据。Keychain可以防止未经授权的应用程序访问用户的敏感数据。

  • 除了做 数据存储,它还可以做 App间的数据共享

如何在iOS应用程序中使用Keychain?

  • 使用 Keychain API 在iOS应用程序中 存储和访问 敏感数据是很容易的。

  • Objective-C 中,可以使用 Security 框架中的 SecItem API 来执行 Keychain操作。

  • Swift 中,可以使用 Security 框架中的 Security.framework 。这个框架提供了一些函数和数据结构,可以帮助我们执行Keychain操作。

访问

keychain 保存的 类型,有5中:

  • kSecClassGenericPassword:存储一般密码,比较常用的item

  • kSecClassInternetPasswork:存储网络密码的item

  • kSecClassCertificate:存储证书的item

  • kSecClassKey:存储私有秘钥的item

  • kSecClassIdentity:存储一个包含证书和私有秘钥的item

使用Keychain API来存储和访问敏感数据的基本步骤

  1. 创建一个 字典,用于 描述要存储的敏感数据

  2. 调用 SecItemAdd 函数来将敏感数据添加到Keychain中。

1
2
OSStatus SecItemAdd(CFDictionaryRef attributes, CFTypeRef * __nullable CF_RETURNS_RETAINED result)
API_AVAILABLE(macos(10.6), ios(2.0));

attributes由四部分组成:

  • item的类型:必选,key = kSecClass,value = 上面说的5种item

  • item 的存储数据

  • 属性,可以用来做一些标记,用于查找或者程序之间的数据共享,但是不同的kSecClass有不同的属性

  • 返回的数据类型,它的设置影响参数 result:对添加的item的引用,如果没有什么需要,可以设置为 nil

注:
1、要保证每次添加都是不同账户
2、注意变量内存释放
3、要注意数据类型要对

  1. 调用 SecItemCopyMatching 函数来从Keychain中检索敏感数据。
1
2
OSStatus SecItemCopyMatching(CFDictionaryRef query, CFTypeRef * __nullable CF_RETURNS_RETAINED result)
API_AVAILABLE(macos(10.6), ios(2.0));

query的组成:

  • item的类型:必选,key = kSecClass,value = 上面说的5种item

  • 要查找的属性及值,这个可以用来筛选掉多余的选项,这个值越详细,最后获取的结果越精确

  • 做进一步的限制,比如大小写是否敏感等等

  • 返回匹配项的个数,但是注意 kSecReturnDatakSecMatchLimit 不能共存,更多的kSecMatchLimit 可以搭配 kSecReturnAttributes 使用

注:
注意一下返回类型

  1. 调用 SecItemUpdate 函数来更新Keychain中的敏感数据。

  2. 调用 SecItemDelete 函数来删除Keychain中的敏感数据。

1
2
OSStatus SecItemDelete(CFDictionaryRef query)
API_AVAILABLE(macos(10.6), ios(2.0));

iOS 中,我们可以使用 Keychain API 来存储和访问敏感数据,下面以 OC 语言为例,实现一个存储 企业ID、账号、密码 的例子:

使用

  1. 创建一个 ZJKeyChainManager 管理类
  • ZJKeyChainManager.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@interface ZJKeyChainManager : NSObject

+ (NSMutableDictionary *)getKeychainQuery:(NSString *)service;

// save enterprise account password to keychain
+ (void)save:(NSString *)service data:(id)data;

// load enterprise account password from keychain
+ (id)load:(NSString *)service;

// delete enterprise account password from keychain
+ (void)delete:(NSString *)serviece;

#pragma mark - public
// 保存登录信息到钥匙串
+ (void)saveLoginInfoToKeyChain;
// 读取钥匙串登录信息
+ (NSMutableArray *)readLoginInfoFromKeyChain;
// 删除钥匙串缓存登录信息
+ (void)deleteLoginInfoFromKeyChain;
@end
  • ZJKeyChainManager.m
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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
#import "ZJKeyChainManager.h"
#import <Security/Security.h>

@implementation ZJKeyChainManager
#pragma mark - 创建一个字典,描述要存储的敏感数据
+ (NSMutableDictionary *)getKeychainQuery:(NSString *)service {
return [NSMutableDictionary dictionaryWithObjectsAndKeys:
(id)kSecClassGenericPassword,(id)kSecClass,
service, (id)kSecAttrService,
service, (id)kSecAttrAccount,
(id)kSecAttrAccessibleAfterFirstUnlock,(id)kSecAttrAccessible,
nil];
}

#pragma mark -写入
+ (void)save:(NSString *)service data:(id)data {
// Get search dictionary
NSMutableDictionary *keychainQuery = [self getKeychainQuery:service];
// Delete old item before add new item
SecItemDelete((CFDictionaryRef)keychainQuery);
// Add new object to search dictionary(Attention:the data format)
[keychainQuery setObject:[NSKeyedArchiver archivedDataWithRootObject:data] forKey:(id)kSecValueData];
// Add item to keychain with the search dictionary
SecItemAdd((CFDictionaryRef)keychainQuery, NULL);
}

#pragma mark -读取
+ (id)load:(NSString *)service {
id ret = nil;
NSMutableDictionary *keychainQuery = [self getKeychainQuery:service];
// Configure the search setting
// Since in our simple case we are expecting only a single attribute to be returned (the password) we can set the attribute kSecReturnData to kCFBooleanTrue
[keychainQuery setObject:(id)kCFBooleanTrue forKey:(id)kSecReturnData];
[keychainQuery setObject:(id)kSecMatchLimitOne forKey:(id)kSecMatchLimit];
CFDataRef keyData = NULL;
if (SecItemCopyMatching((CFDictionaryRef)keychainQuery, (CFTypeRef *)&keyData) == noErr) {
@try {
ret = [NSKeyedUnarchiver unarchiveObjectWithData:(__bridge NSData *)keyData];
} @catch (NSException *e) {
NSLog(@"Unarchive of %@ failed: %@", service, e);
} @finally {
}
}
if (keyData)
CFRelease(keyData);
return ret;
}

#pragma mark -删除
+ (void)delete:(NSString *)service {
NSMutableDictionary *keychainQuery = [self getKeychainQuery:service];
SecItemDelete((CFDictionaryRef)keychainQuery);
}

#pragma mark - public
// 保存登录信息到钥匙串
+ (void)saveLoginInfoToKeyChain{
// 企业ID
NSString *enterpriseId = [NSObject getSaveDataWithKey:JITENTERPRISEID];
// 账号
NSString *account = [NSObject getSaveDataWithKey:JITACCOUNT];
// 密码
NSString *password = [NSObject getSaveDataWithKey:JITPASSWORD];

// 需要存储的键值对
NSMutableDictionary * kvPair = [NSMutableDictionary dictionary];
[kvPair setObject:enterpriseId forKey:KEY_ENTERPRISEID];
[kvPair setObject:account forKey:KEY_ACCOUNT];
[kvPair setObject:password forKey:KEY_PASSWORD];
NSLog(@"%@", kvPair);

// 将存储内容写入keychain
[ZJKeyChainManager save:KEY_ENTERPRISEID_ACCOUNT_PASSWORD data:kvPair];
}
// 读取钥匙串登录信息
+ (NSMutableArray *)readLoginInfoFromKeyChain{
NSMutableArray * mArr = @[].mutableCopy;

// 从keychain中读取存储内容
NSMutableDictionary * readKvPair = (NSMutableDictionary *)[ZJKeyChainManager load:KEY_ENTERPRISEID_ACCOUNT_PASSWORD];
NSString *enterpriseId = [readKvPair objectForKey:KEY_ENTERPRISEID];
NSString *account = [readKvPair objectForKey:KEY_ACCOUNT];
NSString *password = [readKvPair objectForKey:KEY_PASSWORD];
NSLog(@"enterpriseId = %@ account = %@ password = %@", enterpriseId,account,password);

[mArr addObjectsFromArray:@[enterpriseId,account,password]];

return mArr;
}
// 删除钥匙串缓存登录信息
+ (void)deleteLoginInfoFromKeyChain{
// 将存储内容从keychain中删除
[ZJKeyChainManager delete:KEY_ENTERPRISEID_ACCOUNT_PASSWORD];
}

@end

  1. 外部使用
  • 声明键
1
2
3
4
5
6
7
8
// 服务
#define KEY_ENTERPRISEID_ACCOUNT_PASSWORD @"cn.com.jit.DigitalGuard.enterpriseaccountpassword"
// 企业ID
#define KEY_ENTERPRISEID @"cn.com.jit.DigitalGuard.enterpriseid"
// 账号
#define KEY_ACCOUNT @"cn.com.jit.DigitalGuard.account"
// 密码
#define KEY_PASSWORD @"cn.com.jit.DigitalGuard.password"
  • 需要存储的键值对
1
2
3
4
5
6
// 需要存储的键值对
NSMutableDictionary * kvPair = [NSMutableDictionary dictionary];
[kvPair setObject:enterpriseId forKey:KEY_ENTERPRISEID];
[kvPair setObject:account forKey:KEY_ACCOUNT];
[kvPair setObject:password forKey:KEY_PASSWORD];
DLogInfo(@"%@", kvPair);
  • 将存储内容写入keychain
1
2
// 将存储内容写入keychain
[ZJKeyChainManager save:KEY_ENTERPRISEID_ACCOUNT_PASSWORD data:kvPair];
  • 从keychain中读取存储内容
1
2
3
4
5
NSMutableDictionary * readKvPair = (NSMutableDictionary *)[ZJKeyChainManager load:KEY_ENTERPRISEID_ACCOUNT_PASSWORD];
NSString *enterpriseId = [readKvPair objectForKey:KEY_ENTERPRISEID];
NSString *account = [readKvPair objectForKey:KEY_ACCOUNT];
NSString *password = [readKvPair objectForKey:KEY_PASSWORD];
NSLog(@"enterpriseId = %@ account = %@ password = %@", enterpriseId,account,password);
  • 将存储内容从keychain中删除
1
[ZJKeyChainManager delete:KEY_ENTERPRISEID_ACCOUNT_PASSWORD];

其他

保护用户数据安全,我们可以考虑以下几种方案:

  • 使用 Keychain 来存储敏感数据

Keychain 是iOS中一种 安全的存储方式,可以用于存储 用户名、密码、证书、密钥等 敏感数据。Keychain中存储的 数据是加密的只能被当前应用程序访问。因此,使用Keychain来存储敏感数据可以增强应用程序的安全性。

  • 加密敏感数据

如果您需要在应用程序中存储敏感数据,您可以 使用加密算法对数据进行加密。这样可以确保即使数据被盗,攻击者也无法访问敏感信息。

  • 使用HTTPS协议

当您的应用程序需要与服务器进行通信时,建议使用HTTPS协议。HTTPS使用TLS/SSL协议来加密数据,从而确保通信安全。

总结

本文主要介绍了如何使用 keyChain 来存储和访问iOS应用程序中的敏感数据,同时也介绍了一些保护数据安全的一些方案,通过这些措施,我们可以保护用户的数据安全,增强应用程序的安全性。

  • Post title:OC学习57:keyChain钥匙串存储
  • Post author:张建
  • Create time:2023-06-02 11:38:45
  • Post link:https://redefine.ohevan.com/2023/06/02/OC/OC学习57:keyChain钥匙串存储/
  • Copyright Notice:All articles in this blog are licensed under BY-NC-SA unless stating additionally.