iOS开发中使用keyChain保存用户密码

iOS开发中使用keyChain保存用户密码

版权声明:本人原创文章,转载请注明原文链接。
前言:自娱自乐的iOS开发学习实践。为实现一个简单社交类应用做功课,认识到使用keyChain保存用户密码是目前相对稳妥的方案,进行了代码实践,解决了一些更新带来的异常。

提出问题

观察主流的社交类iOS客户端,在用户注册于登陆页面都会提供“记住密码”的选项。作为一个完全的自学者,提出了如下问题:

  • 记住密码背后的原理是什么?
  • 有哪些解决方案,是否存在最优方案
  • 如何实现最优方案?

原理

通过在网络上查询资料,了解到“记住密码”的原理很简单,就是将用户密码保存在手机的文件系统中。但如何保存,则有不同的实现思路:

明文保存到plist文件中

Ray Wenderlich的学徒系列教程中,个人已经学习并实践:对于一些轻量的数据,可以使用NSUserDefaults将其保存到应用的沙盒中的plist文件中。

那么对于用户密码是否可以如法炮制呢?答案是否定的。根据查询的资料介绍,如果沙盒被破解,或者手机被越狱,plist文件就会被获取,明文的用户密码就暴露了,导致安全问题。

加密后保存到plist文件中

基于第一个方案,自然可以想到是否可以对明文进行加密后保存呢?答案是肯定的

iOS提供了多种加密算法,对于用户密码,通常采用的是MD5加密,该加密是不可逆的。使用MD5加密算法需要导入头文件:

#import <CommonCrypto/CommonDigest.h>

那么这个方法是最优方案吗?不是的,因为Apple提供了一个更好的机制:

使用keyChain保存用户密码

根据文档介绍,iOS设备中的keyChain是一个安全的存储容器,可以用来为不同应用保存敏感信息(用户名,密码,网络密码等)。同时,keyChain是一个相对独立的空间,当应用替换或删除时并不会删除keyChain的内容,这对用户来说就十分的便利了。

既然keyChain是Apple提供的专门用于保存敏感信息的容器,同时又具有相对的独立性,那么目前看来,使用keyChain来保存用户名和用户密码是最优的解决方案

更新:经知友指正与查阅资料,证实iPhone越狱后,有技术手段获取keyChain内容。从用户角度来讲,谨慎越狱。从开发者角度来讲,对于这个安全隐患要有清醒认识

实现keyChain保存用户密码

通过查询资料,可以知道Apple是提供了官方的一些方法的。但是本着不要重复造轮子,个人采取了学习其他开发者的代码的方法,对于功能实现原理做了解,不去深究细节

首先,需要导入secutity.framework框架:为了进行实践,个人新建了一个keychain项目。而后:

  1. 在Xcode左侧文件列表中选中项目名称,然后选中TARGETS;
  2. 中间界面选中Build Phases,在Link Binary With Libraries中点击加号;
  3. 搜索找到secutity.framework导入即可,如下图所示:

其次,新建KeyChain类,用于实现向keyChain存储、读取和修改用户名密码。代码如下:

KeyChain.h文件

#import <Foundation/Foundation.h>
#import <Security/Security.h>

@interface KeyChain : NSObject

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

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

// load username and password from keychain
+ (id)load:(NSString *)service;

// delete username and password from keychain
+ (void)delete:(NSString *)serviece;

@end

KeyChain.m文件

#import "KeyChain.h"

@implementation KeyChain

+ (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);
}

@end

为了验证KeyChain类,修改ViewController.m文件代码进行实验。具体如下:

#import "ViewController.h"
#import "KeyChain.h"

@interface ViewController ()

@end

//
NSString * const KEY_USERNAME_PASSWORD = @"com.company.app.usernamepassword";
NSString * const KEY_USERNAME = @"com.company.app.username";
NSString * const KEY_PASSWORD = @"com.company.app.password";

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    
    NSMutableDictionary *userNamePasswordKVPairs = [NSMutableDictionary dictionary];
    [userNamePasswordKVPairs setObject:@"userName" forKey:KEY_USERNAME];
    [userNamePasswordKVPairs setObject:@"password" forKey:KEY_PASSWORD];
    NSLog(@"%@", userNamePasswordKVPairs); //有KV值
    
    // A、将用户名和密码写入keychain
    [KeyChain save:KEY_USERNAME_PASSWORD data:userNamePasswordKVPairs];
    
    // B、从keychain中读取用户名和密码
    NSMutableDictionary *readUsernamePassword = (NSMutableDictionary *)[KeyChain load:KEY_USERNAME_PASSWORD];
    NSString *userName = [readUsernamePassword objectForKey:KEY_USERNAME];
    NSString *password = [readUsernamePassword objectForKey:KEY_PASSWORD];
    NSLog(@"username = %@", userName);
    NSLog(@"password = %@", password);
    
    // C、将用户名和密码从keychain中删除
    [KeyChain delete:KEY_USERNAME_PASSWORD];
    
}

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

@end

实际遇到的问题与解决方法

按理说,运行项目,应该得到的结果应该是得到输出:

2016-10-02 04:10:59.463 keychain[16953:1510836] username = userName
2016-10-02 04:10:59.463 keychain[16953:1510836] password = password

然而我在尝试的时候,却出现了两个问题

第一个问题

在输出中出现了大量个怪异输出如下:

subsystem: com.apple.UIKit, category: HIDEventFiltered, enable_level: 0, persist_level: 0, default_ttl: 0, info_ttl: 0, debug_ttl: 0, generate_symptoms: 0, enable_oversize: 1, privacy_setting: 2, enable_private_data: 0  
subsystem: com.apple.UIKit, category: HIDEventIncoming, enable_level: 0, persist_level: 0, default_ttl: 0, info_ttl: 0, debug_ttl: 0, generate_symptoms: 0, enable_oversize: 1, privacy_setting: 2, enable_private_data: 0  
subsystem: com.apple.BaseBoard, category: MachPort, enable_level: 1, persist_level: 0, default_ttl: 0, info_ttl: 0, debug_ttl: 0, generate_symptoms: 0, enable_oversize: 0, privacy_setting: 0, enable_private_data: 0  
subsystem: com.apple.UIKit, category: StatusBar, enable_level: 0, persist_level: 0, default_ttl: 0, info_ttl: 0, debug_ttl: 0, generate_symptoms: 0, enable_oversize: 1, privacy_setting: 2, enable_private_data: 0  
subsystem: com.apple.BackBoardServices.fence, category: App, enable_level: 1, persist_level: 0, default_ttl: 0, info_ttl: 0, debug_ttl: 0, generate_symptoms: 0, enable_oversize: 0, privacy_setting: 0, enable_private_data: 0  

通过在Stackoverflow上查询,发现这是Xcode8更新后出现的普遍问题,解决方法是:

  1. 按顺序选择Product–>Scheme–>Edict Scheme;
  2. 选择Run中的Arguments;
  3. 选择Environment Variables;
  4. 添加OS_ACTIVITY_MODE字段并设置Value值为disable
  5. 点击Close即可。

而后运行项目,问题解决。

第二个问题

程序关于用户名和密码的输出实际为:

2016-10-02 04:10:59.463 keychain[16953:1510836] username = (null)
2016-10-02 04:10:59.463 keychain[16953:1510836] password = (null)

通过分步NSLog可以发现,NSMutableDictionary类的对象userNamePasswordKVPairs是有键值对的,由此可以判断问题还是在KeyChain类的方法本身。受第一个问题的启发,猜测可能和Xcode或iOS的更新有关,经过查询Stackoverflow,证实了这一个猜测。解决方法:

  1. 在Xcode左侧文件列表中选中项目名称,然后选中TARGETS;
  2. 中间界面选中Capabilities;
  3. Keychain Sharing的开关由off改为on,如下图所示:

而后运行项目,问题解决,得到输出如下:

2016-10-02 04:10:59.463 keychain[16953:1510836] username = userName
2016-10-02 04:10:59.463 keychain[16953:1510836] password = password

由此,整个学习与实践的过程结束。

小结

理解了keyChain保存用户名密码的原理使用现有代码进行了验证,解决了Xcode更新带来的问题。对于代码内部细节尚未进行细致学习,待有需求时再进行。

下一步

基于之前的学习,知道是服务器会对发送来的用户名密码验证,正确则返回token。鉴于Apple要求在2017年所有的通信需要从http变成https,下一步对客户端如何使用https与服务器通信做下功课。


参考文献

作者反馈

  1. 本文系个人业余时间iOS开发自学笔记,水平有限,欢迎批评指正;
  2. 若能指教客户端与服务器端https通信实现的关键词,不胜感激;
  3. 经知友@JamesYu指出iPhone越狱后,KeyChain的安全性无法保证。经查阅资料Keychain 浅析一文,证实确可通过技术手段获取越狱后手机中KeyChain内容。
编辑于 2016-10-05