从零实现.NET RSA加密工具:源码解析与跨平台实战 1. 项目概述在.NET开发中数据安全是绕不开的话题。无论是用户密码、敏感配置还是API通信加密解密都是保障数据安全的基石。市面上虽然有很多现成的工具库比如NETCore.Encrypt但很多时候我们知其然不知其所以然。当遇到一些刁钻的问题比如“为什么我的RSA加密结果每次都不一样”、“PEM格式的密钥怎么导入”、“跨平台加解密怎么保证一致”如果只停留在调用封装好的方法就很难快速定位和解决。今天我们不谈怎么用现成的库而是深入到源码层面手把手带你从零实现一套.NET平台的RSA加密解密工具。我会把我在实际项目中踩过的坑、总结的经验以及那些官方文档里不会写的细节都揉碎了讲给你听。无论你是刚接触加密的新手还是想深入理解RSA原理的老手这篇文章都能让你对.NET下的RSA有一个通透的认识。2. RSA算法核心原理与.NET实现基础2.1 非对称加密的“锁与钥匙”模型理解RSA首先要理解非对称加密。你可以把它想象成一把特殊的锁和两把钥匙一把是公开的“公钥”谁都可以拿去锁门加密数据另一把是私有的“私钥”只有你自己能用来开门解密数据。这个模型完美解决了对称加密中“如何安全传递密钥”的难题。RSA算法的安全性建立在大数分解的困难性上。简单来说它基于三个核心数学步骤密钥生成随机选择两个非常大的质数p和q计算它们的乘积n p * q。n就是模数决定了密钥的长度比如2048位。接着计算欧拉函数φ(n) (p-1)*(q-1)再选择一个与φ(n)互质的整数e作为公钥指数通常取65537。最后计算私钥指数d使得(d * e) mod φ(n) 1。加密对于明文m需要先转换为一个小于n的整数加密过程是c m^e mod n得到密文c。解密用私钥解密密文c过程是m c^d mod n恢复出明文m。在.NET中我们不需要自己实现这些复杂的数学运算System.Security.Cryptography命名空间下的RSA类已经封装好了这一切。但理解原理至关重要它能帮你解释很多现象比如为什么RSA加密的数据长度有限制不能超过模数n的字节数。2.2 .NET中RSA类的演化与选择.NET Framework时代我们常用RSACryptoServiceProvider。到了.NET Core/.NET 5微软引入了更现代、跨平台友好的RSA抽象基类及其具体实现RSA.Create()。这是我们现在推荐的方式。这里有个关键点RSA.Create()在Windows上默认返回的是基于CNG下一代加密技术的实现在Linux/macOS上则使用OpenSSL的实现。这带来了灵活性但也可能引入细微的兼容性问题尤其是在处理密钥格式时。我们后续的源码实现会重点处理这些兼容性问题。注意直接使用new RSACryptoServiceProvider()在非Windows平台上可能会受限或行为不一致在新项目中应尽量避免。2.3 密钥的多种格式XML, PEM, PKCS#8RSA密钥有多种存储格式这是跨系统交互时的主要障碍。XML格式.NET Framework时代的“特产”包含完整的公钥和私钥参数Modulus, Exponent, D, P, Q等可读性好但冗长。ToXmlString和FromXmlString方法专门处理它。PEM格式OpenSSL世界的标准在Linux、macOS和现代加密工具中广泛使用。它以-----BEGIN XXX-----和-----END XXX-----包裹Base64编码的密钥数据。公钥通常是BEGIN PUBLIC KEY私钥可能是BEGIN RSA PRIVATE KEY(PKCS#1) 或BEGIN PRIVATE KEY(PKCS#8)。PKCS#8一种更通用、更安全的私钥封装标准。.NET Core 3.0 的ExportPkcs8PrivateKey和ImportPkcs8PrivateKey方法就是处理这种格式的。我们的源码实现需要能灵活地在这些格式间转换这是保证工具通用性的关键。一个常见的坑是从OpenSSL生成的PEM私钥直接交给.NET的RSA.ImportFromPem可能会失败因为.NET期望的是PKCS#8格式的PEM而OpenSSL默认生成的是PKCS#1格式。3. 核心源码实现构建健壮的RSA工具类理解了基础我们开始动手写代码。目标是创建一个RsaHelper类它要能生成密钥、支持多种格式导入导出、进行标准的加密解密并且要处理好跨平台兼容性。3.1 密钥的生成与格式转换首先我们实现密钥生成。这里我选择使用RSA.Create(int keySize)来生成密钥它是最通用的方式。using System.Security.Cryptography; using System.Text; public class RsaKeyPair { public string PublicKey { get; set; } public string PrivateKey { get; set; } public int KeySize { get; set; } } public static class RsaHelper { /// summary /// 生成RSA密钥对 /// /summary /// param namekeySize密钥长度推荐2048或4096/param /// param nameformat密钥输出格式Xml 或 Pkcs8Pem/param /// returns包含公钥和私钥的对象/returns public static RsaKeyPair GenerateKeyPair(int keySize 2048, string format Pkcs8Pem) { if (keySize 512 || keySize 16384) throw new ArgumentException(密钥长度必须在512到16384位之间, nameof(keySize)); using (RSA rsa RSA.Create(keySize)) { var keyPair new RsaKeyPair { KeySize keySize }; switch (format.ToLowerInvariant()) { case xml: // .NET传统XML格式 keyPair.PrivateKey rsa.ToXmlString(true); // true表示包含私钥 keyPair.PublicKey rsa.ToXmlString(false); break; case pkcs8pem: // PKCS#8格式的PEM跨平台推荐 byte[] privateKeyBytes rsa.ExportPkcs8PrivateKey(); byte[] publicKeyBytes rsa.ExportSubjectPublicKeyInfo(); keyPair.PrivateKey ConvertToPem(privateKeyBytes, PRIVATE KEY); keyPair.PublicKey ConvertToPem(publicKeyBytes, PUBLIC KEY); break; // 可以扩展其他格式如PKCS#1 default: throw new NotSupportedException($不支持的密钥格式: {format}); } return keyPair; } } private static string ConvertToPem(byte[] keyBytes, string label) { string base64 Convert.ToBase64String(keyBytes, Base64FormattingOptions.InsertLineBreaks); return $-----BEGIN {label}-----\n{base64}\n-----END {label}-----; } }实操心得ExportPkcs8PrivateKey导出的是DER编码的二进制数据我们需要手动加上PEM的头尾标识。ExportSubjectPublicKeyInfo则是导出公钥的标准方法。为什么不用ExportRSAPublicKey因为后者导出的是PKCS#1格式的公钥而ExportSubjectPublicKeyInfo导出的是X.509 SubjectPublicKeyInfo格式更通用。3.2 处理不同格式的密钥导入密钥生成后我们还需要能从外部比如其他系统生成的密钥导入。这是兼容性的核心。public static RSA CreateRsaProviderFromKey(string key, bool isPrivateKey false) { ArgumentException.ThrowIfNullOrEmpty(key); RSA rsa RSA.Create(); try { // 1. 尝试判断是否为PEM格式 if (key.Contains(-----BEGIN)) { // 去除PEM头尾和换行符 string base64Key key.Replace(-----BEGIN PUBLIC KEY-----, ) .Replace(-----END PUBLIC KEY-----, ) .Replace(-----BEGIN PRIVATE KEY-----, ) .Replace(-----END PRIVATE KEY-----, ) .Replace(-----BEGIN RSA PRIVATE KEY-----, ) // PKCS#1 .Replace(-----END RSA PRIVATE KEY-----, ) .Replace(\n, ).Replace(\r, ); byte[] keyBytes Convert.FromBase64String(base64Key); // 关键判断尝试导入PKCS#8私钥 if (isPrivateKey) { try { rsa.ImportPkcs8PrivateKey(keyBytes, out _); return rsa; } catch (CryptographicException) { // 如果不是PKCS#8尝试作为PKCS#1导入需要转换 // .NET没有直接导入PKCS#1的方法通常需要借助第三方库或手动解析 // 这里是一个简化示例实际项目中建议使用BouncyCastle等库处理 throw new NotSupportedException(检测到可能是PKCS#1格式的私钥.NET原生支持有限。建议使用PKCS#8格式或使用BouncyCastle库。); } } else { // 导入公钥 rsa.ImportSubjectPublicKeyInfo(keyBytes, out _); return rsa; } } // 2. 判断是否为XML格式 else if (key.Trim().StartsWith(RSAKeyValue)) { rsa.FromXmlString(key); // 注意.NET Core 3.0 的RSA基类没有FromXmlString需要扩展方法或使用RSACryptoServiceProvider // 为了跨平台这里需要特殊处理下文会讲 return HandleXmlKeyForCrossPlatform(rsa, key, isPrivateKey); } else { // 3. 假设是Base64字符串可能是裸的密钥数据 // 这里逻辑复杂通常需要根据上下文判断暂不展开 throw new ArgumentException(无法识别的密钥格式。请提供PEM或XML格式的密钥。); } } catch { rsa.Dispose(); throw; } }踩坑记录上面代码中处理XML格式的部分是个大坑。.NET Core中的RSA基类没有FromXmlString方法这是为了推动使用更标准的密钥格式。如果你需要兼容旧的XML密钥有几种选择在Windows上可以降级使用RSACryptoServiceProvider。使用System.Security.Cryptography.Xml中的RSAKeyValue类来解析XML然后手动提取参数并赋值给RSA实例。寻找或编写一个扩展方法将XML节点中的Modulus和Exponent(以及D,P,Q等私钥参数) 转换为RSAParameters结构体再调用rsa.ImportParameters。这里给出一个处理XML的扩展方法示例using System.Xml; public static class RsaExtensions { public static void FromXmlString(this RSA rsa, string xmlString) { RSAParameters parameters new RSAParameters(); XmlDocument xmlDoc new XmlDocument(); xmlDoc.LoadXml(xmlString); if (xmlDoc.DocumentElement.Name.Equals(RSAKeyValue)) { foreach (XmlNode node in xmlDoc.DocumentElement.ChildNodes) { switch (node.Name) { case Modulus: parameters.Modulus Convert.FromBase64String(node.InnerText); break; case Exponent: parameters.Exponent Convert.FromBase64String(node.InnerText); break; case P: parameters.P Convert.FromBase64String(node.InnerText); break; case Q: parameters.Q Convert.FromBase64String(node.InnerText); break; case DP: parameters.DP Convert.FromBase64String(node.InnerText); break; case DQ: parameters.DQ Convert.FromBase64String(node.InnerText); break; case InverseQ: parameters.InverseQ Convert.FromBase64String(node.InnerText); break; case D: parameters.D Convert.FromBase64String(node.InnerText); break; } } } else { throw new Exception(Invalid XML RSA key.); } rsa.ImportParameters(parameters); } }3.3 加密与解密的实现与填充模式有了密钥就可以实现加密解密了。这里的关键是填充模式Padding。RSA加密明文前需要先进行填充以增加安全性和防止特定攻击。.NET主要支持两种PKCS#1 v1.5 Padding老标准应用广泛。在RSAEncryptionPadding中对应RSAEncryptionPadding.Pkcs1。注意在Linux/macOS上与OpenSSL交互时通常需要使用此模式。OAEP Padding(Optimal Asymmetric Encryption Padding)更安全的新标准推荐在新项目中使用。对应RSAEncryptionPadding.OaepSHA1,OaepSHA256等SHA1已不推荐建议至少使用SHA256。public static string Encrypt(string plainText, string publicKey, RSAEncryptionPadding padding null) { ArgumentException.ThrowIfNullOrEmpty(plainText); ArgumentException.ThrowIfNullOrEmpty(publicKey); padding ?? RSAEncryptionPadding.OaepSHA256; // 默认使用更安全的OAEP-SHA256 using (RSA rsa CreateRsaProviderFromKey(publicKey, false)) // false表示公钥 { // 1. 计算最大加密长度 int maxBlockSize GetMaxEncryptBlockSize(rsa, padding); byte[] plainBytes Encoding.UTF8.GetBytes(plainText); // 2. 如果数据短直接加密 if (plainBytes.Length maxBlockSize) { byte[] cipherBytes rsa.Encrypt(plainBytes, padding); return Convert.ToBase64String(cipherBytes); } // 3. 数据过长需要分段加密非对称加密一般不直接加密长数据通常结合AES // 这里演示分段逻辑但实际长数据加密有更好方案见下文 using (MemoryStream msOutput new MemoryStream()) { for (int offset 0; offset plainBytes.Length; offset maxBlockSize) { int blockSize Math.Min(maxBlockSize, plainBytes.Length - offset); byte[] block new byte[blockSize]; Array.Copy(plainBytes, offset, block, 0, blockSize); byte[] encryptedBlock rsa.Encrypt(block, padding); msOutput.Write(encryptedBlock, 0, encryptedBlock.Length); } return Convert.ToBase64String(msOutput.ToArray()); } } } public static string Decrypt(string cipherText, string privateKey, RSAEncryptionPadding padding null) { ArgumentException.ThrowIfNullOrEmpty(cipherText); ArgumentException.ThrowIfNullOrEmpty(privateKey); padding ?? RSAEncryptionPadding.OaepSHA256; using (RSA rsa CreateRsaProviderFromKey(privateKey, true)) // true表示私钥 { byte[] cipherBytes Convert.FromBase64String(cipherText); int keySizeInBytes rsa.KeySize / 8; int blockSize keySizeInBytes; // RSA加密后的密文块长度等于密钥字节长度 // 如果是分段加密的需要分段解密 if (cipherBytes.Length blockSize) { using (MemoryStream msOutput new MemoryStream()) { for (int offset 0; offset cipherBytes.Length; offset blockSize) { int currentBlockSize Math.Min(blockSize, cipherBytes.Length - offset); byte[] block new byte[currentBlockSize]; Array.Copy(cipherBytes, offset, block, 0, currentBlockSize); byte[] decryptedBlock rsa.Decrypt(block, padding); msOutput.Write(decryptedBlock, 0, decryptedBlock.Length); } return Encoding.UTF8.GetString(msOutput.ToArray()); } } else { byte[] decryptedBytes rsa.Decrypt(cipherBytes, padding); return Encoding.UTF8.GetString(decryptedBytes); } } } private static int GetMaxEncryptBlockSize(RSA rsa, RSAEncryptionPadding padding) { int keySize rsa.KeySize; // 不同填充模式占用不同字节数 if (padding RSAEncryptionPadding.Pkcs1) { // PKCS#1 v1.5 填充占用11字节 return (keySize / 8) - 11; } else if (padding.Mode RSAEncryptionPaddingMode.Oaep) { // OAEP填充占用更多具体取决于Hash算法大小。SHA256是32字节加上其他开销约66字节。 // 简化计算对于OAEP-SHA2562048位密钥(256字节)最大明文长度约为 256 - 2*32 - 2 ≈ 190字节 // 这里我们保守一点用一个通用公式估算。更准确的做法是查阅文档或测试。 int hashSizeInBytes padding.OaepHashAlgorithm.Name switch { SHA1 20, SHA256 32, SHA384 48, SHA512 64, _ 32 // 默认按SHA256算 }; return (keySize / 8) - (2 * hashSizeInBytes) - 2; } throw new NotSupportedException($不支持的填充模式: {padding.Mode}); }重要提醒上面演示的分段加密解密是一种反模式RSA算法设计上就不适合直接加密大量数据因为它速度慢且有长度限制。正确的做法是采用“混合加密”使用RSA来加密一个随机生成的对称密钥如AES密钥然后用这个对称密钥去加密实际的大数据。我们的工具类应该引导用户使用这种模式。4. 高级话题与生产环境实战4.1 混合加密RSAAES的最佳实践在实际项目中几乎不会直接用RSA加密业务数据。标准的做法是发送方生成一个随机的AES密钥比如256位。发送方用接收方的RSA公钥加密这个AES密钥。发送方用这个AES密钥加密实际要发送的明文数据。发送方将加密后的AES密钥RSA加密结果和加密后的数据AES加密结果一起发送给接收方。接收方用自己的RSA私钥解密出AES密钥。接收方用解密出的AES密钥解密出原始数据。我们来完善工具类增加一个EncryptLargeData方法public static class HybridEncryptionHelper { public static (string EncryptedSessionKey, string EncryptedData) EncryptLargeData(string plainText, string rsaPublicKey) { // 1. 生成随机AES密钥和IV using (Aes aes Aes.Create()) { aes.KeySize 256; aes.GenerateKey(); aes.GenerateIV(); byte[] sessionKey aes.Key; byte[] iv aes.IV; // 2. 用AES加密数据 byte[] plainBytes Encoding.UTF8.GetBytes(plainText); byte[] encryptedData; using (var encryptor aes.CreateEncryptor()) using (var ms new MemoryStream()) { using (var cs new CryptoStream(ms, encryptor, CryptoStreamMode.Write)) { cs.Write(plainBytes, 0, plainBytes.Length); cs.FlushFinalBlock(); } encryptedData ms.ToArray(); } // 3. 用RSA公钥加密AES密钥 string encryptedSessionKey; using (RSA rsa RsaHelper.CreateRsaProviderFromKey(rsaPublicKey, false)) { // 将Key和IV一起加密或者分别加密。这里简单起见将Key和IV拼接后加密。 byte[] keyAndIv new byte[sessionKey.Length iv.Length]; Buffer.BlockCopy(sessionKey, 0, keyAndIv, 0, sessionKey.Length); Buffer.BlockCopy(iv, 0, keyAndIv, sessionKey.Length, iv.Length); byte[] encryptedKeyBytes rsa.Encrypt(keyAndIv, RSAEncryptionPadding.OaepSHA256); encryptedSessionKey Convert.ToBase64String(encryptedKeyBytes); } return (encryptedSessionKey, Convert.ToBase64String(encryptedData)); } } public static string DecryptLargeData(string encryptedSessionKey, string encryptedData, string rsaPrivateKey) { byte[] encryptedKeyBytes Convert.FromBase64String(encryptedSessionKey); byte[] cipherDataBytes Convert.FromBase64String(encryptedData); // 1. 用RSA私钥解密出AES密钥和IV byte[] decryptedKeyAndIv; using (RSA rsa RsaHelper.CreateRsaProviderFromKey(rsaPrivateKey, true)) { decryptedKeyAndIv rsa.Decrypt(encryptedKeyBytes, RSAEncryptionPadding.OaepSHA256); } // 2. 拆分出Key和IV int keySize 32; // AES-256 key size in bytes byte[] sessionKey new byte[keySize]; byte[] iv new byte[16]; // AES block size is 16 bytes for IV Buffer.BlockCopy(decryptedKeyAndIv, 0, sessionKey, 0, keySize); Buffer.BlockCopy(decryptedKeyAndIv, keySize, iv, 0, iv.Length); // 3. 用AES解密数据 using (Aes aes Aes.Create()) { aes.Key sessionKey; aes.IV iv; using (var decryptor aes.CreateDecryptor()) using (var ms new MemoryStream(cipherDataBytes)) using (var cs new CryptoStream(ms, decryptor, CryptoStreamMode.Read)) using (var sr new StreamReader(cs)) { return sr.ReadToEnd(); } } } }这才是生产环境中应该使用的模式。我们的RsaHelper应该专注于安全地处理密钥和加密/解密小数据如对称密钥。4.2 签名与验签确保数据完整性与来源RSA另一个核心用途是数字签名。它用私钥“签名”数据任何人都可以用对应的公钥“验签”来验证数据是否被篡改、是否来自私钥持有者。public static class RsaSignatureHelper { public static string SignData(string data, string privateKey, HashAlgorithmName hashAlgorithm default) { hashAlgorithm hashAlgorithm default ? HashAlgorithmName.SHA256 : hashAlgorithm; byte[] dataBytes Encoding.UTF8.GetBytes(data); using (RSA rsa RsaHelper.CreateRsaProviderFromKey(privateKey, true)) { byte[] signature rsa.SignData(dataBytes, hashAlgorithm, RSASignaturePadding.Pkcs1); return Convert.ToBase64String(signature); } } public static bool VerifyData(string data, string signature, string publicKey, HashAlgorithmName hashAlgorithm default) { hashAlgorithm hashAlgorithm default ? HashAlgorithmName.SHA256 : hashAlgorithm; byte[] dataBytes Encoding.UTF8.GetBytes(data); byte[] signatureBytes Convert.FromBase64String(signature); using (RSA rsa RsaHelper.CreateRsaProviderFromKey(publicKey, false)) { return rsa.VerifyData(dataBytes, signatureBytes, hashAlgorithm, RSASignaturePadding.Pkcs1); } } }注意事项签名和加密使用不同的填充模式。签名常用RSASignaturePadding.Pkcs1或Pss。Pkcs1兼容性好Pss(Probabilistic Signature Scheme) 在理论上更安全。选择时需要与验签方约定一致。4.3 性能优化与密钥管理性能RSA操作尤其是解密和签名是CPU密集型操作。对于高频场景可以考虑使用更短的密钥如2048位而非4096位在安全性和性能间权衡。缓存RSA实例。但要注意线程安全最好每个线程或每次操作使用独立的实例或者使用ThreadLocalRSA。对于验签这种用公钥的操作性能压力相对较小公钥也可以安全地缓存和共享。密钥管理私钥必须绝对保密永远不要硬编码在代码中或提交到版本库。应该存储在安全的密钥管理系统如Azure Key Vault, AWS KMS、硬件安全模块HSM或至少是受保护的环境变量/配置文件中。公钥可以公开分发。通常放在配置文件中或通过安全的通道获取。密钥轮换定期更换密钥是良好的安全实践。设计系统时应支持多版本密钥共存平滑过渡。5. 常见问题排查与调试技巧在实际集成和使用中你肯定会遇到各种报错。下面是我总结的一些常见问题及排查思路。5.1 典型错误与解决方案速查表错误现象可能原因排查步骤与解决方案CryptographicException: The parameter is incorrect.或Bad Data1. 密钥格式错误。2. 密钥与操作不匹配如用私钥加密。3. 填充模式不匹配。4. 密文或数据损坏。1. 检查密钥字符串是否完整PEM格式头尾是否正确Base64解码是否成功。2. 确认Encrypt用公钥Decrypt用私钥Sign用私钥Verify用公钥。3.确保加密方和解密方使用完全相同的RSAEncryptionPadding。这是最常见的原因与第三方系统对接时必须明确约定是PKCS1还是OAEP-SHA256。4. 检查数据传输过程是否有编码问题如URL编码损坏了Base64的和/。CryptographicException: Key does not exist.尝试使用不支持的操作或密钥参数不全。1. 导入的私钥可能不完整例如只导入了公钥参数。2. 在使用SignData或Decrypt时确保提供的确实是私钥且包含了必要的私钥参数D, P, Q等。跨平台加解密结果不一致1. 默认填充模式不同。2. 密钥格式兼容性问题。3. 数据编码差异UTF-8 vs UTF-16。1. 显式指定填充模式不要依赖默认值。Linux/OpenSSL世界默认常用PKCS1。2. 统一使用PKCS#8 PEM格式的密钥进行交换。可以使用OpenSSL命令转换openssl pkcs8 -topk8 -inform PEM -in private.pkcs1.pem -outform PEM -nocrypt -out private.pkcs8.pem。3. 加密前双方明确约定将字符串转换为字节数组的编码强烈推荐UTF-8。The input data is not a complete block.解密时密文长度不是密钥字节长度的整数倍可能发生在分段解密逻辑中。1. 确认是否使用了分段加密。如果用了解密也必须分段且分段大小必须与加密时一致通常是密钥字节长度。2. 更推荐改用混合加密模式避免直接对长数据进行RSA分段。性能瓶颈CPU占用高频繁创建RSA对象或使用过长密钥加密大量数据。1. 为RSA对象实现简单的对象池或缓存注意线程安全。2.绝对不要用RSA直接加密超过GetMaxEncryptBlockSize的数据改用混合加密。3. 评估是否可用ECDSA椭圆曲线加密替代RSA在相同安全强度下性能更好。5.2 调试与日志记录建议在开发阶段加入详细的日志记录能快速定位问题。public static string SafeEncrypt(string plainText, string publicKey, RSAEncryptionPadding padding) { _logger.LogDebug($开始RSA加密明文长度: {plainText?.Length}, 密钥前20位: {publicKey?.Substring(0, Math.Min(20, publicKey.Length))}...); try { using (RSA rsa CreateRsaProviderFromKey(publicKey, false)) { _logger.LogDebug($RSA对象创建成功密钥大小: {rsa.KeySize}, 填充模式: {padding}); byte[] plainBytes Encoding.UTF8.GetBytes(plainText); _logger.LogDebug($明文UTF-8字节长度: {plainBytes.Length}, 最大加密块大小: {GetMaxEncryptBlockSize(rsa, padding)}); // ... 加密逻辑 string result Convert.ToBase64String(cipherBytes); _logger.LogDebug($加密成功密文Base64长度: {result.Length}); return result; } } catch (Exception ex) { _logger.LogError(ex, $RSA加密失败。明文: {plainText}, 公钥格式: {(publicKey?.Contains(BEGIN) ?? false ? PEM : Other)}); throw; // 或返回特定错误信息 } }记录密钥指纹如公钥的MD5或SHA256而不是完整密钥既有助于调试又不会泄露敏感信息。5.3 与外部系统如OpenSSL、Java对接这是最复杂的场景。务必建立一份“对接清单”密钥格式双方统一使用PKCS#8 PEM。填充方案明确是PKCS#1 v1.5还是OAEP。如果是OAEPHash算法是什么SHA1? SHA256?。数据编码加密/签名前字符串转字节数组的编码UTF-8。输出格式密文和签名的输出格式Base64还是 Hex。先做端到端测试用对方提供的公钥加密一个简单字符串如test让对方用私钥解密确认基础流程通顺。例如用我们的工具生成一个OpenSSL可读的密钥对并测试var keys RsaHelper.GenerateKeyPair(2048, Pkcs8Pem); File.WriteAllText(private.pem, keys.PrivateKey); File.WriteAllText(public.pem, keys.PublicKey);然后在bash中用OpenSSL验证# 用我们的公钥加密 echo -n Hello from .NET | openssl pkeyutl -encrypt -pubin -inkey public.pem -pkeyopt rsa_padding_mode:oaep -pkeyopt rsa_oaep_md:sha256 | base64 encrypted.txt # 将encrypted.txt的内容用我们的工具解密应该能得到原文。我个人的体会是加密解密本身不复杂但“细节魔鬼”全在格式、编码和参数的匹配上。花时间在项目初期明确并记录下这些规范能节省后期大量的联调时间。最后安全无小事对于真正高敏感的数据务必咨询安全专家并考虑使用专业的密钥管理服务和硬件安全模块。