应用 Crypto++:块密码






4.85/5 (47投票s)
使用 Crypto++ 的分组密码加密数据。
引言
Crypto++ 提供了超过 25 种 分组密码,从 AES 到 XTEA。现代分组密码需要选择算法、模式、反馈大小、填充,有时还需要轮数。本文将向读者展示如何使用 Crypto++ 的分组密码。本文将讨论的主题包括:
- 背景
- Crypto++
- 分组密码
- 流密码
- 模板模式对象与外部密码对象
- 模板模式对象
- 外部密码对象
- 实际区别
- 获取块大小
- 密文窃取错误
- BTEA
- StreamTransformationFilter
- 测试向量
- 消息填充
- 工作模式
- 反馈大小
- 链接或反馈
- 初始化向量
- 密码块链接
- 电子密码本
- 输出反馈
- 密码反馈
- 计数器模式
- 密文窃取
- 消息认证码
- 重复使用加密和解密对象
- 使用分组密码
- 杂项示例
- CTS - 密文窃取
- CTR - 计数器模式
- 块大小和密钥大小
- 密文大小
- 使用 vector<byte>
- 转换
- 带 MAC 的加密器
- 表格
- 块大小和密钥大小
- 明文与密文大小
背景
商业分组密码在 20 世纪 70 年代中期首次亮相。IBM 研究员 Walter Tuchman 和 Horst Feistel 是开发 Lucifer 的团队成员,这是一种 128 位块密码,具有 128 位密钥。Lucifer 于 1971 年由 IBM 申请专利(1974 年颁发美国专利 3,798,359)。Lucifer 为 数据加密标准奠定了基础,该标准于 1975 年提出,并于 1977 年通过 FIPS 46 标准化。1980 年,DES 的四种工作模式在 FIPS 81 中进行了规定。值得注意的是,64 位 DES(带 56 位密钥)比 128 位 Lucifer 更强大。
对称密钥也称为 共享密钥密码学,因为双方使用相同的密钥。这与 非对称密码学(也称为 公钥密码学)形成对比,后者将密钥分为公钥和私钥对。
对称密码解决了安全相关的三大问题。这三大问题统称为 CIA,即 保密性、完整性 和 认证性。 保密性问题是确保我们的通信是私密的(机密性),无需进一步解释。 认证性问题得到解决,因为我们知道谁拥有密钥。广义上讲,通过在密码中添加最终的加密操作,可以实现消息的完整性。
共享密钥加密系统更易于实现且使用更快。此外,对称密码通常设计用于加密任意长度的消息。然而,要使用该系统,双方都必须能够安全地共享密钥。这被称为 密钥分发问题。
对称密码使用线性变换和非线性变换来加密和解密消息。这与非对称密码(如 RSA 密码学或 椭圆曲线密码学)不同,后者利用数学中的困难问题来构建密码系统的强度。 线性变换是旋转和位移等操作。非线性变换包括 XOR、使用 S-box 的替换以及使用 P-box 的排列。线性变换和非线性变换有时被称为混淆和扩散。
最后,如果您正在阅读本文以选择 Crypto++ 分组密码和工作模式,请同时访问 认证加密。
Crypto++
提供的示例使用各种 Crypto++ 对称密码。Crypto++ 可以从 Wei Dai 的 Crypto++ 页面下载。有关编译和集成问题,请访问 将 Crypto++ 集成到 Microsoft Visual C++ 环境中。本文基于之前文章中提出的假设。对于那些对其他 C++ 加密库感兴趣的人,请参阅 Peter Gutmann 的 Cryptlib 或 Victor Shoup 的 NTL。
分组密码
分组密码在称为块的单元上操作数据。块大小取决于所使用的密码,但通常为 64 或 128 位。此规则的一个例外是 SHACAL-2,它使用 256 位块。
如果我们想加密 64 字节长的数据,并且我们选择了一个 128 位块大小的密码,该密码会将 64 字节分成四个块,每个块 128 位。块如何加密在工作模式中有详细介绍。
流密码
Sosemanuk 和 Wake 等密码被设计为流密码。 流密码不需要固定大小的块。分组密码(如 DES 和 AES)可以通过使用 Crypto++ 适配器(称为 StreamTransformationFilter)来模拟流密码。
如果您发现在使用分组密码时需要 1 位或 8 位的反馈大小,请考虑使用流密码。最后,当使用分组密码作为流密码时,仍然存在最小密钥大小。因此,AES 仍然需要 16 字节的密钥材料。
模板模式对象与外部密码对象
当我们使用对称密码时,我们必须选择一个密码和一个模式。选择模式时,我们可以在 Crypto++ 中以两种方式之一使用它。第一种方法使用密码作为模板参数。第二种方法使用外部密码对象。现在是时候指出了,我们可以使用模板模式对象进行加密,并使用外部密码对象进行解密。
模板模式对象
示例 1 到 4 使用分组密码的模板版本。在这种情况下,分组密码是模式对象的模板参数——它持有密码对象的一个实例。
CTR_Mode< AES >::Encryption encryptor;
encryptor.SetKeyWithIV( key, AES::DEFAULT_KEYLENGTH, iv );
...
这使我们能够使用加密和解密对象,而无需了解转换方向的任何细节。
// Encryption
CTR_Mode< AES >::Encryption encryptor;
encryptor.SetKeyWithIV( key, AES::DEFAULT_KEYLENGTH, iv );
StreamTransformationFilter stf( encryptor, new StringSink( cipher ) );
stf.Put( (byte*)plain.c_str(), plain.size() );
stf.MessageEnd();
...
// Decryption
CTR_Mode< AES >::Decryption decryptor;
decryptor.SetKeyWithIV( key, AES::DEFAULT_KEYLENGTH, iv );
StreamTransformationFilter stf( decryptor, new StringSink( recovered ) );
stf.Put( (byte*)cipher.c_str(), cipher.size() );
stf.MessageEnd();
外部密码对象
第二种方法使用外部密码对象。在这种情况下,模式对象持有对外部密码的引用。示例 5 到 9 使用外部密码对象。
AESEncryption aese( key, AES::DEFAULT_KEYLENGTH );
CBC_Mode_ExternalCipher::Encryption encryptor( aese, iv );
...
AESDecryption aesd( key, AES::DEFAULT_KEYLENGTH );
CBC_Mode_ExternalCipher::Decryption decryptor( aesd, iv );
实际区别
获取块大小
使用模板模式对象时,成员函数 CipherModeBase::BlockSize()
是 protected
的。在使用外部密码对象时,这不是问题。结果是,使用模板模式对象的以下代码将无法编译。解决此问题的简单方法是在 CipherModeBase
中将 BlockSize()
函数声明为 public。
byte key[ AES::DEFAULT_KEYLENGTH ];
byte iv[ AES::BLOCKSIZE ];
...
CTR_Mode< AES >::Encryption encryptor;
encryptor.SetKeyWithIV( key, sizeof(key), iv );
cout << "Block Size: ";
cout << encryptor.BlockSize() << " bytes" << endl;
密文窃取错误
当使用 CTS 模式和模板模式对象时,如果明文太短以至于无法进行窃取,以下代码将在 MessageEnd()
处抛出 Crypto++ 异常,提示“CBC_Encryption: message is too short for ciphertext stealing”。
CBC_CTS_Mode< AES >::Encryption encryptor;
encryptor.SetKeyWithIV( key, sizeof(key), iv );
StreamTransformationFilter stf( encryptor, new StringSink( cipher ));
stf.Put( (byte*)plain.c_str(), plain.size() );
stf.MessageEnd();
但是,在使用外部密码对象时,MessageEnd()
将导致 memcpy.asm 中发生访问冲突。
AESEncryption aese( key, AES::DEFAULT_KEYLENGTH );
CBC_CTS_Mode_ExternalCipher::Encryption encryptor( aese, iv );
StreamTransformationFilter stf( encryptor, new StringSink( cipher ));
stf.Put( (byte*)plain.c_str(), plain.size() );
stf.MessageEnd();
BTEA
当需要 BTEA 时,请使用 Needham 和 Wheeler 的参考实现。这是因为 BTEA 是一种可变长度的分组密码,在 Crypto++ 中会引起比解决问题更多的麻烦。原始的 1997 年实现可以在 这里找到。1998 年的更新(由于 Wagner 的分析)可以在 这里找到。
StreamTransformationFilter
无论我们如何设置分组密码,我们几乎总是会使用 StreamTransformationFilter
来加密和解密数据。我们这样做是因为该过滤器为我们处理了缓冲、分块和填充。总之,它使库的使用更加容易。唯一的例外是示例 1,因为我们自己处理了分块和填充。
StreamTransformationFilter stf( encryptor, new StringSink(cipher) );
stf.Put( (byte*)plain.c_str(), plain.size() );
stf.MessageEnd();
如果不是所有要加密的数据都可用,我们可以多次调用 Put()
。在这种情况下,过滤器将为我们缓冲明文。在 MessageEnd()
时,过滤器将根据需要为我们填充消息。std::string
cipher 将保存加密数据,“Hello World”。
StreamTransformationFilter stf( encryptor, new StringSink(cipher) );
...
stf.Put( "Hello", sizeof("Hello") );
stf.Put( " ", sizeof(" ") );
stf.Put( "World", sizeof("World") );
...
stf.MessageEnd();
Crypto++ 使用 Unix 管道范例:数据从源流向目的地。因此,在使用 StreamTransformationFilter
时,我们可能会遇到以下情况:
StringSource( PlainText, true,
new StreamTransformationFilter(
Encryptor,
new StringSink( CipherText )
) // StreamTransformationFilter
); // StringSource
上面,我们从 StringSource
开始,到 StringSink
结束。中间的过滤器执行缓冲、分块和填充。信息通过 StreamTransformationFilter
的流程如图 2 所示。
如果我们可视化使用 StreamTransformationFilter
的系统框图,它将如图 3 所示。请注意,传递给 StreamTransformationFilter
的 BufferedTransformation
参数是加密器对象。BufferedTransformation
是加密器和解密器的基类。
测试向量
所有密码都提供测试向量来测试实现。如果我们发现 Crypto++ 和其他库的结果不同,我们通常可以使用提供的测试向量来验证每个库的结果。例如,如果使用 Triple DES (E-D-E),我们可以使用 FIPS 800-67,附录 B。如果我们关心 AES 实现,FIPS 197,附录 C 将是确切的来源。其他密码,如 BTEA,仅提供作者的参考实现。
消息填充
当消息不是密码块大小的倍数时,ECB 或 CBC 模式消息必须填充。填充的方法和值是加密库和 API 之间互操作性问题的来源。正如 Garth Lancaster 指出的,如果您不了解填充的细节,请使用 StreamTransformationFilter
。在这种情况下,Crypto++ 过滤器将为您填充。
PKCS #5 使用 RFC 1423 作为填充系统:8-(||M|| mod 8)。(注意 PKCS #5 的 DES、RC2 和 RC5 是 8 字节块密码。)该方案生成的填充可以明确移除。例如,如果消息需要一个填充字节,值将是 0x01。如果消息需要两个填充字节,则字节将是 0x02, 0x02,依此类推。
PKCS #7 使用更通用的填充方案。PKCS #7 使用 k - (l mod k) 而不是 mod 8 系统,这对于 k < 256 是明确定义的。在这方面,PKCS #5 是 PKCS #7 的特例。Schneier 和 Ferguson 建议使用 PKCS 或基于附加 0x80 然后用 0x00 填充其余部分的方案。有关其他填充方法,请参阅使用加密中的填充或 Wiki 的块密码工作模式。
在第一个示例(示例 1)中,我们在 ECB 模式下使用 AES。我们手动用字符串“Hello World”和 0x00 填充到密码的块大小,这可能会破坏互操作性。如果使用 StreamTransformationFilter
,Crypto++ 会用 PKCS 填充。但是,我们可以指定 StreamTransformationFilter
如何填充我们的消息。零填充可以通过以下方式实现:
// Setup
byte key[AES::DEFAULT_KEYLENGTH] = { ... };
byte iv[AES::BLOCKSIZE] = { ... };
// Encryption
AESEncryption aese( key, AES::DEFAULT_KEYLENGTH );
CBC_Mode_ExternalCipher::Encryption encryptor( aese, iv );
...
StreamTransformationFilter stf( encryptor,
new StringSink(ciphertext), ZEROS_PADDING );
StreamTransformationFilter::BlockPaddingScheme
提供五种选择:NO_PADDING
、ZEROS_PADDING
、PKCS_PADDING
、ONE_AND_ZEROS_PADDING
和 DEFAULT_PADDING
。ONE_AND_ZEROS_PADDING
是指定 Schneier 和 Ferguson 备用推荐的常量。为了简单起见,将使用默认填充方案。
// Encryption
AESEncryption aese( key, AES::DEFAULT_KEYLENGTH );
CBC_Mode_ExternalCipher::Encryption encryptor( aese, iv );
...
StreamTransformationFilter stf( encryptor, new StringSink(ciphertext) );
工作模式
分组密码与工作模式一起使用。在早期密码学(约 1980 年)中,有四种批准的模式可供选择:ECB、CBC、OFB 和 CFB。这些模式(以及其他)已在 FIPS 81、ANSI X3.106 和 ISO/IEC 10116 中标准化。之后出现了 CTR 和 CTS 等模式。CTR 和 CTS 在 NIST SP800-38A 中标准化(SP800-38A 认可了 FIPS 81 的四种模式,而 ISO 10116 已更新)。工作模式指定上一轮的输出如何作为下一轮的输入。根据密码所需的属性,我们可以选择不同的模式。例如,如果我们想要一种自同步的密码,我们会选择 CFB。如果我们需要的密码能够承受有噪声的传输线路,我们会选择 OFB,因为它抗干扰。如果我们希望密码能够“自恢复”位错误,CBC 模式将是我们的选择。
反馈大小
反馈大小是 Crypto++ 库互操作性问题的根源。例如,当使用默认的 AES/CFB 对象时,OpenSSL 和 Crypto++ 可以很好地互操作,因为两者使用相同的默认反馈大小。Crypto++ 也与嵌入式 SSL 库 XySSL 配合良好。然而,mcrypt(以及基于 mcrypt 的 Python/PHP 脚本)在不调用备用 Crypto++ 构造函数的情况下,与 Crypto++ 的互操作性不佳。例如,Crypto++ 默认反馈大小为 128 位,而 mcrypt 默认使用 CFB 模式,并具有默认反馈大小为 8 位。下面将解释为什么我们选择反馈大小时必须小心。
另一个反馈大小问题的例子是 CLR 的 TripleDESCryptoServiceProvider
类。TripleDESCryptoServiceProvider
是一个三密钥 E-D-E 实现。实例化该类时,两个默认参数是 CBC 模式,反馈大小为 8 位。
根据所使用的模式,可能存在不同的反馈大小(子模式)。例如,NIST 800-38A 为 CFB 模式指定了四种反馈大小:1 位、8 位、64 位和 128 位。要设置不同的反馈大小,请使用 <模式>_Mode_ExternalCipher
对象的备用构造函数指定字节大小。例如,使用 AES 和 CFB 模式,默认密钥(16 字节)、默认块大小(16 字节)和 64 位反馈大小:
// Setup
byte key[AES::DEFAULT_KEYLENGTH] = { ... };
byte iv[AES::BLOCKSIZE] = { ... };
// Encryption
AESEncryption aese( key, AES::DEFAULT_KEYLENGTH );
CFB_Mode_ExternalCipher::Encryption encryptor( aese, iv, 8 /* 64 bits */ );
...
为了简单起见,将使用默认反馈大小,通常是密码的块大小。因此,示例将使用以下内容:
// Encryption
AESEncryption aese( key, AES::DEFAULT_KEYLENGTH );
CFB_Mode_ExternalCipher::Encryption encryptor( aese, iv );
如果模式不支持选定的反馈大小,Crypto++ 将抛出类似“CipherModeBase: feedback size cannot be specified for this cipher mode.”的异常。当尝试将 AES 与某些模式反馈大小一起使用时,我们可能会遇到这种情况,因为 AES 对 Rijndael 有额外的限制。此外,从 modes.h 中可知,反馈大小必须小于或等于块大小。下面显示了来自 CipherModeBase
类 CFB_Mode
的受保护成员函数 SetFeedbackSize()
:
void SetFeedbackSize(unsigned int feedbackSize)
{
if (feedbackSize > BlockSize())
throw InvalidArgument("CFB_Mode: invalid feedback size");
m_feedbackSize = feedbackSize ? feedbackSize : BlockSize();
}
虽然我们可以指定反馈大小,但我们必须意识到选择的含义。例如,OFB 模式使用反馈作为生成密钥流的机制。如果反馈大小等于密码的块大小,反馈将充当 m 位值的排列,其中 m 是块长度。平均周期长度为 2m - 1。例如,如果我们选择 AES(128 位块大小),我们实际上有一个周期长度为 2128 - 1。如果我们选择 64 位或 32 位的反馈大小,我们会将周期长度减小到 2m/2 或 264。这还不是太糟糕。
然而,考虑 DES(和其他 8 字节块密码)的情况,由于遗留安装而流行度有限。DES 有一个 8 字节的块大小。如果我们减小反馈大小,密钥流的周期将是 232,这不是一个很大的数字。根据 FIPS 81,NIST 不支持使用反馈小于 64 位的 OFB 模式。如果您发现需要 1 位或 8 位的反馈大小,请考虑使用流密码。
反馈链接
大多数工作模式都使用某种形式的反馈或链接。这种方法使用前几轮的输出来影响当前几轮的加密。在图 4 中,被馈送到下一轮的输出是密文,但这并不总是如此。
初始化向量
上述方法有一个缺点:第一轮只能使用密钥,因为没有来自前一轮的反馈。这在图 5 中显示。
通过使用初始化向量来克服这个问题。因为初始化向量引导链接过程,所以它的大小将与输出块大小相同。初始化向量的添加如图 6 所示。
初始化向量通常不必是秘密的,但它不应该与同一个密钥重复使用。重复使用的 IV 要么会泄露明文信息(CBC 和 CFB 模式),要么会破坏系统的安全性(OFB 和 CTR 模式)。
初始化向量的要求可以从 IVRequirement()
返回的 IV_REQUIREMENT
类型确定。值将是 UNIQUE_IV
、RANDOM_IV
、UNPREDICTABLE_RANDOM_IV
、INTERNALLY_GENERATED_IV
或 NOT_RESYNCHRONIZABLE
。这些值是枚举,而不是位掩码编译。
电子密码本
电子密码本 (ECB) 是最简单的方法。消息被分解成块,每个块与密钥结合。没有块链接或反馈。通常不建议使用 ECB 模式,因为它会泄露信息。
图 8 显示了由于 ECB 模式中明文字节的重复而导致的密文字节重复。另一个常见示例是加密 Linux 内核的吉祥物 Tux(请参阅 Wiki 上的电子密码本 (ECB))。
密码块链接
图 6 有点过于笼统。为了描述密码块链接 (CBC),我们将执行以下操作。在图 9 中,IV 和明文块在使用 XOR 组合后再输入加密操作。然后密文被馈送到下一组轮次,取代初始化向量。
密码反馈
图 10 描绘了密码反馈 (CFB) 模式。请注意,在图 10 中,我们加密了密钥和初始化向量。然后,输出与明文进行 XOR 操作以产生密文。然后密文被馈送到下一组轮次,以代替初始化向量。
输出反馈
图 11 显示了输出反馈 (OFB) 模式。在此模式下,加密的输出在最终 XOR 之前被抽取。抽取出的输出被馈送到下一组轮次,而最终的 XOR 操作产生密文。因为输出在最终 XOR 之前被抽取,所以密钥流可以在不需要明文或密文的情况下预先计算。明文或密文都不用于反馈机制。
计数器模式
计数器模式 (CTR) 类似于 ECB 和 OFB 模式。CTR 使用一个运行的计数器块来代替初始化向量/反馈机制。它类似于 ECB 模式,因为每个明文块都是独立加密的,而不是通过前一组轮次的结果。它类似于 OFB 模式,因为密钥和计数器被加密,然后结果与明文进行 XOR 操作。视觉表示如图 12 所示。
与初始化向量一样,计数器的大小是密码的块大小。对于默认 AES,这将是 16 字节。NIST 特别出版物 800-38A 规定了使用 CTR 模式的两种方法。第一种方法使用整个块密码大小(AES 的情况为 16 字节)作为单调递增的值。当我们检查 Crypto++ 源代码(modes.cpp)时,我们看到该库使用这种方法(参数 s
是密码的块大小):
inline void IncrementCounterByOne(byte *inout, unsigned int s)
{
for (int i=s-1, carry=1; i>=0 && carry; i--)
carry = !++inout[i];
}
NIST 的第二种方法将计数器块分为两个独立的对:一个 nonce 和一个单调递增的计数器。Nonce 只需要唯一,不一定是随机的。SP 800-38A,B.2 节没有指定 nonce 使用多少位,计数器使用多少位。因此,如果我们选择 AES,我们可以使用最高 8 个字节作为 nonce,最低 8 个字节作为计数器。Nonce 不会改变,而计数器将为消耗的每 16 字节明文而增加。实际上,如果我们的计数器从 0 开始,我们可能永远不会用完 264 个计数器值。无论哪种情况,与初始化向量一样,我们都不能重复使用计数器块。最后,初始计数器块为 0 是可以接受的,因为值只需要唯一而不是随机。请参阅 CTR 模式/初始计数器 (NIST SP800-38A)。
密文窃取
密文窃取 (CTS) 是一种技术,它不将最后一个块填充到密码的块大小。窃取密文时,执行以下三个步骤;窃取发生在第二步:
![]() |
原始明文1 和明文2 |
![]() |
用明文1 的低位比特填充明文2 |
![]() |
交换明文1 和明文2 |
![]() |
截断明文1 |
要使用 CTS 模式,明文必须大于块大小。因此,如果我们使用 DES(块大小为 8 字节),我们可以对 12 字节的明文使用 CTS 模式。如果只有 7 字节的明文,我们将无法使用 DES(或者必须填充明文)。我们不能选择 TwoFish(块大小为 16 字节),因为没有可以窃取的先前块。如果明文有 17 字节,TwoFish 将是一个可行的选择。
CTS 模式的输出在示例 5 中有所展示。在文章末尾,表 3 使用连字符表示对于 CTS 来说太短的明文长度。
消息认证码
MAC 是在 20 世纪 70 年代为金融行业开发的。ANSI X9.9 将 MAC 描述为 8 位十六进制数字,这是通过使用特定密钥将金融消息通过认证算法得到的结果。
工作模式允许我们基于共享密钥加密数据。它解决了保密性和认证性的问题。分组密码还可以用于解决完整性问题。这是确定数据在磁盘或传输过程中是否被篡改的问题。要使用分组密码解决这个问题,我们需要重新审视块链接。
我们已经看到,要引导链接过程,我们必须提供一个初始化向量。然后密码会处理数据,分块直到所有数据都被消耗。数据被消耗后,我们会得到一个额外的输出块,该块不会被馈送到下一组轮次,因为下一组轮次没有明文。这会产生一个唯一的残差。
如果我们结合残差和密钥(图 14),我们就得到了一个消息认证码或 MAC。MAC 也称为密钥散列。这样使用时,MAC 是数字签名的对称密码等效项,但缺少不可否认性。我们丢失了不可否认性,因为密钥是与他人共享的。
虽然可能会诱使我们加密数据(保存密文),然后将残差与密钥进行 XOR 操作以形成完整性代码,但我们不应该这样做,因为生成的完整性代码是不安全的。在这种情况下,完整性代码独立于明文和密文。为了解决这个问题,我们需要对数据进行两次传递——一次加密数据,一次生成 MAC——使用不同的密钥或 IV。有关 Crypto++ 的实现,请参阅“带 MAC 的加密器”和下面的示例 11。
FIPS 81 规定了两种 MAC:CFB 和 CBC。CBC-MAC 基于 DES,是计算消息认证码的常用算法。CFB 模式 MAC 鲜为人知,与 CBC 模式相比存在一些缺点。CBC-MAC 现在被认为对某些消息(如长度可变的)不安全。这导致开发了使用 128 位密码(如带计数器的 AES)的更强 MAC(RFC 3610)。这被称为 CCM,即带计数器的 CBC-MAC。
重复使用加密和解密对象
创建对象、加密数据,然后重置对象以处理下一条消息并不罕见。这种情况经常出现在原型代码中,其中不需要严格的 IV 要求,或者(由于模式选择)加密和解密可以使用同一个对象。此外,在 CTR 模式下手动管理计数器将使用此技术。要执行简单的重置,请使用 Resynchronize()
方法。
// Setup
byte key[ThreeWay::DEFAULT_KEYLENGTH] = { ... };
byte iv[ThreeWay::BLOCKSIZE] = { ... };
// Encryption
ThreeWayEncryption twe( key, ThreeWay::DEFAULT_KEYLENGTH );
CBC_CTS_Mode_ExternalCipher::Encryption encryptor( twe, iv );
StreamTransformationFilter stf( encryptor, new StringSink( cipher ));
stf.Put( (byte*)plain.c_str(), plain.size() );
stf.MessageEnd();
// Reset Encryptor
// IV reuse is dangerous!
encryptor.Resynchronize( iv );
使用分组密码
示例 1 演示了在 Crypto++ 中使用分组密码。我们注意到的第一项是字符串“Hello World”被填充以达到 16 的块大小。然后将密钥初始化为非随机值。正确使用库应包含伪随机值。有关 Crypto++ 的伪随机数生成器,请参阅伪随机数生成器概述。
byte PlainText[] = {
'H','e','l','l','o',' ',
'W','o','r','l','d',
0x0,0x0,0x0,0x0,0x0
};
byte key[ AES::DEFAULT_KEYLENGTH ];
::memset( key, 0x01, AES::DEFAULT_KEYLENGTH );
接下来,我们构造一个加密对象,然后将字节块推入进行加密。下面,sizeof(PlainText)
= AES::BLOCKSIZE
= 16。由于我们使用的是 ECB 模式,因此没有初始化向量。
// Encrypt
ECB_Mode< AES >::Encryption Encryptor( key, sizeof(key) );
byte cbCipherText[AES::BLOCKSIZE];
Encryptor.ProcessData( cbCipherText, PlainText, sizeof(PlainText) );
我们使用ProcessData() 来加密明文,因为它允许我们在单行代码中获得结果。然后,我们进入 DMZ,然后解密密文。最后,我们将结果打印到标准输出。同样,sizeof(CipherText) = AES::BLOCKSIZE = 16。
// Decrypt
ECB_Mode< AES >::Decryption Decryptor( key, sizeof(key) );
byte cbRecoveredText[AES::BLOCKSIZE];
Decryptor.ProcessData( cbRecoveredText, cbCipherText, sizeof(cbCipherText) );
如果我们没有将数据构造为 AES::BLOCKSIZE
(16 字节)的倍数,程序将在 Debug 版本中断言,并在 Release 版本中产生未定义的结果。在前面的示例中,我们需要按密码块大小的倍数处理数据。这需要我们填充数据。这很快就会变得麻烦,而且肯定会出错。示例 2 将使用 StreamTransformationFilter
来纠正这种情况。
string PlainText =
"Voltaire said, Prejudices are what fools use for reason";
// Encryptor
ECB_Mode< AES >::Encryption Encryptor( key, sizeof(key));
// Encryption
StringSource(
PlainText,
true,
new StreamTransformationFilter(
Encryptor,
new StringSink( CipherText )
) // StreamTransformationFilter
); // StringSource
...
// Decryptor
ECB_Mode< AES >::Decryption Decryptor( key, sizeof(key) );
// Decryption
StringSource(
CipherText,
true,
new StreamTransformationFilter(
Decryptor,
new StringSink( RecoveredText )
) // StreamTransformationFilter
); // StringSource
如果我们查看输出,会发现 AES 仍在使用 ECB 模式。该过滤器内部调用加密器的 ProcessData()
。我们不再需要担心缓冲和填充。
示例 3 在 CFB 模式下使用 AES。因为我们使用的是 CFB 模式,所以加密器和解密器将需要初始化向量。与 ECB 和 CBC 模式不同,密文大小与明文大小相同,因为没有发生填充。由于我们现在需要初始化向量,因此此示例中发生的更改在加密器的构造函数中:
// Encryptor
CFB_Mode< AES >::Encryption
Encryptor( key, sizeof(key), iv);
// Encryption
StringSource(
PlainText,
true,
new StreamTransformationFilter(
Encryptor,
new StringSink( CipherText )
) // StreamTransformationFilter
); // StringSource
模板模式对象的最后一个示例,示例 4,由 Jason Smethers 提供。它允许我们在密码和工作模式之间快速跳转,同时将密码包装在 StreamTransformationFilter
中。
#define CIPHER_MODE CFB_Mode
...
#define CIPHER Twofish
...
// Encryptor
CIPHER_MODE< CIPHER >::Encryption
Encryptor( key, sizeof(key), iv );
以下是四次示例运行的结果,指定了不同的密码/模式组合。Crypto++ Blowfish 对象声明其最小密钥大小为 1 字节,尽管 Schneier 声称该算法可以使用从 (0 到 448) 位开始的密钥。这是由于 Crypto++ 在源代码中的模数约简:0 % 密钥长度将产生未定义的结果。我们会偶尔发现这样的小错误。
杂项示例
CTS - 密文窃取
CTS 示例(示例 5)是为了测试密文窃取的特殊情况。这是一个特殊情况,因为我们必须注意没有可以窃取的先前块的情况。当消息长度小于块大小时就会发生这种情况。测试 CTS 代码时,请特别注意消息长度小于块大小的情况,因为 Crypto++ 将在 MessageEnd()
处抛出异常。
string plain = "Too Small";
string cipher, recovered;
// Encryption
ThreeWayEncryption twe( key, ThreeWay::DEFAULT_KEYLENGTH );
CBC_CTS_Mode_ExternalCipher::Encryption encryptor( twe, iv );
StreamTransformationFilter stfe( encryptor, new StringSink( cipher ));
stfe.Put( (byte*)plain.c_str(), plain.size() );
stfe.MessageEnd(); // Be Careful Here
...
// Decryption
ThreeWayDecryption twd( key, ThreeWay::DEFAULT_KEYLENGTH );
CBC_CTS_Mode_ExternalCipher::Decryption decryptor( twd, iv );
StreamTransformationFilter stfd( decryptor, new StringSink( recovered ));
stfd.Put( (byte*)cipher.c_str(), cipher.size() );
stfd.MessageEnd();
为简便起见,本示例使用 StreamTransformationFilter
(没有像第二个和第三个示例那样使用 StringSource
)。这是使用过滤器的另一种方法。在前面的示例中,StringSource
内部调用 Put()
和 MessageEnd()
。
CTR - 计数器模式
示例 10 练习 Crypto++ 的计数器模式。该程序加密大于底层密码块大小的随机值。这迫使库调用 IncrementCounterByOne()
。加密操作返回后,示例会检查下一个将要使用的计数器值。
块大小和密钥大小
示例 6 演示了创建 Crypto++ 分组密码数组,然后检索它们的名称、块大小和密钥长度。程序的输出可在表 2 中找到。下面的代码演示了数组创建:
BlockCipher* Ciphers[] =
{
new AES::Encryption(),
new Blowfish::Encryption(),
new BTEA::Encryption(),
new Camellia::Encryption(),
...
};
填充数组后,我们可以查询数组中的每个密码以获取其各种值。BlockCipher::IVSize()
在此程序中始终返回 0,因为密码对象尚未与模式对象配对。这不成问题,因为我们知道 IV 大小等于密码的块大小。
for(int I = 0; I < COUNTOF(Ciphers); i++ )
{
cout << Ciphers[i]->AlgorithmName();
cout << Ciphers[i]->BlockSize();
cout << Ciphers[i]->DefaultKeySize();
...
}
密文大小
示例 9 是明文大小与密文大小的比较。表 3 显示了 12 字节明文数据的结果。此示例使用相同的 BlockCipher
数组。对于数组中的每个密码,都会执行一次加密以生成统计数据。从示例 5(表 2)中我们知道,我们需要提供的最长默认密钥和初始化向量大小为 32 字节。
const int BYTECOUNT = 32;
byte key[ BYTECOUNT ], iv[ BYTECOUNT ];
memset( key, 0x00, BYTECOUNT );
memset( iv, 0x00, BYTECOUNT );
for(int i = 0; i < COUNTOF( Ciphers ); i++ )
{
cout << "Algorithm: " << Ciphers[i]->AlgorithmName() << endl;
ECB_Mode_ExternalCipher::Encryption ecb( *(Ciphers[i]) );
StreamTransformationFilter stf( ecb, new StringSink( cipher ) );
stf.Put( (byte*)data.c_str(), data.length() );
stf.MessageEnd();
cout << "Ciphertext Size: " << cipher.size() << endl;
...
}
对于需要初始化向量的密码,我们执行以下操作。不幸的是,我们不能使用加密对象的基类指针,因为基类是模板化的。
CBC_Mode_ExternalCipher::Encryption cbc( *(Ciphers[i]), iv );
StreamTransformationFilter stf( cbc, new StringSink( cipher ) );
stf.Put( (byte*)data.c_str(), data.length() );
stf.MessageEnd();
使用 vector<byte>
Crypto++ 邮件列表偶尔会收到关于在 vector<byte>
之间移动数据的询问。只要底层数组是连续的,下面的示例就是有效的。ISO 14882,第 23.2.4 节要求所有类型(布尔值除外)都这样做。另请注意,底层向量不使用安全内存分配。它可在示例 8 中下载。
string plain = "Hello World";
string cipher, recovered;
vector<byte> v;
// Encryption
AESEncryption aese( key, AES::DEFAULT_KEYLENGTH );
CBC_Mode_ExternalCipher::Encryption encryptor( aese, iv );
StreamTransformationFilter stfaese( encryptor, new StringSink(scratch));
stfaese.Put( (byte*)plain.c_str(), plain.size() );
stfaese.MessageEnd();
// Vectorize
v.resize( cipher.size() );
StringSource( cipher, true, new ArraySink( &(v[0]), v.size() ) );
// Decryption
AESDecryption aesd( key, AES::DEFAULT_KEYLENGTH );
CBC_Mode_ExternalCipher::Decryption decryptor( aesd, iv );
StreamTransformationFilter stfaesd( decryptor, new StringSink( recovered ));
stfaesd.Put( &(v[0]), v.size() );
stfaesd.MessageEnd();
转换
在使用外部密码对象(<模式>_Mode_ExternalCipher
)时,我们必须了解密码的转换函数、密钥调度以及由于模式选择而导致的轮次之间的交互。例如,DES 解密操作以正向运行密码,同时反转密钥调度。在这种情况下,解密被设计为加密函数的一个变体,并且(有时)设计动机是成本(硬件节省或更小的代码占位符)。ECB、CBC 和 CTS 允许我们按预期使用库。
// Encryption
AESEncryption aese( key, AES::DEFAULT_KEYLENGTH );
CBC_CTS_Mode_ExternalCipher::Encryption encryptor( aese, iv );
...
// Decryption
AESDecryption aesd( key, AES::DEFAULT_KEYLENGTH );
CBC_CTS_Mode_ExternalCipher::Decryption decryptor( aesd, iv );
但是,在使用 OFB、CTR 或 CFB 模式时,我们必须使用加密对象(对于 AES,是 AESEncryption
)进行解密。这是因为解密受转换方向和密钥调度的影响。
// Encryption
AESEncryption aese( key, AES::DEFAULT_KEYLENGTH );
CTR_Mode_ExternalCipher::Encryption encryptor( aese, iv );
...
// Decryption
AESEncryption aesd( key, AES::DEFAULT_KEYLENGTH );
CTR_Mode_ExternalCipher::Decryption decryptor( aesd, iv );
如果我们错误地退出加密过程,我们将收到一个 Debug 断言,提示:“Assertion failed: m_cipher->IsForwardTransformation(), file c:\crypto++\modes.cpp”。示例 9 用于演示这种情况。这引出了表 1,它告诉我们应该使用哪个对象,因为转换方向和模式交互是影响因素。
操作/模式 |
ECB |
CBC |
OFB |
CFB |
CTR |
CTS |
加密 |
加密 |
加密 |
加密 |
加密 |
加密 |
加密 |
解密 |
解密 |
解密 |
加密 |
加密 |
加密 |
解密 |
带 MAC 的加密器
将对称密码与 MAC 结合使用可以提供保密性和完整性。Crypto++ 在 default.h 中为我们提供了 DefaultEncryptorWithMAC
和 DefaultDecryptorWithMAC
。从 default.h 中提供的 typedef
s 中,Default[En/De]cryptorWithMAC
类使用三重 DES(类 DES_EDE2
)作为 CBC 模式下的分组密码,并使用 SHA 作为哈希。该类使用起来很简单(包含在示例 11 中)。
string message = "secret message";
string password = "password";
string encrypted, recovered;
StringSource(
message,
true,
new DefaultEncryptorWithMAC(
password.c_str(),
new StringSink( encrypted )
) // DefaultEncryptorWithMAC
); // StringSource
StringSource(
encrypted,
true,
new DefaultDecryptorWithMAC(
password.c_str(),
new StringSink( recovered )
) // DefaultDecryptorWithMAC
); // StringSource
cout << "Recovered Text:" << endl;
cout << " " << recovered << endl;
Default[En/De]cryptorWithMAC
提供两个构造函数,因此如果我们想指定一个 byte[]
作为密码短语和长度,我们可以这样做。在这种情况下,我们对 StringSource
的使用将如下所示:
byte password[] = 0x01, 0x02, 0x03, 0x05, 0x07, 0x11, 0x13;
StringSource(
message,
true,
new DefaultEncryptorWithMAC(
password,
sizeof(password),
new StringSink( encrypted )
) // DefaultEncryptorWithMAC
); // StringSource
如果我们只需要一个 AESEncryptorWithMAC
,我们可以做以下两件事之一:
- 将
Default_BlockCipher
的typedef
从DES_EDE2
更改为AES
- 从
ProxyFilter
(Default[En/De]cryptor
源自ProxyFilter
)派生一个新类(AESEncryptor
和AESDecryptor
),并更改默认分组密码。然后,创建第二对类AES[En/De]cryptorWithMAC
,它们使用AESEncryptor
或AESDecryptor
作为分组密码。
第二种方法的实现相当直接,因为 Wei 提供了 DefaultEncryptor
、DefaultDecryptor
、DefaultEncryptorWithMAC
和 DefaultDecryptorWithMAC
的源代码。最后,对于那些有兴趣在加密文件时使用 MAC 的人,请参阅test.cpp,函数 EncryptFile()
和 DecryptFile()
。
表格
编译以下表格是为了让读者能够比较对称密码。示例 5 用于生成表 2,示例 6 用于生成表 3。在表 3 中,连字符表示没有足够的明文可以用于 CTS(密文窃取)模式。
块大小和密钥大小
密码 |
块大小 |
密钥长度 |
||
默认值 | 最低 | 最大 | ||
AES | 16 | 16 | 16 | 32 |
Blowfish | 8 | 16 | 0 | 56 |
Camellia | 16 | 16 | 16 | 32 |
CAST-128 | 8 | 16 | 5 | 16 |
CAST-256 | 16 | 16 | 16 | 32 |
DES | 8 | 8 | 8 | 8 |
DES-EDE2 | 8 | 16 | 16 | 16 |
DES-EDE3 | 8 | 24 | 24 | 24 |
DES-XEX3 | 8 | 24 | 24 | 24 |
GOST | 8 | 32 | 32 | 32 |
IDEA | 8 | 16 | 16 | 16 |
MARS | 16 | 16 | 16 | 56 |
RC2 | 8 | 16 | 1 | 128 |
RC5 | 8 | 16 | 0 | 255 |
RC6 | 16 | 16 | 0 | 255 |
SAFER-K | 8 | 16 | 8 | 16 |
SAFER-SK | 8 | 16 | 8 | 16 |
Serpent | 16 | 16 | 1 | 32 |
SHACAL-2 | 32 | 16 | 1 | 64 |
SHARK-E | 8 | 16 | 1 | 16 |
SKIPJACK | 8 | 10 | 1 | 10 |
3-Way | 12 | 12 | 1 | 12 |
Twofish | 16 | 16 | 0 | 32 |
XTEA | 8 | 16 | 1 | 16 |
明文与密文大小
密码 |
块 |
密文大小 |
|||||
ECB | CBC | OFB | CTR | CFB | CTS | ||
AES |
16 |
16 |
16 |
12 |
12 |
12 |
- |
Blowfish |
8 |
16 |
16 |
12 |
12 |
12 |
12 |
Camellia |
16 |
16 |
16 |
12 |
12 |
12 |
- |
CAST-128 |
8 |
16 |
16 |
12 |
12 |
12 |
12 |
CAST-256 |
16 |
16 |
16 |
12 |
12 |
12 |
- |
DES |
8 |
16 |
16 |
12 |
12 |
12 |
12 |
DES-EDE2 |
8 |
16 |
16 |
12 |
12 |
12 |
12 |
DES-EDE3 |
8 |
16 |
16 |
12 |
12 |
12 |
12 |
DES-XEX3 |
8 |
16 |
16 |
12 |
12 |
12 |
12 |
GOST |
8 |
16 |
16 |
12 |
12 |
12 |
12 |
IDEA |
8 |
16 |
16 |
12 |
12 |
12 |
12 |
MARS |
16 |
16 |
16 |
12 |
12 |
12 |
- |
RC2 |
8 |
16 |
16 |
12 |
12 |
12 |
12 |
RC5 |
8 |
16 |
16 |
12 |
12 |
12 |
12 |
RC6 |
16 |
16 |
16 |
12 |
12 |
12 |
- |
SAFER-K |
8 |
16 |
16 |
12 |
12 |
12 |
12 |
SAFER-SK |
8 |
16 |
16 |
12 |
12 |
12 |
12 |
Serpent |
16 |
16 |
16 |
12 |
12 |
12 |
- |
SHACAL-2 |
32 |
32 |
32 |
12 |
12 |
12 |
- |
SHARK-E |
8 |
16 |
16 |
12 |
12 |
12 |
12 |
SKIPJACK |
8 |
16 |
16 |
12 |
12 |
12 |
12 |
3-Way |
12 |
24 |
24 |
12 |
12 |
12 |
- |
Twofish |
16 |
16 |
16 |
12 |
12 |
12 |
- |
XTEA |
8 |
16 |
16 |
12 |
12 |
12 |
12 |
下载次数
- 下载源代码 - 示例 1 - 3.69 KB
- 下载源代码 - 示例 2 - 3.69 KB
- 下载源代码 - 示例 3 - 3.67 KB
- 下载源代码 - 示例 4 - 5.78 KB
- 下载源代码 - 示例 5 - 3.6 KB
- 下载源代码 - 示例 6 - 5.27 KB
- 下载源代码 - 示例 7 - 5.87 KB
- 下载源代码 - 示例 8 - 3.67 KB
- 下载源代码 - 示例 9 - 3.82 KB
- 下载源代码 - 示例 10 - 3.92 KB
- 下载源代码 - 示例 11 - 3.45 KB
致谢
- Wei Dai 贡献的 Crypto++,以及他在 Crypto++ 邮件列表上的宝贵帮助
- A. Brooke Stephens 博士,他为我打下了密码学基础
- Jason Smethers 提供示例代码
- Garth Lancaster 提供的关于完整性的建议
- Geoff Beier 在 Crypto++ 和 OpenSSL 方面的专业知识
- Stephen Schultz 在 mcrypt、XySSL 和基于 mcrypt 的 PHP/Python 方面的专业知识
- Colin Bell 解决了 Crypto++ BTEA 问题并提供了变通方法
修订
- 2008年04月06日 添加了 BTEA 信息
- 2008年07月03日 添加了测试向量部分
- 2008年07月03日 添加了 CLR 的
TripleDESCryptoServiceProvider
信息 - 2008年05月03日 添加了 Lucifer 专利信息
- 2008年02月28日 添加了“实际区别”子部分
- 2008年02月27日 添加了带 MAC 的加密器
- 2008年02月27日 添加了示例 11
- 2008年02月22日 在 MAC 部分添加了附加信息
- 2008年02月20日 添加了 FIPS、ANSI 和 ISO/IEC 参考文献
- 2008年02月13日 添加了 CTR 模式
- 2008年02月13日 添加了示例 10
- 2008年02月13日 修改了示例 1
- 2008年02月12日 添加了 StreamTransformationFilter 部分
- 2008年02月10日 添加了模式反馈大小和互操作性
- 2008年02月05日 添加了消息填充和互操作性
- 2008年02月04日 添加了轮次转换信息
- 2007年12月06日 添加了示例 8 和 9
- 2007年12月05日 首次发布
校验和
- Sample1.zip
MD5: 8FFDDDC6B2BDB69F66BE3647B5102C4C
SHA-1: 96F92D4B82798A92DF58B84CA258EE1E5E66923A - Sample2.zip
MD5: C3ABD80F4082CB6FE7A6327CDCDC04E1
SHA-1: 378135B0B1FF55D9AF51E3EBFB510315CD1DD925 - Sample3.zip
MD5: FA3AC7A62BFFA5CD68B07FCA9D5E1D18
SHA-1: 8E24795FC5FB1DB7C99479B4B6DD270DB9E84913 - Sample4.zip
MD5: 5F6388B7965D557FB6F3A0DF79DB13B9
SHA-1: ED3F2632257DE0207C5B6764F052BF15385E7370 - Sample5.zip
MD5: E04592E195963805FADAF6AF31B272C3
SHA-1: F3338BBA10514923DAA5ED45A4EE79369C45353A - Sample6.zip
MD5: B370AC54D4DA4E776872ED1D56870A4F
SHA-1: 9C9EDBDD17138A0855AF98E58BA4C804A5C89E0D - Sample7.zip
MD5: D3BB63B47FCED48A737D96CDABDB4CA3
SHA-1: B883805F50E892C4228675E21AEE2D62479A812E - Sample8.zip
MD5: 3356A2E2411C217493D6A807E18DA29E
SHA-1: B5D2A28367B3CD861995E0D0E4AD8ABBF6A102AB - Sample9.zip
MD5: 4705CA41248D78A2523ACB267A71FA9F
SHA-1: 8B9C30E311F891EF2DCCA2586313CEB43848B3CD - Sample10.zip
MD5: 82EA0F9E9DDED57C64B73C52CA207C8B
SHA-1: 4F2ACA7B999F745627332B475EF9B57043EA3732 - Sample11.zip
MD5: D2B44A14A0B4AEE0DC762C1715851DBE
SHA-1: B01A05FCC384D2AB833A6B5D12D30FE886A98681