开发文档笔记 - Keychain Services Programming Guide

Keychain Service Concepts

一直通过第三方库的封装来读写keychain,对keychain本身也不甚了解。新项目又有存储密码的需求,于是趁机翻了翻开发文档,随手做点笔记。

1. keychain的结构

keychain实际上是一个存储了加密数据的数据库

MacOS会为每个登录用户创建一个keychain(login.keychain), 并且每个用户或者应用可以创建多个keychain,数量不受限制。
iOS每个设备只有一个keychain,当用户登录不同的iCloud帐号时,系统对每个iCloud帐号提供在逻辑上区分开的iCloud keychain。

keychain中存储不限数量的keychain items, 每个item由data和attributes组成。不同子类的item的attributes不同。(generic password/ internet password)

kSecAttrSynchronizable属性为synchronizable的item存储在iCloud keychain中,会自动通过网络在所有的设备中同步。

有些keychain item的data需要加密(比如用户密码或者非对称加密的private key), 有些不需要加密,比如公开证书。macOS上访问加密的data的时候需要用户输入密码。在iOS中,keychain在设备解锁的时候就已经处在解锁状态中。

另外keychain item的attributes和data不同,attributes永远不会被加密,在keychain锁住的时候同样可以直接访问。

在macOS中,不通过iCloud同步的受保护的keychain item都有一个access object记录这个keychain item的访问权限。access object包含一个或者多个 access control list (ACL)。每个ACL都有授权标签(authorization tags)来标识这个item可以被用于那些用途。另外每个ACL还有一个应用信任列表(trusted applications),可以不需要用户授权完成ACL中的指定用途。

2. 访问keychain

macOS上如果login.keychain的密码和用户登录密码一样的话,login.keychain会在登录过程中解锁。默认情况下login.keychain就是系统的默认keychain.
用户可以在keychain工具中指定其他的keychain作为默认keychain. 但是用户登录时解锁的keychain仍然是login.keychain(如果密码一样的话)。

iOS中情况比较简单,每个设备只有一个keychain可以访问。解锁设备时keychain解锁。锁上设备时,keychain也锁上。每个app只能访问各自的keychain items, 或者app group中的keychain item。

2.1 high level functions for basic keychain access

不适用于iOS和macOS icloud keychain (呃。。。)

  • 向keychain中增加一个密码
NSString* service = @"myService";
NSString* account = @"username";
NSString* password = @"somethingSecret";
const void* passwordData = [[password dataUsingEncoding:NSUTF8StringEncoding] bytes];
 
OSStatus status = SecKeychainAddGenericPassword(
                      NULL,        // Use default keychain
                      (UInt32)service.length,
                      [service UTF8String],
                      (UInt32)account.length,
                      [account UTF8String],
                      (UInt32)password.length,
                      passwordData,
                      NULL         // Uninterested in item reference
                  );
 
if (status != errSecSuccess) {     // Always check the status
    NSLog(@"Write failed: %@", SecCopyErrorMessageString(status, NULL));
}

macOS上可能会弹窗需要用户输入密码,如果用户输入失败或者取消,status就会返回错误。
SecKeychainAddGenericPassword 方法添加成功之后会自动创建 access object然后自动把调用这个方法的应用添加到信任列表中。

  • 访问存储的密码
UInt32 pwLength = 0;
void* pwData = NULL;
SecKeychainItemRef itemRef = NULL;
 
OSStatus status = SecKeychainFindGenericPassword(
                      NULL,         // Search default keychains
                      (UInt32)service.length,
                      [service UTF8String],
                      (UInt32)account.length,
                      [account UTF8String],
                      &pwLength,
                      &pwData,
                      &itemRef      // Get a reference this time
                  );
 
if (status == errSecSuccess) {
    NSData* data = [NSData dataWithBytes:pwData length:pwLength];
    NSString* password = [[NSString alloc] initWithData:data
                                               encoding:NSUTF8StringEncoding];
    NSLog(@"Read password %@", password);
} else {
    NSLog(@"Read failed: %@", SecCopyErrorMessageString(status, NULL));
}
 
if (pwData) SecKeychainItemFreeContent(NULL, pwData);  // Free memory

如果访问的应用不在受信列表中,macOS会弹窗需要用户确认,用户如果选择always allow之后就会自动将应用添加到受信列表中。

  • 修改密码
    需用先通过SecKeychainFindGenericPassword获取已经存在的item。
    已经存在service和account相同的keychain item的情况下再次写入,会报errSecDuplicateItem这个错误。
OSStatus status = SecKeychainItemModifyAttributesAndData(
                      itemRef,                 // From the read
                      NULL,                    // Attributes unchanged
                      (UInt32)password.length, // As before
                      passwordData
                  );
 
if (status != errSecSuccess) {
    NSLog(@"Update failed: %@", SecCopyErrorMessageString(status, NULL));
}
 
if (itemRef) CFRelease(itemRef);   // Now, free the item reference memory

2.2 Use Lower Level Functions When You Need More Control

Use SecItemAdd to create a new keychain item.
Use SecItemCopyMatching to retrieve a keychain item’s attributes and/or data.
Use SecItemUpdate to modify a keychain item in place.
Use SecItemDelete to remove a keychain item.

2.3 macOS上的高端操作

基本上只有密码管理之类的应用需要用到,暂时不关注
In addition, the macOS Keychain Services API provides functions that allow you to programmatically create new keychains, manipulate elements within a keychain in more sophisticated ways, and manage collections of keychains. For example, in macOS, your app can:

  • Disable or enable Keychain Services functions that display a user interface; for example, a server might want to suppress the Unlock Keychain dialog box and unlock the keychain itself instead.
  • Unlock a locked keychain when the user is unable to do so, as for an unattended server.
  • Add trusted applications to the access object of a keychain item if, for example, a server application wants to let an administration application have access to its passwords.
  • Register a callback function so that your application is called when a keychain event (such as unlocking the keychain) occurs.

1.3 共享keychain item

1.3.1 应用分组(Access Groups)

  • 在iOS和使用iCloud keychain的应用可以通过access groups来在应用间共享keychain item。access groups中的应用必须要属于同一个development team。 macOS上没有使用iCloud keychain的应用只能通过ACL来共享keychain。
  • Keychain Services通过code signature和其包含的entitlements来确认access groups的访问权限。
  • access group的信息记录在kSecAttrAccessGroup这个attribute中
  • Access group 由名称字符串和develop team的前缀组成
  • XCode会自动根据应用的bundleId和development team生成一个keychain access group作为默认group,这是一个仅包含自己的group。如果没有设置kSecAttrAccessGroup的值,并且没有设置entitlements,那么实际上存储的keychain item就属于这个group。
  • 如果是App Group中的应用貌似有其他的机制,详见App Group
  • 写入keychain时如果你的应用中设置了keychain-access-groups entitlements,可以通过kSecAttrAccessGroup指定任意一个有权限的group。如果没有指定一个Group,则默认会选择entitlements数组中的第一个group,如果没有设置keychain-access-groups entitlements,那么会默认使用teamID+bundleId的那个group。
  • 从keychain中搜索数据时,范围是所有有权限的group

1.3.2 Access Control Lists

只在macOS上没有存储成iCloud keychain 中的item才可以用

1.4 Securing Keychain Data 安全使用习惯

Keychain Accessibility - keychain本身的访问

iOS上keychain的加锁和解锁是和设备的加锁和解锁绑定的,不需要太关注。macOS上可以通过设置keychain自动加锁以及设置不同于登录密码的login.keychain密码来提高安全性。

另外用户备份设备时,keychain也会以加密状态被包含在备份中。直到用户把备份恢复到设备中并且通过密码解锁(或者解锁设备后),keychain才会解锁。

Keychain Item Accessibility - keychain item的访问

可以通过两个维度来限制keychain item的访问

  1. 当前设备的锁定状态。
    • When Passcode Set. If the user has not set a passcode, items cannot be stored with this setting. If the user removes the passcode from a device, any items with this setting are deleted from the keychain. Items with this setting can only be accessed if the device is unlocked. Use this setting if your app only needs access to items while running in the foreground.
    • When Unlocked. Items with this setting are only accessible when the device is unlocked. A device without a passcode is considered to always be unlocked. This is the default accessibility when you do not specify the kSecAttrAccessible attribute for a keychain item.
    • After First Unlock. This condition becomes true once the user unlocks the device for the first time after a restart, or if the device does not have a passcode. It remains true until the device restarts again. Use this level of accessibility when your app needs to access the item while running in the background.
    • Always. The item is always accessible, regardless of the locked state of the device. This option is not recommended.
  2. 是否可以通过备份在另一个设备使用 。

以下两个选项时苹果推荐的kSecAttrAccessible选项。
Important: Always use the most restrictive option that makes sense for your app. For apps running entirely in the foreground, them most secure option is kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly. If your app must access keychain items while running in the background, the most secure option is kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly.

参考:
Keychain Services Programming Guide
Swift Sample Code for iOS
Pod库推荐 - SAMKeychain

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