本文档正式定义了由 AES-GCM-HKDF 流式传输密钥表示的数学函数,以 proto 格式编码为 type.googleapis.com/google.crypto.tink.AesGcmHkdfStreamingKey
。
这种加密大致基于 HRRV151。对于安全分析,请参阅 HS202。
键和参数
以下部分对键进行了介绍(本文档中的所有大小均以字节为单位):
- \(\mathrm{KeyValue}\),一个字节字符串。
- \(\mathrm{CiphertextSegmentSize} \in \{1, 2, \ldots, 2^{31}-1\}\).
- \(\mathrm{DerivedKeySize} \in \{16, 32\}\).
- \(\mathrm{HkdfHashType} \in \{\mathrm{SHA1}, \mathrm{SHA256}, \mathrm{SHA512}\}\).
有效键还满足以下属性:
- \(\mathrm{len}(\mathrm{KeyValue}) \geq \mathrm{DerivedKeySize}\).
- \(\mathrm{CiphertextSegmentSize} > \mathrm{DerivedKeySize} + 24\) (这等于稍后介绍的 \(\mathrm{len}(\mathrm{Header}) + 16\) )。
在解析键或创建相应的基元时,Tink 会拒绝不满足其中任何属性的键。
加密功能
为了使用相关数据\(\mathrm{AssociatedData}\)加密消息 \(\mathrm{Msg}\) ,我们需要创建一个标头,将消息拆分为多个分段,对每个分段进行加密,然后将加密的分段串联起来。
创建标头
我们选择一个长度为\(\mathrm{DerivedKeySize}\) 的统一随机字符串 \(\mathrm{Salt}\) 和一个长度为 7 的统一随机字符串 \(\mathrm{NoncePrefix}\)。
然后设置 \(\mathrm{Header} := \mathrm{len}(\mathrm{Header}) \| \mathrm{Salt} \| \mathrm{NoncePrefix}\),其中标头的长度被编码为单个字节。请注意, \(\mathrm{len}(\mathrm{Header}) \in \{24, 40\}\).
接下来,我们使用 HKDF3 来处理由 \(\mathrm{HkdfHashType}\)以及输入 \(\mathrm{ikm} := \mathrm{KeyValue}\)、 \(\mathrm{salt} := \mathrm{Salt}\)和 \(\mathrm{info} := \mathrm{AssociatedData}\)提供的哈希函数,输出长度为 \(\mathrm{DerivedKeySize}\)。我们将结果称为 \(\mathrm{DerivedKey}\)。
拆分消息
接下来,消息 \(\mathrm{Msg}\) 会拆分为以下部分: \(\mathrm{Msg} = M_0 \| M_1 \| \cdots \| M_{n-1}\)。
它们的长度应符合以下要求:
- \(\mathrm{len}(M_0) \in \{0,\ldots, \mathrm{CiphertextSegmentSize} - \mathrm{len}(\mathrm{Header}) - \mathrm{16}\}\).
- 如果为 \(n>1\),则 \(\mathrm{len}(M_1), \ldots, \mathrm{len}(M_{n-1}) \in \{1,\ldots, \mathrm{CiphertextSegmentSize} - \mathrm{16}\}\)。
- 如果为 \(n>1\),则 \(\mathrm{len}(M_{0}), \ldots, \mathrm{len}(M_{n-2})\) 必须具有最大长度(根据上述约束条件)。
\(n\) 不得超过 \(2^{32}\)。否则加密失败。
加密块
为了加密区段 \(M_i\),我们计算 \(\mathrm{IV}_i := \mathrm{NoncePrefix}
\| \mathrm{i} \| b\),其中 \(\mathrm{i}\) 是采用大端序编码的 4 个字节,如果 $i < n-1$ ,则字节 $b$ 为 0x00
,否则为 0x01
。
然后,我们使用 AES-GCM4 进行加密,其中密钥为\(\mathrm{DerivedKey}\),初始化矢量为 \(\mathrm{IV}_i\),关联数据为空字符串。 \(C_i\) 是此加密的结果(即链接的 AES-GCM 参考文档第 5.2.1.2 节中的 \(C\) 与 \(T\) 的串联)。 \(M_i\)
串联加密片段
最后,所有分段都以 \(\mathrm{Header} \| C_0 \| \cdots \| C_{n-1}\)的形式串联起来,即最终的密文。
解密
解密会反加密。我们使用标头获取 \mathrm{NoncePrefix}$$,并单独解密密文的每个分段。
API 可以(且通常允许)允许随机访问,即在不检查文件结尾的情况下访问文件的开头。这是有意为之,因为可以从 \(C_i\)解密 \(M_i\) ,而无需解密之前和其余的所有密文块。
不过,API 应格外小心,不要让用户混淆文件末尾错误和解密错误:在这两种情况下,API 都可能需要返回错误,而忽略差异会导致攻击者能够有效截断文件。
键的序列化和解析
要序列化“Tink Proto”格式的键,我们首先以明显的方式将参数映射到 aes_gcm_hkdf_streaming.proto 中给出的 proto 中。字段 version
需要设置为 0。然后,我们使用常规 proto 序列化对其进行序列化,并将生成的字符串嵌入到 KeyData proto 的值中。我们将 type_url
字段设置为 type.googleapis.com/google.crypto.tink.AesGcmHkdfStreamingKey
。然后,将 key_material_type
设置为 SYMMETRIC
,并将其嵌入密钥集。我们通常将 output_prefix_type
设置为 RAW
。例外情况是,如果解析键时使用了为 output_prefix_type
设置的其他值,Tink 可能会写入 RAW
或先前的值。
为了解析键,我们会反转上述过程(以解析 proto 时的常规方式)。系统会忽略 key_material_type
字段。可以忽略 output_prefix_type
的值,也可以拒绝 output_prefix_type
与 RAW
不同的键。version
不为 0 的密钥必须被拒绝。
已知问题
上述加密函数的实现不应是分支安全的。请参阅叉子安全。
参考
-
Hoang、Reyhanitabar、Rogaway、Vizar,2015 年。在线身份验证加密及其防 Nonce 重用误用。CRYPTO 2015。 https://eprint.iacr.org/2015/189 ↩
-
Hoang、Shen,2020 年。Google Tink 库中流式加密的安全性。https://eprint.iacr.org/2020/1019 ↩
-
RFC 5869。基于 HMAC 的提取和扩展密钥派生函数 (HKDF)。 https://www.rfc-editor.org/rfc/rfc5869 ↩
-
NIST SP 800-38D。关于分块加密操作模式的建议:伽罗瓦/计数器模式 (GCM) 和 GMAC。https://csrc.nist.gov/pubs/sp/800/38/d/final ↩