Confidex
章节

服务器如何存储和同步我们的秘密而不持有密钥来读取它们?我们将了解 Confidex 如何创建其密钥、如何用锁保护它们以及如何使用它们来加密数据和验证更改。

锁

只有被选中之人才能打开这扇门

矩阵重装上阵

想象一下,我们正在构建一个虚拟的 vault 来存储秘密数据。应用程序关闭后,它必须将这些数据保留在笔记本电脑上,并让我们在另一台设备上打开它。金库需要自己的钥匙。我们必须存储它们,以便复制文件使外人无法读取或更改其内容。

lock 是一个保护密钥的小型加密文件. 打开它需要主密码和秘密代码 secret_key, 文件本身不包含这两项秘密. 我们甚至可以把整个锁文件印在早报上. 读者会得到一份副本, 但仅仅公开文件并不会泄露私钥.

假设外部人员获取了该文件,甚至得知了主密码。他们现在可以重复每个只需要密码的计算,但这些计算不会产生打开锁的钥匙:secret_key 也是一个输入。该文件不包含单独的密码检查。打开它仍然需要未知的密码。

该代码来自加密随机数据;它不是一个人发明的。只要 secret_key 未知,即使正确猜出主密码也无法打开锁:攻击者仍然缺乏随机秘密。因此,仅弱密码不会将服务器副本变成实际的字典攻击。

如果攻击者还获得了 secret_key,他们可以离线测试主密码猜测。这是成本高昂的,但最终的难度取决于密码的强度。

密钥

先生们,钥匙有什么作用?钥匙……解锁东西?

加勒比海盗 – 聚魂箱

应用程序从操作系统的加密生成器获取 32 个随机字节,并将它们准备用作密钥。这些准备好的字节是私有主密钥,master_key。

应用程序始终从 master_key 派生相同的密钥:对相同字节重复相同的计算会得到相同的结果。因此,笔记本电脑和手机可以恢复相同的密钥。要在新设备上获取 master_key,应用程序必须首先使用主密码和 secret_key 打开锁。

第一对:加密

我是钥匙大师。

捉鬼敢死队

为了存储加密数据,应用程序根据 master_key 计算公钥:secret_key。它可以加密保管库的数据,而解密则需要master_key。

“公开”描述了该密钥在算法中的作用,而不是必须将其提供给每个人。在 Confidex 中,它保留在用户手中。它也是打开锁所需的第二个秘密,因此它保留在本地并且不会通过网络发送。就其本身而言,secret_key 允许某人加密保管库的数据,但不能解密它或代表其签署更改。

X25519 是一种以 master_key 为输入, 计算 secret_key 的算法. master_key 无法在现实可行的计算量内仅根据 secret_key 恢复. 即使是一台每秒执行 10^18 次曲线运算的假想超级计算机, 进行经典攻击也平均需要约 2.3 万亿年 (SafeCurves 估算).

public bytes Random bytes32 bytes master_key secret_key public bytes Random bytes32 bytes master_key secret_key
从随机字节到加密密钥。

第二对:签名

唯一原创的是签名

如果可以的话来找我

第一对有助于隐藏内容。我们现在需要一种方法来确认保管库所有者授权进行更改。服务器也必须能够检查签名,因此我们需要向其发送公共验证密钥。第一对的公共部分是 secret_key,服务器不得接收该部分。因此,我们为签名创建第二个永久密钥对。

我们没有存储另一个独立的秘密,而是从 master_key 派生一个私有签名密钥并将其命名为 signing_key。应用程序使用它来签署数据和锁。其他设备和服务器使用其公共部分来检查签名而不读取内容。

我们可以从同一个 master_key 恢复这一对。应用程序从中导出一个起始值:唯一确定签名密钥的 32 字节。

为了为不同的作业派生不同的密钥,我们在计算中包含一个固定标签:此处为confidex/vault/ed25519/v1。相同的 master_key 和相同的标签总是产生相同的密钥。

主密码没有进入这些计算。这两对都依赖于 master_key 的随机字节,而不是由人发明的字符串。这种区别稍后会很重要:密码将保护对已存在密钥的访问,因此我们可以在不更改密钥本身的情况下更改它。

派生使用基于哈希的密钥派生函数:HKDF、HMAC-based Extract-and-Expand Key Derivation Function。 Confidex 与 SHA-256 一起使用,因此名称为 HKDF-SHA256。生成的 32 个字节是 Ed25519 对的 seed。 signing_key的公共部分不会泄露secret_key,因此我们可以将其发送到服务器进行签名验证。

public bytes HKDF-SHA256keymaster_keyinfo"confidex/vault/ed25519/v1" signing_key Signature verification public bytes HKDF-SHA256keymaster_keyinfo"confidex/vault/ed25519/v1" signing_key Signature verification
从同一主密钥派生的单独签名密钥。

什么是锁定文件,它有什么作用?

打开吊舱门,HAL。
对不起,戴夫。恐怕我做不到。

太空漫游

锁存储加密的master_key。打开它需要主密码和secret_key。公共标头包含格式版本、保管库标识符、signing_key 的公共部分、盐和密码处理参数。内部格式版本与 master_key 一起加密。

创建锁时,应用程序已经拥有 master_key,因此它会在加密之前计算 secret_key。打开它时,用户单独提供 secret_key:它不存在于锁的公共副本中。

解密后,应用程序使用 X25519 计算 master_key 的公共部分,并要求其与 secret_key 匹配。然后它恢复 signing_key 并将其公共部分与标头中的密钥进行比较。这样,锁就可以进行自我检查。

为了减缓主密码猜测的速度,每次尝试都必须花费时间和内存。 Confidex 使用具有 64 MiB 内存和三遍的 Argon2id。密码处理也使用随机盐。盐并不秘密:它使相同的密码在不同的锁中产生不同的结果。

如果 secret_key 已知,则密码长度会极大地改变攻击的规模。 5 个随机小写拉丁字母的组合大约有 1200 万种,7 个随机小写拉丁字母的组合大约有 80 亿个,12 个字母的组合大约有 9.5 × 10¹⁶个。从 7 个字母到 12 个字母,搜索空间增加了近 1200 万倍。如果在给定机器上搜索 7 个字母平均需要一天,那么搜索 5 个字母大约需要两分钟,搜索 12 个字母大约需要 33,000 年。

为了从 Argon2id 结果和 secret_key 中获取一个键,我们使用前面介绍的 HKDF-SHA256 函数,并使用标签 confidex/lock/wrap/v1。结果是 wrapping_key,即加密锁的密钥。

我们必须隐藏 master_key 并保护它不被篡改。 XChaCha20-Poly1305 使用 wrapping_key 对其进行加密并添加身份验证标记。该标签将密文绑定到公共标头:错误的密码或对受保护字节的更改会导致打开失败。标签和随机一次性值 nonce 与密文一起存储。

每次再次创建或加密锁时,应用程序都会获得新鲜的盐和nonce。加密内容包括1个版本字节和32字节的master_key;一个 24 字节的 nonce 和一个 16 字节的标签与它们一起存储。 算法文档描述了其构造。

恢复的密钥之间的不匹配可能是由于故意畸形的锁或创建它的程序中的错误造成的。当检查标签时,通常会更早地检测到随机字节损坏。

服务器没有检查此标签所需的秘密。因此,为了转移锁定,应用程序会添加使用 signing_key 制作的签名。它涵盖了整个完成的文件,包括公共标头和加密内容。服务器检查签名,并在首次上传时固定 signing_key 的完整公共部分和内部保管库标识符。它会根据该键检查后续更改。锁和加密数据使用相同的 signing_key 进行签名,从 master_key 恢复。

金库

我们现在可以发布锁了。只有知道 secret_key 和 master password 的人才能读取 master_key,而任何人都可以从锁文件中获取公共签名密钥并检查签名数据,而无需查看锁或数据的内容。

考虑一个密码,SuperPa55worD。现在,我们只想保存它:关闭应用程序后,我们应该能够将锁定文件和加密密码发送到任何地方,而不会让任何人看到 SuperPa55worD,同时仍然让他们验证我们的作者身份。当我们有 secret_key 和 master password 时,加密必须是可逆的。对称密码可以使用相同的密钥进行加密和解密。由于我们不存储该密钥,因此我们必须能够使用锁恢复它。我们将其称为message_key。

我们将为每个新加密获取单独的 message_key。我们仍然需要安排它的恢复:将密钥放在密文旁边,任何拥有副本的人都可以读取所有内容。

应用程序根据新的随机字节创建一个临时的 X25519 对,专门用于此加密:record_key。然后,它采用 record_key 的私有部分和保管库的永久公钥 secret_key,并计算共享密钥。一个单独的函数从该秘密派生出 message_key;这个密钥加密我们的密码。

读取时,应用程序采取另一条路线。它使用 master_key 和 record_key 的公共部分。这两个密钥产生相同的共享秘密。从数学上讲,两条路线都以不同的顺序将曲线上的相同初始点乘以两个私人数字。该顺序不会改变结果。因此,我们可以在不保留 record_key 的私有部分的情况下恢复解密密钥。

从两个公钥中获取这个秘密实际上是不可能的:任一路由都需要相应的私钥。公共点不允许我们有效地恢复产生它们的私人号码。这个数学技巧支撑着金库的安全。但这并不能阻止有人给我们一个不同的金库,上面挂着我们的锁。

我们现在可以将密文与 record_key 的公共部分一起存储。以后的读取不再需要 record_key 的私有部分和使用的 message_key。随后,应用程序从 master_key 恢复解密密钥以及 record_key 存储的公共部分。因此,一个永久保管库秘密可以解密许多单独加密的值,每个值都有自己的 message_key 在需要时恢复。

隐藏文本是不够的:我们还必须在阅读时检测到篡改。因此,加密添加了一个身份验证标签,即与加密数据一起检查的短值。如果检查失败,应用程序将拒绝结果。它不得将随机或更改的文本显示为成功解密的密码。

public bytes Fresh random bytes record_key secret_key Shared secret Derive message key message_key Encrypted passwordpacket public bytes Fresh random bytes record_key secret_key Shared secret Derive message key message_key Encrypted passwordpacket
每次加密都有一个新的密钥对。

我们描述了一种结构,其中非对称对有助于建立共享秘密,而 message_key 则对内容进行加密。这称为 Hybrid Public Key Encryption 或 HPKE,并在 RFC 9180 中指定。 Confidex 使用 X25519 就秘密达成一致,使用 HKDF-SHA256 派生密钥,并使用 ChaCha20-Poly1305 进行加密并进行完整性检查。此处,HKDF 采用使用 record_key 和 master_key 对商定的共享密钥;这是与派生 Ed25519 seed 不同的工作。 RFC 5869 描述了 HKDF。

对于下一次加密,应用程序将创建一个新的 record_key 和一个新的 HPKE 上下文。重复使用 message_key 和 nonce 的组合会破坏密码的保护。在Confidex中,密钥和nonce在HPKE内部派生,用于一条消息,然后丢弃上下文。

ChaCha20 本身使用 256 位的 message_key。为了了解直接攻击的感觉,想象一下一台虚构的超级计算机每秒测试 10¹⁸ 个候选密钥。搜索 2²⁵⁶ 可能的键大约需要 1.8 × 10⁵¹ 年。 RFC 8439 描述了该算法及其分析。

一个记录

我们幸福吗?

低俗小说

我们已经保存了密码,但登录网站通常还需要用户名和地址。有时我们需要在它们旁边放一张纸条。如果我们单独存储这些值,应用程序必须以某种方式记住它们属于一起。让我们把这些“文件”装进信封里。我们将这些字段合并到一个集合中,并将其称为 record。

记录可以包含字段的任意组合。应用程序将它们组装成一个数据包,对其进行压缩并使用我们刚刚描述的结构对整个数据包进行加密。字段值、名称、类型和标签都受到保护。更改网站地址,即使保持相同的密码,也会创建整个记录的新版本,并使用新的 record_key 进行加密。

哈希链

您的项目应该使用区块链吗? → 不

XKCD 2267

知道 secret_key 的人可以为我们的保管库准备加密记录。为了避免接受它作为我们的,应用程序使用 signing_key 签署每个记录版本。如果没有此密钥,外部人员无法创建带有服务器和其他设备可接受的签名的版本。

加密版本需要一些附带信息:它属于哪个保管库和记录、何时更改以及它之前的版本。这形成了公共标头。它不包含密码,但仍然需要保护:否则有人可以在不接触密文的情况下更改对象的所有权和关系。应用程序将确切的标头字节与密文一起签名。更改签名数据会使签名无效。

服务器就像一个密封包裹的仓库。它看到了标头、加密内容和签名,但看不到里面的密码,并且它知道收件人存在于该城市。它根据 signing_key 的固定公共部分检查签名,并决定是否接受新版本。它既不需要主密码,也不需要 secret_key 或任何私钥。读取时,客户端检查相同的签名,然后在解密期间使用 HPKE 身份验证标记再次检查内容和标头之间的绑定。

签名还保护版本之间的链接。确切的标头和密文字节的 SHA-256 哈希成为版本的标识符。下一个版本的标头包含上一个版本的标识符。当我们更改网站密码时,应用程序会使用此链接创建并签署一个新对象。旧对象及其签名保持不变。因此,在保留新版本签名的同时替换前一个版本将会失败。如果两个设备进行独立更改,则两个分支都会存活;合并版本可以引用两者。

还记得区块链吗?就在那里:-)。

公共标头允许我们在不解密的情况下检查链接,但某些信息仍然可见。服务器知道客户端提供的标识符、版本链接、更改时间、字段数量以及每个字段值的编码大小。加密隐藏了记录的内容,而不是结构信息。

Header andparent IDs Encrypted recordpacket Sign revision signing_key Signed revision Header andparent IDs Encrypted recordpacket Sign revision signing_key Signed revision
标头和密文一起签名。

同步

“不是数据泄露,免费的分布式备份。”

互联网模因。

为了在多个设备上工作,我们需要在它们之间传输锁定和记录历史记录。对象格式不会改变:我们传输本地存储的相同加密字节和签名。

对于手动传输,应用程序会创建一个包含锁、所有本地存储的加密记录版本以及签名文件列表的 ZIP 存档。不包含密码和 secret_key。在另一台设备上,应用程序首先打开锁,然后检查签名、解密记录并恢复本地保管库副本。导入适用于尚不存在此保管库的设备;它不会将历史记录与现有副本合并。

同步服务器在工作副本之间交换更改。首次上传时,它会收到签名锁、检查签名并根据 signing_key 的公共部分计算公共保管库地址。然后它会记住该密钥和标头中的保管库标识符:这会在服务器上注册保管库。后续更改必须与此标识符匹配并携带来自同一密钥的签名。

新设备需要保管库地址和 secret_key,因此应用程序将它们组合成带有校验和的传输代码。用户将此代码传输到设备并单独输入主密码;仅将公共地址发送到服务器。

打开下载的锁后,应用程序将恢复的密钥的公共部分与 secret_key 和标头进行比较,并根据传输代码检查计算出的地址。然后保存 signing_key 中已验证的公共部分以检查记录。

在同步期间,应用程序会获取缺失的版本并上传其更改。服务器检查签名和到先前版本的链接,存储接受的对象并将它们传递到其他设备。检查接收到的数据、解密数据并解决冲突都发生在用户的设备上。

注册无需提供姓名、电子邮件地址或单独的密码。服务器看到公共地址、签名验证密钥、公共标头、对象大小和版本链接。它还可以查看 IP 地址和请求时间。主密码、secret_key、私钥和明文记录内容保留在用户的设备上。任何知道公共地址的人都可以下载加密的对象,但读取它们需要用户的秘密。

这些检查检测伪造的对象,但不能证明服务器已显示完整的历史记录及其最新状态。它可以隐藏客户端从未见过的分支或返回一致的旧数据集。签名确认单个对象的真实性,而不是服务器响应的完整性。用户可能会注意到另一台设备上缺少更改,但一致的旧历史记录将通过 hash-chain 验证。

更改主密码

那么,陛下愿意更改她的密码吗?

黑客 (1995)

更改主密码仅重新加密锁;里面的master_key保持不变。因此,记录、签名、secret_key 和保管库地址保持不变。新锁使用相同的 signing_key 进行签名。

旧的锁定副本仍会使用旧密码和相同的 secret_key 打开;更改密码不会撤销已经暴露的密钥。

完整图片

锁保护master_key,message_key 加密记录,signing_key 创建签名。 ZIP 档案和服务器接收带有签名的加密对象。

public bytes public bytes public bytes master_key Fresh randombytes secret_key record_key Master password wrapping_key signing_key message_key lock Encrypted records Signed lockand records Signatureverification ZIP archive /sync server public bytes public bytes public bytes master_key Fresh randombytes secret_key record_key Master password wrapping_key signing_key message_key lock Encrypted records Signed lockand records Signatureverification ZIP archive /sync server
密钥、它们保护的内容以及可以共享的内容。