C# CBC 流密码(带有 C# 和 C 中两个开源 AES 实现的包装器)






4.54/5 (18投票s)
2004 年 6 月 28 日
12分钟阅读

156258

3694
.NET 加密文章
引言:.NET 加密
.NET 在 `System.Security` 命名空间中提供了非常好的加密支持 [NETC]。要理解 .NET 加密实现的工作原理,您需要知道大多数接口和实现都是 Microsoft Crypto API 的封装 [CAPI]。Crypto API 的设计相当不错,但它缺少一个重要功能:它不透明 [CGNL]。这意味着您必须高度信任一个您不知道其内部机制的东西。Crypto API 的另一个问题是其默认提供商处理密钥的方式。密钥被加密存储在本地计算机的数据库中。同样,您别无选择,只能信任它。出于这些原因,开源加密库总能在 .NET 中找到用武之地。其中最大的库是 Mono [MONO] 的一部分,它是一个开源重写的 .NET `System.Security.Cryptography` 命名空间。尽管它可用,但非 Mono .NET 用户将在 .NET 应用程序中使用该库时遇到问题。它集成在 Mono 的 corelib.dll 中,您必须导入大量不必要的代码才能使用简单的算法,更不用说命名空间冲突了。因此,其他更简单的开源实现将在 .NET 世界中大受欢迎。
在为 .NET 实现加密提供程序时,最佳的组织方式是实现 `System.Security.Cryptography` 的接口,这样您的提供程序就可以与任何使用 .NET 接口的现有代码统一工作,用户无需学习新的 API。然而,这种完美的软件工程解决方案存在一些安全隐患。鉴于 .NET 接口是众所周知的,任何第三方都更容易拦截对通用接口的调用。由于 .NET 富含元数据的特性,这比在普通二进制文件中这样做更容易。众所周知的接口使得编写可与许多产品通用的间谍软件成为可能。因此,尽管加密提供程序质量上乘且透明,但您永远无法确定在 .NET 框架部署或底层 Crypto API 中没有第三方程序可以收集密钥。一个部分解决方案,从软件工程角度看并非优化,是使用自定义接口。情况类似于使用 Outlook Express 查看电子邮件。由于许多人使用它,一个为 Outlook Express 编写的病毒很可能会击中大量用户。如果您使用自定义电子邮件客户端,没有人会费心为少数用户使用的程序编写病毒(除非有人真的恨您)。本着这种精神,这里的 CBC 流密码使用了自己的简单接口,不依赖于任何 .NET 加密 API 接口和类。实际上,它仅使用 `System` 和 `System.IO` 进行流操作。
CBC 流密码库
一切始于我在 MSDN 杂志 [CSAES] 中找到一个 C# 实现的 AES [FIPS197]。AES 是一种对称分组密码算法,意味着加密和解密使用相同的密钥。CSAES [CSAES] 的 C# 代码易于理解,但不知何故,该实现非常慢,即使我用固定表替换了多项式乘法。可以在 [CAES] 中找到一个免费提供的更快的 C 语言实现的 AES。
要为实际加密使用 AES 分组密码实现,您必须创建一个流密码。最简单的方法是创建一个 ECB(电子密码本)流密码,它基本上使用分组密码加密流的每个块。我的实现也支持 ECB 模式,因为它可能对测试有用。然而,它不安全,因为明文中的相关模式仍然会在密文中可见。CBC(密码块链接)模式可以避免这种弱点,并且相对容易实现。它基本上在加密每个明文块之前,将其与前一个密文块进行 XOR 操作。这样,明文模式在输出中就不再可见。CBC 模式使用初始化向量 (IV) 来 XOR 第一个块。IV 不需要保密,因为它不会透露有关数据(明文或密文)或密钥的任何信息。对于使用给定密钥加密的每个流(文件),IV 必须与当前 IV 不同(不一定随机)。在此提供的 CBC 实现中,IV 大小必须与分组密码的大小相同,AES 始终为 16 字节。CBC 模式是我的流密码的默认模式。
只要有一个封装类实现了下面的 `IBlockCipher` 接口,就可以将任何分组密码实现与该库一起使用。interface IBlockCipher {
void InitCipher(byte[] key); // key.Length is keysize in bytes
void Cipher(byte[] inb, byte[] outb);
void InvCipher(byte[] inb, byte[] outb);
int[] KeySizesInBytes();
// iv length will/must be the same as BlockSizeInBytes
int BlockSizeInBytes();
}
我编写了两个实现了该接口的封装:一个是 C# AES 实现 [CSAES],另一个是快速的 C 语言 AES DLL 实现 [CAES]。这样,这两种算法就可以与我的 CBC 流密码一起使用。请参阅代码中的 'aes.CAes.cs' 文件,了解封装的外观示例。
使用代码
C# 流密码库可以如下使用:
IBlockCipher ibc = new CAes(); // new Aes();
byte[] key = ... ;
byte[] iv = ... ;
StreamCtx ctx = StreamCipher.MakeStreamCtx(ibc, key, iv);
可以通过调用 `ibc.BlockSizeInBytes()` 来获取 `iv` 数组大小,对于 AES,它返回 16。可以通过调用 `ibc.KeySizesInBytes()` 来获取 `key` 数组大小,对于 AES,它返回 {16, 24, 32},分别对应 128、192 和 256 位。必须选择其中一个值。未来对 `IBlockCipher` 的一个改进是返回一个密钥大小步长,就像 .NET 所做的那样。
请注意,`StreamCipher.MakeStreamCtx` 返回的 `StreamCtx` 对象是只读的,您必须始终使用 `StreamCipher.MakeStreamCtx` 来获取有效的上下文。一旦您有了流上下文 `StreamCtx ctx`,就可以使用 StreamCipher 类的其他静态方法对其进行加密或解密流。
Stream instr = ... ; // open plaintext stream
Stream outstr = ... ; // create a stream for ciphertext output
StreamCipher.Encrypt(ctx, instr, outstr);
建议在将流传递给 `StreamCipher` 方法之前对其进行缓冲。要加密字节数组,您可以使用:
byte[] indata = ... ;
byte[] outdata = StreamCipher.Encode(ctx, indata, StreamCipher.ENCRYPT);
解密以类似方式使用相应的方法完成。请参阅 'aes-example.cs' 文件以获取完整示例。
密钥生成
我们上面忽略的一个问题是如何获取密钥。对于 AES,任何大小等于 `IBlockCipher.KeySizesInBytes()` 数组中某个值返回的字节数组都可以。其他算法可能有弱密钥值集,因此 IBlockCipher 的未来改进将是添加一个 `byte[] GenerateKey()` 方法,该方法将由分组密码封装实现。
另一方面,没有人会记住一个 16 或 32 字节的密钥,例如 8ea2b7ca516745bfeafc49904b496089,而不将其写下来。然而,大多数人会发现物理存储密钥(即使是加密的)并非一个可行的解决方案。他们宁愿选择一种更容易记住的密钥形式,通常是以字符串密码(或*口令*)的形式。
基于密码的加密 [PBCS] 比直接使用密钥要弱,因为密码的搜索空间比密钥小。例如,对于 AES 中的 256 位密钥,最坏情况下有 2^256(*读作:2 的 256 次方*)种可能性可以搜索。键盘包含大约 2^6 个唯一的按键(字母、大写字母、数字、特殊字符),可用于密码。这意味着对于一个真正随机的 20 个字符长的密码(我们觉得太复杂而无法记住),暴力搜索空间在最坏情况下仅为 (2^6)^20 = 2^120。这甚至低于 AES 的最小密钥大小 2^128。因此,如果您使用带有 20 个字符密码的密码加密方案,那么对于 AES 使用大于 128 位的密钥就没有意义了。实际上,密码的长度是未知的,这意味着对于最多 20 个字符的密码,搜索空间大小可以计算如下:*sum(for i = 0 to 20) of (2^6)^i*,这仍然与最大项值大致相同,即 2^120。
密码的另一个问题是人们倾向于重复使用它们。也就是说,我们宁愿(重新)使用同一个密码来加密不同的数据单元(文件)。这意味着我们使用相同的密钥来加密大量明文,从而产生大量密文,理论上可以用来探索模式并找到密码(而不是密码)并获得明文。因此,最好使用不同的密钥来加密不同的数据单元,这似乎与拥有一个单一的可重用密码的想法相悖。
在不改变或延长密码的情况下,有两种方法可以改善这种情况(如 [PBCS] 中所述)。第一种方法是在每次使用密码生成密钥时在密码末尾添加几个不同的字节,并为要加密的不同数据单元使用不同的字节。这些字节被称为“*salt*”。salt 字节必须是随机的,或者至少对于我们加密的每个数据流都是不同的。这样,我们每次都会有一个基于同一根密码的新密钥用于加密。要解密数据,除了密码之外,我们必须知道用于加密它的原始 salt。好消息是 salt 不需要保密:如果您要保密 salt,那么 (a) 完全不要使用 salt,而是每次使用不同的密码/密钥;(b) 您必须记住它,然后您处于与记住加密密钥本身相同的位置。因此,虽然 salt 不会扩大密码搜索空间,但它会增加您用于密文的有效密钥空间,使得拥有您所有密文和所有相应 salt 的人无法找到密钥。
第二种技术有效地增加了密码的搜索空间,而没有增加密码的大小。它不仅考虑了搜索空间的大小,还考虑了进行一次尝试所需的时间。当我们说随机的 20 个字符长密码的搜索空间约为 2^120,小于 2^128(最小 AES 密钥大小的空间)时,我们假设测试密码所需的时间与测试密钥所需的时间相同。我们可以通过增加从密码生成密钥所需的时间来改善这种情况。当然,这不能通过在代码中添加延迟来实现,而是通过增加所需的计算量来实现。在 [PBCS] 中,这由所谓的*迭代次数*控制。迭代次数为 2^10 将使有效时间相对于迭代次数为 1 的情况增长约 2^120 * 2^10 = 2^130 个时间单位。更大的值当然更好,但以目前的计算能力,密钥生成速度会过慢。因此,如果您知道密码,您将在大约 2^10 个时间单位内计算出密钥。如果您不知道密码,尝试所有最多 20 个字符的密码的最坏情况将需要大约 2^130 个时间单位。
这两种改进使得使用密码(即使您不更改它们)在安全性上与使用不同的密钥相当。我的库中的 `KeyGen` 类实现了 [PBCS] 中描述的第一种方法(该方法更容易实现,也相对安全)。可以如下从 `string password` 生成密钥:
int keySize = 32;
byte[] salt = ...;
byte[] key = KeyGen.DeriveKey(password, keySize, salt); //iteration count 1024
DeriveKey
使用免费的 C# SHA256 实现来哈希数据。它可以生成最多 32 字节(256 位)长的密钥。
我实现的流密码库没有提供创建 salt 或 IV 的方法,因为不同的应用程序可能使用不同的方法来生成和分发它们。获取给定长度的 IV 和 salt 字节数组的一种简单方法是使用伪随机字节生成器。
在结束之前,我将解释上面计算中的“*时间*”是什么意思。像 2^120 这样的数字意味着,如果一台计算机每秒进行 100 次猜测,那么在最坏的情况下,它需要大约 421495432453359929256661295 年才能完成。然而,如果您拥有 421495432453359929256661295 台计算机,您只需要一秒钟。如果您使用隐藏摄像头或键盘记录器在某人输入密码时窃取密码(等等,等等),您根本不需要那么多计算机。
历史
- 2004 年 7 月 1 日
- 修正了文章中的一些拼写错误。:)
- 更改了 StreamCipher.cs 以使填充与 .NET 的 `SymmetricAlgorithm` 实现(CryptoAPI)兼容。现在可以使用我的库(使用 AES 封装)解密用 .NET Rijndael 加密的数据,反之亦然。
注意: `KeyGen.DeriveKey` 也与 .NET `PasswordDeriveBytes` 兼容。这个调用string password = ...; int keyLength = 16; // up to 32 byte[] salt = ...; int iterationCount = ...; byte[] key = KeyGen.DeriveKey(password, keyLength, salt, iterationCount);
会从密码生成与以下 `System.Security.Cryptography` 代码相同的密钥PasswordDeriveBytes pdb = new PasswordDeriveBytes( password, salt, "SHA256", iterationCount); byte[] key = pdb.GetBytes(keyLength);
- 2004 年 7 月 2 日
- 修改 Decrypt 以在出错时静默失败。
- 删除了我忘记从代码中移除的一些 AES 特定常量。
致谢
Ulrike Meyer 向我解释了为什么 IV 不需要保密。我对 salt 和迭代次数的理解也源于与她就 [PBCS] 进行的一次精彩讨论。James McCaffrey 对我的流密码的第一个 ECB 版本提出的评论鼓励我完成了与标准兼容的 CBC 版本。参考文献
- [CAPI] V. Smyslov, RFC 2628 - Simple Cryptographic Program Interface (Crypto API), 1999, http://www.faqs.org/rfcs/rfc2628.html
- [FIPS197] AES: Federal Information Processing Standards Pub 197, http://csrc.nist.gov/publications/fips/fips197/fips-197.pdf
- [CSAES] James McCaffrey, Keep Your Data Secure with the New Advanced Encryption Standard, MSDN Magazine, 2003. http://msdn.microsoft.com/msdnmag/issues/03/11/AES/default.aspx
- [CAES] Brian Gladman, C AES Implemention, http://fp.gladman.plus.com/AES/index.htm
- [PBCS] B. Kaliski, RFC 2898 - PKCS #5: Password-Based Cryptography Specification Version 2.0, 2000, http://www.faqs.org/rfcs/rfc2898.html
- [CGNL] Bruce Schneier, Crypto-Gram Newsletter, September 15, 1999, http://www.schneier.com/crypto-gram-9909.html
- [MONO] Mono Crypto, http://www.go-mono.com/crypto.html
- [NETC] MSDN System.Security.Cryptography Namespace, 2004 http://msdn.microsoft.com/library/default.asp?url=/library/en-us/cpref/html/frlrfsystemsecuritycryptography.asp