iOS Keychain 存储密码

519 阅读8分钟

一、Keychain

1、概念

Keychain是一个储存在文件下的简单数据库。通常情况下,app里有一个简单的keychain,可以被所有的app使用。 Keychain有任意数量的钥匙链(item),该钥匙链里包含一组属性。该属性和钥匙链的类型相关。创建日期和label对所有的钥匙链是通用的。其他的都是根据钥匙链的类型不同而不同,比如,generic password类型包含service和account属性。 钥匙链可以使用kSecAttrSynchronizable同步属性,被标记为该属性的值都可以被放置在iCloud的钥匙链中,它会被自动同步到相同账号的设备上。 有些钥匙链需要保护起来,比如密码和私人key,都会被加密;对于那些不需要被保护的钥匙链,比如证书,就不会被加密。 在iOS设备上(手机),当屏幕被解锁时,钥匙链的访问权限就会被打开。

2、访问

首先说明其能保存的类型,有5种: kSecClassGenericPassword:存储一般密码,比较常用这个 kSecClassInternetPassword:存储网络密码 kSecClassCertificate:存储证书 kSecClassKey:存储私有密钥 kSecClassIdentity:存储一个包含证书和私有密钥的item

3、Keychain 的一些Key 和 Value 的解释和介绍

3.1、设置信息的保密程度

image.png

3.2、kSecClass 的可选value

image.png

3.3、kSecClassGenericPassword 密码所包含的所有类型参数

image.png

其他密码的参数和普通密码的参数大致相同。

4、代码实例 KeyChainManager

#import <Foundation/Foundation.h>

@interface KeyChainManager : NSObject

/*!
 保存数据

 @data  要存储的数据
 @identifier 存储数据的标示
 */
+(BOOL) keyChainSaveData:(id)data withIdentifier:(NSString*)identifier ;

/*!
 读取数据

 @identifier 存储数据的标示
 */
+(id) keyChainReadData:(NSString*)identifier ;


/*!
 更新数据

 @data  要更新的数据
 @identifier 数据存储时的标示
 */
+(BOOL)keyChainUpdata:(id)data withIdentifier:(NSString*)identifier ;

/*!
 删除数据

 @identifier 数据存储时的标示
 */
+(void) keyChainDelete:(NSString*)identifier ;

@end

#import "KeyChainManager.h"
#import <Security/Security.h>


@implementation KeyChainManager
/*!
 创建生成保存数据查询条件
 */
+(NSMutableDictionary*) keyChainIdentifier:(NSString*)identifier {
    NSMutableDictionary * keyChainMutableDictionary = [NSMutableDictionary dictionaryWithObjectsAndKeys:(id)kSecClassGenericPassword,kSecClass,identifier,kSecAttrService,identifier,kSecAttrAccount,kSecAttrAccessibleAfterFirstUnlock,kSecAttrAccessible, nil];
    return keyChainMutableDictionary;
}

/*!
 保存数据
 */
+(BOOL) keyChainSaveData:(id)data withIdentifier:(NSString*)identifier{
    // 获取存储的数据的条件
    NSMutableDictionary * saveQueryMutableDictionary = [self keyChainIdentifier:identifier];
    // 删除旧的数据
    SecItemDelete((CFDictionaryRef)saveQueryMutableDictionary);
    // 设置新的数据
    [saveQueryMutableDictionary setObject:[NSKeyedArchiver archivedDataWithRootObject:data] forKey:(id)kSecValueData];
    // 添加数据
   OSStatus saveState = SecItemAdd((CFDictionaryRef)saveQueryMutableDictionary, nil);
    // 释放对象
    saveQueryMutableDictionary = nil ;
    // 判断是否存储成功
    if (saveState == errSecSuccess) {
        return YES;
    }
    return NO;
}


/*!
 读取数据
 */
+(id) keyChainReadData:(NSString*)identifier{
    id idObject = nil ;
    // 通过标记获取数据查询条件
    NSMutableDictionary * keyChainReadQueryMutableDictionary = [self keyChainIdentifier:identifier];
    // 这是获取数据的时,必须提供的两个属性
    // TODO: 查询结果返回到 kSecValueData
    [keyChainReadQueryMutableDictionary setObject:(id)kCFBooleanTrue forKey:(id)kSecReturnData];
    // TODO: 只返回搜索到的第一条数据
    [keyChainReadQueryMutableDictionary setObject:(id)kSecMatchLimitOne forKey:(id)kSecMatchLimit];
    // 创建一个数据对象
    CFDataRef keyChainData = nil ;
    // 通过条件查询数据
    if (SecItemCopyMatching((CFDictionaryRef)keyChainReadQueryMutableDictionary , (CFTypeRef *)&keyChainData) == noErr){
        @try {
            idObject = [NSKeyedUnarchiver unarchiveObjectWithData:(__bridge NSData *)(keyChainData)];
        } @catch (NSException * exception){
            NSLog(@"Unarchive of search data where %@ failed of %@ ",identifier,exception);
        }
    }
    if (keyChainData) {
        CFRelease(keyChainData);
    }
    // 释放对象
    keyChainReadQueryMutableDictionary = nil;
    // 返回数据
    return idObject ;
}


/*!
 更新数据

 @data  要更新的数据
 @identifier 数据存储时的标示
 */
+(BOOL)keyChainUpdata:(id)data withIdentifier:(NSString*)identifier {
    // 通过标记获取数据更新的条件
    NSMutableDictionary * keyChainUpdataQueryMutableDictionary = [self keyChainIdentifier:identifier];
    // 创建更新数据字典
    NSMutableDictionary * updataMutableDictionary = [NSMutableDictionary dictionaryWithCapacity:0];
    // 存储数据
    [updataMutableDictionary setObject:[NSKeyedArchiver archivedDataWithRootObject:data] forKey:(id)kSecValueData];
    // 获取存储的状态
    OSStatus  updataStatus = SecItemUpdate((CFDictionaryRef)keyChainUpdataQueryMutableDictionary, (CFDictionaryRef)updataMutableDictionary);
    // 释放对象
    keyChainUpdataQueryMutableDictionary = nil;
    updataMutableDictionary = nil;
    // 判断是否更新成功
    if (updataStatus == errSecSuccess) {
        return  YES ;
    }
    return NO;
}


/*!
 删除数据
 */
+(void) keyChainDelete:(NSString*)identifier {
    // 获取删除数据的查询条件
    NSMutableDictionary * keyChainDeleteQueryMutableDictionary = [self keyChainIdentifier:identifier];
    // 删除指定条件的数据
    SecItemDelete((CFDictionaryRef)keyChainDeleteQueryMutableDictionary);
    // 释放内存
    keyChainDeleteQueryMutableDictionary = nil ;
}

@end
// 存储数据
BOOL save = [KeyChainManager keyChainSaveData:@"思念诉说,眼神多像云朵" withIdentifier:Keychain];
if (save) {
    NSLog(@"存储成功");
}else {
    NSLog(@"存储失败");
}
// 获取数据
NSString * readString = [KeyChainManager keyChainReadData:Keychain];
NSLog(@"获取得到的数据:%@",readString);

// 更新数据
BOOL updata = [KeyChainManager keyChainUpdata:@"长发落寞,我期待的女孩" withIdentifier:Keychain];
if (updata) {
    NSLog(@"更新成功");
}else{
    NSLog(@"更新失败");
}
// 读取数据
NSString * readUpdataString = [KeyChainManager keyChainReadData:Keychain];
NSLog(@"获取更新后得到的数据:%@",readUpdataString);

// 删除数据
[KeyChainManager keyChainDelete:Keychain];
// 读取数据
NSString * readDeleteString = [KeyChainManager keyChainReadData:Keychain];
NSLog(@"获取删除后得到的数据:%@",readDeleteString);

实例2:

//新增keychain
- (void)addKeychainPassword{
    NSDictionary *query = @{(__bridge id)kSecAttrAccessible:(__bridge id)kSecAttrAccessibleWhenUnlocked,(__bridge id)kSecClass:(__bridge id)kSecClassGenericPassword,(__bridge id)kSecValueData:[@"123456" dataUsingEncoding:NSUTF8StringEncoding],(__bridge id)kSecAttrAccount:@"account name",(__bridge id)kSecAttrService:@"loginPassword"};
    /*
     参数一:
     kSecAttrAccessibleWhenUnlocked 表示获取当前密钥只要屏幕处于解锁状态就可以了
     kSecAttrAccessibleAfterFirstUnlock 表示手机第一次解锁就可以获取当前密钥
     kSecAttrAccessibleAlways 表示任何时候都可以获取当前密钥
     kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly 表示获取密钥只能在当前设备,把手机数据恢复到新的手机中是不可用的
     kSecAttrAccessibleWhenUnlockedThisDeviceOnly 非锁定状态,且设备唯一指定,同上
     kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly 第一次解锁,且设备唯一指定,同上
     kSecAttrAccessibleAlwaysThisDeviceOnly  总是可以获取,当然也是设备唯一指定,同上
     参数二:
     kSecClassGenericPassword 为keychain类型
     参数三:
     kSecValueData 存储的数据,就是密码、token存储的地方,要转化为NSData类型
     参数四:
     kSecAttrAccount 为账户名   作为账户密码的唯一索引
     参数五:
     kSecAttrService 为服务名   作为账户密码的唯一索引
     */

    CFTypeRef result;
    OSStatus status = SecItemAdd((__bridge CFDictionaryRef)query, &result);
    
    if (status == errSecSuccess) {
        NSLog(@"添加成功");
    }else{
        NSLog(@"添加失败");
    }
}

//查询keychain
- (void)queryKeychainPassword{
    NSDictionary *query = @{(__bridge id)kSecClass:(__bridge id)kSecClassGenericPassword,
                            (__bridge id)kSecReturnData:@YES,
                            (__bridge id)kSecMatchLimit:(__bridge id)kSecMatchLimitOne,
                            (__bridge id)kSecAttrAccount:@"account name",
                            (__bridge id)kSecAttrService:@"loginPassword"};
    //kSecMatchLimitOne 表示查询返回一条记录,有可能查到多条记录,一般默认返回一条记录
    //kSecMatchLimitAll 表示返回所有记录
    //SecItemCopyMatching函数会根据query里面的查询条件查找对应符合要求的记录,另外根据不同的keychain类型dataTypeRef会返回对应的不同类型如NSArray、NSDictionary、NSData
    
    CFTypeRef dataTypeRef = NULL;
    OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &dataTypeRef);
    if (status == errSecSuccess) {
        NSString *pwd = [[NSString alloc]initWithData:(__bridge NSData*)dataTypeRef encoding:NSUTF8StringEncoding];
        NSLog(@"pwd:%@",pwd);
    }
}

//另外可以通过kSecReturnRef查询其他属性,相对于前面的返回密钥的引用,kSecReturnRef返回的是keychain所有的属性
- (void)queryMoreAttribute{
    NSDictionary *query = @{(__bridge id)kSecClass:(__bridge id)kSecClassGenericPassword,
                            (__bridge id)kSecReturnRef:@YES,
                            (__bridge id)kSecReturnData:@YES,
                            (__bridge id)kSecMatchLimit:(__bridge id)kSecMatchLimitOne,
                            (__bridge id)kSecAttrAccount:@"account name",
                            (__bridge id)kSecAttrService:@"loginPassword"};
    CFTypeRef dataTypeRef = NULL;
    //重点是(__bridge id)kSecReturnRef:@YES,声明返回的数据是整个keychain的所有属性
    
    OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &dataTypeRef);
    
    if (status == errSecSuccess) {
        NSDictionary *dict = (__bridge NSDictionary *)dataTypeRef;
        NSString *acccount = dict[(id)kSecAttrAccount];
        NSLog(@"acccount:%@",acccount);
        NSData *data = dict[(id)kSecValueData];
        NSString *pwd = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
        NSLog(@"pwd:%@",pwd);
        NSString *service = dict[(id)kSecAttrService];
        NSLog(@"service==result:%@", service);
    }
}

//修改keychain
- (void)changeKeychainPassword{
    NSDictionary *query = @{(__bridge id)kSecClass:(__bridge id)kSecClassGenericPassword,
                            (__bridge id)kSecAttrService:@"loginPassword",
                            (__bridge id)kSecAttrAccount:@"account name"};
    NSDictionary *update = @{(__bridge id)kSecValueData:[@"654321" dataUsingEncoding:NSUTF8StringEncoding]};
    
    OSStatus status = SecItemUpdate((__bridge CFDictionaryRef)query, (__bridge CFDictionaryRef)update);
    
    if (status == errSecSuccess) {
        NSLog(@"更新成功");
    }
}

//删除keychain
- (void)deleteKeychainPassword{
    NSDictionary *query = @{
                            (__bridge id)kSecClass : (__bridge id)kSecClassGenericPassword,
                            (__bridge id)kSecAttrService : @"loginPassword",
                            (__bridge id)kSecAttrAccount : @"account name"
                            };
    
    //尽量详细的添加多个属性,避免误删其他keychain
    
    OSStatus status = SecItemDelete((__bridge CFDictionaryRef)query);
    
    if (status == errSecSuccess) {
        NSLog(@"成功删除");
    }
}
5、keychainsharing

iOS keychain有一个特色功能就是 keychainsharing,它能实现多个同一个开发者账号下的多个应用共享keychain,前提是要开启keychainsharing功能,如下图所示:

image.png

可以在keychainsharing中对应的分组,把keychain添加的到分组的操作可以这样写

//sharing Items
//添加sharing Items
- (void)addSharingItems{
    NSDictionary *query = @{(__bridge id)kSecAttrAccessible : (__bridge id)kSecAttrAccessibleWhenUnlocked,
                            (__bridge id)kSecClass : (__bridge id)kSecClassGenericPassword,
                            (__bridge id)kSecValueData : [@"88888888" dataUsingEncoding:NSUTF8StringEncoding],
                            (__bridge id)kSecAttrAccount : @"account name",
                            (__bridge id)kSecAttrAccessGroup : @"5Q8QKERR7H.com.mycom.iOS-keychain",
                            (__bridge id)kSecAttrService : @"loginPassword",
                            (__bridge id)kSecAttrSynchronizable : @YES,
                            };
    /*
     (__bridge id)kSecAttrSynchronizable : @YES 表示可以同步到icloud,如果要同步到其他设备,请注意避免使用DeviceOnly设置等其他和设备唯一性相关的设置
     */
    OSStatus status = SecItemAdd((__bridge CFDictionaryRef)query, nil);
    
    if (status == errSecSuccess) {
        NSLog(@"sharing Items添加成功");
    }else{
        NSLog(@"sharing Items添加失败");
    }
}

查询对应的分组

//查询sharing Items
- (void)querySharingItems{
    NSDictionary *query = @{(__bridge id)kSecClass : (__bridge id)kSecClassGenericPassword,
                            (__bridge id)kSecReturnRef : @YES,
                            (__bridge id)kSecReturnData : @YES,
                            (__bridge id)kSecMatchLimit : (__bridge id)kSecMatchLimitAll,
                            (__bridge id)kSecAttrAccount : @"account name",
                            (__bridge id)kSecAttrAccessGroup : @"5Q8QKERR7H.com.mycom.iOS-keychain",
                            (__bridge id)kSecAttrService : @"loginPassword",
                            };
    
    CFTypeRef dataTypeRef = NULL;
    
    OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &dataTypeRef);
    
    if (status == errSecSuccess) {
        NSLog(@"sharing Items查询成功");
    }else{
        NSLog(@"sharing Items查询失败");
    }
}

和对单个keychain进行操作不同的是添加多了一个kSecAttrAccessGroup属性,用于指明对应的分组,这里要注意的是要添加开发者账号的teamId,用于区分不同的开发者。

5、SSKeychain框架

github地址: github.com/Mingriweiji… demo: github.com/LuPing-Kuan…

如果都是自己定义属性进行增删改查的操作,是比较容易出bug的,而且操作也比较繁琐,毕竟这些操作都是基于C语言的API操作。下面介绍一个简单的轻量级框架SSKeychain来实现我们对密钥的增删改查,闲话少说,直接上代码:

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    

//    [SSKeychain setAccessibilityType:kSecAttrAccessibleWhenUnlocked];  //设置访问权限,不设置则按照默认权限(这个我看源码没有显示默认的是什么,不知道keychain默认保存的权限是不是kSecAttrAccessibleAlways)
    
    [SSKeychain setPassword:@"123456" forService:kKeyChainSaveAccountService account:kKeyChainSaveAccount];   //设置密钥
    [SSKeychain setPassword:@"12345678" forService:kKeyChainSaveAccountService account:kKeyChainSaveAccount1];  //设置密钥
    NSError *error;
    NSString *password = [SSKeychain passwordForService:kKeyChainSaveAccountService account:kKeyChainSaveAccount error:&error];  //获取密钥
    NSLog(@"password:%@,error:%@",password,error);
    
    NSArray *counts = [SSKeychain accountsForService:kKeyChainSaveAccountService error:&error];
    NSLog(@"counts:%@",counts); //获取服务下相关账户所有的属性
    NSError *error1;
    [SSKeychain deletePasswordForService:kKeyChainSaveAccountService account:kKeyChainSaveAccount error:&error1];
    if (error1) {
        NSLog(@"删除失败:%@",error1);
    }else{
        NSLog(@"删除成功");
    }
}