每次您需要使用不同的 IV加密用相同的钥匙。解密在这里无关紧要,它只是使用它给出的任何 IV,而不是“消耗”IV。 “消耗” IV 值的是加密。
GCM 只要求 IV 是唯一的(对于给定的键)。因此,每次加密消息时从 0 开始并递增 1 就可以了。
如果密钥仅在由单个线程管理的单个会话中使用,则递增很容易。如果您的程序是多线程的,并且多个线程可能使用相同的密钥进行加密,则您需要确保不存在不同线程可能在同一时间使用相同 IV 的竞争条件。一种方法是锁定 IV 读取和增量。另一个是使用线程 ID + 每线程计数器作为 IV(但请注意,它必须适合 GCM 的 IV 大小,即 12 字节)。如果在程序的多次调用中使用相同的密钥,则变得更加困难,因为您需要确保可靠地存储 IV(即使程序或整个机器在使用 IV 值后立即崩溃)——在这种情况下,您应该通常避免使用相同的密钥。
我认为 OpenSSL 没有增加 12 字节计数器的功能(但也许它有,但我不知道)。您可以轻松地制作自己的:
uint64_t counter = 0;
encrypt() {
unsigned char iv[12] = {0};
++counter;
memcpy(iv, counter, sizeof counter);
}
这会增加一个 64 位计数器,这在实践中应该足够了。计数器的表示取决于平台(取决于字节序),但只要您将 IV 作为每个密文的一部分发送,这不是问题。如果您使用的是避免发送显式 IV 的网络协议,它将定义递增 IV 的精确方式。
另一种方法是使用随机 IV。 (当然使用 OpenSSL 的随机数,而不是一些非加密随机数。)只要消息数量很少,使用 12 个随机字节作为 IV 就可以了。您需要远低于birthday bound,大约为 2^48(可能的 IV 数量的平方根)。当您接近生日界限时,重复的概率变得不可忽略。请注意可能的攻击,其中对手以某种方式说服您的应用程序生成大量消息(例如,通过伪造或触发“未收到消息,请重新发送”错误)。
GCM 在内部使用 12 字节的 IV。有一个定义明确的接口可以获取任意长度的 IV 输入并将其转换为内部 12 字节 IV,但最好避免这种情况,因为转换为 12 字节时引入冲突的可能性很小。使用 12 字节随机 IV 的几率比使用更长随机 IV 的几率更高。
最后说明:如果可以,请使用AES-SIV 或AES-GCM-SIV,而不是 GCM。 SIV 使内部 IV 依赖于消息,因此重用与 IV 输入相同的值不会导致灾难性故障:对于 AES-SIV 或 AES-GCM-SIV,每次使用不同 IV 的唯一原因是否则可以看到同一条消息何时被多次加密)。 SIV 的缺点是您需要在开始加密之前获得整个消息,即您不能进行流式加密。它也较新,因此受到的支持较少。 OpenSSL 从 3.0.0 版开始支持 AES-SIV,但似乎还不支持 AES-GCM-SIV。 AES-GCM-SIV 在具有 GHASH(GCM 身份验证)计算的硬件加速的现代 PC 和智能手机上的性能稍好一些,但除此之外,我不知道有任何理由比 AES-SIV 更喜欢它。