# C#/.NET 加密解密实战:RSA分块加解密、AES对称加密与哈希算法完整实现
在企业级应用开发中,数据安全是永恒的话题。无论是用户密码存储、敏感数据传输,还是接口签名验证,都离不开加密解密技术的支撑。本文将基于 SwitchData 项目中的 SwitchData.Common.Cryptography 命名空间,深入讲解三大类加密算法的 C# 实现:RSA 非对称加密(含分块处理与数字签名)、AES 对称加密、以及哈希算法。
一、加密算法体系概览
| 算法类型 | 代表算法 | 特点 | 典型场景 |
|---|---|---|---|
| 非对称加密 | RSA | 公钥加密、私钥解密,计算慢 | 密钥交换、数字签名、小数据加密 |
| 对称加密 | AES | 同一密钥加解密,计算快 | 大批量数据加密、文件加密 |
| 哈希算法 | MD5、SHA256 | 单向不可逆,定长输出 | 密码存储、数据完整性校验、签名摘要 |
实战中往往是组合使用:用 RSA 加密 AES 的密钥,用 AES 加密实际数据(混合加密方案),既解决了 RSA 速度慢的问题,又解决了 AES 密钥分发的难题。
二、RSA 非对称加密:分块处理与数字签名
RSA 的核心特点是公钥加密、私钥解密,但原生 RSA 有一个重要限制:单次加密的数据长度不能超过密钥长度减去填充开销。以 2048 位密钥(256字节)+ PKCS#1 填充为例,单次最多加密 256 - 11 = 245 字节。超过这个长度就需要分块加密。
2.1 密钥导入:兼容多种 PEM 格式
实际项目中,RSA 密钥通常以 PEM 格式存储。.NET Core 3.0+ 提供了多种导入方法,但不同的 PEM 格式需要调用不同的 API: - 公钥格式:SubjectPublicKeyInfo (SPKI) 或 PKCS#1 RSAPublicKey - 私钥格式:PKCS#8 或 PKCS#1 RSAPrivateKey
我们的实现采用尝试-回退策略,自动识别格式:
private static RSA ImportPublicKey(string pem)
{
var rsa = RSA.Create();
var keyData = ReadPem(pem);
if (TryImport(() => rsa.ImportSubjectPublicKeyInfo(keyData, out _)))
return rsa;
if (TryImport(() => rsa.ImportRSAPublicKey(keyData, out _)))
return rsa;
throw new ArgumentException("无法解析公钥");
}
private static bool TryImport(Action importAction)
{
try { importAction(); return true; }
catch { return false; }
}
配合工厂方法,使用起来非常直观:
var encryptor = RSA2Cryptographer.FromPublicKey(publicPem);
var decryptor = RSA2Cryptographer.FromPrivateKey(privatePem);
2.2 分块加解密的核心实现
分块加密的关键在于准确计算每块的最大数据长度,不同填充模式的计算公式不同:
private static int GetMaxDataLength(int keySizeInBytes, RSAEncryptionPadding padding)
{
if (padding == RSAEncryptionPadding.Pkcs1)
{
return keySizeInBytes - 11;
}
else if (padding.Mode == RSAEncryptionPaddingMode.Oaep)
{
int hashSize = padding.OaepHashAlgorithm.Name switch
{
"SHA1" => 20, "SHA256" => 32,
"SHA384" => 48, "SHA512" => 64,
_ => throw new NotSupportedException("不支持的OAEP哈希算法")
};
return keySizeInBytes - (2 * hashSize) - 2;
}
throw new NotSupportedException("不支持的填充模式");
}
加密时按 maxBlockSize 切片,每块独立加密后拼接;解密时按 keySizeInBytes 切片,逐块解密后合并:
private byte[] EncryptLargeData(ReadOnlySpan<byte> data, RSAEncryptionPadding padding)
{
int keySizeInBytes = _rsa.KeySize / 8;
int maxBlockSize = GetMaxDataLength(keySizeInBytes, padding);
using var ms = new MemoryStream();
for (int i = 0; i < data.Length; i += maxBlockSize)
{
int currentBlockSize = Math.Min(maxBlockSize, data.Length - i);
var block = data.Slice(i, currentBlockSize);
byte[] encryptedBlock = _rsa.Encrypt(block, padding);
ms.Write(encryptedBlock, 0, encryptedBlock.Length);
}
return ms.ToArray();
}
注意这里使用了 ReadOnlySpan
2.3 数字签名:防篡改与身份认证
RSA 不仅能加密,还能用于数字签名。签名用私钥,验证用公钥:
public string Sign(string content)
{
var data = _encoding.GetBytes(content);
var signatureBytes = _rsa.SignData(data,
HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
return Convert.ToBase64String(signatureBytes);
}
public bool Verify(string content, string signatureBase64)
{
if (string.IsNullOrEmpty(signatureBase64)) return false;
var data = _encoding.GetBytes(content);
var signatureBytes = Convert.FromBase64String(signatureBase64);
return _rsa.VerifyData(data, signatureBytes,
HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
}
2.4 标准 Dispose 模式与克隆
RSA 实现了 IDisposable,需要正确释放非托管资源:
public void Dispose()
{
if (!_disposed)
{
_rsa?.Dispose();
_disposed = true;
}
GC.SuppressFinalize(this);
}
另外还实现了 Clone() 方法,在多线程场景下为每个线程创建独立的 RSA 实例:
public RSA2Cryptographer Clone()
{
var newRsa = RSA.Create();
try
{
newRsa.ImportParameters(_rsa.ExportParameters(true));
}
catch
{
newRsa.ImportParameters(_rsa.ExportParameters(false));
}
return new RSA2Cryptographer(newRsa);
}
三、AES 对称加密:CryptoStream 的优雅封装
AES(Advanced Encryption Standard)是目前应用最广泛的对称加密算法,速度快、安全性高。
3.1 核心实现:CryptoStream 管道模式
.NET 中对称加密的标准模式是使用 CryptoStream,将加密变换包装成 Stream:
private static byte[] Transform(ICryptoTransform transform, byte[] buffer)
{
using var ms = new MemoryStream();
using var cs = new CryptoStream(ms, transform, CryptoStreamMode.Write);
cs.Write(buffer, 0, buffer.Length);
cs.FlushFinalBlock();
return ms.ToArray();
}
这种设计的好处是算法无关——同一个 Transform 方法可以用于任何 SymmetricAlgorithm 派生类(AES、DES、TripleDES 等)。
3.2 构造函数与便捷静态方法
public class SymmetricCryptographer
{
public SymmetricAlgorithm Algorithm { get; }
public SymmetricCryptographer(SymmetricAlgorithm algorithm, byte[] key, byte[] iv)
{
Algorithm = algorithm;
Algorithm.Key = key;
Algorithm.IV = iv;
}
public byte[] Encrypt(byte[] plaintext)
{
using ICryptoTransform transform = Algorithm.CreateEncryptor();
return Transform(transform, plaintext);
}
public byte[] Decrypt(byte[] encryptedText)
{
using ICryptoTransform transform = Algorithm.CreateDecryptor();
return Transform(transform, encryptedText);
}
}
同时提供了开箱即用的静态方法(内置默认密钥),适合内部系统快速加解密:
public static string Encrypt(string plaintext)
{
byte[] input = Encoding.UTF8.GetBytes(plaintext);
using var aes = Aes.Create();
var sc = new SymmetricCryptographer(aes, DefaultKey, DefaultIV);
byte[] output = sc.Encrypt(input);
return Convert.ToBase64String(output);
}
3.3 安全密钥生成
public static byte[] GenerateKey()
{
using var aes = Aes.Create();
aes.GenerateKey();
return aes.Key;
}
public static byte[] GenerateIV()
{
using var aes = Aes.Create();
aes.GenerateIV();
return aes.IV;
}
IV(初始化向量)不需要保密,但必须随机且每次加密都不同,否则相同明文会产生相同密文。
四、哈希算法:单向不可逆的指纹
哈希算法将任意长度的输入转换为固定长度的输出(摘要),具有单向不可逆和输入敏感两个特性。
4.1 多输入类型支持
HashCryptographer 提供了三层重载,覆盖字节数组、流、字符串三种常见输入:
public sealed class HashCryptographer
{
public static byte[] ComputeHash(byte[] buffer, HashAlgorithm hashAlgorithm = null)
{
using var algorithm = hashAlgorithm ?? MD5.Create();
return algorithm.ComputeHash(buffer);
}
public static byte[] ComputeHash(Stream stream, HashAlgorithm hashAlgorithm = null)
{
using var algorithm = hashAlgorithm ?? MD5.Create();
return algorithm.ComputeHash(stream);
}
public static string ComputeHash(string plaintext,
bool removeSplitChar = false,
HashAlgorithm hashAlgorithm = null)
{
var hashBytes = ComputeHash(Encoding.UTF8.GetBytes(plaintext), hashAlgorithm);
if (removeSplitChar)
return Convert.ToHexString(hashBytes);
else
return BitConverter.ToString(hashBytes);
}
}
4.2 常用调用示例
string md5 = HashCryptographer.ComputeHash("hello", removeSplitChar: true);
string sha256 = HashCryptographer.ComputeHash("hello",
removeSplitChar: true, hashAlgorithm: SHA256.Create());
using var fs = File.OpenRead("large.iso");
byte[] fileHash = HashCryptographer.ComputeHash(fs);
MD5 和 SHA1 已被证明存在碰撞风险,密码存储请使用 SHA256 及以上版本,且必须加盐(Salt)。
五、混合加密实战方案
经典的混合加密场景:客户端上传敏感数据到服务器。
流程设计
代码示例
// 客户端
string publicKey = "服务器下发的公钥PEM";
string largeData = "需要加密的大量敏感数据...";
byte[] aesKey = SymmetricCryptographer.GenerateKey();
byte[] aesIV = SymmetricCryptographer.GenerateIV();
using var aes = Aes.Create();
aes.Key = aesKey; aes.IV = aesIV;
var aesCrypto = new SymmetricCryptographer(aes, aesKey, aesIV);
byte[] encryptedData = aesCrypto.Encrypt(Encoding.UTF8.GetBytes(largeData));
var rsaEnc = RSA2Cryptographer.FromPublicKey(publicKey);
string encryptedAesKey = rsaEnc.Encrypt(Convert.ToBase64String(aesKey));
string encryptedAesIV = rsaEnc.Encrypt(Convert.ToBase64String(aesIV));
string dataHash = HashCryptographer.ComputeHash(largeData, true, SHA256.Create());
// 服务器
string privateKey = "服务器私钥PEM";
var rsaDec = RSA2Cryptographer.FromPrivateKey(privateKey);
byte[] decryptedAesKey = Convert.FromBase64String(rsaDec.Decrypt(encryptedAesKey));
byte[] decryptedAesIV = Convert.FromBase64String(rsaDec.Decrypt(encryptedAesIV));
using var aes2 = Aes.Create();
var aesDec = new SymmetricCryptographer(aes2, decryptedAesKey, decryptedAesIV);
string decryptedData = Encoding.UTF8.GetString(aesDec.Decrypt(encryptedData));
string verifyHash = HashCryptographer.ComputeHash(decryptedData, true, SHA256.Create());
if (verifyHash == dataHash) { Console.WriteLine("数据完整"); }
六、最佳实践总结
| 场景 | 推荐方案 | 注意事项 |
|---|---|---|
| 用户密码存储 | BCrypt / PBKDF2 / SHA256 + Salt | 绝对不要用 MD5,更不要明文存储 |
| 大文件加密 | AES-256-CBC / AES-GCM | IV 每次随机,密钥妥善保管 |
| 密钥交换 / API 签名 | RSA-2048+ 签名+验签 | 私钥绝不泄露,公钥可公开 |
| 数据完整性校验 | SHA256 / SHA512 | 防篡改需配合签名或 HMAC |
| HTTPS 传输 | TLS 1.3 | 应用层可再加一层签名做双重保险 |
常见坑点避坑
- RSA 报不正确的长度异常:99% 是没做分块处理,单次加密数据过长
- AES 解密后末尾乱码:忘了调用 FlushFinalBlock(),最后一个分块没处理完整
- 相同明文哈希结果不同:字符串编码不一致,统一用 UTF-8
- RSA 验签始终返回 false:检查签名算法名和填充模式是否两端一致
加密解密是差一点都不行的领域——算法、模式、填充、编码、字节序,任何一个参数不匹配都会导致完全错误的结果。建议封装成统一的工具类(如本文的三个 Cryptographer),在项目中集中管理。
参考代码
本文所有代码来源于 SwitchData 项目的 SwitchData.Common.Cryptography 命名空间: - RSA2Cryptographer.cs - RSA 分块加解密 + 签名验签 + PEM 多格式导入 - SymmetricCryptographer.cs - AES 对称加密封装 + CryptoStream 模式 - HashCryptographer.cs - 哈希算法封装 + 多输入支持 + 格式化输出