# C#/.NET 加密解密实战:RSA分块加解密、AES对称加密与哈希算法完整实现

在企业级应用开发中,数据安全是永恒的话题。无论是用户密码存储、敏感数据传输,还是接口签名验证,都离不开加密解密技术的支撑。本文将基于 SwitchData 项目中的 SwitchData.Common.Cryptography 命名空间,深入讲解三大类加密算法的 C# 实现:RSA 非对称加密(含分块处理与数字签名)、AES 对称加密、以及哈希算法。

一、加密算法体系概览

算法类型 代表算法 特点 典型场景
非对称加密 RSA 公钥加密、私钥解密,计算慢 密钥交换、数字签名、小数据加密
对称加密 AES 同一密钥加解密,计算快 大批量数据加密、文件加密
哈希算法 MD5、SHA256 单向不可逆,定长输出 密码存储、数据完整性校验、签名摘要
flowchart LR A[原始数据] --> B{场景选择} B -->|密钥交换/签名| C[RSA 非对称加密] B -->|大数据加密| D[AES 对称加密] B -->|完整性校验/密码| E[Hash 哈希算法] C --> C1[公钥加密 + 私钥解密] C --> C2[私钥签名 + 公钥验签] D --> D1[同一密钥加/解密] E --> E1[单向不可逆]

实战中往往是组合使用:用 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 来避免不必要的内存分配,这是 .NET 高性能编程的常见技巧。

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)。

五、混合加密实战方案

经典的混合加密场景:客户端上传敏感数据到服务器。

流程设计

sequenceDiagram participant C as 客户端 participant S as 服务器 S->>S: 生成 RSA 密钥对,公钥公开、私钥保密 S-->>C: 下发 RSA 公钥 C->>C: 1. 生成随机 AES 密钥 (32字节) C->>C: 2. 用 AES 加密大文件 C->>C: 3. 用 RSA 公钥加密 AES 密钥 C->>C: 4. 用 SHA256 计算文件哈希 C-->>S: 上传 密文文件 + RSA加密后的AES密钥 + 哈希 S->>S: 1. RSA 私钥解密得到 AES 密钥 S->>S: 2. AES 密钥解密文件 S->>S: 3. 重新计算哈希并对比 S-->>C: 返回校验结果

代码示例

// 客户端
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 应用层可再加一层签名做双重保险

常见坑点避坑

  1. RSA 报不正确的长度异常:99% 是没做分块处理,单次加密数据过长
  2. AES 解密后末尾乱码:忘了调用 FlushFinalBlock(),最后一个分块没处理完整
  3. 相同明文哈希结果不同:字符串编码不一致,统一用 UTF-8
  4. RSA 验签始终返回 false:检查签名算法名和填充模式是否两端一致

加密解密是差一点都不行的领域——算法、模式、填充、编码、字节序,任何一个参数不匹配都会导致完全错误的结果。建议封装成统一的工具类(如本文的三个 Cryptographer),在项目中集中管理。


参考代码

本文所有代码来源于 SwitchData 项目的 SwitchData.Common.Cryptography 命名空间: - RSA2Cryptographer.cs - RSA 分块加解密 + 签名验签 + PEM 多格式导入 - SymmetricCryptographer.cs - AES 对称加密封装 + CryptoStream 模式 - HashCryptographer.cs - 哈希算法封装 + 多输入支持 + 格式化输出