键集

Tink 使用密钥集来实现密钥轮替。正式地说,密钥集是一个包含密钥的非空列表1,其中有一个密钥被指定为主密钥(例如,用于对新明文进行签名和加密的密钥)。此外,密钥集中的密钥会获得唯一 ID2 和密钥状态,后者可让您停用相应密钥,而无需从密钥集中移除这些密钥。

密钥集是用户访问密钥的主要方式(通过 KeysetHandle 类)。这可确保每位用户都拥有同时处理多个密钥的代码。对于大多数加密用户而言,处理多个密钥是一种必要条件:需要能够更改密钥(例如,旧密钥可能会泄露),并且几乎不可能发生原子性“切换到下一个密钥”操作,全局范围内和所有密文均不可能即时应用到代码运行的机器和所有密文。因此,用户需要编写能够在一个键更改为下一个键时正常运行的代码。

示例:AEAD

请考虑这样一个 AEAD 密钥集,其中包含 AEAD 基元的多个密钥。如前所述,每个键都是唯一地指定两个函数: \(\mathrm{Enc}\) 和 \(\mathrm{Dec}\)。该密钥集现在还指定了两个新函数: \(\mathrm{Enc}\) 和 \(\mathrm{Dec}\) - \(\mathrm{Enc}\) 仅仅等于密钥集主密钥的函数 \(\mathrm{Enc}\) ,而该函数 \(\mathrm{Dec}\) 尝试使用所有密钥进行解密,并按某个顺序执行这些密钥(如需了解 Tink 如何提高其性能,请参阅下文)。

值得注意的是,密钥集是完整的键:它们是对所用函数 \(\mathrm{Enc}\) 和\(\mathrm{Dec}\) 的完整描述。这意味着,用户可以编写一个类,将 KeysetHandle 作为输入,表明该类需要对象的完整描述 \(\mathrm{Enc}\) 并 \(\mathrm{Dec}\) 正常运行。这样,用户就可以编写 API 来传达以下信息:要使用此类,您需要提供加密基元的描述。

密钥轮替

假设有一位 Tink 用户,他编写了一个程序,该程序首先从 KMS 获取密钥集,然后根据此密钥集创建 AEAD 对象,最后使用此对象加密和解密密文。

此类用户会自动为密钥轮替做好准备;如果用户当前的选择不再符合标准,还可以切换算法。

不过,在实现此类密钥轮替时必须小心一些:首先,KMS 应向密钥集添加新密钥(但尚未将其设为主密钥)。然后,需要将新密钥集发布到所有二进制文件,以便使用此密钥集的每个二进制文件都具有该密钥集中的最新密钥。只有这样,才能将新密钥设为主密钥,生成的密钥集会再次分发给使用该密钥集的所有二进制文件。

密文中的密钥标识符

请再次考虑 AEAD 密钥集的示例。如果以简单的方式解密密文,则需要 Tink 尝试使用密钥集中的所有密钥进行解密,因为无法知道哪个密钥用于加密该密钥集。这可能会导致大量性能开销。

因此,Tink 允许在密文中添加一个从该 ID 派生的 5 字节字符串作为前缀。按照上文“完整密钥”的原则,此前缀是密钥的一部分,曾使用此密钥派生的所有密文都应具有此前缀。用户创建密钥时,可以选择该密钥是否应使用此类前缀,或是否应使用不带此前缀的密文格式。

当某个密钥位于密钥集中时,Tink 会根据该密钥在密钥集中的 ID 计算此标记。在一个密钥集中,ID 是唯一的2意味着这些标记是唯一的。因此,如果仅使用标记的密钥,则与使用单个密钥进行解密相比,不会出现性能损失:Tink 只需在解密时尝试使用其中一个密钥。

不过,由于此标记是密钥的一部分,这也意味着,仅当相应密钥具有一个特定 ID 时,该密钥才能位于密钥集中。在描述用不同语言关键对象的实现时,这会带来一些影响。


  1. Tink 的某些部分仍会将 Keyset 视为一个集。不过,应进行更改。 原因在于该顺序通常很重要:例如,考虑一下使用 Aead 轮替密钥的典型生命周期。首先,将新密钥添加到密钥集。此键尚未设为主键,但仍处于活跃状态。这一新密钥集已面向所有二进制文件发布。一旦所有二进制文件都知道新密钥,该密钥就会被设为主密钥(只有此时使用此密钥是安全的)。在第二步中,密钥轮替需要知道最后添加的密钥。

  2. 为了与 Google 内部库兼容,Tink 允许在密钥集中使用重复的 ID。将来我们会移除此支持。