为了调用泊学 App 服务端的 HTTP API,我们需要在客户端本地保存 RSA 公钥。这就牵扯到一个问题,应该存在哪呢?实际上,这个问题可以延展到一个更通用的问题:我们应该如何在客户端保存一些希望安全存储的小块数据呢?可能你会立即想到一些方案:
- 加密后存在本地文件里;
- 归档到 user default;
- 保存在 plist 文件里;
- 写入一个诸如 SQLite 的本地数据库中;
实际上,这些做法都无法提供足够的安全性。更好的解决方案是直接保存在 iOS keychain 里。但如果你去翻翻 keychain API 文档就会发现,不仅 keychain 的交互方式和常规数据存储的应用方式截然不同,Apple 提供的 API 用起来也一点儿都不 Swift。你很难在短时间搞清楚数据究竟是如何通过 keychain 存取的。
由于接下来我们会频繁和 keychain 打交道,在接下来的几节内容里,我们另外创建一个小型开源项目 KeychainWrapper。把相关 API 封装成几个 Swift 类,让它用起来,就像 UserDefault
一样简单。
不过,在开始动手之前,我们要先了解一些必要的 keychain 概念。如果你已经非常熟悉 keychain,就可以跳过这一节的内容了。
关于 keychain 的一张图总结
把我们要介绍的 keychain 必备知识用一张图总结,就是这样的:

接下来,我们就逐一过一遍它们。
首先是 keychain item,它是指每一条存储在 keychain 数据库中的记录,每个 item 由两部分构成,分别是加密过的原始数据 (这就是我们要保存的真实内容) 以及给这个数据添加的一些额外的属性,并且,根据要存储的数据不同,我们可以使用的属性也有差别。例如,保存密码信息的可用属性,和保存证书信息的可用属性,是有差别的。之所以要这么做,就是为了通过这些属性,可以唯一确定一个安全信息的用途。现在,大家对这种方式有个概念就行了,稍后实现的时候,我们还会对应代码来看相关的内容。
其次,是 service name 和 access group。我们可以简单把 service name 理解成数据库中的表。在 keychain 中存储的每一条数据,都对应着一个 service name。而 access group 则是在不同 app 之间共享 keychain 时使用的名字。同样,现在知道它们的用途就好了。
最后,是图中右半边的部分,它们定义了 app 在什么时候可以访问到 keychain 中的数据,以及 keychain 中的记录在系统备份时采取的动作。在 keychain 中,这叫做 accessibility。每一个 keychain item 都有它自己的 accessibility,这是在向 keychain 保存数据的时候指定的。可以看到,我们罗列了 5 种情况,按照 iOS 备份的方式可以分成两大类:
一类是在执行加密备份的时候,iOS 会备份的 keychain 数据:
- afterFirstUnlock:设备重启后解锁一次就可以一直访问;
- whenUnlocked:只有在设备解锁情况下才能访问。这也是 keychain 中数据的默认访问权限;
另一类是只在设备本地生效,不会备份的 keychain 数据:
- afterFirstUnlockThisDeviceOnly:访问权限和 afterFirstUnlock 是一样的;
- whenPasscodeSetThisDeviceOnly:只有在设备有密码,并且已经解锁的情况下,才能访问。删除设备密码的时候,iOS 会自动从 keychain 中删除这种属性的记录;
- whenUnlockedThisDeviceOnly:访问权限和 whenUnlocked 是一样的;
其实,还有另外两种类型用于指定在任何时候都可以访问的 keychain 数据,只不过从 iOS 12 开始,它们被标记成废弃了,因此我们就不再提及了。
Keychain的“查询语言”
对 keychain 的工作机制有了一个大致了解之后,我们来看通过 keychain 存取数据的方式。就像 MySQL 通过 SQL 存取数据一样,keychain 也有自己的数据“查询语言”。这里之所以打了引号,是因为它远没有到一个语言这么复杂的程度,只是一个和平时我们直接访问数组或字典这种数据结构不太一样的机制罢了。实际上,对于 keychain 记录的增、删、改、查都是借助于一个形如 [string: any]
的字典完成。在这个字典里,我们用 Apple 预定义好的键值表达要操作的数据以及数据的属性。再把创建好的字典传递给对应的 API 完成特定的操作。现在,大家只要脑子里先有这么个概念就好了。
What's next?
以上,就是为了实现 keychain 封装要铺垫的相关知识。下一节,我们就通过具体的代码,来看这些概念对应的实现。