第二章 对称加密和解密

在本章中,我们将学习对称加密的重要概念——密码、加密模式和填充。将概述现代密码、加密模式和填充类型,并就在哪种情况下使用哪种技术提出建议。这些技术的使用将通过代码示例进行说明。这是第一章,将包含代码示例;因此,如果我们想用旧版本的OpenSSL运行我们的代码,我们还需要学习如何初始化旧版的OpenSSL库。

本章将介绍以下主题:

第二章 对称加密和解密技术要求理解对称加密OpenSSL支持的对称密码概述分组密码和流密码的比较理解对称密码安全多少安全位是足够的?AES密码速览DES和3DES密码速览RC4密码速览ChaCha20密码速览查看OpenSSL支持的其他对称密码国家密码RC密码家族其他密码分组密码操作模式ECB模式速览CBC模式速览CTR模式速览GCM速览AES-GCM-SIV速览其他分组密码操作模式选择分组密码操作模式分组密码的填充如何生成对称加密密钥下载并安装OpenSSL如何在命令行上使用AES加密和解密正在初始化和取消初始化OpenSSL库如何编译和链接OpenSSL如何以编程方式使用AES加密实施加密程序运行加密程序如何以编程方式使用AES解密实现解密程序运行解密程序总结

技术要求

本章将包含可以在命令行上运行的命令以及可以构建和运行的C源代码。对于命令行命令,您将需要带有openssl动态库的 openssl 命令行工具。为了构建C代码,您需要OpenSSL动态或静态库、库头、C编译器和链接器。如果您没有安装OpenSSL组件,本章将教您如何安装它们。

我们将在本章中实施一些示例程序,以练习我们正在学习的内容。这些程序的完整源代码可以在这里找到:

https://github.com/PacktPublishing/Demystifying-Cryptography-with-OpenSSL-3/tree/main/Chapter02

理解对称加密

对称加密symmetric encryption)用于使用加密密钥encryption key)对数据进行加密。请注意,加密密钥与密码password)不同,但加密密钥可以从密码中导出。如何做到这一点将在【第5章“从密码推导加密密钥”】中解释。

对称加密被称为对称加密,因为加密和解密操作都使用相同的密钥。还有非对称加密asymmetric encryption),其中使用不同的密钥(公钥和私钥)进行加密和解密。非对称加密将在【第6章“非对称加密和解密”】中介绍。

为了对称加密某物,您必须使用对称加密算法symmetric encryption algorithm)。这种算法也称为密码cipher)。值得注意的是,术语密码cipher)具有广泛的含义,可以指任何加密或加密相关算法,包括对称和非对称加密算法、消息摘要,甚至是在TLS连接期间使用的一组算法,例如 ECDHE-ECDSA-AES256-GCM-SHA384 。在本书中,我们将根据当前上下文使用术语 cipher 来表示不同的含义。例如,现在,上下文是对称加密——因此,我们使用 cipher 来表示对称加密算法。

被加密的有用数据称为明文plaintext),即使它不是文本。图像、音乐、视频、档案或数据库,任何你想加密的东西都是加密术语中的明文。

加密过程的结果,即加密数据,称为密文ciphertext)。密文几乎从不看起来像文本,但这个术语仍然在行业中使用。

使用给定的术语,我们可以说加密将明文转换为密文,解密将密文转换回明文。

为了加密一些明文,您必须执行以下操作:

  1. 选择加密算法(cipher,密码)。
  2. 生成加密密钥。
  3. 生成初始化向量initialization vectorIV)。
  4. 选择密码操作模式cipher operation mode)。
  5. 选择填充类型padding type)。
  6. 使用所选的密码操作模式,使用密码、密钥和填充对明文进行加密。

根据密码及其操作模式,其中一些步骤可能不需要。例如,流密码和一些分组密码操作模式不需要填充。

对密码学经验不足的人可能会问,“为什么我必须选择这么多参数?当我使用加密档案时,我的档案管理员(archiver)只要求我输入密码(password),而不是密码(cipher)、密钥、IV、模式或填充类型!”答案是,档案管理员或其他用户友好的工具在幕后处理所有这些事情,代表用户做出大部分选择,并根据用户提供的密码生成加密密钥。

尽管如此,我们将学习一些关于提到的基本加密概念,以便做出有意识的选择,并了解这些概念是如何协同工作的。我们将从学习加密算法(或密码,ciphers)开始。

OpenSSL支持的对称密码概述

在本节中,我们将回顾OpenSSL支持的对称加密算法,但首先,我们需要介绍一些概念,这些概念将有助于我们理解密码之间的差异、它们的属性以及它们的优缺点。对称密码分为两类:分组密码(block ciphers)和流密码(stream ciphers)。

分组密码和流密码的比较

分组密码block ciphers)对数据块进行操作。例如,流行的高级加密标准Advanced Encryption StandarAES)密码的分组大小(block size)为128位,这意味着该密码以128位块对数据进行加密或解密。如果数据量大于块大小,则将数据拆分为处理所需大小的块。如果明文plaintext)长度不是块大小的倍数,则通常会根据所选的填充类型padding type)将最后一个块填充到块大小。因此,在大多数分组密码操作模式block cipher operation modes)下,密文ciphertext)长度总是块大小的倍数。

简单解释一下,在分组密码加密过程中会发生什么?加密的输入是明文块(例如,16个字节)、加密密钥,以及可能的一些特定于密码操作模式的数据(例如,之前的密文块或IV)。明文的各个字节被放入列表或矩阵中。然后,对明文字节、加密密钥字节和附加操作模式特定的数据字节执行不同的逻辑和算术运算:比特旋转bit rotations)、异或eXclusive ORXOR)、加法(addition)或减法(subtraction),或交换列表(exchange of full)或矩阵(matrix)的全部或部分字节。这些运算的输出是一个与输入明文块大小相同的密文块。对于解密,输入是密文块、加密密钥和特定于操作模式的数据。加密阶段的操作以相反的顺序执行,因此,密文块被转换回明文块:

2.1-Howblockcipherswork

流密码stream ciphers)不对块进行操作,而是对单个字节甚至数据位进行操作。这意味着流密码不需要填充。它们也没有操作模式,尽管一些研究人员也在尝试引入流密码的操作模式。流密码的这些特性使其更容易使用,特别是对于流数据,比块密码更容易实现,执行速度更快。然而,密码学家认为,现有的流密码通常不如块密码安全,并且与块密码相比,通过实现错误更容易进一步削弱流密码的安全性。

流密码是如何工作的,简单解释一下?在加密和解密过程中,流密码根据加密密钥和IV随机数noncenumber used once使用一次的数字)生成所谓的伪随机密码数字流pseudorandom cipher digit stream)或密钥流keystream)。密码获取加密密钥和IV的字节,并对它们执行逻辑和数学运算,以生成看似随机的无限字节流。这就是我们所指的密钥流。我们可以说,流密码在生成密钥流时,就像伪随机数生成器pseudorandom number generatorPRNG)一样,它由加密密钥和IV播种seeded)。在加密时,密文流是通过将密钥流与明文流组合而产生的。通常,密钥流字节和明文字节只是XOR。在解密时,会生成相同的密钥流并将其与密文流组合。如果密文流ciphertext stream)是通过将密钥流keystream)与明文流plaintext stream)进行XOR运算而产生的,那么明文流plaintext stream)将通过使用相同的XOR运算来恢复,但这次是在密钥流keystream)和密文流ciphertext stream)上。为了产生相同的解密密钥流,必须使用相同的加密密钥和IV:

2.2-Howstreamcipherswork

理解对称密码安全

当密码学家谈论密码的安全性时,他们通常会以安全比特security bits)来衡量密码安全性cipher security)或密码强度cipher strength)。例如,如果攻击者需要执行2256次计算操作来破解密码,则密码安全性为256位。

密码可以具有的最大安全比特数是所使用的加密密钥的长度。例如,如果密码使用256位加密密钥,则其强度不超过256位。这很容易证明。假设攻击者有一些密文,没有加密密钥和相应的明文,但可以区分有效明文和随机数据。攻击者的目标是恢复加密密钥或明文。如果密钥长度key length)为256位,则有2256个可能的加密密钥可用于当前密码。攻击者可以使用暴力破解brute force),也称为穷举搜索exhaustive search)方法,即尝试所有可能的密钥来解密密文。然后,他们必须尝试不超过2256次,也就是说,最多执行2256次计算操作。

这里,计算操作并不意味着非常基本的操作,如中央处理单元(CPU)指令,而是对少量密文(如一个密文块)进行解密的完整尝试。这里重要的是,计算操作的量取决于密钥长度。在现代CPU上,解密一个密文块非常快速且便宜,但执行2256次这样的操作需要数十亿年的时间。加密安全性基于攻击者执行不可行数量的操作来解密密文或恢复密钥的必要性。

重要的是要明白,计算复杂度会根据密钥长度呈指数级增长——这意味着,破解256位密钥的计算成本并不比破解128位密钥高两倍。它的计算成本是2128倍。在正常操作期间,较长的密钥比较短的密钥需要更多的计算能力,但在对称加密的情况下,这种差异并不显著。因此,使用较长的密钥是有意义的。

大多数密码的目标是在密钥允许的范围内尽可能强。在实践中,安全研究人员经常找到比暴力更聪明的方法来攻击加密。这样,密码的安全性可能会减弱,例如,从256位降低到224位。如果在应用了最聪明的攻击后,密码仍然保留了足够的安全位,则认为密码是安全的。

多少安全位是足够的?

截至2021年,安全研究人员的共识和美国国家标准与技术研究所National Institute of Standards and TechnologyNIST)的建议如下:

预计未来几年将有一项这样的技术突破。这就是量子计算quantum computing)。量子计算机已经存在,但它们非常昂贵,而且仍然不足以用于密码破解。量子计算有望将对称密码算法的安全位数减半(halve)。这意味着以前256位强的密码将变得只有128位强,而以前128位强的将把它们的密码强度降低到只有64位。

正如我们之前提到的,安全研究人员一直在寻找更智能的破解密码的方法。因此,任何密码的有效安全性通常都比其密钥长度低一点

流行的对称密码支持长达256位的密钥长度。

鉴于上述考虑,我建议尽可能使用256位对称加密密钥和强对称密码。即使考虑到正在进行的安全研究和量子计算的持续发展,在可预见的未来,这一数量的安全比特也应该足够了。

AES密码速览

AES是最流行的现代对称密码。如果您不确定使用哪种对称密码,请使用此密码。这个密码既快又安全。AES是一种块大小为128位的分组密码。AES密码有三种变体,使用不同长度的密钥:128、192和256位。这三种变体分别被称为AES-128、AES-192和AES-256。

有很多关于AES安全性的研究,以找到比暴力破解密码更聪明的方法。AES坚决反对这些企图。目前最好的攻击只将AES安全强度降低了2位:AES-128、AES-192和AES-256的安全强度分别降低到126、190和254位。

值得一提的是,现代x86和x86_64 CPU具有AES的硬件加速,称为高级加密标准新指令Advanced Encryption Standard New InstructionsAES-NI),使已经很快的AES密码更快。一些ARMv8 CPU支持可选的ARMv8加密扩展ARMv8 Cryptographic Extension),该扩展还可以加速AES。

AES使用一种名为Rijndael的加密算法。Rijndael支持比标准化为AES时更多的块大小和密钥长度。因此,我们可以说AES是Rijndael的一个子集。许多人交替使用 AESRijndael 这两个术语作为同义词。Rijndael算法是由两位比利时密码学家 Vincent RijmenJoan Daemen 开发的。Rijndael这个名字是基于作者 RijDae 的部分姓氏。

为什么AES的名字中有 Standard(标准)?因为AES在2001年被NIST标准化为敏感(sensitive)商业和(美国)政府信息的标准加密算法。值得一提的是,AES并不是用于美国政府敏感信息的唯一密码,但AES是公众可以访问的世界上最受欢迎的对称密码。

DES和3DES密码速览

数据加密标准(Data Encryption StandardDES)是之前的加密标准,是AES的前身。它是一个块大小为64位、密钥长度为56位的分组密码。块大小和密钥长度都太短,密码不安全。密码学研究人员进一步将DES安全性降低到39到41个安全位。在实践中,DES密钥可以在几天甚至几小时内在现代消费级PC上恢复。

3DES 或称 Triple DES 是一种基于常见DES的密码。基本上,3DES是DES应用三次,使用两个或三个不同的密钥。对于三密钥变体,3DES安全性被认为是112位,而对于双密钥变体,只有80位。

不建议对新加密使用DES或3DES密码。然而,DES已经使用了很多年,一些组织可能仍然有DES加密的旧数据。一些传统系统可能也实现了3DES,但没有实现AES以实现网络安全,因此可能需要3DES来实现互操作性。如果遗留系统需要使用旧的不安全密码,强烈建议将这种使用限制在内部网络,并尽快迁移到更现代的密码。

3DES比通常的DES慢三倍。与AES和其他竞争对手相比,DES本身是一种相当慢的密码。在现代工作站上,DES或3DES的缓慢不应该很明显,但如果服务器使用慢速密码为许多客户端提供服务,这种缓慢可能会很明显。这是迁移到更快、更安全的密码的另一个原因。

RC4密码速览

RC4是一种旧的流密码,可以使用长度为40到2048位的密钥。由于其速度和实现的简单性,该密码在过去非常受欢迎,但多年来,该算法中发现了许多流(flows),现在被认为是不安全的。例如,其中一种攻击将具有128位密钥的RC4的安全性降低到只有26位。

关于RC4的一个有趣的事实是,在2013年的一段时间里,RC4被用作主要的SSL/TLS密码,因为SSL 3.0和TLS 1.0中使用的所有块密码都是臭名昭著的 浏览器反SSL/TLS攻击Browser Exploit Against SSL/TLSBEAST)的目标,但RC4作为流密码,不受其影响。

RC4的问题与DES的问题相同。RC4在过去很长一段时间内都很流行,一些组织可能仍然有RC4加密的旧数据。一些组织也可能使用不支持现代加密算法的遗留系统。对这些组织的建议与DES相同:迁移到现代系统和密码,同时将受RC4影响的数据或网络流限制在组织的内部网络中。

在撰写本文时,流行的现代网络浏览器仍然支持3DES与旧服务器的互操作性,但不再支持单个DES或RC4。

ChaCha20密码速览

ChaCha20是一种现代流密码。它既安全又快速。ChaCha20可以与128位或256位密钥一起使用。密码的主要变体使用256位密钥,OpenSSL仅支持主要变体。在撰写本文时,没有已知的攻击可以降低整个20轮ChaCha20密码的安全性,因此,其安全性与密钥长度相同,为256位。

ChaCha20密码是由著名密码学和安全研究人员Daniel J.BernsteinDJB)开发的。DJB还以他的其他项目而闻名,如 qmaildjbdnsPoly1305Curve25519。ChaCha20是同一作者对 Salsa20 密码的性能优化修改。当谷歌开始在他们的Chrome浏览器中使用ChaCha20,以及同一作者的Poly1305 消息认证码message authentication codeMAC)算法时,ChaCha20变得流行起来。随后,OpenSSH也新增了对ChaCha20和Poly1305的支持。ChaCha20成为流行的流密码,有效地成为了RC4的继任者。

如前所述,ChaCha20是一个快速密码。ChaCha20在软件实现方面比AES更快。x86或x86_64和ARMv8上的硬件AES加速使AES比ChaCha20更快,但并非所有CPU都包含此加速。例如,用于移动和其他设备的低端ARM CPU没有这样的优化,也没有台式计算机中x86_64 CPU的处理能力。因此,ChaCha20在低端设备上可能比AES更受欢迎,特别是对于加密网络数据流。

ChaCha20的一个小缺点是它使用了一个只有32位的块计数器block counter)。因此,该密码仅在不超过256GiB数据的连续明文上被认为是安全的。它应该足以用于网络数据流,但可能不足以加密大型文件。一种解决方法是将非常长的明文分割成最多256GiB的较短块,并在加密每个块之前重置块计数器和随机数noncenumber used once)。

查看OpenSSL支持的其他对称密码

OpenSSL支持一些其他对称密码,这些密码不如上述四种密码流行。

其中一些,如BlowfishCAST5GOST89IDEARC2RC5,是块大小为64位或块大小可变的块密码,其OpenSSL实现使用64位的块大小。由于碰撞攻击collision attacks)和生日悖论birthday paradox),不建议将这些密码用于非常长的密文(超过32GiB)。这个问题的解决方案是将长明文分割成小于32 GiB的块,并为每个块使用不同的加密密钥和IV。使用64位块的算法非常古老。其中许多是在20世纪90年代开发的,有些甚至是在70年代开发的。

其他算法,如ARIACamelliaSEEDSM4,是128位块大小的块密码。使用128位块大小,您可以加密非常长的明文,而不必担心冲突攻击,最高可达256 EB。

一些不太流行的算法是好的,但与AES相比,有些算法在技术上存在缺点。许多旧算法比AES慢得多,慢10倍,但OpenSSL支持的大多数不太常见的密码仍然是安全的。我们不会详细回顾每一个不太常见的密码,但以下内容值得一提。

不太流行的密码可以分为以下几类。

国家密码

国家密码是在特定国家开发和标准化的。在开发密码的国家的加密社区中,人们通常对国家密码感到非常自豪。通常,这种密码很好,但在原产国以外并不很受欢迎。

RC密码家族

RC家族是由著名密码学家 Ron Rivest 开发的密码集合。RC代表罗恩密码(Ron's Code)或Rivest密码(Rivest Cipher)。RC2是RC4的前身,是1987年开发的旧密码。RC2容易受到相关密钥攻击。独立密码分析师对这种密码的研究并不多,但密码学界对RC2的安全性持怀疑态度。RC2的另一个缺点是,作为可变密钥长度密码,RC2支持短的不安全密钥。

RC5于1994年开发。该密码以其实现的简单性而闻名。它是一种具有可变密钥长度(0到2040位)和可变块大小(32、64或128位)的密码。OpenSSL仅支持RC5的64位块和高达2040位的密钥。OpenSSL中的默认密钥长度为128位,这也是密码作者推荐的长度。目前还没有针对RC5的实际攻击,但与RC2一样,RC5支持短的不安全密钥,这是一个缺点。

其他密码

国际数据加密算法International Data Encryption AlgorithmIDEA)于1991年开发,旨在替代DES。IDEA被称为PGP版本2中的默认密码,这是PGP广泛使用的时候。IDEA在创建时已获得专利,但最后一项专利于2012年到期。由于这些专利,GnuPG无法在其核心代码中使用IDEA。出于同样的原因,PGP的作者决定在PGP版本3中将默认密码更改为CAST5。密码使用128位密钥。最著名的攻击将其安全性降低到126位。

Blowfish密码可能是由世界上最著名的密码学研究人员Bruce Schneier开发的。Schneier在1993年开发了Blowfish作为一种免费使用的对称密码,当时大多数其他密码都是专有和专利的。Blowfish是一种可变长度密码,支持32到448位的密钥长度。到目前为止,还没有针对16轮Blowfish的实际攻击。但是Blowfish是一种可变密钥长度密码,支持短的不安全密钥。因此,无法预先确定Blowfish加密数据是否安全加密。解密软件必须检查密钥长度,决定加密数据的安全级别,并通知用户解密过程是否是交互式的,但并非所有软件都这样做。这是Blowfish的第二个弱点,除了它的小64位块大小。Blowfish的作者开发了Blowfish的继任者Twofish,它使用128位块大小和最小128、192或256位的密钥长度。他建议Blowfish的用户迁移到Twofish。

分组密码操作模式

分组密码可以在不同的加密模式encryption modes)下运行,也称为操作模式modes of operation)。众所周知,分组密码逐块加密明文数据。加密模式指定如何将密文块链接在一起。我们现在将回顾最流行的操作模式。

ECB模式速览

最简单的操作模式是电子密码本Electronic Code BookECB)。在这种模式下,每个明文块仅使用加密密钥加密为密文块,而不使用 IV 或之前的明文或密文块。然后,将产生的密文块连接起来。

ECB模式如下图所示:

HowECBmodeworks

在ECB模式下,相同的明文总是产生相同的密文。这是一个安全问题,因为明文中的模式保留在密文中,并且对观察者可见。

让我们考虑以下图像及其加密表示:

2.4

如图所示,即使第二张图像被加密,我们也可以看到绘制的内容,并且ECB模式不能提供有效的加密保护。因此,我们应该使用更智能的操作模式,例如密码块链Cipher Block ChainingCBC)模式。

CBC模式速览

在CBC模式下,当前块的密文取决于前一个块的密文。这是怎么回事?与ECB模式不同,在ECB模式下,每个明文块都按原样加密,而在CBC模式下,当前明文块与前一个密文块进行XOR运算,然后对XOR运算的结果进行加密,产生当前密文。

对于不存在先前密文的第一个明文块,会发生什么?第一个明文块与IV进行XOR运算。IV是用于初始化密码的数据块。在许多模式下,包括CBC,IV通常是随机生成的,大小与密码块大小相同。IV通常不保密,与密文一起存储,因为解密需要它。然而,有时,在可能且有意义的情况下,例如在TLS协议1.1及以上版本中,IV以加密形式传输。

CBC操作模式如下图所示:

2.5

以下是CBC模式下同一企鹅图像的加密:

2.6

如图所示,原始图像中的图案在加密过程中没有被保留。观察加密图像,不可能说出原始图像中绘制了什么。正如我们所看到的,CBC模式提供了良好的加密保护。它对观察者隐藏了确切的原始数据,也隐藏了原始数据各部分之间的关系。其他非ECB操作模式会产生类似的看似随机的图像。

CBC模式中使用的IV对于给定的消息必须是不可预测的,以抵抗所选的明文攻击chosen-plaintext attack)。满足这一要求的一个简单方法是随机生成IV。

在几乎所有加密模式下,包括CBC,IV只能用于用给定加密密钥加密的一条消息,不得用于另一条消息。因此,我们可以说IV通常是随机数(nonce)。对于某些操作模式,据说IV是从随机数导出的。

CBC模式发明于1976年,多年来一直是文件和网络流量加密最流行的操作模式。如今,GCM(稍后描述)越来越受欢迎,特别是在网络方面。CBC模式对于文件加密仍然非常好。GCM基于计数器CounterCTR)模式,我们接下来将对此进行回顾。

CTR模式速览

在CTR模式下,明文本身不会像AES那样被加密算法处理。相反,计数器块序列由块密码加密。例如,每个计数器块可能由一个级联的64位随机数和一个实际的64位整数计数器组成,每个下一个块递增。重要的是,第一个计数器块不会被重用来加密具有相同加密密钥的另一个明文,并且所有计数器块都是不同的。

递增计数器的最简单和最流行的方法是将其加1,但这种方法不是强制性的。其他操作,如加或减大于1,或更复杂的操作,如XOR和移位,也是可以接受的。重要的是,反序列不会开始重复。

好的,在CTR模式下,我们加密的是一些计数器序列而不是明文,但这对我们有什么帮助呢?我们如何保护明文?我们将加密的 计数器counter)块与 明文plaintext)块进行XOR运算。XOR运算的结果就是我们的密文。

CTR操作模式如下图所示:

2.7

我们可以说CTR模式将分组密码转换为流密码。事实上,加密计数器块的序列就是我们的密钥流!我们将密钥流与明文进行XOR运算,就像使用流密码时一样!

我们可以用与IV类似的方式来考虑第一个计数器块。与CBC模式中的IV类似,第一个计数块(或至少其随机部分)通常与密文一起保存,因为解密需要它。计数器块增量方法通常也不是秘密。对于OpenSSL,甚至可以在OpenSSL代码中查找它。因此,所有计数器块对于可能的观察者或攻击者来说都是已知的,但如果加密的计数器块不拥有加密密钥,则观察者就不知道它们。如果观察者能够获得加密的计数器块(密钥流),他们将能够通过将密钥流与密文进行异或来恢复明文。

CTR模式下使用的IV不得与另一条消息的相同加密密钥重复使用。如果重用它,将生成相同的密钥流来加密多条消息。使用相同密钥流加密的两个密文可以进行异或运算,结果将与对相应明文进行异或运算相同。明文的随机性通常比密钥流小得多,因此,明文恢复攻击比密文攻击容易得多。此外,如果选择的明文攻击是可能的,那么密钥流可以通过将明文和密文进行异或来恢复,然后恢复的密钥流可以用于解密由相同密钥流加密或由相同密钥和IV产生的任何消息。虽然IV唯一性是CTR模式中非常重要的要求,但与CBC模式不同,IV不可预测性在这种模式中不是要求。这是因为CTR对所选明文攻击的抵抗力是基于下一个密钥流块的不可预测性,而不是IV的不可预见性。一些密码学家批评CTR模式,因为块密码被馈送了非秘密数据以产生密钥流。然而,如今,CTR模式被 Niels FergusonBruce Schneier 等密码学家广泛接受和尊重,他们认为它是安全的。值得一提的是,CTR模式是由另外两位受人尊敬的密码学家 Whitfield DiffieMartin Hellman 发明的,他们以发明TLS中使用的Diffie-Hellman(DH)密钥交换key exchange)方法而闻名。

由于CTR模式将分组密码转换为流密码,因此它具有与流密码相同的优点。在CTR模式下,不需要明文填充,因此避免了填充预言攻击。密钥流也可以预先计算,不一定是在加密时。最后,加密和解密操作只需将密钥流与明文XOR,因此,可以使用预先计算的密钥流并行加密或解密不同的明文块。

CTR模式是更高级的伽罗瓦/计数器模式Galois/Counter modeGCM)的基础。

GCM速览

在GCM中,密文的生成方式与CTR模式相同。计数器块用块密码加密,产生密钥流。然后,密钥流与明文进行异或,生成密文。

与CTR模式的不同之处在于,GCM不仅对明文进行加密,还 对密文进行身份验证authenticates the ciphertext)。这意味着,加密密钥的知识使我们能够验证密文的完整性integrity),或者换句话说,如果验证失败,可以检测到对密文的未经授权的更改。这种检测很有用,因为许多攻击是通过更改密文、将更改后的密文提供给预言机oracle)并分析预言机反馈来实现的。

密码学中的 预言机oracle)是一种程序、设备或网络节点,它拥有加密密钥,能够加密或解密数据。预言机提供一些反馈,有时以(正确或错误)加密或解密数据的形式,有时以错误消息的形式,或者有时预言机暴露其他信息,例如解密所需的时间。这可能会在成功解密和不成功解密之间有所不同。预言机反馈允许攻击者或密码分析员获取可能有助于破解加密的数据。

身份验证加密Authenticated encryptionAE)模式,如GCM,可以检测到对加密数据的未经授权的更改,并拒绝使用损坏的数据。

一些经过身份验证的加密模式允许我们组合加密和未加密的数据进行身份验证。这是存储或传输非机密数据所必需的,但这些数据与加密的机密数据有关,需要保护以防止未经授权的更改。这些加密模式称为关联数据认证加密authenticated encryption with associated dataAEAD)。GCM是一种AEAD模式——添加的额外数据只会经过身份验证,而不会加密。

GCM如何对密文进行身份验证?每个密文块都被转换为一个数字,并与取决于加密密钥的初始块一起用于伽罗瓦域上的有限域算术。因此,这种操作模式被称为伽罗瓦/计数器模式。认证操作产生一个认证标签,解密方可以使用该标签来验证密文的完整性。

GCM中的加密输出包含IV、密文和身份验证标签。

GCM操作可以用下图说明:

2.8

Figure 2.8 – How GCM works

GCM有点特别。它只适用于128位块大小,需要96位IV。因此,它不可能与旧的64位块密码一起使用。OpenSSL只能将GCM与AES密码一起使用。

GCM和其他AE模式对数据进行认证。如果解密方想要验证密文的完整性,则必须处理整个密文以验证身份验证标签。通常,解密和验证是同时进行的。因此,如果验证失败,则应丢弃解密的数据。

AES-GCM中对IV的要求与CTR模式中的要求相同:IV必须是唯一的,这意味着同一IV不得与另一条消息的相同加密密钥重复使用。如果重复使用IV,则应用于CTR模式的相同攻击也可以应用于AES-GCM。此外,攻击者可以恢复用于验证消息的密钥(而不是加密密钥)。使用身份验证密钥,攻击者可以生成虚假的身份验证标签,并将不需要的、看似经过身份验证的数据注入密文流中。

GCM是目前TLS协议中使用的最流行的块加密模式。GCM自TLS 1.2以来一直受到支持。较早版本的TLS协议使用CBC模式。

即使使用非AE加密模式,TLS协议也始终验证加密数据的完整性。TLS协议使用MAC进行验证。在互联网上,过往的流量可能会发生很多不好的事情。有人可以尝试对您的数据进行中间人Man in the MiddleMITM)攻击。其他人可以尝试将您的服务器用作预言机。因此,数据认证在网络上比在本地文件系统上更重要。

正如我们提到的,如果密钥IV对被重新用于另一条消息,GCM很容易受到各种攻击。作为对这一弱点的回应,已经发明了具有合成初始化向量模式的伽罗瓦/计数器模式的高级加密标准Advanced Encryption Standard in Galois/Counter Mode with a Synthetic Initialization VectorAES-GCM-SIV)。

AES-GCM-SIV速览

AES-GCM-SIV是GCM中AES密码的变体。AES-GCM-SIV的独特特征和优势在于,该算法提供了对IV或随机数误用的抵抗力,这意味着该算法确保了相同的IV不会被重复用于不同的消息。

AES-GCM-SIV不像大多数加密模式那样将IV作为输入数据的一部分。相反,该算法采用第二个密钥进行身份验证。AES-GCM-SIV基于明文、额外的已认证数据和认证密钥生成认证标签。生成的认证标签也用作明文加密的IV。生成的IV称为合成初始化向量Synthetic Initialization VectorSIV)。由于IV依赖于明文,因此对于不同的明文,它会有所不同。

AES-GCM-SIV的一个缺点是,该算法需要对明文进行两次传递:一次用于生成身份验证标签和IV,另一次用于加密。

其他分组密码操作模式

分组密码还有许多其他操作模式,如传播CBCpropagating CBCPCBC)、密码反馈cipher feedbackCFB)模式、输出反馈output feedbackOFB)模式、CBC-MAC计数器Counter with CBC-MACCCM)模式、Carter Wegman+CTRCWC)和Sophie Germain计数器模式Sophie Germain Counter ModeSGCM)等。这些模式不太受欢迎;因此,这里不会对它们进行审查。

选择分组密码操作模式

OpenSSL支持的最佳分组密码操作模式被认为是GCM。GCM除了加密外还支持身份验证,不需要填充,因此不受填充oracle攻击的影响。GCM还允许预先计算密钥流,并并行加密和解密。如果您使用GCM,请确保对使用相同加密密钥加密的每个明文使用不同的IV。这非常重要——否则,您的加密将不安全。

以前事实上的标准分组密码操作模式是CBC。你可能会在现有的软件中发现很多CBC的使用,以及很多使用CBC加密的现有数据。CBC模式需要对明文应用填充。如果你开发或支持使用CBC模式的软件,你必须了解什么是填充。

分组密码的填充

在CBC模式下,块密码逐块加密明文数据,但最后一个明文块(在大多数情况下小于块大小)会发生什么?它不能按原样加密,因为分组密码需要一个完整的数据块作为输入。因此,最后一个明文块被填充到块大小。

OpenSSL可以在完成加密时自动添加填充,并在完成解密时将其删除。此功能可以禁用——在这种情况下,开发人员必须自己填充和取消填充明文数据。

密码学家发明了不同类型的填充。对于对称加密,OpenSSL只支持公钥加密标准数字7填充Public Key Cryptography Standard number 7 paddingPKCS#7填充),也称为PKCS7 padding,简称 PKCS padding标准块填充standard block padding)。PKCS#7填充由N个字节组成,每个字节的值为N。例如,如果密码块大小为16个字节(128位),明文的最后一个块只有10个字节的数据,则填充长度将为6个字节,每个填充字节的值将为 0x06 。填充将如下所示:0x06 0x06 0x060x060x06

正如我们所看到的,PKCS#7填充的优点是它对填充的长度进行了编码,从而间接地对最后一个块中有用的明文数据的长度进行编码。因此,整个明文数据的确切长度不需要与数据一起传输。值得注意的是,最后一个数据块的长度对密文的观察者是隐藏的,因为填充字节与最后一个有用的数据字节一起加密。

如果明文长度是块大小的倍数,并且最后一个明文块完全填充了要加密的块,会发生什么?在这种情况下,无论如何都会添加PKCS#7填充。一个仅填充字节的块作为加密的最后一个块附加到明文上。对于具有128位块大小的现代密码,附加了16个字节的填充,每个字节的值为0x10。如果没有添加看似不需要的填充,那么在解密最后一个块后,就不可能知道最后一个字节是填充字节还是明文数据字节。

OpenSSL支持PKCS#7填充是什么意思?这意味着OpenSSL将自动在加密时添加填充,在解密时删除填充,并检查现有的PKCS#7填充是否正确。如果解密的明文没有以N字节的值 N 结束,则现有的填充将是不正确的。在这种情况下,OpenSSL在尝试完成解密时将返回错误。

请注意,PKCS#7填充有时也称为PKCS#5填充。PKCS#5填充使用相同的填充原则(N字节到N的值),但仅定义了最多64位的块。

PKCS#7填充的缺点是,生成的密文容易受到填充预言机攻击。然而,只有当攻击者能够使用相同的加密密钥强制加密大量由攻击者控制的明文时,填充预言机攻击才有可能发生,例如,通过在TLS连接上使用JavaScript发送数据。如果我们控制所有加密的明文(例如,我们只是加密一个文件),那么就不可能,或者至少很难完成填充预言机攻击。

如果您控制加密方法并希望避免填充oracle攻击,则可以使用随机数据手动填充和取消填充。在这种情况下,您必须存储或发送明文的长度,或者至少是最后一个明文块的长度,以及密文。

如何生成对称加密密钥

生成对称加密的加密密钥出奇地容易。您只需从加密安全的随机生成器cryptographically secure random generator)中请求所需数量的随机字节!

哪种随机生成器被认为是加密安全的?它是一个随机生成器,生成极难预测的字节。生成的随机字节的不可预测性是通过使用外部世界不可预测事件引起的熵(entropy)来实现的。通常,加密安全的随机生成器使用由键盘和鼠标输入的不可预测时间引起的熵。如果键盘和鼠标不可用,例如,当应用程序在容器中运行时,可以使用另一个熵源,例如CPU速度的微波动。另一个常见的熵源是可以包含在CPU或另一个芯片中的环形振荡器(a ring oscillator)。

随机字节的正确数量是多少?它与您选择的密码的密钥长度相同。例如,如果您选择了AES-256,则需要256个随机位或32个随机字节。

一些密码,如DES或IDEA,具有弱密钥类,这使得这些密钥的加密不安全,但对于所有流行的密码,随机生成弱密钥的概率非常低。

你从哪里得到加密安全的随机生成器?这取决于您的操作系统(OS)。例如,在Linux上,您可以从 /dev/random 设备获取安全的随机字节,在命令行上运行以下命令:

在这里和本书的其余部分,命令行示例遵循以下约定:

如果你想在C代码中生成安全的随机字节,OpenSSL提供了 RAND_bytes() 函数,该函数使用依赖于操作系统的加密安全伪随机生成器cryptographically secure pseudorandom generatorCSPRNG),并将为你提供所需数量的安全随机字节。您还可以使用 openssl rand 命令在命令行上使用该函数:

所获得的加密密钥可用于对称加密。它还必须保存在安全的地方或安全地传输给解密方,以便以后解密密文。

在许多情况下,你不需要随机加密密钥,但需要一个确定性生成的密钥——例如,从密码或短语中生成的密钥。在这种情况下,您需要使用基于密码的密钥派生函数Password-Based Key Derivation FunctionPBKDF)。关于PBKDF的更多信息将在【第5章“从密码中推导加密密钥”】中给出。

下载并安装OpenSSL

我们现在已经充分了解了对称加密的构建块。是时候利用我们的知识使用OpenSSL加密一些数据了,但首先,我们需要获得OpenSSL所需的组件。

OpenSSL工具包从其官方网站分发(https://www.openssl.org/)以源代码的形式。如果您是软件开发人员,可以使用随源代码提供的文档编译OpenSSL。如果你想要一个特定版本的OpenSSL,或者想用特定的选项编译它,从源代码编译OpenSSL是首选方式。

您还可以以编译的形式获取OpenSSL。几乎所有的Linux发行版都包含一个或多个可安装包形式的OpenSSL。例如,为了在Debian或Ubuntu Linux上安装OpenSSL,您只需要发出一个命令:

另一个Linux发行版可能使用不同的命令,但对于任何Linux发行版,安装都是类似的。FreeBSD、NetBSD或OpenBSD的用户可以从BSD ports安装OpenSSL。macOS用户还可以使用包管理器(如Homebrew或MacPorts)安装OpenSSL。

也可以使用Windows的包管理器(如Scoop、Chocolatey或winget)在Windows上安装OpenSSL。但是,与其他操作系统不同,在Windows上,用户通常不使用包管理器,而是使用独立的安装包。这些独立安装程序也是由第三方开发人员为OpenSSL制作的。这些安装程序的不完整列表可以在以下网址找到 https://wiki.openssl.org/index.php/Binaries

安装OpenSSL后,您的系统上会显示什么?OpenSSL工具包由以下组件组成:

每个组件的目的是什么?

动态库(.so.dll 文件)是链接动态使用OpenSSL库的应用程序以及随后执行这些应用程序所必需的。OpenSSL工具包包括两个库,libcryptolibssl.libcrypto 包含加密算法的实现,libssl 包含SSL和TLS协议的实现。libssl 依赖于 libcrypto,没有它就无法使用。另一方面,如果只需要加密功能,在没有SSL/TLS的情况下,可以在没有 libssl 的情况下使用 libcryptos 。在带有包管理器的系统上,如Linux和BSD,带有OpenSSL动态库的包通常称为 libssl ,可以选择使用版本后缀,例如 libssl3 。如果任何其他已安装的软件包需要OpenSSL库,则必须安装 libssl 软件包。有许多应用程序使用OpenSSL库,因此,libssl 包将安装在大多数系统上。

静态库(.a.lib 文件)是将OpenSSL静态链接到应用程序所必需的。当静态库链接到应用程序时,该静态库包含在应用程序可执行文件中,与动态库不同,不需要在运行时可用。因此,静态库只需要在开发过程中使用。在具有包管理器的系统上,具有OpenSSL静态库的包通常称为 libssl-devlibssl-developenssl-devel 或类似名称。注意 devdevel 后缀——它暗示开发需要该包。

编译需要OpenSSL的C/C++应用程序需要C头文件。它们只用于开发——因此,在具有包管理器的系统上,它们与静态库包含在同一个包中——即 libssl-devlibssl-developenssl-devel

需要使用openssl命令行工具在命令行上使用openssl功能,而无需编写自己的应用程序。该工具对系统管理员和DevOps工程师非常有用。在带有包管理器的系统上,openssl 工具通常包含在一个同名的包中,opensslopenssl 包可能包含 openssl 工具的手册页manmanual),或者这些手册页可能位于一个单独的包含文档的包中。

与每个库一样,OpenSSL也需要文档。OpenSSL最初是Unix的一个库,以手册页的形式为其应用程序编程接口Application Programming InterfaceAPI)提供文档。在带有包管理器的系统上,包含文档的包可以称为 libssl-docopenssl-man 或类似名称。该包还可以包括代码示例。请注意,OpenSSL在其官方网站上也有HTML格式的手册页:https://www.openssl.org/docs/manpages.xhtml

现在,当我们安装了OpenSSL组件后,让我们用一些加密来初步了解一下。

如何在命令行上使用AES加密和解密

我们将使用 openssl 命令行工具加密文件。

让我们生成一个示例文件:

利用我们对对称加密概念的了解,我们为加密选择以下参数:

我们如何从文档中了解如何加密命令行?我们可以从 openssl 工具手册页开始:

在该手册页上,我们可以看到 openssl 工具支持的不同子命令。从手册页中,我们可以看出我们需要 enc 子命令。然后,我们可以参考 openssl-enc 手册页了解 enc 子命令的文档:

openssl-enc 手册页中,我们可以找出子命令需要哪些参数。我们看到 enc 子命令需要加密密钥和IV以及其他参数。我们可以使用 rand 子命令随机生成这些值。其文档可以按常规方式阅读:

好的,我们已经阅读了足够的文档——让我们使用最近获得的知识生成一个256位加密密钥:

我们还需要生成一个128位的IV,长度与块大小相同:

最后,让我们加密一些文件:

openssl 不会向终端打印任何内容。按照Unix的传统,这意味着执行成功。在不成功的情况下,将打印错误。

让我们检查加密文件是否已创建:

请注意,加密文件长11个字节(39043893个字节),3904是16的倍数。如您所料,额外的11个字节是16字节块的填充。

如果你检查加密文件的内容,你会看到看似随机的二进制数据,正如预期的那样。

让我们尝试解密文件:

同样,没有输出,这意味着执行成功。

让我们检查解密文件的内容是否与原始文件完全相同。我们可以通过在编辑器中查看文件的内容来做到这一点,但比较它们的校验和更可靠:

正如我们所看到的,somefile.txtsomefile.txt.decrypted 的大小(3893)和校验和(1830648734)都匹配,这意味着文件是相同的。正如预期的那样, somefile.txt.encrypted 的大小和校验和不同。

如果您使用的是Windows并且没有 cksum 命令,则可以使用OpenSSL的消息摘要message digest)计算功能来计算加密强校验和(消息摘要):

消息摘要比 cksum 命令生成的校验和可靠得多,但加密强度强的消息摘要的计算要慢得多。在我们的例子中,我们不希望 somefile.txt.decrypted 是专门为实现特殊的校验和而精心制作的。因此,我们不需要加密强校验和的优点—— cksum 校验和就可以了。此外,比较 openssl-dgst 输出中的摘要有点困难,因为摘要没有排列。消息摘要将在【第3章“消息摘要”】中详细介绍。

如果我们试图使用错误的加密密钥或IV解密文件,会发生什么?让我们来看看。请注意,密钥的第一个字节已从0x74更改为0x00:

解密失败,但 openssl 是如何检测到失败的?解密只是对16个字节的块进行一系列算术和逻辑运算。我们没有使用经过身份验证的加密来检查数据完整性。我们也没有为 openssl 提供任何校验和来检查解密结果。标准填充起到了帮助作用!最后一个块的解密产生了一个垃圾数据块,该数据块末尾没有正确的填充,openssl 检测到了这一点。

让我们看看校验和列表:

请注意,解密文件比加密文件短16个字节(3888比3904字节)。这是因为在最后一个加密块之前,不可能检测到故障, openssl 将除最后一个块之外的所有块都写入输出文件。正如预期的那样,somefile.txtsomefile.txt.decrypted 的校验和不同。 somefile.txt.decrypted 的内容看起来像垃圾,因为我们试图用错误的密钥解密。

如果我们使用不需要填充的密码操作模式,例如CTR,会怎么样?让我们尝试在CTR模式下解密我们的文件:

没有输出,这意味着 openssl 没有检测到任何错误。记住,我们试图用错误的密钥和错误的模式解密!解密结果不能正确。让我们检查一下文件大小和校验和:

正如预期的那样,somefile.txtsomefile.txt.decrypted 的校验和不同。正如预期的那样, somefile.txt.decrypted 的内容看起来像垃圾。请注意 somefile.txt.decrypted 的大小:3904,与 somefile.tx.encrypted 的大小相同。我们在CTR模式下解密——因此,所有(错误)解密的数据都被视为没有填充的明文。衬垫既没有检查也没有拆除。这就是为什么解密的文件大小与加密的文件大小相同,这就是为什么 openssl 无法检测到解密失败的原因。

正如我们所看到的,为了检查解密结果,使用一些校验和或消息摘要是非常有用的。

到目前为止,我们已经玩够了命令行。让我们编写第一个使用OpenSSL作为库的程序。

正在初始化和取消初始化OpenSSL库

从1.1.0版本开始,OpenSSL库不需要显式初始化和取消初始化。库将自动初始化和取消初始化。

但是,如果需要一些非标准初始化,仍然可以显式初始化OpenSSL。也可以显式取消初始化,但不建议这样做,特别是在多线程程序中,或者如果程序和同一进程中的另一个库都可以使用OpenSSL。请注意,取消初始化后,无法再次初始化OpenSSL。

显式初始化是通过调用 OPENSSL_init_ssl() 函数完成的。显式取消初始化是通过调用 OPENSSL_cleanup() 函数完成的。

低于1.1.0的旧版本OpenSSL将不会自动初始化。如果你需要使用旧的OpenSSL版本,你必须显式地初始化它,使用现在被弃用的 SSL_library_init()EVP_cleanup() 函数。

您可以在这些函数的手册页上阅读API文档:

大多数OpenSSL函数都有一个与函数名同名的手册页。这是一个很好的文档来源。如前所述,手册页也以HTML格式在OpenSSL网站上提供。

让我们编写一个简单的程序,只初始化和取消初始化OpenSSL,只是为了演示如何调用库以及编译和链接使用OpenSSL的程序。以下是它的完整源代码:

init 程序的源代码也可以在GitHub上找到:

https://github.com/PacktPublishing/Demystifying-Cryptography-with-OpenSSL-3/blob/main/Chapter02/init.c

这个程序看起来很简单。让我们了解一下如何编译它并将其与OpenSSL链接。

如何编译和链接OpenSSL

如何编译程序并将其与OpenSSL链接取决于您安装OpenSSL的方式。如果您使用的是Unix,则可以将OpenSSL C头文件和库文件安装到 includelib 系统目录中,如 /usr/include/usr/lib/x86_64-linux-gnu 。在这种情况下,编译和链接可以使用一个简单的命令完成:

头文件和库文件将在 includelib 系统目录中查找。因此,没有必要为标头和库搜索明确指定目录。输出可执行文件将被称为 a.out

如果是在Unix上,并且前面的命令对您不起作用,请检查您是否安装了OpenSSL开发包,如 libssl-devopenssl-devel

如果您尚未在系统范围内安装OpenSSL或不想使用系统范围的安装,则必须明确指定OpenSSL C头文件和库文件的路径:

请注意,我们还使用 -o init 开关指定了输出可执行文件名,因为 a.out 不是最具描述性的名称。请注意, -lssl -lcrypto 开关也位于源文件名后面的命令行中。这很重要。必须在要使用库的文件后指定库。如果一个库使用另一个库,则使用另一库的库将在使用的库之前指定。

指定的库路径可能包括动态或静态OpenSSL库。支持任何一种类型。如果动态库和静态库都可用,则链接器将选择动态库。

这种只需几个开关即可直接调用编译器的方法适用于小程序。对于较大的程序,您需要使用构建工具,如GNU Make、BSD Make或CMake,或IDE,在其中指定所需的路径和开关,并避免在命令行上重复它们。

如果您使用的是Microsoft Windows,则可能会使用IDE,如Microsoft Visual Studio。您需要指定OpenSSL C头文件和库文件的路径,因为在Windows上,没有系统包含和库路径的概念。但是,不要低估命令行构建工具的强大功能,因为它们对于构建自动化和持续集成Continuous Integration)非常有用。

这是一个示例 Makefile 文件, make 工具可以使用它来构建我们的 init 程序:

在这个示例 Makefile 文件中,我们使用了隐式 make 编译规则,通过隐式变量提供编译器选项。如果OpenSSL标头和库在系统路径中,我们可以省略具有隐式 CFLAGSLDFLAGS 变量路径的开关。make 和编译器无法自己找出链接需要哪些库;因此,我们不能省略 LDLIBS 变量的 -lssl -lcrypto 开关。

要使用 make 进行构建,只需运行以下命令:

Makefileinit.c 文件所在的同一目录执行此操作。

让我们检查一下我们编译的可执行文件是否与 libssllibcrypto 链接:

最后,让我们运行可执行文件!

输出与预期一致。

我们已经做好了准备——编写并构建了一个非常简单的应用程序,它调用OpenSSL,但并没有真正做任何有用的事情。它有助于学习如何构建使用OpenSSL的程序。让我们在成功的基础上开发一个更有用的程序——一个加密文件的程序,类似于 openssl enc

如何以编程方式使用AES加密

当使用 OpenSSL 作为库时,我们不必局限于 openssl 工具提供的功能。openssl 是一个很好的工具,但它并没有公开OpenSSL的全部功能。例如,openssl enc 不支持GCM中的加密或解密,但OpenSSL作为一个库允许我们这样做。

在本节中,我们将开发一个程序,该程序可以在GCM中使用AES-256密码对文件进行加密。我们将把我们的程序称为 encrypt

为了避免在命令行上传递太多值,我们将在加密文件中存储IV和身份验证标签。与加密密钥不同,IV和auth标签是公共信息,不必保密。加密文件的格式如下:

位置,字节长度,字节内容
012IV
12密文(ciphertext)
16认证标签

我们的加密程序需要三个命令行参数:

  1. 输入文件名
  2. 输出文件名
  3. 加密密钥,十六进制编码

以下是我们的高层实施计划:

  1. 生成一个随机IV并将其写入输出文件
  2. 初始化加密
  3. 逐块加密,从输入文件中读取明文块,并将得到的密文块写入输出文件
  4. 完成加密
  5. 获取身份验证标签并将其写入输出文件

让我们弄清楚如何执行所有这些阶段。

实施加密程序

以下是我们如何实现加密:

  1. 首先,让我们分配输入和输出缓冲区:

    这些缓冲区将用于在写入之前读取输入数据和存储输出数据。请注意,输出缓冲区比输入缓冲区长一点。这是因为在某些情况下,根据OpenSSL文档,加密可能会产生比明文更长的密文,长达 BLOCK_SIZE - 1 字节。在我们的例子中,这不应该发生,但无论如何,让我们遵循OpenSSL文档。

  2. 生成IV很容易——我们只需要使用我们已经提到的RAND_bytes()函数:

  3. 然后,我们必须初始化加密。初始化加密的第一件事是创建密码上下文:

让我们来谈谈 EVP_ 前缀和 _CTX 后缀。在使用OpenSSL开发程序时,您会经常看到和使用这些缩写词。

EVP代表信封Envelope),表示 Envelope API 的使用。信封API是一个高级API,允许我们完成高级操作,例如,在所需的操作模式下加密长明文。Envelope API将为开发人员提供任务的数据结构和功能集合,例如初始化加密、设置密钥和IV、添加用于加密的明文块、完成加密以及获取加密后数据,如身份验证标签。与Envelope API相比,OpenSSL还具有旧的加密API和函数,如 AES_encrypt()AES_cbc_encrypts()AES_ofb128_encryption()。旧的API有很多缺点:

幸运的是,上述问题在EVP API中得到了解决。自OpenSSL 3.0以来,旧的加密API已被正式弃用。

现在,关于CTX的缩写。 CTX 代表上下文Context)。在使用OpenSSL开发时,您将看到并使用许多上下文——用于加密、消息摘要和消息身份验证代码的计算、密钥推导、TLS连接、TLS会话和TLS设置。什么是上下文?它是一个C结构,用于存储与当前操作相关的数据。一些上下文更高级,它们存储的设置可以应用于同一类型的多个操作,例如,多个TLS连接。

在我们的例子中,EVP_CIPHER_CTX 存储密码类型、加密密钥、IV、标志和加密操作的当前内部状态。可以将密钥和IV等数据设置到 EVP_CIPHER_CTX 中,并从中获取数据,如身份验证标签。EVP_CIPHER_CTX 对象被提供给我们在所选操作模式下加密期间需要调用的所有函数:

  1. 好的,我们已经创建了EVP_CIPHER_CTX对象。接下来我们要做的是使用所选的操作模式、密钥和IV初始化上下文和加密过程:

  2. 初始化完成。现在,我们可以使用EVP_EncryptUpdate()函数逐块加密:

  3. 加密即将完成。现在,我们必须使用 EVP_EncryptFinal() 函数来完成它:

    加密完成是一种取决于所使用的密码和模式的操作。例如,在CBC模式下,完成操作会向最后一个数据块添加填充并对其进行加密,从而产生最后一个密文块。这意味着最终化操作可能会产生数据,因此,它将输出缓冲区作为参数之一。

  4. 在GCM中,完成操作计算身份验证标记,这提醒我们必须从上下文对象中获取它并写入输出文件:

  5. 我们需要做的最后一个操作是释放不再需要的EVP_CIPHER_CTX对象,以避免内存泄漏:

这些就是我们需要的所有OpenSSL操作!

加密程序的完整源代码可以在GitHub上的 encrypt.c 文件中找到:https://github.com/PacktPublishing/Demystifying-Cryptography-with-OpenSSL-3/blob/main/Chapter02/encrypt.c

在给定的URL中,您可以找到完整的实现,但几乎没有错误处理,这样错误处理就不会使代码混乱,代码流也更明显。

这是另一个实现,encrypt-with-extended-error-checking.c ,具有扩展的错误处理:https://github.com/PacktPublishing/Demystifying-Cryptography-with-OpenSSL-3/blob/main/Chapter02/encrypt-with-extended-error-checking.c

代码的可读性不如第一个实现,但第二个实现可能对调试和故障排除有用。

关于代码中 goto 语句的使用,我们都知道 “Go To statement Considered Harmful” (Go To 声明被认为有害)语句,我们不应该过度使用它。一些开发人员认为根本不应该使用 goto ,但有时 goto 是有用的——例如,用于重复清理代码,如前所述。在C++中,我们可以使用析构函数进行自动清理,但在C中,我们没有这种可能性。

运行加密程序

让我们试着运行我们的加密程序:

  1. 生成用于进一步加密的示例文件:

  2. 加密文件:

  3. 检查文件大小和校验和:

正如预期的那样,原始文件和加密文件是不同的。加密文件长28个字节,因为除了密文外,它还包含IV和auth标签。

到目前为止看起来不错,但我们能解密我们加密的内容吗?

如何以编程方式使用AES解密

在本节中,我们将开发 decrypt 程序,该程序可以加密由 encrypt 程序加密的文件。

我们的解密程序将类似于加密程序,也将接受三个命令行参数:

  1. 输入文件名
  2. 输出文件名
  3. 加密密钥,十六进制编码

这次,输入文件是由前面的 encrypt 程序创建的加密文件。

让我们制定一个高层次的计划,类似于我们之前的做法:

  1. 从输入文件中读取IV。
  2. 初始化解密。
  3. 逐块解密,从输入文件读取明文块,并将得到的明文块写入输出文件。
  4. 从输入文件中读取身份验证标签,并将其设置为密码上下文。
  5. 完成解密。

正如我们所看到的,解密计划与加密计划非常相似——初始化、处理和最终确定。让我们看看它是如何在具体代码中实现的。

实现解密程序

以下是我们如何实现解密:

  1. 我们读取IV:

    注意 current_pos 变量。这一次,我们不能依赖读取输入文件的末尾,需要跟踪我们在输入文件中的位置。

  2. 我们初始化解密。请注意,这次我们使用 EVP_DecryptInit() 函数而不是 EVP_EncryptInit() 进行初始化:+

  3. 主要部分是逐块解密。对于解密,我们使用 EVP_DecryptUpdate() 函数而不是 EVP_EncryptUpdate()

    请注意解密循环的这一部分:

    在这里,我们避免从输入文件中读取太多内容,以避免将身份验证标签读取到密文缓冲区中。

  4. 当我们读取并解密密文后,是时候读取身份验证标签并将其设置到密码上下文中了:

    我们必须在完成解密之前设置身份验证标签,否则完成操作将失败。

  5. 最后,为了完成解密,我们必须使用 EVP_DecryptFinal() 函数而不是 EVP_EncryptFinal()

身份验证标签将在完成操作期间进行验证。因此,在以下任何一种情况下,最终确定都将失败:

decrypt 程序的完整源代码可以在GitHub上以 decrypt.c 文件的形式找到: https://github.com/PacktPublishing/Demystifying-Cryptography-with-OpenSSL-3/blob/main/Chapter02/decrypt.c

还有一种替代实现, decrypt-with-extended-error-checking.c ,具有扩展的错误检查功能,可用于调试和故障排除:

https://github.com/PacktPublishing/Demystifying-Cryptography-with-OpenSSL-3/blob/main/Chapter02/decrypt-with-extended-error-checking.c

我们已经实现了解密程序。让我们运行它。

运行解密程序

是时候运行我们的解密程序并检查生成文件的校验和了:

  1. 使用 decrypt 程序解密文件:

  2. 看起来不错!让我们检查一下文件checksums:

    原始文件和解密文件具有相同的大小和校验和。我们的解密确实成功了!

  3. 如果我们向解密程序提供了错误的密钥,会发生什么?让我们检查一下!我们预计在错误解密的文件中会出现错误消息和垃圾。让我们这次使用带有扩展错误检查的程序版本:

    正如预期的那样,解密在完成操作时失败。由于加密密钥错误,身份验证标记验证失败。

  4. 让我们看看这些文件:

原始文件和解密文件大小相同,但校验和不同。如果我们检查解密文件的内容,我们只会看到垃圾。这就是为什么只更改密钥中的一个字节会影响解密。

我可以给从事加密工作的开发人员什么建议?执行时要非常小心,并使用正确的加密密钥!

我们已经学习了如何加密、解密和检查解密是否正确。让我们继续进行总结。

总结

在本章中,我们了解了什么是对称加密以及它与非对称加密的区别。我们还了解了什么是密码,什么是分组密码和流密码,如何衡量密码安全性,以及多少安全性才足够。然后,我们了解了OpenSSL支持哪些密码,在什么情况下应该使用哪些密码,以及应该避免哪些密码。我们还了解了存在哪些密码操作模式,它们有何不同,以及应该使用哪些模式。

我们讨论了填充,为什么它存在,以及在哪些情况下应该使用它。我们还介绍了oracle以及如何下载和安装OpenSSL。我们探索并了解了OpenSSL工具包中包含的内容,如何初始化OpenSSL库,以及如何编译程序并将其与OpenSSL库链接。最后,我们学习了如何生成加密密钥和IV,如何使用OpenSSL命令行工具对文件进行加密和解密,以及如何使用OpenSSL库以编程方式对文件进行加解密。

在本章中,我们使用校验和来检查文件是否相同。它非常有用。我们还提到了消息摘要,这是一种加密强度很强的校验和。在下一章中,我们将了解更多关于消息摘要的信息,特别是OpenSSL支持哪些消息摘要算法,以及如何使用OpenSSL计算消息摘要。