1. 项目概述与核心需求解析最近在做一个需要处理敏感数据的C#项目比如用户上传的文档或者配置信息客户要求这些数据在存储到数据库或者本地文件时必须是加密的。这让我想起了AES高级加密标准它几乎是目前对称加密领域的“黄金标准”速度快、安全性高应用非常广泛。但AES本身只是一个算法真正要用起来还得搭配一套“组合拳”也就是工作模式和填充方式。我这次选择的是AES/ECB/PKCS5Padding这个经典组合。为什么是它因为ECB模式简单直接不需要初始化向量IV在一些对安全性要求不是极端苛刻但需要简单快速加解密的场景下非常合适比如加密一些独立的配置项或者临时令牌。而PKCS5Padding在.NET里实际对应PKCS7则是为了解决AES块加密时数据长度必须对齐的问题。这个项目就是要把这套组合拳在C#里完整地实现出来封装成可靠的工具类方便以后在多个项目里“保存备用”。2. AES/ECB/PKCS5Padding 技术原理与选型考量2.1 AES算法核心对称加密的基石AES是一种分组对称加密算法。所谓“对称”就是加密和解密使用同一把钥匙这把钥匙我们称为密钥Key。它的核心思想是把明文数据切分成固定大小的“块”BlockAES的块大小固定为128位16字节。然后通过多轮的替换、移位、列混合和轮密钥加等操作将明文块“打乱”成密文块。这个过程是可逆的只要拥有正确的密钥就能通过相反的操作恢复出明文。AES根据密钥长度分为AES-128、AES-192和AES-256分别使用128位、192位和256位的密钥。密钥越长安全性理论上越高但计算开销也略大。对于大多数应用场景AES-256已经提供了军用级别的安全性。2.2 ECB模式简单但需慎用选定了AES算法接下来要决定怎么加密多个数据块这就是工作模式Mode of Operation。我这次用的是ECBElectronic Codebook电子密码本模式。它的工作方式非常直观将待加密的明文分割成一个个独立的16字节块然后使用相同的密钥对每一个块单独进行AES加密。它的优点很明显无需IV不需要初始化向量这简化了代码逻辑和密钥管理。你只需要保管好密钥就行。可并行计算由于每个块的加密独立理论上可以并行处理提升大数据的加密速度。简单易懂逻辑清晰易于实现和调试。但它的缺点同样突出这也是为什么它经常被“诟病”的原因ECB模式最大的问题是相同的明文块会被加密成相同的密文块。如果明文中有大量重复的模式那么密文中也会出现明显的模式。一个经典的例子就是加密一张BMP格式的图片未经压缩虽然你看不懂密文的字节但图片的大致轮廓依然可能在密文中显现出来。因此ECB不适合加密含有大量重复或规律性结构的数据比如图像、文档等。那么我为什么还要用ECB这完全取决于场景。在我的项目里需要加密的往往是单个的、较短的字符串如API令牌、序列号或独立的二进制配置片段。这些数据本身重复模式少且每个加密对象都是独立的。在这种情况下ECB的简单性和无需IV的特性就成了优势。它避免了因IV管理不当如重复使用、丢失带来的风险。记住一个原则对于加密独立、随机的短数据ECB是可以接受的对于加密长文本、文件或具有模式的数据请务必使用CBC、CTR等更安全的模式。2.3 PKCS5/PKCS7填充补齐最后一块AES是块加密要求输入数据必须是16字节的整数倍。但我们的明文长度是任意的怎么办这就需要填充Padding。PKCS#7是一种最常用的填充方式在.NET中PaddingMode.PKCS7对应 PKCS#7它与PKCS#5在AES的上下文中是等价的。它的规则很简单假设最后一个块还差N个字节才满16字节那么就用数值N填充这N个字节。例如如果最后一块明文是[0x01, 0x02, 0x03]3个字节还差13个字节。那么填充后的数据就是[0x01, 0x02, 0x03, 0x0D, 0x0D, 0x0D, ...]共13个0x0D。解密时读取密文解密后数据的最后一个字节其值就是填充的长度N然后直接移除最后N个字节就得到了原始明文。这种填充方式非常可靠是.NET中的默认选项。3. C# 实现详解与核心代码封装3.1 使用 .NET 内置的Aes类在C#中我们不需要自己实现AES的复杂轮函数。.NET Framework和.NET Core/5/6/7及以上都提供了强大的System.Security.Cryptography.Aes类。它是一个抽象类我们通常使用它的工厂方法Aes.Create()来获取一个实现实例。这个实例已经为我们配置好了AES算法。核心工具类设计思路我将创建一个静态工具类AesHelper它提供两个核心方法Encrypt和Decrypt。为了灵活性密钥Key将通过参数传入。考虑到ECB模式不需要IV我们在代码中要明确指定。using System; using System.IO; using System.Security.Cryptography; using System.Text; public static class AesHelper { // 编码方式用于处理字符串和字节数组的转换 private static readonly Encoding DefaultEncoding Encoding.UTF8; /// summary /// 使用AES/ECB/PKCS7加密字符串 /// /summary /// param nameplainText待加密的明文/param /// param namekeyBase64Base64格式的密钥必须为16, 24或32字节长度/param /// returnsBase64格式的密文/returns public static string Encrypt(string plainText, string keyBase64) { if (string.IsNullOrEmpty(plainText)) throw new ArgumentNullException(nameof(plainText)); if (string.IsNullOrEmpty(keyBase64)) throw new ArgumentNullException(nameof(keyBase64)); byte[] key Convert.FromBase64String(keyBase64); ValidateKeySize(key); using (Aes aesAlg Aes.Create()) { // 核心配置ECB模式PKCS7填充 aesAlg.Key key; aesAlg.Mode CipherMode.ECB; aesAlg.Padding PaddingMode.PKCS7; // ECB模式不需要IV这里我们显式地将IV设为全零数组虽然它不会被使用 aesAlg.IV new byte[aesAlg.BlockSize / 8]; // 创建加密器 ICryptoTransform encryptor aesAlg.CreateEncryptor(); // 执行加密 byte[] plainBytes DefaultEncoding.GetBytes(plainText); byte[] cipherBytes; using (var msEncrypt new MemoryStream()) { using (var csEncrypt new CryptoStream(msEncrypt, encryptor, CryptoStreamMode.Write)) { csEncrypt.Write(plainBytes, 0, plainBytes.Length); // 必须调用FlushFinalBlock否则最后一块可能不会被正确处理 csEncrypt.FlushFinalBlock(); cipherBytes msEncrypt.ToArray(); } } return Convert.ToBase64String(cipherBytes); } } /// summary /// 使用AES/ECB/PKCS7解密字符串 /// /summary /// param namecipherTextBase64格式的密文/param /// param namekeyBase64Base64格式的密钥必须与加密时相同/param /// returns解密后的明文/returns public static string Decrypt(string cipherText, string keyBase64) { if (string.IsNullOrEmpty(cipherText)) throw new ArgumentNullException(nameof(cipherText)); if (string.IsNullOrEmpty(keyBase64)) throw new ArgumentNullException(nameof(keyBase64)); byte[] key Convert.FromBase64String(keyBase64); ValidateKeySize(key); using (Aes aesAlg Aes.Create()) { aesAlg.Key key; aesAlg.Mode CipherMode.ECB; aesAlg.Padding PaddingMode.PKCS7; aesAlg.IV new byte[aesAlg.BlockSize / 8]; // 创建解密器 ICryptoTransform decryptor aesAlg.CreateDecryptor(); // 执行解密 byte[] cipherBytes Convert.FromBase64String(cipherText); byte[] plainBytes; using (var msDecrypt new MemoryStream(cipherBytes)) { using (var csDecrypt new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Read)) { using (var srDecrypt new StreamReader(csDecrypt, DefaultEncoding)) { // 从CryptoStream中读取所有解密后的数据 plainBytes DefaultEncoding.GetBytes(srDecrypt.ReadToEnd()); } } } return DefaultEncoding.GetString(plainBytes); } } /// summary /// 验证密钥长度是否符合AES要求 /// /summary private static void ValidateKeySize(byte[] key) { if (key null) throw new ArgumentNullException(nameof(key)); int validKeySize key.Length * 8; // 转换为比特 if (validKeySize ! 128 validKeySize ! 192 validKeySize ! 256) { throw new ArgumentException($无效的密钥长度。AES支持128位、192位或256位密钥。提供的密钥为{validKeySize}位。); } } /// summary /// 生成一个指定长度的随机密钥Base64格式 /// /summary /// param namekeySizeInBits密钥长度必须是128、192或256/param /// returnsBase64格式的密钥字符串/returns public static string GenerateKeyBase64(int keySizeInBits 256) { if (keySizeInBits ! 128 keySizeInBits ! 192 keySizeInBits ! 256) throw new ArgumentException(密钥长度必须是128、192或256位。, nameof(keySizeInBits)); using (Aes aes Aes.Create()) { aes.KeySize keySizeInBits; aes.GenerateKey(); // 这个方法会生成一个符合当前KeySize的随机密钥 return Convert.ToBase64String(aes.Key); } } }3.2 代码关键点解析与实操心得密钥管理代码中密钥以Base64字符串形式传入和传出。Base64是一种将二进制数据编码为可打印ASCII字符串的方法非常适合在配置文件、数据库字段或网络传输中存储和传递密钥。切记密钥必须妥善保管绝不能硬编码在代码中或提交到版本控制系统。推荐使用安全的配置管理工具如Azure Key Vault, AWS Secrets Manager或环境变量。FlushFinalBlock()的重要性在加密操作的CryptoStream使用完毕后必须调用FlushFinalBlock()方法。这个方法会触发对最后一块数据的填充和加密。如果忘记调用你可能会得到一个不完整的、无法解密的密文。这是一个非常常见的坑。ValidateKeySize方法这是一个防御性编程的实践。AES算法对密钥长度有严格限制。即使Aes类在设置Key属性时会进行验证并抛出CryptographicException但提前进行明确的、信息更友好的验证能让调用者更快定位问题。GenerateKeyBase64方法为了方便我添加了一个生成随机密钥的辅助方法。在实际项目中密钥应该由系统管理员或安全的密钥管理服务生成和分发而不是每次运行时临时生成。临时生成的密钥无法解密之前加密的数据。IV属性的处理尽管ECB模式不使用IV但Aes类的IV属性在创建加密器/解密器时仍然会被读取。为了避免潜在的“未设置IV”的警告或错误我们显式地将其设置为一个与块大小匹配的全零字节数组。这是一个良好的实践让代码的意图更清晰。4. 完整使用示例与场景适配4.1 基础字符串加解密示例下面演示如何使用上面封装的AesHelper类。class Program { static void Main(string[] args) { try { // 1. 生成一个256位的随机密钥仅演示生产环境应使用固定密钥 string secretKey AesHelper.GenerateKeyBase64(256); Console.WriteLine($生成的密钥 (Base64): {secretKey}); Console.WriteLine($密钥长度 (字节): {Convert.FromBase64String(secretKey).Length}); Console.WriteLine(); // 2. 待加密的敏感信息 string originalText 这是一段需要加密的敏感配置信息比如数据库连接字符串。; Console.WriteLine($原始明文: {originalText}); Console.WriteLine(); // 3. 加密 string encryptedText AesHelper.Encrypt(originalText, secretKey); Console.WriteLine($加密后的密文 (Base64): {encryptedText}); Console.WriteLine(); // 4. 解密 string decryptedText AesHelper.Decrypt(encryptedText, secretKey); Console.WriteLine($解密后的明文: {decryptedText}); Console.WriteLine(); // 5. 验证 Console.WriteLine($加解密是否成功: {originalText decryptedText}); } catch (Exception ex) { Console.WriteLine($发生错误: {ex.Message}); } } }4.2 适配不同场景的扩展上面的工具类处理的是字符串但实际场景可能更多样。场景一加密字节数组如图片、文件片段有时我们需要直接加密二进制数据。可以对工具类进行重载。// 在AesHelper类中添加 public static byte[] Encrypt(byte[] plainBytes, byte[] key) { // ... 参数检查与字符串版本类似但使用字节数组 ... using (Aes aesAlg Aes.Create()) { aesAlg.Key key; aesAlg.Mode CipherMode.ECB; aesAlg.Padding PaddingMode.PKCS7; aesAlg.IV new byte[aesAlg.BlockSize / 8]; using (ICryptoTransform encryptor aesAlg.CreateEncryptor()) using (var ms new MemoryStream()) { using (var cs new CryptoStream(ms, encryptor, CryptoStreamMode.Write)) { cs.Write(plainBytes, 0, plainBytes.Length); cs.FlushFinalBlock(); return ms.ToArray(); } } } } public static byte[] Decrypt(byte[] cipherBytes, byte[] key) { // ... 解密实现与加密对称 ... }场景二加密/解密文件对于大文件不能一次性读入内存。需要使用流Stream的方式分段处理。public static void EncryptFile(string inputFilePath, string outputFilePath, byte[] key) { using (Aes aesAlg Aes.Create()) { aesAlg.Key key; aesAlg.Mode CipherMode.ECB; aesAlg.Padding PaddingMode.PKCS7; aesAlg.IV new byte[aesAlg.BlockSize / 8]; using (ICryptoTransform encryptor aesAlg.CreateEncryptor()) using (FileStream fsInput new FileStream(inputFilePath, FileMode.Open, FileAccess.Read)) using (FileStream fsOutput new FileStream(outputFilePath, FileMode.Create, FileAccess.Write)) using (CryptoStream cs new CryptoStream(fsOutput, encryptor, CryptoStreamMode.Write)) { fsInput.CopyTo(cs); // .NET Core 中的便捷方法自动处理缓冲区 // 在 .NET Framework 中可能需要手动循环读取和写入 } } }注意对于大文件ECB模式的安全性缺陷相同明文块产生相同密文块可能会暴露文件结构信息。如果文件内容有大量重复数据块如某些格式的文档请考虑使用CBC模式并安全地管理IV。5. 常见问题、排查技巧与安全实践5.1 典型错误与解决方案速查表在实际使用中你可能会遇到以下错误。这里整理了一份速查表错误现象可能原因解决方案CryptographicException: 指定的密钥对此算法无效。1. 密钥长度不是128/192/256位。2. 密钥字节数组为空或为null。3. 密钥的Base64字符串格式错误。1. 使用ValidateKeySize方法或检查密钥生成逻辑。2. 确保密钥已正确传入。3. 使用Convert.FromBase64String前验证字符串是否合法。CryptographicException: 要解密的数据长度无效。1. 密文在传输或存储中被损坏或截断。2. 使用了错误的密钥解密。3. 密文的Base64字符串格式错误。1. 检查密文的完整性确保没有丢失字符特别是Base64末尾的。2. 确认解密使用的密钥与加密时完全一致。3. 验证Base64字符串格式。解密后得到乱码或抛出填充异常。1.最常见原因加密和解密时使用的密钥不一致。2. 加密和解密时使用的模式或填充方式不一致如一端用ECB另一端用CBC。3. 明文字符串在加密前和解密后的编码不一致如加密用UTF8解密用ASCII。1. 仔细核对密钥管理流程确保是同一个密钥。2. 检查代码确保CipherMode和PaddingMode设置完全相同。3. 统一使用Encoding.UTF8。建议在工具类内部固定编码。加密后的Base64字符串包含或/在URL中传输有问题。Base64标准字符集中的和/在URL中有特殊含义。使用Convert.ToBase64String()后再进行一次URL安全的替换.Replace(, -).Replace(/, _).Replace(, )。解密前反向替换。或者使用Base64Url编码库。FlushFinalBlock未被调用导致解密失败。加密时使用了CryptoStream但忘记调用FlushFinalBlock()或未正确关闭流。确保在加密操作的最后调用csEncrypt.FlushFinalBlock()或者将CryptoStream包裹在using语句中以确保其被正确关闭关闭时会自动调用FlushFinalBlock。5.2 安全实践与进阶建议密钥生命周期管理生成使用密码学安全的随机数生成器CSPRNG如.NET的RNGCryptoServiceProvider或Aes.Create().GenerateKey()。存储切勿硬编码。使用环境变量、加密的配置文件或专业的密钥管理服务KMS。轮换制定密钥轮换策略。对于长期使用的数据定期更换密钥并用新密钥重新加密数据。关于ECB模式的安全再强调如果你要加密的数据超过一个块16字节并且数据内容可能存在规律或重复请不要使用ECB。改用CBC模式。CBC模式需要一个初始化向量IVIV必须是随机的且每次加密都应不同但可以公开随密文一起存储。这能有效隐藏明文的模式。认证加密AES只保证了机密性别人看不懂但没有保证完整性数据是否被篡改。攻击者可能篡改密文导致解密出无意义但可能有害的数据。对于高安全要求场景应考虑使用认证加密模式如GCMGalois/Counter Mode。GCM同时提供机密性、完整性和身份验证。在.NET中可以使用AesGcm类存在于System.Security.Cryptography命名空间但需要注意其可用性.NET Core 3.0及以上版本支持较好。性能考量Aes类创建的实例是线程不安全的。如果在高并发场景下使用应为每个线程或每个操作创建独立的Aes实例或者使用CreateEncryptor/CreateDecryptor创建的ICryptoTransform对象它们也是非线程安全的。避免在多个线程间共享这些对象。单元测试为你的加密工具类编写单元测试。测试用例应包括正常加解密、空字符串、超长字符串、密钥错误、密文篡改等情况。这能确保代码的可靠性和行为的一致性。这套基于C#的AES/ECB/PKCS5Padding实现虽然模式简单但涵盖了对称加密从原理、选型、实现到安全实践的核心路径。把它封装好、理解透足以应对很多内部系统、配置加密、令牌处理等场景。当需求升级时你可以在此基础上平滑地切换到更复杂的模式如CBC或更现代的方案如AES-GCM因为核心的Aes类和流式操作模式都是相通的。
C#实现AES/ECB/PKCS5Padding加密:原理、代码与安全实践
发布时间:2026/7/1 4:43:31
1. 项目概述与核心需求解析最近在做一个需要处理敏感数据的C#项目比如用户上传的文档或者配置信息客户要求这些数据在存储到数据库或者本地文件时必须是加密的。这让我想起了AES高级加密标准它几乎是目前对称加密领域的“黄金标准”速度快、安全性高应用非常广泛。但AES本身只是一个算法真正要用起来还得搭配一套“组合拳”也就是工作模式和填充方式。我这次选择的是AES/ECB/PKCS5Padding这个经典组合。为什么是它因为ECB模式简单直接不需要初始化向量IV在一些对安全性要求不是极端苛刻但需要简单快速加解密的场景下非常合适比如加密一些独立的配置项或者临时令牌。而PKCS5Padding在.NET里实际对应PKCS7则是为了解决AES块加密时数据长度必须对齐的问题。这个项目就是要把这套组合拳在C#里完整地实现出来封装成可靠的工具类方便以后在多个项目里“保存备用”。2. AES/ECB/PKCS5Padding 技术原理与选型考量2.1 AES算法核心对称加密的基石AES是一种分组对称加密算法。所谓“对称”就是加密和解密使用同一把钥匙这把钥匙我们称为密钥Key。它的核心思想是把明文数据切分成固定大小的“块”BlockAES的块大小固定为128位16字节。然后通过多轮的替换、移位、列混合和轮密钥加等操作将明文块“打乱”成密文块。这个过程是可逆的只要拥有正确的密钥就能通过相反的操作恢复出明文。AES根据密钥长度分为AES-128、AES-192和AES-256分别使用128位、192位和256位的密钥。密钥越长安全性理论上越高但计算开销也略大。对于大多数应用场景AES-256已经提供了军用级别的安全性。2.2 ECB模式简单但需慎用选定了AES算法接下来要决定怎么加密多个数据块这就是工作模式Mode of Operation。我这次用的是ECBElectronic Codebook电子密码本模式。它的工作方式非常直观将待加密的明文分割成一个个独立的16字节块然后使用相同的密钥对每一个块单独进行AES加密。它的优点很明显无需IV不需要初始化向量这简化了代码逻辑和密钥管理。你只需要保管好密钥就行。可并行计算由于每个块的加密独立理论上可以并行处理提升大数据的加密速度。简单易懂逻辑清晰易于实现和调试。但它的缺点同样突出这也是为什么它经常被“诟病”的原因ECB模式最大的问题是相同的明文块会被加密成相同的密文块。如果明文中有大量重复的模式那么密文中也会出现明显的模式。一个经典的例子就是加密一张BMP格式的图片未经压缩虽然你看不懂密文的字节但图片的大致轮廓依然可能在密文中显现出来。因此ECB不适合加密含有大量重复或规律性结构的数据比如图像、文档等。那么我为什么还要用ECB这完全取决于场景。在我的项目里需要加密的往往是单个的、较短的字符串如API令牌、序列号或独立的二进制配置片段。这些数据本身重复模式少且每个加密对象都是独立的。在这种情况下ECB的简单性和无需IV的特性就成了优势。它避免了因IV管理不当如重复使用、丢失带来的风险。记住一个原则对于加密独立、随机的短数据ECB是可以接受的对于加密长文本、文件或具有模式的数据请务必使用CBC、CTR等更安全的模式。2.3 PKCS5/PKCS7填充补齐最后一块AES是块加密要求输入数据必须是16字节的整数倍。但我们的明文长度是任意的怎么办这就需要填充Padding。PKCS#7是一种最常用的填充方式在.NET中PaddingMode.PKCS7对应 PKCS#7它与PKCS#5在AES的上下文中是等价的。它的规则很简单假设最后一个块还差N个字节才满16字节那么就用数值N填充这N个字节。例如如果最后一块明文是[0x01, 0x02, 0x03]3个字节还差13个字节。那么填充后的数据就是[0x01, 0x02, 0x03, 0x0D, 0x0D, 0x0D, ...]共13个0x0D。解密时读取密文解密后数据的最后一个字节其值就是填充的长度N然后直接移除最后N个字节就得到了原始明文。这种填充方式非常可靠是.NET中的默认选项。3. C# 实现详解与核心代码封装3.1 使用 .NET 内置的Aes类在C#中我们不需要自己实现AES的复杂轮函数。.NET Framework和.NET Core/5/6/7及以上都提供了强大的System.Security.Cryptography.Aes类。它是一个抽象类我们通常使用它的工厂方法Aes.Create()来获取一个实现实例。这个实例已经为我们配置好了AES算法。核心工具类设计思路我将创建一个静态工具类AesHelper它提供两个核心方法Encrypt和Decrypt。为了灵活性密钥Key将通过参数传入。考虑到ECB模式不需要IV我们在代码中要明确指定。using System; using System.IO; using System.Security.Cryptography; using System.Text; public static class AesHelper { // 编码方式用于处理字符串和字节数组的转换 private static readonly Encoding DefaultEncoding Encoding.UTF8; /// summary /// 使用AES/ECB/PKCS7加密字符串 /// /summary /// param nameplainText待加密的明文/param /// param namekeyBase64Base64格式的密钥必须为16, 24或32字节长度/param /// returnsBase64格式的密文/returns public static string Encrypt(string plainText, string keyBase64) { if (string.IsNullOrEmpty(plainText)) throw new ArgumentNullException(nameof(plainText)); if (string.IsNullOrEmpty(keyBase64)) throw new ArgumentNullException(nameof(keyBase64)); byte[] key Convert.FromBase64String(keyBase64); ValidateKeySize(key); using (Aes aesAlg Aes.Create()) { // 核心配置ECB模式PKCS7填充 aesAlg.Key key; aesAlg.Mode CipherMode.ECB; aesAlg.Padding PaddingMode.PKCS7; // ECB模式不需要IV这里我们显式地将IV设为全零数组虽然它不会被使用 aesAlg.IV new byte[aesAlg.BlockSize / 8]; // 创建加密器 ICryptoTransform encryptor aesAlg.CreateEncryptor(); // 执行加密 byte[] plainBytes DefaultEncoding.GetBytes(plainText); byte[] cipherBytes; using (var msEncrypt new MemoryStream()) { using (var csEncrypt new CryptoStream(msEncrypt, encryptor, CryptoStreamMode.Write)) { csEncrypt.Write(plainBytes, 0, plainBytes.Length); // 必须调用FlushFinalBlock否则最后一块可能不会被正确处理 csEncrypt.FlushFinalBlock(); cipherBytes msEncrypt.ToArray(); } } return Convert.ToBase64String(cipherBytes); } } /// summary /// 使用AES/ECB/PKCS7解密字符串 /// /summary /// param namecipherTextBase64格式的密文/param /// param namekeyBase64Base64格式的密钥必须与加密时相同/param /// returns解密后的明文/returns public static string Decrypt(string cipherText, string keyBase64) { if (string.IsNullOrEmpty(cipherText)) throw new ArgumentNullException(nameof(cipherText)); if (string.IsNullOrEmpty(keyBase64)) throw new ArgumentNullException(nameof(keyBase64)); byte[] key Convert.FromBase64String(keyBase64); ValidateKeySize(key); using (Aes aesAlg Aes.Create()) { aesAlg.Key key; aesAlg.Mode CipherMode.ECB; aesAlg.Padding PaddingMode.PKCS7; aesAlg.IV new byte[aesAlg.BlockSize / 8]; // 创建解密器 ICryptoTransform decryptor aesAlg.CreateDecryptor(); // 执行解密 byte[] cipherBytes Convert.FromBase64String(cipherText); byte[] plainBytes; using (var msDecrypt new MemoryStream(cipherBytes)) { using (var csDecrypt new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Read)) { using (var srDecrypt new StreamReader(csDecrypt, DefaultEncoding)) { // 从CryptoStream中读取所有解密后的数据 plainBytes DefaultEncoding.GetBytes(srDecrypt.ReadToEnd()); } } } return DefaultEncoding.GetString(plainBytes); } } /// summary /// 验证密钥长度是否符合AES要求 /// /summary private static void ValidateKeySize(byte[] key) { if (key null) throw new ArgumentNullException(nameof(key)); int validKeySize key.Length * 8; // 转换为比特 if (validKeySize ! 128 validKeySize ! 192 validKeySize ! 256) { throw new ArgumentException($无效的密钥长度。AES支持128位、192位或256位密钥。提供的密钥为{validKeySize}位。); } } /// summary /// 生成一个指定长度的随机密钥Base64格式 /// /summary /// param namekeySizeInBits密钥长度必须是128、192或256/param /// returnsBase64格式的密钥字符串/returns public static string GenerateKeyBase64(int keySizeInBits 256) { if (keySizeInBits ! 128 keySizeInBits ! 192 keySizeInBits ! 256) throw new ArgumentException(密钥长度必须是128、192或256位。, nameof(keySizeInBits)); using (Aes aes Aes.Create()) { aes.KeySize keySizeInBits; aes.GenerateKey(); // 这个方法会生成一个符合当前KeySize的随机密钥 return Convert.ToBase64String(aes.Key); } } }3.2 代码关键点解析与实操心得密钥管理代码中密钥以Base64字符串形式传入和传出。Base64是一种将二进制数据编码为可打印ASCII字符串的方法非常适合在配置文件、数据库字段或网络传输中存储和传递密钥。切记密钥必须妥善保管绝不能硬编码在代码中或提交到版本控制系统。推荐使用安全的配置管理工具如Azure Key Vault, AWS Secrets Manager或环境变量。FlushFinalBlock()的重要性在加密操作的CryptoStream使用完毕后必须调用FlushFinalBlock()方法。这个方法会触发对最后一块数据的填充和加密。如果忘记调用你可能会得到一个不完整的、无法解密的密文。这是一个非常常见的坑。ValidateKeySize方法这是一个防御性编程的实践。AES算法对密钥长度有严格限制。即使Aes类在设置Key属性时会进行验证并抛出CryptographicException但提前进行明确的、信息更友好的验证能让调用者更快定位问题。GenerateKeyBase64方法为了方便我添加了一个生成随机密钥的辅助方法。在实际项目中密钥应该由系统管理员或安全的密钥管理服务生成和分发而不是每次运行时临时生成。临时生成的密钥无法解密之前加密的数据。IV属性的处理尽管ECB模式不使用IV但Aes类的IV属性在创建加密器/解密器时仍然会被读取。为了避免潜在的“未设置IV”的警告或错误我们显式地将其设置为一个与块大小匹配的全零字节数组。这是一个良好的实践让代码的意图更清晰。4. 完整使用示例与场景适配4.1 基础字符串加解密示例下面演示如何使用上面封装的AesHelper类。class Program { static void Main(string[] args) { try { // 1. 生成一个256位的随机密钥仅演示生产环境应使用固定密钥 string secretKey AesHelper.GenerateKeyBase64(256); Console.WriteLine($生成的密钥 (Base64): {secretKey}); Console.WriteLine($密钥长度 (字节): {Convert.FromBase64String(secretKey).Length}); Console.WriteLine(); // 2. 待加密的敏感信息 string originalText 这是一段需要加密的敏感配置信息比如数据库连接字符串。; Console.WriteLine($原始明文: {originalText}); Console.WriteLine(); // 3. 加密 string encryptedText AesHelper.Encrypt(originalText, secretKey); Console.WriteLine($加密后的密文 (Base64): {encryptedText}); Console.WriteLine(); // 4. 解密 string decryptedText AesHelper.Decrypt(encryptedText, secretKey); Console.WriteLine($解密后的明文: {decryptedText}); Console.WriteLine(); // 5. 验证 Console.WriteLine($加解密是否成功: {originalText decryptedText}); } catch (Exception ex) { Console.WriteLine($发生错误: {ex.Message}); } } }4.2 适配不同场景的扩展上面的工具类处理的是字符串但实际场景可能更多样。场景一加密字节数组如图片、文件片段有时我们需要直接加密二进制数据。可以对工具类进行重载。// 在AesHelper类中添加 public static byte[] Encrypt(byte[] plainBytes, byte[] key) { // ... 参数检查与字符串版本类似但使用字节数组 ... using (Aes aesAlg Aes.Create()) { aesAlg.Key key; aesAlg.Mode CipherMode.ECB; aesAlg.Padding PaddingMode.PKCS7; aesAlg.IV new byte[aesAlg.BlockSize / 8]; using (ICryptoTransform encryptor aesAlg.CreateEncryptor()) using (var ms new MemoryStream()) { using (var cs new CryptoStream(ms, encryptor, CryptoStreamMode.Write)) { cs.Write(plainBytes, 0, plainBytes.Length); cs.FlushFinalBlock(); return ms.ToArray(); } } } } public static byte[] Decrypt(byte[] cipherBytes, byte[] key) { // ... 解密实现与加密对称 ... }场景二加密/解密文件对于大文件不能一次性读入内存。需要使用流Stream的方式分段处理。public static void EncryptFile(string inputFilePath, string outputFilePath, byte[] key) { using (Aes aesAlg Aes.Create()) { aesAlg.Key key; aesAlg.Mode CipherMode.ECB; aesAlg.Padding PaddingMode.PKCS7; aesAlg.IV new byte[aesAlg.BlockSize / 8]; using (ICryptoTransform encryptor aesAlg.CreateEncryptor()) using (FileStream fsInput new FileStream(inputFilePath, FileMode.Open, FileAccess.Read)) using (FileStream fsOutput new FileStream(outputFilePath, FileMode.Create, FileAccess.Write)) using (CryptoStream cs new CryptoStream(fsOutput, encryptor, CryptoStreamMode.Write)) { fsInput.CopyTo(cs); // .NET Core 中的便捷方法自动处理缓冲区 // 在 .NET Framework 中可能需要手动循环读取和写入 } } }注意对于大文件ECB模式的安全性缺陷相同明文块产生相同密文块可能会暴露文件结构信息。如果文件内容有大量重复数据块如某些格式的文档请考虑使用CBC模式并安全地管理IV。5. 常见问题、排查技巧与安全实践5.1 典型错误与解决方案速查表在实际使用中你可能会遇到以下错误。这里整理了一份速查表错误现象可能原因解决方案CryptographicException: 指定的密钥对此算法无效。1. 密钥长度不是128/192/256位。2. 密钥字节数组为空或为null。3. 密钥的Base64字符串格式错误。1. 使用ValidateKeySize方法或检查密钥生成逻辑。2. 确保密钥已正确传入。3. 使用Convert.FromBase64String前验证字符串是否合法。CryptographicException: 要解密的数据长度无效。1. 密文在传输或存储中被损坏或截断。2. 使用了错误的密钥解密。3. 密文的Base64字符串格式错误。1. 检查密文的完整性确保没有丢失字符特别是Base64末尾的。2. 确认解密使用的密钥与加密时完全一致。3. 验证Base64字符串格式。解密后得到乱码或抛出填充异常。1.最常见原因加密和解密时使用的密钥不一致。2. 加密和解密时使用的模式或填充方式不一致如一端用ECB另一端用CBC。3. 明文字符串在加密前和解密后的编码不一致如加密用UTF8解密用ASCII。1. 仔细核对密钥管理流程确保是同一个密钥。2. 检查代码确保CipherMode和PaddingMode设置完全相同。3. 统一使用Encoding.UTF8。建议在工具类内部固定编码。加密后的Base64字符串包含或/在URL中传输有问题。Base64标准字符集中的和/在URL中有特殊含义。使用Convert.ToBase64String()后再进行一次URL安全的替换.Replace(, -).Replace(/, _).Replace(, )。解密前反向替换。或者使用Base64Url编码库。FlushFinalBlock未被调用导致解密失败。加密时使用了CryptoStream但忘记调用FlushFinalBlock()或未正确关闭流。确保在加密操作的最后调用csEncrypt.FlushFinalBlock()或者将CryptoStream包裹在using语句中以确保其被正确关闭关闭时会自动调用FlushFinalBlock。5.2 安全实践与进阶建议密钥生命周期管理生成使用密码学安全的随机数生成器CSPRNG如.NET的RNGCryptoServiceProvider或Aes.Create().GenerateKey()。存储切勿硬编码。使用环境变量、加密的配置文件或专业的密钥管理服务KMS。轮换制定密钥轮换策略。对于长期使用的数据定期更换密钥并用新密钥重新加密数据。关于ECB模式的安全再强调如果你要加密的数据超过一个块16字节并且数据内容可能存在规律或重复请不要使用ECB。改用CBC模式。CBC模式需要一个初始化向量IVIV必须是随机的且每次加密都应不同但可以公开随密文一起存储。这能有效隐藏明文的模式。认证加密AES只保证了机密性别人看不懂但没有保证完整性数据是否被篡改。攻击者可能篡改密文导致解密出无意义但可能有害的数据。对于高安全要求场景应考虑使用认证加密模式如GCMGalois/Counter Mode。GCM同时提供机密性、完整性和身份验证。在.NET中可以使用AesGcm类存在于System.Security.Cryptography命名空间但需要注意其可用性.NET Core 3.0及以上版本支持较好。性能考量Aes类创建的实例是线程不安全的。如果在高并发场景下使用应为每个线程或每个操作创建独立的Aes实例或者使用CreateEncryptor/CreateDecryptor创建的ICryptoTransform对象它们也是非线程安全的。避免在多个线程间共享这些对象。单元测试为你的加密工具类编写单元测试。测试用例应包括正常加解密、空字符串、超长字符串、密钥错误、密文篡改等情况。这能确保代码的可靠性和行为的一致性。这套基于C#的AES/ECB/PKCS5Padding实现虽然模式简单但涵盖了对称加密从原理、选型、实现到安全实践的核心路径。把它封装好、理解透足以应对很多内部系统、配置加密、令牌处理等场景。当需求升级时你可以在此基础上平滑地切换到更复杂的模式如CBC或更现代的方案如AES-GCM因为核心的Aes类和流式操作模式都是相通的。