C#中使用RijndaelManaged、CryptoStream和CBC模式传输IV的方法
问题解答
1. 加密文件比解密文件大16字节的原因
这是PKCS7填充机制导致的:当明文长度刚好是AES块大小(128位=16字节)的整数倍时,PKCS7会自动填充一个完整的16字节块(填充值为16),确保解密时能正确识别并移除填充。解密后填充被剔除,所以解密文件大小等于原明文大小,加密文件则多了这16字节的填充。如果原明文长度不是16的倍数,加密后的填充字节数会是16 - (原长度 % 16),此时加密文件和原文件的大小差就不是16字节了。
另外明确一点:RijndaelManaged和CryptoStream不会自动将IV写入密文,你当前的代码里也没有手动写入IV的逻辑,所以加密文件中并不包含IV,这也是你现在只能通过密码生成IV来解密的原因。
2. 更安全的IV处理方式:随机生成IV并写入密文
用密码生成IV存在安全风险——IV需要满足随机性(不需要保密,但必须随机),否则会降低加密安全性。正确的流程是:
- 加密阶段:生成随机IV → 先将IV写入加密文件开头 → 再写入加密后的密文
- 解密阶段:先读取文件开头的16字节作为IV → 再用该IV和密钥解密后续内容
改进后的加密代码
public static void EncryptFileA(string strInputFilename, string strOutputFilename, string strPassword) { if (!File.Exists(strInputFilename)) return; using (RijndaelManaged aesCrypto = new RijndaelManaged()) { aesCrypto.KeySize = 256; aesCrypto.BlockSize = 128; aesCrypto.Padding = PaddingMode.PKCS7; aesCrypto.Mode = CipherMode.CBC; // 从密码生成密钥(注意:直接转ASCII不安全,建议用KDF,后文有提示) aesCrypto.Key = ASCIIEncoding.ASCII.GetBytes(strPassword); // 生成随机IV aesCrypto.GenerateIV(); using (FileStream fsCrypt = new FileStream(strOutputFilename, FileMode.Create)) { // 先写入IV到文件开头(IV长度等于块大小,16字节) fsCrypt.Write(aesCrypto.IV, 0, aesCrypto.IV.Length); using (CryptoStream cs = new CryptoStream(fsCrypt, aesCrypto.CreateEncryptor(), CryptoStreamMode.Write)) using (FileStream fsIn = new FileStream(strInputFilename, FileMode.Open)) { try { byte[] buffer = new byte[1024]; int read; while ((read = fsIn.Read(buffer, 0, buffer.Length)) > 0) { cs.Write(buffer, 0, read); } } catch (CryptographicException e) { Console.WriteLine($"CryptographicException: {strInputFilename}"); } catch (Exception e) { Console.WriteLine($"Exception: {strInputFilename}"); } } } } }
改进后的解密代码
public static void DecryptFileA(string strInputFilename, string strOutputFilename, string strPassword) { if (!File.Exists(strInputFilename)) return; using (RijndaelManaged aesCrypto = new RijndaelManaged()) { aesCrypto.KeySize = 256; aesCrypto.BlockSize = 128; aesCrypto.Padding = PaddingMode.PKCS7; aesCrypto.Mode = CipherMode.CBC; aesCrypto.Key = ASCIIEncoding.ASCII.GetBytes(strPassword); using (FileStream fsCrypt = new FileStream(strInputFilename, FileMode.Open)) { // 读取开头的16字节作为IV byte[] iv = new byte[aesCrypto.BlockSize / 8]; fsCrypt.Read(iv, 0, iv.Length); aesCrypto.IV = iv; using (CryptoStream cs = new CryptoStream(fsCrypt, aesCrypto.CreateDecryptor(), CryptoStreamMode.Read)) using (FileStream fsOut = new FileStream(strOutputFilename, FileMode.Create)) { try { byte[] buffer = new byte[1024]; int read; while ((read = cs.Read(buffer, 0, buffer.Length)) > 0) { fsOut.Write(buffer, 0, read); } } catch (CryptographicException e) { Console.WriteLine($"CryptographicException: {strInputFilename}"); } catch (Exception e) { Console.WriteLine($"Exception: {strInputFilename}"); } } } } }
额外安全提示
直接用ASCIIEncoding.ASCII.GetBytes(strPassword)生成密钥不安全,因为密码的熵通常很低,建议用**密钥派生函数(KDF)**比如PBKDF2来从密码生成密钥,同时加入随机盐值(盐值也要写入加密文件),进一步增强安全性。示例片段(加密时):
// 生成随机盐值 byte[] salt = new byte[16]; using (var rng = RandomNumberGenerator.Create()) { rng.GetBytes(salt); } // 用PBKDF2生成密钥 var key = Rfc2898DeriveBytes.Pbkdf2(strPassword, salt, 100000, HashAlgorithmName.SHA256, 32); aesCrypto.Key = key; // 加密时先写入盐值,再写入IV,最后写密文 fsCrypt.Write(salt, 0, salt.Length); fsCrypt.Write(aesCrypto.IV, 0, aesCrypto.IV.Length);
解密时则先读取盐值,再读取IV,最后用PBKDF2生成密钥。
内容的提问来源于stack exchange,提问作者Eloy
相关产品推荐
相关产品推荐

