iOS安全-钥匙串服务(iOS Keychain Services Tasks)

本文描述iOS中基本的钥匙串访问,内容整理于苹果官方文档。
本文主要讲述以下内容:

  • 钥匙串中添加一个条目
  • 钥匙串中查找条目
  • 获取钥匙串条目中的属性和数据
  • 改变钥匙串条目中的属性和数据

注意:在iPhone上,钥匙链的访问权限取决于签名应用程序的描述文件。在应用程序版本中务必要一直使用相同的描述文件。

向应用程序中添加钥匙串服务

大多数iOS应用程序使用钥匙串只是向钥匙串中添加一个密码,修改现有的钥匙串条目,或者在需要的时候检索一个密码。钥匙串服务提供了以下方法来完成这些任务:

  • SecItemAdd 向钥匙串中添加一个条目
  • SecItemUpdate 更改钥匙串中已有的条目
  • SecItemCopyMatching 查找钥匙串条目并提取信息

下图展示了应用程序如何使用这些函数来访问互联网的FTP服务器的流程图。

使用iPhone Keychain Services 访问网络服务器流程图

应用程序用户从选择一个文件传输协议(FTP)服务器开始。应用程序调用SecItemCopyMatching,传一个包含确定密钥串条目的属性的字典。如果钥匙串里有密码, 函数将密码返回到应用程序,将它发送到FTP服务器对用户进行身份验证。如果身份验证成功,结束。如果身份验证失败,应用程序显示一个对话框,要求输入户名和密码。
如果钥匙串里没有相应的密码,SecItemCopyMatching返回errSecItemNotFound结果代码。在这种情况下,应用程序显示一个对话框,要求输入户名和密码。(这个对话框还应该包括一个取消按钮,为了避免流程图变得过于复杂已经省略了)。

从用户那里拿到密码之后,该应用程序继续到FTP服务器验证用户的身份。身份验证成功时,应用程序可以假设用户输入的信息是有效的。应用程序然后显示另一个对话框询问用户是否保存密码钥匙链。如果用户选择不,那么结束。如果用户选择是,则应用程序调用SecItemAdd函数(如果这是一个新的钥匙串条目)或SecItemUpdate函数(更新现有的钥匙串条目),然后结束。

以下代码展示了一个可能使用钥匙串服务功能典型的应用程序,为通用项目获取和设置密码。使用同样的方法你可以获取和设置钥匙串条目属性(如用户名或服务名称)。
以下代码来自苹果官方
注意引入Security.Framework

KeychainWrapper.h

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

@interface KeychainWrapper : NSObject{

  NSMutableDictionary        *keychainData;
  NSMutableDictionary        *genericPasswordQuery;
}

@property (nonatomic, strong) NSMutableDictionary *keychainData;
@property (nonatomic, strong) NSMutableDictionary *genericPasswordQuery;

- (void)mySetObject:(id)inObject forKey:(id)key;
- (id)myObjectForKey:(id)key;
- (void)resetKeychainItem;

@end

KeychainWrapper.m

//Unique string used to identify the keychain item:
static const UInt8 kKeychainItemIdentifier[]    = "com.ios.doris";

@implementation KeychainWrapper


- (id)init
{
    if ((self = [super init])) {
    
    OSStatus keychainErr = noErr;
    // Set up the keychain search dictionary:
    genericPasswordQuery = [[NSMutableDictionary alloc] init];
    // This keychain item is a generic password.
    [genericPasswordQuery setObject:(__bridge id)kSecClassGenericPassword
                             forKey:(__bridge id)kSecClass];
    // The kSecAttrGeneric attribute is used to store a unique string that is used
    // to easily identify and find this keychain item. The string is first
    // converted to an NSData object:
    NSData *keychainItemID = [NSData dataWithBytes:kKeychainItemIdentifier
                                            length:strlen((const char *)kKeychainItemIdentifier)];
    [genericPasswordQuery setObject:keychainItemID forKey:(__bridge id)kSecAttrGeneric];
    // Return the attributes of the first match only:
    [genericPasswordQuery setObject:(__bridge id)kSecMatchLimitOne forKey:(__bridge id)kSecMatchLimit];
    // Return the attributes of the keychain item (the password is
    //  acquired in the secItemFormatToDictionary: method):
    [genericPasswordQuery setObject:(__bridge id)kCFBooleanTrue
                             forKey:(__bridge id)kSecReturnAttributes];
    
    //Initialize the dictionary used to hold return data from the keychain:
    CFMutableDictionaryRef outDictionary = nil;
    // If the keychain item exists, return the attributes of the item:
    keychainErr = SecItemCopyMatching((__bridge CFDictionaryRef)genericPasswordQuery,
                                      (CFTypeRef *)&outDictionary);
    if (keychainErr == noErr) {
        // Convert the data dictionary into the format used by the view controller:
        self.keychainData = [self secItemFormatToDictionary:(__bridge_transfer NSMutableDictionary *)outDictionary];
    } else if (keychainErr == errSecItemNotFound) {
        // Put default values into the keychain if no matching
        // keychain item is found:
        [self resetKeychainItem];
        if (outDictionary) CFRelease(outDictionary);
    } else {
        // Any other error is unexpected.
        NSAssert(NO, @"Serious error.\n");
        if (outDictionary) CFRelease(outDictionary);
    }
}
return self;
}

存到钥匙串:
- (void)mySetObject:(id)inObject forKey:(id)key
{
if (inObject == nil) return;
id currentObject = [keychainData objectForKey:key];
if (![currentObject isEqual:inObject])
{
[keychainData setObject:inObject forKey:key];
[self writeToKeychain];
}
}

从钥匙串中取:
- (id)myObjectForKey:(id)key
{
return [keychainData objectForKey:key];
}

// Reset the values in the keychain item, or create a new item if it
// doesn't already exist:

重置钥匙串中的数据,或者不存在是创建相应条目:
- (void)resetKeychainItem
{
if (!keychainData) //Allocate the keychainData dictionary if it doesn't exist yet.
{
self.keychainData = [[NSMutableDictionary alloc] init];
}
else if (keychainData)
{
// Format the data in the keychainData dictionary into the format needed for a query
// and put it into tmpDictionary:
NSMutableDictionary *tmpDictionary =
[self dictionaryToSecItemFormat:keychainData];
// Delete the keychain item in preparation for resetting the values:
OSStatus errorcode = SecItemDelete((__bridge CFDictionaryRef)tmpDictionary);
NSAssert(errorcode == noErr, @"Problem deleting current keychain item." );
}

// Default generic data for Keychain Item:
[keychainData setObject:@"" forKey:(__bridge id)kSecAttrLabel];
[keychainData setObject:@"" forKey:(__bridge id)kSecAttrDescription];
[keychainData setObject:@"" forKey:(__bridge id)kSecAttrAccount];
[keychainData setObject:@"" forKey:(__bridge id)kSecAttrService];
[keychainData setObject:@"" forKey:(__bridge id)kSecAttrComment];
[keychainData setObject:@"" forKey:(__bridge id)kSecValueData];
}

// Implement the dictionaryToSecItemFormat: method, which takes the attributes that
// you want to add to the keychain item and sets up a dictionary in the format
// needed by Keychain Services:
- (NSMutableDictionary *)dictionaryToSecItemFormat:(NSDictionary *)dictionaryToConvert
{
// This method must be called with a properly populated dictionary
// containing all the right key/value pairs for a keychain item search.

// Create the return dictionary:
NSMutableDictionary *returnDictionary =
[NSMutableDictionary dictionaryWithDictionary:dictionaryToConvert];

// Add the keychain item class and the generic attribute:
NSData *keychainItemID = [NSData dataWithBytes:kKeychainItemIdentifier
                                        length:strlen((const char *)kKeychainItemIdentifier)];
[returnDictionary setObject:keychainItemID forKey:(__bridge id)kSecAttrGeneric];
[returnDictionary setObject:(__bridge id)kSecClassGenericPassword forKey:(__bridge id)kSecClass];

// Convert the password NSString to NSData to fit the API paradigm:
NSString *passwordString = [dictionaryToConvert objectForKey:(__bridge id)kSecValueData];
[returnDictionary setObject:[passwordString dataUsingEncoding:NSUTF8StringEncoding]
                     forKey:(__bridge id)kSecValueData];
return returnDictionary;
}

从钥匙串中取出数据转为字典
- (NSMutableDictionary *)secItemFormatToDictionary:(NSDictionary *)dictionaryToConvert
{
// This method must be called with a properly populated dictionary
// containing all the right key/value pairs for the keychain item.

// Create a return dictionary populated with the attributes:
NSMutableDictionary *returnDictionary = [NSMutableDictionary
                                         dictionaryWithDictionary:dictionaryToConvert];

// To acquire the password data from the keychain item,
// first add the search key and class attribute required to obtain the password:
[returnDictionary setObject:(__bridge id)kCFBooleanTrue forKey:(__bridge id)kSecReturnData];
[returnDictionary setObject:(__bridge id)kSecClassGenericPassword forKey:(__bridge id)kSecClass];
// Then call Keychain Services to get the password:
CFDataRef passwordData = NULL;
OSStatus keychainError = noErr; //
keychainError = SecItemCopyMatching((__bridge CFDictionaryRef)returnDictionary,
                                    (CFTypeRef *)&passwordData);
if (keychainError == noErr)
{
    // Remove the kSecReturnData key; we don't need it anymore:
    [returnDictionary removeObjectForKey:(__bridge id)kSecReturnData];
    
    // Convert the password to an NSString and add it to the return dictionary:
    NSString *password = [[NSString alloc] initWithBytes:[(__bridge_transfer NSData *)passwordData bytes]
                                                  length:[(__bridge NSData *)passwordData length] encoding:NSUTF8StringEncoding];
    [returnDictionary setObject:password forKey:(__bridge id)kSecValueData];
}
// Don't do anything if nothing is found.
else if (keychainError == errSecItemNotFound) {
    NSAssert(NO, @"Nothing was found in the keychain.\n");
    if (passwordData) CFRelease(passwordData);
}
// Any other error is unexpected.
else
{
    NSAssert(NO, @"Serious error.\n");
    if (passwordData) CFRelease(passwordData);
}

return returnDictionary;
}

写到钥匙串的具体实现:

- (void)writeToKeychain
{
CFDictionaryRef attributes = nil;
NSMutableDictionary *updateItem = nil;

// If the keychain item already exists, modify it:
if (SecItemCopyMatching((__bridge CFDictionaryRef)genericPasswordQuery,
                        (CFTypeRef *)&attributes) == noErr)
{
    // First, get the attributes returned from the keychain and add them to the
    // dictionary that controls the update:
    updateItem = [NSMutableDictionary dictionaryWithDictionary:(__bridge_transfer NSDictionary *)attributes];
    
    // Second, get the class value from the generic password query dictionary and
    // add it to the updateItem dictionary:
    [updateItem setObject:[genericPasswordQuery objectForKey:(__bridge id)kSecClass]
                   forKey:(__bridge id)kSecClass];
    
    // Finally, set up the dictionary that contains new values for the attributes:
    NSMutableDictionary *tempCheck = [self dictionaryToSecItemFormat:keychainData];
    //Remove the class--it's not a keychain attribute:
    [tempCheck removeObjectForKey:(__bridge id)kSecClass];
    
    // You can update only a single keychain item at a time.
    OSStatus errorcode = SecItemUpdate(
                                       (__bridge CFDictionaryRef)updateItem,
                                       (__bridge CFDictionaryRef)tempCheck);
    NSAssert(errorcode == noErr, @"Couldn't update the Keychain Item." );
} else {
    // No previous item found; add the new item.
    // The new value was added to the keychainData dictionary in the mySetObject routine,
    // and the other values were added to the keychainData dictionary previously.
    // No pointer to the newly-added items is needed, so pass NULL for the second parameter:
    OSStatus errorcode = SecItemAdd(
                                    (__bridge CFDictionaryRef)[self dictionaryToSecItemFormat:keychainData],
                                    NULL);
    NSAssert(errorcode == noErr, @"Couldn't add the Keychain Item." );
    if (attributes) CFRelease(attributes);
}
}

@end

在这个示例中,通用属性是用来创建一个独一无二的字符串,可以用来轻松识别钥匙串条目。你也可以使用标准的属性,如服务名称和用户名。

运行调试:

详细代码和示例程序:https://github.com/lilufeng/KeychainDemo

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,817评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,329评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,354评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,498评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,600评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,829评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,979评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,722评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,189评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,519评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,654评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,329评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,940评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,762评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,993评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,382评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,543评论 2 349

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,633评论 18 139
  • Ubuntu的发音 Ubuntu,源于非洲祖鲁人和科萨人的语言,发作 oo-boon-too 的音。了解发音是有意...
    萤火虫de梦阅读 99,204评论 9 467
  • 国家电网公司企业标准(Q/GDW)- 面向对象的用电信息数据交换协议 - 报批稿:20170802 前言: 排版 ...
    庭说阅读 10,930评论 6 13
  • 本文是Medusa和Hydra快速入门手册的第二部分,第一部分的传送门这两篇也是后续爆破篇的一部分,至于字典,放在...
    LinuxSelf阅读 2,843评论 0 4
  • 一路走来,只觉得满满的感恩。教会我的,受益匪浅。在路上的,感慨万千。 皎洁如雪的月下,细数扑簌而过的回忆。丝丝侵入...
    三石三味阅读 314评论 2 23