.NET WebApi实现RSA加密与解密,签名与验签

柳向明
2023-12-01

业务场景需求

现有请求方A,与接收方B,以下是请求方的操作:

以下是请求方的操作:
假设 A 传输的内容为 Mark
1.组装报文,如: RSA_Mark (按照消息头_非业务参数_业务参数排序),使用固定的消息头 RSA 方便知道对方解密成功
2.用 A的私钥 对报文 RSA_Mark 签名,假设签名结果为 XJ9B5D1
3.把签名结果组装在原报文末尾,如:RSA_Mark_XJ9B5D1
4.用 B的公钥 对报文 RSA_Mark_XJ9B5D1 加密,结果假设为: NE03WBEN12=
5.将加密结果 NE03WBEN12= 发送给 B


以下是接收方的操作:
1.接到密文 NE03WBEN12=
2.用 B的私钥 进行解密,得到:RSA_Mark_XJ9B5D1
3.检验报文消息头是否为 RSA ,以检验是否是用 B的公钥 进行加密
4.解密成功后, 截取签名 消息尾得到: XJ9B5D1
5.用 A的公钥消息体 进行验签,待验证的消息体为 RSA_Mark ,签名值 XJ9B5D1
6.若成功验签,贼说明该消息来自 A 合法数据

(注:报文排序规则,根据非业务参数和业务参数拼接字符串并按照首字母排序,如果首字母相同,则按照第二个字母排序,以此类推
如:rp_13510103189_1540803537222_1_1540803537_10
: rp_{mobile.value}{sn.value}{source.value}{timestamp.value}{ua.value}

生成RSA密钥对

private const int RsaKeySize = 1024;        //要使用的密钥的大小(以位为单位)
private const string publicKeyFileName = "ServerRSA.Pub";         //公钥
private const string privateKeyFileName = "ServerRSA.Private";    //私钥
private static string basePathToStoreKeys = ConfigurationManager.AppSettings["basePathToStoreServerKeys"];	//从配置文件中读取密钥存放路径

public static string GenerateKeys()
        {
            string path = basePathToStoreKeys;

            using (var rsa = new RSACryptoServiceProvider(RsaKeySize))
            {
                try
                {
                    // 获取私钥和公钥。
                    var publicKey = rsa.ToXmlString(false);
                    var privateKey = rsa.ToXmlString(true);

                    if (!Directory.Exists(path))
                    {
                        Directory.CreateDirectory(path);
                    }

                    bool result = false;
                    string resultMsg = "该路径已存在密钥对,生成失败";

                    // 保存到磁盘
                    if (!File.Exists(Path.Combine(path, publicKeyFileName)))
                    {
                        File.WriteAllText(Path.Combine(path, publicKeyFileName), publicKey);
                        result = true;
                    }
                    if (!File.Exists(Path.Combine(path, privateKeyFileName)))
                    {
                        File.WriteAllText(Path.Combine(path, privateKeyFileName), privateKey);
                        result = true;
                    }

                    if (result)
                    {
                        resultMsg = string.Format("生成的RSA密钥对的路径: {0}\\ [{1}, {2}]", path, publicKeyFileName, privateKeyFileName);
                    }

                    return resultMsg;
                }
                finally
                {
                    rsa.PersistKeyInCsp = false;
                }
            }
        }

报文根据规则进行排序

这里的方法是把对象转为字典,然后对Key值进行升序输出字符串

public static Dictionary<string, object> ObjConvertDic(Dictionary<string, object> dic, T obj)
        {
            //判空
            if (obj == null)
            {
                return dic;
            }

            Type t = obj.GetType(); // 获取对象对应的类, 对应的类型

            PropertyInfo[] pi = t.GetProperties(BindingFlags.Public | BindingFlags.Instance); // 获取当前type公共属性

            string dickeyname = string.Empty;   //用于存储 表名+字段名

            foreach (PropertyInfo p in pi)
            {
                MethodInfo m = p.GetGetMethod();

                if (m != null && m.IsPublic)
                {
                    dickeyname = t.Name + "_" + p.Name;

                    // 进行判NULL处理 以及 重复键处理
                    if (m.Invoke(obj, new object[] { }) != null && !dic.ContainsKey(dickeyname))
                    {
                        dic.Add(dickeyname, m.Invoke(obj, new object[] { })); // 向字典添加元素
                    }
                }
            }
            return dic;
        }

        /// <summary>
        /// 字典中将key值进行升序排序,并将对应的value值拼接为字符串输出
        /// </summary>
        /// <param name="dic"></param>
        /// <returns></returns>
        public static string DicSortToString(Dictionary<string, object> dic)
        {
            //根据字典键升序
            var ascDic = from objDic in dic orderby objDic.Key ascending select objDic;

            //报文消息头
            string str = ConfigurationManager.AppSettings["messageHeader"];

            foreach (var item in ascDic)
            {
                str += "_" + item.Value;
            }

            return str;
        }

用请求方的私钥对报文签名

public static string privateToSign(string str)
        {
            //判空
            if(string.IsNullOrEmpty(str))
            {
                return null;
            }

            //要签名文本编码为base64
            byte[] hashByteSignture = System.Text.Encoding.Unicode.GetBytes(str);

            //加载私钥
            RSACryptoServiceProvider rsa = new RSACryptoServiceProvider();
            var privateXmlKey = File.ReadAllText(Path.Combine(basePathToStoreKeys, privateKeyFileName));
            rsa.FromXmlString(privateXmlKey);
            
            //哈希算法:SHA1(160bit)、SHA256(256bit)、MD5(128bit)
            byte[] sign = rsa.SignData(hashByteSignture, CryptoConfig.MapNameToOID("SHA1"));

            return Convert.ToBase64String(sign);
        }

用接收方的公钥对报文加密

将生成的签名追加到原报文尾部得到新报文
这里加密使用了分段加密,因为1024位的证书,加密时最大支持117个字节,解密时为128;2048位的证书,加密时最大支持245个字节,解密时为256。

/// <summary>
        /// 用给定路径的RSA公钥文件加密纯文本。
        /// </summary>
        /// <param name="plainText">要加密的文本</param>
        /// <param name="pathToPublicKey">用于加密的公钥路径.</param>
        /// <returns>表示加密数据的64位编码字符串.</returns>
        private static string Encrypt(string plainText, string pathToPublicKey)
        {
            using (var rsa = new RSACryptoServiceProvider(RsaKeySize))
            {
                try
                {
                    //加载公钥读取xml
                    string publicXmlKey = File.ReadAllText(pathToPublicKey);
                    rsa.FromXmlString(publicXmlKey);

                    byte[] bytesToEncrypt = Encoding.Unicode.GetBytes(plainText);

                    //分段加密 
                    int keySize = rsa.KeySize / 8;
                    int bufferSize = keySize - 11;
                    byte[] buffer = new byte[bufferSize];

                    //内存流,为系统内存提供读写操作
                    MemoryStream msInput = new MemoryStream(bytesToEncrypt);
                    MemoryStream msOuput = new MemoryStream();
                    int readLen = msInput.Read(buffer, 0, bufferSize);

                    while (readLen > 0)
                    {
                        byte[] dataToEnc = new byte[readLen];
                        Array.Copy(buffer, 0, dataToEnc, 0, readLen);
                        //加密  使用从缓冲区读取的数据将字节块写入当前流
                        byte[] encData = rsa.Encrypt(dataToEnc, false);
                        msOuput.Write(encData, 0, encData.Length);
                        readLen = msInput.Read(buffer, 0, bufferSize);
                    }

                    msInput.Close();
                    byte[] result = msOuput.ToArray();    //得到加密结果
                    msOuput.Close();
                    //var bytesEncrypted = rsa.Encrypt(bytesToEncrypt, false);

                    return Convert.ToBase64String(result);
                }
                finally
                {
                    rsa.PersistKeyInCsp = false;
                }
            }
        }

发送密文给接收方,用接收方的密钥解密

注:非业务参数使用HTTP请求的header头解析。

/// <summary>
        /// Decrypts encrypted text given a RSA private key file path.给定路径的RSA私钥文件解密 加密文本
        /// </summary>
        /// <param name="encryptedText">密文</param>
        /// <param name="pathToPrivateKey">用于解密的私钥路径.</param>
        /// <returns>未加密数据的字符串</returns>
        private static string Decrypt(string encryptedText, string pathToPrivateKey)
        {
            using (var rsa = new RSACryptoServiceProvider(RsaKeySize))
            {
                try
                {
                    //加载私钥
                    string privateXmlKey = File.ReadAllText(pathToPrivateKey);
                    rsa.FromXmlString(privateXmlKey);

                    byte[] bytesEncrypted = Convert.FromBase64String(encryptedText);

                    //分段解密
                    int keySize = rsa.KeySize / 8;
                    byte[] buffer = new byte[keySize];

                    //内存流,为系统内存提供读写操作
                    MemoryStream msInput = new MemoryStream(bytesEncrypted);
                    MemoryStream msOuput = new MemoryStream();
                    int readLen = msInput.Read(buffer, 0, keySize);

                    while (readLen > 0)
                    {
                        byte[] dataToDec = new byte[readLen];
                        Array.Copy(buffer, 0, dataToDec, 0, readLen);
                        //解密    使用从缓冲区读取的数据将字节块写入当前流
                        byte[] encData = rsa.Decrypt(dataToDec, false);
                        msOuput.Write(encData, 0, encData.Length);
                        readLen = msInput.Read(buffer, 0, keySize);
                    }

                    //关闭内存流
                    msInput.Close();
                    byte[] result = msOuput.ToArray();    //得到解密结果
                    msOuput.Close();

                    return System.Text.Encoding.Unicode.GetString(result);
                }
                catch (Exception ex)
                {
                    return ex.ToString();
                }
                finally
                {
                    rsa.PersistKeyInCsp = false;
                }
            }
        }

接收方对数据验签

/// <summary>
        /// 验签
        /// </summary>
        /// <param name="sign"></param>
        /// <returns></returns>
        public static string CheckSign(string message)
        {
            //获取非业务参数header对象的长度
            HeadersInfo headersInfo = new HeadersInfo();
            PropertyInfo[] propertyInfo = headersInfo.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance);

            //截取第一个下划线'_'前的文本为消息头,最后一个下划线'_'后的文本为签名
            string[] list = message.Split('_');
            //判断长度是否合法
            if (list.Length < propertyInfo.Length)
            {
                return UtilityEnum.InspectionResult.Invalid.ToString();
            }

            string messageHeader = list[0];
            string timestamp = list[1];
            //要验证的签名数据
            string signature = list[list.Length - 1];
            byte[] hashByteSignature = Convert.FromBase64String(signature);

            //查看消息头是否正确
            if (messageHeader != ConfigurationManager.AppSettings["messageHeader"])
            {
                return UtilityEnum.InspectionResult.Invalid.ToString();
            }

            //文本截取签名(含下划线'_')后,是已签名的数据
            string buffer = message.Substring(0, message.Length - signature.Length - 1);
            byte[] fromBase64Buffer = Encoding.Unicode.GetBytes(buffer);

            //加载发送方的公钥进行验签
            var rsa = new RSACryptoServiceProvider();
            var publicXmlKey = File.ReadAllText(Path.Combine(ConfigurationManager.AppSettings["basePathToStoreClientKeys"], "ClientRSA.Pub"));
            rsa.FromXmlString(publicXmlKey);

            //MD5 mD5 = new MD5CryptoServiceProvider();
            //rsa.VerifyData(hashByteSignature, mD5, Convert.FromBase64String(buffer));
            //rsa.VerifyData(hashByteSignature, CryptoConfig.MapNameToOID("MD5"), Convert.FromBase64String(buffer));

            //哈希算法:SHA1(160bit)、SHA256(256bit)、MD5(128bit)
            if (rsa.VerifyData(fromBase64Buffer, CryptoConfig.MapNameToOID("SHA1"), hashByteSignature))
            {
                //判断timestamp是否超时
                if (UtilityHelper.IsTimestampValidity(timestamp))
                {
                    return UtilityEnum.InspectionResult.Timeout.ToString();
                }
            }
            else
            {
                return UtilityEnum.InspectionResult.Invalid.ToString();
            }

            return UtilityEnum.InspectionResult.Validity.ToString();
        }

关于Convert.ToBase64String(Byte[])和Encoding.UTF8.GetString(Byte[])的区别

对加密方法返回的byte[],用Convert.ToBase64String
对普通的文字操作,用Encoding.UTF8.GetBytes()

Encoding.UTF8.GetString是针对使用utf8编码得到的字符串对应的byte[]使用,可以还原我们能看懂的字符串
而Convert.ToBase64String是对任意byte[]都可使用,得到的是用字符串表示的byte[]信息 内容类似"QJ5/EYPtJPZ5Inyv="

如果一个地方用Convert.ToBase64String来操作byte[]获得string,而另一个地方要用相同的byte[],最好对应用 Convert.FromBase64String(string);

一般 Base64 用于转格式,如:图片。
Encoding 用于转换编码,如:文字(普通文字不是Base64 编码)

以上。
如有不合理的地方或更好的建议,请不吝赐教,谢谢!

 类似资料: