键集

Tink 使用 Keyset 来启用密钥轮替。从形式上讲,密钥集是一个非空的密钥列表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。我们日后将不再支持这种做法。