密钥圈中的大多数密钥将包含某种形式的 entropy,并且将具有指示“CBC 模式加密 + HMAC 验证”或“GCM 加密 + 验证”的算法信息。 在这些情况下,我们将嵌入式熵称为此密钥的主密钥材料(或 KM),我们执行密钥派生函数来派生用于实际加密操作的密钥。
注释
键是抽象的,自定义实现的行为可能不如下。 如果密钥自行实现 IAuthenticatedEncryptor ,而非使用我们的内置工厂之一,那么本节所描述的机制将不再适用。
其他经过身份验证的数据和子密钥派生
该 IAuthenticatedEncryptor 接口充当所有经过身份验证的加密作的核心接口。 其 Encrypt 方法采用两个缓冲区:明文和附加认证数据(AAD)。 纯文本内容在调用 IDataProtector.Protect 时保持不变,但 AAD 由系统生成,由三个组件组成:
标识此版本数据保护系统的 32 位魔数标头 09 F0 C9 F0。
128 位密钥 ID。
由创建正在执行此操作的
IDataProtector的目的链构成的可变长度字符串。
由于 AAD 对于所有三个组件的元组是唯一的,因此我们可以使用它从 KM 派生新密钥,而不是在所有加密作中使用 KM 本身。 每次调用 IAuthenticatedEncryptor.Encrypt时,都会执行以下密钥派生过程:
( K_E, K_H ) = SP800_108_CTR_HMACSHA512(K_M, AAD, contextHeader || keyModifier)
在这里,我们在计数器模式下调用 NIST SP800-108 KDF(请参阅 NIST SP800-108,第5.1节),并使用以下参数:
密钥派生密钥 (KDK) =
K_MPRF = HMACSHA512
标记 = 附加认证数据
context = contextHeader ||keyModifier
上下文标头的长度可变,实质上是我们为其派生 K_E 和 K_H的算法的指纹。 密钥修饰符是为每次调用 Encrypt 随机生成的 128 位字符串,用于确保 KE 和 KH 在此特定身份验证加密操作中是唯一的,即使 KDF 的所有其他输入都是常量。
对于 CBC 模式加密 + HMAC 验证作, | K_E | 是对称块密码密钥的长度,并且 | K_H | 是 HMAC 例程的摘要大小。 对于 GCM 加密 + 验证操作, | K_H | = 0。
CBC 模式加密 + HMAC 验证
通过上述机制生成后 K_E ,我们将生成随机初始化向量,并运行对称块密码算法来加密纯文本。 然后,初始化向量和密码文本通过使用密钥 K_H 初始化的 HMAC 例程运行,以生成 MAC。 此过程和返回值在下图中以图形形式表示。
output:= keyModifier || iv || E_cbc (K_E,iv,data) || HMAC(K_H, iv || E_cbc (K_E,iv,data))
注释
在将 magic 标头和密钥 ID 返回到调用方之前,该 IDataProtector.Protect 实现会将 magic 标头和密钥 ID 追加 到输出。 由于魔术标头和密钥 ID 是 AAD 的隐式部分,并且密钥修饰符作为输入馈给 KDF,这意味着最终返回的有效负载中的每个字节都由 MAC 身份验证。
Galois/计数器模式加密与验证
一旦K_E通过上述机制生成,我们将生成一个随机的96位nonce,并运行对称块密码算法用于加密纯文本,从而生成128位身份验证标记。
output := keyModifier || nonce || E_gcm (K_E,nonce,data) || authTag
注释
尽管 GCM 原生支持 AAD 的概念,但我们仍然只向原始 KDF 馈送 AAD,而是选择传递一个空字符串给 GCM 作为其 AAD 参数。 这么做的原因有两个。 首先, 为了支持敏捷性 ,我们绝不想直接用作 K_M 加密密钥。 此外,GCM 对其输入施加了非常严格的唯一性要求。 GCM 加密例程在具有相同(密钥、nonce)对的两组或更多不同输入数据集上调用的概率不得超过 2^-32。 如果修复 K_E,在触碰 2^-32 限之前,无法执行超过 2^32 次加密操作。 这似乎是一个非常大量的操作,但对于高流量的 Web 服务器来说,在短短几天内就能处理 40 亿个请求,这完全在这些密钥的正常生命周期之内。 为了符合 2^-32 概率限制条件,我们继续使用 128 位密钥修饰符和 96 位 nonce,这将根本上扩展任何给定K_M的可用操作计数。 为了简单起见,我们在 CBC 和 GCM 操作之间共享 KDF 代码路径,并且由于 AAD 已在 KDF 中考虑,因此无需将其转发到 GCM 操作。