开发文档笔记 - 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

你可能感兴趣的:(开发文档笔记 - Keychain Services Programming Guide)