HPKE笔记
这几天看ECH和HPKE的笔记,然后惊讶地发现国内没有人看这个的,就知乎上有个机器人一样的东西提到一句,所以就打算记录下这个东西并简单总结,所以有了这片博客。从某种程度来说,这片博客大部分内容是RFCHybrid Public Key Encryption draft-irtf-cfrg-hpke-09的翻译,当然里面会穿插一些具体的内容。
2024/07/04,这几天重新翻笔记发现一些错误。再重新看了下HPKE标准发现,已经正式发布,而且内容发生了一些变化,因此做一些更正。
注意,以“我:”开始的内容,都是我自己写的东西,不在官方内容里。纯粹是方便理解加的。
0 HPKE是什么(白话版)
HPKE即hybrid public-key encryption(HPKE),翻译成中文是复合公钥加密方案。HPKE提供的实际上是利用接收端公钥产生会话秘钥,并加密任意长度的明文信息的功能。说的好像很复杂, 举个例子就明白了,TLS协议就是一个关联了一定上下文信息拓展后的HPKE。
按照具体模块划分,HPKE可以视为秘钥衍生函数(key derivation function (KDF)),密钥封装机制(key encapsulation mechanism(KEM))与加密并认证机制(authenticated encryption with additional data (AEAD))三个组合起来一种实现。目前HPKE适用于任何基于非对称密钥封装机制(KEM)、密钥派生函数(KDF)和带有附加数据的认证加密(AEAD)加密函数的组合。
上面这段话说起来好像还是很难懂,我用最简单(可能会有部分术语需要阅读下面的内容)的话语解释下:
HPKE实际上是发送方利用接收端的公私钥对里面的公钥,和本地私有或者临时产生的公私钥对里面的私钥计算得出一个共享的密钥。这个共享的密钥是不会明文传递的,它只会把地私有或者临时产生的公私钥对里面的公钥传递给接收方。发送方和接收方除了传递公钥外,还有一些诸如附加信息,比方说认证信息,普通信息参与计算产生一个密钥派生上下文,里面会包含nonce等信息。使用这些上下文信息与共享密钥就可以对明文做加密运算了。
举个简单的例子,就可以明白了,这里以DH交换协议为例子,请注意这里需要读者知道DH交换的基本前提:
DH(skX, pkY)
: 使用私钥skX和公钥pkY执行非交互式Diffie-Hellman交换(这里的非交互我理解是指没有真正的走网络协议啥的传递),以生成长度为Ndh的Diffie-Hellman共享密钥。这个过程说白了就是一个公式DH(skE, pkR)=DH(skR, pkE)
下面给出HPKE在DH上的核心流程,即计算得出KEM共享密钥:
-
首先生成一个临时KEM公私密钥对(KEM Ephemeral Key Pair),使用临时私钥和接受方公钥执行Diffie-Hellman交换计算得出一个共享的DH交换结果(这个结果是只有通信双方知道),DH交换结果参与运算得出KEM共享密钥。而临时公钥经过编码后会发送给接收方。如果关键的代码为
def Encap(pkR): skE, pkE = GenerateKeyPair() dh = DH(skE, pkR) enc = SerializePublicKey(pkE) pkRm = SerializePublicKey(pkR) kem_context = concat(enc, pkRm) shared_secret = ExtractAndExpand(dh, kem_context) return shared_secret, enc
-
接收方收到编码过的发送方临时公钥,解码后和自己的接收方私钥做DH交换,利用DH交换结果得到得出KEM共享密钥
def Decap(enc, skR): pkE = DeserializePublicKey(enc) dh = DH(skR, pkE) pkRm = SerializePublicKey(pk(skR)) kem_context = concat(enc, pkRm) shared_secret = ExtractAndExpand(dh, kem_context) return shared_secret
-
计算
1 总览
自从公钥秘钥体制诞生就有很多尝试结合非对称加密和对称加密来提供机密性的例子,这种结合往往是在利用对称加密的性能优势同时,使用非对称机密的管理优势(我理解指可以方便的暴露公共密钥)。传统的实现方式是使用非对称秘钥加密对称秘钥。HPKE采用了一种不同的手法:使用公钥生迭代生成对称秘钥和封装后的密钥。具体来说,就是加密消息传达了一个用公钥密码体制(交换算法)封装的(对称)加密密钥,以及一个或多个使用该密钥加密的任意大小的密文。
2 名词定义
2.1 基础名词
以下术语用于描述 HPKE 的操作、角色和行为,这部分没有任何理解的成本,主要是区分开谁是谁:
(skX, pkX)
:角色X使用的一对密钥封装机制( Key Encapsulation Mechanism (KEM))密钥对。X可以是 S(发送方), R(接收方),或者是E(临时派生,不过一般这个最后会变成S用的密钥),”skX” 是秘钥 “pkX” 是公钥。我:这个东西可以视为HPKE最基本的东西了,没有密钥封装机制密钥对或者说公私密钥对就没有安全的派生共享密钥的方法,这里的KEM Key Pair可以理解为(EC)DHE密钥协商的起点。pk(skX)
:和KEM私钥对应的KEM公钥,X代表角色是谁。对发送方而言,skX可能是临时生成的。Sender (S)
:发送者。Recipient (R)
:接受者Ephemeral (E)
: 临时产生的随机值,一般是只用一次,大多数情况都是发送方会调用,参与一次“会话”计算共享密钥流程。concat(x0, ..., xN)
:拼接字符串。比方说”concat(0x01, 0x0203, 0x040506) = 0x010203040506”.- “
random(n)
”: 产生长度为”n” 字节长度的伪随机字符串 - “
I2OSP(n, w)
“:转换非负整数n为一个长度为w的使用网络字节序或者说大端的字符串。参考RFC8017 - “
OS2IP(x)
“:将字符串x
转换为一个非负整数,如 [RFC8017] 中所述,这里假定字符串X为大端字节顺序。
2.2 HPKE的加解密基础名词
具体到一套HPKE的内部,按照上面提到的三个模块划分,有很多基础的名词需要理解。
-
KEM
-
GenerateKeyPair()
:随机算法用于生成秘钥对(skX, pkX)
。我:很多时候通信发送方是“匿名”的,只能随机生成一个公私密钥对 -
DeriveKeyPair(ikm)"
:确定性的密钥延生算法,从ikm衍生出固定的秘钥对 “(skX, pkX)
”. -
SerializePublicKey(pkX)
:将公钥”pkX”.编码为”Npk”长度 的字符串(说白了就是序列化) -
“
DeserializePublicKey(pkXm)
“:反序列化公钥 -
“
Encap(pkR)
“一整套流程,包括:1 使用随机算法生成一个临时,固定长度的对称密钥(被称为“KEM 共享密钥”)2 生成一个1里面KEM共享密钥的封装结果,这个结果只有pkR,即持有pkR的私钥接收方可以解密。 -
“
Decap(enc, skR)
“使用私钥skR从KEM共享密钥的封装结果enc
中还原临时,固定长度的对称密钥也就是KEM共享秘钥 -
“
AuthEncap(pkR, skS)
“:同Encap类似,但是附加一个验证:确保KEM共享秘钥由skS的拥有者,即发送方生成。 -
“
AuthDecap(enc, skR, pkS)
“:同Decap类似,但是接收端可以验证KEM共享秘钥由skS的拥有者生成。
-
-
KDF
-
“
Extract(salt, ikm)
“:结合可选参数salt,从输入密钥材料ikm里,提取固定长度为Nh字节的伪随机秘钥。 -
“
Expand(prk, info, L)
“:利用刚才的伪随机秘钥,即prk,结合可选字符串info,拓展为长度为L的最终产生的秘钥材料。
-
-
AEAD
-
“
Seal(key, nonce, aad, pt)
“。使用关联数据aad和对称秘钥key和nonce加密明文pt,产生秘文和tag,最终的结果为ct -
“
Open(key, nonce, aad, ct)
” 使用关联数据aad和对称秘钥key和nonce解密秘文ct,产生明文pt。需要验证密文中tag是不是有效的。
-
一般说HPKE的时候,都会提具体的密码套件是啥,这里密码套件是一个(KEM、KDF、AEAD)组成的三元组,三元组里面每一项都是具体的算法。
还有一点要注意,有一个基本的秘钥操作:
def LabeledExtract(salt, label, ikm):
labeled_ikm = concat("HPKE-v1", suite_id, label, ikm)
return Extract(salt, labeled_ikm)
def LabeledExpand(prk, label, info, L):
labeled_info = concat(I2OSP(L, 2), "HPKE-v1", suite_id, label, info)
return Expand(prk, labeled_info, L)
3 HPKE的内容
这部分给出HPKE提供的关键函数和完整的流程定义,我把官方的解释做了一下润色,不过直接看英文版文档我理解就够了。
官方定义了几种 HPKE 变体(这个变体含义好像很难理解,可以这么理解:HPKE对应大类人,而人细分为很多变体,比方说黑人,白人黄种人blablabla)。所有变体都需要输入接收方公钥和明文pt
,并生成封装后的密钥enc
和一堆密文序列ct
。HPKE的构建方式保证只有skR
的持有者,即接收端能够从enc
解封装密钥并解密密文。所有算法还接受一个info
参数,可用于影响密钥的生成(比方说提供密钥身份信息)和一个aad
参数,用于给AEAD 算法提供额外的认证数据。¶
除了将公钥加密产生enc的基本情况外,官方还提供三种认证变体:
- 一种认证通过PSK就是预共享密钥,即你有PSK,你是真用户。这种模式叫做mode_psk,用一字节表示,内容为0x01
- 一种认证通过持有KEM 私钥,就是你有私钥,你是真用户。这种模式叫做mode_auth,用一字节表示,内容为0x02
- 一种认证同时拥有预共享密钥和 KEM 私钥的情况,这个结合上面两种算法。这种模式叫做mode_auth_psk,用一字节表示,内容为0x03
这三种认证变体都会为加密操作,即产生enc的过程,提供额外的密钥材料。
综上,可以将HPKE视为包含两步的完整流程:
- 建立一个在发送方和接收方之间共享的加密上下文(之所以不是加解密上下文,注意HPKE只用于单向传输,发送方不能又用它加密,又解密,我觉得这是英文里面只提加密上下文的原因)。¶。这里加密上下文可以理解为包含AEAD 算法,密钥进行编码,nonce的结构。这里nonce不会参与多个明文加密行为。这个加密上下文还支持用于导出密钥(用于比方说复用等环境,可以对标TLS)
- 使用该上下文来加密或解密内容。
接下来细说两个步骤。
3.1 创建加密上下文
无论哪种HPKE的变体需要输入一下内容来参与运算,从而得出加密上下文
-
mode,一字节HPKE 模式,就上面提到的三种变体+基础模式
-
“shared_secret”,KEM共享秘钥,本次会话生成
-
“info”,应用程序提供的信息 (可选,初始值为空字符串,就是””).
-
“psk”,发送方和接收方都持有的预共享密钥(PSK)(可选;默认值为空字符串)。
-
“psk_id”,PSK 的标识符(可选;默认值 ““),因为可能有多个PSK
在 Auth 和 AuthPSK 模式中,接收方可以确信发送方持有私有密钥skS
。对于上面提到的DHKEM 变体,发送端的密钥是可能泄露了的,所以如果可选的话,最好使用PSK或者Auth模式。
好,现在我们开始看看针对发送方或者接收方如何产生加密上下文,首先需要指明HPKE的ciphersuite,HPKE 算法标识符,包含 KEM kem_id
、KDF kdf_id
和 AEAD aead_id
的信息。诸如里面的xxx_id一般是预先定义好的。
suite_id = concat(
"HPKE",
I2OSP(kem_id, 2),
I2OSP(kdf_id, 2),
I2OSP(aead_id, 2)
)
具体的计算流程如下(请注意,这里不涉及如何传递Enc),ROLE为接收方或者发送方,简单来说就是:
- 检查HPKE模式和传递的PSK信息是否充足,如果不匹配就报错
- 输入上面的输入内容,计算得出通信密钥
default_psk = ""
default_psk_id = ""
def VerifyPSKInputs(mode, psk, psk_id):
got_psk = (psk != default_psk)
got_psk_id = (psk_id != default_psk_id)
if got_psk != got_psk_id:
raise Exception("Inconsistent PSK inputs")
if got_psk and (mode in [mode_base, mode_auth]):
raise Exception("PSK input provided when not needed")
if (not got_psk) and (mode in [mode_psk, mode_auth_psk]):
raise Exception("Missing required PSK input")
def KeySchedule<ROLE>(mode, shared_secret, info, psk, psk_id):
VerifyPSKInputs(mode, psk, psk_id)
psk_id_hash = LabeledExtract("", "psk_id_hash", psk_id)
info_hash = LabeledExtract("", "info_hash", info)
key_schedule_context = concat(mode, psk_id_hash, info_hash)
secret = LabeledExtract(shared_secret, "secret", psk)
key = LabeledExpand(secret, "key", key_schedule_context, Nk)
base_nonce = LabeledExpand(secret, "base_nonce",
key_schedule_context, Nn)
exporter_secret = LabeledExpand(secret, "exp",
key_schedule_context, Nh)
return Context<ROLE>(key, base_nonce, 0, exporter_secret)
3.1.1 HPKE_BASE Mode 加密公钥
HPKE 方案的最基本功能是KEM私钥拥有者能够对进行加密操作。调用SetupBaseS()
和 SetupBaseR()
流程可以针对性地建立用于加密和解密的上下文。
这里KEM 共享密钥产生需要 KDF函数结合,描述密钥交换的信息以及调用者提供的显式 info 参数来计算得出。。
流程参数pkR是接受方公钥,enc 是一个封装后的的 KEM 共享密钥。
def SetupBaseS(pkR, info):
shared_secret, enc = Encap(pkR)
return enc, KeyScheduleS(mode_base, shared_secret, info,
default_psk, default_psk_id)
def SetupBaseR(enc, skR, info):
shared_secret = Decap(enc, skR)
return KeyScheduleR(mode_base, shared_secret, info,
default_psk, default_psk_id)
3.1.2 使用PSK认证
这种变体通过允许接收方通过验证发送方拥有给定的预共享密钥(PSK)来认证发送方。在[第 9.1 节][0]中有更详细描述,PSK 是如何在某些对手模型中提供了更高的保密。这里假设双方都都清楚该使用哪个PSK和具体psk_id是什么。
这种模式和HPKE_BASE模式的主要区别在于,psk
和psk_id
值被用作 KDF 的ikm
输入(而不是使用空字符串)
def SetupPSKS(pkR, info, psk, psk_id):
shared_secret, enc = Encap(pkR)
return enc, KeyScheduleS(mode_psk, shared_secret, info, psk, psk_id)
def SetupPSKR(enc, skR, info, psk, psk_id):
shared_secret = Decap(enc, skR)
return KeyScheduleR(mode_psk, shared_secret, info, psk, psk_id)
3.1.3 使用非对称秘钥认证
这种变体通过允许接收方验证发送方拥有给定的 KEM 私钥。说白了就是因为 AuthDecap(enc, skR, pkS) 只有在封装KEM shared key的 enc 由 AuthEncap(pkR, skS) 生成时(其中 skS 是与 pkS 对应的私钥),才会产生正确的 KEM 共享密钥。换句话说,最多两个实体(在 DHKEM 的情况下恰好是两个)可以生成此密钥,因此如果接收方最多为一个,那么发送方极有可能是另一个。
与base mode的主要区别在于对 Encap() 和 Decap() 的调用被对 AuthEncap() 和 AuthDecap() 的调用所取代,它们将发送方的公钥添加到其内部上下文字符串中。函数参数 pkR 和 pkS 是公钥,而 enc 是封装的 KEM 共享密钥。¶
显然,这种变体只能与提供 AuthEncap() 和 AuthDecap() 过程的 KEM 一起使用。
此机制仅验证发送方的密钥对,而不是任何其他标识符。如果应用程序希望将 HPKE 密文或导出的秘密与发送方的其他标识(例如电子邮件地址或域名)绑定,则应将其他标识符包含在 info 参数中,以避免身份错误绑定问题
使用公钥认证的情况下构建加密环境。
def SetupAuthS(pkR, info, skS):
shared_secret, enc = AuthEncap(pkR, skS)
return enc, KeyScheduleS(mode_auth, shared_secret, info,
default_psk, default_psk_id)
def SetupAuthR(enc, skR, info, pkS):
shared_secret = AuthDecap(enc, skR, pkS)
return KeyScheduleR(mode_auth, shared_secret, info,
default_psk, default_psk_id)
3.1.5 使用PSK+非对称秘钥认证
使用公钥+PSK认证的情况下构建加密环境。这个我就不多赘述了。基本没变化
def SetupAuthPSKS(pkR, info, psk, psk_id, skS):
shared_secret, enc = AuthEncap(pkR, skS)
return enc, KeyScheduleS(mode_auth_psk, shared_secret, info,
psk, psk_id)
def SetupAuthPSKR(enc, skR, info, psk, psk_id, pkS):
shared_secret = AuthDecap(enc, skR, pkS)
return KeyScheduleR(mode_auth_psk, shared_secret, info,
psk, psk_id)
3.2 加密与解密
上面产生了具体的加密上下文,那么接下来就可以进行数据的加密和解密了。
HPKE 允许在一次会话中进行多次加密操作。由于设置中涉及的公钥操作通常比对称加密或解密更昂贵,这使得应用程序能够分摊公钥操作的成本,降低总体开销。
然而,为了避免随机数重用,这种加密必须是有状态的。上述每个设置过程都会生成一个特定于角色的上下文对象,该对象存储 AEAD 和秘密导出参数。AEAD 参数包括
秘钥导出参数为:
- HPKE具体的ciphersuite,可以和上面的suite_id对上
- exporter_secret导出秘钥
这些参数除了seq number都是常量,每次加解密操作使用nonce都是base_nonce和当前seq number异或的结果。这里要注意加解密是需要我们上面写的AAD的参与的。具体加解密参数也一并附加上了。
def ContextS.Seal(aad, pt):
ct = Seal(self.key, self.ComputeNonce(self.seq), aad, pt)
self.IncrementSeq()
return ct
def ContextR.Open(aad, ct):
pt = Open(self.key, self.ComputeNonce(self.seq), aad, ct)
if pt == OpenError:
raise OpenError
self.IncrementSeq()
return pt
def Context<ROLE>.ComputeNonce(seq):
seq_bytes = I2OSP(seq, Nn)
return xor(self.base_nonce, seq_bytes)
def Context<ROLE>.IncrementSeq():
if self.seq >= (1 << (8*Nn)) - 1:
raise MessageLimitReachedError
self.seq += 1
5 总结和FAQ
总结以下几点:
- HPKE的优缺点是什么?
- HPKE适用于哪些场景?
一些具体的问题:
- HPKE和ECDH有什么区别?严格的来说HPKE和ECDH的原理是类似的,都是基于DH交换体系的基础知识产生共享秘钥,无法是多加了一个AEAD。但是两个的场景不一致,HPKE用于混合公钥加密,可以理解为一种加密套件。而ECDH是密钥交换算法,用于握手。
- HPKE有哪些具体的应用场景呢?ECH混淆加密等等
6 Streaming AEAD
今天看到一个流AEAD的概念,原先讨论AEAD的时候第一反应都是块加密,AES-GCM,忽然提到流AEAD就没反应过来,看了下google的文档,说是提供:
- 底层加密模式的选择使部分明文可以通过解密和认证部分密文快速获得,而不需要处理整个密文。
- 加密必须在一个会话中完成。修改现有的密文或对其进行追加是不可能的。
tink的网页里面解释说是对OAE的具体实现,想了想没想明白咋回事,就直接去看了参考的文档Online Authenticated-Encryption and its Nonce-Reuse Misuse-Resistance论文,简单一些的理解就是使用已经算出来的数据和明文做异或作为IV参与到另外一块做运算,然后需要产生对应AAD一起作为输入参数进行运算。然后每次输出的结构都会拥有部分的tag,这个tag就和aes-gcm的tag是类似的,是这一部分数据认证的结果。当然,这个解释没说明白很多细节,比方说nonce如何产生,如何划分数据的segment?所以找了几篇论文和一些算法看下,如果前两篇没看懂,那么可以先看第三篇把里面的方案的都解释的差不多了再继续看:
- General Classification of the Authenticated Encryption Schemes for the CAESAR Competition
- Online Authenticated-Encryption and its Nonce-Reuse Misuse-Resistance
- 基于双管道结构的在线加密方案
简单来说OAE分为几个不同版本:
- OAE1:安全特性被多种质疑,mac在数据末尾。
- OAE2:对nonce,iv的要求更严格些,mac要随一小段一点一点产生。当然这篇文章里面给出了oae2的一些实现方式,比方说chain/block啥的。
下面我们看几种具体的streaming aead算法落地实践,一个是grain-128aead,另一个是tink提供的算法。
6.1 Grain-128AEAD
基础组件为:
- key长度为128bit,nonce为96bit
- 具体包含两个building block,一个是预输出生成器,使用线性反馈移位寄存器+非线性反馈移位寄存器+预输出函数构成。第二个building block由认证生成器包含一个移位寄存器和累加器。设计和Grain-128a很相似,但是并不完全一致。
- 线性反馈移位寄存器和非线性反馈移位寄存器的具体方程可以直接看论文,我就不贴了挺明白的
- 认证器的寄存器部件保存最近的64个奇数位
- key和nonce的生成,使用初始的key和nonce填充线性/非线性反馈移位寄存器
使用方法为:
- 使用第一个building block生成流密钥,密文的内容是流密钥和明文异或的结果
- 累加器需要累加最后的结果到末尾作为tag,但是这也太晚了,所以这个实际上是oae1
总结:嗯,挺复杂,还麻烦。
6.2 tink的具体实现
加密的包裹都是调用stream_segment_encrypter.h的头文件,stream_segment_encrypter会对每个segment做不同的参数派生,借此保证密文是不能调换的。其格式如同下面的注释
//
// | other | header | 1st ciphertext segment |
// | ...... 2nd ciphertext segment ..... |
// | ...... 3rd ciphertext segment ..... |
// | ...... ... ..... |
// | ...... last ciphertext segment |
//
// where the following holds:
// * each line above, except for the last one,
// contains get_ciphertext_segment_size() bytes
// * each segment, except for the 1st and the last one,
// encrypts get_plaintext_segment_size() bytes of plaintext
// * if the ciphertext stream encrypts at least one byte of plaintext,
// then the last segment encrypts at least one byte of plaintext
// * 'other' is get_ciphertext_offset() bytes long, and represents potential
// other bytes already written to the stream; the purpose of ciphertext
// offset is to allow alignment of ciphertext segments with segments
// of the underlying storage or transmission stream.
namespace crypto {
namespace tink {
namespace subtle {
class AesGcmHkdfStreaming : public NonceBasedStreamingAead {
public:
struct Params { //存储具体的参与运算的参数
util::SecretData ikm; //ikm初始密钥材料
HashType hkdf_hash; //派生密钥的hash方法
int derived_key_size; //派生密钥的大小
int ciphertext_segment_size;//这一个ciphertext的segment的大小
int ciphertext_offset;
};
static util::StatusOr<std::unique_ptr<AesGcmHkdfStreaming>> New(
Params params);
static constexpr crypto::tink::internal::FipsCompatibility kFipsStatus =
crypto::tink::internal::FipsCompatibility::kNotFips;
protected:
util::StatusOr<std::unique_ptr<StreamSegmentEncrypter>> NewSegmentEncrypter(
absl::string_view associated_data) const override; //具体加密
util::StatusOr<std::unique_ptr<StreamSegmentDecrypter>> NewSegmentDecrypter(
absl::string_view associated_data) const override; //具体解密
private:
explicit AesGcmHkdfStreaming(Params params)
: ikm_(std::move(params.ikm)),
hkdf_hash_(params.hkdf_hash),
derived_key_size_(params.derived_key_size),
ciphertext_segment_size_(params.ciphertext_segment_size),
ciphertext_offset_(params.ciphertext_offset) {}
const util::SecretData ikm_;
const HashType hkdf_hash_;
const int derived_key_size_;
const int ciphertext_segment_size_;
const int ciphertext_offset_;
};
} // namespace subtle
} // namespace tink
} // namespace crypto
#endif // TINK_SUBTLE_AES_GCM_HKDF_STREAMING_H_
具体的解密包括初始化流程如下,参考aes_gcm_hkdf_streaming.cc 文件,具体流程分为几步:
-
使用传入的参数初始化加密解密参数,使用已经生成的相同的原始密钥材料ikm_,随机生成salt,使用相同的associated_data产生hkdf派生密钥,产生固定的nonce prefix。产生aead加解密context,这个aead ctx要参与对每次数据的加密。
-
对一段明文进行加密,都使用一开始产生的SegmentEncryptor,每次对一段明文进行加密都递增一个conunter,将counter拼接nonce prefix作为初始化向量IV进行加密。使用该iv和hkdf派生密钥参与到明文数据加密运算当中。
/* 参与运算的第一步,从ikm计算出来派生的具体的结果*/
util::StatusOr<std::unique_ptr<StreamSegmentEncrypter>>
AesGcmHkdfStreaming::NewSegmentEncrypter( absl::string_view associated_data) const {
AesGcmHkdfStreamSegmentEncrypter::Params params;
params.salt = Random::GetRandomBytes(derived_key_size_); //随机生成salt
/* 使用随机生成的salt,ikm_,aad派生出来派生密钥,这个派生密钥参与运算*/
auto hkdf_result = Hkdf::ComputeHkdf(hkdf_hash_, ikm_, params.salt, associated_data, derived_key_size_);
if (!hkdf_result.ok()) return hkdf_result.status();
params.key = std::move(hkdf_result).ValueOrDie();
params.ciphertext_offset = ciphertext_offset_;
params.ciphertext_segment_size = ciphertext_segment_size_;
return AesGcmHkdfStreamSegmentEncrypter::New(std::move(params));
}
/* 最后面上面的函数 */
util::StatusOr<std::unique_ptr<StreamSegmentEncrypter>>
AesGcmHkdfStreamSegmentEncrypter::New(Params params) {
/* 验证参数的有效性 */
auto status = Validate(params);
if (!status.ok()) return status;
/* 根据密钥创建aead环境,这里的key是上面的派生密钥 */
auto ctx_or = CreateAeadCtx(params.key);
if (!ctx_or.ok()) return ctx_or.status();
auto ctx = std::move(ctx_or).ValueOrDie();
return {absl::WrapUnique(
new AesGcmHkdfStreamSegmentEncrypter(std::move(ctx), params))};
}
/* 当前segment的加密流程 nonce_prefix_是随机产生的*/
AesGcmHkdfStreamSegmentEncrypter::AesGcmHkdfStreamSegmentEncrypter(
bssl::UniquePtr<EVP_AEAD_CTX> ctx, const Params& params)
: ctx_(std::move(ctx)),
nonce_prefix_(Random::GetRandomBytes(kNoncePrefixSizeInBytes)),
header_(CreateHeader(params.salt, nonce_prefix_)),
ciphertext_segment_size_(params.ciphertext_segment_size),
ciphertext_offset_(params.ciphertext_offset) {}
/* 具体的解密流程,每个segment实际上就是要加密的一段片段 */
util::Status AesGcmHkdfStreamSegmentEncrypter::EncryptSegment(
const std::vector<uint8_t>& plaintext,
bool is_last_segment,
std::vector<uint8_t>* ciphertext_buffer) {
if (plaintext.size() > get_plaintext_segment_size()) {
return util::Status(util::error::INVALID_ARGUMENT, "plaintext too long");
}
if (ciphertext_buffer == nullptr) {
return util::Status(util::error::INVALID_ARGUMENT, "ciphertext_buffer must be non-null");
}
if (get_segment_number() > std::numeric_limits<uint32_t>::max() ||
(get_segment_number() == std::numeric_limits<uint32_t>::max() &&
!is_last_segment)) {
return util::Status(util::error::INVALID_ARGUMENT, "too many segments");
}
/* 进行加密会多出来一个kTagSizeInBytes的长度*/
int ct_size = plaintext.size() + kTagSizeInBytes;
ciphertext_buffer->resize(ct_size);
// Construct IV. IV的构建需要是初始的nonce_prefix_的数据+当前segment的数据
std::vector<uint8_t> iv(kNonceSizeInBytes);
memcpy(iv.data(), nonce_prefix_.data(), kNoncePrefixSizeInBytes);
BigEndianStore32(iv.data() + kNoncePrefixSizeInBytes, static_cast<uint32_t>(get_segment_number()));
iv.back() = is_last_segment ? 1 : 0;
/* 执行加密,使用IV,已经初始化好了的aead ctx进行加密运算*/
size_t out_len;
if (!EVP_AEAD_CTX_seal(
ctx_.get(), ciphertext_buffer->data(), &out_len,
ciphertext_buffer->size(),
iv.data(), iv.size(),
plaintext.data(), plaintext.size(),
/* ad = */ nullptr, /* ad.length() = */ 0)) {
return util::Status(util::error::INTERNAL,
absl::StrCat("Encryption failed: ",
SubtleUtilBoringSSL::GetErrors()));
}
/* 将Segment号加一,下次需要使用这个进行运算*/
IncSegmentNumber();
return util::OkStatus();
}
结尾
唉,尴尬