From 3d7ff8ac3c50d1c30bec1734da584aae77d32939 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=AB=98=E5=9D=9A=E6=9E=9C?= <753610399@qq.com> Date: Wed, 29 Apr 2020 07:26:22 +0800 Subject: [PATCH 01/15] =?UTF-8?q?=E5=8D=87=E7=BA=A71.1=E7=89=88=E6=9C=AC?= =?UTF-8?q?=E4=BB=A3=E7=A0=81=EF=BC=8C=E7=BB=93=E6=9E=84=E6=9B=B4=E5=90=88?= =?UTF-8?q?=E7=90=86=EF=BC=8C=E6=94=AF=E6=8C=81=E6=9B=B4=E5=B9=BF=E6=B3=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Program.cs | 32 ++- Properties/AssemblyInfo.cs | 8 +- README.md | 205 ++++++++++-------- RSA.cs | 72 +++++-- RSA_PEM.cs | 431 ++++++++++++++++++++++++++++++++----- RSA_Unit.cs | 60 ------ vs.csproj | 2 +- 7 files changed, 579 insertions(+), 231 deletions(-) delete mode 100644 RSA_Unit.cs diff --git a/Program.cs b/Program.cs index 7341f67..b189c18 100644 --- a/Program.cs +++ b/Program.cs @@ -1,20 +1,20 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -namespace RSA { +namespace com.github.xiangyuecn.rsacsharp { + /// + /// RSA、RSA_PEM测试控制台主程序 + /// GitHub: https://github.com/xiangyuecn/RSA-csharp + /// class Program { static void RSATest() { var rsa = new RSA(512); - Console.WriteLine("【512私钥(XML)】:"); + Console.WriteLine("【" + rsa.KeySize + "私钥(XML)】:"); Console.WriteLine(rsa.ToXML()); Console.WriteLine(); - Console.WriteLine("【512私钥(PEM)】:"); + Console.WriteLine("【" + rsa.KeySize + "私钥(PEM)】:"); Console.WriteLine(rsa.ToPEM_PKCS1()); Console.WriteLine(); - Console.WriteLine("【512公钥(PEM)】:"); + Console.WriteLine("【" + rsa.KeySize + "公钥(PEM)】:"); Console.WriteLine(rsa.ToPEM_PKCS1(true)); Console.WriteLine(); @@ -41,6 +41,22 @@ static void RSATest() { Console.WriteLine("XML:" + (rsa3.ToXML() == rsa.ToXML())); Console.WriteLine("PKCS1:" + (rsa3.ToPEM_PKCS1() == rsa.ToPEM_PKCS1())); Console.WriteLine("PKCS8:" + (rsa3.ToPEM_PKCS8() == rsa.ToPEM_PKCS8())); + + //--------RSA_PEM验证--------- + RSA_PEM pem = rsa.ToPEM(); + Console.WriteLine("【RSA_PEM是否和原始RSA一致】:"); + Console.WriteLine(pem.KeySize + "位"); + Console.WriteLine("XML:" + (pem.ToXML(false) == rsa.ToXML())); + Console.WriteLine("PKCS1:" + (pem.ToPEM(false, false) == rsa.ToPEM_PKCS1())); + Console.WriteLine("PKCS8:" + (pem.ToPEM(false, true) == rsa.ToPEM_PKCS8())); + Console.WriteLine("仅公钥:"); + Console.WriteLine("XML:" + (pem.ToXML(true) == rsa.ToXML(true))); + Console.WriteLine("PKCS1:" + (pem.ToPEM(true, false) == rsa.ToPEM_PKCS1(true))); + Console.WriteLine("PKCS8:" + (pem.ToPEM(true, true) == rsa.ToPEM_PKCS8(true))); + + var rsa4 = new RSA(new RSA_PEM(pem.Key_Modulus, pem.Key_Exponent, pem.Key_D)); + Console.WriteLine("【用n、e、d构造解密】"); + Console.WriteLine(rsa4.DecodeOrNull(en)); } diff --git a/Properties/AssemblyInfo.cs b/Properties/AssemblyInfo.cs index 36132e0..8d60d1b 100644 --- a/Properties/AssemblyInfo.cs +++ b/Properties/AssemblyInfo.cs @@ -10,14 +10,14 @@ [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("")] [assembly: AssemblyProduct("RSA-csharp")] -[assembly: AssemblyCopyright("Copyright © 2018")] +[assembly: AssemblyCopyright("Copyright © xiangyuecn")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] // 将 ComVisible 设置为 false 使此程序集中的类型 // 对 COM 组件不可见。 如果需要从 COM 访问此程序集中的类型, // 则将该类型上的 ComVisible 特性设置为 true。 -[assembly: ComVisible(false)] +[assembly: ComVisible(true)] // 如果此项目向 COM 公开,则下列 GUID 用于类型库的 ID [assembly: Guid("7661a9da-7f07-403a-8e49-5224ae79a009")] @@ -29,5 +29,5 @@ // 生成号 // 修订号 // -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] +[assembly: AssemblyVersion("1.1.0.0")] +[assembly: AssemblyFileVersion("1.1.0.0")] diff --git a/README.md b/README.md index 3722c3e..b4a4d84 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,15 @@ -# RSA-csharp的帮助文档 +# :open_book:RSA-csharp的帮助文档 +本项目核心功能为:支持`.Net`环境下`PEM`(`PKCS#1`、`PKCS#8`)格式RSA密钥对导入、导出。 -## 跑起来 +附带实现了一个RSA封装操作类,和一个测试控制台程序。 + +支持`.NET Core`、`.NET Framework`;已移植到Java版:[RSA-java](https://github.com/xiangyuecn/RSA-java/blob/master/RSA_PEM.java);你可以只copy `RSA_PEM.cs` 文件到你的项目中使用,只需这一个文件你就拥有了通过PEM格式密钥创建`RSACryptoServiceProvider`的能力。 clone下来用vs应该能够直接打开,经目测看起来没什么卵用的文件都svn:ignore掉了(svn滑稽。 -## 主要支持 +## 提供支持 - 通过`XML格式`密钥对创建RSA - 通过`PEM格式`密钥对创建RSA @@ -17,13 +20,123 @@ clone下来用vs应该能够直接打开,经目测看起来没什么卵用的 - `PEM格式`秘钥对和`XML格式`秘钥对互转 -## 已知问题 -代码在.NET Framework 4.5测试过,如果要用在.NET Core下,除了RSA_PEM.cs可以copy过去用用外,其他几个文件不建议拿到.NET Core下使用;因为RSA_PEM里面除了RSACryptoServiceProvider、RSAParameters之外的代码是容易用别的语言来实现,而这两个玩意在这个文件里面几乎算是可有可无的东西。问题在于我用的RSACryptoServiceProvider在.NET Core下面就是个残废,参考[/issues/1](https://github.com/xiangyuecn/RSA-csharp/issues/1),手动额外实现一下FromXmlString、ToXmlString(关键是会去用这两个函数完全是被.NET Framework的RSACryptoServiceProvider逼的啊,要是他直接支持PEM秘钥,这个仓库都省了,不可能因为RSACryptoServiceProvider的XML格式而去支持XML格式~)。 + +# :open_book:文档 + +## 【RSA_PEM.cs】 +此文件不依赖任何文件,可以直接copy这个文件到你项目中用;通过`FromPEM`、`ToPEM` 和`FromXML`、`ToXML`这两对方法,可以实现PEM`PKCS#1`、`PKCS#8`相互转换,PEM、XML的相互转换。 + +项目里面需要引入程序集`System.Numerics`用来支持`BigInteger`,vs默认创建的项目是不会自动引入此程序集的,要手动引入。 + +注:openssl `RSAPublicKey_out`导出的公钥,字节码内并不带[OID](http://www.oid-info.com/get/1.2.840.113549.1.1.1)(目测是因为不带OID所以openssl自己都不支持用这个公钥来加密数据),RSA_PEM支持此格式公钥的导入,但不提供此种格式公钥的导出。 + +### 实例属性 + +`byte[]`:`Key_Modulus`(模数n,公钥、私钥都有)、`Key_Exponent`(公钥指数e,公钥、私钥都有)、`Key_D`(私钥指数d,只有私钥的时候才有);有这3个足够用来加密解密。 + +`byte[]`:`Val_P`(prime1)、`Val_Q`(prime2)、`Val_DP`(exponent1)、`Val_DQ`(exponent2)、`Val_InverseQ`(coefficient); (PEM中的私钥才有的更多的数值;可通过n、e、d反推出这些值(只是反推出有效值,和原始的值大概率不同))。 + +`int`:`KeySize`(密钥位数) + +`bool`:`HasPrivate`(是否包含私钥) + +### 构造方法 + +`RSA_PEM(RSACryptoServiceProvider rsa, bool convertToPublic = false)`:通过RSA中的公钥和私钥构造一个PEM,如果convertToPublic含私钥的RSA将只读取公钥,仅含公钥的RSA不受影响。 + +`RSA_PEM(byte[] modulus, byte[] exponent, byte[] d, byte[] p, byte[] q, byte[] dp, byte[] dq, byte[] inverseQ)`:通过全量的PEM字段数据构造一个PEM,除了模数modulus和公钥指数exponent必须提供外,其他私钥指数信息要么全部提供,要么全部不提供(导出的PEM就只包含公钥)注意:所有参数首字节如果是0,必须先去掉。 + +`RSA_PEM(byte[] modulus, byte[] exponent, byte[] dOrNull)`:通过公钥指数和私钥指数构造一个PEM,会反推计算出P、Q但和原始生成密钥的P、Q极小可能相同。注意:所有参数首字节如果是0,必须先去掉。出错将会抛出异常。私钥指数可以不提供,导出的PEM就只包含公钥。 + + +### 实例方法 + +`RSACryptoServiceProvider GetRSA()`:将PEM中的公钥私钥转成RSA对象,如果未提供私钥,RSA中就只包含公钥。 + +`string ToPEM(bool convertToPublic, bool usePKCS8)`:将RSA中的密钥对转换成PEM格式,usePKCS8=false时返回PKCS#1格式,否则返回PKCS#8格式,如果convertToPublic含私钥的RSA将只返回公钥,仅含公钥的RSA不受影响。 + +`string ToXML(bool convertToPublic)`:将RSA中的密钥对转换成XML格式,如果convertToPublic含私钥的RSA将只返回公钥,仅含公钥的RSA不受影响。 + + +### 静态方法 + +`static RSA_PEM FromPEM(string pem)`:用PEM格式密钥对创建RSA,支持PKCS#1、PKCS#8格式的PEM,出错将会抛出异常。pem格式如:`-----BEGIN XXX KEY-----....-----END XXX KEY-----`。 + +`static RSA_PEM FromXML(string xml)`:将XML格式密钥转成PEM,支持公钥xml、私钥xml,出错将会抛出异常。 + + + + +## 【RSA.cs】 +此文件依赖`RSA_PEM.cs`,封装了加密、解密、签名、验证、秘钥导入导出操作。 + +### 构造方法 + +`RSA(int keySize)`:用指定密钥大小创建一个新的RSA,会生成新密钥,出错抛异常。 + +`RSA(string xml)`:通过XML格式密钥,创建一个RSA,xml内可以只包含一个公钥或私钥,或都包含,出错抛异常。,`XML格式`如:`...` + +`RSA(string pem, bool noop)`:通过`PEM格式`密钥对创建RSA(noop参数随意填),PEM可以是公钥或私钥,支持`PKCS#1`、`PKCS#8`格式,pem格式如:`-----BEGIN XXX KEY-----....-----END XXX KEY-----`。 + +`RSA(RSA_PEM pem)`:通过一个pem对象创建RSA,pem为公钥或私钥,出错抛异常。 + + +### 实例属性 + +`RSACryptoServiceProvider`:`RSAObject`(最底层的RSACryptoServiceProvider对象) + +`int`:`KeySize`(密钥位数) + +`bool`:`HasPrivate`(是否包含私钥) + + +### 实例方法 + +`string ToXML(bool convertToPublic = false)`:导出`XML格式`秘钥对。如果RSA包含私钥,默认会导出私钥,设置仅仅导出公钥时只会导出公钥;不包含私钥只会导出公钥。 + +`string ToPEM_PKCS1(bool convertToPublic = false)`:导出`PEM PKCS#1格式`秘钥对。如果RSA包含私钥,默认会导出私钥,设置仅仅导出公钥时只会导出公钥;不包含私钥只会导出公钥。 + +`string ToPEM_PKCS8(bool convertToPublic = false)`:导出`PEM PKCS#8格式`秘钥对。如果RSA包含私钥,默认会导出私钥,设置仅仅导出公钥时只会导出公钥;不包含私钥只会导出公钥。 + +`RSA_PEM ToPEM(bool convertToPublic = false)`:导出RSA_PEM对象,如果convertToPublic含私钥的RSA将只返回公钥,仅含公钥的RSA不受影响。 + +`string Encode(string str)`:加密操作,支持任意长度数据。 + +`byte[] Encode(byte[] data)`:加密数据,支持任意长度数据,出错抛异常。 + +`string DecodeOrNull(string str)`:解密字符串(utf-8),解密异常返回null。 + +`byte[] DecodeOrNull(byte[] data)`:解密数据,解密异常返回null。 + +`string Sign(string hash, string str)`:对str进行签名,并指定hash算法(如:SHA256)。 + +`byte[] Sign(string hash, byte[] data)`:对data进行签名,并指定hash算法(如:SHA256)。 + +`bool Verify(string hash, string sgin, string str)`:验证字符串str的签名是否是sgin,并指定hash算法(如:SHA256)。 + +`bool Verify(string hash, byte[] sgin, byte[] data)`:验证data的签名是否是sgin,并指定hash算法(如:SHA256)。 + + + -## 前言、自述、还有啥 +# :open_book:图例 + +控制台运行: + +![控制台运行](images/1.png) + +RSA工具(非开源): + +![RSA工具](images/2.png) + + + + + +# :open_book:知识库 在写一个小转换工具时加入了RSA加密解密支持(见图RSA工具),秘钥输入框支持填写XML和PEM格式,操作类型里面支持XML->PEM、PEM->XML的转换。 @@ -266,83 +379,3 @@ yZKNX3VxmLEHXQ== ``` - - - -# C# RSA操作类 - - -## 主要文件 - -### RSA.cs -此文件依赖`RSA_PEM.cs`,用于进行加密、解密、签名、验证、秘钥导入导出操作。 - -#### [构造函数] new RSA(`1024`) -通过指定密钥长度来创建RSA,会生成新密钥。 - -#### [构造函数] new RSA(`""`) -通过`XML格式`密钥对创建RSA,xml可以是公钥或私钥,`XML格式`如: -``` -mMPfjN/kRMw7WKsAa7P2rPzsvBkROTpp6jldBH5BIgiI4nM21RBZ2d6kYy2wwE35gcTUOnOhYGvJ19vIJRB/2i/0RtinaSPCjFKigLMzcnbB0nofTEinHec4EuDbhNLvnkewgfJDloqYiw0JmN/JKTN+qUVnTUJaSGkw6OSISSc=AQAB -``` - -#### [构造函数] new RSA(`"PEM"`, `any`) -通过`PEM格式`密钥对创建RSA,PEM可以是公钥或私钥,支持`PKCS#1`、`PKCS#8`格式,`PEM格式`如: -``` ------BEGIN PUBLIC KEY----- -MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCYw9+M3+REzDtYqwBrs/as/Oy8 -GRE5OmnqOV0EfkEiCIjiczbVEFnZ3qRjLbDATfmBxNQ6c6Fga8nX28glEH/aL/RG -2KdpI8KMUqKAszNydsHSeh9MSKcd5zgS4NuE0u+eR7CB8kOWipiLDQmY38kpM36p -RWdNQlpIaTDo5IhJJwIDAQAB ------END PUBLIC KEY----- -``` - -#### [方法] .ToXML(`[false]是否仅仅导出公钥`) -导出`XML格式`秘钥对。如果RSA包含私钥,默认会导出私钥,设置仅仅导出公钥时只会导出公钥;不包含私钥只会导出公钥。 - -#### [方法] .ToPEM_PKCS1|.ToPEM_PKCS8(`[false]是否仅仅导出公钥`) -导出`PEM格式`秘钥对,两个方法分别导出`PKCS#1`、`PKCS#8`格式。如果RSA包含私钥,默认会导出私钥,设置仅仅导出公钥时只会导出公钥;不包含私钥只会导出公钥。 - -#### [方法] .Encode(`"字符串"|bytes`) -加密操作,支持任意长度数据。 - -#### [方法] .DecodeOrNull(`"Base64字符串"|bytes`) -解密操作,解密失败返回null,支持任意长度数据。 - -#### [方法] .Sign(`"hash"`, `"字符串"|bytes`) -通过hash算法(MD5、SHA1等)来对数据进行签名。 - -#### [方法] .Verify(`"hash"`, `"sign"`, `"字符串"`) -通过hash算法(MD5、SHA1等)来验证字符串是否和sign签名一致。 - - -### RSA_PEM.cs -此文件不依赖任何文件,可以单独copy来用(`RSA_Unit`里面的方法可以忽略) - -#### [静态方法] .FromPEM(`"PEM"`) -通过`PEM格式`秘钥对来创建`RSACryptoServiceProvider`,PEM可以是公钥或私钥。 - -#### [静态方法] .ToPEM(`RSACryptoServiceProvider`, `exportPublicOnly`, `usePKCS8`) -将RSA中的密钥对导出成`PEM格式`,`usePKCS8=false`时返回`PKCS#1格式`,否则返回`PKCS#8格式`。如果RSA包含私钥,默认会导出私钥,设置仅仅导出公钥时只会导出公钥;不包含私钥只会导出公钥。 - - - -## 次要文件 - -## RSA_Unit.cs -封装的一些通用方法,如:base64。没有此文件也可以,引用的地方用别的代码实现。 - -## Program.cs -控制台入口文件,用来测试的,里面包含了主要的使用用例。 - - - -## 图例 - -控制台运行: - -![控制台运行](images/1.png) - -RSA工具: - -![RSA工具](images/2.png) \ No newline at end of file diff --git a/RSA.cs b/RSA.cs index fc6aa42..242938c 100644 --- a/RSA.cs +++ b/RSA.cs @@ -1,14 +1,12 @@ using System; -using System.Collections.Generic; using System.IO; -using System.Linq; using System.Security.Cryptography; using System.Text; -using System.Threading.Tasks; -namespace RSA { +namespace com.github.xiangyuecn.rsacsharp { /// /// RSA操作类 + /// GitHub: https://github.com/xiangyuecn/RSA-csharp /// public class RSA { /// @@ -21,23 +19,29 @@ public string ToXML(bool convertToPublic = false) { /// 导出PEM PKCS#1格式密钥对,如果convertToPublic含私钥的RSA将只返回公钥,仅含公钥的RSA不受影响 /// public string ToPEM_PKCS1(bool convertToPublic = false) { - return RSA_PEM.ToPEM(rsa, convertToPublic, false); + return new RSA_PEM(rsa).ToPEM(convertToPublic, false); } /// /// 导出PEM PKCS#8格式密钥对,如果convertToPublic含私钥的RSA将只返回公钥,仅含公钥的RSA不受影响 /// public string ToPEM_PKCS8(bool convertToPublic = false) { - return RSA_PEM.ToPEM(rsa, convertToPublic, true); + return new RSA_PEM(rsa).ToPEM(convertToPublic, true); } + /// + /// 将密钥对导出成PEM对象,如果convertToPublic含私钥的RSA将只返回公钥,仅含公钥的RSA不受影响 + /// + public RSA_PEM ToPEM(bool convertToPublic = false) { + return new RSA_PEM(rsa, convertToPublic); + } + - /// /// 加密字符串(utf-8),出错抛异常 /// public string Encode(string str) { - return RSA_Unit.Base64EncodeBytes(Encode(Encoding.UTF8.GetBytes(str))); + return Convert.ToBase64String(Encode(Encoding.UTF8.GetBytes(str))); } /// /// 加密数据,出错抛异常 @@ -73,7 +77,8 @@ public string DecodeOrNull(string str) { if (String.IsNullOrEmpty(str)) { return null; } - var byts = RSA_Unit.Base64DecodeBytes(str); + byte[] byts = null; + try { byts = Convert.FromBase64String(str); } catch { } if (byts == null) { return null; } @@ -118,7 +123,7 @@ public byte[] DecodeOrNull(byte[] data) { /// 对str进行签名,并指定hash算法(如:SHA256) /// public string Sign(string hash, string str) { - return RSA_Unit.Base64EncodeBytes(Sign(hash, Encoding.UTF8.GetBytes(str))); + return Convert.ToBase64String(Sign(hash, Encoding.UTF8.GetBytes(str))); } /// /// 对data进行签名,并指定hash算法(如:SHA256) @@ -130,7 +135,8 @@ public byte[] Sign(string hash, byte[] data) { /// 验证字符串str的签名是否是sgin,并指定hash算法(如:SHA256) /// public bool Verify(string hash, string sgin, string str) { - var byts = RSA_Unit.Base64DecodeBytes(sgin); + byte[] byts = null; + try { byts = Convert.FromBase64String(sgin); } catch { } if (byts == null) { return false; } @@ -151,18 +157,36 @@ public bool Verify(string hash, byte[] sgin, byte[] data) { private RSACryptoServiceProvider rsa; - private void _init() { - var rsaParams = new CspParameters(); - rsaParams.Flags = CspProviderFlags.UseMachineKeyStore; - rsa = new RSACryptoServiceProvider(rsaParams); + /// + /// 最底层的RSACryptoServiceProvider对象 + /// + public RSACryptoServiceProvider RSAObject { + get { + return rsa; + } + } + + /// + /// 密钥位数 + /// + public int KeySize { + get { + return rsa.KeySize; + } + } + /// + /// 是否包含私钥 + /// + public bool HasPrivate { + get { + return !rsa.PublicOnly; + } } /// /// 用指定密钥大小创建一个新的RSA,出错抛异常 /// public RSA(int keySize) { - _init(); - var rsaParams = new CspParameters(); rsaParams.Flags = CspProviderFlags.UseMachineKeyStore; rsa = new RSACryptoServiceProvider(keySize, rsaParams); @@ -171,7 +195,9 @@ public RSA(int keySize) { /// 通过指定的密钥,创建一个RSA,xml内可以只包含一个公钥或私钥,或都包含,出错抛异常 /// public RSA(string xml) { - _init(); + var rsaParams = new CspParameters(); + rsaParams.Flags = CspProviderFlags.UseMachineKeyStore; + rsa = new RSACryptoServiceProvider(rsaParams); rsa.FromXmlString(xml); } @@ -179,9 +205,13 @@ public RSA(string xml) { /// 通过一个pem文件创建RSA,pem为公钥或私钥,出错抛异常 /// public RSA(string pem, bool noop) { - _init(); - - rsa = RSA_PEM.FromPEM(pem); + rsa = RSA_PEM.FromPEM(pem).GetRSA(); + } + /// + /// 通过一个pem对象创建RSA,pem为公钥或私钥,出错抛异常 + /// + public RSA(RSA_PEM pem) { + rsa = pem.GetRSA(); } } } diff --git a/RSA_PEM.cs b/RSA_PEM.cs index 701d88c..a633765 100644 --- a/RSA_PEM.cs +++ b/RSA_PEM.cs @@ -1,29 +1,244 @@ using System; -using System.Collections.Generic; using System.IO; +using System.Numerics; using System.Linq; using System.Security.Cryptography; using System.Text; using System.Text.RegularExpressions; -using System.Threading.Tasks; -namespace RSA { +namespace com.github.xiangyuecn.rsacsharp { /// - /// RSA PEM格式秘钥对的解析和导出 + /// RSA PEM格式密钥对的解析和导出 + /// GitHub: https://github.com/xiangyuecn/RSA-csharp /// public class RSA_PEM { /// - /// 用PEM格式密钥对创建RSA,支持PKCS#1、PKCS#8格式的PEM + /// modulus 模数n,公钥、私钥都有 + /// + public byte[] Key_Modulus; + /// + /// publicExponent 公钥指数e,公钥、私钥都有 + /// + public byte[] Key_Exponent; + /// + /// privateExponent 私钥指数d,只有私钥的时候才有 + /// + public byte[] Key_D; + + //以下参数只有私钥才有 https://docs.microsoft.com/zh-cn/dotnet/api/system.security.cryptography.rsaparameters?redirectedfrom=MSDN&view=netframework-4.8 + /// + /// prime1 + /// + public byte[] Val_P; + /// + /// prime2 + /// + public byte[] Val_Q; + /// + /// exponent1 + /// + public byte[] Val_DP; + /// + /// exponent2 + /// + public byte[] Val_DQ; + /// + /// coefficient + /// + public byte[] Val_InverseQ; + + private RSA_PEM() { } + + /// + /// 通过RSA中的公钥和私钥构造一个PEM,如果convertToPublic含私钥的RSA将只读取公钥,仅含公钥的RSA不受影响 + /// + public RSA_PEM(RSACryptoServiceProvider rsa, bool convertToPublic = false) { + var isPublic = convertToPublic || rsa.PublicOnly; + var param = rsa.ExportParameters(!isPublic); + + Key_Modulus = param.Modulus; + Key_Exponent = param.Exponent; + + if (!isPublic) { + Key_D = param.D; + + Val_P = param.P; + Val_Q = param.Q; + Val_DP = param.DP; + Val_DQ = param.DQ; + Val_InverseQ = param.InverseQ; + } + } + /// + /// 通过全量的PEM字段数据构造一个PEM,除了模数modulus和公钥指数exponent必须提供外,其他私钥指数信息要么全部提供,要么全部不提供(导出的PEM就只包含公钥) + /// 注意:所有参数首字节如果是0,必须先去掉 + /// + public RSA_PEM(byte[] modulus, byte[] exponent, byte[] d, byte[] p, byte[] q, byte[] dp, byte[] dq, byte[] inverseQ) { + Key_Modulus = modulus; + Key_Exponent = exponent; + Key_D = d; + + Val_P = p; + Val_Q = q; + Val_DP = dp; + Val_DQ = dq; + Val_InverseQ = inverseQ; + } + /// + /// 通过公钥指数和私钥指数构造一个PEM,会反推计算出P、Q但和原始生成密钥的P、Q极小可能相同 + /// 注意:所有参数首字节如果是0,必须先去掉 + /// 出错将会抛出异常 + /// + /// 必须提供模数 + /// 必须提供公钥指数 + /// 私钥指数可以不提供,导出的PEM就只包含公钥 + public RSA_PEM(byte[] modulus, byte[] exponent, byte[] dOrNull) { + Key_Modulus = modulus;//modulus + Key_Exponent = exponent;//publicExponent + + if (dOrNull != null) { + Key_D = dOrNull;//privateExponent + + //反推P、Q + BigInteger n = BigX(modulus); + BigInteger e = BigX(exponent); + BigInteger d = BigX(dOrNull); + BigInteger p = FindFactor(e, d, n); + BigInteger q = n / p; + if (p.CompareTo(q) > 0) { + BigInteger t = p; + p = q; + q = t; + } + BigInteger exp1 = d % (p - BigInteger.One); + BigInteger exp2 = d % (q - BigInteger.One); + BigInteger coeff = BigInteger.ModPow(q, p - 2, p); + + Val_P = BigB(p);//prime1 + Val_Q = BigB(q);//prime2 + Val_DP = BigB(exp1);//exponent1 + Val_DQ = BigB(exp2);//exponent2 + Val_InverseQ = BigB(coeff);//coefficient + } + } + + /// + /// 密钥位数 /// - public static RSACryptoServiceProvider FromPEM(string pem) { + public int KeySize { + get { + return Key_Modulus.Length * 8; + } + } + /// + /// 是否包含私钥 + /// + public bool HasPrivate { + get { + return Key_D != null; + } + } + /// + /// 将PEM中的公钥私钥转成RSA对象,如果未提供私钥,RSA中就只包含公钥 + /// + public RSACryptoServiceProvider GetRSA() { var rsaParams = new CspParameters(); rsaParams.Flags = CspProviderFlags.UseMachineKeyStore; var rsa = new RSACryptoServiceProvider(rsaParams); var param = new RSAParameters(); + param.Modulus = Key_Modulus; + param.Exponent = Key_Exponent; + if (Key_D != null) { + param.D = Key_D; + param.P = Val_P; + param.Q = Val_Q; + param.DP = Val_DP; + param.DQ = Val_DQ; + param.InverseQ = Val_InverseQ; + } + rsa.ImportParameters(param); + return rsa; + } + /// + /// 转成正整数,如果是负数,需要加前导0转成正整数 + /// + static public BigInteger BigX(byte[] bigb) { + if (bigb[0] > 0x7F) { + byte[] c = new byte[bigb.Length + 1]; + Array.Copy(bigb, 0, c, 1, bigb.Length); + bigb = c; + } + return new BigInteger(bigb.Reverse().ToArray());//C#的二进制是反的 + } + /// + /// BigInt导出byte整数首字节>0x7F的会加0前导,保证正整数,因此需要去掉0 + /// + static public byte[] BigB(BigInteger bigx) { + byte[] val = bigx.ToByteArray().Reverse().ToArray();//C#的二进制是反的 + if (val[0] == 0) { + byte[] c = new byte[val.Length - 1]; + Array.Copy(val, 1, c, 0, c.Length); + val = c; + } + return val; + } + /// + /// 由n e d 反推 P Q + /// 资料: https://stackoverflow.com/questions/43136036/how-to-get-a-rsaprivatecrtkey-from-a-rsaprivatekey + /// https://v2ex.com/t/661736 + /// + static private BigInteger FindFactor(BigInteger e, BigInteger d, BigInteger n) { + BigInteger edMinus1 = e * d - BigInteger.One; + int s = -1; + if (edMinus1 != BigInteger.Zero) { + s = (int)(BigInteger.Log(edMinus1 & -edMinus1) / BigInteger.Log(2)); + } + BigInteger t = edMinus1 >> s; + + long now = DateTime.Now.Ticks; + for (int aInt = 2; true; aInt++) { + if (aInt % 10 == 0 && DateTime.Now.Ticks - now > 3000 * 10000) { + throw new Exception("推算RSA.P超时");//测试最多循环2次,1024位的速度很快 8ms + } + + BigInteger aPow = BigInteger.ModPow(new BigInteger(aInt), t, n); + for (int i = 1; i <= s; i++) { + if (aPow == BigInteger.One) { + break; + } + if (aPow == n - BigInteger.One) { + break; + } + BigInteger aPowSquared = aPow * aPow % n; + if (aPowSquared == BigInteger.One) { + return BigInteger.GreatestCommonDivisor(aPow - BigInteger.One, n); + } + aPow = aPowSquared; + } + } + } + + + + + + + + + + + + /// + /// 用PEM格式密钥对创建RSA,支持PKCS#1、PKCS#8格式的PEM + /// 出错将会抛出异常 + /// + static public RSA_PEM FromPEM(string pem) { + RSA_PEM param = new RSA_PEM(); var base64 = _PEMCode.Replace(pem, ""); - var data = RSA_Unit.Base64DecodeBytes(base64); + byte[] data = null; + try { data = Convert.FromBase64String(base64); } catch { } if (data == null) { throw new Exception("PEM内容无效"); } @@ -52,7 +267,10 @@ public static RSACryptoServiceProvider FromPEM(string pem) { idx++; len--; } - var val = data.sub(idx, len); + var val = new byte[len]; + for (var i = 0; i < len; i++) { + val[i] = data[idx + i]; + } idx += len; return val; }; @@ -76,20 +294,24 @@ public static RSACryptoServiceProvider FromPEM(string pem) { /****使用公钥****/ //读取数据总长度 readLen(0x30); - if (!eq(_SeqOID)) { - throw new Exception("PEM未知格式"); + + //看看有没有oid + var idx2 = idx; + if (eq(_SeqOID)) { + //读取1长度 + readLen(0x03); + idx++;//跳过0x00 + //读取2长度 + readLen(0x30); + } else { + idx = idx2; } - //读取1长度 - readLen(0x03); - idx++;//跳过0x00 - //读取2长度 - readLen(0x30); //Modulus - param.Modulus = readBlock(); + param.Key_Modulus = readBlock(); //Exponent - param.Exponent = readBlock(); + param.Key_Exponent = readBlock(); } else if (pem.Contains("PRIVATE KEY")) { /****使用私钥****/ //读取数据总长度 @@ -117,24 +339,23 @@ public static RSACryptoServiceProvider FromPEM(string pem) { } //读取数据 - param.Modulus = readBlock(); - param.Exponent = readBlock(); - param.D = readBlock(); - param.P = readBlock(); - param.Q = readBlock(); - param.DP = readBlock(); - param.DQ = readBlock(); - param.InverseQ = readBlock(); + param.Key_Modulus = readBlock(); + param.Key_Exponent = readBlock(); + param.Key_D = readBlock(); + param.Val_P = readBlock(); + param.Val_Q = readBlock(); + param.Val_DP = readBlock(); + param.Val_DQ = readBlock(); + param.Val_InverseQ = readBlock(); } else { throw new Exception("pem需要BEGIN END标头"); } - rsa.ImportParameters(param); - return rsa; + return param; } - static private Regex _PEMCode = new Regex(@"--+.+?--+|\s+"); - static private byte[] _SeqOID = new byte[] { 0x30, 0x0D, 0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x01, 0x05, 0x00 }; - static private byte[] _Ver = new byte[] { 0x02, 0x01, 0x00 }; + static private readonly Regex _PEMCode = new Regex(@"--+.+?--+|\s+"); + static private readonly byte[] _SeqOID = new byte[] { 0x30, 0x0D, 0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x01, 0x05, 0x00 }; + static private readonly byte[] _Ver = new byte[] { 0x02, 0x01, 0x00 }; @@ -146,7 +367,7 @@ public static RSACryptoServiceProvider FromPEM(string pem) { /// /// 将RSA中的密钥对转换成PEM格式,usePKCS8=false时返回PKCS#1格式,否则返回PKCS#8格式,如果convertToPublic含私钥的RSA将只返回公钥,仅含公钥的RSA不受影响 /// - public static string ToPEM(RSACryptoServiceProvider rsa, bool convertToPublic, bool usePKCS8) { + public string ToPEM(bool convertToPublic, bool usePKCS8) { //https://www.jianshu.com/p/25803dd9527d //https://www.cnblogs.com/ylz8401/p/8443819.html //https://blog.csdn.net/jiayanhui2877/article/details/47187077 @@ -190,12 +411,30 @@ public static string ToPEM(RSACryptoServiceProvider rsa, bool convertToPublic, b return ms.ToArray(); }; + Action writeAll = (stream, byts) => { + stream.Write(byts, 0, byts.Length); + }; + Func TextBreak = (text, line) => { + var idx = 0; + var len = text.Length; + var str = new StringBuilder(); + while (idx < len) { + if (idx > 0) { + str.Append('\n'); + } + if (idx + line >= len) { + str.Append(text.Substring(idx)); + } else { + str.Append(text.Substring(idx, line)); + } + idx += line; + } + return str.ToString(); + }; - if (rsa.PublicOnly || convertToPublic) { + if (Key_D == null || convertToPublic) { /****生成公钥****/ - var param = rsa.ExportParameters(false); - //写入总字节数,不含本段长度,额外需要24字节的头,后续计算好填入 ms.WriteByte(0x30); @@ -203,7 +442,7 @@ public static string ToPEM(RSACryptoServiceProvider rsa, bool convertToPublic, b //固定内容 // encoded OID sequence for PKCS #1 rsaEncryption szOID_RSA_RSA = "1.2.840.113549.1.1.1" - ms.writeAll(_SeqOID); + writeAll(ms, _SeqOID); //从0x00开始的后续长度 ms.WriteByte(0x03); @@ -215,10 +454,10 @@ public static string ToPEM(RSACryptoServiceProvider rsa, bool convertToPublic, b var index3 = (int)ms.Length; //写入Modulus - writeBlock(param.Modulus); + writeBlock(Key_Modulus); //写入Exponent - writeBlock(param.Exponent); + writeBlock(Key_Exponent); //计算空缺的长度 @@ -229,23 +468,22 @@ public static string ToPEM(RSACryptoServiceProvider rsa, bool convertToPublic, b byts = writeLen(index1, byts); - return "-----BEGIN PUBLIC KEY-----\n" + RSA_Unit.TextBreak(RSA_Unit.Base64EncodeBytes(byts), 64) + "\n-----END PUBLIC KEY-----"; + return "-----BEGIN PUBLIC KEY-----\n" + TextBreak(Convert.ToBase64String(byts), 64) + "\n-----END PUBLIC KEY-----"; } else { /****生成私钥****/ - var param = rsa.ExportParameters(true); //写入总字节数,后续写入 ms.WriteByte(0x30); int index1 = (int)ms.Length; //写入版本号 - ms.writeAll(_Ver); + writeAll(ms, _Ver); //PKCS8 多一段数据 int index2 = -1, index3 = -1; if (usePKCS8) { //固定内容 - ms.writeAll(_SeqOID); + writeAll(ms, _SeqOID); //后续内容长度 ms.WriteByte(0x04); @@ -256,18 +494,18 @@ public static string ToPEM(RSACryptoServiceProvider rsa, bool convertToPublic, b index3 = (int)ms.Length; //写入版本号 - ms.writeAll(_Ver); + writeAll(ms, _Ver); } //写入数据 - writeBlock(param.Modulus); - writeBlock(param.Exponent); - writeBlock(param.D); - writeBlock(param.P); - writeBlock(param.Q); - writeBlock(param.DP); - writeBlock(param.DQ); - writeBlock(param.InverseQ); + writeBlock(Key_Modulus); + writeBlock(Key_Exponent); + writeBlock(Key_D); + writeBlock(Val_P); + writeBlock(Val_Q); + writeBlock(Val_DP); + writeBlock(Val_DQ); + writeBlock(Val_InverseQ); //计算空缺的长度 @@ -284,8 +522,99 @@ public static string ToPEM(RSACryptoServiceProvider rsa, bool convertToPublic, b if (!usePKCS8) { flag = " RSA" + flag; } - return "-----BEGIN" + flag + "-----\n" + RSA_Unit.TextBreak(RSA_Unit.Base64EncodeBytes(byts), 64) + "\n-----END" + flag + "-----"; + return "-----BEGIN" + flag + "-----\n" + TextBreak(Convert.ToBase64String(byts), 64) + "\n-----END" + flag + "-----"; } } + + + + + + + + + + + + + + + + /// + /// 将XML格式密钥转成PEM,支持公钥xml、私钥xml + /// 出错将会抛出异常 + /// + static public RSA_PEM FromXML(string xml) { + RSA_PEM rtv = new RSA_PEM(); + + Match xmlM = xmlExp.Match(xml); + if (!xmlM.Success) { + throw new Exception("XML内容不符合要求"); + } + + Match tagM = xmlTagExp.Match(xmlM.Groups[1].Value); + while (tagM.Success) { + string tag = tagM.Groups[1].Value; + string b64 = tagM.Groups[2].Value; + byte[] val = Convert.FromBase64String(b64); + switch (tag) { + case "Modulus": rtv.Key_Modulus = val; break; + case "Exponent": rtv.Key_Exponent = val; break; + case "D": rtv.Key_D = val; break; + + case "P": rtv.Val_P = val; break; + case "Q": rtv.Val_Q = val; break; + case "DP": rtv.Val_DP = val; break; + case "DQ": rtv.Val_DQ = val; break; + case "InverseQ": rtv.Val_InverseQ = val; break; + } + tagM = tagM.NextMatch(); + } + + if (rtv.Key_Modulus == null || rtv.Key_Exponent == null) { + throw new Exception("XML公钥丢失"); + } + if (rtv.Key_D != null) { + if (rtv.Val_P == null || rtv.Val_Q == null || rtv.Val_DP == null || rtv.Val_DQ == null || rtv.Val_InverseQ == null) { + return new RSA_PEM(rtv.Key_Modulus, rtv.Key_Exponent, rtv.Key_D); + } + } + + return rtv; + } + static private readonly Regex xmlExp = new Regex("\\s*([<>\\/\\+=\\w\\s]+)\\s*"); + static private readonly Regex xmlTagExp = new Regex("<(.+?)>\\s*([^<]+?)\\s* + /// 将RSA中的密钥对转换成XML格式 + /// ,如果convertToPublic含私钥的RSA将只返回公钥,仅含公钥的RSA不受影响 + /// + public string ToXML(bool convertToPublic) { + StringBuilder str = new StringBuilder(); + str.Append(""); + str.Append("" + Convert.ToBase64String(Key_Modulus) + ""); + str.Append("" + Convert.ToBase64String(Key_Exponent) + ""); + if (Key_D == null || convertToPublic) { + /****生成公钥****/ + //NOOP + } else { + /****生成私钥****/ + str.Append("

" + Convert.ToBase64String(Val_P) + "

"); + str.Append("" + Convert.ToBase64String(Val_Q) + ""); + str.Append("" + Convert.ToBase64String(Val_DP) + ""); + str.Append("" + Convert.ToBase64String(Val_DQ) + ""); + str.Append("" + Convert.ToBase64String(Val_InverseQ) + ""); + str.Append("" + Convert.ToBase64String(Key_D) + ""); + } + str.Append("
"); + return str.ToString(); + } + + + } } diff --git a/RSA_Unit.cs b/RSA_Unit.cs deleted file mode 100644 index 1373389..0000000 --- a/RSA_Unit.cs +++ /dev/null @@ -1,60 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace RSA { - /// - /// 封装的一些通用方法 - /// - public class RSA_Unit { - static public string Base64EncodeBytes(byte[] byts) { - return Convert.ToBase64String(byts); - } - static public byte[] Base64DecodeBytes(string str) { - try { - return Convert.FromBase64String(str); - } catch { - return null; - } - } - /// - /// 把字符串按每行多少个字断行 - /// - static public string TextBreak(string text, int line) { - var idx = 0; - var len = text.Length; - var str = new StringBuilder(); - while (idx < len) { - if (idx > 0) { - str.Append('\n'); - } - if (idx + line >= len) { - str.Append(text.Substring(idx)); - } else { - str.Append(text.Substring(idx, line)); - } - idx += line; - } - return str.ToString(); - } - } - - static public class Extensions { - /// - /// 从数组start开始到指定长度复制一份 - /// - static public T[] sub(this T[] arr, int start, int count) { - T[] val = new T[count]; - for (var i = 0; i < count; i++) { - val[i] = arr[start + i]; - } - return val; - } - static public void writeAll(this Stream stream, byte[] byts) { - stream.Write(byts, 0, byts.Length); - } - } -} diff --git a/vs.csproj b/vs.csproj index bed5d19..8fff7d8 100644 --- a/vs.csproj +++ b/vs.csproj @@ -37,6 +37,7 @@ + @@ -48,7 +49,6 @@ - From da96ac0bcf07bd6de57ff7458723628a5a64c570 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=AB=98=E5=9D=9A=E6=9E=9C?= <753610399@qq.com> Date: Wed, 29 Apr 2020 07:37:39 +0800 Subject: [PATCH 02/15] =?UTF-8?q?=E8=B0=83=E6=95=B4=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index b4a4d84..7c33498 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@ # :open_book:RSA-csharp的帮助文档 -本项目核心功能为:支持`.Net`环境下`PEM`(`PKCS#1`、`PKCS#8`)格式RSA密钥对导入、导出。 +本项目核心功能为:支持`.NET Core`、`.NET Framework`环境下`PEM`(`PKCS#1`、`PKCS#8`)格式RSA密钥对导入、导出。 附带实现了一个RSA封装操作类,和一个测试控制台程序。 -支持`.NET Core`、`.NET Framework`;已移植到Java版:[RSA-java](https://github.com/xiangyuecn/RSA-java/blob/master/RSA_PEM.java);你可以只copy `RSA_PEM.cs` 文件到你的项目中使用,只需这一个文件你就拥有了通过PEM格式密钥创建`RSACryptoServiceProvider`的能力。 +你可以只copy `RSA_PEM.cs` 文件到你的项目中使用,只需这一个文件你就拥有了通过PEM格式密钥创建`RSACryptoServiceProvider`的能力。clone整个项目代码用vs应该能够直接打开,经目测看起来没什么卵用的文件都svn:ignore掉了(svn滑稽。 -clone下来用vs应该能够直接打开,经目测看起来没什么卵用的文件都svn:ignore掉了(svn滑稽。 +【Java版】:[RSA-java](https://github.com/xiangyuecn/RSA-java) ## 提供支持 @@ -33,13 +33,13 @@ clone下来用vs应该能够直接打开,经目测看起来没什么卵用的 ### 实例属性 -`byte[]`:`Key_Modulus`(模数n,公钥、私钥都有)、`Key_Exponent`(公钥指数e,公钥、私钥都有)、`Key_D`(私钥指数d,只有私钥的时候才有);有这3个足够用来加密解密。 +`byte[]`:**Key_Modulus**(模数n,公钥、私钥都有)、**Key_Exponent**(公钥指数e,公钥、私钥都有)、**Key_D**(私钥指数d,只有私钥的时候才有);有这3个足够用来加密解密。 -`byte[]`:`Val_P`(prime1)、`Val_Q`(prime2)、`Val_DP`(exponent1)、`Val_DQ`(exponent2)、`Val_InverseQ`(coefficient); (PEM中的私钥才有的更多的数值;可通过n、e、d反推出这些值(只是反推出有效值,和原始的值大概率不同))。 +`byte[]`:**Val_P**(prime1)、**Val_Q**(prime2)、**Val_DP**(exponent1)、**Val_DQ**(exponent2)、**Val_InverseQ**(coefficient); (PEM中的私钥才有的更多的数值;可通过n、e、d反推出这些值(只是反推出有效值,和原始的值大概率不同))。 -`int`:`KeySize`(密钥位数) +`int`:**KeySize**(密钥位数) -`bool`:`HasPrivate`(是否包含私钥) +`bool`:**HasPrivate**(是否包含私钥) ### 构造方法 @@ -84,11 +84,11 @@ clone下来用vs应该能够直接打开,经目测看起来没什么卵用的 ### 实例属性 -`RSACryptoServiceProvider`:`RSAObject`(最底层的RSACryptoServiceProvider对象) +`RSACryptoServiceProvider`:**RSAObject**(最底层的RSACryptoServiceProvider对象) -`int`:`KeySize`(密钥位数) +`int`:**KeySize**(密钥位数) -`bool`:`HasPrivate`(是否包含私钥) +`bool`:**HasPrivate**(是否包含私钥) ### 实例方法 From 396f2f7e1cd97b164590f41bf27253f5568af8b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=AB=98=E5=9D=9A=E6=9E=9C?= <753610399@qq.com> Date: Wed, 29 Apr 2020 11:05:45 +0800 Subject: [PATCH 03/15] Create LICENSE --- LICENSE | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..457bc98 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 xiangyuecn + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. From 2b11403498db0ab4e369da27fc91a2bc11b06e2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=AB=98=E5=9D=9A=E6=9E=9C?= <753610399@qq.com> Date: Wed, 29 Apr 2020 11:16:08 +0800 Subject: [PATCH 04/15] =?UTF-8?q?=E8=B0=83=E6=95=B4=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 62 +++++++++++++++++++++++++++---------------------------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index 7c33498..1a2ed97 100644 --- a/README.md +++ b/README.md @@ -33,37 +33,37 @@ ### 实例属性 -`byte[]`:**Key_Modulus**(模数n,公钥、私钥都有)、**Key_Exponent**(公钥指数e,公钥、私钥都有)、**Key_D**(私钥指数d,只有私钥的时候才有);有这3个足够用来加密解密。 +byte[]:**Key_Modulus**(模数n,公钥、私钥都有)、**Key_Exponent**(公钥指数e,公钥、私钥都有)、**Key_D**(私钥指数d,只有私钥的时候才有);有这3个足够用来加密解密。 -`byte[]`:**Val_P**(prime1)、**Val_Q**(prime2)、**Val_DP**(exponent1)、**Val_DQ**(exponent2)、**Val_InverseQ**(coefficient); (PEM中的私钥才有的更多的数值;可通过n、e、d反推出这些值(只是反推出有效值,和原始的值大概率不同))。 +byte[]:**Val_P**(prime1)、**Val_Q**(prime2)、**Val_DP**(exponent1)、**Val_DQ**(exponent2)、**Val_InverseQ**(coefficient); (PEM中的私钥才有的更多的数值;可通过n、e、d反推出这些值(只是反推出有效值,和原始的值大概率不同))。 -`int`:**KeySize**(密钥位数) +int:**KeySize**(密钥位数) -`bool`:**HasPrivate**(是否包含私钥) +bool:**HasPrivate**(是否包含私钥) ### 构造方法 -`RSA_PEM(RSACryptoServiceProvider rsa, bool convertToPublic = false)`:通过RSA中的公钥和私钥构造一个PEM,如果convertToPublic含私钥的RSA将只读取公钥,仅含公钥的RSA不受影响。 +**RSA_PEM(RSACryptoServiceProvider rsa, bool convertToPublic = false)**:通过RSA中的公钥和私钥构造一个PEM,如果convertToPublic含私钥的RSA将只读取公钥,仅含公钥的RSA不受影响。 -`RSA_PEM(byte[] modulus, byte[] exponent, byte[] d, byte[] p, byte[] q, byte[] dp, byte[] dq, byte[] inverseQ)`:通过全量的PEM字段数据构造一个PEM,除了模数modulus和公钥指数exponent必须提供外,其他私钥指数信息要么全部提供,要么全部不提供(导出的PEM就只包含公钥)注意:所有参数首字节如果是0,必须先去掉。 +**RSA_PEM(byte[] modulus, byte[] exponent, byte[] d, byte[] p, byte[] q, byte[] dp, byte[] dq, byte[] inverseQ)**:通过全量的PEM字段数据构造一个PEM,除了模数modulus和公钥指数exponent必须提供外,其他私钥指数信息要么全部提供,要么全部不提供(导出的PEM就只包含公钥)注意:所有参数首字节如果是0,必须先去掉。 -`RSA_PEM(byte[] modulus, byte[] exponent, byte[] dOrNull)`:通过公钥指数和私钥指数构造一个PEM,会反推计算出P、Q但和原始生成密钥的P、Q极小可能相同。注意:所有参数首字节如果是0,必须先去掉。出错将会抛出异常。私钥指数可以不提供,导出的PEM就只包含公钥。 +**RSA_PEM(byte[] modulus, byte[] exponent, byte[] dOrNull)**:通过公钥指数和私钥指数构造一个PEM,会反推计算出P、Q但和原始生成密钥的P、Q极小可能相同。注意:所有参数首字节如果是0,必须先去掉。出错将会抛出异常。私钥指数可以不提供,导出的PEM就只包含公钥。 ### 实例方法 -`RSACryptoServiceProvider GetRSA()`:将PEM中的公钥私钥转成RSA对象,如果未提供私钥,RSA中就只包含公钥。 +**RSACryptoServiceProvider GetRSA()**:将PEM中的公钥私钥转成RSA对象,如果未提供私钥,RSA中就只包含公钥。 -`string ToPEM(bool convertToPublic, bool usePKCS8)`:将RSA中的密钥对转换成PEM格式,usePKCS8=false时返回PKCS#1格式,否则返回PKCS#8格式,如果convertToPublic含私钥的RSA将只返回公钥,仅含公钥的RSA不受影响。 +**string ToPEM(bool convertToPublic, bool usePKCS8)**:将RSA中的密钥对转换成PEM格式,usePKCS8=false时返回PKCS#1格式,否则返回PKCS#8格式,如果convertToPublic含私钥的RSA将只返回公钥,仅含公钥的RSA不受影响。 -`string ToXML(bool convertToPublic)`:将RSA中的密钥对转换成XML格式,如果convertToPublic含私钥的RSA将只返回公钥,仅含公钥的RSA不受影响。 +**string ToXML(bool convertToPublic)**:将RSA中的密钥对转换成XML格式,如果convertToPublic含私钥的RSA将只返回公钥,仅含公钥的RSA不受影响。 ### 静态方法 -`static RSA_PEM FromPEM(string pem)`:用PEM格式密钥对创建RSA,支持PKCS#1、PKCS#8格式的PEM,出错将会抛出异常。pem格式如:`-----BEGIN XXX KEY-----....-----END XXX KEY-----`。 +**static RSA_PEM FromPEM(string pem)**:用PEM格式密钥对创建RSA,支持PKCS#1、PKCS#8格式的PEM,出错将会抛出异常。pem格式如:`-----BEGIN XXX KEY-----....-----END XXX KEY-----`。 -`static RSA_PEM FromXML(string xml)`:将XML格式密钥转成PEM,支持公钥xml、私钥xml,出错将会抛出异常。 +**static RSA_PEM FromXML(string xml)**:将XML格式密钥转成PEM,支持公钥xml、私钥xml,出错将会抛出异常。 @@ -73,49 +73,49 @@ ### 构造方法 -`RSA(int keySize)`:用指定密钥大小创建一个新的RSA,会生成新密钥,出错抛异常。 +**RSA(int keySize)**:用指定密钥大小创建一个新的RSA,会生成新密钥,出错抛异常。 -`RSA(string xml)`:通过XML格式密钥,创建一个RSA,xml内可以只包含一个公钥或私钥,或都包含,出错抛异常。,`XML格式`如:`...` +**RSA(string xml)**:通过XML格式密钥,创建一个RSA,xml内可以只包含一个公钥或私钥,或都包含,出错抛异常。`XML格式`如:`...` -`RSA(string pem, bool noop)`:通过`PEM格式`密钥对创建RSA(noop参数随意填),PEM可以是公钥或私钥,支持`PKCS#1`、`PKCS#8`格式,pem格式如:`-----BEGIN XXX KEY-----....-----END XXX KEY-----`。 +**RSA(string pem, bool noop)**:通过`PEM格式`密钥对创建RSA(noop参数随意填),PEM可以是公钥或私钥,支持`PKCS#1`、`PKCS#8`格式,pem格式如:`-----BEGIN XXX KEY-----....-----END XXX KEY-----`。 -`RSA(RSA_PEM pem)`:通过一个pem对象创建RSA,pem为公钥或私钥,出错抛异常。 +**RSA(RSA_PEM pem)**:通过一个pem对象创建RSA,pem为公钥或私钥,出错抛异常。 ### 实例属性 -`RSACryptoServiceProvider`:**RSAObject**(最底层的RSACryptoServiceProvider对象) +RSACryptoServiceProvider:**RSAObject**(最底层的RSACryptoServiceProvider对象) -`int`:**KeySize**(密钥位数) +int:**KeySize**(密钥位数) -`bool`:**HasPrivate**(是否包含私钥) +bool:**HasPrivate**(是否包含私钥) ### 实例方法 -`string ToXML(bool convertToPublic = false)`:导出`XML格式`秘钥对。如果RSA包含私钥,默认会导出私钥,设置仅仅导出公钥时只会导出公钥;不包含私钥只会导出公钥。 +**string ToXML(bool convertToPublic = false)**:导出`XML格式`秘钥对。如果RSA包含私钥,默认会导出私钥,设置仅仅导出公钥时只会导出公钥;不包含私钥只会导出公钥。 -`string ToPEM_PKCS1(bool convertToPublic = false)`:导出`PEM PKCS#1格式`秘钥对。如果RSA包含私钥,默认会导出私钥,设置仅仅导出公钥时只会导出公钥;不包含私钥只会导出公钥。 +**string ToPEM_PKCS1(bool convertToPublic = false)**:导出`PEM PKCS#1格式`秘钥对。如果RSA包含私钥,默认会导出私钥,设置仅仅导出公钥时只会导出公钥;不包含私钥只会导出公钥。 -`string ToPEM_PKCS8(bool convertToPublic = false)`:导出`PEM PKCS#8格式`秘钥对。如果RSA包含私钥,默认会导出私钥,设置仅仅导出公钥时只会导出公钥;不包含私钥只会导出公钥。 +**string ToPEM_PKCS8(bool convertToPublic = false)**:导出`PEM PKCS#8格式`秘钥对。如果RSA包含私钥,默认会导出私钥,设置仅仅导出公钥时只会导出公钥;不包含私钥只会导出公钥。 -`RSA_PEM ToPEM(bool convertToPublic = false)`:导出RSA_PEM对象,如果convertToPublic含私钥的RSA将只返回公钥,仅含公钥的RSA不受影响。 +**RSA_PEM ToPEM(bool convertToPublic = false)**:导出RSA_PEM对象,如果convertToPublic含私钥的RSA将只返回公钥,仅含公钥的RSA不受影响。 -`string Encode(string str)`:加密操作,支持任意长度数据。 +**string Encode(string str)**:加密操作,支持任意长度数据。 -`byte[] Encode(byte[] data)`:加密数据,支持任意长度数据,出错抛异常。 +**byte[] Encode(byte[] data)**:加密数据,支持任意长度数据,出错抛异常。 -`string DecodeOrNull(string str)`:解密字符串(utf-8),解密异常返回null。 +**string DecodeOrNull(string str)**:解密字符串(utf-8),解密异常返回null。 -`byte[] DecodeOrNull(byte[] data)`:解密数据,解密异常返回null。 +**byte[] DecodeOrNull(byte[] data)**:解密数据,解密异常返回null。 -`string Sign(string hash, string str)`:对str进行签名,并指定hash算法(如:SHA256)。 +**string Sign(string hash, string str)**:对str进行签名,并指定hash算法(如:SHA256)。 -`byte[] Sign(string hash, byte[] data)`:对data进行签名,并指定hash算法(如:SHA256)。 +**byte[] Sign(string hash, byte[] data)**:对data进行签名,并指定hash算法(如:SHA256)。 -`bool Verify(string hash, string sgin, string str)`:验证字符串str的签名是否是sgin,并指定hash算法(如:SHA256)。 +**bool Verify(string hash, string sgin, string str)**:验证字符串str的签名是否是sgin,并指定hash算法(如:SHA256)。 -`bool Verify(string hash, byte[] sgin, byte[] data)`:验证data的签名是否是sgin,并指定hash算法(如:SHA256)。 +**bool Verify(string hash, byte[] sgin, byte[] data)**:验证data的签名是否是sgin,并指定hash算法(如:SHA256)。 From 6f5633fdd11ac71f8864542db8a143e1d3f09c3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=AB=98=E5=9D=9A=E6=9E=9C?= <753610399@qq.com> Date: Wed, 29 Apr 2020 11:32:30 +0800 Subject: [PATCH 05/15] =?UTF-8?q?=E8=B0=83=E6=95=B4=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/README.md b/README.md index 1a2ed97..a65d7e0 100644 --- a/README.md +++ b/README.md @@ -379,3 +379,25 @@ yZKNX3VxmLEHXQ== ``` + +## openssl RSA常用命令行 +``` bat +::生成密钥对 +openssl genrsa -out private.pem 1024 + +::提取公钥PKCS#8 +openssl rsa -in private.pem -pubout -out public.pem + +::转换成RSAPublicKey PKCS#1? +openssl rsa -pubin -in public.pem -RSAPublicKey_out -out public.pem.rsakey + +::加密 +echo abcd123 | openssl rsautl -encrypt -inkey public.pem -pubin -out data.enc.bin + +::解密 +openssl rsautl -decrypt -in data.enc.bin -inkey private.pem -out data.dec.txt + +::测试RSAPublicKey PKCS#1?,不出意外会出错 +::因为这个公钥里面没有OID,通过RSA_PEM转换成PKCS#1自动带上OID就能正常加密 +echo abcd123 | openssl rsautl -encrypt -inkey public.pem.rsakey -pubin +``` From 7ca6faeaf139c98492ce9e12e5736cd4d274cb41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=AB=98=E5=9D=9A=E6=9E=9C?= <753610399@qq.com> Date: Wed, 29 Apr 2020 12:30:40 +0800 Subject: [PATCH 06/15] =?UTF-8?q?=E7=BC=96=E8=BE=91=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 32 +++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index a65d7e0..b9d7717 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,19 @@ # :open_book:RSA-csharp的帮助文档 -本项目核心功能为:支持`.NET Core`、`.NET Framework`环境下`PEM`(`PKCS#1`、`PKCS#8`)格式RSA密钥对导入、导出。 +本项目核心功能:支持`.NET Core`、`.NET Framework`环境下`PEM`(`PKCS#1`、`PKCS#8`)格式RSA密钥对导入、导出。 -附带实现了一个RSA封装操作类,和一个测试控制台程序。 +底层实现采用PEM文件二进制层面上进行字节码解析,简单轻巧0依赖;附带实现了一个RSA封装操作类,和一个测试控制台程序。 你可以只copy `RSA_PEM.cs` 文件到你的项目中使用,只需这一个文件你就拥有了通过PEM格式密钥创建`RSACryptoServiceProvider`的能力。clone整个项目代码用vs应该能够直接打开,经目测看起来没什么卵用的文件都svn:ignore掉了(svn滑稽。 【Java版】:[RSA-java](https://github.com/xiangyuecn/RSA-java) -## 提供支持 +## 特性 - 通过`XML格式`密钥对创建RSA - 通过`PEM格式`密钥对创建RSA +- 通过指定密钥位数创建RSA(生成公钥、私钥) - RSA加密、解密 - RSA签名、验证 - 导出`XML格式`公钥、私钥 @@ -21,6 +22,13 @@ +## 【QQ群】交流与支持 + +欢迎加QQ群:421882406,纯小写口令:`xiangyuecn` + + + + # :open_book:文档 @@ -31,6 +39,16 @@ 注:openssl `RSAPublicKey_out`导出的公钥,字节码内并不带[OID](http://www.oid-info.com/get/1.2.840.113549.1.1.1)(目测是因为不带OID所以openssl自己都不支持用这个公钥来加密数据),RSA_PEM支持此格式公钥的导入,但不提供此种格式公钥的导出。 + +### 构造方法 + +**RSA_PEM(RSACryptoServiceProvider rsa, bool convertToPublic = false)**:通过RSA中的公钥和私钥构造一个PEM,如果convertToPublic含私钥的RSA将只读取公钥,仅含公钥的RSA不受影响。 + +**RSA_PEM(byte[] modulus, byte[] exponent, byte[] d, byte[] p, byte[] q, byte[] dp, byte[] dq, byte[] inverseQ)**:通过全量的PEM字段数据构造一个PEM,除了模数modulus和公钥指数exponent必须提供外,其他私钥指数信息要么全部提供,要么全部不提供(导出的PEM就只包含公钥)注意:所有参数首字节如果是0,必须先去掉。 + +**RSA_PEM(byte[] modulus, byte[] exponent, byte[] dOrNull)**:通过公钥指数和私钥指数构造一个PEM,会反推计算出P、Q但和原始生成密钥的P、Q极小可能相同。注意:所有参数首字节如果是0,必须先去掉。出错将会抛出异常。私钥指数可以不提供,导出的PEM就只包含公钥。 + + ### 实例属性 byte[]:**Key_Modulus**(模数n,公钥、私钥都有)、**Key_Exponent**(公钥指数e,公钥、私钥都有)、**Key_D**(私钥指数d,只有私钥的时候才有);有这3个足够用来加密解密。 @@ -41,14 +59,6 @@ int:**KeySize**(密钥位数) bool:**HasPrivate**(是否包含私钥) -### 构造方法 - -**RSA_PEM(RSACryptoServiceProvider rsa, bool convertToPublic = false)**:通过RSA中的公钥和私钥构造一个PEM,如果convertToPublic含私钥的RSA将只读取公钥,仅含公钥的RSA不受影响。 - -**RSA_PEM(byte[] modulus, byte[] exponent, byte[] d, byte[] p, byte[] q, byte[] dp, byte[] dq, byte[] inverseQ)**:通过全量的PEM字段数据构造一个PEM,除了模数modulus和公钥指数exponent必须提供外,其他私钥指数信息要么全部提供,要么全部不提供(导出的PEM就只包含公钥)注意:所有参数首字节如果是0,必须先去掉。 - -**RSA_PEM(byte[] modulus, byte[] exponent, byte[] dOrNull)**:通过公钥指数和私钥指数构造一个PEM,会反推计算出P、Q但和原始生成密钥的P、Q极小可能相同。注意:所有参数首字节如果是0,必须先去掉。出错将会抛出异常。私钥指数可以不提供,导出的PEM就只包含公钥。 - ### 实例方法 From f0c8e019434e58a4492020e5c92ab84ca46c625c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=AB=98=E5=9D=9A=E6=9E=9C?= <753610399@qq.com> Date: Wed, 29 Apr 2020 16:11:50 +0800 Subject: [PATCH 07/15] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/README.md b/README.md index b9d7717..9a2bf91 100644 --- a/README.md +++ b/README.md @@ -411,3 +411,14 @@ openssl rsautl -decrypt -in data.enc.bin -inkey private.pem -out data.dec.txt ::因为这个公钥里面没有OID,通过RSA_PEM转换成PKCS#1自动带上OID就能正常加密 echo abcd123 | openssl rsautl -encrypt -inkey public.pem.rsakey -pubin ``` + + + + +# :star:捐赠 +如果这个库有帮助到您,请 Star 一下。 + +您也可以使用支付宝或微信打赏作者: + +![](https://gitee.com/xiangyuecn/Recorder/raw/master/assets/donate-alipay.png) ![](https://gitee.com/xiangyuecn/Recorder/raw/master/assets/donate-weixin.png) + From 68ef91d4559cca1161fb8877ecaa59de11849c1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=AB=98=E5=9D=9A=E6=9E=9C?= <753610399@qq.com> Date: Wed, 8 Jul 2020 22:34:21 +0800 Subject: [PATCH 08/15] =?UTF-8?q?=E5=8D=87=E7=BA=A71.2=E7=89=88=E6=9C=AC:?= =?UTF-8?q?=20=E5=85=AC=E9=92=A5=E4=B9=9F=E5=8C=BA=E5=88=86PKCS#1=E5=92=8C?= =?UTF-8?q?PKCS#8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Program.cs | 37 +++++++++++++-------- Properties/AssemblyInfo.cs | 4 +-- README.md | 59 ++++++++++++++++++++++++--------- RSA.cs | 30 ++++++++++------- RSA_PEM.cs | 67 +++++++++++++++++++++++++++----------- 5 files changed, 136 insertions(+), 61 deletions(-) diff --git a/Program.cs b/Program.cs index b189c18..f2bd67e 100644 --- a/Program.cs +++ b/Program.cs @@ -11,11 +11,11 @@ static void RSATest() { Console.WriteLine("【" + rsa.KeySize + "私钥(XML)】:"); Console.WriteLine(rsa.ToXML()); Console.WriteLine(); - Console.WriteLine("【" + rsa.KeySize + "私钥(PEM)】:"); - Console.WriteLine(rsa.ToPEM_PKCS1()); + Console.WriteLine("【" + rsa.KeySize + "私钥(PKCS#1)】:"); + Console.WriteLine(rsa.ToPEM().ToPEM_PKCS1()); Console.WriteLine(); - Console.WriteLine("【" + rsa.KeySize + "公钥(PEM)】:"); - Console.WriteLine(rsa.ToPEM_PKCS1(true)); + Console.WriteLine("【" + rsa.KeySize + "公钥(PKCS#8)】:"); + Console.WriteLine(rsa.ToPEM().ToPEM_PKCS8(true)); Console.WriteLine(); var str = "abc内容123"; @@ -30,33 +30,44 @@ static void RSATest() { Console.WriteLine(rsa.Sign("SHA1", str)); Console.WriteLine(); - var rsa2 = new RSA(rsa.ToPEM_PKCS8(), true); + var rsa2 = new RSA(rsa.ToPEM().ToPEM_PKCS8(), true); Console.WriteLine("【用PEM新创建的RSA是否和上面的一致】:"); Console.WriteLine("XML:" + (rsa2.ToXML() == rsa.ToXML())); - Console.WriteLine("PKCS1:" + (rsa2.ToPEM_PKCS1() == rsa.ToPEM_PKCS1())); - Console.WriteLine("PKCS8:" + (rsa2.ToPEM_PKCS8() == rsa.ToPEM_PKCS8())); + Console.WriteLine("PKCS1:" + (rsa2.ToPEM().ToPEM_PKCS1() == rsa.ToPEM().ToPEM_PKCS1())); + Console.WriteLine("PKCS8:" + (rsa2.ToPEM().ToPEM_PKCS8() == rsa.ToPEM().ToPEM_PKCS8())); var rsa3 = new RSA(rsa.ToXML()); Console.WriteLine("【用XML新创建的RSA是否和上面的一致】:"); Console.WriteLine("XML:" + (rsa3.ToXML() == rsa.ToXML())); - Console.WriteLine("PKCS1:" + (rsa3.ToPEM_PKCS1() == rsa.ToPEM_PKCS1())); - Console.WriteLine("PKCS8:" + (rsa3.ToPEM_PKCS8() == rsa.ToPEM_PKCS8())); + Console.WriteLine("PKCS1:" + (rsa3.ToPEM().ToPEM_PKCS1() == rsa.ToPEM().ToPEM_PKCS1())); + Console.WriteLine("PKCS8:" + (rsa3.ToPEM().ToPEM_PKCS8() == rsa.ToPEM().ToPEM_PKCS8())); //--------RSA_PEM验证--------- RSA_PEM pem = rsa.ToPEM(); Console.WriteLine("【RSA_PEM是否和原始RSA一致】:"); Console.WriteLine(pem.KeySize + "位"); Console.WriteLine("XML:" + (pem.ToXML(false) == rsa.ToXML())); - Console.WriteLine("PKCS1:" + (pem.ToPEM(false, false) == rsa.ToPEM_PKCS1())); - Console.WriteLine("PKCS8:" + (pem.ToPEM(false, true) == rsa.ToPEM_PKCS8())); + Console.WriteLine("PKCS1:" + (pem.ToPEM_PKCS1() == rsa.ToPEM().ToPEM_PKCS1())); + Console.WriteLine("PKCS8:" + (pem.ToPEM_PKCS8() == rsa.ToPEM().ToPEM_PKCS8())); Console.WriteLine("仅公钥:"); Console.WriteLine("XML:" + (pem.ToXML(true) == rsa.ToXML(true))); - Console.WriteLine("PKCS1:" + (pem.ToPEM(true, false) == rsa.ToPEM_PKCS1(true))); - Console.WriteLine("PKCS8:" + (pem.ToPEM(true, true) == rsa.ToPEM_PKCS8(true))); + Console.WriteLine("PKCS1:" + (pem.ToPEM_PKCS1(true) == rsa.ToPEM().ToPEM_PKCS1(true))); + Console.WriteLine("PKCS8:" + (pem.ToPEM_PKCS8(true) == rsa.ToPEM().ToPEM_PKCS8(true))); var rsa4 = new RSA(new RSA_PEM(pem.Key_Modulus, pem.Key_Exponent, pem.Key_D)); Console.WriteLine("【用n、e、d构造解密】"); Console.WriteLine(rsa4.DecodeOrNull(en)); + + + + + Console.WriteLine(); + Console.WriteLine(); + Console.WriteLine("【" + rsa.KeySize + "私钥(PKCS#8)】:"); + Console.WriteLine(rsa.ToPEM().ToPEM_PKCS8()); + Console.WriteLine(); + Console.WriteLine("【" + rsa.KeySize + "公钥(PKCS#1)】:不常见的公钥格式"); + Console.WriteLine(rsa.ToPEM().ToPEM_PKCS1(true)); } diff --git a/Properties/AssemblyInfo.cs b/Properties/AssemblyInfo.cs index 8d60d1b..2405ae3 100644 --- a/Properties/AssemblyInfo.cs +++ b/Properties/AssemblyInfo.cs @@ -29,5 +29,5 @@ // 生成号 // 修订号 // -[assembly: AssemblyVersion("1.1.0.0")] -[assembly: AssemblyFileVersion("1.1.0.0")] +[assembly: AssemblyVersion("1.2.0.0")] +[assembly: AssemblyFileVersion("1.2.0.0")] diff --git a/README.md b/README.md index 9a2bf91..3f615bf 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ 项目里面需要引入程序集`System.Numerics`用来支持`BigInteger`,vs默认创建的项目是不会自动引入此程序集的,要手动引入。 -注:openssl `RSAPublicKey_out`导出的公钥,字节码内并不带[OID](http://www.oid-info.com/get/1.2.840.113549.1.1.1)(目测是因为不带OID所以openssl自己都不支持用这个公钥来加密数据),RSA_PEM支持此格式公钥的导入,但不提供此种格式公钥的导出。 +注:`openssl rsa -in 私钥文件 -pubout`导出的是PKCS#8格式公钥(用的比较多),`openssl rsa -pubin -in PKCS#8公钥文件 -RSAPublicKey_out`导出的是PKCS#1格式公钥(用的比较少)。 ### 构造方法 @@ -64,7 +64,11 @@ bool:**HasPrivate**(是否包含私钥) **RSACryptoServiceProvider GetRSA()**:将PEM中的公钥私钥转成RSA对象,如果未提供私钥,RSA中就只包含公钥。 -**string ToPEM(bool convertToPublic, bool usePKCS8)**:将RSA中的密钥对转换成PEM格式,usePKCS8=false时返回PKCS#1格式,否则返回PKCS#8格式,如果convertToPublic含私钥的RSA将只返回公钥,仅含公钥的RSA不受影响。 +**string ToPEM(bool convertToPublic, bool privateUsePKCS8, bool publicUsePKCS8)**:将RSA中的密钥对转换成PEM格式。convertToPublic:等于true时含私钥的RSA将只返回公钥,仅含公钥的RSA不受影响 。**privateUsePKCS8**:私钥的返回格式,等于true时返回PKCS#8格式(`-----BEGIN PRIVATE KEY-----`),否则返回PKCS#1格式(`-----BEGIN RSA PRIVATE KEY-----`),返回公钥时此参数无效;两种格式使用都比较常见。**publicUsePKCS8**:公钥的返回格式,等于true时返回PKCS#8格式(`-----BEGIN PUBLIC KEY-----`),否则返回PKCS#1格式(`-----BEGIN RSA PUBLIC KEY-----`),返回私钥时此参数无效;一般用的多的是true PKCS#8格式公钥,PKCS#1格式公钥似乎比较少见。 + +**string ToPEM_PKCS1(bool convertToPublic=false)**:ToPEM方法的简化写法,不管公钥还是私钥都返回PKCS#1格式;似乎导出PKCS#1公钥用的比较少,PKCS#8的公钥用的多些,私钥#1#8都差不多。 + +**string ToPEM_PKCS8(bool convertToPublic=false)**:ToPEM方法的简化写法,不管公钥还是私钥都返回PKCS#8格式。 **string ToXML(bool convertToPublic)**:将RSA中的密钥对转换成XML格式,如果convertToPublic含私钥的RSA将只返回公钥,仅含公钥的RSA不受影响。 @@ -91,6 +95,10 @@ bool:**HasPrivate**(是否包含私钥) **RSA(RSA_PEM pem)**:通过一个pem对象创建RSA,pem为公钥或私钥,出错抛异常。 +**RSA(byte[] modulus, byte[] exponent, byte[] d, byte[] p, byte[] q, byte[] dp, byte[] dq, byte[] inverseQ)**:本方法会先生成RSA_PEM再创建RSA。通过全量的PEM字段数据构造一个PEM,除了模数modulus和公钥指数exponent必须提供外,其他私钥指数信息要么全部提供,要么全部不提供(导出的PEM就只包含公钥)注意:所有参数首字节如果是0,必须先去掉。 + +**RSA(byte[] modulus, byte[] exponent, byte[] dOrNull)**:本方法会先生成RSA_PEM再创建RSA。通过公钥指数和私钥指数构造一个PEM,会反推计算出P、Q但和原始生成密钥的P、Q极小可能相同。注意:所有参数首字节如果是0,必须先去掉。出错将会抛出异常。私钥指数可以不提供,导出的PEM就只包含公钥。 + ### 实例属性 @@ -105,11 +113,7 @@ bool:**HasPrivate**(是否包含私钥) **string ToXML(bool convertToPublic = false)**:导出`XML格式`秘钥对。如果RSA包含私钥,默认会导出私钥,设置仅仅导出公钥时只会导出公钥;不包含私钥只会导出公钥。 -**string ToPEM_PKCS1(bool convertToPublic = false)**:导出`PEM PKCS#1格式`秘钥对。如果RSA包含私钥,默认会导出私钥,设置仅仅导出公钥时只会导出公钥;不包含私钥只会导出公钥。 - -**string ToPEM_PKCS8(bool convertToPublic = false)**:导出`PEM PKCS#8格式`秘钥对。如果RSA包含私钥,默认会导出私钥,设置仅仅导出公钥时只会导出公钥;不包含私钥只会导出公钥。 - -**RSA_PEM ToPEM(bool convertToPublic = false)**:导出RSA_PEM对象,如果convertToPublic含私钥的RSA将只返回公钥,仅含公钥的RSA不受影响。 +**RSA_PEM ToPEM(bool convertToPublic = false)**:导出RSA_PEM对象,如果convertToPublic含私钥的RSA将只返回公钥,仅含公钥的RSA不受影响;通过RSA_PEM.ToPEM方法可以导出PEM文本。 **string Encode(string str)**:加密操作,支持任意长度数据。 @@ -189,11 +193,10 @@ PEM格式中,每段数据基本上都是`type+长度数据占用位数+长度 内容前面要加0(可能现在全部是加0吧,数据结尾这个字节不满8位?什么情况下会出现不够1字节?不够就用二进制0补齐,然后内容前面加补了几位)。 -### PEM公钥编码格式 -`PKCS#1`、`PKCS#8`公钥编码都是统一的格式。 +### PEM PKCS#8公钥编码格式 ``` -/*****1024位公钥*****/ +/*****1024位PKCS#8公钥*****/ -----BEGIN PUBLIC KEY----- MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCYw9+M3+REzDtYqwBrs/as/Oy8 GRE5OmnqOV0EfkEiCIjiczbVEFnZ3qRjLbDATfmBxNQ6c6Fga8nX28glEH/aL/RG @@ -201,7 +204,7 @@ GRE5OmnqOV0EfkEiCIjiczbVEFnZ3qRjLbDATfmBxNQ6c6Fga8nX28glEH/aL/RG RWdNQlpIaTDo5IhJJwIDAQAB -----END PUBLIC KEY----- -/*****二进制表述*****/ +/*****二进制表述(文本是16进制下同)*****/ 30819F300D06092A864886F70D010101050003818D003081890281810098C3DF8CDFE444CC3B58AB006BB3F6ACFCECBC1911393A69EA395D047E41220888E27336D51059D9DEA4632DB0C04DF981C4D43A73A1606BC9D7DBC825107FDA2FF446D8A76923C28C52A280B3337276C1D27A1F4C48A71DE73812E0DB84D2EF9E47B081F243968A988B0D0998DFC929337EA945674D425A486930E8E48849270203010001 @@ -221,7 +224,6 @@ RWdNQlpIaTDo5IhJJwIDAQAB , 30_SEQUENCE:{ 02_INTEGER: "整数" , 02_INTEGER: "整数" - , 02_INTEGER: "整数" } } */ @@ -260,6 +262,33 @@ RWdNQlpIaTDo5IhJJwIDAQAB ``` +### PEM PKCS#1公钥编码格式 + +``` +/*****1024位PKCS#1公钥*****/ +-----BEGIN RSA PUBLIC KEY----- +MIGJAoGBAJjD34zf5ETMO1irAGuz9qz87LwZETk6aeo5XQR+QSIIiOJzNtUQWdne +pGMtsMBN+YHE1DpzoWBrydfbyCUQf9ov9EbYp2kjwoxSooCzM3J2wdJ6H0xIpx3n +OBLg24TS755HsIHyQ5aKmIsNCZjfySkzfqlFZ01CWkhpMOjkiEknAgMBAAE= +-----END RSA PUBLIC KEY----- + +/*****二进制表述*****/ +3081890281810098C3DF8CDFE444CC3B58AB006BB3F6ACFCECBC1911393A69EA395D047E41220888E27336D51059D9DEA4632DB0C04DF981C4D43A73A1606BC9D7DBC825107FDA2FF446D8A76923C28C52A280B3337276C1D27A1F4C48A71DE73812E0DB84D2EF9E47B081F243968A988B0D0998DFC929337EA945674D425A486930E8E48849270203010001 + + +/*****二进制分解(和PKCS#8公钥格式就是只留了N、E两个数据,及其简单)*****/ +30 81 89 + + /*RSA Modulus*/ + 02 81 81 + 0098C3DF8CDFE444CC3B58AB006BB3F6ACFCECBC1911393A69EA395D047E41220888E27336D51059D9DEA4632DB0C04DF981C4D43A73A1606BC9D7DBC825107FDA2FF446D8A76923C28C52A280B3337276C1D27A1F4C48A71DE73812E0DB84D2EF9E47B081F243968A988B0D0998DFC929337EA945674D425A486930E8E4884927 + + /*RSA Exponent*/ + 02 03 + 010001 +``` + + ### PEM PKCS#1私钥编码格式 ``` @@ -398,7 +427,7 @@ openssl genrsa -out private.pem 1024 ::提取公钥PKCS#8 openssl rsa -in private.pem -pubout -out public.pem -::转换成RSAPublicKey PKCS#1? +::转换成RSAPublicKey PKCS#1 openssl rsa -pubin -in public.pem -RSAPublicKey_out -out public.pem.rsakey ::加密 @@ -407,8 +436,8 @@ echo abcd123 | openssl rsautl -encrypt -inkey public.pem -pubin -out data.enc.bi ::解密 openssl rsautl -decrypt -in data.enc.bin -inkey private.pem -out data.dec.txt -::测试RSAPublicKey PKCS#1?,不出意外会出错 -::因为这个公钥里面没有OID,通过RSA_PEM转换成PKCS#1自动带上OID就能正常加密 +::测试RSAPublicKey PKCS#1,不出意外会出错 +::因为这个公钥里面没有OID,通过RSA_PEM转换成PKCS#8自动带上OID就能正常加密 echo abcd123 | openssl rsautl -encrypt -inkey public.pem.rsakey -pubin ``` diff --git a/RSA.cs b/RSA.cs index 242938c..3d5185a 100644 --- a/RSA.cs +++ b/RSA.cs @@ -16,18 +16,6 @@ public string ToXML(bool convertToPublic = false) { return rsa.ToXmlString(!rsa.PublicOnly && !convertToPublic); } /// - /// 导出PEM PKCS#1格式密钥对,如果convertToPublic含私钥的RSA将只返回公钥,仅含公钥的RSA不受影响 - /// - public string ToPEM_PKCS1(bool convertToPublic = false) { - return new RSA_PEM(rsa).ToPEM(convertToPublic, false); - } - /// - /// 导出PEM PKCS#8格式密钥对,如果convertToPublic含私钥的RSA将只返回公钥,仅含公钥的RSA不受影响 - /// - public string ToPEM_PKCS8(bool convertToPublic = false) { - return new RSA_PEM(rsa).ToPEM(convertToPublic, true); - } - /// /// 将密钥对导出成PEM对象,如果convertToPublic含私钥的RSA将只返回公钥,仅含公钥的RSA不受影响 /// public RSA_PEM ToPEM(bool convertToPublic = false) { @@ -213,5 +201,23 @@ public RSA(string pem, bool noop) { public RSA(RSA_PEM pem) { rsa = pem.GetRSA(); } + /// + /// 本方法会先生成RSA_PEM再创建RSA:通过公钥指数和私钥指数构造一个PEM,会反推计算出P、Q但和原始生成密钥的P、Q极小可能相同 + /// 注意:所有参数首字节如果是0,必须先去掉 + /// 出错将会抛出异常 + /// + /// 必须提供模数 + /// 必须提供公钥指数 + /// 私钥指数可以不提供,导出的PEM就只包含公钥 + public RSA(byte[] modulus, byte[] exponent, byte[] dOrNull) { + rsa = new RSA_PEM(modulus, exponent, dOrNull).GetRSA(); + } + /// + /// 本方法会先生成RSA_PEM再创建RSA:通过全量的PEM字段数据构造一个PEM,除了模数modulus和公钥指数exponent必须提供外,其他私钥指数信息要么全部提供,要么全部不提供(导出的PEM就只包含公钥) + /// 注意:所有参数首字节如果是0,必须先去掉 + /// + public RSA(byte[] modulus, byte[] exponent, byte[] d, byte[] p, byte[] q, byte[] dp, byte[] dq, byte[] inverseQ) { + rsa = new RSA_PEM(modulus, exponent, d, p, q, dp, dq, inverseQ).GetRSA(); + } } } diff --git a/RSA_PEM.cs b/RSA_PEM.cs index a633765..d007a16 100644 --- a/RSA_PEM.cs +++ b/RSA_PEM.cs @@ -295,7 +295,7 @@ static public RSA_PEM FromPEM(string pem) { //读取数据总长度 readLen(0x30); - //看看有没有oid + //检测PKCS8 var idx2 = idx; if (eq(_SeqOID)) { //读取1长度 @@ -363,11 +363,30 @@ static public RSA_PEM FromPEM(string pem) { - /// - /// 将RSA中的密钥对转换成PEM格式,usePKCS8=false时返回PKCS#1格式,否则返回PKCS#8格式,如果convertToPublic含私钥的RSA将只返回公钥,仅含公钥的RSA不受影响 + /// 将RSA中的密钥对转换成PEM PKCS#1格式 + /// 。convertToPublic:等于true时含私钥的RSA将只返回公钥,仅含公钥的RSA不受影响 + /// 。公钥如:-----BEGIN RSA PUBLIC KEY-----,私钥如:-----BEGIN RSA PRIVATE KEY----- + /// 。似乎导出PKCS#1公钥用的比较少,PKCS#8的公钥用的多些,私钥#1#8都差不多 /// - public string ToPEM(bool convertToPublic, bool usePKCS8) { + public string ToPEM_PKCS1(bool convertToPublic = false) { + return ToPEM(convertToPublic, false, false); + } + /// + /// 将RSA中的密钥对转换成PEM PKCS#8格式 + /// 。convertToPublic:等于true时含私钥的RSA将只返回公钥,仅含公钥的RSA不受影响 + /// 。公钥如:-----BEGIN PUBLIC KEY-----,私钥如:-----BEGIN PRIVATE KEY----- + /// + public string ToPEM_PKCS8(bool convertToPublic = false) { + return ToPEM(convertToPublic, true, true); + } + /// + /// 将RSA中的密钥对转换成PEM格式 + /// 。convertToPublic:等于true时含私钥的RSA将只返回公钥,仅含公钥的RSA不受影响 + /// 。privateUsePKCS8:私钥的返回格式,等于true时返回PKCS#8格式(-----BEGIN PRIVATE KEY-----),否则返回PKCS#1格式(-----BEGIN RSA PRIVATE KEY-----),返回公钥时此参数无效;两种格式使用都比较常见 + /// 。publicUsePKCS8:公钥的返回格式,等于true时返回PKCS#8格式(-----BEGIN PUBLIC KEY-----),否则返回PKCS#1格式(-----BEGIN RSA PUBLIC KEY-----),返回私钥时此参数无效;一般用的多的是true PKCS#8格式公钥,PKCS#1格式似乎比较少见公钥 + /// + public string ToPEM(bool convertToPublic, bool privateUsePKCS8, bool publicUsePKCS8) { //https://www.jianshu.com/p/25803dd9527d //https://www.cnblogs.com/ylz8401/p/8443819.html //https://blog.csdn.net/jiayanhui2877/article/details/47187077 @@ -440,18 +459,22 @@ public string ToPEM(bool convertToPublic, bool usePKCS8) { ms.WriteByte(0x30); var index1 = (int)ms.Length; - //固定内容 - // encoded OID sequence for PKCS #1 rsaEncryption szOID_RSA_RSA = "1.2.840.113549.1.1.1" - writeAll(ms, _SeqOID); + //PKCS8 多一段数据 + int index2 = -1, index3 = -1; + if (publicUsePKCS8) { + //固定内容 + // encoded OID sequence for PKCS #1 rsaEncryption szOID_RSA_RSA = "1.2.840.113549.1.1.1" + writeAll(ms, _SeqOID); - //从0x00开始的后续长度 - ms.WriteByte(0x03); - var index2 = (int)ms.Length; - ms.WriteByte(0x00); + //从0x00开始的后续长度 + ms.WriteByte(0x03); + index2 = (int)ms.Length; + ms.WriteByte(0x00); - //后续内容长度 - ms.WriteByte(0x30); - var index3 = (int)ms.Length; + //后续内容长度 + ms.WriteByte(0x30); + index3 = (int)ms.Length; + } //写入Modulus writeBlock(Key_Modulus); @@ -463,12 +486,18 @@ public string ToPEM(bool convertToPublic, bool usePKCS8) { //计算空缺的长度 var byts = ms.ToArray(); - byts = writeLen(index3, byts); - byts = writeLen(index2, byts); + if (index2 != -1) { + byts = writeLen(index3, byts); + byts = writeLen(index2, byts); + } byts = writeLen(index1, byts); - return "-----BEGIN PUBLIC KEY-----\n" + TextBreak(Convert.ToBase64String(byts), 64) + "\n-----END PUBLIC KEY-----"; + var flag = " PUBLIC KEY"; + if (!publicUsePKCS8) { + flag = " RSA" + flag; + } + return "-----BEGIN" + flag + "-----\n" + TextBreak(Convert.ToBase64String(byts), 64) + "\n-----END" + flag + "-----"; } else { /****生成私钥****/ @@ -481,7 +510,7 @@ public string ToPEM(bool convertToPublic, bool usePKCS8) { //PKCS8 多一段数据 int index2 = -1, index3 = -1; - if (usePKCS8) { + if (privateUsePKCS8) { //固定内容 writeAll(ms, _SeqOID); @@ -519,7 +548,7 @@ public string ToPEM(bool convertToPublic, bool usePKCS8) { var flag = " PRIVATE KEY"; - if (!usePKCS8) { + if (!privateUsePKCS8) { flag = " RSA" + flag; } return "-----BEGIN" + flag + "-----\n" + TextBreak(Convert.ToBase64String(byts), 64) + "\n-----END" + flag + "-----"; From 3b2c98e7e79e44886608ab0d402b2089806acf40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=AB=98=E5=9D=9A=E6=9E=9C?= <753610399@qq.com> Date: Thu, 1 Oct 2020 00:31:53 +0800 Subject: [PATCH 09/15] =?UTF-8?q?=E4=BF=AE=E5=A4=8DQQ:284485094=E6=8F=90?= =?UTF-8?q?=E5=87=BA=E7=9A=84bug=EF=BC=9AC#=E7=94=9F=E6=88=90=E5=AF=86?= =?UTF-8?q?=E9=92=A5=E5=81=B6=E5=B0=94=E4=BC=9A=E6=9C=89=E5=8F=82=E6=95=B0?= =?UTF-8?q?=E4=BD=8D=E6=95=B0=E4=BC=9A=E5=B0=91=E4=B8=80=E4=BD=8D=EF=BC=8C?= =?UTF-8?q?Java=E7=94=9F=E6=88=90=E7=9A=84=E5=B0=B1=E6=B2=A1=E6=9C=89?= =?UTF-8?q?=E8=BF=99=E7=A7=8D=E9=97=AE=E9=A2=98=EF=BC=8C=E5=B7=B2=E5=85=BC?= =?UTF-8?q?=E5=AE=B9C#=20RSACryptoServiceProvider=E7=94=9F=E6=88=90?= =?UTF-8?q?=E7=9A=84=E8=BF=99=E7=A7=8D=E7=89=B9=E6=AE=8A=E5=AF=86=E9=92=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Program.cs | 1 + README.md | 12 ++++++++++++ RSA_PEM.cs | 53 ++++++++++++++++++++++++++++++++++------------------- 3 files changed, 47 insertions(+), 19 deletions(-) diff --git a/Program.cs b/Program.cs index f2bd67e..b431b27 100644 --- a/Program.cs +++ b/Program.cs @@ -78,6 +78,7 @@ static void Main(string[] args) { Console.WriteLine("◆◆◆◆◆◆◆◆◆◆◆◆ RSA测试 ◆◆◆◆◆◆◆◆◆◆◆◆"); Console.WriteLine("---------------------------------------------------------"); + //for (var i = 0; i < 1000; i++) { Console.WriteLine("第"+i+"次>>>>>"); RSATest(); } RSATest(); Console.WriteLine("-------------------------------------------------------------"); diff --git a/README.md b/README.md index 3f615bf..e711373 100644 --- a/README.md +++ b/README.md @@ -418,6 +418,18 @@ yZKNX3VxmLEHXQ== ``` +## Bug:C#生成的非正常密钥 +RSACryptoServiceProvider生成的密钥,有一定概率生成某些字段首字节为0的密钥(测试时发现几百个里面有一个),解析这种数据就难搞了,Java生成的密钥就没有出现这种情况。直接放2个样本吧,这两个样本是有效的密钥: +``` +样本一:D参数首字节是0 +3pfIaO4DYwrsymg7xddlr2gN2rtl2C+H3+RX6Nc3GQMJ+otZn5exMg5cMIWe+aS3mWB9XJi9AxlRdRAFf4j1PQ==AQAB

/nSZaBqEGehINeMa805aAqjNOhlHNYGZmF/C4evMvsc=

3/GsDJA/AnKq/lqbpCr1OB5h2wIKsLlPGafPljFzN9s=CSHGH6ZT91oOvWBZJ0I4mL/WHa+qjpEIIh/Nrq33uyE=nIg/m3SEJoDiVvIcko7YYwaRndT6hfaxfJxYtIISKDs=rok4ds5VOzho0h+4uvTeeOjEL1jxDaarUlnb3gxB6yY=ADQrXs04+5I6/URzKY807KAvww+A3F3OxgmzeucXidJSc+RMfVzkdf30X2iJHTj20EpzSfwCnlRNERCYXIqskQ==
+ +样本二:DP参数首字节是0 +zbTQkRxbyJfCYnEzUjG3rWWRCpWYka5rWmnkqPXCYvLdZ5OIJEy+2Rgu9wCAnCdCBWMRLdUWjmJdNQizBcITVQ==AQAB

+N3qwya7gWN568BuciUwkSlgWVlORusk267Nkkiu3jc=

05o0BiIaDFYtHYNUh5/ROgDSkWqPXjy8Nlmh0S6QdNM=AJemo2hIMfqmo6UFnkfwYagTjqLjyM9uewdjfeGmaOk=ZK3D/v8Owbvm71njSDxkQmLNzV6UJFRlgL6Y3Xx4Qv0=zHAAUftduljSvsxV0TAblEX+FUXw3unb3M6oaow7+R0=n7XqVTAiZuzFBG+FfCSTynHYGdKqITm9qfYbjb85zF4EWxZWdMjm/9V2guKbRYGV9Xo96PowNnjeJ+NkbG5B5Q==
+``` + +> 这几个问题是QQ:284485094 提出的,循环一下测试很容易出现这个现象,现在的代码已经兼容了这种密钥。 + ## openssl RSA常用命令行 ``` bat diff --git a/RSA_PEM.cs b/RSA_PEM.cs index d007a16..2768b64 100644 --- a/RSA_PEM.cs +++ b/RSA_PEM.cs @@ -76,13 +76,14 @@ public RSA_PEM(RSACryptoServiceProvider rsa, bool convertToPublic = false) { public RSA_PEM(byte[] modulus, byte[] exponent, byte[] d, byte[] p, byte[] q, byte[] dp, byte[] dq, byte[] inverseQ) { Key_Modulus = modulus; Key_Exponent = exponent; - Key_D = d; - - Val_P = p; - Val_Q = q; - Val_DP = dp; - Val_DQ = dq; - Val_InverseQ = inverseQ; + Key_D = BigL(d, modulus.Length); + + int keyLen = modulus.Length / 2; + Val_P = BigL(p, keyLen); + Val_Q = BigL(q, keyLen); + Val_DP = BigL(dp, keyLen); + Val_DQ = BigL(dq, keyLen); + Val_InverseQ = BigL(inverseQ, keyLen); } /// /// 通过公钥指数和私钥指数构造一个PEM,会反推计算出P、Q但和原始生成密钥的P、Q极小可能相同 @@ -97,7 +98,7 @@ public RSA_PEM(byte[] modulus, byte[] exponent, byte[] dOrNull) { Key_Exponent = exponent;//publicExponent if (dOrNull != null) { - Key_D = dOrNull;//privateExponent + Key_D = BigL(dOrNull, modulus.Length);//privateExponent //反推P、Q BigInteger n = BigX(modulus); @@ -114,11 +115,12 @@ public RSA_PEM(byte[] modulus, byte[] exponent, byte[] dOrNull) { BigInteger exp2 = d % (q - BigInteger.One); BigInteger coeff = BigInteger.ModPow(q, p - 2, p); - Val_P = BigB(p);//prime1 - Val_Q = BigB(q);//prime2 - Val_DP = BigB(exp1);//exponent1 - Val_DQ = BigB(exp2);//exponent2 - Val_InverseQ = BigB(coeff);//coefficient + int keyLen = modulus.Length / 2; + Val_P = BigL(BigB(p), keyLen);//prime1 + Val_Q = BigL(BigB(q), keyLen);//prime2 + Val_DP = BigL(BigB(exp1), keyLen);//exponent1 + Val_DQ = BigL(BigB(exp2), keyLen);//exponent2 + Val_InverseQ = BigL(BigB(coeff), keyLen);//coefficient } } @@ -184,6 +186,17 @@ static public byte[] BigB(BigInteger bigx) { return val; } /// + /// 某些密钥参数可能会少一位(32个byte只有31个,目测是密钥生成器的问题,只在c#生成的密钥中发现这种参数,java中生成的密钥没有这种现象),直接修正一下就行;这个问题与BigB有本质区别,不能动BigB + /// + static public byte[] BigL(byte[] bytes, int keyLen) { + if (keyLen - bytes.Length == 1) { + byte[] c = new byte[bytes.Length + 1]; + Array.Copy(bytes, 0, c, 1, bytes.Length); + bytes = c; + } + return bytes; + } + /// /// 由n e d 反推 P Q /// 资料: https://stackoverflow.com/questions/43136036/how-to-get-a-rsaprivatecrtkey-from-a-rsaprivatekey /// https://v2ex.com/t/661736 @@ -341,12 +354,14 @@ static public RSA_PEM FromPEM(string pem) { //读取数据 param.Key_Modulus = readBlock(); param.Key_Exponent = readBlock(); - param.Key_D = readBlock(); - param.Val_P = readBlock(); - param.Val_Q = readBlock(); - param.Val_DP = readBlock(); - param.Val_DQ = readBlock(); - param.Val_InverseQ = readBlock(); + int keyLen = param.Key_Modulus.Length; + param.Key_D = BigL(readBlock(), keyLen); + keyLen = keyLen / 2; + param.Val_P = BigL(readBlock(), keyLen); + param.Val_Q = BigL(readBlock(), keyLen); + param.Val_DP = BigL(readBlock(), keyLen); + param.Val_DQ = BigL(readBlock(), keyLen); + param.Val_InverseQ = BigL(readBlock(), keyLen); } else { throw new Exception("pem需要BEGIN END标头"); } From 7bbfc96496c0b8dc718cfbfad8e1b35d1f2bec1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=AB=98=E5=9D=9A=E6=9E=9C?= <753610399@qq.com> Date: Thu, 1 Oct 2020 01:36:51 +0800 Subject: [PATCH 10/15] =?UTF-8?q?=E5=8F=91=E5=B8=831.3=E7=89=88=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Properties/AssemblyInfo.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Properties/AssemblyInfo.cs b/Properties/AssemblyInfo.cs index 2405ae3..87cc024 100644 --- a/Properties/AssemblyInfo.cs +++ b/Properties/AssemblyInfo.cs @@ -29,5 +29,5 @@ // 生成号 // 修订号 // -[assembly: AssemblyVersion("1.2.0.0")] -[assembly: AssemblyFileVersion("1.2.0.0")] +[assembly: AssemblyVersion("1.3.0.0")] +[assembly: AssemblyFileVersion("1.3.0.0")] From ad1b778eca8f7b7348a9e9c6b478548cb719ddb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=AB=98=E5=9D=9A=E6=9E=9C?= <753610399@qq.com> Date: Fri, 13 Nov 2020 12:09:11 +0800 Subject: [PATCH 11/15] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index e711373..a79bf0e 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,13 @@ 注:`openssl rsa -in 私钥文件 -pubout`导出的是PKCS#8格式公钥(用的比较多),`openssl rsa -pubin -in PKCS#8公钥文件 -RSAPublicKey_out`导出的是PKCS#1格式公钥(用的比较少)。 +### 静态方法 + +**static RSA_PEM FromPEM(string pem)**:用PEM格式密钥对创建RSA,支持PKCS#1、PKCS#8格式的PEM,出错将会抛出异常。pem格式如:`-----BEGIN XXX KEY-----....-----END XXX KEY-----`。 + +**static RSA_PEM FromXML(string xml)**:将XML格式密钥转成PEM,支持公钥xml、私钥xml,出错将会抛出异常。xml格式如:`....`。 + + ### 构造方法 **RSA_PEM(RSACryptoServiceProvider rsa, bool convertToPublic = false)**:通过RSA中的公钥和私钥构造一个PEM,如果convertToPublic含私钥的RSA将只读取公钥,仅含公钥的RSA不受影响。 @@ -73,13 +80,6 @@ bool:**HasPrivate**(是否包含私钥) **string ToXML(bool convertToPublic)**:将RSA中的密钥对转换成XML格式,如果convertToPublic含私钥的RSA将只返回公钥,仅含公钥的RSA不受影响。 -### 静态方法 - -**static RSA_PEM FromPEM(string pem)**:用PEM格式密钥对创建RSA,支持PKCS#1、PKCS#8格式的PEM,出错将会抛出异常。pem格式如:`-----BEGIN XXX KEY-----....-----END XXX KEY-----`。 - -**static RSA_PEM FromXML(string xml)**:将XML格式密钥转成PEM,支持公钥xml、私钥xml,出错将会抛出异常。 - - ## 【RSA.cs】 From 96a6c71a5298dd93ff70c09b2f310e191817f70d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=AB=98=E5=9D=9A=E6=9E=9C?= <753610399@qq.com> Date: Thu, 9 Jun 2022 01:27:37 +0800 Subject: [PATCH 12/15] =?UTF-8?q?=E5=8F=91=E5=B8=831.5=E7=89=88=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Program.cs | 177 ++++++++++++++++++++++------ README.md | 174 +++++++++++++++++++++------ RSA_ForCore.cs | 229 ++++++++++++++++++++++++++++++++++++ RSA.cs => RSA_ForWindows.cs | 52 ++++---- RSA_PEM.cs | 61 ++++++---- vs.csproj | 3 +- 6 files changed, 570 insertions(+), 126 deletions(-) create mode 100644 RSA_ForCore.cs rename RSA.cs => RSA_ForWindows.cs (75%) diff --git a/Program.cs b/Program.cs index b431b27..52c49ad 100644 --- a/Program.cs +++ b/Program.cs @@ -2,17 +2,70 @@ namespace com.github.xiangyuecn.rsacsharp { /// - /// RSA、RSA_PEM测试控制台主程序 + /// RSA、RSA_PEM测试控制台主程序,.NET Core、.NET Framework均可测试 /// GitHub: https://github.com/xiangyuecn/RSA-csharp /// class Program { - static void RSATest() { - var rsa = new RSA(512); + static void RSA_ForCoreTest(bool fast) { + Console.WriteLine(hr); + Console.WriteLine(ht + " RSA_ForCoreTest:仅限.NET Core下可测试 " + ht); + Console.WriteLine(hr); + + Console.WriteLine("RSA_ForCore:只支持.NET Core环境,可跨平台使用。如需在.NET Framework中使用,请用RSA_ForWindows(也支持.NET Core,但只能Windows系统中使用)。"); + Console.WriteLine(); + if (!RSA_ForCore.IS_CORE) { + Console.WriteLine("非.NET Core环境,不测试!"); + return; + } + + //新生成一个RSA密钥,也可以通过已有的pem、xml文本密钥来创建RSA + var rsa = new RSA_ForCore(512); + // var rsa = new RSA_ForCore("pem或xml文本密钥"); + // var rsa = new RSA_ForCore(RSA_PEM.FromPEM("pem文本密钥")); + // var rsa = new RSA_ForCore(RSA_PEM.FromXML("xml文本密钥")); + + ForXxxxxxTest(rsa, fast); + } + static void RSA_ForWindowsTest(bool fast) { + Console.WriteLine(hr); + Console.WriteLine(ht + " RSA_ForWindowsTest:仅限Windows系统可测试 " + ht); + Console.WriteLine(hr); + + Console.WriteLine("RSA_ForWindows:.NET Core、.NET Framework均可用,但由于使用的RSACryptoServiceProvider不支持跨平台,只支持在Windows系统中使用,要跨平台请使用仅支持.NET Core的RSA_ForCore。"); + Console.WriteLine(); + if (!isWindows) { + Console.WriteLine("非Windows系统,不测试!"); + return; + } + + //新生成一个RSA密钥,也可以通过已有的pem、xml文本密钥来创建RSA + var rsa = new RSA_ForWindows(512); + // var rsa = new RSA_ForWindows("pem或xml文本密钥"); + // var rsa = new RSA_ForWindows(RSA_PEM.FromPEM("pem文本密钥")); + // var rsa = new RSA_ForWindows(RSA_PEM.FromXML("xml文本密钥")); + + ForXxxxxxTest(rsa, fast); + } + + + + static void ForXxxxxxTest(dynamic forXxxxxx, bool fast) { + dynamic rsa = forXxxxxx; + //两种rsa都有相同的方法,为什么没有抽象类,因为懒,实际使用应当有明确的类型,就像下面两行,解开注释就能恢复正常 + // RSA_ForWindows rsa = (RSA_ForWindows)forXxxxxx; + // RSA_ForCore rsa = (RSA_ForCore)forXxxxxx; + + //提取密钥pem字符串 + string pem_pkcs1 = rsa.ToPEM().ToPEM_PKCS1(); + string pem_pkcs8 = rsa.ToPEM().ToPEM_PKCS8(); + //提取密钥xml字符串 + string xml = rsa.ToXML(); + Console.WriteLine("【" + rsa.KeySize + "私钥(XML)】:"); - Console.WriteLine(rsa.ToXML()); + Console.WriteLine(xml); Console.WriteLine(); Console.WriteLine("【" + rsa.KeySize + "私钥(PKCS#1)】:"); - Console.WriteLine(rsa.ToPEM().ToPEM_PKCS1()); + Console.WriteLine(pem_pkcs1); Console.WriteLine(); Console.WriteLine("【" + rsa.KeySize + "公钥(PKCS#8)】:"); Console.WriteLine(rsa.ToPEM().ToPEM_PKCS8(true)); @@ -24,39 +77,66 @@ static void RSATest() { Console.WriteLine(en); Console.WriteLine("【解密】:"); - Console.WriteLine(rsa.DecodeOrNull(en)); + var de = rsa.DecodeOrNull(en); + AssertMsg(de, de == str); + + if (!fast) { + var str2 = str; for (var i = 0; i < 15; i++) str2 += str2; + Console.WriteLine("【长文本加密解密】:"); + AssertMsg(str2.Length + "个字 OK", rsa.DecodeOrNull(rsa.Encode(str2)) == str2); + } Console.WriteLine("【签名SHA1】:"); - Console.WriteLine(rsa.Sign("SHA1", str)); + var sign = rsa.Sign("SHA1", str); + Console.WriteLine(sign); + AssertMsg("校验 OK", rsa.Verify("SHA1", sign, str)); Console.WriteLine(); - var rsa2 = new RSA(rsa.ToPEM().ToPEM_PKCS8(), true); + //用pem文本创建RSA + var rsa2 = new RSA_ForWindows(RSA_PEM.FromPEM(pem_pkcs8)); Console.WriteLine("【用PEM新创建的RSA是否和上面的一致】:"); - Console.WriteLine("XML:" + (rsa2.ToXML() == rsa.ToXML())); - Console.WriteLine("PKCS1:" + (rsa2.ToPEM().ToPEM_PKCS1() == rsa.ToPEM().ToPEM_PKCS1())); - Console.WriteLine("PKCS8:" + (rsa2.ToPEM().ToPEM_PKCS8() == rsa.ToPEM().ToPEM_PKCS8())); + Assert("XML:", rsa2.ToXML() == rsa.ToXML()); + Assert("PKCS1:", rsa2.ToPEM().ToPEM_PKCS1() == rsa.ToPEM().ToPEM_PKCS1()); + Assert("PKCS8:", rsa2.ToPEM().ToPEM_PKCS8() == rsa.ToPEM().ToPEM_PKCS8()); - var rsa3 = new RSA(rsa.ToXML()); + //用xml文本创建RSA + var rsa3 = new RSA_ForWindows(RSA_PEM.FromXML(xml)); Console.WriteLine("【用XML新创建的RSA是否和上面的一致】:"); - Console.WriteLine("XML:" + (rsa3.ToXML() == rsa.ToXML())); - Console.WriteLine("PKCS1:" + (rsa3.ToPEM().ToPEM_PKCS1() == rsa.ToPEM().ToPEM_PKCS1())); - Console.WriteLine("PKCS8:" + (rsa3.ToPEM().ToPEM_PKCS8() == rsa.ToPEM().ToPEM_PKCS8())); - - //--------RSA_PEM验证--------- - RSA_PEM pem = rsa.ToPEM(); - Console.WriteLine("【RSA_PEM是否和原始RSA一致】:"); - Console.WriteLine(pem.KeySize + "位"); - Console.WriteLine("XML:" + (pem.ToXML(false) == rsa.ToXML())); - Console.WriteLine("PKCS1:" + (pem.ToPEM_PKCS1() == rsa.ToPEM().ToPEM_PKCS1())); - Console.WriteLine("PKCS8:" + (pem.ToPEM_PKCS8() == rsa.ToPEM().ToPEM_PKCS8())); - Console.WriteLine("仅公钥:"); - Console.WriteLine("XML:" + (pem.ToXML(true) == rsa.ToXML(true))); - Console.WriteLine("PKCS1:" + (pem.ToPEM_PKCS1(true) == rsa.ToPEM().ToPEM_PKCS1(true))); - Console.WriteLine("PKCS8:" + (pem.ToPEM_PKCS8(true) == rsa.ToPEM().ToPEM_PKCS8(true))); - - var rsa4 = new RSA(new RSA_PEM(pem.Key_Modulus, pem.Key_Exponent, pem.Key_D)); - Console.WriteLine("【用n、e、d构造解密】"); - Console.WriteLine(rsa4.DecodeOrNull(en)); + Assert("XML:", rsa3.ToXML() == rsa.ToXML()); + Assert("PKCS1:", rsa3.ToPEM().ToPEM_PKCS1() == rsa.ToPEM().ToPEM_PKCS1()); + Assert("PKCS8:", rsa3.ToPEM().ToPEM_PKCS8() == rsa.ToPEM().ToPEM_PKCS8()); + + //--------RSA_PEM私钥验证--------- + { + RSA_PEM pem = rsa.ToPEM(); + Console.WriteLine("【RSA_PEM是否和原始RSA一致】:"); + Console.WriteLine(pem.KeySize + "位"); + Assert("XML:", pem.ToXML(false) == rsa.ToXML()); + Assert("PKCS1:", pem.ToPEM_PKCS1() == rsa.ToPEM().ToPEM_PKCS1()); + Assert("PKCS8:", pem.ToPEM_PKCS8() == rsa.ToPEM().ToPEM_PKCS8()); + Console.WriteLine("仅公钥:"); + Assert("XML:", pem.ToXML(true) == rsa.ToXML(true)); + Assert("PKCS1:", pem.ToPEM_PKCS1(true) == rsa.ToPEM().ToPEM_PKCS1(true)); + Assert("PKCS8:", pem.ToPEM_PKCS8(true) == rsa.ToPEM().ToPEM_PKCS8(true)); + } + //--------RSA_PEM公钥验证--------- + { + var rsaPublic = new RSA_ForWindows(rsa.ToPEM(true)); + RSA_PEM pem = rsaPublic.ToPEM(); + Console.WriteLine("【RSA_PEM仅公钥是否和原始RSA一致】:"); + Console.WriteLine(pem.KeySize + "位"); + Assert("XML:", pem.ToXML(false) == rsa.ToXML(true)); + Assert("PKCS1:", pem.ToPEM_PKCS1() == rsa.ToPEM().ToPEM_PKCS1(true)); + Assert("PKCS8:", pem.ToPEM_PKCS8() == rsa.ToPEM().ToPEM_PKCS8(true)); + } + + if (!fast) { + RSA_PEM pem = rsa.ToPEM(); + var rsa4 = new RSA_ForWindows(new RSA_PEM(pem.Key_Modulus, pem.Key_Exponent, pem.Key_D)); + Console.WriteLine("【用n、e、d构造解密】"); + de = rsa4.DecodeOrNull(en); + AssertMsg(de, de == str); + } @@ -71,18 +151,37 @@ static void RSATest() { } + static void Assert(string msg, bool check) { + AssertMsg(msg + check, check); + } + static void AssertMsg(string msg, bool check) { + if (!check) throw new Exception(msg); + Console.WriteLine(msg); + } + + static readonly string ht = "◆◆◆◆◆◆◆◆◆◆◆◆"; + static readonly string hr = "---------------------------------------------------------"; + static bool isWindows = true; + static void Main(string[] _) { +#if (NETCOREAPP || NETSTANDARD || NET) //https://docs.microsoft.com/zh-cn/dotnet/csharp/language-reference/preprocessor-directives + if (!System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Windows)) { + isWindows = false; + } +#endif + + //for (var i = 0; i < 1000; i++) { Console.WriteLine("第"+i+"次>>>>>"); RSA_ForCoreTest(true); RSA_ForWindowsTest(true); } + RSA_ForCoreTest(false); - static void Main(string[] args) { - Console.WriteLine("---------------------------------------------------------"); - Console.WriteLine("◆◆◆◆◆◆◆◆◆◆◆◆ RSA测试 ◆◆◆◆◆◆◆◆◆◆◆◆"); - Console.WriteLine("---------------------------------------------------------"); + Console.WriteLine(hr); + Console.WriteLine(); - //for (var i = 0; i < 1000; i++) { Console.WriteLine("第"+i+"次>>>>>"); RSATest(); } - RSATest(); + RSA_ForWindowsTest(false); + Console.WriteLine(); - Console.WriteLine("-------------------------------------------------------------"); - Console.WriteLine("◆◆◆◆◆◆◆◆◆◆◆◆ 回车退出... ◆◆◆◆◆◆◆◆◆◆◆◆"); + Console.WriteLine(hr); + Console.WriteLine(ht + " 回车退出... " + ht); + Console.WriteLine(hr); Console.WriteLine(); Console.ReadLine(); } diff --git a/README.md b/README.md index a79bf0e..d3a946e 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,26 @@ +**【[源GitHub仓库](https://github.com/xiangyuecn/RSA-csharp)】 | 【[Gitee镜像库](https://gitee.com/xiangyuecn/RSA-csharp)】如果本文档图片没有显示,请手动切换到Gitee镜像库阅读文档。** + # :open_book:RSA-csharp的帮助文档 本项目核心功能:支持`.NET Core`、`.NET Framework`环境下`PEM`(`PKCS#1`、`PKCS#8`)格式RSA密钥对导入、导出。 -底层实现采用PEM文件二进制层面上进行字节码解析,简单轻巧0依赖;附带实现了一个RSA封装操作类,和一个测试控制台程序。 +你可以只copy `RSA_PEM.cs` 文件到你的项目中使用,只需这一个文件你就拥有了通过PEM格式密钥创建`RSA`或`RSACryptoServiceProvider`的能力。也可以clone整个项目代码用vs直接打开进行测试。 + +底层实现采用PEM文件二进制层面上进行字节码解析,简单轻巧0依赖;附带实现了两个RSA封装操作类(`RSA_ForCore.cs`、`RSA_ForWindows.cs`),和一个测试控制台程序(`Program.cs`)。 + +源文件|平台支持|功能说明|限制 +:-:|:-:|:-|:- +**RSA_PEM.cs**|.NET Core、.NET Framework|用于解析和导出PEM,创建RSA实例|无 +**RSA_ForWindows.cs**|.NET Core、.NET Framework|RSA操作类,封装了加密、解密、验签|只支持在Windows系统中使用,因为使用的RSACryptoServiceProvider不支持跨平台 +**RSA_ForCore.cs**|.NET Core|RSA操作类,封装了加密、解密、验签|只支持.NET Core环境,可跨平台使用 -你可以只copy `RSA_PEM.cs` 文件到你的项目中使用,只需这一个文件你就拥有了通过PEM格式密钥创建`RSACryptoServiceProvider`的能力。clone整个项目代码用vs应该能够直接打开,经目测看起来没什么卵用的文件都svn:ignore掉了(svn滑稽。 +**如需功能定制,网站、App、小程序开发等需求,请加本文档下面的QQ群,联系群主(即作者),谢谢~** 【Java版】:[RSA-java](https://github.com/xiangyuecn/RSA-java) +[​](?) + ## 特性 - 通过`XML格式`密钥对创建RSA @@ -21,6 +33,45 @@ - `PEM格式`秘钥对和`XML格式`秘钥对互转 +[​](?) + +## 如何加密、解密、签名、校验 +得到了RSA_PEM后,加密解密就异常简单了,没那么多啰嗦难懂的代码: +``` c# +//先解析pem,公钥私钥都行,如果是xml就用 RSA_PEM.FromXML("....") +var pem=RSA_PEM.FromPEM("-----BEGIN XXX KEY-----..此处意思意思..-----END XXX KEY-----"); + +//直接创建RSA操作类 +var rsa=new RSA_ForWindows(pem); //这个只能在Windows系统里面运行 +//var rsa=new RSA_ForCore(pem); //这个支持跨平台,但只支持.NET Core + +//var rsa=new RSA_ForWindows(2048); //也可以直接生成新密钥,rsa.ToPEM()得到pem对象 +//var rsa=new RSA_ForCore(2048); + +//加密 +var enTxt=rsa.Encode("测试123"); + +//解密 +var deTxt=rsa.DecodeOrNull(enTxt); + +//签名 +var sign=rsa.Sign("SHA1", "测试123"); + +//校验签名 +var isVerify=rsa.Verify("SHA1", sign, "测试123"); + +//导出pem文本 +var pemTxt=rsa.ToPEM().ToPEM_PKCS8(); + +Console.WriteLine(pemTxt+"\n"+enTxt+"\n"+deTxt+"\n"+sign+"\n"+isVerify); +Console.ReadLine(); +//****更多的实例,请阅读 Program.cs**** +//****更多功能方法,请阅读下面的详细文档**** +``` + + + +[​](?) ## 【QQ群】交流与支持 @@ -30,26 +81,41 @@ + + + +[​](?) + +[​](?) + +[​](?) + +[​](?) + +[​](?) + +[​](?) + # :open_book:文档 ## 【RSA_PEM.cs】 此文件不依赖任何文件,可以直接copy这个文件到你项目中用;通过`FromPEM`、`ToPEM` 和`FromXML`、`ToXML`这两对方法,可以实现PEM`PKCS#1`、`PKCS#8`相互转换,PEM、XML的相互转换。 -项目里面需要引入程序集`System.Numerics`用来支持`BigInteger`,vs默认创建的项目是不会自动引入此程序集的,要手动引入。 +Framework项目里面需要引入程序集`System.Numerics`用来支持`BigInteger`,vs默认创建的项目是不会自动引入此程序集的,要手动引入,Core的不需要。 注:`openssl rsa -in 私钥文件 -pubout`导出的是PKCS#8格式公钥(用的比较多),`openssl rsa -pubin -in PKCS#8公钥文件 -RSAPublicKey_out`导出的是PKCS#1格式公钥(用的比较少)。 ### 静态方法 -**static RSA_PEM FromPEM(string pem)**:用PEM格式密钥对创建RSA,支持PKCS#1、PKCS#8格式的PEM,出错将会抛出异常。pem格式如:`-----BEGIN XXX KEY-----....-----END XXX KEY-----`。 +`RSA_PEM` **FromPEM(string pem)**:用PEM格式密钥对创建RSA,支持PKCS#1、PKCS#8格式的PEM,出错将会抛出异常。pem格式如:`-----BEGIN XXX KEY-----....-----END XXX KEY-----`。 -**static RSA_PEM FromXML(string xml)**:将XML格式密钥转成PEM,支持公钥xml、私钥xml,出错将会抛出异常。xml格式如:`....`。 +`RSA_PEM` **FromXML(string xml)**:将XML格式密钥转成PEM,支持公钥xml、私钥xml,出错将会抛出异常。xml格式如:`....`。 ### 构造方法 -**RSA_PEM(RSACryptoServiceProvider rsa, bool convertToPublic = false)**:通过RSA中的公钥和私钥构造一个PEM,如果convertToPublic含私钥的RSA将只读取公钥,仅含公钥的RSA不受影响。 +**RSA_PEM(RSA rsa, bool convertToPublic = false)**:通过RSA中的公钥和私钥构造一个PEM,如果convertToPublic含私钥的RSA将只读取公钥,仅含公钥的RSA不受影响。 **RSA_PEM(byte[] modulus, byte[] exponent, byte[] d, byte[] p, byte[] q, byte[] dp, byte[] dq, byte[] inverseQ)**:通过全量的PEM字段数据构造一个PEM,除了模数modulus和公钥指数exponent必须提供外,其他私钥指数信息要么全部提供,要么全部不提供(导出的PEM就只包含公钥)注意:所有参数首字节如果是0,必须先去掉。 @@ -58,83 +124,97 @@ ### 实例属性 -byte[]:**Key_Modulus**(模数n,公钥、私钥都有)、**Key_Exponent**(公钥指数e,公钥、私钥都有)、**Key_D**(私钥指数d,只有私钥的时候才有);有这3个足够用来加密解密。 +`byte[]`:**Key_Modulus**(模数n,公钥、私钥都有)、**Key_Exponent**(公钥指数e,公钥、私钥都有)、**Key_D**(私钥指数d,只有私钥的时候才有);有这3个足够用来加密解密。 -byte[]:**Val_P**(prime1)、**Val_Q**(prime2)、**Val_DP**(exponent1)、**Val_DQ**(exponent2)、**Val_InverseQ**(coefficient); (PEM中的私钥才有的更多的数值;可通过n、e、d反推出这些值(只是反推出有效值,和原始的值大概率不同))。 +`byte[]`:**Val_P**(prime1)、**Val_Q**(prime2)、**Val_DP**(exponent1)、**Val_DQ**(exponent2)、**Val_InverseQ**(coefficient); (PEM中的私钥才有的更多的数值;可通过n、e、d反推出这些值(只是反推出有效值,和原始的值大概率不同))。 -int:**KeySize**(密钥位数) +`int` **KeySize**:密钥位数。 -bool:**HasPrivate**(是否包含私钥) +`bool` **HasPrivate**:是否包含私钥。 ### 实例方法 -**RSACryptoServiceProvider GetRSA()**:将PEM中的公钥私钥转成RSA对象,如果未提供私钥,RSA中就只包含公钥。 +`RSA` **GetRSA_ForCore()**:将PEM中的公钥私钥转成RSA对象,如果未提供私钥,RSA中就只包含公钥。返回的RSA支持跨平台使用,但只支持在.NET Core环境中使用。 -**string ToPEM(bool convertToPublic, bool privateUsePKCS8, bool publicUsePKCS8)**:将RSA中的密钥对转换成PEM格式。convertToPublic:等于true时含私钥的RSA将只返回公钥,仅含公钥的RSA不受影响 。**privateUsePKCS8**:私钥的返回格式,等于true时返回PKCS#8格式(`-----BEGIN PRIVATE KEY-----`),否则返回PKCS#1格式(`-----BEGIN RSA PRIVATE KEY-----`),返回公钥时此参数无效;两种格式使用都比较常见。**publicUsePKCS8**:公钥的返回格式,等于true时返回PKCS#8格式(`-----BEGIN PUBLIC KEY-----`),否则返回PKCS#1格式(`-----BEGIN RSA PUBLIC KEY-----`),返回私钥时此参数无效;一般用的多的是true PKCS#8格式公钥,PKCS#1格式公钥似乎比较少见。 +`RSACryptoServiceProvider` **GetRSA_ForWindows()**:将PEM中的公钥私钥转成RSA对象,如果未提供私钥,RSA中就只包含公钥。.NET Core、.NET Framework均可用,但返回的RSACryptoServiceProvider不支持跨平台,所以只支持在Windows系统中使用。 -**string ToPEM_PKCS1(bool convertToPublic=false)**:ToPEM方法的简化写法,不管公钥还是私钥都返回PKCS#1格式;似乎导出PKCS#1公钥用的比较少,PKCS#8的公钥用的多些,私钥#1#8都差不多。 +`string` **ToPEM(bool convertToPublic, bool privateUsePKCS8, bool publicUsePKCS8)**:将RSA中的密钥对转换成PEM格式。convertToPublic:等于true时含私钥的RSA将只返回公钥,仅含公钥的RSA不受影响 。**privateUsePKCS8**:私钥的返回格式,等于true时返回PKCS#8格式(`-----BEGIN PRIVATE KEY-----`),否则返回PKCS#1格式(`-----BEGIN RSA PRIVATE KEY-----`),返回公钥时此参数无效;两种格式使用都比较常见。**publicUsePKCS8**:公钥的返回格式,等于true时返回PKCS#8格式(`-----BEGIN PUBLIC KEY-----`),否则返回PKCS#1格式(`-----BEGIN RSA PUBLIC KEY-----`),返回私钥时此参数无效;一般用的多的是true PKCS#8格式公钥,PKCS#1格式公钥似乎比较少见。 -**string ToPEM_PKCS8(bool convertToPublic=false)**:ToPEM方法的简化写法,不管公钥还是私钥都返回PKCS#8格式。 +`string` **ToPEM_PKCS1(bool convertToPublic=false)**:ToPEM方法的简化写法,不管公钥还是私钥都返回PKCS#1格式;似乎导出PKCS#1公钥用的比较少,PKCS#8的公钥用的多些,私钥#1#8都差不多。 -**string ToXML(bool convertToPublic)**:将RSA中的密钥对转换成XML格式,如果convertToPublic含私钥的RSA将只返回公钥,仅含公钥的RSA不受影响。 +`string` **ToPEM_PKCS8(bool convertToPublic=false)**:ToPEM方法的简化写法,不管公钥还是私钥都返回PKCS#8格式。 +`string` **ToXML(bool convertToPublic)**:将RSA中的密钥对转换成XML格式,如果convertToPublic含私钥的RSA将只返回公钥,仅含公钥的RSA不受影响。 -## 【RSA.cs】 -此文件依赖`RSA_PEM.cs`,封装了加密、解密、签名、验证、秘钥导入导出操作。 -### 构造方法 +## 【RSA_ForWindows.cs】【RSA_ForCore.cs】 +这两文件依赖`RSA_PEM.cs`,两个类的方法都是相同的(未做抽象,因为懒,要用那个就直接用哪个),封装了加密、解密、签名、验证、秘钥导入导出操作。 -**RSA(int keySize)**:用指定密钥大小创建一个新的RSA,会生成新密钥,出错抛异常。 +### 构造方法 -**RSA(string xml)**:通过XML格式密钥,创建一个RSA,xml内可以只包含一个公钥或私钥,或都包含,出错抛异常。`XML格式`如:`...` +**RSA_For??(int keySize)**:用指定密钥大小创建一个新的RSA,会生成新密钥,出错抛异常。 -**RSA(string pem, bool noop)**:通过`PEM格式`密钥对创建RSA(noop参数随意填),PEM可以是公钥或私钥,支持`PKCS#1`、`PKCS#8`格式,pem格式如:`-----BEGIN XXX KEY-----....-----END XXX KEY-----`。 +**RSA_For??(string pemOrXML)**:通过`PEM格式`或`XML格式`密钥,创建一个RSA,pem或xml内可以只包含一个公钥或私钥,或都包含,出错抛异常。`XML格式`如:`...`。pem支持`PKCS#1`、`PKCS#8`格式,格式如:`-----BEGIN XXX KEY-----....-----END XXX KEY-----`。 -**RSA(RSA_PEM pem)**:通过一个pem对象创建RSA,pem为公钥或私钥,出错抛异常。 +**RSA_For??(RSA_PEM pem)**:通过一个pem对象创建RSA,pem为公钥或私钥,出错抛异常。 -**RSA(byte[] modulus, byte[] exponent, byte[] d, byte[] p, byte[] q, byte[] dp, byte[] dq, byte[] inverseQ)**:本方法会先生成RSA_PEM再创建RSA。通过全量的PEM字段数据构造一个PEM,除了模数modulus和公钥指数exponent必须提供外,其他私钥指数信息要么全部提供,要么全部不提供(导出的PEM就只包含公钥)注意:所有参数首字节如果是0,必须先去掉。 +**RSA_For??(byte[] modulus, byte[] exponent, byte[] d, byte[] p, byte[] q, byte[] dp, byte[] dq, byte[] inverseQ)**:本方法会先生成RSA_PEM再创建RSA。通过全量的PEM字段数据构造一个PEM,除了模数modulus和公钥指数exponent必须提供外,其他私钥指数信息要么全部提供,要么全部不提供(导出的PEM就只包含公钥)注意:所有参数首字节如果是0,必须先去掉。 -**RSA(byte[] modulus, byte[] exponent, byte[] dOrNull)**:本方法会先生成RSA_PEM再创建RSA。通过公钥指数和私钥指数构造一个PEM,会反推计算出P、Q但和原始生成密钥的P、Q极小可能相同。注意:所有参数首字节如果是0,必须先去掉。出错将会抛出异常。私钥指数可以不提供,导出的PEM就只包含公钥。 +**RSA_For??(byte[] modulus, byte[] exponent, byte[] dOrNull)**:本方法会先生成RSA_PEM再创建RSA。通过公钥指数和私钥指数构造一个PEM,会反推计算出P、Q但和原始生成密钥的P、Q极小可能相同。注意:所有参数首字节如果是0,必须先去掉。出错将会抛出异常。私钥指数可以不提供,导出的PEM就只包含公钥。 ### 实例属性 -RSACryptoServiceProvider:**RSAObject**(最底层的RSACryptoServiceProvider对象) +`RSA`|`RSACryptoServiceProvider` **RSAObject**:最底层的RSA或RSACryptoServiceProvider对象。 -int:**KeySize**(密钥位数) +`int` **KeySize**:密钥位数。 -bool:**HasPrivate**(是否包含私钥) +`bool` **HasPrivate**:是否包含私钥。 ### 实例方法 -**string ToXML(bool convertToPublic = false)**:导出`XML格式`秘钥对。如果RSA包含私钥,默认会导出私钥,设置仅仅导出公钥时只会导出公钥;不包含私钥只会导出公钥。 +`string` **ToXML(bool convertToPublic = false)**:导出`XML格式`秘钥对。如果RSA包含私钥,默认会导出私钥,设置仅仅导出公钥时只会导出公钥;不包含私钥只会导出公钥。 + +`RSA_PEM` **ToPEM(bool convertToPublic = false)**:导出RSA_PEM对象(然后可以通过RSA_PEM.ToPEM方法导出PEM文本),如果convertToPublic含私钥的RSA将只返回公钥,仅含公钥的RSA不受影响。 + +`string` **Encode(string str)**:加密操作,支持任意长度数据,出错抛异常。 + +`byte[]` **Encode(byte[] data)**:加密数据,支持任意长度数据,出错抛异常。 + +`string` **DecodeOrNull(string str)**:解密字符串(utf-8),解密异常返回null。 + +`byte[]` **DecodeOrNull(byte[] data)**:解密数据,解密异常返回null。 + +`string` **Sign(string hash, string str)**:对str进行签名,并指定hash算法(如:SHA256)。 + +`byte[]` **Sign(string hash, byte[] data)**:对data进行签名,并指定hash算法(如:SHA256)。 + +`bool` **Verify(string hash, string sign, string str)**:验证字符串str的签名是否是sign,并指定hash算法(如:SHA256)。 -**RSA_PEM ToPEM(bool convertToPublic = false)**:导出RSA_PEM对象,如果convertToPublic含私钥的RSA将只返回公钥,仅含公钥的RSA不受影响;通过RSA_PEM.ToPEM方法可以导出PEM文本。 +`bool` **Verify(string hash, byte[] sign, byte[] data)**:验证data的签名是否是sign,并指定hash算法(如:SHA256)。 -**string Encode(string str)**:加密操作,支持任意长度数据。 -**byte[] Encode(byte[] data)**:加密数据,支持任意长度数据,出错抛异常。 -**string DecodeOrNull(string str)**:解密字符串(utf-8),解密异常返回null。 -**byte[] DecodeOrNull(byte[] data)**:解密数据,解密异常返回null。 -**string Sign(string hash, string str)**:对str进行签名,并指定hash算法(如:SHA256)。 -**byte[] Sign(string hash, byte[] data)**:对data进行签名,并指定hash算法(如:SHA256)。 -**bool Verify(string hash, string sgin, string str)**:验证字符串str的签名是否是sgin,并指定hash算法(如:SHA256)。 -**bool Verify(string hash, byte[] sgin, byte[] data)**:验证data的签名是否是sgin,并指定hash算法(如:SHA256)。 +[​](?) +[​](?) +[​](?) +[​](?) +[​](?) +[​](?) # :open_book:图例 @@ -150,6 +230,20 @@ RSA工具(非开源): + + +[​](?) + +[​](?) + +[​](?) + +[​](?) + +[​](?) + +[​](?) + # :open_book:知识库 在写一个小转换工具时加入了RSA加密解密支持(见图RSA工具),秘钥输入框支持填写XML和PEM格式,操作类型里面支持XML->PEM、PEM->XML的转换。 @@ -456,6 +550,12 @@ echo abcd123 | openssl rsautl -encrypt -inkey public.pem.rsakey -pubin +[​](?) + +[​](?) + +[​](?) + # :star:捐赠 如果这个库有帮助到您,请 Star 一下。 diff --git a/RSA_ForCore.cs b/RSA_ForCore.cs new file mode 100644 index 0000000..6b796e6 --- /dev/null +++ b/RSA_ForCore.cs @@ -0,0 +1,229 @@ +using System; +using System.IO; +using System.Security.Cryptography; +using System.Text; + +namespace com.github.xiangyuecn.rsacsharp { + /// + /// RSA操作类,只支持.NET Core环境,可跨平台使用。如需在.NET Framework中使用,请用RSA_ForWindows(也支持.NET Core,但只能Windows系统中使用) + /// GitHub: https://github.com/xiangyuecn/RSA-csharp + /// + public class RSA_ForCore { + /// + /// 导出XML格式密钥对,如果convertToPublic含私钥的RSA将只返回公钥,仅含公钥的RSA不受影响 + /// + public string ToXML(bool convertToPublic = false) { + return ToPEM(convertToPublic).ToXML(convertToPublic); + } + /// + /// 将密钥对导出成PEM对象,如果convertToPublic含私钥的RSA将只返回公钥,仅含公钥的RSA不受影响 + /// + public RSA_PEM ToPEM(bool convertToPublic = false) { + return new RSA_PEM(rsa, convertToPublic); + } + + + + + /// + /// 加密字符串(utf-8),出错抛异常 + /// + public string Encode(string str) { + return Convert.ToBase64String(Encode(Encoding.UTF8.GetBytes(str))); + } + /// + /// 加密数据,出错抛异常 + /// + public byte[] Encode(byte[] data) { + int blockLen = rsa.KeySize / 8 - 11; + if (data.Length <= blockLen) { + return rsa.Encrypt(data, RSAEncryptionPadding.Pkcs1); + } + + using (var dataStream = new MemoryStream(data)) + using (var enStream = new MemoryStream()) { + Byte[] buffer = new Byte[blockLen]; + int len = dataStream.Read(buffer, 0, blockLen); + + while (len > 0) { + Byte[] block = new Byte[len]; + Array.Copy(buffer, 0, block, 0, len); + + Byte[] enBlock = rsa.Encrypt(block, RSAEncryptionPadding.Pkcs1); + enStream.Write(enBlock, 0, enBlock.Length); + + len = dataStream.Read(buffer, 0, blockLen); + } + + return enStream.ToArray(); + } + } + /// + /// 解密字符串(utf-8),解密异常返回null + /// + public string DecodeOrNull(string str) { + if (String.IsNullOrEmpty(str)) { + return null; + } + byte[] byts = null; + try { byts = Convert.FromBase64String(str); } catch { } + if (byts == null) { + return null; + } + var val = DecodeOrNull(byts); + if (val == null) { + return null; + } + return Encoding.UTF8.GetString(val); + } + /// + /// 解密数据,解密异常返回null + /// + public byte[] DecodeOrNull(byte[] data) { + try { + int blockLen = rsa.KeySize / 8; + if (data.Length <= blockLen) { + return rsa.Decrypt(data, RSAEncryptionPadding.Pkcs1); + } + + using (var dataStream = new MemoryStream(data)) + using (var deStream = new MemoryStream()) { + Byte[] buffer = new Byte[blockLen]; + int len = dataStream.Read(buffer, 0, blockLen); + + while (len > 0) { + Byte[] block = new Byte[len]; + Array.Copy(buffer, 0, block, 0, len); + + Byte[] deBlock = rsa.Decrypt(block, RSAEncryptionPadding.Pkcs1); + deStream.Write(deBlock, 0, deBlock.Length); + + len = dataStream.Read(buffer, 0, blockLen); + } + + return deStream.ToArray(); + } + } catch { + return null; + } + } + /// + /// 对str进行签名,并指定hash算法(如:SHA256) + /// + public string Sign(string hash, string str) { + return Convert.ToBase64String(Sign(hash, Encoding.UTF8.GetBytes(str))); + } + /// + /// 对data进行签名,并指定hash算法(如:SHA256) + /// + public byte[] Sign(string hash, byte[] data) { + return rsa.SignData(data, new HashAlgorithmName(hash), RSASignaturePadding.Pkcs1); + } + /// + /// 验证字符串str的签名是否是sign,并指定hash算法(如:SHA256) + /// + public bool Verify(string hash, string sign, string str) { + byte[] byts = null; + try { byts = Convert.FromBase64String(sign); } catch { } + if (byts == null) { + return false; + } + return Verify(hash, byts, Encoding.UTF8.GetBytes(str)); + } + /// + /// 验证data的签名是否是sign,并指定hash算法(如:SHA256) + /// + public bool Verify(string hash, byte[] sign, byte[] data) { + try { + return rsa.VerifyData(data, sign, new HashAlgorithmName(hash), RSASignaturePadding.Pkcs1); + } catch { + return false; + } + } + + + + + /// + /// 最底层的RSA对象 + /// + public RSA RSAObject { + get { + return rsa; + } + } + + /// + /// 密钥位数 + /// + public int KeySize { + get { + return rsa.KeySize; + } + } + /// + /// 是否包含私钥 + /// + public bool HasPrivate { + get { + return ToPEM().HasPrivate; + } + } + + /// + /// 用指定密钥大小创建一个新的RSA,会生成新密钥,出错抛异常 + /// + public RSA_ForCore(int keySize) { + rsa = RSA.Create(); + rsa.KeySize = keySize; + } + /// + /// 通过指定的pem文件密钥或xml字符串密钥,创建一个RSA,pem或xml内可以只包含一个公钥或私钥,或都包含,出错抛异常 + /// + public RSA_ForCore(string pemOrXML) { + if (pemOrXML.Trim().StartsWith("<")) { + rsa = RSA_PEM.FromXML(pemOrXML).GetRSA_ForCore(); + } else { + rsa = RSA_PEM.FromPEM(pemOrXML).GetRSA_ForCore(); + } + } + /// + /// 通过一个pem对象创建RSA,pem为公钥或私钥,出错抛异常 + /// + public RSA_ForCore(RSA_PEM pem) { + rsa = pem.GetRSA_ForCore(); + } + /// + /// 本方法会先生成RSA_PEM再创建RSA:通过公钥指数和私钥指数构造一个PEM,会反推计算出P、Q但和原始生成密钥的P、Q极小可能相同 + /// 注意:所有参数首字节如果是0,必须先去掉 + /// 出错将会抛出异常 + /// + /// 必须提供模数 + /// 必须提供公钥指数 + /// 私钥指数可以不提供,导出的PEM就只包含公钥 + public RSA_ForCore(byte[] modulus, byte[] exponent, byte[] dOrNull) { + rsa = new RSA_PEM(modulus, exponent, dOrNull).GetRSA_ForCore(); + } + /// + /// 本方法会先生成RSA_PEM再创建RSA:通过全量的PEM字段数据构造一个PEM,除了模数modulus和公钥指数exponent必须提供外,其他私钥指数信息要么全部提供,要么全部不提供(导出的PEM就只包含公钥) + /// 注意:所有参数首字节如果是0,必须先去掉 + /// + public RSA_ForCore(byte[] modulus, byte[] exponent, byte[] d, byte[] p, byte[] q, byte[] dp, byte[] dq, byte[] inverseQ) { + rsa = new RSA_PEM(modulus, exponent, d, p, q, dp, dq, inverseQ).GetRSA_ForCore(); + } + + + + //.NET Framework 兼容编译 +#if (NETCOREAPP || NETSTANDARD || NET) //https://docs.microsoft.com/zh-cn/dotnet/csharp/language-reference/preprocessor-directives + private RSA rsa; + static public bool IS_CORE = true; +#else + private dynamic rsa; + static public bool IS_CORE = false; + class RSAEncryptionPadding { static public object Pkcs1; } + class RSASignaturePadding { static public object Pkcs1; } + class HashAlgorithmName { public HashAlgorithmName(string _) { } } +#endif + } +} diff --git a/RSA.cs b/RSA_ForWindows.cs similarity index 75% rename from RSA.cs rename to RSA_ForWindows.cs index 3d5185a..862a288 100644 --- a/RSA.cs +++ b/RSA_ForWindows.cs @@ -5,10 +5,10 @@ namespace com.github.xiangyuecn.rsacsharp { /// - /// RSA操作类 + /// RSA操作类,.NET Core、.NET Framework均可用,但由于使用的RSACryptoServiceProvider不支持跨平台,只支持在Windows系统中使用,要跨平台请使用仅支持.NET Core的RSA_ForCore /// GitHub: https://github.com/xiangyuecn/RSA-csharp /// - public class RSA { + public class RSA_ForWindows { /// /// 导出XML格式密钥对,如果convertToPublic含私钥的RSA将只返回公钥,仅含公钥的RSA不受影响 /// @@ -120,22 +120,22 @@ public byte[] Sign(string hash, byte[] data) { return rsa.SignData(data, hash); } /// - /// 验证字符串str的签名是否是sgin,并指定hash算法(如:SHA256) + /// 验证字符串str的签名是否是sign,并指定hash算法(如:SHA256) /// - public bool Verify(string hash, string sgin, string str) { + public bool Verify(string hash, string sign, string str) { byte[] byts = null; - try { byts = Convert.FromBase64String(sgin); } catch { } + try { byts = Convert.FromBase64String(sign); } catch { } if (byts == null) { return false; } return Verify(hash, byts, Encoding.UTF8.GetBytes(str)); } /// - /// 验证data的签名是否是sgin,并指定hash算法(如:SHA256) + /// 验证data的签名是否是sign,并指定hash算法(如:SHA256) /// - public bool Verify(string hash, byte[] sgin, byte[] data) { + public bool Verify(string hash, byte[] sign, byte[] data) { try { - return rsa.VerifyData(data, hash, sgin); + return rsa.VerifyData(data, hash, sign); } catch { return false; } @@ -172,34 +172,28 @@ public bool HasPrivate { } /// - /// 用指定密钥大小创建一个新的RSA,出错抛异常 + /// 用指定密钥大小创建一个新的RSA,会生成新密钥,出错抛异常 /// - public RSA(int keySize) { + public RSA_ForWindows(int keySize) { var rsaParams = new CspParameters(); rsaParams.Flags = CspProviderFlags.UseMachineKeyStore; rsa = new RSACryptoServiceProvider(keySize, rsaParams); } /// - /// 通过指定的密钥,创建一个RSA,xml内可以只包含一个公钥或私钥,或都包含,出错抛异常 + /// 通过指定的pem文件密钥或xml字符串密钥,创建一个RSA,pem或xml内可以只包含一个公钥或私钥,或都包含,出错抛异常 /// - public RSA(string xml) { - var rsaParams = new CspParameters(); - rsaParams.Flags = CspProviderFlags.UseMachineKeyStore; - rsa = new RSACryptoServiceProvider(rsaParams); - - rsa.FromXmlString(xml); - } - /// - /// 通过一个pem文件创建RSA,pem为公钥或私钥,出错抛异常 - /// - public RSA(string pem, bool noop) { - rsa = RSA_PEM.FromPEM(pem).GetRSA(); + public RSA_ForWindows(string pemOrXML) { + if (pemOrXML.Trim().StartsWith("<")) { + rsa = RSA_PEM.FromXML(pemOrXML).GetRSA_ForWindows(); + } else { + rsa = RSA_PEM.FromPEM(pemOrXML).GetRSA_ForWindows(); + } } /// /// 通过一个pem对象创建RSA,pem为公钥或私钥,出错抛异常 /// - public RSA(RSA_PEM pem) { - rsa = pem.GetRSA(); + public RSA_ForWindows(RSA_PEM pem) { + rsa = pem.GetRSA_ForWindows(); } /// /// 本方法会先生成RSA_PEM再创建RSA:通过公钥指数和私钥指数构造一个PEM,会反推计算出P、Q但和原始生成密钥的P、Q极小可能相同 @@ -209,15 +203,15 @@ public RSA(RSA_PEM pem) { /// 必须提供模数 /// 必须提供公钥指数 /// 私钥指数可以不提供,导出的PEM就只包含公钥 - public RSA(byte[] modulus, byte[] exponent, byte[] dOrNull) { - rsa = new RSA_PEM(modulus, exponent, dOrNull).GetRSA(); + public RSA_ForWindows(byte[] modulus, byte[] exponent, byte[] dOrNull) { + rsa = new RSA_PEM(modulus, exponent, dOrNull).GetRSA_ForWindows(); } /// /// 本方法会先生成RSA_PEM再创建RSA:通过全量的PEM字段数据构造一个PEM,除了模数modulus和公钥指数exponent必须提供外,其他私钥指数信息要么全部提供,要么全部不提供(导出的PEM就只包含公钥) /// 注意:所有参数首字节如果是0,必须先去掉 /// - public RSA(byte[] modulus, byte[] exponent, byte[] d, byte[] p, byte[] q, byte[] dp, byte[] dq, byte[] inverseQ) { - rsa = new RSA_PEM(modulus, exponent, d, p, q, dp, dq, inverseQ).GetRSA(); + public RSA_ForWindows(byte[] modulus, byte[] exponent, byte[] d, byte[] p, byte[] q, byte[] dp, byte[] dq, byte[] inverseQ) { + rsa = new RSA_PEM(modulus, exponent, d, p, q, dp, dq, inverseQ).GetRSA_ForWindows(); } } } diff --git a/RSA_PEM.cs b/RSA_PEM.cs index 2768b64..3dc1a78 100644 --- a/RSA_PEM.cs +++ b/RSA_PEM.cs @@ -8,7 +8,7 @@ namespace com.github.xiangyuecn.rsacsharp { /// - /// RSA PEM格式密钥对的解析和导出 + /// RSA PEM格式密钥对的解析和导出,.NET Core、.NET Framework均可用 /// GitHub: https://github.com/xiangyuecn/RSA-csharp /// public class RSA_PEM { @@ -27,23 +27,23 @@ public class RSA_PEM { //以下参数只有私钥才有 https://docs.microsoft.com/zh-cn/dotnet/api/system.security.cryptography.rsaparameters?redirectedfrom=MSDN&view=netframework-4.8 /// - /// prime1 + /// prime1,只有私钥的时候才有 /// public byte[] Val_P; /// - /// prime2 + /// prime2,只有私钥的时候才有 /// public byte[] Val_Q; /// - /// exponent1 + /// exponent1,只有私钥的时候才有 /// public byte[] Val_DP; /// - /// exponent2 + /// exponent2,只有私钥的时候才有 /// public byte[] Val_DQ; /// - /// coefficient + /// coefficient,只有私钥的时候才有 /// public byte[] Val_InverseQ; @@ -52,9 +52,16 @@ private RSA_PEM() { } /// /// 通过RSA中的公钥和私钥构造一个PEM,如果convertToPublic含私钥的RSA将只读取公钥,仅含公钥的RSA不受影响 /// - public RSA_PEM(RSACryptoServiceProvider rsa, bool convertToPublic = false) { - var isPublic = convertToPublic || rsa.PublicOnly; - var param = rsa.ExportParameters(!isPublic); + public RSA_PEM(RSA rsa, bool convertToPublic = false) { + var param = rsa.ExportParameters(false); + if (!convertToPublic) { + if (!(rsa is RSACryptoServiceProvider) || !((RSACryptoServiceProvider)rsa).PublicOnly) { + try { //公钥时,填true可能会抛异常 + param = rsa.ExportParameters(true); + } catch (Exception) { } + } + } + var isPublic = convertToPublic || param.D == null; Key_Modulus = param.Modulus; Key_Exponent = param.Exponent; @@ -76,14 +83,17 @@ public RSA_PEM(RSACryptoServiceProvider rsa, bool convertToPublic = false) { public RSA_PEM(byte[] modulus, byte[] exponent, byte[] d, byte[] p, byte[] q, byte[] dp, byte[] dq, byte[] inverseQ) { Key_Modulus = modulus; Key_Exponent = exponent; - Key_D = BigL(d, modulus.Length); - - int keyLen = modulus.Length / 2; - Val_P = BigL(p, keyLen); - Val_Q = BigL(q, keyLen); - Val_DP = BigL(dp, keyLen); - Val_DQ = BigL(dq, keyLen); - Val_InverseQ = BigL(inverseQ, keyLen); + + if (d != null) { + Key_D = BigL(d, modulus.Length); + + int keyLen = modulus.Length / 2; + Val_P = BigL(p, keyLen); + Val_Q = BigL(q, keyLen); + Val_DP = BigL(dp, keyLen); + Val_DQ = BigL(dq, keyLen); + Val_InverseQ = BigL(inverseQ, keyLen); + } } /// /// 通过公钥指数和私钥指数构造一个PEM,会反推计算出P、Q但和原始生成密钥的P、Q极小可能相同 @@ -141,13 +151,25 @@ public bool HasPrivate { } } /// - /// 将PEM中的公钥私钥转成RSA对象,如果未提供私钥,RSA中就只包含公钥 + /// 将PEM中的公钥私钥转成RSA对象,如果未提供私钥,RSA中就只包含公钥。返回的RSA支持跨平台使用,但只支持在.NET Core环境中使用 /// - public RSACryptoServiceProvider GetRSA() { + public RSA GetRSA_ForCore() { + RSA rsa = RSA.Create(); + setToRSA(rsa); + return rsa; + } + /// + /// 将PEM中的公钥私钥转成RSA对象,如果未提供私钥,RSA中就只包含公钥。.NET Core、.NET Framework均可用,但返回的RSACryptoServiceProvider不支持跨平台,所以只支持在Windows系统中使用 + /// + public RSACryptoServiceProvider GetRSA_ForWindows() { var rsaParams = new CspParameters(); rsaParams.Flags = CspProviderFlags.UseMachineKeyStore; var rsa = new RSACryptoServiceProvider(rsaParams); + setToRSA(rsa); + return rsa; + } + private void setToRSA(RSA rsa) { var param = new RSAParameters(); param.Modulus = Key_Modulus; param.Exponent = Key_Exponent; @@ -160,7 +182,6 @@ public RSACryptoServiceProvider GetRSA() { param.InverseQ = Val_InverseQ; } rsa.ImportParameters(param); - return rsa; } /// /// 转成正整数,如果是负数,需要加前导0转成正整数 diff --git a/vs.csproj b/vs.csproj index 8fff7d8..9f0df61 100644 --- a/vs.csproj +++ b/vs.csproj @@ -47,7 +47,8 @@ - + + From 2fefdfeca7db157668209d82d1fd87af9fd73319 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=AB=98=E5=9D=9A=E6=9E=9C?= <753610399@qq.com> Date: Thu, 9 Jun 2022 13:49:07 +0800 Subject: [PATCH 13/15] =?UTF-8?q?=E5=8F=91=E5=B8=831.5=E7=89=88=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Program.cs | 105 +++++++--------- README.md | 33 +++--- RSA_ForWindows.cs | 217 ---------------------------------- RSA_PEM.cs | 1 + RSA_ForCore.cs => RSA_Util.cs | 156 +++++++++++++++++------- vs.csproj | 3 +- 6 files changed, 177 insertions(+), 338 deletions(-) delete mode 100644 RSA_ForWindows.cs rename RSA_ForCore.cs => RSA_Util.cs (56%) diff --git a/Program.cs b/Program.cs index 52c49ad..1754067 100644 --- a/Program.cs +++ b/Program.cs @@ -6,54 +6,14 @@ namespace com.github.xiangyuecn.rsacsharp { /// GitHub: https://github.com/xiangyuecn/RSA-csharp /// class Program { - static void RSA_ForCoreTest(bool fast) { - Console.WriteLine(hr); - Console.WriteLine(ht + " RSA_ForCoreTest:仅限.NET Core下可测试 " + ht); - Console.WriteLine(hr); - - Console.WriteLine("RSA_ForCore:只支持.NET Core环境,可跨平台使用。如需在.NET Framework中使用,请用RSA_ForWindows(也支持.NET Core,但只能Windows系统中使用)。"); - Console.WriteLine(); - if (!RSA_ForCore.IS_CORE) { - Console.WriteLine("非.NET Core环境,不测试!"); - return; - } - + static void RSATest(bool fast) { //新生成一个RSA密钥,也可以通过已有的pem、xml文本密钥来创建RSA - var rsa = new RSA_ForCore(512); - // var rsa = new RSA_ForCore("pem或xml文本密钥"); - // var rsa = new RSA_ForCore(RSA_PEM.FromPEM("pem文本密钥")); - // var rsa = new RSA_ForCore(RSA_PEM.FromXML("xml文本密钥")); - - ForXxxxxxTest(rsa, fast); - } - static void RSA_ForWindowsTest(bool fast) { - Console.WriteLine(hr); - Console.WriteLine(ht + " RSA_ForWindowsTest:仅限Windows系统可测试 " + ht); - Console.WriteLine(hr); - - Console.WriteLine("RSA_ForWindows:.NET Core、.NET Framework均可用,但由于使用的RSACryptoServiceProvider不支持跨平台,只支持在Windows系统中使用,要跨平台请使用仅支持.NET Core的RSA_ForCore。"); - Console.WriteLine(); - if (!isWindows) { - Console.WriteLine("非Windows系统,不测试!"); - return; - } - - //新生成一个RSA密钥,也可以通过已有的pem、xml文本密钥来创建RSA - var rsa = new RSA_ForWindows(512); - // var rsa = new RSA_ForWindows("pem或xml文本密钥"); - // var rsa = new RSA_ForWindows(RSA_PEM.FromPEM("pem文本密钥")); - // var rsa = new RSA_ForWindows(RSA_PEM.FromXML("xml文本密钥")); - - ForXxxxxxTest(rsa, fast); - } + var rsa = new RSA_Util(512); + // var rsa = new RSA_Util("pem或xml文本密钥"); + // var rsa = new RSA_Util(RSA_PEM.FromPEM("pem文本密钥")); + // var rsa = new RSA_Util(RSA_PEM.FromXML("xml文本密钥")); - - - static void ForXxxxxxTest(dynamic forXxxxxx, bool fast) { - dynamic rsa = forXxxxxx; - //两种rsa都有相同的方法,为什么没有抽象类,因为懒,实际使用应当有明确的类型,就像下面两行,解开注释就能恢复正常 - // RSA_ForWindows rsa = (RSA_ForWindows)forXxxxxx; - // RSA_ForCore rsa = (RSA_ForCore)forXxxxxx; + if (!checkPlatform(rsa)) return; //提取密钥pem字符串 string pem_pkcs1 = rsa.ToPEM().ToPEM_PKCS1(); @@ -61,7 +21,7 @@ static void ForXxxxxxTest(dynamic forXxxxxx, bool fast) { //提取密钥xml字符串 string xml = rsa.ToXML(); - Console.WriteLine("【" + rsa.KeySize + "私钥(XML)】:"); + AssertMsg("【" + rsa.KeySize + "私钥(XML)】:", rsa.KeySize == 512); Console.WriteLine(xml); Console.WriteLine(); Console.WriteLine("【" + rsa.KeySize + "私钥(PKCS#1)】:"); @@ -93,14 +53,14 @@ static void ForXxxxxxTest(dynamic forXxxxxx, bool fast) { Console.WriteLine(); //用pem文本创建RSA - var rsa2 = new RSA_ForWindows(RSA_PEM.FromPEM(pem_pkcs8)); + var rsa2 = new RSA_Util(RSA_PEM.FromPEM(pem_pkcs8)); Console.WriteLine("【用PEM新创建的RSA是否和上面的一致】:"); Assert("XML:", rsa2.ToXML() == rsa.ToXML()); Assert("PKCS1:", rsa2.ToPEM().ToPEM_PKCS1() == rsa.ToPEM().ToPEM_PKCS1()); Assert("PKCS8:", rsa2.ToPEM().ToPEM_PKCS8() == rsa.ToPEM().ToPEM_PKCS8()); //用xml文本创建RSA - var rsa3 = new RSA_ForWindows(RSA_PEM.FromXML(xml)); + var rsa3 = new RSA_Util(RSA_PEM.FromXML(xml)); Console.WriteLine("【用XML新创建的RSA是否和上面的一致】:"); Assert("XML:", rsa3.ToXML() == rsa.ToXML()); Assert("PKCS1:", rsa3.ToPEM().ToPEM_PKCS1() == rsa.ToPEM().ToPEM_PKCS1()); @@ -121,7 +81,7 @@ static void ForXxxxxxTest(dynamic forXxxxxx, bool fast) { } //--------RSA_PEM公钥验证--------- { - var rsaPublic = new RSA_ForWindows(rsa.ToPEM(true)); + var rsaPublic = new RSA_Util(rsa.ToPEM(true)); RSA_PEM pem = rsaPublic.ToPEM(); Console.WriteLine("【RSA_PEM仅公钥是否和原始RSA一致】:"); Console.WriteLine(pem.KeySize + "位"); @@ -132,7 +92,7 @@ static void ForXxxxxxTest(dynamic forXxxxxx, bool fast) { if (!fast) { RSA_PEM pem = rsa.ToPEM(); - var rsa4 = new RSA_ForWindows(new RSA_PEM(pem.Key_Modulus, pem.Key_Exponent, pem.Key_D)); + var rsa4 = new RSA_Util(new RSA_PEM(pem.Key_Modulus, pem.Key_Exponent, pem.Key_D)); Console.WriteLine("【用n、e、d构造解密】"); de = rsa4.DecodeOrNull(en); AssertMsg(de, de == str); @@ -159,28 +119,55 @@ static void AssertMsg(string msg, bool check) { Console.WriteLine(msg); } - static readonly string ht = "◆◆◆◆◆◆◆◆◆◆◆◆"; - static readonly string hr = "---------------------------------------------------------"; - static bool isWindows = true; - static void Main(string[] _) { + static bool checkPlatform(RSA_Util rsa) { + Console.WriteLine(hr); + Console.WriteLine(ht + " " + + (RSA_Util.UseCore == RSA_Util.IS_CORE ? "【默认RSA实现】" : "【强制切换RSA实现类】") + + "当前RSA实现类:" + rsa.RSAObject.GetType().Name + + " " + ht); + Console.WriteLine(hr); + if (RSA_Util.UseCore == RSA_Util.IS_CORE) { + return true; + } + + + var isWindows = true; #if (NETCOREAPP || NETSTANDARD || NET) //https://docs.microsoft.com/zh-cn/dotnet/csharp/language-reference/preprocessor-directives if (!System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Windows)) { isWindows = false; } #endif - //for (var i = 0; i < 1000; i++) { Console.WriteLine("第"+i+"次>>>>>"); RSA_ForCoreTest(true); RSA_ForWindowsTest(true); } + //强制切换了RSA实现类,检查一下是否支持 + if (!rsa.RSAIsUseCore) {// RSACryptoServiceProvider实现类 + if (!isWindows) { + Console.WriteLine("强制切换了RSA实现类,当前使用的RSACryptoServiceProvider不支持跨平台,只支持在Windows系统中使用。"); + Console.WriteLine("非Windows系统,不测试!"); + return false; + } + } + return true; + } + + static readonly string ht = "◆◆◆◆◆◆◆◆◆◆◆◆"; + static readonly string hr = "---------------------------------------------------------"; + static void Main(string[] _) { + long startTime = DateTime.Now.Ticks; + + // for (var i = 0; i < 1000; i++) { Console.WriteLine("第" + i + "次>>>>>"); RSA_Util.UseCore = !RSA_Util.IS_CORE; RSATest(true); RSA_Util.UseCore = RSA_Util.IS_CORE; RSATest(true); } - RSA_ForCoreTest(false); + RSATest(false); Console.WriteLine(hr); Console.WriteLine(); - RSA_ForWindowsTest(false); + // 强制切换一下RSA实现类进行测试 + RSA_Util.UseCore = !RSA_Util.IS_CORE; + RSATest(false); Console.WriteLine(); Console.WriteLine(hr); - Console.WriteLine(ht + " 回车退出... " + ht); + Console.WriteLine(ht + " 耗时:" + (DateTime.Now.Ticks - startTime) / 10000 + "ms 回车退出... " + ht); Console.WriteLine(hr); Console.WriteLine(); Console.ReadLine(); diff --git a/README.md b/README.md index d3a946e..22cc1c6 100644 --- a/README.md +++ b/README.md @@ -6,13 +6,13 @@ 你可以只copy `RSA_PEM.cs` 文件到你的项目中使用,只需这一个文件你就拥有了通过PEM格式密钥创建`RSA`或`RSACryptoServiceProvider`的能力。也可以clone整个项目代码用vs直接打开进行测试。 -底层实现采用PEM文件二进制层面上进行字节码解析,简单轻巧0依赖;附带实现了两个RSA封装操作类(`RSA_ForCore.cs`、`RSA_ForWindows.cs`),和一个测试控制台程序(`Program.cs`)。 +底层实现采用PEM文件二进制层面上进行字节码解析,简单轻巧0依赖;附带实现了一个RSA封装操作类(`RSA_Util.cs`),和一个测试控制台程序(`Program.cs`)。 -源文件|平台支持|功能说明|限制 +源文件|平台支持|功能说明|依赖项 :-:|:-:|:-|:- **RSA_PEM.cs**|.NET Core、.NET Framework|用于解析和导出PEM,创建RSA实例|无 -**RSA_ForWindows.cs**|.NET Core、.NET Framework|RSA操作类,封装了加密、解密、验签|只支持在Windows系统中使用,因为使用的RSACryptoServiceProvider不支持跨平台 -**RSA_ForCore.cs**|.NET Core|RSA操作类,封装了加密、解密、验签|只支持.NET Core环境,可跨平台使用 +**RSA_Util.cs**|.NET Core、.NET Framework|RSA操作类,封装了加密、解密、验签|RSA_PEM +Program.cs|.NET Core、.NET Framework|测试控制台程序|RSA_PEM、RSA_Util **如需功能定制,网站、App、小程序开发等需求,请加本文档下面的QQ群,联系群主(即作者),谢谢~** @@ -42,11 +42,8 @@ var pem=RSA_PEM.FromPEM("-----BEGIN XXX KEY-----..此处意思意思..-----END XXX KEY-----"); //直接创建RSA操作类 -var rsa=new RSA_ForWindows(pem); //这个只能在Windows系统里面运行 -//var rsa=new RSA_ForCore(pem); //这个支持跨平台,但只支持.NET Core - -//var rsa=new RSA_ForWindows(2048); //也可以直接生成新密钥,rsa.ToPEM()得到pem对象 -//var rsa=new RSA_ForCore(2048); +var rsa=new RSA_Util(pem); +//var rsa=new RSA_Util(2048); //也可以直接生成新密钥,rsa.ToPEM()得到pem对象 //加密 var enTxt=rsa.Encode("测试123"); @@ -150,25 +147,27 @@ Framework项目里面需要引入程序集`System.Numerics`用来支持`BigInteg -## 【RSA_ForWindows.cs】【RSA_ForCore.cs】 -这两文件依赖`RSA_PEM.cs`,两个类的方法都是相同的(未做抽象,因为懒,要用那个就直接用哪个),封装了加密、解密、签名、验证、秘钥导入导出操作。 +## 【RSA_Util.cs】 +这个文件依赖`RSA_PEM.cs`,封装了加密、解密、签名、验证、秘钥导入导出操作;.NET Core下由实际的RSA实现类提供支持,.NET Framework下由RSACryptoServiceProvider提供支持。 ### 构造方法 -**RSA_For??(int keySize)**:用指定密钥大小创建一个新的RSA,会生成新密钥,出错抛异常。 +**RSA_Util(int keySize)**:用指定密钥大小创建一个新的RSA,会生成新密钥,出错抛异常。 -**RSA_For??(string pemOrXML)**:通过`PEM格式`或`XML格式`密钥,创建一个RSA,pem或xml内可以只包含一个公钥或私钥,或都包含,出错抛异常。`XML格式`如:`...`。pem支持`PKCS#1`、`PKCS#8`格式,格式如:`-----BEGIN XXX KEY-----....-----END XXX KEY-----`。 +**RSA_Util(string pemOrXML)**:通过`PEM格式`或`XML格式`密钥,创建一个RSA,pem或xml内可以只包含一个公钥或私钥,或都包含,出错抛异常。`XML格式`如:`...`。pem支持`PKCS#1`、`PKCS#8`格式,格式如:`-----BEGIN XXX KEY-----....-----END XXX KEY-----`。 -**RSA_For??(RSA_PEM pem)**:通过一个pem对象创建RSA,pem为公钥或私钥,出错抛异常。 +**RSA_Util(RSA_PEM pem)**:通过一个pem对象创建RSA,pem为公钥或私钥,出错抛异常。 -**RSA_For??(byte[] modulus, byte[] exponent, byte[] d, byte[] p, byte[] q, byte[] dp, byte[] dq, byte[] inverseQ)**:本方法会先生成RSA_PEM再创建RSA。通过全量的PEM字段数据构造一个PEM,除了模数modulus和公钥指数exponent必须提供外,其他私钥指数信息要么全部提供,要么全部不提供(导出的PEM就只包含公钥)注意:所有参数首字节如果是0,必须先去掉。 +**RSA_Util(byte[] modulus, byte[] exponent, byte[] d, byte[] p, byte[] q, byte[] dp, byte[] dq, byte[] inverseQ)**:本方法会先生成RSA_PEM再创建RSA。通过全量的PEM字段数据构造一个PEM,除了模数modulus和公钥指数exponent必须提供外,其他私钥指数信息要么全部提供,要么全部不提供(导出的PEM就只包含公钥)注意:所有参数首字节如果是0,必须先去掉。 -**RSA_For??(byte[] modulus, byte[] exponent, byte[] dOrNull)**:本方法会先生成RSA_PEM再创建RSA。通过公钥指数和私钥指数构造一个PEM,会反推计算出P、Q但和原始生成密钥的P、Q极小可能相同。注意:所有参数首字节如果是0,必须先去掉。出错将会抛出异常。私钥指数可以不提供,导出的PEM就只包含公钥。 +**RSA_Util(byte[] modulus, byte[] exponent, byte[] dOrNull)**:本方法会先生成RSA_PEM再创建RSA。通过公钥指数和私钥指数构造一个PEM,会反推计算出P、Q但和原始生成密钥的P、Q极小可能相同。注意:所有参数首字节如果是0,必须先去掉。出错将会抛出异常。私钥指数可以不提供,导出的PEM就只包含公钥。 ### 实例属性 -`RSA`|`RSACryptoServiceProvider` **RSAObject**:最底层的RSA或RSACryptoServiceProvider对象。 +`RSA` **RSAObject**:最底层的RSA对象,.NET Core下为实际的RSA实现类,.NET Framework下RSACryptoServiceProvider。 + +`bool` **RSAIsUseCore**:最底层的RSA对象是否是使用的rsaCore(RSA),否则将是使用的rsaFramework(RSACryptoServiceProvider)。 `int` **KeySize**:密钥位数。 diff --git a/RSA_ForWindows.cs b/RSA_ForWindows.cs deleted file mode 100644 index 862a288..0000000 --- a/RSA_ForWindows.cs +++ /dev/null @@ -1,217 +0,0 @@ -using System; -using System.IO; -using System.Security.Cryptography; -using System.Text; - -namespace com.github.xiangyuecn.rsacsharp { - /// - /// RSA操作类,.NET Core、.NET Framework均可用,但由于使用的RSACryptoServiceProvider不支持跨平台,只支持在Windows系统中使用,要跨平台请使用仅支持.NET Core的RSA_ForCore - /// GitHub: https://github.com/xiangyuecn/RSA-csharp - /// - public class RSA_ForWindows { - /// - /// 导出XML格式密钥对,如果convertToPublic含私钥的RSA将只返回公钥,仅含公钥的RSA不受影响 - /// - public string ToXML(bool convertToPublic = false) { - return rsa.ToXmlString(!rsa.PublicOnly && !convertToPublic); - } - /// - /// 将密钥对导出成PEM对象,如果convertToPublic含私钥的RSA将只返回公钥,仅含公钥的RSA不受影响 - /// - public RSA_PEM ToPEM(bool convertToPublic = false) { - return new RSA_PEM(rsa, convertToPublic); - } - - - - - /// - /// 加密字符串(utf-8),出错抛异常 - /// - public string Encode(string str) { - return Convert.ToBase64String(Encode(Encoding.UTF8.GetBytes(str))); - } - /// - /// 加密数据,出错抛异常 - /// - public byte[] Encode(byte[] data) { - int blockLen = rsa.KeySize / 8 - 11; - if (data.Length <= blockLen) { - return rsa.Encrypt(data, false); - } - - using (var dataStream = new MemoryStream(data)) - using (var enStream = new MemoryStream()) { - Byte[] buffer = new Byte[blockLen]; - int len = dataStream.Read(buffer, 0, blockLen); - - while (len > 0) { - Byte[] block = new Byte[len]; - Array.Copy(buffer, 0, block, 0, len); - - Byte[] enBlock = rsa.Encrypt(block, false); - enStream.Write(enBlock, 0, enBlock.Length); - - len = dataStream.Read(buffer, 0, blockLen); - } - - return enStream.ToArray(); - } - } - /// - /// 解密字符串(utf-8),解密异常返回null - /// - public string DecodeOrNull(string str) { - if (String.IsNullOrEmpty(str)) { - return null; - } - byte[] byts = null; - try { byts = Convert.FromBase64String(str); } catch { } - if (byts == null) { - return null; - } - var val = DecodeOrNull(byts); - if (val == null) { - return null; - } - return Encoding.UTF8.GetString(val); - } - /// - /// 解密数据,解密异常返回null - /// - public byte[] DecodeOrNull(byte[] data) { - try { - int blockLen = rsa.KeySize / 8; - if (data.Length <= blockLen) { - return rsa.Decrypt(data, false); - } - - using (var dataStream = new MemoryStream(data)) - using (var deStream = new MemoryStream()) { - Byte[] buffer = new Byte[blockLen]; - int len = dataStream.Read(buffer, 0, blockLen); - - while (len > 0) { - Byte[] block = new Byte[len]; - Array.Copy(buffer, 0, block, 0, len); - - Byte[] deBlock = rsa.Decrypt(block, false); - deStream.Write(deBlock, 0, deBlock.Length); - - len = dataStream.Read(buffer, 0, blockLen); - } - - return deStream.ToArray(); - } - } catch { - return null; - } - } - /// - /// 对str进行签名,并指定hash算法(如:SHA256) - /// - public string Sign(string hash, string str) { - return Convert.ToBase64String(Sign(hash, Encoding.UTF8.GetBytes(str))); - } - /// - /// 对data进行签名,并指定hash算法(如:SHA256) - /// - public byte[] Sign(string hash, byte[] data) { - return rsa.SignData(data, hash); - } - /// - /// 验证字符串str的签名是否是sign,并指定hash算法(如:SHA256) - /// - public bool Verify(string hash, string sign, string str) { - byte[] byts = null; - try { byts = Convert.FromBase64String(sign); } catch { } - if (byts == null) { - return false; - } - return Verify(hash, byts, Encoding.UTF8.GetBytes(str)); - } - /// - /// 验证data的签名是否是sign,并指定hash算法(如:SHA256) - /// - public bool Verify(string hash, byte[] sign, byte[] data) { - try { - return rsa.VerifyData(data, hash, sign); - } catch { - return false; - } - } - - - - - private RSACryptoServiceProvider rsa; - /// - /// 最底层的RSACryptoServiceProvider对象 - /// - public RSACryptoServiceProvider RSAObject { - get { - return rsa; - } - } - - /// - /// 密钥位数 - /// - public int KeySize { - get { - return rsa.KeySize; - } - } - /// - /// 是否包含私钥 - /// - public bool HasPrivate { - get { - return !rsa.PublicOnly; - } - } - - /// - /// 用指定密钥大小创建一个新的RSA,会生成新密钥,出错抛异常 - /// - public RSA_ForWindows(int keySize) { - var rsaParams = new CspParameters(); - rsaParams.Flags = CspProviderFlags.UseMachineKeyStore; - rsa = new RSACryptoServiceProvider(keySize, rsaParams); - } - /// - /// 通过指定的pem文件密钥或xml字符串密钥,创建一个RSA,pem或xml内可以只包含一个公钥或私钥,或都包含,出错抛异常 - /// - public RSA_ForWindows(string pemOrXML) { - if (pemOrXML.Trim().StartsWith("<")) { - rsa = RSA_PEM.FromXML(pemOrXML).GetRSA_ForWindows(); - } else { - rsa = RSA_PEM.FromPEM(pemOrXML).GetRSA_ForWindows(); - } - } - /// - /// 通过一个pem对象创建RSA,pem为公钥或私钥,出错抛异常 - /// - public RSA_ForWindows(RSA_PEM pem) { - rsa = pem.GetRSA_ForWindows(); - } - /// - /// 本方法会先生成RSA_PEM再创建RSA:通过公钥指数和私钥指数构造一个PEM,会反推计算出P、Q但和原始生成密钥的P、Q极小可能相同 - /// 注意:所有参数首字节如果是0,必须先去掉 - /// 出错将会抛出异常 - /// - /// 必须提供模数 - /// 必须提供公钥指数 - /// 私钥指数可以不提供,导出的PEM就只包含公钥 - public RSA_ForWindows(byte[] modulus, byte[] exponent, byte[] dOrNull) { - rsa = new RSA_PEM(modulus, exponent, dOrNull).GetRSA_ForWindows(); - } - /// - /// 本方法会先生成RSA_PEM再创建RSA:通过全量的PEM字段数据构造一个PEM,除了模数modulus和公钥指数exponent必须提供外,其他私钥指数信息要么全部提供,要么全部不提供(导出的PEM就只包含公钥) - /// 注意:所有参数首字节如果是0,必须先去掉 - /// - public RSA_ForWindows(byte[] modulus, byte[] exponent, byte[] d, byte[] p, byte[] q, byte[] dp, byte[] dq, byte[] inverseQ) { - rsa = new RSA_PEM(modulus, exponent, d, p, q, dp, dq, inverseQ).GetRSA_ForWindows(); - } - } -} diff --git a/RSA_PEM.cs b/RSA_PEM.cs index 3dc1a78..5ecbc3c 100644 --- a/RSA_PEM.cs +++ b/RSA_PEM.cs @@ -154,6 +154,7 @@ public bool HasPrivate { /// 将PEM中的公钥私钥转成RSA对象,如果未提供私钥,RSA中就只包含公钥。返回的RSA支持跨平台使用,但只支持在.NET Core环境中使用 /// public RSA GetRSA_ForCore() { + RSACryptoServiceProvider.UseMachineKeyStore = true; RSA rsa = RSA.Create(); setToRSA(rsa); return rsa; diff --git a/RSA_ForCore.cs b/RSA_Util.cs similarity index 56% rename from RSA_ForCore.cs rename to RSA_Util.cs index 6b796e6..2145398 100644 --- a/RSA_ForCore.cs +++ b/RSA_Util.cs @@ -5,10 +5,10 @@ namespace com.github.xiangyuecn.rsacsharp { /// - /// RSA操作类,只支持.NET Core环境,可跨平台使用。如需在.NET Framework中使用,请用RSA_ForWindows(也支持.NET Core,但只能Windows系统中使用) + /// RSA操作类,.NET Core、.NET Framework均可用:.NET Core下由实际的RSA实现类提供支持,.NET Framework下由RSACryptoServiceProvider提供支持。 /// GitHub: https://github.com/xiangyuecn/RSA-csharp /// - public class RSA_ForCore { + public class RSA_Util { /// /// 导出XML格式密钥对,如果convertToPublic含私钥的RSA将只返回公钥,仅含公钥的RSA不受影响 /// @@ -19,7 +19,7 @@ public string ToXML(bool convertToPublic = false) { /// 将密钥对导出成PEM对象,如果convertToPublic含私钥的RSA将只返回公钥,仅含公钥的RSA不受影响 /// public RSA_PEM ToPEM(bool convertToPublic = false) { - return new RSA_PEM(rsa, convertToPublic); + return new RSA_PEM(RSAObject, convertToPublic); } @@ -35,21 +35,30 @@ public string Encode(string str) { /// 加密数据,出错抛异常 /// public byte[] Encode(byte[] data) { - int blockLen = rsa.KeySize / 8 - 11; + int blockLen = KeySize / 8 - 11; if (data.Length <= blockLen) { - return rsa.Encrypt(data, RSAEncryptionPadding.Pkcs1); + if (rsaFramework != null) { + return rsaFramework.Encrypt(data, false); + } else { + return rsaCore.Encrypt(data, RSAEncryptionPadding.Pkcs1); + } } using (var dataStream = new MemoryStream(data)) using (var enStream = new MemoryStream()) { - Byte[] buffer = new Byte[blockLen]; + byte[] buffer = new byte[blockLen]; int len = dataStream.Read(buffer, 0, blockLen); while (len > 0) { - Byte[] block = new Byte[len]; + byte[] block = new byte[len]; Array.Copy(buffer, 0, block, 0, len); - Byte[] enBlock = rsa.Encrypt(block, RSAEncryptionPadding.Pkcs1); + byte[] enBlock; + if (rsaFramework != null) { + enBlock = rsaFramework.Encrypt(block, false); + } else { + enBlock = rsaCore.Encrypt(block, RSAEncryptionPadding.Pkcs1); + } enStream.Write(enBlock, 0, enBlock.Length); len = dataStream.Read(buffer, 0, blockLen); @@ -81,21 +90,30 @@ public string DecodeOrNull(string str) { /// public byte[] DecodeOrNull(byte[] data) { try { - int blockLen = rsa.KeySize / 8; + int blockLen = KeySize / 8; if (data.Length <= blockLen) { - return rsa.Decrypt(data, RSAEncryptionPadding.Pkcs1); + if (rsaFramework != null) { + return rsaFramework.Decrypt(data, false); + } else { + return rsaCore.Decrypt(data, RSAEncryptionPadding.Pkcs1); + } } using (var dataStream = new MemoryStream(data)) using (var deStream = new MemoryStream()) { - Byte[] buffer = new Byte[blockLen]; + byte[] buffer = new byte[blockLen]; int len = dataStream.Read(buffer, 0, blockLen); while (len > 0) { - Byte[] block = new Byte[len]; + byte[] block = new byte[len]; Array.Copy(buffer, 0, block, 0, len); - Byte[] deBlock = rsa.Decrypt(block, RSAEncryptionPadding.Pkcs1); + byte[] deBlock; + if (rsaFramework != null) { + deBlock = rsaFramework.Decrypt(block, false); + } else { + deBlock = rsaCore.Decrypt(block, RSAEncryptionPadding.Pkcs1); + } deStream.Write(deBlock, 0, deBlock.Length); len = dataStream.Read(buffer, 0, blockLen); @@ -117,7 +135,11 @@ public string Sign(string hash, string str) { /// 对data进行签名,并指定hash算法(如:SHA256) /// public byte[] Sign(string hash, byte[] data) { - return rsa.SignData(data, new HashAlgorithmName(hash), RSASignaturePadding.Pkcs1); + if (rsaFramework != null) { + return rsaFramework.SignData(data, hash); + } else { + return rsaCore.SignData(data, new HashAlgorithmName(hash), RSASignaturePadding.Pkcs1); + } } /// /// 验证字符串str的签名是否是sign,并指定hash算法(如:SHA256) @@ -135,7 +157,11 @@ public bool Verify(string hash, string sign, string str) { /// public bool Verify(string hash, byte[] sign, byte[] data) { try { - return rsa.VerifyData(data, sign, new HashAlgorithmName(hash), RSASignaturePadding.Pkcs1); + if (rsaFramework != null) { + return rsaFramework.VerifyData(data, hash, sign); + } else { + return rsaCore.VerifyData(data, sign, new HashAlgorithmName(hash), RSASignaturePadding.Pkcs1); + } } catch { return false; } @@ -144,21 +170,17 @@ public bool Verify(string hash, byte[] sign, byte[] data) { - /// - /// 最底层的RSA对象 - /// - public RSA RSAObject { - get { - return rsa; - } - } /// /// 密钥位数 /// public int KeySize { get { - return rsa.KeySize; + if (rsaFramework != null) { + return rsaFramework.KeySize; + } else { + return rsaCore.KeySize; + } } } /// @@ -166,32 +188,45 @@ public int KeySize { /// public bool HasPrivate { get { - return ToPEM().HasPrivate; + if (rsaFramework != null) { + return !rsaFramework.PublicOnly; + } else { + return ToPEM().HasPrivate; + } } } /// /// 用指定密钥大小创建一个新的RSA,会生成新密钥,出错抛异常 /// - public RSA_ForCore(int keySize) { - rsa = RSA.Create(); - rsa.KeySize = keySize; + public RSA_Util(int keySize) { + RSA rsa = null; + if (UseCore) { + rsa = RSA.Create(); + rsa.KeySize = keySize; + } + if (rsa == null || rsa is RSACryptoServiceProvider) { + var rsaParams = new CspParameters(); + rsaParams.Flags = CspProviderFlags.UseMachineKeyStore; + rsa = new RSACryptoServiceProvider(keySize, rsaParams); + } + SetRSA__(rsa); } /// /// 通过指定的pem文件密钥或xml字符串密钥,创建一个RSA,pem或xml内可以只包含一个公钥或私钥,或都包含,出错抛异常 /// - public RSA_ForCore(string pemOrXML) { + public RSA_Util(string pemOrXML) { if (pemOrXML.Trim().StartsWith("<")) { - rsa = RSA_PEM.FromXML(pemOrXML).GetRSA_ForCore(); + SetRSA__(RSA_PEM.FromXML(pemOrXML)); } else { - rsa = RSA_PEM.FromPEM(pemOrXML).GetRSA_ForCore(); + SetRSA__(RSA_PEM.FromPEM(pemOrXML)); } } /// /// 通过一个pem对象创建RSA,pem为公钥或私钥,出错抛异常 /// - public RSA_ForCore(RSA_PEM pem) { - rsa = pem.GetRSA_ForCore(); + public RSA_Util(RSA_PEM pem) { + SetRSA__(pem); } /// /// 本方法会先生成RSA_PEM再创建RSA:通过公钥指数和私钥指数构造一个PEM,会反推计算出P、Q但和原始生成密钥的P、Q极小可能相同 @@ -201,28 +236,63 @@ public RSA_ForCore(RSA_PEM pem) { /// 必须提供模数 /// 必须提供公钥指数 /// 私钥指数可以不提供,导出的PEM就只包含公钥 - public RSA_ForCore(byte[] modulus, byte[] exponent, byte[] dOrNull) { - rsa = new RSA_PEM(modulus, exponent, dOrNull).GetRSA_ForCore(); + public RSA_Util(byte[] modulus, byte[] exponent, byte[] dOrNull) { + SetRSA__(new RSA_PEM(modulus, exponent, dOrNull)); } /// /// 本方法会先生成RSA_PEM再创建RSA:通过全量的PEM字段数据构造一个PEM,除了模数modulus和公钥指数exponent必须提供外,其他私钥指数信息要么全部提供,要么全部不提供(导出的PEM就只包含公钥) /// 注意:所有参数首字节如果是0,必须先去掉 /// - public RSA_ForCore(byte[] modulus, byte[] exponent, byte[] d, byte[] p, byte[] q, byte[] dp, byte[] dq, byte[] inverseQ) { - rsa = new RSA_PEM(modulus, exponent, d, p, q, dp, dq, inverseQ).GetRSA_ForCore(); + public RSA_Util(byte[] modulus, byte[] exponent, byte[] d, byte[] p, byte[] q, byte[] dp, byte[] dq, byte[] inverseQ) { + SetRSA__(new RSA_PEM(modulus, exponent, d, p, q, dp, dq, inverseQ)); } + + private RSACryptoServiceProvider rsaFramework; + /// + /// 最底层的RSA对象,.NET Core下为实际的RSA实现类,.NET Framework下RSACryptoServiceProvider + /// + public RSA RSAObject { + get { + return rsaFramework != null ? rsaFramework : rsaCore; + } + } + /// + /// 最底层的RSA对象是否是使用的rsaCore(RSA),否则将是使用的rsaFramework(RSACryptoServiceProvider) + /// + public bool RSAIsUseCore { + get { + return rsaCore != null; + } + } + private void SetRSA__(RSA_PEM pem) { + if (UseCore) { + SetRSA__(pem.GetRSA_ForCore()); + } else { + SetRSA__(pem.GetRSA_ForWindows()); + } + } + private void SetRSA__(RSA rsa) { + if (rsa is RSACryptoServiceProvider) { + rsaFramework = (RSACryptoServiceProvider)rsa; + } else { + rsaCore = rsa; + } + } + //.NET Framework 兼容编译 #if (NETCOREAPP || NETSTANDARD || NET) //https://docs.microsoft.com/zh-cn/dotnet/csharp/language-reference/preprocessor-directives - private RSA rsa; - static public bool IS_CORE = true; + private RSA rsaCore; + static public readonly bool IS_CORE = true; + static public bool UseCore = true; #else - private dynamic rsa; - static public bool IS_CORE = false; - class RSAEncryptionPadding { static public object Pkcs1; } - class RSASignaturePadding { static public object Pkcs1; } + private dynamic rsaCore = null; + static public readonly bool IS_CORE = false; + static public bool UseCore = false; + class RSAEncryptionPadding { static public object Pkcs1 = null; } + class RSASignaturePadding { static public object Pkcs1 = null; } class HashAlgorithmName { public HashAlgorithmName(string _) { } } #endif } diff --git a/vs.csproj b/vs.csproj index 9f0df61..f7dac32 100644 --- a/vs.csproj +++ b/vs.csproj @@ -47,8 +47,7 @@ - - + From 34b1efccca594a5b71b216bcf34b6496e1a92aa5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=AB=98=E5=9D=9A=E6=9E=9C?= <753610399@qq.com> Date: Sun, 10 Sep 2023 13:39:49 +0800 Subject: [PATCH 14/15] =?UTF-8?q?Release=20Update=20v1.6,=20RSA=5FUtil?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=E5=A2=9E=E5=BC=BA=EF=BC=8C=E6=8F=90=E4=BE=9B?= =?UTF-8?q?=E7=BC=96=E8=AF=91=E8=BF=90=E8=A1=8C=E8=84=9A=E6=9C=AC=EF=BC=8C?= =?UTF-8?q?=E6=8F=90=E4=BE=9B=E4=B8=AD=E8=8B=B1=E5=8F=8C=E8=AF=AD=E6=94=AF?= =?UTF-8?q?=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Program.cs | 1258 +++++++++++++++++++++++++++++++++--- Properties/AssemblyInfo.cs | 33 - README-English.md | 378 +++++++++++ README.md | 274 +++++--- RSA_PEM.cs | 152 +++-- RSA_Util.cs | 956 ++++++++++++++++++++++----- Test-Build-Run.bat | 189 ++++++ Test-Build-Run.sh | 104 +++ images/1-en.png | Bin 0 -> 11352 bytes images/1.png | Bin 64869 -> 12105 bytes images/2.png | Bin 54348 -> 0 bytes scripts/Create-dll.bat | 254 ++++++++ scripts/Create-dll.sh | 105 +++ vs.csproj | 62 -- vs.sln | 22 - 15 files changed, 3291 insertions(+), 496 deletions(-) delete mode 100644 Properties/AssemblyInfo.cs create mode 100644 README-English.md create mode 100644 Test-Build-Run.bat create mode 100644 Test-Build-Run.sh create mode 100644 images/1-en.png delete mode 100644 images/2.png create mode 100644 scripts/Create-dll.bat create mode 100644 scripts/Create-dll.sh delete mode 100644 vs.csproj delete mode 100644 vs.sln diff --git a/Program.cs b/Program.cs index 1754067..92fa6f0 100644 --- a/Program.cs +++ b/Program.cs @@ -1,4 +1,13 @@ using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Security.Cryptography; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading; namespace com.github.xiangyuecn.rsacsharp { /// @@ -6,6 +15,12 @@ namespace com.github.xiangyuecn.rsacsharp { /// GitHub: https://github.com/xiangyuecn/RSA-csharp /// class Program { + static public void Main(string[] args) { + //【请在这里编写你自己的测试代码】 + + ShowMenu(args); + } + static void RSATest(bool fast) { //新生成一个RSA密钥,也可以通过已有的pem、xml文本密钥来创建RSA var rsa = new RSA_Util(512); @@ -13,102 +28,158 @@ static void RSATest(bool fast) { // var rsa = new RSA_Util(RSA_PEM.FromPEM("pem文本密钥")); // var rsa = new RSA_Util(RSA_PEM.FromXML("xml文本密钥")); - if (!checkPlatform(rsa)) return; - + //得到pem对象 + RSA_PEM pem = rsa.ToPEM(false); //提取密钥pem字符串 - string pem_pkcs1 = rsa.ToPEM().ToPEM_PKCS1(); - string pem_pkcs8 = rsa.ToPEM().ToPEM_PKCS8(); + string pem_pkcs1 = pem.ToPEM_PKCS1(); + string pem_pkcs8 = pem.ToPEM_PKCS8(); //提取密钥xml字符串 string xml = rsa.ToXML(); - AssertMsg("【" + rsa.KeySize + "私钥(XML)】:", rsa.KeySize == 512); - Console.WriteLine(xml); - Console.WriteLine(); - Console.WriteLine("【" + rsa.KeySize + "私钥(PKCS#1)】:"); - Console.WriteLine(pem_pkcs1); - Console.WriteLine(); - Console.WriteLine("【" + rsa.KeySize + "公钥(PKCS#8)】:"); - Console.WriteLine(rsa.ToPEM().ToPEM_PKCS8(true)); - Console.WriteLine(); + AssertMsg(T("【" + rsa.KeySize + "私钥(XML)】:", "[ " + rsa.KeySize + " Private Key (XML) ]:"), rsa.KeySize == 512); + S(xml); + S(); + ST("【" + rsa.KeySize + "私钥(PKCS#1)】:", "[ " + rsa.KeySize + " Private Key (PKCS#1) ]:"); + S(pem_pkcs1); + S(); + ST("【" + rsa.KeySize + "公钥(PKCS#8)】:", "[ " + rsa.KeySize + " Public Key (PKCS#8) ]:"); + S(pem.ToPEM_PKCS8(true)); + S(); - var str = "abc内容123"; - var en = rsa.Encode(str); - Console.WriteLine("【加密】:"); - Console.WriteLine(en); + var str = T("abc内容123", "abc123"); + var en = rsa.Encrypt("PKCS1", str); + ST("【加密】:", "[ Encrypt ]:"); + S(en); - Console.WriteLine("【解密】:"); - var de = rsa.DecodeOrNull(en); + ST("【解密】:", "[ Decrypt ]:"); + var de = rsa.Decrypt("PKCS1", en); AssertMsg(de, de == str); if (!fast) { var str2 = str; for (var i = 0; i < 15; i++) str2 += str2; - Console.WriteLine("【长文本加密解密】:"); - AssertMsg(str2.Length + "个字 OK", rsa.DecodeOrNull(rsa.Encode(str2)) == str2); + ST("【长文本加密解密】:", "[ Long text encryption and decryption ]:"); + AssertMsg(str2.Length + T("个字 OK", " characters OK"), rsa.Decrypt("PKCS1", rsa.Encrypt("PKCS1", str2)) == str2); } - Console.WriteLine("【签名SHA1】:"); + ST("【签名SHA1】:", "[ Signature SHA1 ]:"); var sign = rsa.Sign("SHA1", str); Console.WriteLine(sign); - AssertMsg("校验 OK", rsa.Verify("SHA1", sign, str)); + AssertMsg(T("校验 OK", "Verify OK"), rsa.Verify("SHA1", sign, str)); Console.WriteLine(); //用pem文本创建RSA var rsa2 = new RSA_Util(RSA_PEM.FromPEM(pem_pkcs8)); - Console.WriteLine("【用PEM新创建的RSA是否和上面的一致】:"); + ST("【用PEM新创建的RSA是否和上面的一致】:", "[ Is the newly created RSA with PEM consistent with the above ]:"); Assert("XML:", rsa2.ToXML() == rsa.ToXML()); - Assert("PKCS1:", rsa2.ToPEM().ToPEM_PKCS1() == rsa.ToPEM().ToPEM_PKCS1()); - Assert("PKCS8:", rsa2.ToPEM().ToPEM_PKCS8() == rsa.ToPEM().ToPEM_PKCS8()); + Assert("PKCS1:", rsa2.ToPEM().ToPEM_PKCS1() == pem.ToPEM_PKCS1()); + Assert("PKCS8:", rsa2.ToPEM().ToPEM_PKCS8() == pem.ToPEM_PKCS8()); //用xml文本创建RSA var rsa3 = new RSA_Util(RSA_PEM.FromXML(xml)); - Console.WriteLine("【用XML新创建的RSA是否和上面的一致】:"); + ST("【用XML新创建的RSA是否和上面的一致】:", "[ Is the newly created RSA with XML consistent with the above ]:"); Assert("XML:", rsa3.ToXML() == rsa.ToXML()); - Assert("PKCS1:", rsa3.ToPEM().ToPEM_PKCS1() == rsa.ToPEM().ToPEM_PKCS1()); - Assert("PKCS8:", rsa3.ToPEM().ToPEM_PKCS8() == rsa.ToPEM().ToPEM_PKCS8()); + Assert("PKCS1:", rsa3.ToPEM().ToPEM_PKCS1() == pem.ToPEM_PKCS1()); + Assert("PKCS8:", rsa3.ToPEM().ToPEM_PKCS8() == pem.ToPEM_PKCS8()); //--------RSA_PEM私钥验证--------- - { - RSA_PEM pem = rsa.ToPEM(); - Console.WriteLine("【RSA_PEM是否和原始RSA一致】:"); - Console.WriteLine(pem.KeySize + "位"); - Assert("XML:", pem.ToXML(false) == rsa.ToXML()); - Assert("PKCS1:", pem.ToPEM_PKCS1() == rsa.ToPEM().ToPEM_PKCS1()); - Assert("PKCS8:", pem.ToPEM_PKCS8() == rsa.ToPEM().ToPEM_PKCS8()); - Console.WriteLine("仅公钥:"); - Assert("XML:", pem.ToXML(true) == rsa.ToXML(true)); - Assert("PKCS1:", pem.ToPEM_PKCS1(true) == rsa.ToPEM().ToPEM_PKCS1(true)); - Assert("PKCS8:", pem.ToPEM_PKCS8(true) == rsa.ToPEM().ToPEM_PKCS8(true)); - } + //使用PEM全量参数构造pem对象 + RSA_PEM pemX = new RSA_PEM(pem.Key_Modulus, pem.Key_Exponent, pem.Key_D, pem.Val_P, pem.Val_Q, pem.Val_DP, pem.Val_DQ, pem.Val_InverseQ); + ST("【RSA_PEM是否和原始RSA一致】:", "[ Is RSA_PEM consistent with the original RSA ]:"); + S(pemX.KeySize + T("位", " bits")); + Assert("XML:", pemX.ToXML(false) == pem.ToXML(false)); + Assert("PKCS1:", pemX.ToPEM_PKCS1() == pem.ToPEM_PKCS1()); + Assert("PKCS8:", pemX.ToPEM_PKCS8() == pem.ToPEM_PKCS8()); + ST("仅公钥:", "Public Key Only:"); + Assert("XML:", pemX.ToXML(true) == pem.ToXML(true)); + Assert("PKCS1:", pemX.ToPEM_PKCS1(true) == pem.ToPEM_PKCS1(true)); + Assert("PKCS8:", pemX.ToPEM_PKCS8(true) == pem.ToPEM_PKCS8(true)); + //--------RSA_PEM公钥验证--------- - { - var rsaPublic = new RSA_Util(rsa.ToPEM(true)); - RSA_PEM pem = rsaPublic.ToPEM(); - Console.WriteLine("【RSA_PEM仅公钥是否和原始RSA一致】:"); - Console.WriteLine(pem.KeySize + "位"); - Assert("XML:", pem.ToXML(false) == rsa.ToXML(true)); - Assert("PKCS1:", pem.ToPEM_PKCS1() == rsa.ToPEM().ToPEM_PKCS1(true)); - Assert("PKCS8:", pem.ToPEM_PKCS8() == rsa.ToPEM().ToPEM_PKCS8(true)); - } + RSA_PEM pemY = new RSA_PEM(pem.Key_Modulus, pem.Key_Exponent, null); + ST("【RSA_PEM仅公钥是否和原始RSA一致】:", "[ RSA_PEM only public key is consistent with the original RSA ]:"); + S(pemY.KeySize + T("位", " bits")); + Assert("XML:", pemY.ToXML(false) == pem.ToXML(true)); + Assert("PKCS1:", pemY.ToPEM_PKCS1() == pem.ToPEM_PKCS1(true)); + Assert("PKCS8:", pemY.ToPEM_PKCS8() == pem.ToPEM_PKCS8(true)); if (!fast) { - RSA_PEM pem = rsa.ToPEM(); - var rsa4 = new RSA_Util(new RSA_PEM(pem.Key_Modulus, pem.Key_Exponent, pem.Key_D)); - Console.WriteLine("【用n、e、d构造解密】"); - de = rsa4.DecodeOrNull(en); + //使用n、e、d构造pem对象 + RSA_PEM pem4 = new RSA_PEM(pem.Key_Modulus, pem.Key_Exponent, pem.Key_D); + RSA_Util rsa4 = new RSA_Util(pem4); + ST("【用n、e、d构造解密】", "[ Construct decryption with n, e, d ]"); + de = rsa4.Decrypt("PKCS1", en); AssertMsg(de, de == str); - } + AssertMsg(T("校验 OK", "Verify OK"), rsa4.Verify("SHA1", sign, str)); + //对调交换公钥私钥 + ST("【Unsafe|对调公钥私钥,私钥加密公钥解密】", "[ Unsafe | Swap the public key and private key, private key encryption and public key decryption ]"); + rsa4 = rsa.SwapKey_Exponent_D__Unsafe(); + try { + var en4 = rsa4.Encrypt("PKCS1", str); + var sign4 = rsa4.Sign("SHA1", str); + de = rsa4.Decrypt("PKCS1", en4); + AssertMsg(de, de == str); + AssertMsg(T("校验 OK", "Verify OK"), rsa4.Verify("SHA1", sign4, str)); + } catch (Exception e) { + if (!RSA_Util.IS_CoreOr46 && !RSA_Util.IsUseBouncyCastle) { + S(T("不支持在RSACryptoServiceProvider中使用:", "Not supported in RSACryptoServiceProvider: ") + e.Message); + } else { + throw e; + } + } + rsa4 = rsa4.SwapKey_Exponent_D__Unsafe(); + de = rsa4.Decrypt("PKCS1", en); + AssertMsg(de, de == str); + AssertMsg(T("校验 OK", "Verify OK"), rsa4.Verify("SHA1", sign, str)); + } - Console.WriteLine(); - Console.WriteLine(); - Console.WriteLine("【" + rsa.KeySize + "私钥(PKCS#8)】:"); - Console.WriteLine(rsa.ToPEM().ToPEM_PKCS8()); - Console.WriteLine(); - Console.WriteLine("【" + rsa.KeySize + "公钥(PKCS#1)】:不常见的公钥格式"); - Console.WriteLine(rsa.ToPEM().ToPEM_PKCS1(true)); + + if (!fast) { + S(); + ST("【测试一遍所有的加密、解密填充方式】 按回车键继续测试...", "[ Test all the encryption and decryption padding mode ] Press Enter to continue testing..."); + ReadIn(); + RSA_Util rsa5 = new RSA_Util(2048); + testPaddings(false, rsa5, true); + } + } + static Type Type_RuntimeInformation(Type[] outOSPlatform) { +#if (RSA_BUILD__NET_CORE || NETCOREAPP || NETSTANDARD || NET) //csproj:PropertyGroup.DefineConstants + https://docs.microsoft.com/zh-cn/dotnet/csharp/language-reference/preprocessor-directives + if (outOSPlatform != null) outOSPlatform[0] = typeof(OSPlatform); + return typeof(RuntimeInformation); +#else + //.NET Framework 4.7.1 才有 都在mscorlib.dll里面 + var type = typeof(ComVisibleAttribute).Assembly.GetType("System.Runtime.InteropServices.RuntimeInformation"); + if (type != null && outOSPlatform != null) outOSPlatform[0] = type.Assembly.GetType("System.Runtime.InteropServices.OSPlatform"); + return type; +#endif } + static bool NET_IsWindows() { + Type[] typeOSPlatform = new Type[1]; + Type type = Type_RuntimeInformation(typeOSPlatform); + if (type != null) { + dynamic a1 = typeOSPlatform[0].GetProperty("Windows").GetValue(null); + return (bool)RSA_Util.FindFunc(type, "IsOSPlatform", new string[] { "os" }).Invoke(null, new object[] { a1 }); + } + var ver = Environment.OSVersion.VersionString.ToLower(); + return ver.Contains("microsoft") && ver.Contains("windows"); + } + static string NET_Ver() { + string val, os; + Type type = Type_RuntimeInformation(null); + if (type != null) { + val = (string)type.GetProperty("FrameworkDescription").GetValue(null); + os = (string)type.GetProperty("OSDescription").GetValue(null); + } else { + val = "EnvVer-" + Environment.Version; + os = (NET_IsWindows() ? "Windows" : "Linux?"); + } + val += " | " + os; + return val; + } + + static void Assert(string msg, bool check) { @@ -119,58 +190,1045 @@ static void AssertMsg(string msg, bool check) { Console.WriteLine(msg); } - static bool checkPlatform(RSA_Util rsa) { - Console.WriteLine(hr); - Console.WriteLine(ht + " " - + (RSA_Util.UseCore == RSA_Util.IS_CORE ? "【默认RSA实现】" : "【强制切换RSA实现类】") - + "当前RSA实现类:" + rsa.RSAObject.GetType().Name - + " " + ht); - Console.WriteLine(hr); - if (RSA_Util.UseCore == RSA_Util.IS_CORE) { - return true; + + /// 控制台输出一个换行 + static private void S() { + Console.WriteLine(); + } + /// 控制台输出内容 + static private void S(string s) { + Console.WriteLine(s); + } + /// 控制台输出内容 + 简版多语言支持,根据当前语言返回中文或英文,简化调用 + static private void ST(string zh, string en) { + Console.WriteLine(T(zh, en)); + } + /// 简版多语言支持,根据当前语言返回中文或英文,简化调用 + static private string T(string zh, string en) { + return RSA_PEM.T(zh, en); + } + static string ReadIn() { + return Console.ReadLine(); + } + static string ReadPath(string tips, string tips2) { + while (true) { + ST("请输入" + tips + "路径" + tips2 + ": ", "Please enter " + tips + " path" + tips2 + ":"); + Console.Write("> "); + string path = ReadIn().Trim(); + if (path.Length == 0 || path.StartsWith("+")) { + return path; + } + if (!File.Exists(path) && !Directory.Exists(path)) { + ST("文件[" + path + "]不存在", "File [" + path + "] does not exist"); + continue; + } + return path; + } + } + static byte[] ReadFile(string path) { + return File.ReadAllBytes(path); + } + static void WriteFile(string path, byte[] val) { + File.WriteAllBytes(path, val); + } + static readonly string HR = "---------------------------------------------------------"; + + + static private Assembly Bc__Assembly = null; + static private string[] Bc__Dlls = new string[] { + "BouncyCastle.Crypto.dll", "BouncyCastle.Cryptography.dll" + }; + static bool CanLoad_BouncyCastle() { + if (Bc__Assembly != null) return true; + Assembly bc = null; + foreach (var dll in Bc__Dlls) { + try { + bc = Assembly.LoadFrom(dll); + Bc__Assembly = bc; + break; + } catch { } } + return bc != null; + } + static void printEnv() { + S(".NET Version: " + NET_Ver() + " RSA_PEM.Lang=" + RSA_PEM.Lang); + if (RSA_Util.IsUseBouncyCastle) return; + var errs = ""; + if (!RSA_Util.IS_CoreOr46) { + errs += errs.Length > 0 ? T("、", ", ") : ""; + errs += T("除OAEP+SHA1以外的所有OAEP加密填充模式", "All OAEP encryption padding modes except for OAEP+SHA1"); - var isWindows = true; -#if (NETCOREAPP || NETSTANDARD || NET) //https://docs.microsoft.com/zh-cn/dotnet/csharp/language-reference/preprocessor-directives - if (!System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Windows)) { - isWindows = false; + errs += errs.Length > 0 ? T("、", ", ") : ""; + errs += T("PSS签名填充模式(其他填充模式不影响)", "PSS signature padding mode (other padding modes do not affect)"); } -#endif - //强制切换了RSA实现类,检查一下是否支持 - if (!rsa.RSAIsUseCore) {// RSACryptoServiceProvider实现类 - if (!isWindows) { - Console.WriteLine("强制切换了RSA实现类,当前使用的RSACryptoServiceProvider不支持跨平台,只支持在Windows系统中使用。"); - Console.WriteLine("非Windows系统,不测试!"); - return false; + bool _; + if (!SupportHash("SHA-512/256", false, out _)) { + errs += errs.Length > 0 ? T("、", ", ") : ""; + errs += T("SHA-512/224(/256)摘要算法", "SHA-512/224 (/256) digest algorithm"); + } + if (!SupportHash("SHA3-256", false, out _)) { + errs += errs.Length > 0 ? T("、", ", ") : ""; + errs += T("SHA3系列摘要算法", "SHA3 series digest algorithm"); + } + ST("*** .NET不支持NoPadding加密填充模式、不支持SHA-512/224(/256)摘要算法、需要.NET8以上才支持SHA3系列摘要算法,可通过引入BouncyCastle加密增强库来扩充.NET加密功能。", "*** .NET does not support the NoPadding encryption padding mode, does not support the SHA-512/224 (/256) digest algorithm, and requires .NET8 or higher to support the SHA3 series digest algorithm. You can expand the .NET encryption function by introducing the BouncyCastle encryption enhancement library."); + if (errs.Length > 0) { + ST("*** 当前.NET版本太低,不支持:" + errs + ";如需获得这些功能支持,解决办法1:升级使用高版本.NET来运行本测试程序(可能支持);解决办法2:引入BouncyCastle即可得到全部支持。", "*** The current .NET version is too low and does not support: " + errs + "; if you need to obtain support for these functions, solution 1: upgrade to a higher version of .NET to run this test program (may be supported); solution 2: Full support is available with the introduction of BouncyCastle."); + } + ST("*** 如需获得全部加密签名模式支持,可按此方法引入BouncyCastle加密增强库:到 https://www.nuget.org/packages/Portable.BouncyCastle 下载得到NuGet包(或使用 BouncyCastle.Cryptography 包),用压缩软件提取其中lib目录内对应.NET版本下的BouncyCastle.Crypto.dll,放到本测试程序目录内,然后通过测试菜单B进行注册即可得到全部支持。", "*** If you need full encryption and signature mode support, you can introduce BouncyCastle encryption enhancement library in this way: Go to https://www.nuget.org/packages/Portable.BouncyCastle to download the NuGet package (or use the BouncyCastle.Cryptography package), and use compression software to extract it The lib directory corresponds to BouncyCastle.Crypto.dll under the .NET version, place it in the directory of this test program, and then register it through test menu B to get full support."); + } + static bool SupportHash(string hash, bool checkBc, out bool isBc) { + object obj = RSA_Util.HashFromName(hash); + var val = obj != null; + isBc = false; + if (val || !checkBc) { + return val; + } + if (BcAssembly != null) { + try { + obj = BcAssembly.GetType("Org.BouncyCastle.Security.DigestUtilities").GetMethod("GetDigest", new Type[] { typeof(string) }).Invoke(null, new object[] { hash }); + } catch { + obj = null; + } + if (obj != null) { + isBc = true; + return true; } } - return true; + return val; } + static Assembly BcAssembly = null; + static void testProvider(bool checkOpenSSL) { + if (CanLoad_BouncyCastle()) { + if (BcAssembly == null) { + ST("检测到BouncyCastle加密增强库,是否要进行注册?(Y/N) Y", "The BouncyCastle encryption enhancement library is detected, do you want to register? (Y/N) Y"); + } else { + ST("已注册BouncyCastle加密增强库,是否要保持注册?(Y/N) Y", "BouncyCastle encryption enhancement library has been registered, do you want to keep it registered? (Y/N) Y"); + } + Console.Write("> "); + string val = ReadIn().Trim().ToUpper(); + try { + if (BcAssembly == null && "N" != val) { + BcAssembly = Bc__Assembly; + RSA_Util.UseBouncyCastle(BcAssembly); + ST("已注册BouncyCastle加密增强库", "BouncyCastle encryption enhancement library registered"); + } + if (BcAssembly != null && "N" == val) { + RSA_Util.UseBouncyCastle(null); + BcAssembly = null; + ST("已取消注册BouncyCastle加密增强库", "Unregistered BouncyCastle encryption enhancement library"); + } + } catch (Exception e) { + S(T("BouncyCastle操作失败:", "BouncyCastle operation failed: ") + e.Message); + } + } + printEnv(); + S(); - static readonly string ht = "◆◆◆◆◆◆◆◆◆◆◆◆"; - static readonly string hr = "---------------------------------------------------------"; - static void Main(string[] _) { - long startTime = DateTime.Now.Ticks; + RSA_Util rsa = new RSA_Util(2048); + string[] Hashs = new string[] { + "SHA-1","SHA-256","SHA-224","SHA-384","SHA-512" + ,"SHA3-256","SHA3-224","SHA3-384","SHA3-512" + ,"SHA-512/224","SHA-512/256","MD5" + }; - // for (var i = 0; i < 1000; i++) { Console.WriteLine("第" + i + "次>>>>>"); RSA_Util.UseCore = !RSA_Util.IS_CORE; RSATest(true); RSA_Util.UseCore = RSA_Util.IS_CORE; RSATest(true); } + S("MessageDigest" + T("支持情况:", " support status:")); + { + var Ss = new List(Hashs); + Ss.Add("MD2"); + Ss.Add("SHAKE128"); Ss.Add("SHAKE256");//https://blog.csdn.net/weixin_42579622/article/details/111644921 + foreach (var s in Ss) { + var key = s; bool isBc; - RSATest(false); + if (SupportHash(key, true, out isBc)) { + S(" " + key + " | Provider: " + (isBc ? "BouncyCastle" : ".NET")); + } else { + S(" [x] " + key); + } + } + } - Console.WriteLine(hr); - Console.WriteLine(); + S("Encrypt Padding Mode" + T("支持情况:", " support status:")); + for (int i = 0; i < 1; i++) { + var v1 = i == 9999 ? "NONE" : "ECB"; + var Ss = new List(new string[] {"NoPadding" + ,"PKCS1Padding" + ,"OAEPPadding"}); + foreach (var s in Hashs) { + Ss.Add("OAEPwith" + s + "andMGF1Padding"); + } + foreach (var s in Ss) { + string key = "RSA/" + v1 + "/" + s, key2 = key; + RSA_Util.UseBouncyCastle(null); + for (var n = 0; n < 2; n++) { + try { + rsa.Encrypt(key, "123"); + S(" " + key + " | Provider: " + (n == 1 ? "BouncyCastle" : ".NET")); + } catch { + if (n == 0 && BcAssembly != null) { + RSA_Util.UseBouncyCastle(BcAssembly); + continue; + } + S(" [x] " + key); + } + break; + } + RSA_Util.UseBouncyCastle(BcAssembly); + } + } - // 强制切换一下RSA实现类进行测试 - RSA_Util.UseCore = !RSA_Util.IS_CORE; - RSATest(false); - Console.WriteLine(); + S("Signature Padding Mode" + T("支持情况:", " support status:")); + for (int i = 0; i < 3; i++) { + string v2 = i == 1 ? "/PSS" : ""; + string[] Ss = i == 2 ? new string[] { "RSASSA-PSS" } : Hashs; + foreach (var s in Ss) { + string key = i == 2 ? s : (s.Replace("SHA-", "SHA") + "withRSA" + v2), key2 = key; + RSA_Util.UseBouncyCastle(null); + for (var n = 0; n < 2; n++) { + try { + rsa.Sign(key, "123"); + S(" " + key + " | Provider: " + (n == 1 ? "BouncyCastle" : ".NET")); + } catch { + if (n == 0 && BcAssembly != null) { + RSA_Util.UseBouncyCastle(BcAssembly); + continue; + } + S(" [x] " + key); + } + break; + } + RSA_Util.UseBouncyCastle(BcAssembly); + } + } - Console.WriteLine(hr); - Console.WriteLine(ht + " 耗时:" + (DateTime.Now.Ticks - startTime) / 10000 + "ms 回车退出... " + ht); - Console.WriteLine(hr); - Console.WriteLine(); - Console.ReadLine(); + S(HR); + ST("测试一遍所有的加密、解密填充方式:", "Test all the encryption and decryption padding mode:"); + testPaddings(checkOpenSSL, rsa, true); } + /// 测试一遍所有的加密、解密填充方式 + static int testPaddings(bool checkOpenSSL, RSA_Util rsa, bool log) { + int errCount = 0; + var errMsgs = new List(); + var txt = "1234567890"; + if (!checkOpenSSL) { + txt += txt + txt + txt + txt; txt += txt;//100 + txt += txt + txt + txt + txt; txt += txt + "a";//1001 + } + byte[] txtData = Encoding.UTF8.GetBytes(txt); + + if (checkOpenSSL) { + try { + runOpenSSL(rsa, txtData); + } catch (Exception e) { + S(T("运行OpenSSL失败:", "Failed to run OpenSSL: ") + e.Message); + return errCount; + } + } + + var encKeys = RSA_Util.RSAPadding_Enc_DefaultKeys(); + foreach (var type in encKeys) { + var errMsg = ""; + try { + { + byte[] enc = rsa.Encrypt(type, txtData); + byte[] dec = rsa.Decrypt(type, enc); + bool isOk = true; + if (dec.Length != txtData.Length) { + isOk = false; + } else { + for (int i = 0; i < dec.Length; i++) { + if (dec[i] != txtData[i]) { + isOk = false; break; + } + } + } + if (!isOk) { + errMsg = T("解密结果不一致", "Decryption results are inconsistent"); + throw new Exception(errMsg); + } + } + if (checkOpenSSL) { + byte[] enc; + try { + enc = testOpenSSL(true, type); + } catch (Exception e) { + errMsg = "+OpenSSL: " + T("OpenSSL加密出错", "OpenSSL encryption error"); + throw e; + } + byte[] dec = rsa.Decrypt(type, enc); + bool isOk = true; + if (dec.Length != txtData.Length) { + isOk = false; + } else { + for (int i = 0; i < dec.Length; i++) { + if (dec[i] != txtData[i]) { + isOk = false; break; + } + } + } + if (!isOk) { + errMsg = "+OpenSSL: " + T("解密结果不一致", "Decryption results are inconsistent"); + throw new Exception(errMsg); + } + } + if (log) { + S(" " + (checkOpenSSL ? " [+OpenSSL]" : "") + " " + T("加密解密:", "Encryption decryption: ") + type + " | " + RSA_Util.RSAPadding_Enc(type)); + } + } catch (Exception e) { + if (!log && RSA_Util.IsDotNetSupportError(e.Message)) { + //NOOP + } else { + errCount++; + if (errMsg.Length == 0) errMsg = T("加密解密出现异常", "An exception occurred in encryption decryption"); + errMsg = " [x] " + errMsg + ": " + type + " | " + RSA_Util.RSAPadding_Enc(type); + S(errMsg); + errMsgs.Add(errMsg + T("。", ". ") + e.Message); + } + } + } + + var signKeys = RSA_Util.RSAPadding_Sign_DefaultKeys(); + foreach (var type in signKeys) { + var errMsg = ""; + try { + { + byte[] sign = rsa.Sign(type, txtData); + var isOk = rsa.Verify(type, sign, txtData); + if (!isOk) { + errMsg = T("未通过校验", "Failed verification"); + throw new Exception(errMsg); + } + } + if (checkOpenSSL) { + byte[] sign; + try { + sign = testOpenSSL(false, type); + } catch (Exception e) { + errMsg = "+OpenSSL: " + T("OpenSSL签名出错", "OpenSSL signature error"); + throw e; + } + var isOk = rsa.Verify(type, sign, txtData); + if (!isOk) { + errMsg = "+OpenSSL: " + T("未通过校验", "Failed verification"); + throw new Exception(errMsg); + } + } + if (log) { + S(" " + (checkOpenSSL ? " [+OpenSSL]" : "") + " " + T("签名验证:", "Signature verification: ") + type + " | " + RSA_Util.RSAPadding_Sign(type)); + } + } catch (Exception e) { + if (!log && RSA_Util.IsDotNetSupportError(e.Message)) { + //NOOP + } else { + errCount++; + if (errMsg.Length == 0) errMsg = T("签名验证出现异常", "An exception occurred in signature verification"); + errMsg = " [x] " + errMsg + ": " + type + " | " + RSA_Util.RSAPadding_Sign(type); + S(errMsg); + errMsgs.Add(errMsg + T("。", ". ") + e.Message); + } + } + } + if (log) { + if (errMsgs.Count == 0) { + ST("填充方式全部测试通过。", "All padding mode tests passed."); + } else { + ST("按回车键显示详细错误消息...", "Press Enter to display detailed error message..."); + ReadIn(); + } + } + if (errMsgs.Count > 0) { + S(string.Join("\n", errMsgs)); + } + closeOpenSSL(); + return errCount; + } + /// 多线程并发调用同一个RSA + static void threadRun() { + int ThreadCount = Math.Max(5, Environment.ProcessorCount - 1); + bool Abort = false; + int Count = 0; + int ErrCount = 0; + RSA_Util rsa = new RSA_Util(2048); + S(T("正在测试中,线程数:", "Under test, number of threads: ") + ThreadCount + T(",按回车键结束测试...", ", press enter to end the test...")); + + for (int i = 0; i < ThreadCount; i++) { + new Thread(() => { + while (!Abort) { + int err = testPaddings(false, rsa, false); + if (err > 0) { + Interlocked.Add(ref ErrCount, err); + } + Interlocked.Increment(ref Count); + } + }).Start(); + } + + long t1 = DateTime.Now.Ticks; + new Thread(() => { + while (!Abort) { + Console.Write("\r" + T("已测试" + Count + "次,", "Tested " + Count + " times, ") + + ErrCount + T("个错误,", " errors, ") + + T("耗时", "") + (DateTime.Now.Ticks - t1) / 10000 / 1000 + T("秒", " seconds total")); + try { + Thread.Sleep(1000); + } catch { } + } + }).Start(); + + ReadIn(); + Abort = true; + ST("多线程并发调用同一个RSA测试已结束。", "Multiple threads concurrently calling the same RSA test is over."); + S(); + } + + + + static void keyTools() { + ST("===== RSA密钥工具:生成密钥、转换密钥格式 =====" + , "===== RSA key tool: generate key, convert key format ====="); + ST("请使用下面可用命令进行操作,命令[]内的为可选参数,参数可用\"\"包裹。", "Please use the following commands to operate. The parameters in the command `[]` are optional parameters, and the parameters can be wrapped with \"\"."); + S(HR); + S("`new 1024 [-pkcs8] [saveFile [puboutFile]]`: " + T("生成新的RSA密钥,指定位数和格式:xml、pkcs1、或pkcs8(默认),提供saveFile可保存私钥到文件,提供puboutFile可额外保存一个公钥文件", "Generate a new RSA key, specify the number of digits and format: xml, pkcs1, or pkcs8 (default), provide saveFile to save the private key to a file, and provide puboutFile to save an additional public key file")); + S(HR); + S("`convert -pkcs1 [-pubout] [-swap] oldFile [newFile]`: " + T("转换密钥格式,提供已有密钥文件oldFile(支持xml、pem格式公钥或私钥),指定要转换成的格式:xml、pkcs1、或pkcs8,提供了-pubout时只导出公钥,提供了-swap时交换公钥指数私钥指数(非常规的:私钥加密公钥解密),提供newFile可保存到文件", "To convert the key format, provide the existing key file oldFile (support xml, pem format public key or private key), specify the format to be converted into: xml, pkcs1, or pkcs8, only export the public key when -pubout is provided, swap public key exponent and private key exponent when -swap is provided (unconventional: private key encryption and public key decryption), and provide newFile Can save to file")); + S(HR); + S("`exit`: " + T("输入 exit 退出工具", "Enter exit to quit the tool")); + while (true) { + loop: + Console.Write("> "); + var inStr = ReadIn().Trim(); + if (inStr.Length == 0) { + ST("输入为空,请重新输入!如需退出请输入exit", "The input is empty, please re-enter! If you need to exit, please enter exit"); + continue; + } + if (inStr.ToLower() == "exit") { + ST("bye! 已退出。", "bye! has exited."); + S(); + return; + } + var args = new List(); + Regex exp = new Regex("(-?)(?:([^\"\\s]+)|\"(.*?)\")\\s*"); + var sb = exp.Replace(inStr, (m) => { + var m1 = m.Groups[1].Value; + var m2 = m.Groups[2] == null ? "" : m.Groups[2].Value; + if (m2.Length > 0) { + args.Add(m1 + m2); + } else { + args.Add(m1 + m.Groups[3].Value); + } + return ""; + }); + if (sb.Length > 0) { + ST("参数无效:" + sb, "Invalid parameter: " + sb); + continue; + } + + var cmdName = args[0].ToLower(); args.RemoveAt(0); + bool nextSave = false; + RSA_Util rsa = null; string type = "", save = "", save2 = ""; bool pubOut = false; + + if (cmdName == "new") {// 生成新的pem密钥 + type = "pkcs8"; string len = ""; + while (args.Count > 0) { + string param = args[0], p = param.ToLower(); args.RemoveAt(0); + + var m = new Regex("^(\\d+)$").Match(p); + if (m.Success) { len = m.Groups[1].Value; continue; } + + m = new Regex("^-(xml|pkcs1|pkcs8)$").Match(p); + if (m.Success) { type = m.Groups[1].Value; continue; } + + if (save.Length == 0 && !p.StartsWith("-")) { save = param; continue; } + if (save2.Length == 0 && !p.StartsWith("-")) { save2 = param; continue; } + + ST("未知参数:" + param, "Unknown parameter: " + param); + goto loop; + } + if (len.Length == 0) { ST("请提供密钥位数!", "Please provide key digits!"); goto loop; } + try { + rsa = new RSA_Util(Convert.ToInt32(len)); + } catch (Exception e) { + S(T("生成密钥出错:", "Error generating key: ") + e.Message); + goto loop; + } + nextSave = true; + } + + if (cmdName == "convert") {// 转换密钥格式 + string old = ""; bool swap = false; + while (args.Count > 0) { + string param = args[0], p = param.ToLower(); args.RemoveAt(0); + + var m = new Regex("^-(xml|pkcs1|pkcs8)$").Match(p); + if (m.Success) { type = m.Groups[1].Value; continue; } + + if (p == "-pubout") { pubOut = true; continue; } + if (p == "-swap") { swap = true; continue; } + + if (old.Length == 0 && !p.StartsWith("-")) { old = param; continue; } + + if (save.Length == 0 && !p.StartsWith("-")) { save = param; continue; } + + ST("未知参数:" + param, "Unknown parameter: " + param); + goto loop; + } + if (type.Length == 0) { ST("请提供要转换成的格式!", "Please provide the format to convert to!"); goto loop; } + if (old.Length == 0) { ST("请提供已有密钥文件!", "Please provide an existing key file!"); goto loop; } + try { + var oldTxt = Encoding.UTF8.GetString(ReadFile(old)); + rsa = new RSA_Util(oldTxt); + if (swap) rsa = rsa.SwapKey_Exponent_D__Unsafe(); + } catch (Exception e) { + S(T("读取密钥文件出错", "Error reading key file ") + " (" + old + "): " + e.Message); + goto loop; + } + nextSave = true; + } + + while (nextSave) { + string val; + if (type == "xml") { + val = rsa.ToXML(pubOut); + } else { + bool pkcs8 = type == "pkcs8"; + val = rsa.ToPEM(false).ToPEM(pubOut, pkcs8, pkcs8); + } + if (save.Length == 0) { + S(val); + } else { + save = Path.GetFullPath(save); + try { + WriteFile(save, Encoding.UTF8.GetBytes(val)); + } catch (Exception e) { + S(T("保存文件出错", "Error saving file ") + " (" + save + "): " + e.Message); + } + S(T("密钥文件已保存到:", "The key file has been saved to: ") + save); + } + if (save2.Length > 0) { + save = save2; save2 = ""; + pubOut = true; + continue; + } + S(); + goto loop; + } + ST("未知命令:" + cmdName, "Unknown command: " + cmdName); + } + } + + + static RSA_PEM loadKey = null; static string loadKeyFile = ""; + /// 设置:加载密钥PEM文件 + static void setLoadKey() { + string path = ReadPath(T("密钥文件", "Key File") + , T(",或文件夹(内含private.pem、test.txt)。或输入'+1024 pkcs8'生成一个新密钥(填写位数、pkcs1、pkcs8)", ", or a folder (containing private.pem, test.txt). Or enter '+1024 pkcs8' to generate a new key (fill in digits, pkcs1, pkcs8) ")); + if (path.StartsWith("+")) {//创建一个新密钥 + Match m = new Regex("^\\+(\\d+)\\s+pkcs([18])$", RegexOptions.IgnoreCase).Match(path); + if (!m.Success) { + ST("格式不正确,请重新输入!", "The format is incorrect, please re-enter!"); + setLoadKey(); + } else { + int keySize = Convert.ToInt32(m.Groups[1].Value); + RSA_Util rsa = new RSA_Util(keySize); + bool isPkcs8 = m.Groups[2].Value == "8"; + RSA_PEM pem = rsa.ToPEM(false); + S(keySize + T("位私钥已生成,请复制此文本保存到private.pem文件:", " bit private key has been generated. Please copy this text and save it to the private.pem file:")); + S(pem.ToPEM(false, isPkcs8, isPkcs8)); + S(keySize + T("位公钥已生成,请复制此文本保存到public.pem文件:", " bit public key has been generated. Please copy this text and save it to the public.pem file:")); + S(pem.ToPEM(true, isPkcs8, isPkcs8)); + waitAnyKey = true; + } + return; + } + if (path.Length == 0 && loadKeyFile.Length == 0) { + ST("未输入文件,已取消操作", "No file input, operation cancelled"); + return; + } + if (path.Length == 0) { + path = loadKeyFile; + ST("重新加载密钥文件", "Reload key file"); + } + + if (Directory.Exists(path)) { + string txtPath = path + Path.DirectorySeparatorChar + "test.txt"; + path = path + Path.DirectorySeparatorChar + "private.pem"; + if (!File.Exists(path)) { + ST("此文件夹中没有private.pem文件!", "There is no private.pem file in this folder!"); + setLoadKey(); + return; + } + if (File.Exists(txtPath)) {//顺带加载文件夹里面的目标源文件 + loadSrcBytes = ReadFile(txtPath); + loadSrcFile = txtPath; + } + } + string txt = Encoding.UTF8.GetString(ReadFile(path)); + loadKey = RSA_PEM.FromPEM(txt); + loadKeyFile = path; + } + + static byte[] loadSrcBytes = null; static string loadSrcFile = ""; + /// 设置:加载目标源文件 + static void setLoadSrcBytes() { + string path = ReadPath(T("目标源文件", "Target Source File"), ""); + if (path.Length == 0 && loadSrcFile.Length == 0) { + ST("未输入文件,已取消操作", "No file input, operation cancelled"); + return; + } + if (path.Length == 0) { + path = loadSrcFile; + ST("重新加载目标源文件", "Reload target source file"); + } + loadSrcBytes = ReadFile(path); + loadSrcFile = path; + } + + static string encType = ""; + /// 设置加密填充模式 + static bool setEncType() { + S(T("请输入加密填充模式", "Please enter the encryption Padding mode") + + (encType.Length > 0 ? T(",回车使用当前值", ", press Enter to use the current value ") + encType : "") + + T(";填充模式取值可选:", "; Padding mode values: ") + string.Join(", ", RSA_Util.RSAPadding_Enc_DefaultKeys()) + + T(", 或其他支持的值", ", or other supported values")); + Console.Write("> "); + string val = ReadIn().Trim(); + if (val.Length > 0) { + encType = val; + } + if (encType.Length == 0) { + ST("未设置,已取消操作", "Not set, operation canceled"); + } + return encType.Length > 0; + } + /// 加密 + static void execEnc() { + string save = loadSrcFile + ".enc.bin"; + S(T("密钥文件:", "Key file: ") + loadKeyFile); + S(T("目标文件:", "Target file: ") + loadSrcFile); + S(T("填充模式:", "Padding mode: ") + encType + " | " + RSA_Util.RSAPadding_Enc(encType)); + ST("正在加密目标源文件...", "Encrypting target source file..."); + RSA_Util rsa = new RSA_Util(loadKey); + long t1 = DateTime.Now.Ticks; + byte[] data = rsa.Encrypt(encType, loadSrcBytes); + S(T("加密耗时:", "Encryption time: ") + (DateTime.Now.Ticks - t1) / 10000 + "ms"); + WriteFile(save, data); + S(T("已加密,结果已保存:", "Encrypted, the result is saved: ") + save); + } + /// 解密对比 + static void execDec() { + string encPath = loadSrcFile + ".enc.bin"; + S(T("密钥文件:", "Key file: ") + loadKeyFile); + S(T("密文文件:", "Ciphertext file: ") + encPath); + S(T("对比文件:", "Compare files: ") + loadSrcFile); + S(T("填充模式:", "Padding mode: ") + encType + " | " + RSA_Util.RSAPadding_Enc(encType)); + byte[] + data = ReadFile(encPath); + ST("正在解密文件...", "Decrypting file..."); + RSA_Util rsa = new RSA_Util(loadKey); + long t1 = DateTime.Now.Ticks; + byte[] val = rsa.Decrypt(encType, data); + S(T("解密耗时:", "Decryption time: ") + (DateTime.Now.Ticks - t1) / 10000 + "ms"); + WriteFile(loadSrcFile + ".dec.txt", val); + bool isOk = true; + if (val.Length != loadSrcBytes.Length) { + isOk = false; + } else { + for (int i = 0; i < val.Length; i++) { + if (val[i] != loadSrcBytes[i]) { + isOk = false; break; + } + } + } + if (isOk) { + ST("解密成功,和对比文件的内容一致。", "The decryption is successful, which is consistent with the content of the comparison file."); + return; + } + throw new Exception(T("解密结果和对比文件的内容不一致!", "The decryption result is inconsistent with the content of the comparison file!")); + } + + + static string signType = ""; + /// 设置签名hash+填充模式 + static bool setSignType() { + S(T("请输入签名Hash+填充模式", "Please enter the signature Hash+Padding mode") + + (signType.Length > 0 ? T(",回车使用当前值", ", press Enter to use the current value ") + signType : "") + + T(";签名模式取值可选:", "; Signature mode values: ") + string.Join(", ", RSA_Util.RSAPadding_Sign_DefaultKeys()) + + T(", 或其他支持的值", ", or other supported values")); + Console.Write("> "); + string val = ReadIn().Trim(); + if (val.Length > 0) { + signType = val; + } + if (signType.Length == 0) { + ST("未设置,已取消操作", "Not set, operation canceled"); + } + return signType.Length > 0; + } + /// 签名 + static void execSign() { + string save = loadSrcFile + ".sign.bin"; + S(T("密钥文件:", "Key file: ") + loadKeyFile); + S(T("目标文件:", "Target file: ") + loadSrcFile); + S(T("签名模式:", "Signature mode: ") + signType + " | " + RSA_Util.RSAPadding_Sign(signType)); + ST("正在给目标源文件签名...", "Signing target source file..."); + RSA_Util rsa = new RSA_Util(loadKey); + byte[] data = rsa.Sign(signType, loadSrcBytes); + WriteFile(save, data); + S(T("已签名,结果已保存:", "Signed, results saved: ") + save); + } + /// 验证签名 + static void execVerify() { + string binPath = loadSrcFile + ".sign.bin"; + S(T("密钥文件:", "Key file: ") + loadKeyFile); + S(T("目标文件:", "Target file: ") + loadSrcFile); + S(T("签名文件:", "Signature file: ") + binPath); + S(T("签名模式:", "Signature mode: ") + signType + " | " + RSA_Util.RSAPadding_Sign(signType)); + byte[] data = ReadFile(binPath); + ST("正在验证签名...", "Verifying signature..."); + RSA_Util rsa = new RSA_Util(loadKey); + bool val = rsa.Verify(signType, data, loadSrcBytes); + if (val) { + ST("签名验证成功。", "Signature verification successful."); + return; + } + throw new Exception(T("签名验证失败!", "Signature verification failed!")); + } + + + + + + /// 调用openssl相关测试代码 + static void runOpenSSL(RSA_Util rsa, byte[] data) { + var shell = "/bin/bash"; + if (NET_IsWindows()) { + shell = "cmd"; + } + + S(T("正在打开OpenSSL...", "Opening OpenSSL...") + " Shell: " + shell); + closeOpenSSL(); + openSSLProc = new Process(); + ProcessStartInfo info = openSSLProc.StartInfo; + info.FileName = shell; + info.UseShellExecute = false; + info.RedirectStandardError = true; + info.RedirectStandardInput = true; + info.RedirectStandardOutput = true; + info.CreateNoWindow = true; + openSSLProc.Start(); + + openSSLBuffer = new StringBuilder(); + openSSLErrBuffer = new StringBuilder(); + var threadSync = openSSLThreadSync; + openSSLThread1 = new Thread(() => { + try { + while (openSSLThreadSync == threadSync) { + var line = openSSLProc.StandardOutput.ReadLine(); + if (line != null) { + openSSLBuffer.Append(line).Append('\n'); + } + } + } catch { } + }); + openSSLThread2 = new Thread(() => { + try { + while (openSSLThreadSync == threadSync) { + var line = openSSLProc.StandardError.ReadLine(); + if (line != null) { + openSSLErrBuffer.Append(line).Append('\n'); + } + } + } catch { } + }); + openSSLThread1.Start(); + openSSLThread2.Start(); + + WriteFile("test_openssl_key.pem", Encoding.UTF8.GetBytes(rsa.ToPEM(false).ToPEM_PKCS8(false))); + WriteFile("test_openssl_data.txt", data); + + byte[] no = new byte[rsa.KeySize / 8]; + Array.Copy(data, 0, no, no.Length - data.Length, data.Length); + WriteFile("test_openssl_data.txt.nopadding.txt", no); + + openSSLProc.StandardInput.Write("openssl version\necho " + openSSLBoundary + "\n"); + openSSLProc.StandardInput.Flush(); + while (true) { + if (openSSLBuffer.ToString().IndexOf(openSSLBoundary) != -1) { + if (openSSLErrBuffer.Length > 0) { + closeOpenSSL(); + throw new Exception(T("打开OpenSSL出错:", "Error opening OpenSSL: ") + openSSLErrBuffer.ToString().Trim()); + } + S("OpenSSL Version: " + openSSLBuffer.ToString().Trim()); + break; + } + Thread.Sleep(10); + } + } + static private Process openSSLProc; + static private StringBuilder openSSLBuffer, openSSLErrBuffer; + static private Thread openSSLThread1, openSSLThread2; + static private int openSSLThreadSync; + static private readonly string openSSLBoundary = "--openSSL boundary--"; + static void closeOpenSSL() { + openSSLThreadSync++; + if (openSSLProc == null) return; + try { + openSSLProc.Kill(); + openSSLProc.Dispose(); + } catch { } + openSSLProc = null; + } + static byte[] testOpenSSL(bool encOrSign, string mode) { + bool debug = false; string cmd = ""; + string keyFile = "test_openssl_key.pem", txtFile = "test_openssl_data.txt"; + string save = txtFile + (encOrSign ? ".enc.bin" : ".sign.bin"); + if (encOrSign) {//加密 + if (mode == "NO") { + cmd = "openssl pkeyutl -encrypt -pkeyopt rsa_padding_mode:none -in " + txtFile + ".nopadding.txt -inkey " + keyFile + " -out " + save; + } else if (mode == "PKCS1") { + cmd = "openssl pkeyutl -encrypt -pkeyopt rsa_padding_mode:pkcs1 -in " + txtFile + " -inkey " + keyFile + " -out " + save; + } else if (mode.StartsWith("OAEP+")) { + string hash = mode.Replace("OAEP+", "").Replace("-512/", "512-"); + cmd = "openssl pkeyutl -encrypt -pkeyopt rsa_padding_mode:oaep -pkeyopt rsa_oaep_md:" + hash + " -in " + txtFile + " -inkey " + keyFile + " -out " + save; + } + } else {//签名 + if (mode.StartsWith("PKCS1+")) { + string hash = mode.Replace("PKCS1+", "").Replace("-512/", "512-"); + cmd = "openssl dgst -" + hash + " -binary -sign " + keyFile + " -out " + save + " " + txtFile; + } else if (mode.StartsWith("PSS+")) { + string hash = mode.Replace("PSS+", "").Replace("-512/", "512-"); + cmd = "openssl dgst -" + hash + " -binary -out " + txtFile + ".hash " + txtFile; + cmd += "\n"; + cmd += "openssl pkeyutl -sign -pkeyopt digest:" + hash + " -pkeyopt rsa_padding_mode:pss -pkeyopt rsa_pss_saltlen:-1 -in " + txtFile + ".hash -inkey " + keyFile + " -out " + save; + } + } + if (cmd.Length == 0) { + string msg = T("无效mode:", "Invalid mode: ") + mode; + S("[OpenSSL Code Error] " + msg); + throw new Exception(msg); + } + if (File.Exists(save)) { + File.Delete(save); + } + + if (debug) S("[OpenSSL Cmd][" + mode + "]" + cmd); + openSSLBuffer.Length = 0; + openSSLErrBuffer.Length = 0; + openSSLProc.StandardInput.Write(cmd + "\n"); + openSSLProc.StandardInput.Write("echo " + openSSLBoundary + "\n"); + openSSLProc.StandardInput.Flush(); + + while (true) { + if (openSSLBuffer.ToString().IndexOf(openSSLBoundary) != -1) { + if (openSSLErrBuffer.Length > 0) { + if (debug) S("[OpenSSL Error]\n" + openSSLErrBuffer + "\n[End]"); + throw new Exception("OpenSSL Error: " + openSSLErrBuffer.ToString().Trim()); + } + if (debug) S("[OpenSSL Output]\n" + openSSLBuffer + "\n[End] save:" + Path.GetFullPath(save)); + break; + } + Thread.Sleep(10); + } + return ReadFile(save); + } + + static void showOpenSSLTips() { + ST("===== OpenSSL中RSA相关的命令行调用命令 =====" + , "===== RSA-related command-line invocation commands in OpenSSL ====="); + S(); + ST("::先准备一个测试文件 test.txt 里面填少量内容,openssl不支持自动分段加密" + , "::First prepare a test file test.txt and fill in a small amount of content, openssl does not support automatic segmentation encryption"); + S(); + ST("::生成新密钥", "::Generate new key"); + S("openssl genrsa -out private.pem 1024"); + S(); + ST("::提取公钥PKCS#8", "::Extract public key PKCS#8"); + S("openssl rsa -in private.pem -pubout -out public.pem"); + S(); + ST("::转换成RSAPublicKey PKCS#1", "::Convert to RSAPublicKey PKCS#1"); + S("openssl rsa -pubin -in public.pem -RSAPublicKey_out -out public.pem.rsakey"); + ST("::测试RSAPublicKey PKCS#1,不出意外会出错。因为这个公钥里面没有OID,通过RSA_PEM转换成PKCS#8自动带上OID就能正常加密" + , "::Test RSAPublicKey PKCS#1, no accident will go wrong. Because there is no OID in this public key, it can be encrypted normally by converting RSA_PEM into PKCS#8 and automatically bringing OID"); + S("echo abcd123 | openssl rsautl -encrypt -inkey public.pem.rsakey -pubin"); + S(); + S(); + S(); + ST("::加密和解密,填充方式:PKCS1" + , "::Encryption and decryption, padding mode: PKCS1"); + S("openssl pkeyutl -encrypt -pkeyopt rsa_padding_mode:pkcs1 -in test.txt -pubin -inkey public.pem -out test.txt.enc.bin"); + S("openssl pkeyutl -decrypt -pkeyopt rsa_padding_mode:pkcs1 -in test.txt.enc.bin -inkey private.pem -out test.txt.dec.txt"); + S(); + ST("::加密和解密,填充方式:OAEP+SHA256,掩码生成函数MGF1使用相同的hash算法" + , "::Encryption and decryption, padding mode: OAEP+SHA256, mask generation function MGF1 uses the same hash algorithm"); + S("openssl pkeyutl -encrypt -pkeyopt rsa_padding_mode:oaep -pkeyopt rsa_oaep_md:sha256 -in test.txt -pubin -inkey public.pem -out test.txt.enc.bin"); + S("openssl pkeyutl -decrypt -pkeyopt rsa_padding_mode:oaep -pkeyopt rsa_oaep_md:sha256 -in test.txt.enc.bin -inkey private.pem -out test.txt.dec.txt"); + S(); + S(); + ST("::命令行参数中的sha256可以换成md5、sha1等;如需sha3系列,就换成sha3-256即可" + , "::The sha256 in the command line parameters can be replaced by md5, sha1, etc.; if you need the sha3 series, you can replace it with sha3-256"); + S(); + S(); + ST("::签名和验证,填充方式:PKCS1+SHA256", "::Signature and verification, padding mode: PKCS1+SHA256"); + S("openssl dgst -sha256 -binary -sign private.pem -out test.txt.sign.bin test.txt"); + S("openssl dgst -sha256 -binary -verify public.pem -signature test.txt.sign.bin test.txt"); + S(); + ST("::签名和验证,填充方式:PSS+SHA256 ,salt=-1使用hash长度=256/8,掩码生成函数MGF1使用相同的hash算法" + , "::Signature and verification, padding mode: PSS+SHA256, salt=-1 use hash length=256/8, mask generation function MGF1 uses the same hash algorithm"); + S("openssl dgst -sha256 -binary -out test.txt.hash test.txt"); + S("openssl pkeyutl -sign -pkeyopt digest:sha256 -pkeyopt rsa_padding_mode:pss -pkeyopt rsa_pss_saltlen:-1 -in test.txt.hash -inkey private.pem -out test.txt.sign.bin"); + S("openssl pkeyutl -verify -pkeyopt digest:sha256 -pkeyopt rsa_padding_mode:pss -pkeyopt rsa_pss_saltlen:-1 -in test.txt.hash -pubin -inkey public.pem -sigfile test.txt.sign.bin"); + S(); + S(); + } + + + + + static bool waitAnyKey = true; + static void ShowMenu(string[] args) { + if (args != null && args.Length > 0) { + foreach (var v in args) { + if (v.StartsWith("-zh=")) { + RSA_PEM.Lang = v.StartsWith("-zh=1") ? "zh" : "en"; + } + } + S(args.Length + T("个启动参数:", " startup parameters: ") + string.Join(" ", args)); + S(); + } + + bool newRun = true; + while (true) { + if (newRun) { + newRun = false; + S("====== https://github.com/xiangyuecn/RSA-csharp ======"); + printEnv(); + S(HR); + } + + var isSet = loadKeyFile.Length > 0 && loadSrcFile.Length > 0; + var setTips = isSet ? "" : " " + T("[不可用]请先设置4、5", "[Unavailable] Please set 4, 5 first") + " "; + var floadTips = T("[已加载,修改后需重新加载]", "[loaded, need to reload after modification]"); + var fileName = loadSrcFile.Length > 0 ? Path.GetFileName(loadSrcFile) : "test.txt"; + + S(T("【功能菜单】", "[ Menu ]") + " .NET Version: " + NET_Ver()); + S("1. " + T("测试:运行基础功能测试(1次)", "Test: Run basic functional tests (1 time)")); + S("2. " + T("测试:运行基础功能测试(1000次)", "Test: Run basic functional tests (1000 times)")); + S("3. " + T("测试:多线程并发调用同一个RSA", "Test: Multiple threads call the same RSA concurrently")); + S(HR); + S("4. " + T("设置:加载密钥PEM文件", "Setup: Load key PEM file") + (loadKeyFile.Length > 0 ? " " + floadTips + Path.GetFileName(loadKeyFile) + " " + loadKey.KeySize + " bits" : "")); + S("5. " + T("设置:加载目标源文件", "Setup: Load Target Source File") + (loadSrcFile.Length > 0 ? " " + floadTips + fileName + " " + loadSrcBytes.Length + " Bytes" : "")); + S("6. " + T("加密 ", "Encrypt") + setTips + " " + fileName + " -> " + fileName + ".enc.bin"); + S("7. " + T("解密对比", "Decrypt") + setTips + " " + fileName + ".enc.bin -> " + fileName + ".dec.txt"); + S("8. " + T("签名 ", "Sign ") + setTips + " " + fileName + " -> " + fileName + ".sign.bin"); + S("9. " + T("验证签名", "Verify ") + setTips + " " + fileName + ".sign.bin"); + S(HR); + S("A. " + T("RSA密钥工具:生成密钥、转换密钥格式", "RSA key tool: generate key, convert key format")); + S("B. " + T("显示当前环境支持的加密和签名填充模式,输入 B2 可同时对比OpenSSL结果", "Display the encryption and signature padding modes supported by the current environment, enter B2 to compare OpenSSL results at the same time") + + " (" + (CanLoad_BouncyCastle() ? (BcAssembly == null ? + T("可注册BouncyCastle加密增强库", "Can register BouncyCastle encryption enhancement library") + : T("已注册BouncyCastle加密增强库", "BouncyCastle encryption enhancement library registered") + ) : T("未检测到BouncyCastle加密增强库", "BouncyCastle encryption enhancement library was not detected")) + ")"); + S("C. " + T("显示OpenSSL中RSA相关的命令行调用命令", "Display RSA-related command line calls in OpenSSL")); + S("*. " + T("输入 exit 退出,输入 lang=zh|en 切换显示语言", "Enter exit to exit, enter lang=zh|en to switch display language") + + (RSA_Util.IS_CORE ? "" : T(",输入 net45 或 net46 切换高低版本Framework兼容模式", ", enter net45 or net46 to switch between high and low version Framework compatibility mode") + + " (" + T("当前为:", "Currently: ") + (RSA_Util.IS_CoreOr46 ? "net46" : "net45") + ")")); + S(); + ST("请输入菜单序号:", "Please enter the menu number:"); + Console.Write("> "); + + waitAnyKey = true; + while (true) { + var inTxt = ReadIn().Trim().ToUpper(); + + try { + if (inTxt == "1") { + RSATest(false); + } else if (inTxt == "2") { + for (int i = 0; i < 1000; i++) { ST("第" + i + "次>>>>>", i + "th time>>>>>"); RSATest(true); } + } else if (inTxt == "3") { + waitAnyKey = false; + threadRun(); + } else if (inTxt == "4") { + waitAnyKey = false; + setLoadKey(); + } else if (inTxt == "5") { + waitAnyKey = false; + setLoadSrcBytes(); + } else if (isSet && inTxt == "6") { + bool next = setEncType(); + if (next) { + execEnc(); + } + } else if (isSet && inTxt == "7") { + bool next = setEncType(); + if (next) { + execDec(); + } + } else if (isSet && inTxt == "8") { + bool next = setSignType(); + if (next) { + execSign(); + } + } else if (isSet && inTxt == "9") { + bool next = setSignType(); + if (next) { + execVerify(); + } + } else if (inTxt == "A") { + waitAnyKey = false; + keyTools(); + } else if (inTxt == "B" || inTxt == "B2") { + testProvider(inTxt == "B2"); + } else if (inTxt == "C") { + showOpenSSLTips(); + } else if (inTxt.StartsWith("LANG=")) { + waitAnyKey = false; newRun = true; + if (inTxt == "LANG=ZH") { + RSA_PEM.Lang = "zh"; + S("已切换语言成简体中文"); + } else if (inTxt == "LANG=EN") { + RSA_PEM.Lang = "en"; + S("Switched language to English-US"); + } else { + waitAnyKey = true; newRun = false; + ST("语言设置命令无效!", "Invalid language setting command!"); + } + } else if (inTxt == "NET45" || inTxt == "NET46") { + if (RSA_Util.IS_CORE) { + ST(".NET Core下无需进行此配置", "This configuration is not required under .NET Core"); + } else if (inTxt == "NET45") { + RSA_Util.IS_CoreOr46_Test_Set(-1); + ST("已配置使用.NET Framework 4.5及以下版本模式进行测试", "Configured to use .NET Framework 4.5 and below version mode for testing"); + } else { + RSA_Util.IS_CoreOr46_Test_Set(1); + ST("已配置使用.NET Framework 4.6及以上版本模式进行测试", "Configured to use .NET Framework 4.6 and above version mode for testing"); + } + } else if (inTxt == "EXIT") { + S("bye!"); + return; + } else { + inTxt = ""; + ST("序号无效,请重新输入菜单序号!", "The menu number is invalid, please re-enter the menu number!"); + Console.Write("> "); + continue; + } + } catch (Exception e) { + S(e.ToString()); + Thread.Sleep(100); + waitAnyKey = true; + } + break; + } + + if (waitAnyKey) { + ST("按任意键继续...", "Press any key to continue..."); + Console.ReadKey(); + } + S(); + } + } + + } } diff --git a/Properties/AssemblyInfo.cs b/Properties/AssemblyInfo.cs deleted file mode 100644 index 87cc024..0000000 --- a/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// 有关程序集的常规信息通过以下 -// 特性集控制。更改这些特性值可修改 -// 与程序集关联的信息。 -[assembly: AssemblyTitle("https://github.com/xiangyuecn/RSA-csharp")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("RSA-csharp")] -[assembly: AssemblyCopyright("Copyright © xiangyuecn")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// 将 ComVisible 设置为 false 使此程序集中的类型 -// 对 COM 组件不可见。 如果需要从 COM 访问此程序集中的类型, -// 则将该类型上的 ComVisible 特性设置为 true。 -[assembly: ComVisible(true)] - -// 如果此项目向 COM 公开,则下列 GUID 用于类型库的 ID -[assembly: Guid("7661a9da-7f07-403a-8e49-5224ae79a009")] - -// 程序集的版本信息由下面四个值组成: -// -// 主版本 -// 次版本 -// 生成号 -// 修订号 -// -[assembly: AssemblyVersion("1.3.0.0")] -[assembly: AssemblyFileVersion("1.3.0.0")] diff --git a/README-English.md b/README-English.md new file mode 100644 index 0000000..639c757 --- /dev/null +++ b/README-English.md @@ -0,0 +1,378 @@ +# :open_book:RSA-csharp Usage Documentation + +> This document is translated from Chinese to English using Google Translate. + +**Functions of this project: support `PEM` (`PKCS#1`, `PKCS#8`) format RSA key generation, import, and export in `.NET Core` and `.NET Framework` environments; a variety of common RSA encryption signatures padding algorithm support.** + +- Support .NET Framework 4.5+, .NET Standard 2.0+ (.NET Core 2.0+, .NET 5+) +- RSA can be created through `PEM`, `XML` format keys +- RSA can be created by specifying key digits and key parameters +- Can export `PEM`, `XML` format public key, private key; format mutual conversion +- Public key encryption, private key decryption: `NoPadding`, `PKCS1Padding`, `OAEP+MD5`, `OAEP+SHA1 ... SHA3-512` +- Private key signature, public key verification: `PKCS1+SHA1 ... SHA3-512`, `PKCS1+MD5`, `PSS+SHA1 ... SHA3-512` +- Unconventional: private key encryption, public key decryption, public key signature, private key verification +- Multilingual support: provide Chinese and English language support +- There is also a Java version [RSA-java](https://github.com/xiangyuecn/RSA-java), all encrypted signature algorithms are interoperable in `Java`, `.NET`, `OpenSSL` +- The source code is simple, and compile and test `.bat|.sh` scripts are provided. The source code can be modified and run without Visual Studio, and can be used by copying + +[​](?) + +You can just copy the `RSA_PEM.cs` and `RSA_Util.cs` files to your project to use all the functions. You can also clone the entire project code and double-click `Test-Build-Run.bat` to run the test directly (macOS, linux use the terminal to run `.sh`), through the `scripts/Create-dll.bat(sh)` script can generate dll files for project reference. + +The underlying implementation of the `RSA_PEM` class uses bytecode parsing of the PEM file at the binary level, which is simple, lightweight, and zero-dependent; `RSA_Util` is an encapsulation of the RSA operation class, which supports cross-platform use, and can optionally be used with the `BouncyCastle` encryption enhancement library Richer support for cryptographic signature modes is available. + + +Source Files|Platform Support|Instructions|Dependencies +:-:|:-:|:-|:- +**RSA_PEM.cs**|.NET Core, .NET Framework|Used to parse and export PEM, create RSA instance|NONE +**RSA_Util.cs**|.NET Core, .NET Framework|RSA operation class, which encapsulates encryption, decryption, and signature verification|RSA_PEM +Program.cs|.NET Core, .NET Framework|Test console program|RSA_PEM, RSA_Util + +[​](?) + +**Screenshot of Test-Build-Run.bat test compilation and operation:** + +![console test](images/1-en.png) + + +[​](?) + +[​](?) + +## Quick Start: Encryption, Decryption, Signature, Verification + +### Step 1: Reference RSA-csharp +- Method 1: Copy the `RSA_PEM.cs` and `RSA_Util.cs` files directly to your project and use them. +- Method 2: Use the `scripts/Create-dll.bat(sh)` script to generate a dll file, and add the reference of this dll to the project to use it. +- Method 3: Download the corresponding version of the dll file in Releases (that is, the dll generated by the method 2 script), and add the reference of this dll to the project to use it. + +> Note: The .NET Framework project may needs to manually reference the `System.Numerics` assembly to support `BigInteger`. The project created by vs by default does not automatically import this assembly, and the .NET Core project does not need this operation. + +### Step 2: Write the code +``` c# +//Parse pem or xml first, and both public and private keys can be parsed +//var pem=RSA_PEM.FromPEM("-----BEGIN XXX KEY-----....-----END XXX KEY-----"); +//var pem=RSA_PEM.FromXML("...."); + +//Directly create RSA operation classes, which can be created as global objects, and both encryption and decryption signatures support concurrent calls +//var rsa=new RSA_Util(pem); +var rsa=new RSA_Util(2048); //You can also directly generate a new key, rsa.ToPEM() gets the pem object + +//Optionally register the BouncyCastle encryption enhancement library (just register once when the program starts), which is used to implement the encryption signature padding mode not supported by .NET, NuGet: Portable.BouncyCastle or BouncyCastle.Cryptography +//RSA_Util.UseBouncyCastle(typeof(RsaEngine).Assembly); + +//Encrypt with public key, padding mode: PKCS1, you can use OAEP+SHA256 and other padding modes +var enTxt=rsa.Encrypt("PKCS1", "test123"); +//Decrypt with private key +var deTxt=rsa.Decrypt("PKCS1", enTxt); + +//Sign with private key, padding mode: PKCS1+SHA1, PSS+SHA256 and other padding modes can be used +var sign=rsa.Sign("PKCS1+SHA1", "test123"); +//Verify with public key +var isVerify=rsa.Verify("PKCS1+SHA1", sign, "test123"); + +//Export PEM text +var pemTxt=rsa.ToPEM().ToPEM_PKCS8(); + +//Unconventional (unsafe, not recommended): private key encryption, public key decryption, public key signature, private key verification +RSA_Util rsa2=rsa.SwapKey_Exponent_D__Unsafe(); +//... rsa2.Encrypt rsa2.Decrypt rsa2.Sign rsa2.Verify + +Console.WriteLine(pemTxt+"\n"+enTxt+"\n"+deTxt+"\n"+sign+"\n"+isVerify); +Console.ReadLine(); +//****For more examples, please read Program.cs**** +//****For more functional methods, please read the detailed documentation below**** +``` + +**If you need function customization, website, app, small program development, etc., please add the QQ group below and contact the group owner (ie the author), thank you~** + + + +[​](?) + +## [QQ group] communication and support + +Welcome to join QQ group: 421882406, pure lowercase password: `xiangyuecn` + + + + + + + + +[​](?) + +[​](?) + +[​](?) + +[​](?) + +[​](?) + +[​](?) + +# :open_book:Documentation + +## Encryption Paddings + +> In the table below, Frame is the support of .NET Framework, Core is the support of .NET Core, and BC is the support of the BouncyCastle encryption enhancement library (can be registered through the RSA_Util.UseBouncyCastle method); √ means support, × means no support, and other values are A certain version starts to support; among them, the mask generation function MGF1 of OAEP uses the same Hash algorithm as OAEP. + +Padding|Algorithm|Frame|Core|BC +:-|:-|:-:|:-:|:-: +NO|RSA/ECB/NoPadding|×|×|√ +PKCS1 |RSA/ECB/PKCS1Padding|√|√|√ +OAEP+SHA1 |RSA/ECB/OAEPwithSHA-1andMGF1Padding|√|√|√ +OAEP+SHA256|RSA/ECB/OAEPwithSHA-256andMGF1Padding|4.6+|√|√ +OAEP+SHA224|RSA/ECB/OAEPwithSHA-224andMGF1Padding|×|×|√ +OAEP+SHA384|RSA/ECB/OAEPwithSHA-384andMGF1Padding|4.6+|√|√ +OAEP+SHA512|RSA/ECB/OAEPwithSHA-512andMGF1Padding|4.6+|√|√ +OAEP+SHA-512/224|RSA/ECB/OAEPwithSHA-512/224andMGF1Padding|×|×|√ +OAEP+SHA-512/256|RSA/ECB/OAEPwithSHA-512/256andMGF1Padding|×|×|√ +OAEP+SHA3-256|RSA/ECB/OAEPwithSHA3-256andMGF1Padding|×|8+|√ +OAEP+SHA3-224|RSA/ECB/OAEPwithSHA3-224andMGF1Padding|×|×|√ +OAEP+SHA3-384|RSA/ECB/OAEPwithSHA3-384andMGF1Padding|×|8+|√ +OAEP+SHA3-512|RSA/ECB/OAEPwithSHA3-512andMGF1Padding|×|8+|√ +OAEP+MD5 |RSA/ECB/OAEPwithMD5andMGF1Padding|4.6+|√|√ + + + +## Signature Paddings + +> In the table below, Frame is the support of .NET Framework, Core is the support of .NET Core, and BC is the support of the BouncyCastle encryption enhancement library (can be registered through the RSA_Util.UseBouncyCastle method); √ means support, × means no support, and other values are A certain version starts to support; the number of salt bytes of PSS is equal to the number of bytes of the hash algorithm used, the mask generation function MGF1 of PSS uses the same hash algorithm as that of PSS, and the value of the trailing attribute TrailerField is fixed at 0xBC. + +Padding|Algorithm|Frame|Core|BC +:-|:-|:-:|:-:|:-: +SHA1 ... SHA3-512|Same as PKCS1+SHA***||| +PKCS1+SHA1 |SHA1withRSA|√|√|√ +PKCS1+SHA256|SHA256withRSA|√|√|√ +PKCS1+SHA224|SHA224withRSA|×|×|√ +PKCS1+SHA384|SHA384withRSA|√|√|√ +PKCS1+SHA512|SHA512withRSA|√|√|√ +PKCS1+SHA-512/224|SHA512/224withRSA|×|×|√ +PKCS1+SHA-512/256|SHA512/256withRSA|×|×|√ +PKCS1+SHA3-256|SHA3-256withRSA|×|8+|√ +PKCS1+SHA3-224|SHA3-224withRSA|×|×|√ +PKCS1+SHA3-384|SHA3-384withRSA|×|8+|√ +PKCS1+SHA3-512|SHA3-512withRSA|×|8+|√ +PKCS1+MD5 |MD5withRSA|√|√|√ +PSS+SHA1 |SHA1withRSA/PSS|4.6+|√|√ +PSS+SHA256|SHA256withRSA/PSS|4.6+|√|√ +PSS+SHA224|SHA224withRSA/PSS|×|×|√ +PSS+SHA384|SHA384withRSA/PSS|4.6+|√|√ +PSS+SHA512|SHA512withRSA/PSS|4.6+|√|√ +PSS+SHA-512/224|SHA512/224withRSA/PSS|×|×|√ +PSS+SHA-512/256|SHA512/256withRSA/PSS|×|×|√ +PSS+SHA3-256|SHA3-256withRSA/PSS|×|8+|√ +PSS+SHA3-224|SHA3-224withRSA/PSS|×|×|√ +PSS+SHA3-384|SHA3-384withRSA/PSS|×|8+|√ +PSS+SHA3-512|SHA3-512withRSA/PSS|×|8+|√ +PSS+MD5 |MD5withRSA/PSS|4.6+|√|√ + + + +[​](?) + +[​](?) + +## RSA_PEM Class Documentation +The `RSA_PEM.cs` file does not depend on any files, you can directly copy this file to use in your project; through `FromPEM`, `ToPEM` and `FromXML`, `ToXML` two pairs of methods, you can implement PEM `PKCS#1`, `PKCS#8` mutual conversion, PEM, XML mutual conversion. + +Note: `openssl rsa -in privateKey -pubout` exports PKCS#8 format public key (used more), `openssl rsa -pubin -in PKCS#8 publicKey -RSAPublicKey_out` exports PKCS#1 format public key (rarely used). + + +### Static Attributes and Methods + +`RSA_PEM` **FromPEM(string pem)**: Create RSA with PEM format key, support PKCS#1, PKCS#8 format PEM, error will throw an exception. pem format such as: `-----BEGIN XXX KEY-----....-----END XXX KEY-----`. + +`RSA_PEM` **FromXML(string xml)**: Convert the key in XML format to PEM, support public key xml, private key xml, and an exception will be thrown if an error occurs. xml format such as: `....`. + +`string` **T(string zh, string en)**: Simplified multi-language support, returns Chinese or English according to the current language `Lang` value. + +`string` **Lang**: Simplified multi-language support, value: `zh` (Simplified Chinese), `en` (English-US), the default value is based on the system, and the specified language can be assigned. + + +### Construction Methods + +**RSA_PEM(RSA rsa, bool convertToPublic = false)**: Construct a PEM through the public key or private key in RSA. If convertToPublic, the RSA containing the private key will only read the public key, and the RSA containing only the public key will not be affected. + +**RSA_PEM(byte[] modulus, byte[] exponent, byte[] d, byte[] p, byte[] q, byte[] dp, byte[] dq, byte[] inverseQ)**: Construct a PEM through the full amount of PEM field parameters. Except for the modulus and public key exponent that must be provided, other private key exponent information must be provided or not provided at all (the exported PEM only contains the public key) Note: all If the first byte of the parameter is 0, it must be removed first. + +**RSA_PEM(byte[] modulus, byte[] exponent, byte[] dOrNull)**: Construct a PEM through the public key exponent and the private key exponent, and P and Q will be calculated in reverse, but they are extremely unlikely to be the same as the P and Q of the original generated key. Note: If the first byte of all parameters is 0, it must be removed first. Errors will throw exceptions. The private key exponent may not be provided, and the exported PEM only contains the public key. + + +### Instance Attributes + +`byte[]`: **Key_Modulus**(Modulus n, both public key and private key), **Key_Exponent**(Public key exponent e, both public key and private key), **Key_D**(Private key exponent d, only available when private key); These 3 are enough for encryption and decryption. + +`byte[]`: **Val_P**(prime1), **Val_Q**(prime2), **Val_DP**(exponent1), **Val_DQ**(exponent2), **Val_InverseQ**(coefficient); The private key in PEM has more values; these values can be deduced through n, e, and d (only the effective value is deduced, which is different from the original value with high probability). + +`int` **KeySize**: Key digits. + +`bool` **HasPrivate**: Whether to include the private key. + + +### Instance Methods + +`RSA` **GetRSA_ForCore()**: Convert the public key or private key in PEM into an RSA object. If the private key is not provided, RSA only contains the public key. The returned RSA supports cross-platform use, but only supports use in the .NET Core environment. + +`RSACryptoServiceProvider` **GetRSA_ForWindows()**: Convert the public key or private key in PEM into an RSA object. If the private key is not provided, RSA only contains the public key. Both .NET Core and .NET Framework are available, but the returned RSACryptoServiceProvider does not support cross-platform, so it can only be used in Windows systems. + +`void` **GetRSA__ImportParameters(RSA rsa)**: Import the key parameter into the RSA object, this method is called in `GetRSA_ForCore`, `GetRSA_ForWindows`, and can be used to create other types of RSA. + +`RSA_PEM` **CopyToNew(bool convertToPublic = false)**: Copy the key in the current PEM to a new PEM object. convertToPublic: When equal to true, the PEM containing the private key will only return the public key, and the PEM containing only the public key will not be affected. + +`RSA_PEM` **SwapKey_Exponent_D__Unsafe()**: [Unsafe and not recommended] Swap the public key exponent (Key_Exponent) and the private key exponent (Key_D): use the public key as the private key (new.Key_D=this.Key_Exponent) and the private key as the public key (new.Key_Exponent=this.Key_D), returns a new PEM object; for example, used for: private key encryption, public key decryption, this is an unconventional usage. The current object must contain a private key, otherwise an exception will be thrown if it cannot be swapped. Note: It is very insecure to use the public key as a private key, because the public key exponent of most generated keys is 0x10001 (AQAB), which is too easy to guess and cannot be used as a real private key. + +`byte[]` **ToDER(bool convertToPublic, bool privateUsePKCS8, bool publicUsePKCS8)**: Convert the key pair in RSA to DER format. The DER format is binary data before Base64 text encoding in PEM. Refer to the ToPEM method for parameter meanings. + +`string` **ToPEM(bool convertToPublic, bool privateUsePKCS8, bool publicUsePKCS8)**: Convert the key in RSA to PEM format. convertToPublic: When it is equal to true, the RSA containing the private key will only return the public key, and the RSA containing only the public key will not be affected. **privateUsePKCS8**: The return format of the private key, when it is equal to true, it returns the PKCS#8 format (`-----BEGIN PRIVATE KEY-----`), otherwise returns PKCS#1 format (`-----BEGIN RSA PRIVATE KEY-----`), this parameter is invalid when returning a public key; Both formats are used more commonly. **publicUsePKCS8**: The return format of the public key, when it is equal to true, it returns the PKCS#8 format (`-----BEGIN PUBLIC KEY-----`), otherwise returns PKCS#1 format (`-----BEGIN RSA PUBLIC KEY-----`), this parameter is invalid when returning the private key; Generally, the true PKCS#8 format public key is mostly used, and the PKCS#1 format public key seems to be relatively rare. + +`string` **ToPEM_PKCS1(bool convertToPublic=false)**: Simplified writing of the ToPEM method, regardless of the public key or the private key, it returns the PKCS#1 format; it seems that the export of the PKCS#1 public key is less used, and the PKCS#8 public key is used more, and the private key #1#8 is almost. + +`string` **ToPEM_PKCS8(bool convertToPublic=false)**: Simplified writing of the ToPEM method, regardless of whether the public key or the private key returns the PKCS#8 format. + +`string` **ToXML(bool convertToPublic)**: Convert the key in RSA to XML format. If convertToPublic, the RSA containing the private key will only return the public key, and the RSA containing only the public key will not be affected. + + + + +[​](?) + +[​](?) + +## RSA_Util Class Documentation +The `RSA_Util.cs` file depends on `RSA_PEM.cs`, which encapsulates encryption, decryption, signature, verification, and key import and export operations; .NET Core is supported by the actual RSA implementation class, .NET Framework 4.5 and below is supported by RSACryptoServiceProvider, .NET Framework 4.6 and above is supported by RSACng; or the BouncyCastle encryption enhancement library is introduced to provide support. + + +### Static Attributes and Methods + +`string` **RSAPadding_Enc(string padding)**: Convert the encryption padding into the corresponding Algorithm string, such as: `PKCS1 -> RSA/ECB/PKCS1Padding`. + +`string` **RSAPadding_Sign(string hash)**: Convert the signature padding into the corresponding Algorithm string, such as: `PKCS1+SHA1 -> SHA1withRSA`. + +`bool` **IsDotNetSupportError(string errMsg)**: Determine whether the exception message is an error caused by .NET compatibility. + +`void` **UseBouncyCastle(Assembly bouncyCastleAssembly)**: Forces the use of the BouncyCastle cryptographic enhancement library for RSA operations. Just call it once after the program starts, directly call the class in BouncyCastle, pass in the assembly: `UseBouncyCastle(typeof(RsaEngine).Assembly)`, pass in null to cancel the use. The project introduces the BouncyCastle encryption enhancement library to expand the .NET encryption function, NuGet: Portable.BouncyCastle or BouncyCastle.Cryptography, document https://www.bouncycastle.org/csharp/, call this method to register when the program starts to get All encrypted signature padding are supported. + +`bool` **IsUseBouncyCastle**: Whether to force the use of the BouncyCastle encryption enhancement library for RSA operations. When true, .NET RSA will not be used. + +`bool` **IS_CORE**: Whether the current running environment is .NET Core, false is .NET Framework. + + +### Construction Methods + +**RSA_Util(int keySize)**: Create a new RSA with the specified key size, a new key will be generated, and an exception will be thrown if an error occurs. + +**RSA_Util(string pemOrXML)**: Create an RSA with a key in `PEM` or `XML` format, which can be a public key or a private key, and throws an exception if an error occurs. XML format such as: `...`. pem supports `PKCS#1`, `PKCS#8` format, the format is as follows: `-----BEGIN XXX KEY-----....-----END XXX KEY-----`. + +**RSA_Util(RSA_PEM pem)**: Create RSA through a pem object, where pem is a public key or private key, and an exception is thrown if an error occurs. + +**RSA_Util(byte[] modulus, byte[] exponent, byte[] d, byte[] p, byte[] q, byte[] dp, byte[] dq, byte[] inverseQ)**: This method will first generate RSA_PEM and then create RSA. Construct a PEM through the full amount of PEM field data. Except for the modulus and public key exponent, all other private key exponent information must be provided or not provided (the exported PEM only contains the public key). Note: all parameters If the first byte is 0, it must be removed first. + +**RSA_Util(byte[] modulus, byte[] exponent, byte[] dOrNull)**: This method will first generate RSA_PEM and then create RSA. Constructing a PEM through the public key exponent and the private key exponent will calculate P and Q in reverse, but they are extremely unlikely to be the same as the P and Q of the original generated key. Note: If the first byte of all parameters is 0, it must be removed first. Errors will throw exceptions. The private key exponent may not be provided, and the exported PEM only contains the public key. + + +### Instance Attributes + +`RSA` **RSAObject**: Get RSA object, the actual RSA implementation class under .NET Core, RSACryptoServiceProvider under .NET Framework 4.5 and below, RSACng under .NET Framework 4.6 and above; Note: .NET RSA will not be used when IsUseBouncyCastle. + +`bool` **RSAIsUseCore**: Whether the used RSA object is the used .NET Core (RSA), otherwise it will be the used .NET Framework (4.5 and below RSACryptoServiceProvider, 4.6 and above RSACng); Note: When IsUseBouncyCastle will not use .NET RSA. + +`int` **KeySize**: Key digits. + +`bool` **HasPrivate**: Whether to include the private key. + + +### Instance Methods + +`string` **ToXML(bool convertToPublic = false)**: Export the secret key in XML format. If the RSA contains a private key, the private key will be exported by default. When only the public key is set, only the public key will be exported; if the private key is not included, only the public key will be exported. + +`RSA_PEM` **ToPEM(bool convertToPublic = false)**: Export RSA_PEM object (then you can export PEM text by RSA_PEM.ToPEM method), if convertToPublic RSA containing private key will only return public key, RSA containing only public key will not be affected. + +`RSA_Util` **SwapKey_Exponent_D__Unsafe()**: [Unsafe and not recommended] Swap the public key exponent (Key_Exponent) and the private key exponent (Key_D): use the public key as the private key (new.Key_D=this.Key_Exponent) and the private key as the public key (new. Key_Exponent=this.Key_D), returns a new RSA object; for example, used for: private key encryption, public key decryption, this is an unconventional usage. The current object must contain a private key, otherwise an exception will be thrown if it cannot be swapped. Note: It is very insecure to use the public key as a private key, because the public key exponent of most generated keys is 0x10001 (AQAB), which is too easy to guess and cannot be used as a real private key. The swapped key does not support use in RSACryptoServiceProvider (.NET Framework 4.5 and below): `!IS_CoreOr46 && !IsUseBouncyCastle`. + +`string` **Encrypt(string padding, string str)**: Encrypt arbitrary length string (utf-8) returns base64, and an exception is thrown if an error occurs. This method is thread safe. padding specifies the encryption padding, such as: PKCS1, OAEP+SHA256 uppercase, refer to the encryption padding table above, and the default is PKCS1 when using a null value. + +`byte[]` **Encrypt(string padding, byte[] data)**: Encrypt arbitrary length data, and throw an exception if an error occurs. This method is thread safe. + +`string` **Decrypt(string padding, string str)**: Decrypt arbitrary length ciphertext (base64) to get string (utf-8), and throw an exception if an error occurs. This method is thread safe. padding specifies the encryption padding, such as: PKCS1, OAEP+SHA256 uppercase, refer to the encryption padding table above, and the default is PKCS1 when using a null value. + +`byte[]` **Decrypt(string padding, byte[] data)**: Decrypt arbitrary length data, and throw an exception if an error occurs. This method is thread safe. + +`string` **Sign(string hash, string str)**: Sign the string str, return the base64 result, and throw an exception if an error occurs. This method is thread safe. hash specifies the signature digest algorithm and signature padding, such as: SHA256, PSS+SHA1 uppercase, refer to the signature padding table above. + +`byte[]` **Sign(string hash, byte[] data)**: Sign the data, and throw an exception if an error occurs. This method is thread safe. + +`bool` **Verify(string hash, string sign, string str)**: Verify whether the signature of the string str is sign (base64), and throw an exception if an error occurs. This method is thread safe. hash specifies the signature digest algorithm and signature padding, such as: SHA256, PSS+SHA1 uppercase, refer to the signature padding table above. + +`bool` **Verify(string hash, byte[] sign, byte[] data)**: Verify whether the signature of data is sign, and throw an exception if an error occurs. This method is thread safe. + + + + + + +[​](?) + +[​](?) + +## OpenSSL RSA common command line reference +``` bat +::First prepare a test file test.txt and fill in a small amount of content, openssl does not support automatic segmentation encryption + +::Generate new key +openssl genrsa -out private.pem 1024 + +::Extract public key PKCS#8 +openssl rsa -in private.pem -pubout -out public.pem + +::Convert to RSAPublicKey PKCS#1 +openssl rsa -pubin -in public.pem -RSAPublicKey_out -out public.pem.rsakey +::Test RSAPublicKey PKCS#1, no accident will go wrong. Because there is no OID in this public key, it can be encrypted normally by converting RSA_PEM into PKCS#8 and automatically bringing OID +echo abcd123 | openssl rsautl -encrypt -inkey public.pem.rsakey -pubin + + + +::Encryption and decryption, padding mode: PKCS1 +openssl pkeyutl -encrypt -pkeyopt rsa_padding_mode:pkcs1 -in test.txt -pubin -inkey public.pem -out test.txt.enc.bin +openssl pkeyutl -decrypt -pkeyopt rsa_padding_mode:pkcs1 -in test.txt.enc.bin -inkey private.pem -out test.txt.dec.txt + +::Encryption and decryption, padding mode: OAEP+SHA256, mask generation function MGF1 uses the same hash algorithm +openssl pkeyutl -encrypt -pkeyopt rsa_padding_mode:oaep -pkeyopt rsa_oaep_md:sha256 -in test.txt -pubin -inkey public.pem -out test.txt.enc.bin +openssl pkeyutl -decrypt -pkeyopt rsa_padding_mode:oaep -pkeyopt rsa_oaep_md:sha256 -in test.txt.enc.bin -inkey private.pem -out test.txt.dec.txt + + +::The sha256 in the command line parameters can be replaced by md5, sha1, etc.; if you need the sha3 series, you can replace it with sha3-256 + + +::Signature and verification, padding mode: PKCS1+SHA256 +openssl dgst -sha256 -binary -sign private.pem -out test.txt.sign.bin test.txt +openssl dgst -sha256 -binary -verify public.pem -signature test.txt.sign.bin test.txt + +::Signature and verification, padding mode: PSS+SHA256, salt=-1 use hash length=256/8, mask generation function MGF1 uses the same hash algorithm +openssl dgst -sha256 -binary -out test.txt.hash test.txt +openssl pkeyutl -sign -pkeyopt digest:sha256 -pkeyopt rsa_padding_mode:pss -pkeyopt rsa_pss_saltlen:-1 -in test.txt.hash -inkey private.pem -out test.txt.sign.bin +openssl pkeyutl -verify -pkeyopt digest:sha256 -pkeyopt rsa_padding_mode:pss -pkeyopt rsa_pss_saltlen:-1 -in test.txt.hash -pubin -inkey public.pem -sigfile test.txt.sign.bin +``` + + + + + + + +[​](?) + +[​](?) + +[​](?) + +# :star:Donate +If this library is helpful to you, please star it. + +You can also use Alipay or WeChat to donate to the author: + +![](https://gitee.com/xiangyuecn/Recorder/raw/master/assets/donate-alipay.png) ![](https://gitee.com/xiangyuecn/Recorder/raw/master/assets/donate-weixin.png) + diff --git a/README.md b/README.md index 22cc1c6..f277192 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,26 @@ **【[源GitHub仓库](https://github.com/xiangyuecn/RSA-csharp)】 | 【[Gitee镜像库](https://gitee.com/xiangyuecn/RSA-csharp)】如果本文档图片没有显示,请手动切换到Gitee镜像库阅读文档。** -# :open_book:RSA-csharp的帮助文档 +# :open_book:RSA-csharp使用文档 ( [English Documentation](README-English.md) ) -本项目核心功能:支持`.NET Core`、`.NET Framework`环境下`PEM`(`PKCS#1`、`PKCS#8`)格式RSA密钥对导入、导出。 +**本项目核心功能:支持`.NET Core`、`.NET Framework`环境下`PEM`(`PKCS#1`、`PKCS#8`)格式RSA密钥生成、导入、导出,多种常见RSA加密、签名填充算法支持。** -你可以只copy `RSA_PEM.cs` 文件到你的项目中使用,只需这一个文件你就拥有了通过PEM格式密钥创建`RSA`或`RSACryptoServiceProvider`的能力。也可以clone整个项目代码用vs直接打开进行测试。 +- 支持.NET Framework 4.5+、.NET Standard 2.0+(.NET Core 2.0+、.NET 5+) +- 可通过`PEM`、`XML`格式密钥创建RSA +- 可通过指定密钥位数、密钥参数创建RSA +- 可导出`PEM`、`XML`格式公钥、私钥,格式相互转换 +- 公钥加密、私钥解密:`NoPadding`、`PKCS1Padding`、`OAEP+MD5`、`OAEP+SHA1 ... SHA3-512` +- 私钥签名、公钥验证:`PKCS1+SHA1 ... SHA3-512`、`PKCS1+MD5`、`PSS+SHA1 ... SHA3-512` +- 非常规的:私钥加密、公钥解密,公钥签名、私钥验证 +- 多语言支持:提供中文、英文两种语言支持 +- 另有Java版 [RSA-java](https://github.com/xiangyuecn/RSA-java),所有加密签名算法在`Java`、`.NET`、`OpenSSL`中均可互通 +- 源码简单,提供编译测试`.bat|.sh`脚本,无需Visual Studio即可修改和运行,copy即用 + +[​](?) + +你可以只copy `RSA_PEM.cs`、`RSA_Util.cs` 文件到你的项目中使用,即可使用上所有的功能。也可以clone整个项目代码双击 `Test-Build-Run.bat` 即可直接运行测试(macOS、linux用终端运行`.sh`的),通过`scripts/Create-dll.bat(sh)`脚本可生成dll文件供项目引用。 + +`RSA_PEM`类底层实现采用PEM文件二进制层面上进行字节码解析,简单轻巧0依赖;`RSA_Util`为封装RSA操作类,支持跨平台使用,可选搭配使用`BouncyCastle`加密增强库可获得更丰富的加密签名模式支持。 -底层实现采用PEM文件二进制层面上进行字节码解析,简单轻巧0依赖;附带实现了一个RSA封装操作类(`RSA_Util.cs`),和一个测试控制台程序(`Program.cs`)。 源文件|平台支持|功能说明|依赖项 :-:|:-:|:-|:- @@ -14,58 +28,65 @@ **RSA_Util.cs**|.NET Core、.NET Framework|RSA操作类,封装了加密、解密、验签|RSA_PEM Program.cs|.NET Core、.NET Framework|测试控制台程序|RSA_PEM、RSA_Util -**如需功能定制,网站、App、小程序开发等需求,请加本文档下面的QQ群,联系群主(即作者),谢谢~** +[​](?) -【Java版】:[RSA-java](https://github.com/xiangyuecn/RSA-java) +**Test-Build-Run.bat 测试编译运行截图:** + +![控制台测试](images/1.png) [​](?) -## 特性 +[​](?) -- 通过`XML格式`密钥对创建RSA -- 通过`PEM格式`密钥对创建RSA -- 通过指定密钥位数创建RSA(生成公钥、私钥) -- RSA加密、解密 -- RSA签名、验证 -- 导出`XML格式`公钥、私钥 -- 导出`PEM格式`公钥、私钥 -- `PEM格式`秘钥对和`XML格式`秘钥对互转 +## 快速使用:加密、解密、签名、校验 +### 步骤一:引入RSA-csharp +- 方法1:直接复制 `RSA_PEM.cs`、`RSA_Util.cs` 文件到你的项目中使用。 +- 方法2:使用`scripts/Create-dll.bat(sh)`脚本生成dll文件,项目添加这个dll的引用即可使用。 +- 方法3:下载Releases中对应版本的dll文件(就是方法2脚本生成的dll),项目添加这个dll的引用即可使用。 -[​](?) +> 注意:.NET Framework项目可能需要手动引入`System.Numerics`程序集来支持`BigInteger`,vs默认创建的项目没有自动引入此程序集,.NET Core项目不需要此操作。 -## 如何加密、解密、签名、校验 -得到了RSA_PEM后,加密解密就异常简单了,没那么多啰嗦难懂的代码: -``` c# -//先解析pem,公钥私钥都行,如果是xml就用 RSA_PEM.FromXML("....") -var pem=RSA_PEM.FromPEM("-----BEGIN XXX KEY-----..此处意思意思..-----END XXX KEY-----"); -//直接创建RSA操作类 -var rsa=new RSA_Util(pem); -//var rsa=new RSA_Util(2048); //也可以直接生成新密钥,rsa.ToPEM()得到pem对象 +### 步骤二:编写代码 +``` c# +//先解析pem或xml,公钥私钥均可解析 +//var pem=RSA_PEM.FromPEM("-----BEGIN XXX KEY-----....-----END XXX KEY-----"); +//var pem=RSA_PEM.FromXML("...."); -//加密 -var enTxt=rsa.Encode("测试123"); +//直接创建RSA操作类,可创建成全局对象,加密解密签名均支持并发调用 +//var rsa=new RSA_Util(pem); +var rsa=new RSA_Util(2048); //也可以直接生成新密钥,rsa.ToPEM()得到pem对象 -//解密 -var deTxt=rsa.DecodeOrNull(enTxt); +//可选注册BouncyCastle加密增强库(程序启动时注册一次即可),用来实现.NET不支持的加密签名填充方式,NuGet:Portable.BouncyCastle或BouncyCastle.Cryptography +//RSA_Util.UseBouncyCastle(typeof(RsaEngine).Assembly); -//签名 -var sign=rsa.Sign("SHA1", "测试123"); +//公钥加密,填充方式:PKCS1,可以使用 OAEP+SHA256 等填充方式 +var enTxt=rsa.Encrypt("PKCS1", "测试123"); +//私钥解密 +var deTxt=rsa.Decrypt("PKCS1", enTxt); -//校验签名 -var isVerify=rsa.Verify("SHA1", sign, "测试123"); +//私钥签名,填充方式:PKCS1+SHA1,可以使用 PSS+SHA256 等填充方式 +var sign=rsa.Sign("PKCS1+SHA1", "测试123"); +//公钥校验签名 +var isVerify=rsa.Verify("PKCS1+SHA1", sign, "测试123"); //导出pem文本 var pemTxt=rsa.ToPEM().ToPEM_PKCS8(); +//非常规的(不安全、不建议使用):私钥加密、公钥解密,公钥签名、私钥验证 +RSA_Util rsa2=rsa.SwapKey_Exponent_D__Unsafe(); +//... rsa2.Encrypt rsa2.Decrypt rsa2.Sign rsa2.Verify + Console.WriteLine(pemTxt+"\n"+enTxt+"\n"+deTxt+"\n"+sign+"\n"+isVerify); Console.ReadLine(); //****更多的实例,请阅读 Program.cs**** //****更多功能方法,请阅读下面的详细文档**** ``` +**如需功能定制,网站、App、小程序开发等需求,请加下面的QQ群,联系群主(即作者),谢谢~** + [​](?) @@ -95,20 +116,83 @@ Console.ReadLine(); # :open_book:文档 -## 【RSA_PEM.cs】 -此文件不依赖任何文件,可以直接copy这个文件到你项目中用;通过`FromPEM`、`ToPEM` 和`FromXML`、`ToXML`这两对方法,可以实现PEM`PKCS#1`、`PKCS#8`相互转换,PEM、XML的相互转换。 +## 加密填充方式 + +> 下表中Frame为.NET Framework支持情况,Core为.NET Core的支持情况,BC为BouncyCastle加密增强库支持情况(可通过RSA_Util.UseBouncyCastle方法注册);√为支持,×为不支持,其他值为某版本开始支持;其中OAEP的掩码生成函数MGF1使用和OAEP相同的Hash算法。 + +加密填充方式|Algorithm|Frame|Core|BC +:-|:-|:-:|:-:|:-: +NO|RSA/ECB/NoPadding|×|×|√ +PKCS1 |RSA/ECB/PKCS1Padding|√|√|√ +OAEP+SHA1 |RSA/ECB/OAEPwithSHA-1andMGF1Padding|√|√|√ +OAEP+SHA256|RSA/ECB/OAEPwithSHA-256andMGF1Padding|4.6+|√|√ +OAEP+SHA224|RSA/ECB/OAEPwithSHA-224andMGF1Padding|×|×|√ +OAEP+SHA384|RSA/ECB/OAEPwithSHA-384andMGF1Padding|4.6+|√|√ +OAEP+SHA512|RSA/ECB/OAEPwithSHA-512andMGF1Padding|4.6+|√|√ +OAEP+SHA-512/224|RSA/ECB/OAEPwithSHA-512/224andMGF1Padding|×|×|√ +OAEP+SHA-512/256|RSA/ECB/OAEPwithSHA-512/256andMGF1Padding|×|×|√ +OAEP+SHA3-256|RSA/ECB/OAEPwithSHA3-256andMGF1Padding|×|8+|√ +OAEP+SHA3-224|RSA/ECB/OAEPwithSHA3-224andMGF1Padding|×|×|√ +OAEP+SHA3-384|RSA/ECB/OAEPwithSHA3-384andMGF1Padding|×|8+|√ +OAEP+SHA3-512|RSA/ECB/OAEPwithSHA3-512andMGF1Padding|×|8+|√ +OAEP+MD5 |RSA/ECB/OAEPwithMD5andMGF1Padding|4.6+|√|√ + + + +## 签名填充方式 + +> 下表中Frame为.NET Framework支持情况,Core为.NET Core的支持情况,BC为BouncyCastle加密增强库支持情况(可通过RSA_Util.UseBouncyCastle方法注册);√为支持,×为不支持,其他值为某版本开始支持;其中PSS的salt字节数等于使用的Hash算法字节数,PSS的掩码生成函数MGF1使用和PSS相同的Hash算法,跟踪属性TrailerField取值固定为0xBC。 + +签名填充方式|Algorithm|Frame|Core|BC +:-|:-|:-:|:-:|:-: +SHA1 ... SHA3-512|等同于PKCS1+SHA***||| +PKCS1+SHA1 |SHA1withRSA|√|√|√ +PKCS1+SHA256|SHA256withRSA|√|√|√ +PKCS1+SHA224|SHA224withRSA|×|×|√ +PKCS1+SHA384|SHA384withRSA|√|√|√ +PKCS1+SHA512|SHA512withRSA|√|√|√ +PKCS1+SHA-512/224|SHA512/224withRSA|×|×|√ +PKCS1+SHA-512/256|SHA512/256withRSA|×|×|√ +PKCS1+SHA3-256|SHA3-256withRSA|×|8+|√ +PKCS1+SHA3-224|SHA3-224withRSA|×|×|√ +PKCS1+SHA3-384|SHA3-384withRSA|×|8+|√ +PKCS1+SHA3-512|SHA3-512withRSA|×|8+|√ +PKCS1+MD5 |MD5withRSA|√|√|√ +PSS+SHA1 |SHA1withRSA/PSS|4.6+|√|√ +PSS+SHA256|SHA256withRSA/PSS|4.6+|√|√ +PSS+SHA224|SHA224withRSA/PSS|×|×|√ +PSS+SHA384|SHA384withRSA/PSS|4.6+|√|√ +PSS+SHA512|SHA512withRSA/PSS|4.6+|√|√ +PSS+SHA-512/224|SHA512/224withRSA/PSS|×|×|√ +PSS+SHA-512/256|SHA512/256withRSA/PSS|×|×|√ +PSS+SHA3-256|SHA3-256withRSA/PSS|×|8+|√ +PSS+SHA3-224|SHA3-224withRSA/PSS|×|×|√ +PSS+SHA3-384|SHA3-384withRSA/PSS|×|8+|√ +PSS+SHA3-512|SHA3-512withRSA/PSS|×|8+|√ +PSS+MD5 |MD5withRSA/PSS|4.6+|√|√ + + + +[​](?) + +[​](?) -Framework项目里面需要引入程序集`System.Numerics`用来支持`BigInteger`,vs默认创建的项目是不会自动引入此程序集的,要手动引入,Core的不需要。 +## RSA_PEM 类文档 +`RSA_PEM.cs`文件不依赖任何文件,可以直接copy这个文件到你项目中用;通过`FromPEM`、`ToPEM` 和`FromXML`、`ToXML`这两对方法,可以实现PEM`PKCS#1`、`PKCS#8`相互转换,PEM、XML的相互转换。 注:`openssl rsa -in 私钥文件 -pubout`导出的是PKCS#8格式公钥(用的比较多),`openssl rsa -pubin -in PKCS#8公钥文件 -RSAPublicKey_out`导出的是PKCS#1格式公钥(用的比较少)。 -### 静态方法 +### 静态属性和方法 `RSA_PEM` **FromPEM(string pem)**:用PEM格式密钥对创建RSA,支持PKCS#1、PKCS#8格式的PEM,出错将会抛出异常。pem格式如:`-----BEGIN XXX KEY-----....-----END XXX KEY-----`。 `RSA_PEM` **FromXML(string xml)**:将XML格式密钥转成PEM,支持公钥xml、私钥xml,出错将会抛出异常。xml格式如:`....`。 +`string` **T(string zh, string en)**:简版多语言支持,根据当前语言`Lang`值返回中文或英文。 + +`string` **Lang**:简版多语言支持,取值:`zh`(简体中文)、`en`(English-US),默认根据系统取值,可赋值指定语言。 + ### 构造方法 @@ -136,6 +220,14 @@ Framework项目里面需要引入程序集`System.Numerics`用来支持`BigInteg `RSACryptoServiceProvider` **GetRSA_ForWindows()**:将PEM中的公钥私钥转成RSA对象,如果未提供私钥,RSA中就只包含公钥。.NET Core、.NET Framework均可用,但返回的RSACryptoServiceProvider不支持跨平台,所以只支持在Windows系统中使用。 +`void` **GetRSA__ImportParameters(RSA rsa)**:将密钥参数导入到RSA对象中,`GetRSA_ForCore`、`GetRSA_ForWindows`中调用了本方法,可用于创建其他类型的RSA时使用。 + +`RSA_PEM` **CopyToNew(bool convertToPublic = false)**:将当前PEM中的密钥对复制出一个新的PEM对象。convertToPublic:等于true时含私钥的PEM将只返回公钥,仅含公钥的PEM不受影响。 + +`RSA_PEM` **SwapKey_Exponent_D__Unsafe()**:【不安全、不建议使用】对调交换公钥指数(Key_Exponent)和私钥指数(Key_D):把公钥当私钥使用(new.Key_D=this.Key_Exponent)、私钥当公钥使用(new.Key_Exponent=this.Key_D),返回一个新PEM对象;比如用于:私钥加密、公钥解密,这是非常规的用法。当前对象必须含私钥,否则无法交换会直接抛异常。注意:把公钥当私钥使用是非常不安全的,因为绝大部分生成的密钥的公钥指数为 0x10001(AQAB),太容易被猜测到,无法作为真正意义上的私钥。 + +`byte[]` **ToDER(bool convertToPublic, bool privateUsePKCS8, bool publicUsePKCS8)**:将RSA中的密钥对转换成DER格式,DER格式为PEM中的Base64文本编码前的二进制数据,参数含义参考ToPEM方法。 + `string` **ToPEM(bool convertToPublic, bool privateUsePKCS8, bool publicUsePKCS8)**:将RSA中的密钥对转换成PEM格式。convertToPublic:等于true时含私钥的RSA将只返回公钥,仅含公钥的RSA不受影响 。**privateUsePKCS8**:私钥的返回格式,等于true时返回PKCS#8格式(`-----BEGIN PRIVATE KEY-----`),否则返回PKCS#1格式(`-----BEGIN RSA PRIVATE KEY-----`),返回公钥时此参数无效;两种格式使用都比较常见。**publicUsePKCS8**:公钥的返回格式,等于true时返回PKCS#8格式(`-----BEGIN PUBLIC KEY-----`),否则返回PKCS#1格式(`-----BEGIN RSA PUBLIC KEY-----`),返回私钥时此参数无效;一般用的多的是true PKCS#8格式公钥,PKCS#1格式公钥似乎比较少见。 `string` **ToPEM_PKCS1(bool convertToPublic=false)**:ToPEM方法的简化写法,不管公钥还是私钥都返回PKCS#1格式;似乎导出PKCS#1公钥用的比较少,PKCS#8的公钥用的多些,私钥#1#8都差不多。 @@ -147,8 +239,28 @@ Framework项目里面需要引入程序集`System.Numerics`用来支持`BigInteg -## 【RSA_Util.cs】 -这个文件依赖`RSA_PEM.cs`,封装了加密、解密、签名、验证、秘钥导入导出操作;.NET Core下由实际的RSA实现类提供支持,.NET Framework下由RSACryptoServiceProvider提供支持。 +[​](?) + +[​](?) + +## RSA_Util 类文档 +`RSA_Util.cs`文件依赖`RSA_PEM.cs`,封装了加密、解密、签名、验证、秘钥导入导出操作;.NET Core下由实际的RSA实现类提供支持,.NET Framework 4.5及以下由RSACryptoServiceProvider提供支持,.NET Framework 4.6及以上由RSACng提供支持;或者引入BouncyCastle加密增强库提供支持。 + + +### 静态属性和方法 + +`string` **RSAPadding_Enc(string padding)**:将加密填充方式转换成对应的Algorithm字符串,比如`PKCS1 -> RSA/ECB/PKCS1Padding`。 + +`string` **RSAPadding_Sign(string hash)**:将签名填充方式转换成对应的Algorithm字符串,比如`PKCS1+SHA1 -> SHA1withRSA`。 + +`bool` **IsDotNetSupportError(string errMsg)**:判断异常消息是否是因为.NET兼容性产生的错误。 + +`void` **UseBouncyCastle(Assembly bouncyCastleAssembly)**:强制使用BouncyCastle加密增强库进行RSA操作。只需在程序启动后调用一次即可,直接调用一下BouncyCastle里面的类,传入程序集:`UseBouncyCastle(typeof(RsaEngine).Assembly)`,传入null取消使用。项目中引入BouncyCastle加密增强库来扩充.NET加密功能,NuGet:Portable.BouncyCastle或BouncyCastle.Cryptography,文档 https://www.bouncycastle.org/csharp/ ,在程序启动时调用本方法进行注册即可得到全部的加密签名填充方式支持。 + +`bool` **IsUseBouncyCastle**:是否强制使用BouncyCastle加密增强库进行RSA操作,为true时将不会使用.NET的RSA。 + +`bool` **IS_CORE**:当前运行环境是否为.NET Core,false为.NET Framework。 + ### 构造方法 @@ -165,9 +277,9 @@ Framework项目里面需要引入程序集`System.Numerics`用来支持`BigInteg ### 实例属性 -`RSA` **RSAObject**:最底层的RSA对象,.NET Core下为实际的RSA实现类,.NET Framework下RSACryptoServiceProvider。 +`RSA` **RSAObject**:获取最底层的RSA对象,.NET Core下为实际的RSA实现类,.NET Framework 4.5及以下RSACryptoServiceProvider,.NET Framework 4.6及以上RSACng;注意:IsUseBouncyCastle时将不会使用.NET的RSA。 -`bool` **RSAIsUseCore**:最底层的RSA对象是否是使用的rsaCore(RSA),否则将是使用的rsaFramework(RSACryptoServiceProvider)。 +`bool` **RSAIsUseCore**:最底层的RSA对象是否是使用的.NET Core(RSA),否则将是使用的.NET Framework(4.5及以下RSACryptoServiceProvider、4.6及以上RSACng);注意:IsUseBouncyCastle时将不会使用.NET的RSA。 `int` **KeySize**:密钥位数。 @@ -180,23 +292,23 @@ Framework项目里面需要引入程序集`System.Numerics`用来支持`BigInteg `RSA_PEM` **ToPEM(bool convertToPublic = false)**:导出RSA_PEM对象(然后可以通过RSA_PEM.ToPEM方法导出PEM文本),如果convertToPublic含私钥的RSA将只返回公钥,仅含公钥的RSA不受影响。 -`string` **Encode(string str)**:加密操作,支持任意长度数据,出错抛异常。 +`RSA_Util` **SwapKey_Exponent_D__Unsafe()**:【不安全、不建议使用】对调交换公钥指数(Key_Exponent)和私钥指数(Key_D):把公钥当私钥使用(new.Key_D=this.Key_Exponent)、私钥当公钥使用(new.Key_Exponent=this.Key_D),返回一个新RSA对象;比如用于:私钥加密、公钥解密,这是非常规的用法。当前对象必须含私钥,否则无法交换会直接抛异常。注意:把公钥当私钥使用是非常不安全的,因为绝大部分生成的密钥的公钥指数为 0x10001(AQAB),太容易被猜测到,无法作为真正意义上的私钥。交换后的密钥不支持在RSACryptoServiceProvider(.NET Framework 4.5及以下版本)中使用:`!IS_CoreOr46 && !IsUseBouncyCastle`。 -`byte[]` **Encode(byte[] data)**:加密数据,支持任意长度数据,出错抛异常。 +`string` **Encrypt(string padding, string str)**:加密任意长度字符串(utf-8)返回base64,出错抛异常。本方法线程安全。padding指定填充方式,如:PKCS1、OAEP+SHA256大写,参考上面的加密填充方式表格,使用空值时默认为PKCS1。 -`string` **DecodeOrNull(string str)**:解密字符串(utf-8),解密异常返回null。 +`byte[]` **Encrypt(string padding, byte[] data)**:加密任意长度数据,出错抛异常。本方法线程安全。 -`byte[]` **DecodeOrNull(byte[] data)**:解密数据,解密异常返回null。 +`string` **Decrypt(string padding, string str)**:解密任意长度密文(base64)得到字符串(utf-8),出错抛异常。本方法线程安全。padding指定填充方式,如:PKCS1、OAEP+SHA256大写,参考上面的加密填充方式表格,使用空值时默认为PKCS1。 -`string` **Sign(string hash, string str)**:对str进行签名,并指定hash算法(如:SHA256)。 +`byte[]` **Decrypt(string padding, byte[] data)**:解密任意长度数据,出错抛异常。本方法线程安全。 -`byte[]` **Sign(string hash, byte[] data)**:对data进行签名,并指定hash算法(如:SHA256)。 +`string` **Sign(string hash, string str)**:对字符串str进行签名,返回base64结果,出错抛异常。本方法线程安全。hash指定签名摘要算法和填充方式,如:SHA256、PSS+SHA1大写,参考上面的签名填充方式表格。 -`bool` **Verify(string hash, string sign, string str)**:验证字符串str的签名是否是sign,并指定hash算法(如:SHA256)。 - -`bool` **Verify(string hash, byte[] sign, byte[] data)**:验证data的签名是否是sign,并指定hash算法(如:SHA256)。 +`byte[]` **Sign(string hash, byte[] data)**:对data进行签名,出错抛异常。本方法线程安全。 +`bool` **Verify(string hash, string sign, string str)**:验证字符串str的签名是否是sign(base64),出错抛异常。本方法线程安全。hash指定签名摘要算法和填充方式,如:SHA256、PSS+SHA1大写,参考上面的签名填充方式表格。 +`bool` **Verify(string hash, byte[] sign, byte[] data)**:验证data的签名是否是sign,出错抛异常。本方法线程安全。 @@ -207,24 +319,44 @@ Framework项目里面需要引入程序集`System.Numerics`用来支持`BigInteg [​](?) -[​](?) +## OpenSSL RSA常用命令行参考 +``` bat +::先准备一个测试文件 test.txt 里面填少量内容,openssl不支持自动分段加密 -[​](?) +::生成新密钥 +openssl genrsa -out private.pem 1024 + +::提取公钥PKCS#8 +openssl rsa -in private.pem -pubout -out public.pem + +::转换成RSAPublicKey PKCS#1 +openssl rsa -pubin -in public.pem -RSAPublicKey_out -out public.pem.rsakey +::测试RSAPublicKey PKCS#1,不出意外会出错。因为这个公钥里面没有OID,通过RSA_PEM转换成PKCS#8自动带上OID就能正常加密 +echo abcd123 | openssl rsautl -encrypt -inkey public.pem.rsakey -pubin -[​](?) -[​](?) -# :open_book:图例 +::加密和解密,填充方式:PKCS1 +openssl pkeyutl -encrypt -pkeyopt rsa_padding_mode:pkcs1 -in test.txt -pubin -inkey public.pem -out test.txt.enc.bin +openssl pkeyutl -decrypt -pkeyopt rsa_padding_mode:pkcs1 -in test.txt.enc.bin -inkey private.pem -out test.txt.dec.txt -控制台运行: +::加密和解密,填充方式:OAEP+SHA256,掩码生成函数MGF1使用相同的hash算法 +openssl pkeyutl -encrypt -pkeyopt rsa_padding_mode:oaep -pkeyopt rsa_oaep_md:sha256 -in test.txt -pubin -inkey public.pem -out test.txt.enc.bin +openssl pkeyutl -decrypt -pkeyopt rsa_padding_mode:oaep -pkeyopt rsa_oaep_md:sha256 -in test.txt.enc.bin -inkey private.pem -out test.txt.dec.txt -![控制台运行](images/1.png) -RSA工具(非开源): +::命令行参数中的sha256可以换成md5、sha1等;如需sha3系列,就换成sha3-256即可 -![RSA工具](images/2.png) +::签名和验证,填充方式:PKCS1+SHA256 +openssl dgst -sha256 -binary -sign private.pem -out test.txt.sign.bin test.txt +openssl dgst -sha256 -binary -verify public.pem -signature test.txt.sign.bin test.txt + +::签名和验证,填充方式:PSS+SHA256 ,salt=-1使用hash长度=256/8,掩码生成函数MGF1使用相同的hash算法 +openssl dgst -sha256 -binary -out test.txt.hash test.txt +openssl pkeyutl -sign -pkeyopt digest:sha256 -pkeyopt rsa_padding_mode:pss -pkeyopt rsa_pss_saltlen:-1 -in test.txt.hash -inkey private.pem -out test.txt.sign.bin +openssl pkeyutl -verify -pkeyopt digest:sha256 -pkeyopt rsa_padding_mode:pss -pkeyopt rsa_pss_saltlen:-1 -in test.txt.hash -pubin -inkey public.pem -sigfile test.txt.sign.bin +``` @@ -245,9 +377,7 @@ RSA工具(非开源): # :open_book:知识库 -在写一个小转换工具时加入了RSA加密解密支持(见图RSA工具),秘钥输入框支持填写XML和PEM格式,操作类型里面支持XML->PEM、PEM->XML的转换。 - -实现相应功能发现原有RSA操作类不能良好工作,PEM->XML没问题,只要能通过PEM创建RSA,就能用`RSACryptoServiceProvider`自带方法导出XML。但XML->PEM没有找到相应的简单实现方法,大部分博客写的用BouncyCastle库来操作,代码是少,但BouncyCastle就有好几兆大小,我的小工具啊才100K;所以自己实现了一个支持导出`PKCS#1`、`PKCS#8`格式PEM密钥的方法`RSA_PEM.ToPEM`。 +在写一个小转换工具时加入了RSA加密解密支持,实现相应功能发现原有RSA操作类不能良好工作,PEM->XML没问题,只要能通过PEM创建RSA,就能用`RSACryptoServiceProvider`自带方法导出XML。但XML->PEM没有找到相应的简单实现方法,大部分博客写的用BouncyCastle库来操作,代码是少,但BouncyCastle就有好几兆大小,我的小工具啊才100K;所以自己实现了一个支持导出`PKCS#1`、`PKCS#8`格式PEM密钥的方法`RSA_PEM.ToPEM`。 操作过程中发现原有RSA操作类不支持用`PKCS#8`格式PEM密钥来创建RSA对象(用的[RSACryptoServiceProviderExtension](https://www.cnblogs.com/adylee/p/3611461.html)的扩展方法来支持PEM密钥),仅支持`PKCS#1`,所以又自己实现了一个从PEM密钥来创建`RSACryptoServiceProvider`的方法`RSA_PEM.FromPEM`。 @@ -524,28 +654,6 @@ RSACryptoServiceProvider生成的密钥,有一定概率生成某些字段首 > 这几个问题是QQ:284485094 提出的,循环一下测试很容易出现这个现象,现在的代码已经兼容了这种密钥。 -## openssl RSA常用命令行 -``` bat -::生成密钥对 -openssl genrsa -out private.pem 1024 - -::提取公钥PKCS#8 -openssl rsa -in private.pem -pubout -out public.pem - -::转换成RSAPublicKey PKCS#1 -openssl rsa -pubin -in public.pem -RSAPublicKey_out -out public.pem.rsakey - -::加密 -echo abcd123 | openssl rsautl -encrypt -inkey public.pem -pubin -out data.enc.bin - -::解密 -openssl rsautl -decrypt -in data.enc.bin -inkey private.pem -out data.dec.txt - -::测试RSAPublicKey PKCS#1,不出意外会出错 -::因为这个公钥里面没有OID,通过RSA_PEM转换成PKCS#8自动带上OID就能正常加密 -echo abcd123 | openssl rsautl -encrypt -inkey public.pem.rsakey -pubin -``` - diff --git a/RSA_PEM.cs b/RSA_PEM.cs index 5ecbc3c..b618e32 100644 --- a/RSA_PEM.cs +++ b/RSA_PEM.cs @@ -5,6 +5,7 @@ using System.Security.Cryptography; using System.Text; using System.Text.RegularExpressions; +using System.Globalization; namespace com.github.xiangyuecn.rsacsharp { /// @@ -154,9 +155,11 @@ public bool HasPrivate { /// 将PEM中的公钥私钥转成RSA对象,如果未提供私钥,RSA中就只包含公钥。返回的RSA支持跨平台使用,但只支持在.NET Core环境中使用 /// public RSA GetRSA_ForCore() { - RSACryptoServiceProvider.UseMachineKeyStore = true; RSA rsa = RSA.Create(); - setToRSA(rsa); + if (rsa is RSACryptoServiceProvider) { + return GetRSA_ForWindows(); + } + GetRSA__ImportParameters(rsa); return rsa; } /// @@ -167,10 +170,13 @@ public RSACryptoServiceProvider GetRSA_ForWindows() { rsaParams.Flags = CspProviderFlags.UseMachineKeyStore; var rsa = new RSACryptoServiceProvider(rsaParams); - setToRSA(rsa); + GetRSA__ImportParameters(rsa); return rsa; } - private void setToRSA(RSA rsa) { + /// + /// 将密钥参数导入到RSA对象中 + /// + public void GetRSA__ImportParameters(RSA rsa) { var param = new RSAParameters(); param.Modulus = Key_Modulus; param.Exponent = Key_Exponent; @@ -234,7 +240,7 @@ static private BigInteger FindFactor(BigInteger e, BigInteger d, BigInteger n) { long now = DateTime.Now.Ticks; for (int aInt = 2; true; aInt++) { if (aInt % 10 == 0 && DateTime.Now.Ticks - now > 3000 * 10000) { - throw new Exception("推算RSA.P超时");//测试最多循环2次,1024位的速度很快 8ms + throw new Exception(T("推算RSA.P超时", "Estimated RSA.P timeout"));//测试最多循环2次,1024位的速度很快 8ms } BigInteger aPow = BigInteger.ModPow(new BigInteger(aInt), t, n); @@ -275,7 +281,7 @@ static public RSA_PEM FromPEM(string pem) { byte[] data = null; try { data = Convert.FromBase64String(base64); } catch { } if (data == null) { - throw new Exception("PEM内容无效"); + throw new Exception(T("PEM内容无效", "Invalid PEM content")); } var idx = 0; @@ -293,7 +299,7 @@ static public RSA_PEM FromPEM(string pem) { return data[idx++]; } } - throw new Exception("PEM未能提取到数据"); + throw new Exception(T("PEM未能提取到数据", "Failed to extract data from PEM")); }; //读取块数据 Func readBlock = () => { @@ -354,7 +360,7 @@ static public RSA_PEM FromPEM(string pem) { //读取版本号 if (!eq(_Ver)) { - throw new Exception("PEM未知版本"); + throw new Exception(T("PEM未知版本", "Unknown PEM version")); } //检测PKCS8 @@ -367,7 +373,7 @@ static public RSA_PEM FromPEM(string pem) { //读取版本号 if (!eq(_Ver)) { - throw new Exception("PEM版本无效"); + throw new Exception(T("PEM版本无效", "Invalid PEM version")); } } else { idx = idx2; @@ -385,7 +391,7 @@ static public RSA_PEM FromPEM(string pem) { param.Val_DQ = BigL(readBlock(), keyLen); param.Val_InverseQ = BigL(readBlock(), keyLen); } else { - throw new Exception("pem需要BEGIN END标头"); + throw new Exception(T("pem需要BEGIN END标头", "pem requires BEGIN END header")); } return param; @@ -400,6 +406,32 @@ static public RSA_PEM FromPEM(string pem) { + + + + /// + /// 将当前PEM中的密钥对复制出一个新的PEM对象 + /// 。convertToPublic:等于true时含私钥的PEM将只返回公钥,仅含公钥的PEM不受影响 + /// + public RSA_PEM CopyToNew(bool convertToPublic = false) { + if (convertToPublic) { + return new RSA_PEM(Key_Modulus, Key_Exponent, null, null, null, null, null, null); + } + return new RSA_PEM(Key_Modulus, Key_Exponent, Key_D, Val_P, Val_Q, Val_DP, Val_DQ, Val_InverseQ); + } + /// + /// 【不安全、不建议使用】对调交换公钥指数(Key_Exponent)和私钥指数(Key_D):把公钥当私钥使用(new.Key_D=this.Key_Exponent)、私钥当公钥使用(new.Key_Exponent=this.Key_D),返回一个新PEM对象;比如用于:私钥加密、公钥解密,这是非常规的用法 + /// 。当前对象必须含私钥,否则无法交换会直接抛异常 + /// 。注意:把公钥当私钥使用是非常不安全的,因为绝大部分生成的密钥的公钥指数为 0x10001(AQAB),太容易被猜测到,无法作为真正意义上的私钥 + /// + public RSA_PEM SwapKey_Exponent_D__Unsafe() { + if (Key_D == null) throw new Exception(T("SwapKey只支持私钥", "SwapKey only supports private keys")); + return new RSA_PEM(Key_Modulus, Key_D, Key_Exponent); + } + + + + /// /// 将RSA中的密钥对转换成PEM PKCS#1格式 /// 。convertToPublic:等于true时含私钥的RSA将只返回公钥,仅含公钥的RSA不受影响 @@ -424,6 +456,46 @@ public string ToPEM_PKCS8(bool convertToPublic = false) { /// 。publicUsePKCS8:公钥的返回格式,等于true时返回PKCS#8格式(-----BEGIN PUBLIC KEY-----),否则返回PKCS#1格式(-----BEGIN RSA PUBLIC KEY-----),返回私钥时此参数无效;一般用的多的是true PKCS#8格式公钥,PKCS#1格式似乎比较少见公钥 /// public string ToPEM(bool convertToPublic, bool privateUsePKCS8, bool publicUsePKCS8) { + byte[] der = ToDER(convertToPublic, privateUsePKCS8, publicUsePKCS8); + if (Key_D == null || convertToPublic) { + var flag = " PUBLIC KEY"; + if (!publicUsePKCS8) { + flag = " RSA" + flag; + } + return "-----BEGIN" + flag + "-----\n" + TextBreak(Convert.ToBase64String(der), 64) + "\n-----END" + flag + "-----"; + } else { + var flag = " PRIVATE KEY"; + if (!privateUsePKCS8) { + flag = " RSA" + flag; + } + return "-----BEGIN" + flag + "-----\n" + TextBreak(Convert.ToBase64String(der), 64) + "\n-----END" + flag + "-----"; + } + } + // 把字符串按每行多少个字断行 + static private string TextBreak(string text, int line) { + var idx = 0; + var len = text.Length; + var str = new StringBuilder(); + while (idx < len) { + if (idx > 0) { + str.Append('\n'); + } + if (idx + line >= len) { + str.Append(text.Substring(idx)); + } else { + str.Append(text.Substring(idx, line)); + } + idx += line; + } + return str.ToString(); + } + /// + /// 将RSA中的密钥对转换成DER格式,DER格式为PEM中的Base64文本编码前的二进制数据 + /// 。convertToPublic:等于true时含私钥的RSA将只返回公钥,仅含公钥的RSA不受影响 + /// 。privateUsePKCS8:私钥的返回格式,等于true时返回PKCS#8格式,否则返回PKCS#1格式,返回公钥时此参数无效;两种格式使用都比较常见 + /// 。publicUsePKCS8:公钥的返回格式,等于true时返回PKCS#8格式,否则返回PKCS#1格式,返回私钥时此参数无效;一般用的多的是true PKCS#8格式公钥,PKCS#1格式似乎比较少见公钥 + /// + public byte[] ToDER(bool convertToPublic, bool privateUsePKCS8, bool publicUsePKCS8) { //https://www.jianshu.com/p/25803dd9527d //https://www.cnblogs.com/ylz8401/p/8443819.html //https://blog.csdn.net/jiayanhui2877/article/details/47187077 @@ -470,23 +542,6 @@ public string ToPEM(bool convertToPublic, bool privateUsePKCS8, bool publicUsePK Action writeAll = (stream, byts) => { stream.Write(byts, 0, byts.Length); }; - Func TextBreak = (text, line) => { - var idx = 0; - var len = text.Length; - var str = new StringBuilder(); - while (idx < len) { - if (idx > 0) { - str.Append('\n'); - } - if (idx + line >= len) { - str.Append(text.Substring(idx)); - } else { - str.Append(text.Substring(idx, line)); - } - idx += line; - } - return str.ToString(); - }; if (Key_D == null || convertToPublic) { @@ -529,12 +584,7 @@ public string ToPEM(bool convertToPublic, bool privateUsePKCS8, bool publicUsePK } byts = writeLen(index1, byts); - - var flag = " PUBLIC KEY"; - if (!publicUsePKCS8) { - flag = " RSA" + flag; - } - return "-----BEGIN" + flag + "-----\n" + TextBreak(Convert.ToBase64String(byts), 64) + "\n-----END" + flag + "-----"; + return byts; } else { /****生成私钥****/ @@ -583,12 +633,7 @@ public string ToPEM(bool convertToPublic, bool privateUsePKCS8, bool publicUsePK } byts = writeLen(index1, byts); - - var flag = " PRIVATE KEY"; - if (!privateUsePKCS8) { - flag = " RSA" + flag; - } - return "-----BEGIN" + flag + "-----\n" + TextBreak(Convert.ToBase64String(byts), 64) + "\n-----END" + flag + "-----"; + return byts; } } @@ -615,7 +660,7 @@ static public RSA_PEM FromXML(string xml) { Match xmlM = xmlExp.Match(xml); if (!xmlM.Success) { - throw new Exception("XML内容不符合要求"); + throw new Exception(T("XML内容不符合要求", "XML content does not meet requirements")); } Match tagM = xmlTagExp.Match(xmlM.Groups[1].Value); @@ -638,7 +683,7 @@ static public RSA_PEM FromXML(string xml) { } if (rtv.Key_Modulus == null || rtv.Key_Exponent == null) { - throw new Exception("XML公钥丢失"); + throw new Exception(T("XML公钥丢失", "Public key in XML is missing")); } if (rtv.Key_D != null) { if (rtv.Val_P == null || rtv.Val_Q == null || rtv.Val_DP == null || rtv.Val_DQ == null || rtv.Val_InverseQ == null) { @@ -682,5 +727,30 @@ public string ToXML(bool convertToPublic) { + /// + /// 简版多语言支持,根据当前语言返回中文或英文 + /// + static public string T(string zh, string en) { + return Lang == "zh" ? zh : en; + } + static private string _lang; + /// + /// 简版多语言支持,取值:zh(简体中文)、en(English-US),默认根据系统取值 + /// + static public string Lang { + get { + if (_lang == null) { + var locale = CultureInfo.CurrentCulture.Name.Replace('_', '-').ToLower(); + if (Regex.IsMatch(locale, "\\b(zh|cn)\\b")) { + _lang = "zh"; + } else { + _lang = "en"; + } + } + return _lang; + } + set { _lang = value; } + } + } } diff --git a/RSA_Util.cs b/RSA_Util.cs index 2145398..9f45c4d 100644 --- a/RSA_Util.cs +++ b/RSA_Util.cs @@ -1,148 +1,355 @@ -using System; +// 1:直接编译调用.NET Framework 4.6以上版本或.NET Core代码,0:使用反射进行调用;使用.NET Framework 4.6以上版本框架或使用.NET Core框架时可改成1 +#define RSA_Util_NewNET_CompileCode_0 +#if (RSA_BUILD__NET_CORE || NETCOREAPP || NETSTANDARD || NET) //使用.NET Core框架时自动设为1。csproj:PropertyGroup.DefineConstants + https://docs.microsoft.com/zh-cn/dotnet/csharp/language-reference/preprocessor-directives +#define RSA_Util_NewNET_CompileCode_1 +#endif + +// 1:直接编译使用BouncyCastle的代码,0:使用反射进行调用;调用了RSA_Util.UseBouncyCastle方法时可改成1 +#define RSA_Util_BouncyCastle_CompileCode_0 + + +using System; using System.IO; +using System.Reflection; using System.Security.Cryptography; using System.Text; +using System.Text.RegularExpressions; + +#if RSA_Util_BouncyCastle_CompileCode_1 +using Org.BouncyCastle.Crypto; +using Org.BouncyCastle.Crypto.Encodings; +using Org.BouncyCastle.Crypto.Engines; +using Org.BouncyCastle.Crypto.Parameters; +using Org.BouncyCastle.Crypto.Signers; +using Org.BouncyCastle.Security; +using BcInt = Org.BouncyCastle.Math.BigInteger; +#endif namespace com.github.xiangyuecn.rsacsharp { /// - /// RSA操作类,.NET Core、.NET Framework均可用:.NET Core下由实际的RSA实现类提供支持,.NET Framework下由RSACryptoServiceProvider提供支持。 + /// RSA操作类,.NET Core、.NET Framework均可用:.NET Core下由实际的RSA实现类提供支持,.NET Framework 4.5及以下由RSACryptoServiceProvider提供支持,.NET Framework 4.6及以上由RSACng提供支持;或者引入BouncyCastle加密增强库提供支持。 /// GitHub: https://github.com/xiangyuecn/RSA-csharp /// public class RSA_Util { /// - /// 导出XML格式密钥对,如果convertToPublic含私钥的RSA将只返回公钥,仅含公钥的RSA不受影响 + /// 导出XML格式密钥,如果convertToPublic含私钥的RSA将只返回公钥,仅含公钥的RSA不受影响 /// public string ToXML(bool convertToPublic = false) { return ToPEM(convertToPublic).ToXML(convertToPublic); } /// - /// 将密钥对导出成PEM对象,如果convertToPublic含私钥的RSA将只返回公钥,仅含公钥的RSA不受影响 + /// 将密钥导出成PEM对象,如果convertToPublic含私钥的RSA将只返回公钥,仅含公钥的RSA不受影响 /// public RSA_PEM ToPEM(bool convertToPublic = false) { - return new RSA_PEM(RSAObject, convertToPublic); + return PEM__.CopyToNew(convertToPublic); + } + /// + /// 【不安全、不建议使用】对调交换公钥指数(Key_Exponent)和私钥指数(Key_D):把公钥当私钥使用(new.Key_D=this.Key_Exponent)、私钥当公钥使用(new.Key_Exponent=this.Key_D),返回一个新RSA对象;比如用于:私钥加密、公钥解密,这是非常规的用法 + /// 。当前对象必须含私钥,否则无法交换会直接抛异常 + /// 。注意:把公钥当私钥使用是非常不安全的,因为绝大部分生成的密钥的公钥指数为 0x10001(AQAB),太容易被猜测到,无法作为真正意义上的私钥 + /// 。交换后的密钥不支持在RSACryptoServiceProvider(.NET Framework 4.5及以下版本)中使用:!IS_CoreOr46 And !IsUseBouncyCastle + /// + public RSA_Util SwapKey_Exponent_D__Unsafe() { + return new RSA_Util(PEM__.SwapKey_Exponent_D__Unsafe()); } - /// - /// 加密字符串(utf-8),出错抛异常 + /// 内置加密解密填充方式列表 /// - public string Encode(string str) { - return Convert.ToBase64String(Encode(Encoding.UTF8.GetBytes(str))); + static public string[] RSAPadding_Enc_DefaultKeys() { + string s = "NO, PKCS1"; + s += ", OAEP+SHA1, OAEP+SHA256, OAEP+SHA224, OAEP+SHA384, OAEP+SHA512"; + s += ", OAEP+SHA-512/224, OAEP+SHA-512/256"; + s += ", OAEP+SHA3-256, OAEP+SHA3-224, OAEP+SHA3-384, OAEP+SHA3-512"; + s += ", OAEP+MD5"; + return s.Split(new string[] { ", " }, StringSplitOptions.None); } /// - /// 加密数据,出错抛异常 + /// 将填充方式格式化成内置的RSA加密解密填充模式,padding取值和对应的填充模式: + /// + ///
null: 等同于PKCS1 + ///
"": 等同于PKCS1 + ///
RSA: 等同于PKCS1 + ///
PKCS: 等同于PKCS1 + ///
RAW: 等同于NO + ///
OAEP: 等同于OAEP+SHA1 + ///
RSA/ECB/OAEPPadding: 等同于OAEP+SHA1 + ///
+ ///
NO: RSA/ECB/NoPadding + ///
PKCS1: RSA/ECB/PKCS1Padding (默认值,等同于"RSA") + ///
OAEP+SHA1 : RSA/ECB/OAEPwithSHA-1andMGF1Padding + ///
OAEP+SHA256: RSA/ECB/OAEPwithSHA-256andMGF1Padding + ///
OAEP+SHA224: RSA/ECB/OAEPwithSHA-224andMGF1Padding + ///
OAEP+SHA384: RSA/ECB/OAEPwithSHA-384andMGF1Padding + ///
OAEP+SHA512: RSA/ECB/OAEPwithSHA-512andMGF1Padding + ///
OAEP+SHA-512/224: RSA/ECB/OAEPwithSHA-512/224andMGF1Padding (SHA-512/*** 2012年发布) + ///
OAEP+SHA-512/256: RSA/ECB/OAEPwithSHA-512/256andMGF1Padding + ///
OAEP+SHA3-256: RSA/ECB/OAEPwithSHA3-256andMGF1Padding (SHA3-*** 2015年发布) + ///
OAEP+SHA3-224: RSA/ECB/OAEPwithSHA3-224andMGF1Padding + ///
OAEP+SHA3-384: RSA/ECB/OAEPwithSHA3-384andMGF1Padding + ///
OAEP+SHA3-512: RSA/ECB/OAEPwithSHA3-512andMGF1Padding + ///
OAEP+MD5 : RSA/ECB/OAEPwithMD5andMGF1Padding + ///
+ ///
如果padding包含RSA字符串,将原样返回此值,用于提供可能支持的任何值 + ///
非以上取值,将会抛异常 + ///
+ ///
其中OAEP的掩码生成函数MGF1使用和OAEP相同的Hash算法 + ///
+ ///
以上填充模式全部可用于BouncyCastle的RSA实现;但如果是使用的.NET自带的RSA实现,将会有部分模式无法支持:不支持全部SHA224、SHA-512/256、SHA-512/224,SHA3需要.NET8以上才支持,.NET Framework 4.5及以下只持OAEP+SHA1不支持其他OAEP + ///
+ ///
参考:https://learn.microsoft.com/zh-cn/dotnet/api/system.security.cryptography.rsaencryptionpadding + ///
///
- public byte[] Encode(byte[] data) { - int blockLen = KeySize / 8 - 11; - if (data.Length <= blockLen) { - if (rsaFramework != null) { - return rsaFramework.Encrypt(data, false); - } else { - return rsaCore.Encrypt(data, RSAEncryptionPadding.Pkcs1); - } - } - - using (var dataStream = new MemoryStream(data)) - using (var enStream = new MemoryStream()) { - byte[] buffer = new byte[blockLen]; - int len = dataStream.Read(buffer, 0, blockLen); + static public string RSAPadding_Enc(string padding) { + string val = padding; + if (val == null || val.Length == 0) val = "PKCS1"; + val = val.ToUpper(); - while (len > 0) { - byte[] block = new byte[len]; - Array.Copy(buffer, 0, block, 0, len); - - byte[] enBlock; - if (rsaFramework != null) { - enBlock = rsaFramework.Encrypt(block, false); - } else { - enBlock = rsaCore.Encrypt(block, RSAEncryptionPadding.Pkcs1); - } - enStream.Write(enBlock, 0, enBlock.Length); + if ("RSA" == val || "PKCS" == val) val = "PKCS1"; + if ("OAEP" == val || val.EndsWith("/OAEPPADDING")) val = "OAEP+SHA1"; + if ("RAW" == val) val = "NO"; + if (val.IndexOf("RSA") != -1) return padding; - len = dataStream.Read(buffer, 0, blockLen); + switch (val) { + case "PKCS1": return "RSA/ECB/PKCS1Padding"; + case "NO": return "RSA/ECB/NoPadding"; + } + if (val.StartsWith("OAEP+")) { + val = val.Replace("OAEP+", ""); + switch (val) { + case "SHA1": + case "SHA256": + case "SHA224": + case "SHA384": + case "SHA512": + case "SHA512/224": + case "SHA512/256": + val = "SHA-" + val.Substring(3); break; + } + switch (val) { + case "SHA-1": + case "SHA-256": + case "SHA-224": + case "SHA-384": + case "SHA-512": + case "SHA3-256": + case "SHA3-224": + case "SHA3-384": + case "SHA3-512": + case "SHA-512/224": + case "SHA-512/256": + case "MD5": + return "RSA/ECB/OAEPwith" + val + "andMGF1Padding"; } - - return enStream.ToArray(); } + throw new Exception(T("RSAPadding_Enc未定义Padding: ", "RSAPadding_Enc does not define Padding: ") + padding); + } + + /// + /// 内置签名填充方式列表 + /// + static public string[] RSAPadding_Sign_DefaultKeys() { + string s = "PKCS1+SHA1, PKCS1+SHA256, PKCS1+SHA224, PKCS1+SHA384, PKCS1+SHA512"; + s += ", PKCS1+SHA-512/224, PKCS1+SHA-512/256"; + s += ", PKCS1+SHA3-256, PKCS1+SHA3-224, PKCS1+SHA3-384, PKCS1+SHA3-512"; + s += ", PKCS1+MD5"; + s += ", PSS+SHA1, PSS+SHA256, PSS+SHA224, PSS+SHA384, PSS+SHA512"; + s += ", PSS+SHA-512/224, PSS+SHA-512/256"; + s += ", PSS+SHA3-256, PSS+SHA3-224, PSS+SHA3-384, PSS+SHA3-512"; + s += ", PSS+MD5"; + return s.Split(new string[] { ", " }, StringSplitOptions.None); } /// - /// 解密字符串(utf-8),解密异常返回null + /// 将填充方式转换成内置的RSA签名填充模式,hash取值和对应的填充模式: + /// + ///
SHA*** : 等同于PKCS1+SHA***,比如"SHA256" == "PKCS1+SHA256" + ///
MD5 : 等同于PKCS1+MD5 + ///
RSASSA-PSS: 等同于PSS+SHA1 + ///
+ ///
PKCS1+SHA1 : SHA1withRSA + ///
PKCS1+SHA256: SHA256withRSA + ///
PKCS1+SHA224: SHA224withRSA + ///
PKCS1+SHA384: SHA384withRSA + ///
PKCS1+SHA512: SHA512withRSA + ///
PKCS1+SHA-512/224: SHA512/224withRSA (SHA-512/*** 2012年发布) + ///
PKCS1+SHA-512/256: SHA512/256withRSA + ///
PKCS1+SHA3-256: SHA3-256withRSA (SHA3-*** 2015年发布) + ///
PKCS1+SHA3-224: SHA3-224withRSA + ///
PKCS1+SHA3-384: SHA3-384withRSA + ///
PKCS1+SHA3-512: SHA3-512withRSA + ///
PKCS1+MD5 : MD5withRSA + ///
+ ///
PSS+SHA1 : SHA1withRSA/PSS + ///
PSS+SHA256: SHA256withRSA/PSS + ///
PSS+SHA224: SHA224withRSA/PSS + ///
PSS+SHA384: SHA384withRSA/PSS + ///
PSS+SHA512: SHA512withRSA/PSS + ///
PSS+SHA-512/224: SHA512/224withRSA/PSS (SHA-512/*** 2012年发布) + ///
PSS+SHA-512/256: SHA512/256withRSA/PSS + ///
PSS+SHA3-256: SHA3-256withRSA/PSS (SHA3-*** 2015年发布) + ///
PSS+SHA3-224: SHA3-224withRSA/PSS + ///
PSS+SHA3-384: SHA3-384withRSA/PSS + ///
PSS+SHA3-512: SHA3-512withRSA/PSS + ///
PSS+MD5 : MD5withRSA/PSS + ///
+ ///
如果hash包含RSA字符串,将原样返回此值,用于提供可能支持的任何值 + ///
非以上取值,将会抛异常 + ///
+ ///
其中PSS的salt字节数等于使用的Hash算法字节数,PSS的掩码生成函数MGF1使用和PSS相同的Hash算法,跟踪属性TrailerField取值固定为0xBC + ///
+ ///
以上填充模式全部可用于BouncyCastle的RSA实现;但如果是使用的.NET自带的RSA实现,将会有部分模式无法支持:不支持全部SHA224、SHA-512/256、SHA-512/224,SHA3需要.NET8以上才支持,.NET Framework 4.5及以下不支持PSS + ///
+ ///
参考:https://learn.microsoft.com/zh-cn/dotnet/api/system.security.cryptography.rsasignaturepadding + ///
///
- public string DecodeOrNull(string str) { - if (String.IsNullOrEmpty(str)) { - return null; + static public string RSAPadding_Sign(string hash) { + string val = hash == null ? "" : hash; + val = val.ToUpper(); + + if ("RSASSA-PSS" == val) val = "PSS+SHA1"; + if (val.IndexOf("RSA") != -1) return hash; + + string pss = ""; + if (val.StartsWith("PSS+")) { + val = val.Substring(4); + pss = "/PSS"; + } else if (val.StartsWith("PKCS1+")) { + val = val.Substring(6); } - byte[] byts = null; - try { byts = Convert.FromBase64String(str); } catch { } - if (byts == null) { - return null; + switch (val) { + case "SHA-1": + case "SHA-256": + case "SHA-224": + case "SHA-384": + case "SHA-512": + case "SHA-512/224": + case "SHA-512/256": + val = val.Replace("-", ""); break; } - var val = DecodeOrNull(byts); - if (val == null) { - return null; + switch (val) { + case "SHA1": + case "SHA256": + case "SHA224": + case "SHA384": + case "SHA512": + case "SHA3-256": + case "SHA3-224": + case "SHA3-384": + case "SHA3-512": + case "SHA512/224": + case "SHA512/256": + case "MD5": + return val + "withRSA" + pss; } - return Encoding.UTF8.GetString(val); + throw new Exception(T("RSAPadding_Sign未定义Hash: ", "RSAPadding_Sign does not define Hash: ") + hash); + } + + static private string NetNotSupportMsg(string tag) { + return T(".NET不支持" + tag + ",解决办法:", ".NET does not support " + tag + ", solution: ") + Msg_Bc; + } + static private string NetLowVerSupportMsg(string tag) { + return T(".NET Framework版本低于4.6,不支持" + tag + ",解决办法:升级使用.NET Framework 4.6及以上版本,或者", "The .NET Framework version is lower than 4.6 and does not support " + tag + ". Solution: upgrade to .NET Framework 4.6 and above, or ") + Msg_Bc; + } + static private string Msg_Bc { + get { + return T("引入BouncyCastle加密增强库来扩充.NET加密功能(NuGet:Portable.BouncyCastle或BouncyCastle.Cryptography,文档 https://www.bouncycastle.org/csharp/ ),并且在程序启动时调用" + Msg_Bc_Reg + "进行注册即可得到全部支持。", "import the BouncyCastle encryption enhancement library to expand the .NET encryption function (NuGet: Portable.BouncyCastle or BouncyCastle.Cryptography, documentation https://www.bouncycastle.org/csharp/ ), and call" + Msg_Bc_Reg + "to register when the program starts to get full support."); + } + } + static private readonly string Msg_Bc_Reg = " `RSA_Util.UseBouncyCastle( typeof(RsaEngine).Assembly )` "; + /// + /// 是否是因为.NET兼容性产生的错误 + /// + static public bool IsDotNetSupportError(string errMsg) { + return errMsg.Contains(Msg_Bc_Reg); } /// - /// 解密数据,解密异常返回null + /// 将Hash算法名字转换成.NET对象,不支持的将返回null,hash需大写 + /// 。.NET 对 HashAlgorithm.Create 支持混乱,中间有些版本不允许调用 /// - public byte[] DecodeOrNull(byte[] data) { + static public HashAlgorithm HashFromName(string hash) { + HashAlgorithm obj = null; + try { obj = HashAlgorithm.Create(hash); } catch { } + if (obj == null) try { obj = HashAlgorithm.Create(hash.Replace("SHA-", "SHA")); } catch { } + if (obj != null) return obj; try { - int blockLen = KeySize / 8; - if (data.Length <= blockLen) { - if (rsaFramework != null) { - return rsaFramework.Decrypt(data, false); - } else { - return rsaCore.Decrypt(data, RSAEncryptionPadding.Pkcs1); + var types = typeof(SHA1).Assembly.GetTypes(); + var name1 = hash.Replace("-", "");//SHA256 + var name2 = hash.Replace("-", "_");//SHA3_256 + foreach (var type in types) { + var name = type.Name.ToUpper(); + if (name == name1 || name == name2) { + var fn = type.GetMethod("Create", new Type[0]); + if (fn != null && typeof(HashAlgorithm).IsAssignableFrom(fn.ReturnType)) { + return (HashAlgorithm)fn.Invoke(null, new object[0]); + } } } + } catch { } + return null; + } + static private void checkHashSupport(string hash) { + if (HashFromName(hash) == null) { + throw new Exception(T("本机.NET版本不支持" + hash + "摘要算法,升级使用更高版本的.NET可能会得到支持,或者", "The native .NET version does not support the " + hash + " digest algorithm, upgrading to a later version of .NET may be supported, or ") + Msg_Bc); + } + } + /// + /// 简版多语言支持,根据当前语言返回中文或英文,简化调用 + /// + static private string T(string zh, string en) { + return RSA_PEM.T(zh, en); + } - using (var dataStream = new MemoryStream(data)) - using (var deStream = new MemoryStream()) { - byte[] buffer = new byte[blockLen]; - int len = dataStream.Read(buffer, 0, blockLen); - - while (len > 0) { - byte[] block = new byte[len]; - Array.Copy(buffer, 0, block, 0, len); - byte[] deBlock; - if (rsaFramework != null) { - deBlock = rsaFramework.Decrypt(block, false); - } else { - deBlock = rsaCore.Decrypt(block, RSAEncryptionPadding.Pkcs1); - } - deStream.Write(deBlock, 0, deBlock.Length); - len = dataStream.Read(buffer, 0, blockLen); - } - return deStream.ToArray(); - } - } catch { - return null; + /// + /// 加密任意长度字符串(utf-8)返回base64,出错抛异常。本方法线程安全。padding指定填充方式(如:PKCS1、OAEP+SHA256大写),使用空值时默认为PKCS1,取值参考 + /// + public string Encrypt(string padding, string str) { + return Convert.ToBase64String(__Encrypt(padding, Encoding.UTF8.GetBytes(str))); + } + /// + /// 加密任意长度数据,出错抛异常。本方法线程安全。padding指定填充方式(如:PKCS1、OAEP+SHA256大写),使用空值时默认为PKCS1,取值参考 + /// + public byte[] Encrypt(string padding, byte[] data) { + return __Encrypt(padding, data); + } + /// + /// 解密任意长度密文(base64)得到字符串(utf-8),出错抛异常。本方法线程安全。padding指定填充方式(如:PKCS1、OAEP+SHA256大写),使用空值时默认为PKCS1,取值参考 + /// + public string Decrypt(string padding, string str) { + if (string.IsNullOrEmpty(str)) { + return ""; } + byte[] byts = Convert.FromBase64String(str); + var val = __Decrypt(padding, byts); + return Encoding.UTF8.GetString(val); } /// - /// 对str进行签名,并指定hash算法(如:SHA256) + /// 解密任意长度数据,出错抛异常。本方法线程安全。padding指定填充方式(如:PKCS1、OAEP+SHA256大写),使用空值时默认为PKCS1,取值参考 + /// + public byte[] Decrypt(string padding, byte[] data) { + return __Decrypt(padding, data); + } + + + + /// + /// 对字符串str进行签名,返回base64结果,出错抛异常。本方法线程安全。hash指定签名摘要算法和填充方式(如:SHA256、PSS+SHA1大写),取值参考 /// public string Sign(string hash, string str) { - return Convert.ToBase64String(Sign(hash, Encoding.UTF8.GetBytes(str))); + return Convert.ToBase64String(__Sign(hash, Encoding.UTF8.GetBytes(str))); } /// - /// 对data进行签名,并指定hash算法(如:SHA256) + /// 对data进行签名,出错抛异常。本方法线程安全。hash指定签名摘要算法和填充方式(如:SHA256、PSS+SHA1大写),取值参考 /// public byte[] Sign(string hash, byte[] data) { - if (rsaFramework != null) { - return rsaFramework.SignData(data, hash); - } else { - return rsaCore.SignData(data, new HashAlgorithmName(hash), RSASignaturePadding.Pkcs1); - } + return __Sign(hash, data); } /// - /// 验证字符串str的签名是否是sign,并指定hash算法(如:SHA256) + /// 验证字符串str的签名是否是sign(base64),出错抛异常。本方法线程安全。hash指定签名摘要算法和填充方式(如:SHA256、PSS+SHA1大写),取值参考 /// public bool Verify(string hash, string sign, string str) { byte[] byts = null; @@ -150,58 +357,25 @@ public bool Verify(string hash, string sign, string str) { if (byts == null) { return false; } - return Verify(hash, byts, Encoding.UTF8.GetBytes(str)); + return __Verify(hash, byts, Encoding.UTF8.GetBytes(str)); } /// - /// 验证data的签名是否是sign,并指定hash算法(如:SHA256) + /// 验证data的签名是否是sign,出错抛异常。本方法线程安全。hash指定签名摘要算法和填充方式(如:SHA256、PSS+SHA1大写),取值参考 /// public bool Verify(string hash, byte[] sign, byte[] data) { - try { - if (rsaFramework != null) { - return rsaFramework.VerifyData(data, hash, sign); - } else { - return rsaCore.VerifyData(data, sign, new HashAlgorithmName(hash), RSASignaturePadding.Pkcs1); - } - } catch { - return false; - } + return __Verify(hash, sign, data); } - /// - /// 密钥位数 - /// - public int KeySize { - get { - if (rsaFramework != null) { - return rsaFramework.KeySize; - } else { - return rsaCore.KeySize; - } - } - } - /// - /// 是否包含私钥 - /// - public bool HasPrivate { - get { - if (rsaFramework != null) { - return !rsaFramework.PublicOnly; - } else { - return ToPEM().HasPrivate; - } - } - } - /// /// 用指定密钥大小创建一个新的RSA,会生成新密钥,出错抛异常 /// public RSA_Util(int keySize) { RSA rsa = null; - if (UseCore) { + if (IS_CORE) { rsa = RSA.Create(); rsa.KeySize = keySize; } @@ -210,23 +384,23 @@ public RSA_Util(int keySize) { rsaParams.Flags = CspProviderFlags.UseMachineKeyStore; rsa = new RSACryptoServiceProvider(keySize, rsaParams); } - SetRSA__(rsa); + SetPEM__(new RSA_PEM(rsa, false)); } /// /// 通过指定的pem文件密钥或xml字符串密钥,创建一个RSA,pem或xml内可以只包含一个公钥或私钥,或都包含,出错抛异常 /// public RSA_Util(string pemOrXML) { if (pemOrXML.Trim().StartsWith("<")) { - SetRSA__(RSA_PEM.FromXML(pemOrXML)); + SetPEM__(RSA_PEM.FromXML(pemOrXML)); } else { - SetRSA__(RSA_PEM.FromPEM(pemOrXML)); + SetPEM__(RSA_PEM.FromPEM(pemOrXML)); } } /// /// 通过一个pem对象创建RSA,pem为公钥或私钥,出错抛异常 /// public RSA_Util(RSA_PEM pem) { - SetRSA__(pem); + SetPEM__(pem); } /// /// 本方法会先生成RSA_PEM再创建RSA:通过公钥指数和私钥指数构造一个PEM,会反推计算出P、Q但和原始生成密钥的P、Q极小可能相同 @@ -237,63 +411,535 @@ public RSA_Util(RSA_PEM pem) { /// 必须提供公钥指数 /// 私钥指数可以不提供,导出的PEM就只包含公钥 public RSA_Util(byte[] modulus, byte[] exponent, byte[] dOrNull) { - SetRSA__(new RSA_PEM(modulus, exponent, dOrNull)); + SetPEM__(new RSA_PEM(modulus, exponent, dOrNull)); } /// /// 本方法会先生成RSA_PEM再创建RSA:通过全量的PEM字段数据构造一个PEM,除了模数modulus和公钥指数exponent必须提供外,其他私钥指数信息要么全部提供,要么全部不提供(导出的PEM就只包含公钥) /// 注意:所有参数首字节如果是0,必须先去掉 /// public RSA_Util(byte[] modulus, byte[] exponent, byte[] d, byte[] p, byte[] q, byte[] dp, byte[] dq, byte[] inverseQ) { - SetRSA__(new RSA_PEM(modulus, exponent, d, p, q, dp, dq, inverseQ)); + SetPEM__(new RSA_PEM(modulus, exponent, d, p, q, dp, dq, inverseQ)); } - private RSACryptoServiceProvider rsaFramework; /// - /// 最底层的RSA对象,.NET Core下为实际的RSA实现类,.NET Framework下RSACryptoServiceProvider + /// 密钥位数 + /// + public int KeySize { get; private set; } + /// + /// 是否包含私钥 + /// + public bool HasPrivate { get; private set; } + + + /// + /// 获取最底层的RSA对象,.NET Core下为实际的RSA实现类,.NET Framework 4.5及以下RSACryptoServiceProvider,.NET Framework 4.6及以上RSACng;注意:IsUseBouncyCastle时将不会使用.NET的RSA /// public RSA RSAObject { get { - return rsaFramework != null ? rsaFramework : rsaCore; + return createRSA(); } } /// - /// 最底层的RSA对象是否是使用的rsaCore(RSA),否则将是使用的rsaFramework(RSACryptoServiceProvider) + /// 最底层的RSA对象是否是使用的.NET Core(RSA),否则将是使用的.NET Framework(4.5及以下RSACryptoServiceProvider、4.6及以上RSACng);注意:IsUseBouncyCastle时将不会使用.NET的RSA /// public bool RSAIsUseCore { get { - return rsaCore != null; + return IS_CORE && !(createRSA() is RSACryptoServiceProvider); + } + } + + + private void SetPEM__(RSA_PEM pem) { + PEM__ = pem; + KeySize = pem.KeySize; + HasPrivate = pem.HasPrivate; + } + private RSA_PEM PEM__; + private RSA createRSA() { + if (IS_CORE) return PEM__.GetRSA_ForCore(); + if (IS_CoreOr46) { //必须使用RSACng,不然新填充方式会抛出不支持 + return GetRSA_WindowsCng(PEM__); + } + return PEM__.GetRSA_ForWindows(); + } + + + + + + + + /******************底层加密解密调用*******************/ + + /// + /// 加密 + /// + private byte[] __Encrypt(string padding, byte[] data) { + string ctype = RSAPadding_Enc(padding), CType = ctype.ToUpper(); + + int blockLen = KeySize / 8; + if (CType.IndexOf("OAEP") != -1) { + //OAEP填充占用 2*hashLen+2 字节:https://www.rfc-editor.org/rfc/rfc8017.html#section-7.1.1 + int shaLen; string _; + __OaepParam(ctype, out _, out _, out shaLen); + int sub = 2 * shaLen / 8 + 2; + blockLen -= sub; + if (blockLen < 1) { + string min = "NaN"; if (sub > 0) min = (int)Math.Pow(2, Math.Ceiling(Math.Log(sub * 8) / Math.Log(2))) + ""; + throw new Exception("RSA[" + ctype + "][keySize=" + KeySize + "] " + T("密钥位数不能小于", "Key digits cannot be less than ") + min); + } + } else if (CType.IndexOf("NOPADDING") != -1) { + //NOOP 无填充,不够数量时会在开头给0 + } else { + //PKCS1填充占用11字节:https://www.rfc-editor.org/rfc/rfc8017.html#section-7.2.1 + blockLen -= 11; + } + + return __EncDec(true, ctype, data, blockLen); + } + /// + /// 解密 + /// + private byte[] __Decrypt(string padding, byte[] data) { + string ctype = RSAPadding_Enc(padding); + + int blockLen = KeySize / 8; + return __EncDec(false, ctype, data, blockLen); + } + static private Regex OAEP_Exp = new Regex("^RSA/(.+?)/OAEPWITHSHA(3-|-?512/)?[\\-/]?(\\d+)ANDMGF1PADDING$"); + static private void __OaepParam(string ctype, out string outType, out string outHash, out int outLen) { + string CType = ctype.ToUpper(); bool isMd5 = false; + if (CType.IndexOf("MD5") != -1) { + isMd5 = true; CType = CType.Replace("MD5", "SHA-128");//伪装成SHA简化逻辑 + } + Match m = OAEP_Exp.Match(CType); + if (!m.Success) { + throw new Exception(ctype + T("不在预定义列表内,无法识别出Hash算法", " is not in the predefined list, and the Hash algorithm cannot be recognized")); + } + int shaN = Convert.ToInt32(m.Groups[3].Value); + outLen = shaN == 1 ? 160 : shaN;//sha1 为 160位 + outType = "RSA/" + m.Groups[1].Value + "/OAEPPadding"; + + string hash; + if (isMd5) { + hash = "MD5"; + } else { + hash = "SHA-" + shaN; string m2 = m.Groups[2].Value; + if (m2 != null && m2.Length != 0) { + if (m2.IndexOf("512") != -1) { + hash = "SHA-512/" + shaN; + } else { + hash = "SHA3-" + shaN; + } + } } + outHash = hash; } - private void SetRSA__(RSA_PEM pem) { - if (UseCore) { - SetRSA__(pem.GetRSA_ForCore()); + private byte[] __EncDec(bool isEnc, string ctype, byte[] data, int blockLen) { + string ctype0 = ctype, CType = ctype.ToUpper(); + bool isNO = false, isOaep = false; + + string hash = null; int shaLen; + if (CType.IndexOf("OAEP") != -1) { + isOaep = true; + __OaepParam(ctype, out ctype, out hash, out shaLen); + } else if (CType.IndexOf("NOPADDING") != -1) { + isNO = true; + } + + Func process; + Action destory; + + if (rsaBouncyCastle != null) { + //使用BouncyCastle进行加密解密 +#if RSA_Util_BouncyCastle_CompileCode_1 + ICipherParameters key = Bc_Key(isEnc); + IAsymmetricBlockCipher cipher = new RsaEngine(); + if (isNO) { + //NOOP 无填充,不够数量时会在开头给0 + } else if (isOaep) { + IDigest hashObj = DigestUtilities.GetDigest(hash); + cipher = new OaepEncoding(cipher, hashObj, hashObj, null); + } else { + cipher = new Pkcs1Encoding(cipher); + } + cipher.Init(isEnc, key); + destory = () => { cipher = null; }; + process = (offset, len) => { + return cipher.ProcessBlock(data, offset, len); + }; +#else + object key = Bc_Key(isEnc); + object cipher = rsaBouncyCastle.GetType(BcName_RsaEngine).GetConstructor(new Type[0]).Invoke(new object[0]); + if (isNO) { + //NOOP 无填充,不够数量时会在开头给0 + } else if (isOaep) { + object hashObj = rsaBouncyCastle.GetType("Org.BouncyCastle.Security.DigestUtilities").GetMethod("GetDigest", new Type[] { typeof(string) }).Invoke(null, new object[] { hash }); + cipher = FindCtor(rsaBouncyCastle.GetType("Org.BouncyCastle.Crypto.Encodings.OaepEncoding"), new string[] { "iasym", "idigest", "idigest", "byte" }).Invoke(new object[] { cipher, hashObj, hashObj, null }); + } else { + cipher = FindCtor(rsaBouncyCastle.GetType("Org.BouncyCastle.Crypto.Encodings.Pkcs1Encoding"), new string[] { "iasym" }).Invoke(new object[] { cipher }); + } + FindFunc(cipher.GetType(), "Init", new string[] { "bool", "" }).Invoke(cipher, new object[] { isEnc, key }); + destory = () => { cipher = null; }; + var processBlock = FindFunc(cipher.GetType(), "ProcessBlock", new string[] { "byte", "int", "int" }); + process = (offset, len) => { + return (byte[])processBlock.Invoke(cipher, new object[] { data, offset, len }); + }; +#endif + } else if (IS_CoreOr46) { + //使用高版本RSA进行加密解密,4.6+ 或 Core + if (isNO) throw new Exception(NetNotSupportMsg(ctype0 + T("加密填充模式", " encryption padding mode"))); + string hashName = null; + if (isOaep) { + checkHashSupport(hash); + hashName = hash.Replace("SHA-", "SHA"); + } + +#if RSA_Util_NewNET_CompileCode_1 + RSAEncryptionPadding padding = RSAEncryptionPadding.Pkcs1; + if (isOaep) { + padding = RSAEncryptionPadding.CreateOaep(new HashAlgorithmName(hashName)); + } + RSA rsa = createRSA(); +#else + dynamic padding = Type_RSAEncryptionPadding.GetProperty("Pkcs1").GetValue(null); + if (isOaep) { + padding = FindFunc(Type_RSAEncryptionPadding, "CreateOaep", new string[] { "hashalg" }).Invoke(null, new object[] { Get_HashAlgorithmName(hashName) }); + } + dynamic rsa = createRSA(); +#endif + destory = () => { rsa.Dispose(); rsa = null; }; + process = (offset, len) => { + byte[] bytes = new byte[len]; + Array.Copy(data, offset, bytes, 0, len); + if (isEnc) return rsa.Encrypt(bytes, padding); + return rsa.Decrypt(bytes, padding); + }; } else { - SetRSA__(pem.GetRSA_ForWindows()); + //使用低版本RSA进行加密解密,4.6以下版本 + if (isNO) throw new Exception(NetNotSupportMsg(ctype0 + T("加密填充模式", " encryption padding mode"))); + if (isOaep && hash != "SHA-1") throw new Exception(NetLowVerSupportMsg(ctype0 + T("加密填充模式(只支持SHA-1)", " encryption padding mode (only SHA-1 is supported)"))); + + RSACryptoServiceProvider rsa = (RSACryptoServiceProvider)createRSA(); + destory = () => { rsa.Dispose(); rsa = null; }; + process = (offset, len) => { + byte[] bytes = new byte[len]; + Array.Copy(data, offset, bytes, 0, len); + if (isEnc) return rsa.Encrypt(bytes, isOaep); + return rsa.Decrypt(bytes, isOaep); + }; + } + + //数据分段进行加密解密 + using (var stream = new MemoryStream()) { + int start = 0; + while (start < data.Length) { + int len = blockLen; + if (start + len > data.Length) { + len = data.Length - start; + } + + byte[] val = process(start, len); + if (!isEnc && isNO) { + //没有填充时,去掉开头的0 + int idx = 0; + for (; idx < val.Length; idx++) { + if (val[idx] != 0) break; + } + byte[] val2 = new byte[val.Length - idx]; + Array.Copy(val, idx, val2, 0, val2.Length); + val = val2; + } + stream.Write(val, 0, val.Length); + start += len; + } + destory(); + return stream.ToArray(); } } - private void SetRSA__(RSA rsa) { - if (rsa is RSACryptoServiceProvider) { - rsaFramework = (RSACryptoServiceProvider)rsa; + + + + /******************底层签名验证调用*******************/ + + private byte[] __Sign(string hash, byte[] data) { + byte[] val; bool _; + __SignVerify(true, hash, data, null, out val, out _); + return val; + } + private bool __Verify(string hash, byte[] sign, byte[] data) { + byte[] _; bool val; + __SignVerify(false, hash, data, sign, out _, out val); + return val; + } + static private Regex HS_Exp = new Regex("^SHA(3-|-?512/)?[\\-/]?(\\d+)WITHRSA$"); + private void __SignVerify(bool isSign, string hashType, byte[] data, byte[] signData, out byte[] signVal, out bool verifyVal) { + string stype = RSAPadding_Sign(hashType), SType = stype.ToUpper(); + + bool isPss = SType.EndsWith("/PSS"); + if (isPss) { + SType = SType.Substring(0, stype.Length - 4); + } + bool isMd5 = SType.IndexOf("MD5") != -1; + if (isMd5) { + SType = SType.Replace("MD5", "SHA-128");//伪装成SHA简化逻辑 + } + + Match m = HS_Exp.Match(SType); + if (!m.Success) { + throw new Exception(stype + T("不在预定义列表内,无法识别出Hash算法", " is not in the predefined list, and the Hash algorithm cannot be recognized")); + } + int shaN = Convert.ToInt32(m.Groups[2].Value); + int shaLen = shaN == 1 ? 160 : shaN;//sha1 为 160位 + + string hash; + if (isMd5) { + hash = "MD5"; } else { - rsaCore = rsa; + hash = "SHA-" + shaN; string m2 = m.Groups[1].Value; + if (m2 != null && m2.Length != 0) { + if (m2.IndexOf("512") != -1) { + hash = "SHA-512/" + shaN; + } else { + hash = "SHA3-" + shaN; + } + } + } + + if (rsaBouncyCastle != null) { + //使用BouncyCastle进行签名验证 +#if RSA_Util_BouncyCastle_CompileCode_1 + ICipherParameters key = Bc_Key(!isSign); + IDigest hashObj = DigestUtilities.GetDigest(hash); + ISigner signer; + if (isPss) { + signer = new PssSigner(new RsaEngine(), hashObj, hashObj, shaLen / 8, 0xBC); + } else { + signer = new RsaDigestSigner(hashObj); + } + signer.Init(isSign, key); + signer.BlockUpdate(data, 0, data.Length); + if (isSign) { + signVal = signer.GenerateSignature(); + verifyVal = false; + } else { + signVal = null; + verifyVal = signer.VerifySignature(signData); + } +#else + object key = Bc_Key(!isSign); + object hashObj = rsaBouncyCastle.GetType("Org.BouncyCastle.Security.DigestUtilities").GetMethod("GetDigest", new Type[] { typeof(string) }).Invoke(null, new object[] { hash }); + object signer; + if (isPss) { + object cipher = rsaBouncyCastle.GetType(BcName_RsaEngine).GetConstructor(new Type[0]).Invoke(new object[0]); + signer = FindCtor(rsaBouncyCastle.GetType("Org.BouncyCastle.Crypto.Signers.PssSigner"), new string[] { "iasym", "idigest", "idigest", "int", "byte" }).Invoke(new object[] { cipher, hashObj, hashObj, shaLen / 8, (byte)0xBC }); + } else { + signer = FindCtor(rsaBouncyCastle.GetType("Org.BouncyCastle.Crypto.Signers.RsaDigestSigner"), new string[] { "idigest" }).Invoke(new object[] { hashObj }); + } + FindFunc(signer.GetType(), "Init", new string[] { "bool", "" }).Invoke(signer, new object[] { isSign, key }); + FindFunc(signer.GetType(), "BlockUpdate", new string[] { "byte", "int", "int" }).Invoke(signer, new object[] { data, 0, data.Length }); + if (isSign) { + signVal = (byte[])FindFunc(signer.GetType(), "GenerateSignature", new string[0]).Invoke(signer, new object[0]); + verifyVal = false; + } else { + signVal = null; + verifyVal = (bool)FindFunc(signer.GetType(), "VerifySignature", new string[] { "byte" }).Invoke(signer, new object[] { signData }); + } +#endif + return; } + + var hashName = hash.Replace("SHA-", "SHA"); + if (IS_CoreOr46) { + //使用高版本RSA进行加密解密,4.6+ 或 Core + checkHashSupport(hash); + +#if RSA_Util_NewNET_CompileCode_1 + RSA rsa = createRSA(); + var hashObj = new HashAlgorithmName(hashName); + var padding = RSASignaturePadding.Pkcs1; + if (isPss) { + padding = RSASignaturePadding.Pss; + } +#else + Type SP = typeof(RSA).Assembly.GetType(Space_Cryptography + "RSASignaturePadding"); + dynamic rsa = createRSA(); + dynamic hashObj = Get_HashAlgorithmName(hashName); + dynamic padding = SP.GetProperty("Pkcs1").GetValue(null); + if (isPss) { + padding = SP.GetProperty("Pss").GetValue(null); + } +#endif + if (isSign) { + signVal = rsa.SignData(data, hashObj, padding); + verifyVal = false; + } else { + signVal = null; + verifyVal = rsa.VerifyData(data, signData, hashObj, padding); + } + rsa.Dispose(); + return; + } else { + //使用低版本RSA进行加密解密,4.6以下版本 + if (isPss) throw new Exception(NetLowVerSupportMsg(T("所有PSS签名填充模式", "All PSS signature padding modes"))); + checkHashSupport(hash); + + RSACryptoServiceProvider rsa = (RSACryptoServiceProvider)createRSA(); + if (isSign) { + signVal = rsa.SignData(data, hashName); + verifyVal = false; + } else { + signVal = null; + verifyVal = rsa.VerifyData(data, hashName, signData); + } + rsa.Dispose(); + return; + } + } + + + + + /// + /// 反射查找出参数匹配的方法,方法名字为".ctor"时查找构造方法;参数名字为空匹配任意参数,小写前缀匹配 + /// + static public MethodBase FindFunc(Type type, string func, string[] paramNames) { + MethodBase[] arr; bool isCtor = false; + if (func == ".ctor") { + arr = type.GetConstructors(); isCtor = true; + } else { + arr = type.GetMethods(); + } + foreach (var m in arr) { + if (!isCtor && m.Name != func) continue; + var ps = m.GetParameters(); MethodBase find = null; + if (ps.Length == paramNames.Length) { + find = m; + for (int i = 0; i < ps.Length; i++) { + var n = paramNames[i]; + if (n.Length > 0 && !ps[i].ParameterType.Name.ToLower().StartsWith(n)) { + find = null; break; + } + } + } + if (find != null) return find; + } + throw new Exception(T(type.FullName + "中未找到方法", "Method not found in " + type.FullName + ": ") + func + "(" + string.Join(",", paramNames) + ")"); + } + static public ConstructorInfo FindCtor(Type type, string[] paramNames) { + return (ConstructorInfo)FindFunc(type, ".ctor", paramNames); } - //.NET Framework 兼容编译 -#if (NETCOREAPP || NETSTANDARD || NET) //https://docs.microsoft.com/zh-cn/dotnet/csharp/language-reference/preprocessor-directives - private RSA rsaCore; - static public readonly bool IS_CORE = true; - static public bool UseCore = true; + + + + /****************平台差异兼容处理****************/ + + /// + /// 使用BouncyCastle的RSA实现进行加密,提供BouncyCastle的程序集 + /// + static private Assembly rsaBouncyCastle; + /// + /// 是否强制使用BouncyCastle加密增强库进行RSA操作,为true时将不会使用.NET的RSA + /// + static public bool IsUseBouncyCastle { + get { return rsaBouncyCastle != null; } + } + /// + /// 强制使用BouncyCastle加密增强库进行RSA操作。只需在程序启动后调用一次即可,直接调用一下BouncyCastle里面的类,传入程序集:UseBouncyCastle(typeof(RsaEngine).Assembly),传入null取消使用 + /// + static public void UseBouncyCastle(Assembly bouncyCastleAssembly) { + if (bouncyCastleAssembly != null && bouncyCastleAssembly.GetType(BcName_RsaEngine) == null) { + throw new Exception(T("UseBouncyCastle方法必须传入BouncyCastle的Assembly", "The UseBouncyCastle method must pass in the Assembly of BouncyCastle")); + } + rsaBouncyCastle = bouncyCastleAssembly; + } + static private readonly string BcName_RsaEngine = "Org.BouncyCastle.Crypto.Engines.RsaEngine"; + private dynamic Bc_Key(bool usePub) { + var k = PEM__; + Func BigX = (bytes) => { + byte[] val = new byte[bytes.Length + 1]; + Array.Copy(bytes, 0, val, 1, bytes.Length); + return val; + }; +#if RSA_Util_BouncyCastle_CompileCode_1 + BcInt[] ks = new BcInt[] { new BcInt(BigX(k.Key_Modulus)), new BcInt(BigX(k.Key_Exponent)), new BcInt(BigX(k.Key_D)), new BcInt(BigX(k.Val_P)), new BcInt(BigX(k.Val_Q)), new BcInt(BigX(k.Val_DP)), new BcInt(BigX(k.Val_DQ)), new BcInt(BigX(k.Val_InverseQ)) }; + if (usePub) { + return new RsaKeyParameters(false, ks[0], ks[1]); + } + return new RsaPrivateCrtKeyParameters(ks[0], ks[1], ks[2], ks[3], ks[4], ks[5], ks[6], ks[7]); #else - private dynamic rsaCore = null; - static public readonly bool IS_CORE = false; - static public bool UseCore = false; - class RSAEncryptionPadding { static public object Pkcs1 = null; } - class RSASignaturePadding { static public object Pkcs1 = null; } - class HashAlgorithmName { public HashAlgorithmName(string _) { } } + var BInt = rsaBouncyCastle.GetType("Org.BouncyCastle.Math.BigInteger").GetConstructor(new Type[] { typeof(byte[]) }); + object[] ks = new object[] { BInt.Invoke(new object[] { BigX(k.Key_Modulus) }), BInt.Invoke(new object[] { BigX(k.Key_Exponent) }), BInt.Invoke(new object[] { BigX(k.Key_D) }), BInt.Invoke(new object[] { BigX(k.Val_P) }), BInt.Invoke(new object[] { BigX(k.Val_Q) }), BInt.Invoke(new object[] { BigX(k.Val_DP) }), BInt.Invoke(new object[] { BigX(k.Val_DQ) }), BInt.Invoke(new object[] { BigX(k.Val_InverseQ) }) }; + if (usePub) { + return FindCtor(rsaBouncyCastle.GetType("Org.BouncyCastle.Crypto.Parameters.RsaKeyParameters"), new string[] { "bool", "big", "big" }).Invoke(new object[] { false, ks[0], ks[1] }); + } + return FindCtor(rsaBouncyCastle.GetType("Org.BouncyCastle.Crypto.Parameters.RsaPrivateCrtKeyParameters"), new string[] { "big", "big", "big", "big", "big", "big", "big", "big" }).Invoke(ks); #endif + } + + + /// + /// 当前运行环境是否为.NET Core,false为.NET Framework + /// + static public bool IS_CORE { + get { + if (is__core == null) { + is__core = new bool[] { !typeof(RSA).Assembly.ToString().ToLower().Contains("mscorlib") }; + } + return is__core[0]; + } + } + static private bool[] is__core; + /// + /// 当前运行环境是否是.NET Framework 4.6以上或.NET Core + /// + static public bool IS_CoreOr46 { + get { + if (IS_CORE) return true; + if (is__core_or_46 == null) { + Type type = Type_RSAEncryptionPadding; + is__core_or_46 = new int[] { type != null ? 1 : -1 }; + } + return is__core_or_46[0] > 0; + } + } + static private int[] is__core_or_46; + /// + /// .NET Framework 下测试,可以指定以高版本运行还是低版本运行,方便测试,取值:0重设为默认,1高版本,-1低版本 + /// + static public void IS_CoreOr46_Test_Set(int val) { + is__core_or_46 = val == 0 ? null : new int[] { val }; + } + + + + //.NET Framework 低版本兼容,4.6以上或Core才有的类 + /// + /// 4.6以上使用RSACng,RSACng支持的部分填充方式如果换成RSACryptoServiceProvider会抛不支持的异常 + /// + static private RSA GetRSA_WindowsCng(RSA_PEM pem) { + //.NET Core里面没有RSACng,兼容编译 + //统一反射进行获取,Framework全在System.Core.dll里面 + var type = typeof(ECDsa).Assembly.GetType(Space_Cryptography + "RSACng"); + if (type == null) return null; //Core + var rsa = (RSA)type.GetConstructor(new Type[0]).Invoke(new object[0]); + pem.GetRSA__ImportParameters(rsa); + return rsa; + } + static private readonly string Space_Cryptography = "System.Security.Cryptography."; + static private Type Type_RSAEncryptionPadding { + get { + return typeof(RSA).Assembly.GetType(Space_Cryptography + "RSAEncryptionPadding"); + } + } + static private dynamic Get_HashAlgorithmName(string hash) { + return typeof(RSA).Assembly.GetType(Space_Cryptography + "HashAlgorithmName").GetConstructor(new Type[] { typeof(string) }).Invoke(new object[] { hash }); + } + + } } diff --git a/Test-Build-Run.bat b/Test-Build-Run.bat new file mode 100644 index 0000000..90857ce --- /dev/null +++ b/Test-Build-Run.bat @@ -0,0 +1,189 @@ +@echo off +::[zh_CN] ��Windowsϵͳ��˫����������ű��ļ����Զ����.cs�ļ���������С����Ȱ�װ��.NET Framework 4.5+������.NET Core SDK��֧��.NET Core 2.0�����ϰ汾��.NET 5+�� +::�������BouncyCastle������ǿ�⣨BouncyCastle.xxxx.dll������ֱ�Ӹ��ƶ�Ӧ�汾��dll�ļ�����Դ���Ŀ¼���������к󼴿ɻ��ȫ������ǩ��ģʽ֧�� + +::[en_US] Double-click to run this script file in the Windows system, and automatically complete the compilation and operation of the .cs file. Need to install .NET Framework 4.5+ or .NET Core SDK (support .NET Core 2.0 and above, .NET 5+) +::If you have BouncyCastle encryption enhancement library (BouncyCastle.xxxx.dll), please directly copy the corresponding version of the dll file to the root directory of this source code. After compiling and running, you can get all encryption signature mode support + + +cls +::chcp 437 +set isZh=0 +ver | find "�汾%qjkTTT%" > nul && set isZh=1 +goto Run +:echo2 + if "%isZh%"=="1" echo %~1 + if "%isZh%"=="0" echo %~2 + goto:eof + +:Run +cd /d %~dp0 +call:echo2 "��ʾ���ԣ��������� %cd%" "Language: English %cd%" +echo. +call:echo2 "ѡ���������ģʽ���������ţ� " "Select the compilation and running mode, please enter the number:" +call:echo2 " 1. ʹ��.NET Framework���б��루֧��.NET Framework 4.5�����ϰ汾�� " " 1. Use .NET Framework to compile (support .NET Framework 4.5 and above)" +call:echo2 " 2. ʹ��.NET Core���б��루֧��.NET Core 2.0�����ϰ汾��.NET 5+�� " " 2. Use .NET Core to compile (support .NET Core 2.0 and above, .NET 5+)" +call:echo2 " 3. �˳� " " 3. Exit" + +set step=&set /p step=^> + if "%step%"=="1" goto RunFramework + if "%step%"=="2" goto RunDotnet + if "%step%"=="3" goto End + call:echo2 "�����Ч�����������룡 " "The number is invalid, please re-enter!" + goto Run + + +:findDLL + set dllName=%~1 + set dllPath=target\%~1 + if not exist %dllPath% set dllPath= + if "%dllPath%"=="" goto dllPath_End + call:echo2 "��⵽�����ɵ�dll��%dllPath%���Ƿ�ʹ�ô�dll������ԣ�(Y/N) N " "Generated dll detected: %dllPath%, do you want to use this dll to participate in the test? (Y/N) N" + set step=&set /p step=^> + if /i not "%step%"=="Y" set dllPath= + if not "%dllPath%"=="" ( + call:echo2 "dll������ԣ�%dllPath%" "dll participates in the test: %dllPath%" + echo. + ) + :dllPath_End + goto:eof + + +:RunDotnet +call:findDLL "RSA-CSharp.NET-Standard.dll" + +::.NET CLI telemetry https://learn.microsoft.com/en-us/dotnet/core/tools/telemetry +set DOTNET_CLI_TELEMETRY_OPTOUT=1 + +call:echo2 "���ڶ�ȡ.NET Core�汾�� " "Reading .NET Core Version:" + +dotnet --version +if errorlevel 1 ( + echo. + call:echo2 "��Ҫ��װ.NET Core SDK [֧��.NET Core 2.0�����ϰ汾��.NET 5+] ����ʹ��.NET Coreģʽ��������.cs�ļ������߳���ѡ��.NET Frameworkģʽ���б��롣���Ե� https://dotnet.microsoft.com/zh-cn/download/dotnet ���ذ�װ.NET Core SDK " "You need to install .NET Core SDK [support .NET Core 2.0 and above, .NET 5+] to compile and run .cs files using .NET Core mode, or try to select .NET Framework mode for compilation. You can go to https://dotnet.microsoft.com/en-us/download/dotnet to download and install the .NET Core SDK" + goto Pause +) + + +set rootDir=rsaTest +echo. +call:echo2 "���ڴ�.NET Core��Ŀ%rootDir%..." "Creating .NET Core project %rootDir%..." +if not exist %rootDir% ( + md %rootDir% +) else ( + del %rootDir%\* /Q > nul +) + +cd %rootDir% +dotnet new console +if errorlevel 1 goto if_dncE +if not exist %rootDir%*proj goto if_dncE + goto dncE_if + :if_dncE + echo. + call:echo2 "������Ŀ����ִ��ʧ�� " "The command to create a project failed to execute" + goto Pause + :dncE_if +echo. + +setlocal enabledelayedexpansion +for /f "delims=" %%f in ('dir /b %rootDir%*proj') do ( + for /f "delims=" %%v in (%%f) do ( + set a=%%v + set "a=!a:= $(DefineConstants);RSA_BUILD__NET_CORE!" + set "a=!a:Nullable>enable=Nullable>disable!" + if not "%dllPath%"=="" ( + set "a=!a:=%dllName%True!" + ) + echo !a!>>tmp.txt + ) + move tmp.txt %%f > nul + call:echo2 "���޸�proj��Ŀ�����ļ���%%f��������RSA_BUILD__NET_CORE����������� " "Modified proj project configuration file: %%f, enabled RSA_BUILD__NET_CORE conditional compilation symbol" + echo. +) + +cd .. +if "%dllPath%"=="" ( + xcopy *.cs %rootDir% /Y > nul +) else ( + xcopy Program.cs %rootDir% /Y > nul + xcopy %dllPath% %rootDir% /Y > nul +) +if exist *.dll ( + xcopy *.dll %rootDir% /Y > nul +) +cd %rootDir% + + + +echo. +call:echo2 "���ڱ���.NET Core��Ŀ%rootDir%..." "Compiling .NET Core project %rootDir%..." +echo. +dotnet run -cmd=1 -zh=%isZh% +goto Pause + + + + + + +:RunFramework +call:findDLL "RSA-CSharp.NET-Framework.dll" +if not exist target\RSA-CSharp.NET-Framework.dll ( + call:findDLL "RSA-CSharp.NET-Standard.dll" +) + +cd /d C:\Windows\Microsoft.NET\Framework\v4.* +set FwDir=%cd%\ +::set FwDir=C:\Windows\Microsoft.NET\Framework\xxxx\ +echo .NET Framework Path: %FwDir% + +call:echo2 "���ڶ�ȡ.NET Framework�汾�� " "Reading .NET Framework Version:" +%FwDir%MSBuild /ver +if errorlevel 1 ( + echo. + call:echo2 "��Ҫ��װ.NET Framework 4.5�����ϰ汾����ʹ��.NET Frameworkģʽ��������.cs�ļ������߳���ѡ��.NET Coreģʽ���б��롣���Ե� https://dotnet.microsoft.com/zh-cn/download/dotnet-framework ���ذ�װ.NET Framework " "You need to install .NET Framework 4.5 or above to compile and run .cs files using .NET Framework mode, or try to select .NET Core mode for compilation. You can go to https://dotnet.microsoft.com/en-us/download/dotnet-framework to download and install .NET Framework" + goto Pause +) +cd /d %~dp0 + + +set rootDir=rsaTestFw +echo. +call:echo2 "���ڴ���.NET Framework��Ŀ%rootDir%..." "Creating .NET Framework project %rootDir%..." +if not exist %rootDir% ( + md %rootDir% +) else ( + del %rootDir%\* /Q > nul +) + +if "%dllPath%"=="" ( + xcopy *.cs %rootDir% /Y > nul +) else ( + xcopy Program.cs %rootDir% /Y > nul + xcopy %dllPath% %rootDir% /Y > nul +) +if exist *.dll ( + xcopy *.dll %rootDir% /Y > nul +) +cd %rootDir% + +echo. +call:echo2 "���ڱ���.NET Framework��Ŀ%rootDir%..." "Compiling .NET Framework project %rootDir%..." +echo. +::https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/compiler-options/ +set rd= +if not "%dllPath%"=="" set rd=/r:"%dllName%" +%FwDir%csc /t:exe /r:"%FwDir%System.Numerics.dll" %rd% /out:%rootDir%.exe *.cs +if errorlevel 1 ( + echo. + call:echo2 "��Ŀ%rootDir%����ʧ�� " "Compilation failed for project %rootDir%" + goto Pause +) + +%rootDir%.exe -cmd=1 -zh=%isZh% + +:Pause +pause +goto Run +:End \ No newline at end of file diff --git a/Test-Build-Run.sh b/Test-Build-Run.sh new file mode 100644 index 0000000..42f5c3d --- /dev/null +++ b/Test-Build-Run.sh @@ -0,0 +1,104 @@ +#!/usr/bin/env bash +#[zh_CN] 在Linux、macOS系统终端中运行这个脚本文件,自动完成.cs文件编译和运行。需先安装了.NET Core SDK,支持.NET Core 2.0及以上版本(.NET 5+) +#如果你有BouncyCastle加密增强库(BouncyCastle.xxxx.dll),请直接复制对应版本的dll文件到源码根目录,编译运行后即可获得全部加密签名模式支持 + +#[en_US] Run this script file in the Linux and macOS system terminals to automatically complete the compilation and operation of the .cs file. .NET Core SDK needs to be installed first, support .NET Core 2.0 and above (.NET 5+) +#If you have BouncyCastle encryption enhancement library (BouncyCastle.xxxx.dll), please directly copy the corresponding version of the dll file to the root directory of this source code. After compiling and running, you can get all encryption signature mode support + + +clear + +isZh=0 +if [ $(echo ${LANG/_/-} | grep -Ei "\\b(zh|cn)\\b") ]; then isZh=1; fi + +function echo2(){ + if [ $isZh == 1 ]; then echo $1; + else echo $2; fi +} +cd `dirname $0` +echo2 "显示语言:简体中文 `pwd`" "Language: English `pwd`" +function err(){ + if [ $isZh == 1 ]; then echo -e "\e[31m$1\e[0m"; + else echo -e "\e[31m$2\e[0m"; fi +} +function exit2(){ + if [ $isZh == 1 ]; then read -n1 -rp "请按任意键退出..." key; + else read -n1 -rp "Press any key to exit..."; fi + exit +} + + +dllName="RSA-CSharp.NET-Standard.dll" +dllPath="target/$dllName" +if [ ! -e $dllPath ]; then dllPath=""; fi +if [ "$dllPath" != "" ]; then + echo2 "检测到已生成的dll:${dllPath},是否使用此dll参与测试?(Y/N) N" "Generated dll detected: ${dllPath}, do you want to use this dll to participate in the test? (Y/N) N" + read -rp "> " step + if [ "${step^^}" != "Y" ]; then dllPath=""; fi + if [ "$dllPath" != "" ]; then + echo2 "dll参与测试:$dllPath" "dll participates in the test: $dllPath" + echo + fi +fi + + +#.NET CLI telemetry https://learn.microsoft.com/en-us/dotnet/core/tools/telemetry +DOTNET_CLI_TELEMETRY_OPTOUT=1 + +echo2 "正在读取.NET Core版本:" "Reading .NET Core Version:" + +dotnet --version +[ ! $? -eq 0 ] && { + echo + err "需要安装.NET Core SDK [支持.NET Core 2.0及以上版本,.NET 5+] 才能使用本脚本编译运行.cs文件,可以到 https://learn.microsoft.com/zh-cn/dotnet/core/install/ 下载安装SDK"\ + "You need to install .NET Core SDK [support .NET Core 2.0 and above, .NET 5+] to use this script to compile and run the .cs file. You can download and install the SDK at https://learn.microsoft.com/en-us/dotnet/core/install/"; + exit2; +} + + +rootDir=rsaTest +echo +echo2 "正在创.NET Core项目${rootDir}..." "Creating .NET Core project ${rootDir}..." +if [ ! -e $rootDir ]; then + mkdir -p $rootDir +else + rm ${rootDir}/* > /dev/null 2>&1 +fi + +cd $rootDir +dotnet new console +[ ! $? -eq 0 -o ! -e $rootDir*proj ] && { + echo + err "创建项目命令执行失败" "The command to create a project failed to execute" + exit2; +} +echo + +projFile=`ls $rootDir*proj`; +sed -i -e 's// \$(DefineConstants);RSA_BUILD__NET_CORE<\/DefineConstants>/g' $projFile; +sed -i -e 's/Nullable>enable/Nullable>disable/g' $projFile +if [ "$dllPath" != "" ]; then + sed -i -e 's/<\/Project>/'"${dllName}"'<\/HintPath>True<\/Private><\/Reference><\/ItemGroup><\/Project>/g' $projFile +fi +echo2 "已修改proj项目配置文件:${projFile},已启用RSA_BUILD__NET_CORE条件编译符号" "Modified proj project configuration file: ${projFile}, enabled RSA_BUILD__NET_CORE conditional compilation symbol" +echo + +cd .. +if [ "$dllPath" == "" ]; then + cp *.cs $rootDir > /dev/null +else + cp Program.cs $rootDir > /dev/null + cp $dllPath $rootDir > /dev/null +fi +if [ -e *.dll ]; then + cp *.dll $rootDir > /dev/null +fi +cd $rootDir + + + +echo2 "正在编译.NET Core项目${rootDir}..." "Compiling .NET Core project ${rootDir}..." +echo +dotnet run -cmd=1 -zh=${isZh} + +exit2; diff --git a/images/1-en.png b/images/1-en.png new file mode 100644 index 0000000000000000000000000000000000000000..f3565fc2611417c6584fd3d7c095b9a461037918 GIT binary patch literal 11352 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 literal 0 HcmV?d00001 diff --git a/images/1.png b/images/1.png index dc7c468c5573222fd9b75bec92879a95825b7daf..aa54014b40f6a229c177ded2ba319998144316ab 100644 GIT binary patch literal 12105 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 diff --git a/images/2.png b/images/2.png deleted file mode 100644 index bd16a208e5e340e3747cf97a342dd4dd9e45db51..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 54348 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 Fe*v2~W2*oF diff --git a/scripts/Create-dll.bat b/scripts/Create-dll.bat new file mode 100644 index 0000000..9f24b26 --- /dev/null +++ b/scripts/Create-dll.bat @@ -0,0 +1,254 @@ +@echo off +::[zh_CN] ��Windowsϵͳ��˫����������ű��ļ����Զ����.cs�ļ���������ͨ�õ�dll��exe�����Ȱ�װ��.NET Framework 4.5+����.NET Core SDK��֧��.NET Core 2.0�����ϰ汾��.NET 5+�� +::[en_US] Double-click to run this script file in the Windows system, and automatically complete the compilation of the .cs file to generate common dll and exe. Need to install .NET Framework 4.5+, and .NET Core SDK (support .NET Core 2.0 and above, .NET 5+) + + +::.NET Core --framework: https://learn.microsoft.com/en-us/dotnet/standard/frameworks +set CoreTarget=netstandard2.0 +::TargetFrameworkVersion +set FrameworkTarget=v4.5 + + +cls +::chcp 437 +set isZh=0 +ver | find "�汾%qjkTTT%" > nul && set isZh=1 +goto Run +:echo2 + if "%isZh%"=="1" echo %~1 + if "%isZh%"=="0" echo %~2 + goto:eof + +:Run +cd /d %~dp0 +cd ..\ +call:echo2 "��ʾ���ԣ��������� %cd%" "Language: English %cd%" +echo. +call:echo2 "�������ţ� " "Please enter the number:" +call:echo2 " 1. ��������dll�İ汾�ţ���ǰ��%dllVer%�� " " 1. Configure the version number of the generated dll (currently: %dllVer%)" +call:echo2 " 2. ʹ��.NET Core���б�������.NET Standard 2.0��dll��֧��.NET Core 2.0�����ϰ汾��.NET 5+��������.NET Framework 4.6.2�����ϰ汾 " " 2. Use .NET Core to compile and generate .NET Standard 2.0 dll (support .NET Core 2.0 and above, .NET 5+), compatible with .NET Framework 4.6.2 and above" +call:echo2 " 3. ʹ��.NET Framework 4.5���б�������Framework���õ�dll " " 3. Use .NET Framework 4.5 to compile and generate dll available to Framework" +call:echo2 " 4. ʹ��.NET Framework 4.5���б�������exe " " 4. Use .NET Framework 4.5 to compile and generate exe" +call:echo2 " 5. �˳� " " 5. Exit" + +set step=&set /p step=^> + if "%step%"=="1" goto SetVer + if "%step%"=="5" goto End + + if "%dllVer%"=="" ( + call:echo2 "�������ð汾�ţ� " "Please configure the version number first!" + goto Run + ) + if "%step%"=="2" goto RunDotnet + + set FrameworkType=dll + if "%step%"=="3" goto RunFramework + if "%step%"=="4" ( + set FrameworkType=exe + goto RunFramework + ) + + call:echo2 "�����Ч�����������룡 " "The number is invalid, please re-enter!" + goto Run + +:SetVer + call:echo2 "����������dll�İ汾�ţ� " "Please enter the version number of the generated dll:" + set dllVer=&set /p dllVer=^> + goto Run + +:RunDotnet +::.NET CLI telemetry https://learn.microsoft.com/en-us/dotnet/core/tools/telemetry +set DOTNET_CLI_TELEMETRY_OPTOUT=1 + +set projName=RSA-CSharp.NET-Standard +set rootDir=target\%projName% +echo. +call:echo2 "���ڴ�.NET Core��Ŀ%rootDir%..." "Creating .NET Core project %rootDir%..." +if not exist %rootDir% ( + md %rootDir% +) else ( + del %rootDir%\* /Q > nul +) + +cd %rootDir% +dotnet new classlib -f %CoreTarget% +if errorlevel 1 goto if_dncE +if not exist %projName%*proj goto if_dncE + goto dncE_if + :if_dncE + echo. + call:echo2 "������Ŀ����ִ��ʧ�ܣ������Ƿ�װ��.NET Core 2.0�����ϰ汾��SDK��.NET 5+�� " "The execution of the command to create a project failed. Please check whether the SDK of .NET Core 2.0 and above (.NET 5+) is installed" + goto Pause + :dncE_if +echo. + + +set prop="%dllVer% %date:~,10%, MIT, Copyright %date:~,4% xiangyuecn xiangyuecn %CoreTarget%, https://github.com/xiangyuecn/RSA-csharp RSA-csharp, %CoreTarget%, %date:~,10%" + +setlocal enabledelayedexpansion +for /f "delims=" %%f in ('dir /b %projName%*proj') do ( + for /f "delims=" %%v in (%%f) do ( + set a=%%v + set "a=!a:= $(DefineConstants);RSA_BUILD__NET_CORE %prop:~1,-1%!" + set "a=!a:Nullable>enable=Nullable>disable!" + echo !a!>>tmp.txt + ) + move tmp.txt %%f > nul + call:echo2 "���޸�proj��Ŀ�����ļ���%%f��������RSA_BUILD__NET_CORE����������� " "Modified proj project configuration file: %%f, enabled RSA_BUILD__NET_CORE conditional compilation symbol" + echo. +) + +del *.cs /Q > nul +xcopy ..\..\RSA_PEM.cs /Y > nul +xcopy ..\..\RSA_Util.cs /Y > nul + + + +echo. +call:echo2 "���ڱ���.NET Core��Ŀ%rootDir%..." "Compiling .NET Core project %rootDir%..." +echo. +dotnet build -c Release +if errorlevel 1 ( + echo. + call:echo2 "����dllʧ�� " "Failed to generate dll" + goto Pause +) + +cd ..\.. +set dllRaw=%rootDir%\bin\Release\%CoreTarget%\%projName%.dll +set dllPath=target\%projName%.dll +del %dllPath% /Q > nul 2>&1 +if exist %dllPath% ( + echo. + call:echo2 "�޷�ɾ�����ļ���%dllPath% " "Unable to delete old file: %dllPath%" + goto Pause +) +xcopy %dllRaw% target /Y + +if not exist %dllPath% ( + echo. + call:echo2 "δ��λ�����ɵ�dll�ļ�·�����뵽%rootDir%\binѰ�����ɵ�dll�ļ� " "The generated dll file path is not located, please go to %rootDir%\bin to find the generated dll file" + goto Pause +) +echo. +call:echo2 "������dll���ļ���Դ���Ŀ¼��%dllPath%�� " "The dll has been generated, and the file is in the root directory of the source code: %dllPath%." +echo. +goto Pause + + + +:RunFramework +cd /d C:\Windows\Microsoft.NET\Framework\v4.* +set FwDir=%cd%\ +::set FwDir=C:\Windows\Microsoft.NET\Framework\xxxx\ +echo .NET Framework Path: %FwDir% + +call:echo2 "���ڶ�ȡ.NET Framework�汾�� " "Reading .NET Framework Version:" +%FwDir%MSBuild /ver +if errorlevel 1 ( + echo. + call:echo2 "��Ҫ��װ.NET Framework 4.5�����ϰ汾����ʹ��.NET Frameworkģʽ��������.cs�ļ������߳���ѡ��.NET Coreģʽ���б��롣���Ե� https://dotnet.microsoft.com/zh-cn/download/dotnet-framework ���ذ�װ.NET Framework " "You need to install .NET Framework 4.5 or above to compile and run .cs files using .NET Framework mode, or try to select .NET Core mode for compilation. You can go to https://dotnet.microsoft.com/en-us/download/dotnet-framework to download and install .NET Framework" + goto Pause +) +cd /d %~dp0 +cd ..\ + + +if "%FrameworkType%"=="exe" ( + set projName=RSA-CSharp.NET-Framework-v%dllVer%-Test +) else ( + set projName=RSA-CSharp.NET-Framework +) +set rootDir=target\%projName% +echo. +call:echo2 "���ڴ���.NET Framework��Ŀ%rootDir%..." "Creating .NET Framework project %rootDir%..." +if not exist %rootDir% ( + md %rootDir% +) else ( + del %rootDir%\* /Q > nul +) +cd %rootDir% + +xcopy ..\..\RSA_PEM.cs /Y > nul +xcopy ..\..\RSA_Util.cs /Y > nul +if "%FrameworkType%"=="exe" xcopy ..\..\Program.cs /Y > nul +call:createFrameworkProj + +echo. +call:echo2 "���ڱ���.NET Framework��Ŀ%rootDir%..." "Compiling .NET Framework project %rootDir%..." +echo. +::https://learn.microsoft.com/en-us/visualstudio/msbuild/msbuild-command-line-reference +%FwDir%MSBuild build.proj -property:Configuration=Release +if errorlevel 1 ( + echo. + call:echo2 "��Ŀ%rootDir%����ʧ�� " "Compilation failed for project %rootDir%" + goto Pause +) + +cd ..\.. +set fileExt=%FrameworkType% +set dllRaw=%rootDir%\bin\Release\%projName%.%fileExt% +set dllPath=target\%projName%.%fileExt% +del %dllPath% /Q > nul 2>&1 +if exist %dllPath% ( + echo. + call:echo2 "�޷�ɾ�����ļ���%dllPath% " "Unable to delete old file: %dllPath%" + goto Pause +) +xcopy %dllRaw% target /Y + +if not exist %dllPath% ( + echo. + call:echo2 "δ��λ�����ɵ�%fileExt%�ļ�·�����뵽%rootDir%\binѰ�����ɵ�%fileExt%�ļ� " "The generated %fileExt% file path is not located, please go to %rootDir%\bin to find the generated %fileExt% file" + goto Pause +) +echo. +call:echo2 "������%fileExt%���ļ���Դ���Ŀ¼��%dllPath%�� " "The %fileExt% has been generated, and the file is in the root directory of the source code: %dllPath%." +echo. +goto Pause + + + + +:createFrameworkProj + echo using System.Reflection;>AssemblyInfo.cs + echo using System.Runtime.CompilerServices;>>AssemblyInfo.cs + echo using System.Runtime.InteropServices;>>AssemblyInfo.cs + echo [assembly: AssemblyTitle("%projName%")]>>AssemblyInfo.cs + echo [assembly: AssemblyCopyright("%date:~,10%, MIT, Copyright %date:~,4% xiangyuecn")]>>AssemblyInfo.cs + echo [assembly: AssemblyCompany("xiangyuecn")]>>AssemblyInfo.cs + echo [assembly: AssemblyProduct(".NET Framework %FrameworkTarget%, https://github.com/xiangyuecn/RSA-csharp")]>>AssemblyInfo.cs + echo [assembly: AssemblyDescription("RSA-csharp, .NET Framework %FrameworkTarget%, %date:~,10%")]>>AssemblyInfo.cs + echo [assembly: AssemblyVersion("%dllVer%.0.0")]>>AssemblyInfo.cs + echo [assembly: AssemblyFileVersion("%dllVer%.0.0")]>>AssemblyInfo.cs + + + echo ^>build.proj + echo ^>>build.proj + echo ^%FrameworkTarget%^>>build.proj + if "%FrameworkType%"=="exe" ( + echo ^Exe^>>build.proj + ) else ( + echo ^Library^>>build.proj + ) + echo ^bin\Release\^>>build.proj + echo ^%projName%^>>build.proj + echo ^>>build.proj + + echo ^>>build.proj + echo ^>>build.proj + echo ^>>build.proj + echo ^>>build.proj + echo ^>>build.proj + + echo ^>>build.proj + echo ^>>build.proj + echo ^>>build.proj + echo ^>>build.proj + goto:eof + + +:Pause +pause +goto Run +:End \ No newline at end of file diff --git a/scripts/Create-dll.sh b/scripts/Create-dll.sh new file mode 100644 index 0000000..a6d9180 --- /dev/null +++ b/scripts/Create-dll.sh @@ -0,0 +1,105 @@ +#!/usr/bin/env bash +#[zh_CN] 在Linux、macOS系统终端中运行这个脚本文件,自动完成.cs文件编译生成.NET Standard 2.0的dll。需先安装了.NET Core SDK(支持.NET Core 2.0及以上版本,.NET 5+) +#[en_US] Run this script file in the terminal of Linux and macOS system to automatically compile the .cs file and generate the dll of .NET Standard 2.0. The .NET Core SDK needs to be installed first (supports .NET Core 2.0 and above, .NET 5+) + + +#.NET Core --framework: https://learn.microsoft.com/en-us/dotnet/standard/frameworks +CoreTargetSDK=netstandard2.0 + + +clear + +isZh=0 +if [ $(echo ${LANG/_/-} | grep -Ei "\\b(zh|cn)\\b") ]; then isZh=1; fi + +function echo2(){ + if [ $isZh == 1 ]; then echo $1; + else echo $2; fi +} +cd `dirname $0` +cd ../ +echo2 "显示语言:简体中文 `pwd`" "Language: English `pwd`" +function err(){ + if [ $isZh == 1 ]; then echo -e "\e[31m$1\e[0m"; + else echo -e "\e[31m$2\e[0m"; fi +} +function exit2(){ + if [ $isZh == 1 ]; then read -n1 -rp "请按任意键退出..." key; + else read -n1 -rp "Press any key to exit..."; fi + exit +} + + +echo2 "本脚本默认生成.NET Standard 2.0的dll,支持.NET Core 2.0及以上版本(.NET 5+),兼容.NET Framework 4.6.2及以上版本(请使用同名的bat脚本来创建Framework专用的dll)。" "This script generates .NET Standard 2.0 dll by default, supports .NET Core 2.0 and above (.NET 5+), and is compatible with .NET Framework 4.6.2 and above (Please use the bat script with the same name to create a Framework-specific dll)." +echo +echo2 "请输入需要生成的dll版本号:" "Please enter the version number of the dll that needs to be generated:" +read -rp "> " dllVer + + +#.NET CLI telemetry https://learn.microsoft.com/en-us/dotnet/core/tools/telemetry +DOTNET_CLI_TELEMETRY_OPTOUT=1 + + +projName=RSA-CSharp.NET-Standard +rootDir=target/$projName + +echo2 "正在创.NET Core项目${rootDir}..." "Creating .NET Core project ${rootDir}..." +if [ ! -e $rootDir ]; then + mkdir -p $rootDir +else + rm ${rootDir}/* > /dev/null 2>&1 +fi + +cd $rootDir +dotnet new classlib -f $CoreTargetSDK +[ ! $? -eq 0 -o ! -e $projName*proj ] && { + echo + err "创建项目命令执行失败,请检查是否安装了.NET Core 2.0及以上版本的SDK(.NET 5+)" "The execution of the command to create a project failed. Please check whether the SDK of .NET Core 2.0 and above (.NET 5+) is installed" + exit2; +} +echo + +prop="${dllVer}<\\/Version> `date '+%Y-%m-%d'`, MIT, Copyright `date '+%Y'` xiangyuecn<\\/Copyright> xiangyuecn<\\/Authors> ${CoreTargetSDK}, https:\\/\\/github.com\\/xiangyuecn\\/RSA-csharp<\\/Product> RSA-csharp, ${CoreTargetSDK}, `date '+%Y-%m-%d'`<\\/Description>" + +projFile=`ls $projName*proj`; +sed -i -e 's// \$(DefineConstants);RSA_BUILD__NET_CORE<\/DefineConstants> '"${prop}"'/g' $projFile; +sed -i -e 's/Nullable>enable/Nullable>disable/g' $projFile +echo2 "已修改proj项目配置文件:${projFile},已启用RSA_BUILD__NET_CORE条件编译符号" "Modified proj project configuration file: ${projFile}, enabled RSA_BUILD__NET_CORE conditional compilation symbol" +echo + +rm *.cs > /dev/null 2>&1 +cp ../../RSA_PEM.cs ./ > /dev/null +cp ../../RSA_Util.cs ./ > /dev/null + + + +echo2 "正在编译.NET Core项目${rootDir}..." "Compiling .NET Core project ${rootDir}..." +echo +dotnet build -c Release +[ ! $? -eq 0 ] && { + echo + err "生成dll失败" "Failed to generate dll" + exit2; +} + +cd ../../ +dllRaw=${rootDir}/bin/Release/${CoreTargetSDK}/${projName}.dll +dllPath=target/${projName}.dll +rm $dllPath > /dev/null 2>&1 +[ -e $dllPath ] && { + echo + err "无法删除旧文件:${dllPath}" "Unable to delete old file: ${dllPath}" + exit2; +} +cp $dllRaw target + +[ ! -e $dllPath ] && { + echo + err "未定位到生成的dll文件路径,请到${rootDir}/bin寻找生成的dll文件 " "The generated dll file path is not located, please go to ${rootDir}/bin to find the generated dll file" + exit2; +} +echo +echo2 "已生成dll,文件在源码根目录:${dllPath}。 " "The dll has been generated, and the file is in the root directory of the source code: ${dllPath}." +echo + +exit2; diff --git a/vs.csproj b/vs.csproj deleted file mode 100644 index f7dac32..0000000 --- a/vs.csproj +++ /dev/null @@ -1,62 +0,0 @@ - - - - - Debug - AnyCPU - {2DAD0BFE-E417-4AD8-8C44-7578F6758947} - Exe - Properties - RSA - RSA - v4.5 - 512 - - - AnyCPU - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - AnyCPU - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/vs.sln b/vs.sln deleted file mode 100644 index 7317ac4..0000000 --- a/vs.sln +++ /dev/null @@ -1,22 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 2013 -VisualStudioVersion = 12.0.21005.1 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RSA", "vs.csproj", "{2DAD0BFE-E417-4AD8-8C44-7578F6758947}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {2DAD0BFE-E417-4AD8-8C44-7578F6758947}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {2DAD0BFE-E417-4AD8-8C44-7578F6758947}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2DAD0BFE-E417-4AD8-8C44-7578F6758947}.Release|Any CPU.ActiveCfg = Release|Any CPU - {2DAD0BFE-E417-4AD8-8C44-7578F6758947}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection -EndGlobal From 1e4ae877ab43c58f0ead8276d46146205432e312 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=AB=98=E5=9D=9A=E6=9E=9C?= <753610399@qq.com> Date: Tue, 12 Sep 2023 16:19:48 +0800 Subject: [PATCH 15/15] =?UTF-8?q?Release=20Update=20v1.6,=20=E9=87=8D?= =?UTF-8?q?=E6=96=B0=E5=8F=91=E5=B8=83=EF=BC=8C=E8=B0=83=E6=95=B4=E9=83=A8?= =?UTF-8?q?=E5=88=86=E9=80=BB=E8=BE=91=EF=BC=8C=E4=BF=AE=E5=A4=8D=E9=94=99?= =?UTF-8?q?=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Program.cs | 50 +++++++++++++++++++---------- README-English.md | 10 +++--- README.md | 10 +++--- RSA_Util.cs | 82 ++++++++++++++++++++++++++++++++++++++++------- 4 files changed, 115 insertions(+), 37 deletions(-) diff --git a/Program.cs b/Program.cs index 92fa6f0..13bd32e 100644 --- a/Program.cs +++ b/Program.cs @@ -114,13 +114,18 @@ static void RSATest(bool fast) { //对调交换公钥私钥 ST("【Unsafe|对调公钥私钥,私钥加密公钥解密】", "[ Unsafe | Swap the public key and private key, private key encryption and public key decryption ]"); - rsa4 = rsa.SwapKey_Exponent_D__Unsafe(); + var rsaPri = rsa.SwapKey_Exponent_D__Unsafe(); + var rsaPub = new RSA_Util(rsa.ToPEM(true)).SwapKey_Exponent_D__Unsafe(); + if (!RSA_Util.IsUseBouncyCastle) { + rsaPub = rsaPri; + ST(".NET自带的RSA不支持仅含公钥的密钥进行解密和签名,使用NoPadding填充方式或IsUseBouncyCastle时无此问题", "The RSA that comes with .NET does not support decryption and signing with keys containing only public keys. This problem does not occur when using NoPadding or IsUseBouncyCastle."); + } try { - var en4 = rsa4.Encrypt("PKCS1", str); - var sign4 = rsa4.Sign("SHA1", str); - de = rsa4.Decrypt("PKCS1", en4); + var enPri = rsaPri.Encrypt("PKCS1", str); + var signPub = rsaPub.Sign("SHA1", str); + de = rsaPub.Decrypt("PKCS1", enPri); AssertMsg(de, de == str); - AssertMsg(T("校验 OK", "Verify OK"), rsa4.Verify("SHA1", sign4, str)); + AssertMsg(T("校验 OK", "Verify OK"), rsaPri.Verify("SHA1", signPub, str)); } catch (Exception e) { if (!RSA_Util.IS_CoreOr46 && !RSA_Util.IsUseBouncyCastle) { S(T("不支持在RSACryptoServiceProvider中使用:", "Not supported in RSACryptoServiceProvider: ") + e.Message); @@ -129,7 +134,7 @@ static void RSATest(bool fast) { } } - rsa4 = rsa4.SwapKey_Exponent_D__Unsafe(); + rsa4 = rsaPri.SwapKey_Exponent_D__Unsafe(); de = rsa4.Decrypt("PKCS1", en); AssertMsg(de, de == str); AssertMsg(T("校验 OK", "Verify OK"), rsa4.Verify("SHA1", sign, str)); @@ -141,7 +146,7 @@ static void RSATest(bool fast) { ST("【测试一遍所有的加密、解密填充方式】 按回车键继续测试...", "[ Test all the encryption and decryption padding mode ] Press Enter to continue testing..."); ReadIn(); RSA_Util rsa5 = new RSA_Util(2048); - testPaddings(false, rsa5, true); + testPaddings(false, rsa5, new RSA_Util(rsa5.ToPEM(true)), true); } } static Type Type_RuntimeInformation(Type[] outOSPlatform) { @@ -404,10 +409,20 @@ static void testProvider(bool checkOpenSSL) { S(HR); ST("测试一遍所有的加密、解密填充方式:", "Test all the encryption and decryption padding mode:"); - testPaddings(checkOpenSSL, rsa, true); + testPaddings(checkOpenSSL, rsa, new RSA_Util(rsa.ToPEM(true)), true); + + S(HR); + ST("Unsafe|是否要对调公钥私钥(私钥加密公钥解密)重新测试一遍?(Y/N) N", "Unsafe | Do you want to swap the public and private keys (private key encryption and public key decryption) and test again? (Y/N) N"); + Console.Write("> "); + string yn = ReadIn().Trim().ToUpper(); + if (yn == "Y") { + var rsaPri = rsa.SwapKey_Exponent_D__Unsafe(); + var rsaPub = new RSA_Util(rsa.ToPEM(true)).SwapKey_Exponent_D__Unsafe(); + testPaddings(checkOpenSSL, rsaPub, rsaPri, true); + } } /// 测试一遍所有的加密、解密填充方式 - static int testPaddings(bool checkOpenSSL, RSA_Util rsa, bool log) { + static int testPaddings(bool checkOpenSSL, RSA_Util rsaPri, RSA_Util rsaPub, bool log) { int errCount = 0; var errMsgs = new List(); var txt = "1234567890"; @@ -419,7 +434,7 @@ static int testPaddings(bool checkOpenSSL, RSA_Util rsa, bool log) { if (checkOpenSSL) { try { - runOpenSSL(rsa, txtData); + runOpenSSL(rsaPri.HasPrivate ? rsaPri : rsaPub, txtData); } catch (Exception e) { S(T("运行OpenSSL失败:", "Failed to run OpenSSL: ") + e.Message); return errCount; @@ -431,8 +446,8 @@ static int testPaddings(bool checkOpenSSL, RSA_Util rsa, bool log) { var errMsg = ""; try { { - byte[] enc = rsa.Encrypt(type, txtData); - byte[] dec = rsa.Decrypt(type, enc); + byte[] enc = rsaPub.Encrypt(type, txtData); + byte[] dec = rsaPri.Decrypt(type, enc); bool isOk = true; if (dec.Length != txtData.Length) { isOk = false; @@ -456,7 +471,7 @@ static int testPaddings(bool checkOpenSSL, RSA_Util rsa, bool log) { errMsg = "+OpenSSL: " + T("OpenSSL加密出错", "OpenSSL encryption error"); throw e; } - byte[] dec = rsa.Decrypt(type, enc); + byte[] dec = rsaPri.Decrypt(type, enc); bool isOk = true; if (dec.Length != txtData.Length) { isOk = false; @@ -493,8 +508,8 @@ static int testPaddings(bool checkOpenSSL, RSA_Util rsa, bool log) { var errMsg = ""; try { { - byte[] sign = rsa.Sign(type, txtData); - var isOk = rsa.Verify(type, sign, txtData); + byte[] sign = rsaPri.Sign(type, txtData); + var isOk = rsaPub.Verify(type, sign, txtData); if (!isOk) { errMsg = T("未通过校验", "Failed verification"); throw new Exception(errMsg); @@ -508,7 +523,7 @@ static int testPaddings(bool checkOpenSSL, RSA_Util rsa, bool log) { errMsg = "+OpenSSL: " + T("OpenSSL签名出错", "OpenSSL signature error"); throw e; } - var isOk = rsa.Verify(type, sign, txtData); + var isOk = rsaPub.Verify(type, sign, txtData); if (!isOk) { errMsg = "+OpenSSL: " + T("未通过校验", "Failed verification"); throw new Exception(errMsg); @@ -550,12 +565,13 @@ static void threadRun() { int Count = 0; int ErrCount = 0; RSA_Util rsa = new RSA_Util(2048); + RSA_Util rsaPub = new RSA_Util(rsa.ToPEM(true)); S(T("正在测试中,线程数:", "Under test, number of threads: ") + ThreadCount + T(",按回车键结束测试...", ", press enter to end the test...")); for (int i = 0; i < ThreadCount; i++) { new Thread(() => { while (!Abort) { - int err = testPaddings(false, rsa, false); + int err = testPaddings(false, rsa, rsaPub, false); if (err > 0) { Interlocked.Add(ref ErrCount, err); } diff --git a/README-English.md b/README-English.md index 639c757..3358702 100644 --- a/README-English.md +++ b/README-English.md @@ -75,8 +75,10 @@ var isVerify=rsa.Verify("PKCS1+SHA1", sign, "test123"); var pemTxt=rsa.ToPEM().ToPEM_PKCS8(); //Unconventional (unsafe, not recommended): private key encryption, public key decryption, public key signature, private key verification -RSA_Util rsa2=rsa.SwapKey_Exponent_D__Unsafe(); -//... rsa2.Encrypt rsa2.Decrypt rsa2.Sign rsa2.Verify +RSA_Util rsaS_Private=rsa.SwapKey_Exponent_D__Unsafe(); +RSA_Util rsaS_Public=new RSA_Util(rsa.ToPEM(true)).SwapKey_Exponent_D__Unsafe(); +//... rsaS_Private.Encrypt rsaS_Public.Decrypt +//... rsaS_Public.Sign rsaS_Private.Verify Console.WriteLine(pemTxt+"\n"+enTxt+"\n"+deTxt+"\n"+sign+"\n"+isVerify); Console.ReadLine(); @@ -121,7 +123,7 @@ Welcome to join QQ group: 421882406, pure lowercase password: `xiangyuecn` Padding|Algorithm|Frame|Core|BC :-|:-|:-:|:-:|:-: -NO|RSA/ECB/NoPadding|×|×|√ +NO|RSA/ECB/NoPadding|√|√|√ PKCS1 |RSA/ECB/PKCS1Padding|√|√|√ OAEP+SHA1 |RSA/ECB/OAEPwithSHA-1andMGF1Padding|√|√|√ OAEP+SHA256|RSA/ECB/OAEPwithSHA-256andMGF1Padding|4.6+|√|√ @@ -291,7 +293,7 @@ The `RSA_Util.cs` file depends on `RSA_PEM.cs`, which encapsulates encryption, d `RSA_PEM` **ToPEM(bool convertToPublic = false)**: Export RSA_PEM object (then you can export PEM text by RSA_PEM.ToPEM method), if convertToPublic RSA containing private key will only return public key, RSA containing only public key will not be affected. -`RSA_Util` **SwapKey_Exponent_D__Unsafe()**: [Unsafe and not recommended] Swap the public key exponent (Key_Exponent) and the private key exponent (Key_D): use the public key as the private key (new.Key_D=this.Key_Exponent) and the private key as the public key (new. Key_Exponent=this.Key_D), returns a new RSA object; for example, used for: private key encryption, public key decryption, this is an unconventional usage. The current object must contain a private key, otherwise an exception will be thrown if it cannot be swapped. Note: It is very insecure to use the public key as a private key, because the public key exponent of most generated keys is 0x10001 (AQAB), which is too easy to guess and cannot be used as a real private key. The swapped key does not support use in RSACryptoServiceProvider (.NET Framework 4.5 and below): `!IS_CoreOr46 && !IsUseBouncyCastle`. +`RSA_Util` **SwapKey_Exponent_D__Unsafe()**: [Unsafe and not recommended] Swap the public key exponent (Key_Exponent) and the private key exponent (Key_D): use the public key as the private key (new.Key_D=this.Key_Exponent) and the private key as the public key (new.Key_Exponent=this.Key_D), returns a new RSA object; for example, used for: private key encryption, public key decryption, this is an unconventional usage. If the current key only contains the public key, the swap will not occur, and the returned new RSA will allow decryption and signing operations with the public key; However, the RSA that comes with .NET does not support decryption and signing with keys containing only the public key, and the exponent must be swapped (If it is .NET Framework 4.5 and below, public and private keys are not supported), there is no such problem when using NoPadding or IsUseBouncyCastle. Note: It is very unsafe to use a public key as a private key, because the public key exponent of most generated keys is 0x10001 (AQAB), which is too easy to guess and cannot be used as a true private key. In some private key encryption implementations, such as Java's own RSA, when using non-NoPadding padding, encryption with private key objects may use EMSA-PKCS1-v1_5 padding (using the private key exponent to construct a public key object does not have this problem ), so when interoperating between different programs, you may need to use the corresponding padding algorithm to first fill the data, and then use NoPadding padding to encrypt (decryption also uses NoPadding padding to decrypt, and then remove the padding data). `string` **Encrypt(string padding, string str)**: Encrypt arbitrary length string (utf-8) returns base64, and an exception is thrown if an error occurs. This method is thread safe. padding specifies the encryption padding, such as: PKCS1, OAEP+SHA256 uppercase, refer to the encryption padding table above, and the default is PKCS1 when using a null value. diff --git a/README.md b/README.md index f277192..ab36234 100644 --- a/README.md +++ b/README.md @@ -76,8 +76,10 @@ var isVerify=rsa.Verify("PKCS1+SHA1", sign, "测试123"); var pemTxt=rsa.ToPEM().ToPEM_PKCS8(); //非常规的(不安全、不建议使用):私钥加密、公钥解密,公钥签名、私钥验证 -RSA_Util rsa2=rsa.SwapKey_Exponent_D__Unsafe(); -//... rsa2.Encrypt rsa2.Decrypt rsa2.Sign rsa2.Verify +RSA_Util rsaS_Private=rsa.SwapKey_Exponent_D__Unsafe(); +RSA_Util rsaS_Public=new RSA_Util(rsa.ToPEM(true)).SwapKey_Exponent_D__Unsafe(); +//... rsaS_Private.Encrypt rsaS_Public.Decrypt +//... rsaS_Public.Sign rsaS_Private.Verify Console.WriteLine(pemTxt+"\n"+enTxt+"\n"+deTxt+"\n"+sign+"\n"+isVerify); Console.ReadLine(); @@ -122,7 +124,7 @@ Console.ReadLine(); 加密填充方式|Algorithm|Frame|Core|BC :-|:-|:-:|:-:|:-: -NO|RSA/ECB/NoPadding|×|×|√ +NO|RSA/ECB/NoPadding|√|√|√ PKCS1 |RSA/ECB/PKCS1Padding|√|√|√ OAEP+SHA1 |RSA/ECB/OAEPwithSHA-1andMGF1Padding|√|√|√ OAEP+SHA256|RSA/ECB/OAEPwithSHA-256andMGF1Padding|4.6+|√|√ @@ -292,7 +294,7 @@ PSS+MD5 |MD5withRSA/PSS|4.6+|√|√ `RSA_PEM` **ToPEM(bool convertToPublic = false)**:导出RSA_PEM对象(然后可以通过RSA_PEM.ToPEM方法导出PEM文本),如果convertToPublic含私钥的RSA将只返回公钥,仅含公钥的RSA不受影响。 -`RSA_Util` **SwapKey_Exponent_D__Unsafe()**:【不安全、不建议使用】对调交换公钥指数(Key_Exponent)和私钥指数(Key_D):把公钥当私钥使用(new.Key_D=this.Key_Exponent)、私钥当公钥使用(new.Key_Exponent=this.Key_D),返回一个新RSA对象;比如用于:私钥加密、公钥解密,这是非常规的用法。当前对象必须含私钥,否则无法交换会直接抛异常。注意:把公钥当私钥使用是非常不安全的,因为绝大部分生成的密钥的公钥指数为 0x10001(AQAB),太容易被猜测到,无法作为真正意义上的私钥。交换后的密钥不支持在RSACryptoServiceProvider(.NET Framework 4.5及以下版本)中使用:`!IS_CoreOr46 && !IsUseBouncyCastle`。 +`RSA_Util` **SwapKey_Exponent_D__Unsafe()**:【不安全、不建议使用】对调交换公钥指数(Key_Exponent)和私钥指数(Key_D):把公钥当私钥使用(new.Key_D=this.Key_Exponent)、私钥当公钥使用(new.Key_Exponent=this.Key_D),返回一个新RSA对象;比如用于:私钥加密、公钥解密,这是非常规的用法。当前密钥如果只包含公钥,将不会发生对调,返回的新RSA将允许用公钥进行解密和签名操作;但.NET自带的RSA不支持仅含公钥的密钥进行解密和签名,必须进行指数对调(如果是.NET Framework 4.5及以下版本,公钥私钥均不支持),使用NoPadding填充方式或IsUseBouncyCastle时无此问题。注意:把公钥当私钥使用是非常不安全的,因为绝大部分生成的密钥的公钥指数为 0x10001(AQAB),太容易被猜测到,无法作为真正意义上的私钥。部分私钥加密实现中,比如Java自带的RSA,使用非NoPadding填充方式时,用私钥对象进行加密可能会采用EMSA-PKCS1-v1_5填充方式(用私钥指数构造成公钥对象无此问题),因此在不同程序之间互通时,可能需要自行使用对应填充算法先对数据进行填充,然后再用NoPadding填充方式进行加密(解密也按NoPadding填充进行解密,然后去除填充数据)。 `string` **Encrypt(string padding, string str)**:加密任意长度字符串(utf-8)返回base64,出错抛异常。本方法线程安全。padding指定填充方式,如:PKCS1、OAEP+SHA256大写,参考上面的加密填充方式表格,使用空值时默认为PKCS1。 diff --git a/RSA_Util.cs b/RSA_Util.cs index 9f45c4d..31ff216 100644 --- a/RSA_Util.cs +++ b/RSA_Util.cs @@ -10,6 +10,7 @@ using System; using System.IO; +using System.Numerics; using System.Reflection; using System.Security.Cryptography; using System.Text; @@ -44,12 +45,17 @@ public RSA_PEM ToPEM(bool convertToPublic = false) { return PEM__.CopyToNew(convertToPublic); } /// - /// 【不安全、不建议使用】对调交换公钥指数(Key_Exponent)和私钥指数(Key_D):把公钥当私钥使用(new.Key_D=this.Key_Exponent)、私钥当公钥使用(new.Key_Exponent=this.Key_D),返回一个新RSA对象;比如用于:私钥加密、公钥解密,这是非常规的用法 - /// 。当前对象必须含私钥,否则无法交换会直接抛异常 - /// 。注意:把公钥当私钥使用是非常不安全的,因为绝大部分生成的密钥的公钥指数为 0x10001(AQAB),太容易被猜测到,无法作为真正意义上的私钥 - /// 。交换后的密钥不支持在RSACryptoServiceProvider(.NET Framework 4.5及以下版本)中使用:!IS_CoreOr46 And !IsUseBouncyCastle + /// 【不安全、不建议使用】对调交换公钥指数(Key_Exponent)和私钥指数(Key_D):把公钥当私钥使用(new.Key_D=this.Key_Exponent)、私钥当公钥使用(new.Key_Exponent=this.Key_D),返回一个新RSA对象;比如用于:私钥加密、公钥解密,这是非常规的用法。 + ///

当前密钥如果只包含公钥,将不会发生对调,返回的新RSA将允许用公钥进行解密和签名操作;但.NET自带的RSA不支持仅含公钥的密钥进行解密和签名,必须进行指数对调(如果是.NET Framework 4.5及以下版本,公钥私钥均不支持),使用NoPadding填充方式或IsUseBouncyCastle时无此问题。 + ///

注意:把公钥当私钥使用是非常不安全的,因为绝大部分生成的密钥的公钥指数为 0x10001(AQAB),太容易被猜测到,无法作为真正意义上的私钥。 + ///

部分私钥加密实现中,比如Java自带的RSA,使用非NoPadding填充方式时,用私钥对象进行加密可能会采用EMSA-PKCS1-v1_5填充方式(用私钥指数构造成公钥对象无此问题),因此在不同程序之间互通时,可能需要自行使用对应填充算法先对数据进行填充,然后再用NoPadding填充方式进行加密(解密也按NoPadding填充进行解密,然后去除填充数据)。 ///
public RSA_Util SwapKey_Exponent_D__Unsafe() { + if (PEM__.Key_D == null) { + var rsa = new RSA_Util(PEM__.CopyToNew(false)); + rsa.allowKeyDNull = true; + return rsa; + } return new RSA_Util(PEM__.SwapKey_Exponent_D__Unsafe()); } @@ -467,6 +473,15 @@ private RSA createRSA() { } + private bool allowKeyDNull; + private void checkKeyD(bool usePub) { + if (usePub) return; + if (PEM__.Key_D != null) return; + if (allowKeyDNull) return; + throw new Exception(T("当前是公钥,常规情况下不允许进行Decrypt或Sign操作,可以调用SwapKey方法来允许进行此操作", "Currently it is a public key. Decrypt or Sign operations are not allowed under normal circumstances. You can call the SwapKey method to allow this operation.")); + } + + @@ -539,6 +554,7 @@ static private void __OaepParam(string ctype, out string outType, out string out outHash = hash; } private byte[] __EncDec(bool isEnc, string ctype, byte[] data, int blockLen) { + checkKeyD(isEnc); string ctype0 = ctype, CType = ctype.ToUpper(); bool isNO = false, isOaep = false; @@ -589,6 +605,29 @@ private byte[] __EncDec(bool isEnc, string ctype, byte[] data, int blockLen) { return (byte[])processBlock.Invoke(cipher, new object[] { data, offset, len }); }; #endif + } else if (isNO) { + //.NET不支持NoPadding,手动实现一下 + var n = RSA_PEM.BigX(PEM__.Key_Modulus); + var e = RSA_PEM.BigX(PEM__.Key_Exponent); + if (!isEnc && PEM__.Key_D != null) {//如果未提供私钥,将用公钥解密 + e = RSA_PEM.BigX(PEM__.Key_D); + } + process = (offset, len) => { + if (isEnc) { + byte[] pad0 = new byte[blockLen]; + Array.Copy(data, offset, pad0, pad0.Length - len, len); + var m = RSA_PEM.BigX(pad0); + var c = BigInteger.ModPow(m, e, n); + return RSA_PEM.BigB(c); + } else { + var enc = new byte[len]; + Array.Copy(data, offset, enc, 0, len); + var m = RSA_PEM.BigX(enc); + var c = BigInteger.ModPow(m, e, n); + return RSA_PEM.BigB(c); + } + }; + destory = () => { }; } else if (IS_CoreOr46) { //使用高版本RSA进行加密解密,4.6+ 或 Core if (isNO) throw new Exception(NetNotSupportMsg(ctype0 + T("加密填充模式", " encryption padding mode"))); @@ -677,6 +716,7 @@ private bool __Verify(string hash, byte[] sign, byte[] data) { } static private Regex HS_Exp = new Regex("^SHA(3-|-?512/)?[\\-/]?(\\d+)WITHRSA$"); private void __SignVerify(bool isSign, string hashType, byte[] data, byte[] signData, out byte[] signVal, out bool verifyVal) { + checkKeyD(!isSign); string stype = RSAPadding_Sign(hashType), SType = stype.ToUpper(); bool isPss = SType.EndsWith("/PSS"); @@ -866,17 +906,35 @@ private dynamic Bc_Key(bool usePub) { return val; }; #if RSA_Util_BouncyCastle_CompileCode_1 - BcInt[] ks = new BcInt[] { new BcInt(BigX(k.Key_Modulus)), new BcInt(BigX(k.Key_Exponent)), new BcInt(BigX(k.Key_D)), new BcInt(BigX(k.Val_P)), new BcInt(BigX(k.Val_Q)), new BcInt(BigX(k.Val_DP)), new BcInt(BigX(k.Val_DQ)), new BcInt(BigX(k.Val_InverseQ)) }; - if (usePub) { - return new RsaKeyParameters(false, ks[0], ks[1]); - } + BcInt[] ks = new BcInt[8]; + ks[0] = new BcInt(BigX(k.Key_Modulus)); + ks[1] = new BcInt(BigX(k.Key_Exponent)); + checkKeyD(usePub); + if (usePub || k.Key_D == null) { + return new RsaKeyParameters(!usePub, ks[0], ks[1]); + } + ks[2] = new BcInt(BigX(k.Key_D)); + ks[3] = new BcInt(BigX(k.Val_P)); + ks[4] = new BcInt(BigX(k.Val_Q)); + ks[5] = new BcInt(BigX(k.Val_DP)); + ks[6] = new BcInt(BigX(k.Val_DQ)); + ks[7] = new BcInt(BigX(k.Val_InverseQ)); return new RsaPrivateCrtKeyParameters(ks[0], ks[1], ks[2], ks[3], ks[4], ks[5], ks[6], ks[7]); #else var BInt = rsaBouncyCastle.GetType("Org.BouncyCastle.Math.BigInteger").GetConstructor(new Type[] { typeof(byte[]) }); - object[] ks = new object[] { BInt.Invoke(new object[] { BigX(k.Key_Modulus) }), BInt.Invoke(new object[] { BigX(k.Key_Exponent) }), BInt.Invoke(new object[] { BigX(k.Key_D) }), BInt.Invoke(new object[] { BigX(k.Val_P) }), BInt.Invoke(new object[] { BigX(k.Val_Q) }), BInt.Invoke(new object[] { BigX(k.Val_DP) }), BInt.Invoke(new object[] { BigX(k.Val_DQ) }), BInt.Invoke(new object[] { BigX(k.Val_InverseQ) }) }; - if (usePub) { - return FindCtor(rsaBouncyCastle.GetType("Org.BouncyCastle.Crypto.Parameters.RsaKeyParameters"), new string[] { "bool", "big", "big" }).Invoke(new object[] { false, ks[0], ks[1] }); - } + object[] ks = new object[8]; + ks[0] = BInt.Invoke(new object[] { BigX(k.Key_Modulus) }); + ks[1] = BInt.Invoke(new object[] { BigX(k.Key_Exponent) }); + checkKeyD(usePub); + if (usePub || k.Key_D == null) {//如果未提供私钥,将用公钥解密、签名 + return FindCtor(rsaBouncyCastle.GetType("Org.BouncyCastle.Crypto.Parameters.RsaKeyParameters"), new string[] { "bool", "big", "big" }).Invoke(new object[] { !usePub, ks[0], ks[1] }); + } + ks[2] = BInt.Invoke(new object[] { BigX(k.Key_D) }); + ks[3] = BInt.Invoke(new object[] { BigX(k.Val_P) }); + ks[4] = BInt.Invoke(new object[] { BigX(k.Val_Q) }); + ks[5] = BInt.Invoke(new object[] { BigX(k.Val_DP) }); + ks[6] = BInt.Invoke(new object[] { BigX(k.Val_DQ) }); + ks[7] = BInt.Invoke(new object[] { BigX(k.Val_InverseQ) }); return FindCtor(rsaBouncyCastle.GetType("Org.BouncyCastle.Crypto.Parameters.RsaPrivateCrtKeyParameters"), new string[] { "big", "big", "big", "big", "big", "big", "big", "big" }).Invoke(ks); #endif }