一、c#中的对称加密概述
对称加密算法在加密和解密时使用相同的密钥。Framework提供了四种对称加密算法。AES、DES、Rijndael、RC2。
DES:全称为Data Encryption Standard,即数据加密标准,是一种使用密钥加密的块算法,1977年被美国联邦*的国家标准局确定为联邦资料处理标准(FIPS),并授权在非密级*通信中使用,随后该算法在国际上广泛流传开来。
RC2:RC2是由著名密码学家Ron Rivest设计的一种传统对称分组加密算法,它可作为DES算法的建议替代算法。
Rijndael:这个类从.NET Framework 1.0开始就已经存在了。
Aes:这个类是在.NET Framework 3.5中引入的。
这些算法中Rijndael算法是最优秀的。Rijndael即快速又安全。它拥有两个实现:
Rijndael和Aes这两个实现几乎是等价的,但是Aes不允许通过更改块尺寸来削弱加密强度。CLR安全团队推荐使用Aes类。
Rijndael和Aes支持16字节、24字节和32字节的对称密钥长度:这几种长度目前均认为是安全的。
关于AES加密算法的细节,可参考下面这篇文章。
二、加解密示例
//加密
byte[] key = { 99, 66, 33, 11, 8, 6,59,68,21,25,36,21,24,6,6,6 };
byte[] iv = { 99, 66, 33, 11,5,5,5,5,5,6,6,6,5,5,5,9 };
byte[] data = { 1, 1, 2, 2, 3, 3, 4, 4, 5, 5 };
using (SymmetricAlgorithm symmetric = Aes.Create())
{
using (ICryptoTransform tran = symmetric.CreateEncryptor(key, iv))
{
using (Stream f = File.Create("aes.txt"))
{
using (Stream c = new CryptoStream(f, tran, CryptoStreamMode.Write))
{
c.Write(data, 0, data.Length);
}
}
}
}
//解密
using (SymmetricAlgorithm symmetric = Aes.Create())
{
using (ICryptoTransform tran = symmetric.CreateDecryptor(key, iv))
{
using (Stream f = File.OpenRead("aes.txt"))
{
using (Stream c = new CryptoStream(f, tran, CryptoStreamMode.Read))
{
for(int b; (b=c.ReadByte())>-1;)
{
Console.WriteLine(b + " ");
}
}
}
}
}
如果使用错误的密钥进行解密,则CryptoStream会抛出CryptographicException,而捕获该异常是测试密钥是否正确的唯一途径。
除了密钥之外,示例中还生成了一个初始化向量(Initialization Vector, IV)。这16字节的序列也是密码的一部分(和密钥相似),但它并不是保密的。当传输加密信息时,IV会以明文的方式进行传输(可能是在消息的头部进行传输),但每一段信息中其值都不相同。这样即使有些消息的未加密版本都是类似的,但是加密后的信息也会难以识别。
如果无须IV的保护,可令16字节密钥和IV的值相同。但是,使用相同的IV发送多条消息会削弱密码的安全性并使破解的可能性大大增加。
CryptoStream则像是管道工,它关注于流的处理。因此我们可以将Aes替换为另一种对称加密算法而仍旧使用CryptoStream。
CryptoStream是双向的。因此可以使用CryptoStreamMode.Read来读取流,也可以使用CryptoStreamMode.Write来写入流。加密器和解密器,与读取和写入可以形成四种组合。这些组合可能会令人茫然!为了帮助理解,可以将读取理解为“拉”,而将写入理解为“推”。如果仍旧有疑问则可以首先用Write操作进行加密,而用Read操作进行解密,这种组合是最自然也最常见的。
请使用System.Cryptography命名空间下的RandomNumberGenerator来生成随机密钥或IV。此类随机数生成器生成的数字是真正难以预测的,或称为具备密码学强度的(而System.Random则无法保证这一点)。以下是一个示例:
//随机生成密钥
byte[] key_random = new byte[16];
byte[] iv_random = new byte[16];
RandomNumberGenerator rand = RandomNumberGenerator.Create();
//随机生成一个key
rand.GetBytes(key_random);
//随机生成一个iv
rand.GetBytes(iv_random);
如果不指定密钥和IV,则加密算法会自动生成密码学强度的随机数作为密钥和IV。这些值可以通过Aes对象的Key和IV属性进行查看。
三、内存加密
我们可以使用MemoryStream完全将加密和解密放在内存中。
public class AesTools
{
static byte[] Encrypt(byte[] data, byte[] key, byte[] iv)
{
using (SymmetricAlgorithm symmetric = Aes.Create())
{
symmetric.KeySize = 128;
symmetric.BlockSize = 128;
symmetric.Mode = CipherMode.CBC;
symmetric.Padding = PaddingMode.PKCS7;
using (ICryptoTransform tran = symmetric.CreateEncryptor(key, iv))
{
return EnCrypt(data, tran);
}
}
}
static byte[] Decrypt(byte[] data, byte[] key, byte[] iv)
{
using (SymmetricAlgorithm symmetric = Aes.Create())
{
symmetric.KeySize = 128;
symmetric.BlockSize = 128;
symmetric.Mode = CipherMode.CBC;
symmetric.Padding = PaddingMode.PKCS7;
using (ICryptoTransform tran = symmetric.CreateDecryptor(key, iv))
{
return DeCrypt(data, tran);
}
}
}
static byte[] EnCrypt(byte[] data, ICryptoTransform tran)
{
return tran.TransformFinalBlock(data, 0, data.Length);
}
static byte[] DeCrypt(byte[] data, ICryptoTransform tran)
{
return tran.TransformFinalBlock(data, 0, data.Length);
}
public static string Encrypt(string data, byte[] key, byte[] iv)
{
return Convert.ToBase64String(Encrypt(Encoding.UTF8.GetBytes(data), key, iv));
}
public static string Decrypt(string data, byte[] key, byte[] iv)
{
return Encoding.UTF8.GetString(Decrypt(Convert.FromBase64String(data), key, iv));
}
}
测试代码
//随机生成密钥
byte[] key_random = new byte[16];
byte[] iv_random = new byte[16];
RandomNumberGenerator rand = RandomNumberGenerator.Create();
//随机生成一个key
rand.GetBytes(key_random);
//随机生成一个iv
rand.GetBytes(iv_random);
string encrypted = AesTools.Encrypt("yeah小小的猫咪", key_random, iv_random);
Console.WriteLine(encrypted);
string decrypted = AesTools.Decrypt(encrypted, key_random, iv_random);
Console.WriteLine(decrypted);
可以看到如下输出
四、串联加密流
CryptoStream是一个装饰器,它可以将其他的流串联起来。以下示例会先将压缩的加密文本写入文件,之后再从该文件中读取这些内容。
所有名称为单个字符的变量都是链条的一部分。而那些algorithm、encryptor和decryptor将协助CryptoStream进行加密工作。
不论流的大小如何,上述串联加密流的方式只会占用很少的内存。
五、销毁加密对象
销毁CryptoStream可以确保将加密流内部缓存的数据刷新到基础流中。加密流的内部缓存是非常必要的,因为加密算法会将数据分块处理而不是一个字节一个字节地处理。
和其他的流不太一样,CryptoStream的Flush方法并不会进行任何操作。如果需要刷新加密流(但并不销毁它),则必须使用FlushFinalBlock方法。和Flush不同,FlushFinalBlock只能调用一次,并且在调用之后该流就不能再写入任何数据了。
在我们的范例中,我们还销毁了Aes算法对象和ICryptoTransform对象(encryptor和decryptor)。Rijndael变换并不需要实际进行销毁,但销毁操作仍然扮演着一个重要的角色。该操作会将对称密钥和关联的数据从内存中清除,以防止本机运行的其他软件(尤其是恶意软件)探测这些数据。我们不能依赖垃圾回收器来执行这种操作因为垃圾回收只会将内存进行标记(标记为未使用状态),而不会将内存的每一个字节填充为零。
除了可以使用using销毁Aes对象之外,还可以使用Clear方法。它的销毁语义鲜为人知,因此它将Dispose方法用显式实现的方式隐藏了起来。
六、密钥管理的建议
对加密密钥进行硬编码是不可取的,因为使用常见的工具就可以将程序集反编译为可读代码。一个较好方案是在每一次安装时制作一个随机密钥,并使用Windows数据保护来安全地存储它(或者使用Windows数据保护加密整条消息)。如果要加密消息流,则公钥加密仍然是目前的最佳选择。