【實用小技巧】RSA非對稱加解密及XML&PEM格式互換方案

​ 最近因考慮接口安全問題,有實現給WEB API實現統一的參數鑑權功能,以防止請求參數被篡改或重複執行,參數鑑權方法基本與常見的鑑權思路相同,採用(timestamp+sign),而我爲了防止timestamp被更改,sign算法(timestamp+相關參數排序、格式化後拼接再MD5)也由於在前端是不安全的,故對timestamp採起使用非對稱加解密,以儘量的保證生成的sign不易被破解或替換;html

RSA加解密(即:非對稱加解密)

生成公鑰、私鑰對方法(C#),生成出來後默認都是XML格式:前端

public static Tuple<string, string> GeneratePublicAndPrivateKeyPair()
        {
            using (RSACryptoServiceProvider rsa = new RSACryptoServiceProvider())
            {
                string publicKey = rsa.ToXmlString(false); // 公鑰
                string privateKey = rsa.ToXmlString(true); // 私鑰

                return Tuple.Create(publicKey, privateKey);
            }
        }

使用公鑰加密:(支持分段加密,普通單次加密可能會由於內容過長而報錯)

public static string RSAEncrypt(string publicKey, string rawInput)
        {
            if (string.IsNullOrEmpty(rawInput))
            {
                return string.Empty;
            }

            if (string.IsNullOrWhiteSpace(publicKey))
            {
                throw new ArgumentException("Invalid Public Key");
            }

            using (var rsaProvider = new RSACryptoServiceProvider())
            {
                var inputBytes = Encoding.UTF8.GetBytes(rawInput);//有含義的字符串轉化爲字節流
                rsaProvider.FromXmlString(publicKey);//載入公鑰
                int bufferSize = (rsaProvider.KeySize / 8) - 11;//單塊最大長度
                var buffer = new byte[bufferSize];
                using (MemoryStream inputStream = new MemoryStream(inputBytes),
                     outputStream = new MemoryStream())
                {
                    while (true)
                    { //分段加密
                        int readSize = inputStream.Read(buffer, 0, bufferSize);
                        if (readSize <= 0)
                        {
                            break;
                        }

                        var temp = new byte[readSize];
                        Array.Copy(buffer, 0, temp, 0, readSize);
                        var encryptedBytes = rsaProvider.Encrypt(temp, false);
                        outputStream.Write(encryptedBytes, 0, encryptedBytes.Length);
                    }
                    return Convert.ToBase64String(outputStream.ToArray());//轉化爲字節流方便傳輸
                }
            }
        }

使用私鑰解密:(支持分段解密,普通單次解密可能會由於密文過長而報錯)

public static string RSADecrypt(string privateKey,string encryptedInput)
        {
            if (string.IsNullOrEmpty(encryptedInput))
            {
                return string.Empty;
            }

            if (string.IsNullOrWhiteSpace(privateKey))
            {
                throw new ArgumentException("Invalid Private Key");
            }

            using (var rsaProvider = new RSACryptoServiceProvider())
            {
                var inputBytes = Convert.FromBase64String(encryptedInput);
                rsaProvider.FromXmlString(privateKey);
                int bufferSize = rsaProvider.KeySize / 8;
                var buffer = new byte[bufferSize];
                using (MemoryStream inputStream = new MemoryStream(inputBytes),
                     outputStream = new MemoryStream())
                {
                    while (true)
                    {
                        int readSize = inputStream.Read(buffer, 0, bufferSize);
                        if (readSize <= 0)
                        {
                            break;
                        }

                        var temp = new byte[readSize];
                        Array.Copy(buffer, 0, temp, 0, readSize);
                        var rawBytes = rsaProvider.Decrypt(temp, false);
                        outputStream.Write(rawBytes, 0, rawBytes.Length);
                    }
                    return Encoding.UTF8.GetString(outputStream.ToArray());
                }
            }
        }

若是都是C#項目可能如上兩個方法就能夠了,但若是須要與WEB前端、JAVA等其它編程語言協同交互處理時(好比:WEB前端用公鑰加密,後端C#私鑰解密),則可能由於公鑰與私鑰的格式不相同而致使沒法正常的進行對接【前端、JAVA 等語言使用的是PEM格式的,而C#使用的是XML格式】,網上查XML轉PEM格式方案時,都是複製自:http://www.javashuo.com/article/p-epyajnzz-nz.html 這篇文章,但其實這篇文章也只是寫了私鑰XML轉PEM格式,並無說明公鑰XML如何轉PEM格式,並且只寫了支持從文件中獲取內容再轉換,方案不全,可是給了我(夢在旅途,http://www.zuowenjun.cn or zuowj.cnblogs.com.cn)思路,我通過各類驗證,最終實現了比較友好的PEM與XML格式的相互轉換方式,且通過單元測試驗證經過,在此分享給你們。算法

以下是完整的XML與PEM格式轉換器類代碼;(注意需引入BouncyCastle nuget包)

using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.Math;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;

namespace Zuowj.Common
{
    /// <summary>
    /// RSA公鑰、私鑰對格式(XML與PEM)轉換器
    /// author:zuowenjun
    /// date:2020-12-29
    /// </summary>
    public static class RsaKeysFormatConverter
    {
        /// <summary>
        /// XML公鑰轉成Pem公鑰
        /// </summary>
        /// <param name="xmlPublicKey"></param>
        /// <returns></returns>
        public static string XmlPublicKeyToPem(string xmlPublicKey)
        {
            RSAParameters rsaParam;
            using (RSACryptoServiceProvider rsa = new RSACryptoServiceProvider())
            {
                rsa.FromXmlString(xmlPublicKey);
                rsaParam = rsa.ExportParameters(false);
            }
            RsaKeyParameters param = new RsaKeyParameters(false, new BigInteger(1, rsaParam.Modulus), new BigInteger(1, rsaParam.Exponent));

            string pemPublicKeyStr = null;
            using (var ms = new MemoryStream())
            {
                using (var sw = new StreamWriter(ms))
                {
                    var pemWriter = new Org.BouncyCastle.OpenSsl.PemWriter(sw);
                    pemWriter.WriteObject(param);
                    sw.Flush();

                    byte[] buffer = new byte[ms.Length];
                    ms.Position = 0;
                    ms.Read(buffer, 0, (int)ms.Length);
                    pemPublicKeyStr = Encoding.UTF8.GetString(buffer);
                }
            }

            return pemPublicKeyStr;
        }

        /// <summary>
        /// Pem公鑰轉成XML公鑰
        /// </summary>
        /// <param name="pemPublicKeyStr"></param>
        /// <returns></returns>
        public static string PemPublicKeyToXml(string pemPublicKeyStr)
        {
            RsaKeyParameters pemPublicKey;
            using (var ms = new MemoryStream(Encoding.UTF8.GetBytes(pemPublicKeyStr)))
            {
                using (var sr = new StreamReader(ms))
                {
                    var pemReader = new Org.BouncyCastle.OpenSsl.PemReader(sr);
                    pemPublicKey = (RsaKeyParameters)pemReader.ReadObject();
                }
            }

            var p = new RSAParameters
            {
                Modulus = pemPublicKey.Modulus.ToByteArrayUnsigned(),
                Exponent = pemPublicKey.Exponent.ToByteArrayUnsigned()
            };

            string xmlPublicKeyStr;
            using (var rsa = new RSACryptoServiceProvider())
            {
                rsa.ImportParameters(p);
                xmlPublicKeyStr = rsa.ToXmlString(false);
            }

            return xmlPublicKeyStr;
        }

        /// <summary>
        /// XML私鑰轉成PEM私鑰
        /// </summary>
        /// <param name="xmlPrivateKey"></param>
        /// <returns></returns>
        public static string XmlPrivateKeyToPem(string xmlPrivateKey)
        {
            RSAParameters rsaParam;
            using (RSACryptoServiceProvider rsa = new RSACryptoServiceProvider())
            {
                rsa.FromXmlString(xmlPrivateKey);
                rsaParam = rsa.ExportParameters(true);
            }

            var param = new RsaPrivateCrtKeyParameters(
                new BigInteger(1, rsaParam.Modulus), new BigInteger(1, rsaParam.Exponent), new BigInteger(1, rsaParam.D),
                new BigInteger(1, rsaParam.P), new BigInteger(1, rsaParam.Q), new BigInteger(1, rsaParam.DP), new BigInteger(1, rsaParam.DQ),
                new BigInteger(1, rsaParam.InverseQ));

            string pemPrivateKeyStr = null;
            using (var ms = new MemoryStream())
            {
                using (var sw = new StreamWriter(ms))
                {
                    var pemWriter = new Org.BouncyCastle.OpenSsl.PemWriter(sw);
                    pemWriter.WriteObject(param);
                    sw.Flush();

                    byte[] buffer = new byte[ms.Length];
                    ms.Position = 0;
                    ms.Read(buffer, 0, (int)ms.Length);
                    pemPrivateKeyStr = Encoding.UTF8.GetString(buffer);
                }
            }

            return pemPrivateKeyStr;
        }

        /// <summary>
        /// Pem私鑰轉成XML私鑰
        /// </summary>
        /// <param name="pemPrivateKeyStr"></param>
        /// <returns></returns>
        public static string PemPrivateKeyToXml(string pemPrivateKeyStr)
        {
            RsaPrivateCrtKeyParameters pemPrivateKey;
            using (var ms = new MemoryStream(Encoding.UTF8.GetBytes(pemPrivateKeyStr)))
            {
                using (var sr = new StreamReader(ms))
                {
                    var pemReader = new Org.BouncyCastle.OpenSsl.PemReader(sr);
                    var keyPair = (AsymmetricCipherKeyPair)pemReader.ReadObject();
                    pemPrivateKey = (RsaPrivateCrtKeyParameters)keyPair.Private;
                }
            }

            var p = new RSAParameters
            {
                Modulus = pemPrivateKey.Modulus.ToByteArrayUnsigned(),
                Exponent = pemPrivateKey.PublicExponent.ToByteArrayUnsigned(),
                D = pemPrivateKey.Exponent.ToByteArrayUnsigned(),
                P = pemPrivateKey.P.ToByteArrayUnsigned(),
                Q = pemPrivateKey.Q.ToByteArrayUnsigned(),
                DP = pemPrivateKey.DP.ToByteArrayUnsigned(),
                DQ = pemPrivateKey.DQ.ToByteArrayUnsigned(),
                InverseQ = pemPrivateKey.QInv.ToByteArrayUnsigned(),
            };

            string xmlPrivateKeyStr;
            using (var rsa = new RSACryptoServiceProvider())
            {
                rsa.ImportParameters(p);
                xmlPrivateKeyStr = rsa.ToXmlString(true);
            }

            return xmlPrivateKeyStr;
        }

    }
}

以下是單元測試代碼:

//公鑰(XML、PEM格式互)測試
string srcPublicKey = 「具體的XML Public Key」;
            string pemPublicKeyStr=  RsaKeysFormatConverter.XmlPublicKeyToPem(publicKey);
            string xmlPublicKeyStr= RsaKeysFormatConverter.PemPublicKeyToXml(pemPublicKeyStr);
            Assert.AreEqual(srcPublicKey, xmlPublicKeyStr);
//私鑰(XML、PEM格式互)測試
string srcPrivateKey = 「具體的XML Private Key」;
            string pemPrivateKeyStr = RsaKeysFormatConverter.XmlPrivateKeyToPem(srcPrivateKey);
            string xmlPrivateKeyStr = RsaKeysFormatConverter.PemPrivateKeyToXml(pemPrivateKeyStr);
            Assert.AreEqual(privateKey,xmlPrivateKeyStr)

固然也能夠不用這麼費勁本身實現格式轉換,可使用在線網站直接轉換:https://the-x.cn/certificate/XmlToPem.aspx ,另外也有一篇文章實現了相似的功能,但生成的PEM格式並不是完整的格式,缺乏註釋頭尾:https://www.cnblogs.com/datous/p/RSAKeyConvert.html編程

相關文章
相關標籤/搜索