65.9K
CodeProject 正在变化。 阅读更多。
Home

应用 Crypto++:块密码

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.85/5 (47投票s)

2007年12月6日

CPOL

30分钟阅读

viewsIcon

251893

downloadIcon

10904

使用 Crypto++ 的分组密码加密数据。

引言

Crypto++ 提供了超过 25 种 分组密码,从 AES 到 XTEA。现代分组密码需要选择算法、模式、反馈大小、填充,有时还需要轮数。本文将向读者展示如何使用 Crypto++ 的分组密码。本文将讨论的主题包括:

  • 背景
  • Crypto++
  • 分组密码
  • 流密码
  • 模板模式对象与外部密码对象
    • 模板模式对象
    • 外部密码对象
    • 实际区别
      • 获取块大小
      • 密文窃取错误
      • BTEA
  • StreamTransformationFilter
  • 测试向量
  • 消息填充
  • 工作模式
    • 反馈大小
    • 链接或反馈
    • 初始化向量
    • 密码块链接
    • 电子密码本
    • 输出反馈
    • 密码反馈
    • 计数器模式
    • 密文窃取
  • 消息认证码
  • 重复使用加密和解密对象
  • 使用分组密码
  • 杂项示例
    • CTS - 密文窃取
    • CTR - 计数器模式
    • 块大小和密钥大小
    • 密文大小
    • 使用 vector<byte>
    • 转换
    • 带 MAC 的加密器
  • 表格
    • 块大小和密钥大小
    • 明文与密文大小

背景

商业分组密码在 20 世纪 70 年代中期首次亮相。IBM 研究员 Walter TuchmanHorst 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 位。块如何加密在工作模式中有详细介绍。

Blocking Plain Text

图 1:分块明文

流密码

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 Data Flow

图 2:管道

如果我们可视化使用 StreamTransformationFilter 的系统框图,它将如图 3 所示。请注意,传递给 StreamTransformationFilterBufferedTransformation 参数是加密器对象。BufferedTransformation 是加密器和解密器的基类。

StreamTransformationFilter Data Flow

图 3:StreamTransformationFilter 数据流

测试向量

所有密码都提供测试向量来测试实现。如果我们发现 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_PADDINGZEROS_PADDINGPKCS_PADDINGONE_AND_ZEROS_PADDINGDEFAULT_PADDINGONE_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 中可知,反馈大小必须小于或等于块大小。下面显示了来自 CipherModeBaseCFB_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 中,被馈送到下一轮的输出是密文,但这并不总是如此。

Block Chaining

图 4:块链接

初始化向量

上述方法有一个缺点:第一轮只能使用密钥,因为没有来自前一轮的反馈。这在图 5 中显示。

Initial State

图 5:初始状态

通过使用初始化向量来克服这个问题。因为初始化向量引导链接过程,所以它的大小将与输出块大小相同。初始化向量的添加如图 6 所示。

Initialization Vector

图 6:初始化向量

初始化向量通常不必是秘密的,但它不应该与同一个密钥重复使用。重复使用的 IV 要么会泄露明文信息(CBC 和 CFB 模式),要么会破坏系统的安全性(OFB 和 CTR 模式)。

初始化向量的要求可以从 IVRequirement() 返回的 IV_REQUIREMENT 类型确定。值将是 UNIQUE_IVRANDOM_IVUNPREDICTABLE_RANDOM_IVINTERNALLY_GENERATED_IVNOT_RESYNCHRONIZABLE。这些值是枚举,而不是位掩码编译。

电子密码本

电子密码本 (ECB) 是最简单的方法。消息被分解成块,每个块与密钥结合。没有块链接或反馈。通常不建议使用 ECB 模式,因为它会泄露信息。

ECB Mode

图 7:ECB 模式

图 8 显示了由于 ECB 模式中明文字节的重复而导致的密文字节重复。另一个常见示例是加密 Linux 内核的吉祥物 Tux(请参阅 Wiki 上的电子密码本 (ECB))。

ECB Mode

图 8:ECB 模式输出

密码块链接

图 6 有点过于笼统。为了描述密码块链接 (CBC),我们将执行以下操作。在图 9 中,IV 和明文块在使用 XOR 组合后再输入加密操作。然后密文被馈送到下一组轮次,取代初始化向量。

CBC Mode

图 9:CBC 模式

密码反馈

图 10 描绘了密码反馈 (CFB) 模式。请注意,在图 10 中,我们加密了密钥和初始化向量。然后,输出与明文进行 XOR 操作以产生密文。然后密文被馈送到下一组轮次,以代替初始化向量。

CFB Mode

图 10:CBC 模式

输出反馈

图 11 显示了输出反馈 (OFB) 模式。在此模式下,加密的输出在最终 XOR 之前被抽取。抽取出的输出被馈送到下一组轮次,而最终的 XOR 操作产生密文。因为输出在最终 XOR 之前被抽取,所以密钥流可以在不需要明文或密文的情况下预先计算。明文或密文都不用于反馈机制。

OFB Mode

图 11:OFB 模式

计数器模式

计数器模式 (CTR) 类似于 ECB 和 OFB 模式。CTR 使用一个运行的计数器块来代替初始化向量/反馈机制。它类似于 ECB 模式,因为每个明文块都是独立加密的,而不是通过前一组轮次的结果。它类似于 OFB 模式,因为密钥和计数器被加密,然后结果与明文进行 XOR 操作。视觉表示如图 12 所示。

OFB Mode

图 12:CTR 模式

与初始化向量一样,计数器的大小是密码的块大小。对于默认 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) 是一种技术,它不将最后一个块填充到密码的块大小。窃取密文时,执行以下三个步骤;窃取发生在第二步:

Cipher Text Stealing

原始明文1 和明文2

Cipher Text Stealing 用明文1 的低位比特填充明文2
Cipher Text Stealing

交换明文1 和明文2

Cipher Text Stealing 截断明文1

要使用 CTS 模式,明文必须大于块大小。因此,如果我们使用 DES(块大小为 8 字节),我们可以对 12 字节的明文使用 CTS 模式。如果只有 7 字节的明文,我们将无法使用 DES(或者必须填充明文)。我们不能选择 TwoFish(块大小为 16 字节),因为没有可以窃取的先前块。如果明文有 17 字节,TwoFish 将是一个可行的选择。

CTS 模式的输出在示例 5 中有所展示。在文章末尾,表 3 使用连字符表示对于 CTS 来说太短的明文长度。

消息认证码

MAC 是在 20 世纪 70 年代为金融行业开发的。ANSI X9.9 将 MAC 描述为 8 位十六进制数字,这是通过使用特定密钥将金融消息通过认证算法得到的结果。

工作模式允许我们基于共享密钥加密数据。它解决了保密性认证性的问题。分组密码还可以用于解决完整性问题。这是确定数据在磁盘或传输过程中是否被篡改的问题。要使用分组密码解决这个问题,我们需要重新审视块链接。

我们已经看到,要引导链接过程,我们必须提供一个初始化向量。然后密码会处理数据,分块直到所有数据都被消耗。数据被消耗后,我们会得到一个额外的输出块,该块会被馈送到下一组轮次,因为下一组轮次没有明文。这会产生一个唯一的残差。

Unique Residue

图 13:唯一残差

如果我们结合残差和密钥(图 14),我们就得到了一个消息认证码或 MAC。MAC 也称为密钥散列。这样使用时,MAC 是数字签名的对称密码等效项,但缺少不可否认性。我们丢失了不可否认性,因为密钥是与他人共享的。

Message Authentication Code

图 14: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 % 密钥长度将产生未定义的结果。我们会偶尔发现这样的小错误。

图 15:各种示例 4 执行的结果

杂项示例

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 );

Crypto++ Assertion

如果我们错误地退出加密过程,我们将收到一个 Debug 断言,提示:“Assertion failed: m_cipher->IsForwardTransformation(), file c:\crypto++\modes.cpp”。示例 9 用于演示这种情况。这引出了表 1,它告诉我们应该使用哪个对象,因为转换方向和模式交互是影响因素。

操作/模式

ECB

CBC

OFB

CFB

CTR

CTS

加密

加密

加密

加密

加密

加密

加密

解密

解密

解密

加密

加密

加密

解密

表 1:操作/模式/对象要求

带 MAC 的加密器

将对称密码与 MAC 结合使用可以提供保密性和完整性。Crypto++ 在 default.h 中为我们提供了 DefaultEncryptorWithMACDefaultDecryptorWithMAC。从 default.h 中提供的 typedefs 中,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_BlockCiphertypedefDES_EDE2 更改为 AES
  • ProxyFilterDefault[En/De]cryptor 源自 ProxyFilter)派生一个新类(AESEncryptorAESDecryptor),并更改默认分组密码。然后,创建第二对类 AES[En/De]cryptorWithMAC,它们使用 AESEncryptorAESDecryptor 作为分组密码。

第二种方法的实现相当直接,因为 Wei 提供了 DefaultEncryptorDefaultDecryptorDefaultEncryptorWithMACDefaultDecryptorWithMAC 的源代码。最后,对于那些有兴趣在加密文件时使用 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
表 2:对称密码块和密钥大小

明文与密文大小

密码


大小

密文大小

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

表 3:明文大小(12 字节)与密文大小

下载次数

致谢

  • 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

© . All rights reserved.