iOS/macOS 26 密码导出功能幕后
@PlatyHsu:如今,密码管理工具的选择不可谓不多。苹果也凑上了热闹,在去年的系统更新中加入了独立的「密码」app。然而,要在这些工具之间迁移,传统上是非常困难的。每个工具都使用互不兼容的格式;即使导出为 CSV 格式,由于双方的字段定义和功能集不同,也几乎无法无损转换。
近年 passkey(通行密钥)的普及进一步加重了这个问题。passkey 基于非对称加密,其中的私钥存放在设备的安全芯片里,公钥存放在网站或应用的服务器上。登录时,网站用公钥生成一个只有使用私钥才能通过的验证(challenge),从而验证身份。
显然,要保证这个机制的安全,私钥必须牢牢绑定在硬件上,不可轻易复制或导出,否则就和一个普通的密码无异了。因此,目前 passkey 的迁移和同步通常都是厂商在自己的生态内通过加密传输实现的,很难互相打通。这就导致了 passkey 经常受到的一种批评:明明是为了通用和便捷而采用的技术,反而造成了更多的封闭和麻烦。
macOS 26 和 iOS 26 引入的一些新变化,显示出这些问题得到改善的迹象。WWDC25 讲座《通行密钥更新》提到,通过一项与 FIDO 联盟(一个成立于 2013 年,旨在制定和推广身份验证标准的产业协会)成员合作的标准化数据格式,现在可以安全地在密码管理器之间迁移。迁移过程中,无须向磁盘导出任何不安全的明文密码,数据完全在应用之间传输。
你目前已经可以在 iOS 26 上尝试这个功能。首先,安装第三方密码管理器 Bitwarden,这是目前唯一正式支持接收 Password app 导出数据的第三方工具。然后,在自带的 Password app 中点击右上角的「…」,选择 Export Data to Another App。从列表中选择要导出的条目,再选择导出到 Bitwarden。经过 Face ID 验证后,就可以在 Bitwarden 中看到结果了。整个过程不需要导出外部文件,也不需要在应用之间来回切换。

(macOS 上的 Password app 也有相同功能的菜单项,但本文写作时暂时还没有发现能显示在导出选项中的第三方工具。不知为何,iOS 和 macOS 的用户手册都完全忽略了这项新功能。)
要支持这项新的导入和导出体验,第三方应用需要支持 iOS/macOS 26 的新增 API ASCredentialExportManager
和 ASCredentialImportManager
。不过,如上述 WWDC 讲座所介绍,这些并不是苹果独自发明的,而是对一项通用标准的实现。
根据这项名为「凭证交换协议」(Credential Exchange Protocol, CXP)的标准,新旧两个密码管理器可以根据这样的流程来安全传输凭证:
┌──────────────────────────────────────────────────────┐
│ Authorizing Party │
└─────┬──────────────────────────────────────────┬─────┘
│ (2) Determine (1) Create Export │
Migration Key Request
┌────────────┴──┐ ┌────┴──────────┐
│ ◀─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┤ │
│ │ │ │
│ │ │ │
│ │ │ │
│┌─────────────┐│ │ │
││ (3)Encrypt ││ │ │
││ Credential ││ │ │
││ Data ││ │ │
│└─────────────┘│ │ │
│ │ (4) Send Export Response │ │
│ │─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ▶┌─────────────┐│
│ │ ││ (5) Decrypt ││
│ │ ││ Bundle ││
│ │ │└─────────────┘│
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ Exporter │ │ Importer │
│ │ │ │
└───────────────┘ └───────────────┘
用通俗的话来描述:
- 导入方(新密码管理器)创建一个导出请求,其中包含请求范围、自己的公钥、加密配置以及一项身份验证等;
- 导出方(旧密码管理器)收到请求后,提示用户授权。获得用户同意后,用导入方的公钥和自己的私钥计算出一个加密密钥,用来加密导出的凭证数据。同时,在导入方发来的身份验证上签名,以证明自己的身份;
- 导出方将加密的凭证数据、自己的公钥以及对身份验证的签名回复打包成一个导出响应,发送给导入方;
- 导入方收到响应后,验证签名,确认导出方的身份。然后,使用自己的私钥和导出方的公钥,独立计算出加密密钥,进而解密凭证数据,解析后存入自己的数据库中。
可以看出,这个机制的一个优势在于加密密钥无需传输,而是由双方独立计算得出。这确保了即使传输过程被拦截,攻击者也无法解密凭证数据。
此外,CXP 还支持多种导出「模式」来适应不同的场景。iOS 和 macOS 此次新增的导出功能属于「直接传输」(direct)模式,也就是两个应用通过 API 直接通讯并传输响应。而如果涉及跨设备迁移等无法直接传输的情况,也可以使用「间接传输」(indirect)模式,让旧应用将相应内容存储为一个加密文件,然后手动导入新应用。最后还有一种特殊的「自传输」(self)模式,本质上就是一种安全备份功能,可以将凭证数据导出为加密文件,以便日后恢复。
解决了传输安全性的问题,还要考虑如何实现不同应用之间的格式兼容。这就是与 CXP 配套的「凭证传输格式」(Credential Exchange Format, CXF)要解决的问题。CXF 实际上提供了一套用 JSON 来描述凭证信息的通用模板,它的顶层结构大概长得像这样:
{
"version": {
"major": 1,
"minor": 0
},
"exporterRpId": "exporter.example.com",
"exporterDisplayName": "Exporter app",
"timestamp": 1705228800,
"accounts": [
{
"id": "DZSXp7iBQY-Fg-OofakQtQ",
"username": "jane_smith",
"email": "[email protected]",
"fullName": "Jane Smith",
"items": [
{
"id": "9OF-QjVDQo2Wp2xWPw6ZhA",
"creationAt": 1705142400,
"modifiedAt": 1705228800,
"title": "GitHub Login",
"scope": {
"urls": ["https://github.com"],
},
"credentials": [
// ...
]
},
// ...
],
"collections": [
// ...
]
}
]
}
可以看出,数据的开头给出了格式版本、导出方信息和时间戳等信息,然后是一个 accounts
数组,其中包含了此次导出的一或多个用户账户,是导出数据的核心。accounts
的内部是一个三层结构: