From 7fff2b70ece1f3bc0aff734a2c0d6476188f6fbc Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Tue, 21 May 2024 11:31:17 +0800 Subject: [PATCH] =?UTF-8?q?feat(wxapi):=20=E6=96=B0=E5=A2=9E=E5=AE=89?= =?UTF-8?q?=E5=85=A8=E9=89=B4=E6=9D=83=E6=A8=A1=E5=BC=8F=E6=89=80=E9=9C=80?= =?UTF-8?q?=E7=9A=84=20AES=E3=80=81RSA=E3=80=81SM2=E3=80=81SM4=20=E7=AD=89?= =?UTF-8?q?=E7=AE=97=E6=B3=95=E5=B7=A5=E5=85=B7=E7=B1=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../SKIT.FlurlHttpClient.Wechat.Api.csproj | 1 + .../Utilities/AESUtility.cs | 159 ++++++- .../Utilities/RSAUtility.cs | 217 +++++++++ .../Utilities/SM2Utility.cs | 437 ++++++++++++++++++ .../Utilities/SM4Utility.cs | 138 ++++++ .../TestCase_ToolsAESUtilityTests.cs | 28 ++ .../TestCase_ToolsRSAUtilityTests.cs | 42 ++ .../TestCase_ToolsSM2UtilityTests.cs | 42 ++ .../TestCase_ToolsSM4UtilityTests.cs | 37 ++ 9 files changed, 1099 insertions(+), 2 deletions(-) create mode 100644 src/SKIT.FlurlHttpClient.Wechat.Api/Utilities/RSAUtility.cs create mode 100644 src/SKIT.FlurlHttpClient.Wechat.Api/Utilities/SM2Utility.cs create mode 100644 src/SKIT.FlurlHttpClient.Wechat.Api/Utilities/SM4Utility.cs create mode 100644 test/SKIT.FlurlHttpClient.Wechat.Api.UnitTests/TestCase_ToolsRSAUtilityTests.cs create mode 100644 test/SKIT.FlurlHttpClient.Wechat.Api.UnitTests/TestCase_ToolsSM2UtilityTests.cs create mode 100644 test/SKIT.FlurlHttpClient.Wechat.Api.UnitTests/TestCase_ToolsSM4UtilityTests.cs diff --git a/src/SKIT.FlurlHttpClient.Wechat.Api/SKIT.FlurlHttpClient.Wechat.Api.csproj b/src/SKIT.FlurlHttpClient.Wechat.Api/SKIT.FlurlHttpClient.Wechat.Api.csproj index 935d06e9..e8a90152 100644 --- a/src/SKIT.FlurlHttpClient.Wechat.Api/SKIT.FlurlHttpClient.Wechat.Api.csproj +++ b/src/SKIT.FlurlHttpClient.Wechat.Api/SKIT.FlurlHttpClient.Wechat.Api.csproj @@ -40,6 +40,7 @@ + diff --git a/src/SKIT.FlurlHttpClient.Wechat.Api/Utilities/AESUtility.cs b/src/SKIT.FlurlHttpClient.Wechat.Api/Utilities/AESUtility.cs index 7e08bb5c..6e680ecb 100644 --- a/src/SKIT.FlurlHttpClient.Wechat.Api/Utilities/AESUtility.cs +++ b/src/SKIT.FlurlHttpClient.Wechat.Api/Utilities/AESUtility.cs @@ -1,5 +1,9 @@ using System; using System.Security.Cryptography; +using System.Text; +using Org.BouncyCastle.Crypto; +using Org.BouncyCastle.Crypto.Parameters; +using Org.BouncyCastle.Security; namespace SKIT.FlurlHttpClient.Wechat.Api.Utilities { @@ -10,11 +14,16 @@ namespace SKIT.FlurlHttpClient.Wechat.Api.Utilities /// public static class AESUtility { + /// + /// 填充模式:NoPadding。 + /// + public const string PADDING_MODE_NOPADDING = "NoPadding"; + /// /// 基于 CBC 模式解密数据。 /// /// AES 密钥字节数组。 - /// 加密使用的初始化向量字节数组。 + /// 初始化向量字节数组。 /// 待解密数据字节数组。 /// 解密后的数据字节数组。 public static byte[] DecryptWithCBC(byte[] keyBytes, byte[] ivBytes, byte[] cipherBytes) @@ -56,11 +65,86 @@ namespace SKIT.FlurlHttpClient.Wechat.Api.Utilities return EncodedString.ToLiteralString(plainBytes); } + /// + /// 基于 GCM 模式解密数据。 + /// + /// AES 密钥字节数组。 + /// 初始化向量字节数组。 + /// 附加数据字节数组。 + /// 待解密数据字节数组。 + /// 填充模式。(默认值:) + /// 解密后的数据字节数组。 + public static byte[] DecryptWithGCM(byte[] keyBytes, byte[] nonceBytes, byte[]? associatedDataBytes, byte[] cipherBytes, string paddingMode = PADDING_MODE_NOPADDING) + { + const int KEY_LENGTH_BYTE = 32; + const int NONCE_LENGTH_BYTE = 12; + const int TAG_LENGTH_BYTE = 16; + + if (keyBytes is null) throw new ArgumentNullException(nameof(keyBytes)); + if (keyBytes.Length != KEY_LENGTH_BYTE) throw new ArgumentException($"Invalid key byte length (expected: {KEY_LENGTH_BYTE}, actual: {keyBytes.Length}).", nameof(keyBytes)); + if (nonceBytes is null) throw new ArgumentNullException(nameof(nonceBytes)); + if (nonceBytes.Length != NONCE_LENGTH_BYTE) throw new ArgumentException($"Invalid nonce byte length (expected: {NONCE_LENGTH_BYTE}, actual: {nonceBytes.Length}).", nameof(nonceBytes)); + if (cipherBytes is null) throw new ArgumentNullException(nameof(cipherBytes)); + if (cipherBytes.Length < TAG_LENGTH_BYTE) throw new ArgumentException($"Invalid cipher byte length (expected: more than {TAG_LENGTH_BYTE}, actual: {cipherBytes.Length}).", nameof(cipherBytes)); + +#if NET5_0_OR_GREATER + using (AesGcm aes = new AesGcm(keyBytes)) + { + byte[] cipherWithoutTagBytes = new byte[cipherBytes.Length - TAG_LENGTH_BYTE]; + byte[] tagBytes = new byte[TAG_LENGTH_BYTE]; + Buffer.BlockCopy(cipherBytes, 0, cipherWithoutTagBytes, 0, cipherWithoutTagBytes.Length); + Buffer.BlockCopy(cipherBytes, cipherWithoutTagBytes.Length, tagBytes, 0, tagBytes.Length); + + byte[] plainBytes = new byte[cipherWithoutTagBytes.Length]; + aes.Decrypt(nonceBytes, cipherWithoutTagBytes, tagBytes, plainBytes, associatedDataBytes); + return plainBytes; + } +#else + IBufferedCipher cipher = CipherUtilities.GetCipher($"AES/GCM/{paddingMode}"); + ICipherParameters cipherParams = new AeadParameters( + new KeyParameter(keyBytes), + TAG_LENGTH_BYTE * 8, + nonceBytes, + associatedDataBytes + ); + cipher.Init(false, cipherParams); + byte[] plainBytes = new byte[cipher.GetOutputSize(cipherBytes.Length)]; + int len = cipher.ProcessBytes(cipherBytes, 0, cipherBytes.Length, plainBytes, 0); + cipher.DoFinal(plainBytes, len); + return plainBytes; +#endif + } + + /// + /// 基于 GCM 模式解密数据。 + /// + /// 经过编码后的(通常为 Base64)AES 密钥。 + /// 经过编码后的(通常为 Base64)初始化向量。 + /// 经过编码后的(通常为 Base64)附加数据。 + /// 经过编码后的(通常为 Base64)待解密数据。 + /// 填充模式。(默认值:) + /// 解密后的数据。 + public static EncodedString DecryptWithGCM(EncodedString encodingKey, EncodedString encodingNonce, EncodedString encodingAssociatedData, EncodedString encodingCipher, string paddingMode = PADDING_MODE_NOPADDING) + { + if (encodingKey.Value is null) throw new ArgumentNullException(nameof(encodingKey)); + if (encodingNonce.Value is null) throw new ArgumentNullException(nameof(encodingNonce)); + if (encodingCipher.Value is null) throw new ArgumentNullException(nameof(encodingCipher)); + + byte[] plainBytes = DecryptWithGCM( + keyBytes: EncodedString.FromString(encodingKey, fallbackEncodingKind: EncodingKinds.Base64), + nonceBytes: EncodedString.FromString(encodingNonce, fallbackEncodingKind: EncodingKinds.Base64), + associatedDataBytes: encodingAssociatedData.Value is not null ? EncodedString.FromString(encodingAssociatedData, fallbackEncodingKind: EncodingKinds.Base64) : null, + cipherBytes: EncodedString.FromString(encodingCipher, fallbackEncodingKind: EncodingKinds.Base64), + paddingMode: paddingMode + ); + return EncodedString.ToLiteralString(plainBytes); + } + /// /// 基于 CBC 模式加密数据。 /// /// AES 密钥字节数组。 - /// 加密使用的初始化向量字节数组。 + /// 初始化向量字节数组。 /// 待加密数据字节数组。 /// 加密后的数据字节数组。 public static byte[] EncryptWithCBC(byte[] keyBytes, byte[] ivBytes, byte[] plainBytes) @@ -101,5 +185,76 @@ namespace SKIT.FlurlHttpClient.Wechat.Api.Utilities ); return EncodedString.ToBase64String(plainBytes); } + + /// + /// 基于 GCM 模式加密数据。 + /// + /// AES 密钥字节数组。 + /// 初始化向量字节数组。 + /// 附加数据字节数组。 + /// 待加密数据字节数组。 + /// 填充模式。(默认值:) + /// 加密后的数据字节数组。 + public static byte[] EncryptWithGCM(byte[] keyBytes, byte[] nonceBytes, byte[]? associatedDataBytes, byte[] plainBytes, string paddingMode = PADDING_MODE_NOPADDING) + { + const int TAG_LENGTH_BYTE = 16; + + if (keyBytes is null) throw new ArgumentNullException(nameof(keyBytes)); + if (nonceBytes is null) throw new ArgumentNullException(nameof(nonceBytes)); + if (plainBytes is null) throw new ArgumentNullException(nameof(plainBytes)); + +#if NET5_0_OR_GREATER + using (AesGcm aes = new AesGcm(keyBytes)) + { + byte[] cipherBytes = new byte[plainBytes.Length]; + byte[] tagBytes = new byte[TAG_LENGTH_BYTE]; + aes.Encrypt(nonceBytes, plainBytes, cipherBytes, tagBytes, associatedDataBytes); + + byte[] cipherWithTagBytes = new byte[cipherBytes.Length + tagBytes.Length]; + Buffer.BlockCopy(cipherBytes, 0, cipherWithTagBytes, 0, cipherBytes.Length); + Buffer.BlockCopy(tagBytes, 0, cipherWithTagBytes, cipherBytes.Length, tagBytes.Length); + return cipherWithTagBytes; + } +#else + + IBufferedCipher cipher = CipherUtilities.GetCipher($"AES/GCM/{paddingMode}"); + ICipherParameters cipherParams = new AeadParameters( + new KeyParameter(keyBytes), + TAG_LENGTH_BYTE * 8, + nonceBytes, + associatedDataBytes + ); + cipher.Init(true, cipherParams); + byte[] cipherBytes = new byte[cipher.GetOutputSize(plainBytes.Length)]; + int len = cipher.ProcessBytes(plainBytes, 0, plainBytes.Length, cipherBytes, 0); + cipher.DoFinal(cipherBytes, len); + return cipherBytes; +#endif + } + + /// + /// 基于 GCM 模式加密数据。 + /// + /// 经过编码后的(通常为 Base64)AES 密钥。 + /// 经过编码后的(通常为 Base64)初始化向量。 + /// 经过编码后的(通常为 Base64)附加数据。 + /// 待加密数据。 + /// 填充模式。(默认值:) + /// 经过 Base64 编码的加密后的数据。 + public static EncodedString EncryptWithGCM(EncodedString encodingKey, EncodedString encodingNonce, EncodedString encodingAssociatedData, string plainData, string paddingMode = PADDING_MODE_NOPADDING) + { + if (encodingKey.Value is null) throw new ArgumentNullException(nameof(encodingKey)); + if (encodingNonce.Value is null) throw new ArgumentNullException(nameof(encodingNonce)); + if (plainData is null) throw new ArgumentNullException(nameof(plainData)); + + byte[] plainBytes = EncryptWithGCM( + keyBytes: EncodedString.FromString(encodingKey, fallbackEncodingKind: EncodingKinds.Base64), + nonceBytes: EncodedString.FromString(encodingNonce, fallbackEncodingKind: EncodingKinds.Base64), + associatedDataBytes: encodingAssociatedData.Value is not null ? EncodedString.FromString(encodingAssociatedData, fallbackEncodingKind: EncodingKinds.Base64) : null, + plainBytes: Encoding.UTF8.GetBytes(plainData), + paddingMode: paddingMode + ); + return EncodedString.ToBase64String(plainBytes); + } } } diff --git a/src/SKIT.FlurlHttpClient.Wechat.Api/Utilities/RSAUtility.cs b/src/SKIT.FlurlHttpClient.Wechat.Api/Utilities/RSAUtility.cs new file mode 100644 index 00000000..8a0406e8 --- /dev/null +++ b/src/SKIT.FlurlHttpClient.Wechat.Api/Utilities/RSAUtility.cs @@ -0,0 +1,217 @@ +using System; +using System.IO; +using System.Security.Cryptography; +using System.Text.RegularExpressions; +using Org.BouncyCastle.Crypto; +using Org.BouncyCastle.Crypto.Parameters; +using Org.BouncyCastle.OpenSsl; +using Org.BouncyCastle.Security; + +namespace SKIT.FlurlHttpClient.Wechat.Api.Utilities +{ + using SKIT.FlurlHttpClient.Primitives; + + /// + /// RSA 算法工具类。 + /// + public static class RSAUtility + { + /// + /// 签名算法:SHA-256withRSA。 + /// + private const string DIGEST_ALGORITHM_SHA256 = "SHA-256withRSA"; + + private static byte[] ConvertPrivateKeyPemToByteArray(string privateKeyPem) + { + if (!privateKeyPem.StartsWith("-----BEGIN PRIVATE KEY-----")) + { + using (TextReader textReader = new StringReader(privateKeyPem)) + using (PemReader pemReader = new PemReader(textReader)) + { + object pemObject = pemReader.ReadObject(); + + if (pemObject is AsymmetricCipherKeyPair) + { + // PKCS#1 格式 + AsymmetricCipherKeyPair cipherKeyPair = (AsymmetricCipherKeyPair)pemObject; + using (TextWriter textWriter = new StringWriter()) + using (PemWriter pemWriter = new PemWriter(textWriter)) + { + Pkcs8Generator pkcs8 = new Pkcs8Generator(cipherKeyPair.Private); + pemWriter.WriteObject(pkcs8); + pemWriter.Writer.Close(); + + privateKeyPem = textWriter.ToString()!; + } + } + else if (pemObject is RsaPrivateCrtKeyParameters) + { + // PKCS#8 格式 + } + else + { + throw new NotSupportedException("Private key format is not supported."); + } + } + } + + privateKeyPem = privateKeyPem + .Replace("-----BEGIN PRIVATE KEY-----", string.Empty) + .Replace("-----END PRIVATE KEY-----", string.Empty); + privateKeyPem = Regex.Replace(privateKeyPem, "\\s+", string.Empty); + return Convert.FromBase64String(privateKeyPem); + } + + private static byte[] ConvertPublicKeyPemToByteArray(string publicKeyPem) + { + if (!publicKeyPem.StartsWith("-----BEGIN PUBLIC KEY-----")) + { + using (TextReader textReader = new StringReader(publicKeyPem)) + using (PemReader pemReader = new PemReader(textReader)) + { + object pemObject = pemReader.ReadObject(); + if (pemObject is RsaKeyParameters) + { + // PKCS#1 或 PKCS#8 格式 + RsaKeyParameters rsaKeyParams = (RsaKeyParameters)pemObject; + using (TextWriter textWriter = new StringWriter()) + using (PemWriter pemWriter = new PemWriter(textWriter)) + { + pemWriter.WriteObject(rsaKeyParams); + pemWriter.Writer.Close(); + + publicKeyPem = textWriter.ToString()!; + } + } + else + { + throw new NotSupportedException("Public key format is not supported."); + } + } + } + + publicKeyPem = publicKeyPem + .Replace("-----BEGIN PUBLIC KEY-----", string.Empty) + .Replace("-----END PUBLIC KEY-----", string.Empty); + publicKeyPem = Regex.Replace(publicKeyPem, "\\s+", string.Empty); + return Convert.FromBase64String(publicKeyPem); + } + +#if NET5_0_OR_GREATER +#else + private static RsaKeyParameters ParsePrivateKeyToParameters(byte[] privateKeyBytes) + { + return (RsaKeyParameters)PrivateKeyFactory.CreateKey(privateKeyBytes); + } + + private static RsaKeyParameters ParsePublicKeyToParameters(byte[] publicKeyBytes) + { + return (RsaKeyParameters)PublicKeyFactory.CreateKey(publicKeyBytes); + } + + private static byte[] Sign(RsaKeyParameters rsaPrivateKeyParams, byte[] messageBytes, string digestAlgorithm) + { + ISigner signer = SignerUtilities.GetSigner(digestAlgorithm); + signer.Init(true, rsaPrivateKeyParams); + signer.BlockUpdate(messageBytes, 0, messageBytes.Length); + return signer.GenerateSignature(); + } + + private static bool Verify(RsaKeyParameters rsaPublicKeyParams, byte[] messageBytes, byte[] signBytes, string digestAlgorithm) + { + ISigner signer = SignerUtilities.GetSigner(digestAlgorithm); + signer.Init(false, rsaPublicKeyParams); + signer.BlockUpdate(messageBytes, 0, messageBytes.Length); + return signer.VerifySignature(signBytes); + } +#endif + + /// + /// 使用私钥基于 SHA-256 算法生成签名。 + /// + /// PKCS#8 私钥字节数组。 + /// 待签名的数据字节数组。 + /// 签名算法。(默认值:) + /// 签名字节数组。 + public static byte[] SignWithSHA256(byte[] privateKeyBytes, byte[] messageBytes) + { + if (privateKeyBytes is null) throw new ArgumentNullException(nameof(privateKeyBytes)); + if (messageBytes is null) throw new ArgumentNullException(nameof(messageBytes)); + +#if NET5_0_OR_GREATER + using (RSA rsa = RSA.Create()) + { + rsa.ImportPkcs8PrivateKey(privateKeyBytes, out _); + return rsa.SignData(messageBytes, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); + } +#else + RsaKeyParameters rsaPrivateKeyParams = ParsePrivateKeyToParameters(privateKeyBytes); + return Sign(rsaPrivateKeyParams, messageBytes, DIGEST_ALGORITHM_SHA256); +#endif + } + + /// + /// 使用私钥基于 SHA-256 算法生成签名。 + /// + /// PKCS#1/PKCS#8 私钥(PEM 格式)。 + /// 待签名的数据。 + /// 签名算法。(默认值:) + /// 经过 Base64 编码的签名。 + public static EncodedString SignWithSHA256(string privateKeyPem, string messageData) + { + if (privateKeyPem is null) throw new ArgumentNullException(nameof(privateKeyPem)); + if (messageData is null) throw new ArgumentNullException(nameof(messageData)); + + byte[] privateKeyBytes = ConvertPrivateKeyPemToByteArray(privateKeyPem); + byte[] messageBytes = EncodedString.FromLiteralString(messageData); + byte[] signBytes = SignWithSHA256(privateKeyBytes, messageBytes); + return EncodedString.ToBase64String(signBytes); + } + + /// + /// 使用公钥基于 SHA-256 算法验证签名。 + /// + /// PKCS#8 公钥字节数组。 + /// 待验证的数据字节数组。 + /// 签名字节数组。 + /// 签名算法。(默认值:) + /// 验证结果。 + public static bool VerifyWithSHA256(byte[] publicKeyBytes, byte[] messageBytes, byte[] signBytes) + { + if (publicKeyBytes is null) throw new ArgumentNullException(nameof(publicKeyBytes)); + if (messageBytes is null) throw new ArgumentNullException(nameof(messageBytes)); + if (signBytes is null) throw new ArgumentNullException(nameof(signBytes)); + +#if NET5_0_OR_GREATER + using (RSA rsa = RSA.Create()) + { + rsa.ImportSubjectPublicKeyInfo(publicKeyBytes, out _); + return rsa.VerifyData(messageBytes, signBytes, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); + } +#else + RsaKeyParameters rsaPublicKeyParams = ParsePublicKeyToParameters(publicKeyBytes); + return Verify(rsaPublicKeyParams, messageBytes, signBytes, DIGEST_ALGORITHM_SHA256); +#endif + } + + /// + /// 使用公钥基于 SHA-256 算法验证签名。 + /// + /// PKCS#1/PKCS#8 公钥(PEM 格式)。 + /// 待验证的数据。 + /// 经过编码后的(通常为 Base64)签名。 + /// 签名算法。(默认值:) + /// 验证结果。 + public static bool VerifyWithSHA256(string publicKeyPem, string messageData, EncodedString encodingSignature) + { + if (publicKeyPem is null) throw new ArgumentNullException(nameof(publicKeyPem)); + if (messageData is null) throw new ArgumentNullException(nameof(messageData)); + if (encodingSignature.Value is null) throw new ArgumentNullException(nameof(encodingSignature)); + + byte[] publicKeyBytes = ConvertPublicKeyPemToByteArray(publicKeyPem); + byte[] messageBytes = EncodedString.FromLiteralString(messageData); + byte[] signBytes = EncodedString.FromString(encodingSignature, fallbackEncodingKind: EncodingKinds.Base64); + return VerifyWithSHA256(publicKeyBytes, messageBytes, signBytes); + } + } +} diff --git a/src/SKIT.FlurlHttpClient.Wechat.Api/Utilities/SM2Utility.cs b/src/SKIT.FlurlHttpClient.Wechat.Api/Utilities/SM2Utility.cs new file mode 100644 index 00000000..5ee65c9c --- /dev/null +++ b/src/SKIT.FlurlHttpClient.Wechat.Api/Utilities/SM2Utility.cs @@ -0,0 +1,437 @@ +using System; +using System.Linq; +using System.Text.RegularExpressions; +using Org.BouncyCastle.Asn1; +using Org.BouncyCastle.Asn1.GM; +using Org.BouncyCastle.Asn1.X9; +using Org.BouncyCastle.Crypto; +using Org.BouncyCastle.Crypto.Parameters; +using Org.BouncyCastle.Math; +using Org.BouncyCastle.Security; +using Org.BouncyCastle.Utilities; +using Org.BouncyCastle.Utilities.Encoders; + +namespace SKIT.FlurlHttpClient.Wechat.Api.Utilities +{ + using SKIT.FlurlHttpClient.Primitives; + + /// + /// SM2 算法工具类。 + /// 此实现遵循国家标准 GM/T 0009-2012 的有关规定。 + /// + public static class SM2Utility + { + private static readonly X9ECParameters SM2_ECX9_PARAMS = GMNamedCurves.GetByName("SM2P256v1"); + private static readonly ECDomainParameters SM2_DOMAIN_PARAMS = new ECDomainParameters(SM2_ECX9_PARAMS.Curve, SM2_ECX9_PARAMS.G, SM2_ECX9_PARAMS.N); + private static readonly byte[] SM2_DEFAULT_UID = new byte[] { 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38 }; + private const int SM2_RS_LENGTH = 32; + + private static byte[] ConvertPrivateKeyPemToByteArray(string privateKeyPem) + { + privateKeyPem = privateKeyPem + .Replace("-----BEGIN PRIVATE KEY-----", string.Empty) + .Replace("-----END PRIVATE KEY-----", string.Empty); + privateKeyPem = Regex.Replace(privateKeyPem, "\\s+", string.Empty); + return Convert.FromBase64String(privateKeyPem); + } + + private static byte[] ConvertPublicKeyPemToByteArray(string publicKeyPem) + { + publicKeyPem = publicKeyPem + .Replace("-----BEGIN PUBLIC KEY-----", string.Empty) + .Replace("-----END PUBLIC KEY-----", string.Empty); + publicKeyPem = Regex.Replace(publicKeyPem, "\\s+", string.Empty); + return Convert.FromBase64String(publicKeyPem); + } + + private static ECPrivateKeyParameters ParsePrivateKeyToParameters(byte[] privateKeyBytes) + { + return (ECPrivateKeyParameters)PrivateKeyFactory.CreateKey(privateKeyBytes); + } + + private static ECPrivateKeyParameters ParseECPrivateKeyToParameters(byte[] ecPrivateKeyBytes) + { + BigInteger ecPrivateKeyParamsD = new BigInteger(Hex.ToHexString(ecPrivateKeyBytes), 16); + return new ECPrivateKeyParameters(ecPrivateKeyParamsD, SM2_DOMAIN_PARAMS); + } + + private static ECPublicKeyParameters ParsePublicKeyToParameters(byte[] publicKeyBytes) + { + return (ECPublicKeyParameters)PublicKeyFactory.CreateKey(publicKeyBytes); + } + + private static ECPublicKeyParameters ParseECPublicKeyToParameters(byte[] ecPublicKeyBytes) + { + const int KEY_BYTE_LENGTH = 64; + + bool unzipped = ecPublicKeyBytes.FirstOrDefault() == 0x04; + if (unzipped && ecPublicKeyBytes.Length != KEY_BYTE_LENGTH + 1) + throw new ArgumentException($"Invalid key byte length (expected: {KEY_BYTE_LENGTH}, actual: {ecPublicKeyBytes.Length - 1}).", nameof(ecPublicKeyBytes)); + if (!unzipped && ecPublicKeyBytes.Length != KEY_BYTE_LENGTH) + throw new ArgumentException($"Invalid key byte length (expected: {KEY_BYTE_LENGTH}, actual: {ecPublicKeyBytes.Length}).", nameof(ecPublicKeyBytes)); + + byte[] ecPublicKeyXBytes = new byte[KEY_BYTE_LENGTH / 2]; + byte[] ecPublicKeyYBytes = new byte[KEY_BYTE_LENGTH / 2]; + Buffer.BlockCopy(ecPublicKeyBytes, unzipped ? 1 : 0, ecPublicKeyXBytes, 0, ecPublicKeyXBytes.Length); + Buffer.BlockCopy(ecPublicKeyBytes, ecPublicKeyXBytes.Length + (unzipped ? 1 : 0), ecPublicKeyYBytes, 0, ecPublicKeyYBytes.Length); + + BigInteger ecPublicKeyParamsX = new BigInteger(Hex.ToHexString(ecPublicKeyXBytes), 16); + BigInteger ecPublicKeyParamsY = new BigInteger(Hex.ToHexString(ecPublicKeyYBytes), 16); + return new ECPublicKeyParameters(SM2_ECX9_PARAMS.Curve.CreatePoint(ecPublicKeyParamsX, ecPublicKeyParamsY), SM2_DOMAIN_PARAMS); + } + + private static byte[] ConvertRsToAsn1(byte[] rs) + { + BigInteger r = new BigInteger(1, Arrays.CopyOfRange(rs, 0, SM2_RS_LENGTH)); + BigInteger s = new BigInteger(1, Arrays.CopyOfRange(rs, SM2_RS_LENGTH, SM2_RS_LENGTH * 2)); + + Asn1EncodableVector vector = new Asn1EncodableVector(); + vector.Add(new DerInteger(r)); + vector.Add(new DerInteger(s)); + + DerSequence sequence = new DerSequence(vector); + return sequence.GetEncoded("DER"); + } + + private static byte[] ConvertAsn1ToRs(byte[] asn1) + { + Asn1Sequence sequence = Asn1Sequence.GetInstance(asn1); + byte[] r = ConvertBigIntegerToFixedLengthByteArray(DerInteger.GetInstance(sequence[0]).Value); + byte[] s = ConvertBigIntegerToFixedLengthByteArray(DerInteger.GetInstance(sequence[1]).Value); + + byte[] tmp = new byte[SM2_RS_LENGTH * 2]; + Buffer.BlockCopy(r, 0, tmp, 0, r.Length); + Buffer.BlockCopy(s, 0, tmp, SM2_RS_LENGTH, s.Length); + return tmp; + } + + private static byte[] ConvertBigIntegerToFixedLengthByteArray(BigInteger bigInt) + { + // For SM2P256v1, N is 00fffffffeffffffffffffffffffffffff7203df6b21c6052b53bbf40939d54123, + // R and S are the tmp of mod N, so they should be less than N and have length <= 32 + + byte[] rs = bigInt.ToByteArray(); + if (rs.Length == SM2_RS_LENGTH) + { + return rs; + } + else if (rs.Length == SM2_RS_LENGTH + 1 && rs[0] == 0) + { + return Arrays.CopyOfRange(rs, 1, SM2_RS_LENGTH + 1); + } + else if (rs.Length < SM2_RS_LENGTH) + { + byte[] result = new byte[SM2_RS_LENGTH]; + Arrays.Fill(result, (byte)0); + Buffer.BlockCopy(rs, 0, result, SM2_RS_LENGTH - rs.Length, rs.Length); + return result; + } + else + { + throw new ArgumentException(); + } + } + + private static byte[] SignWithSM3(ECPrivateKeyParameters sm2PrivateKeyParams, byte[] uidBytes, byte[] messageBytes, bool asn1Encoding) + { + ISigner signer = SignerUtilities.GetSigner("SM3withSM2"); + signer.Init(true, new ParametersWithID(sm2PrivateKeyParams, uidBytes)); + signer.BlockUpdate(messageBytes, 0, messageBytes.Length); + byte[] signBytes = signer.GenerateSignature(); + + // BouncyCastle 库的签名结果默认 ASN.1 编码,如不需要需要手动转换 + if (!asn1Encoding) + { + signBytes = ConvertAsn1ToRs(signBytes); + } + + return signBytes; + } + + private static bool VerifyWithSM3(ECPublicKeyParameters sm2PublicKeyParams, byte[] uidBytes, byte[] messageBytes, byte[] signBytes, bool asn1Encoding) + { + ISigner signer = SignerUtilities.GetSigner("SM3withSM2"); + signer.Init(false, new ParametersWithID(sm2PublicKeyParams, uidBytes)); + signer.BlockUpdate(messageBytes, 0, messageBytes.Length); + + // BouncyCastle 库的签名结果默认 ASN.1 编码,如不需要需要手动转换 + if (!asn1Encoding) + { + signBytes = ConvertRsToAsn1(signBytes); + } + + return signer.VerifySignature(signBytes); + } + + /// + /// 使用私钥基于 SM3 算法生成签名。 + /// + /// PKCS#8 私钥字节数组。 + /// 用户标识符字节数组。 + /// 数据字节数组。 + /// 指示签名结果是否为 ASN.1 编码的形式。(默认值:true) + /// 签名字节数组。 + public static byte[] SignWithSM3(byte[] privateKeyBytes, byte[] uidBytes, byte[] messageBytes, bool asn1Encoding = true) + { + if (privateKeyBytes is null) throw new ArgumentNullException(nameof(privateKeyBytes)); + if (uidBytes is null) throw new ArgumentNullException(nameof(uidBytes)); + if (messageBytes is null) throw new ArgumentNullException(nameof(messageBytes)); + + ECPrivateKeyParameters sm2PrivateKeyParams = ParsePrivateKeyToParameters(privateKeyBytes); + return SignWithSM3(sm2PrivateKeyParams, uidBytes, messageBytes, asn1Encoding); + } + + /// + /// 使用私钥基于 SM3 算法生成签名。 + /// + /// PKCS#8 私钥字节数组。 + /// 待签名的数据字节数组。 + /// 指示签名结果是否为 ASN.1 编码的形式。(默认值:true) + /// 签名字节数组。 + public static byte[] SignWithSM3(byte[] privateKeyBytes, byte[] messageBytes, bool asn1Encoding = true) + { + return SignWithSM3( + privateKeyBytes: privateKeyBytes, + uidBytes: SM2_DEFAULT_UID, + messageBytes: messageBytes, + asn1Encoding: asn1Encoding + ); + } + + /// + /// 使用私钥基于 SM3 算法生成签名。 + /// + /// PKCS#8 私钥(PEM 格式)。 + /// 待签名的数据。 + /// 指示签名结果是否为 ASN.1 编码的形式。(默认值:true) + /// 经过 Base64 编码的签名。 + public static EncodedString SignWithSM3(string privateKeyPem, string messageData, bool asn1Encoding = true) + { + if (privateKeyPem is null) throw new ArgumentNullException(nameof(privateKeyPem)); + if (messageData is null) throw new ArgumentNullException(nameof(messageData)); + + byte[] signBytes = SignWithSM3( + privateKeyBytes: ConvertPrivateKeyPemToByteArray(privateKeyPem), + messageBytes: EncodedString.FromLiteralString(messageData), + asn1Encoding: asn1Encoding + ); + return EncodedString.ToBase64String(signBytes); + } + + /// + /// 使用 EC 私钥基于 SM3 算法生成签名。 + /// + /// EC 私钥字节数组。 + /// 用户标识符字节数组。 + /// 待签名的数据字节数组。 + /// 指示签名结果是否为 ASN.1 编码的形式。(默认值:true) + /// 签名字节数组。 + public static byte[] SignWithSM3ByECPrivateKey(byte[] ecPrivateKeyBytes, byte[] uidBytes, byte[] messageBytes, bool asn1Encoding = true) + { + if (ecPrivateKeyBytes is null) throw new ArgumentNullException(nameof(ecPrivateKeyBytes)); + if (uidBytes is null) throw new ArgumentNullException(nameof(uidBytes)); + if (messageBytes is null) throw new ArgumentNullException(nameof(messageBytes)); + + ECPrivateKeyParameters sm2PrivateKeyParams = ParseECPrivateKeyToParameters(ecPrivateKeyBytes); + return SignWithSM3(sm2PrivateKeyParams, uidBytes, messageBytes, asn1Encoding); + } + + /// + /// 使用 EC 私钥基于 SM3 算法生成签名。 + /// + /// EC 私钥字节数组。 + /// 待签名的数据字节数组。 + /// 指示签名结果是否为 ASN.1 编码的形式。(默认值:true) + /// 签名字节数组。 + public static byte[] SignWithSM3ByECPrivateKey(byte[] ecPrivateKeyBytes, byte[] messageBytes, bool asn1Encoding = true) + { + return SignWithSM3ByECPrivateKey( + ecPrivateKeyBytes: ecPrivateKeyBytes, + uidBytes: SM2_DEFAULT_UID, + messageBytes: messageBytes, + asn1Encoding: asn1Encoding + ); + } + + /// + /// 使用 EC 私钥基于 SM3 算法生成签名。 + /// + /// 经过编码后的(通常为十六进制)EC 私钥。 + /// 用户标识符字节数组。 + /// 待签名的数据字节数组。 + /// 指示签名结果是否为 ASN.1 编码的形式。(默认值:true) + /// 签名字节数组。 + public static byte[] SignWithSM3ByECPrivateKey(EncodedString encodingECPrivateKey, byte[] uidBytes, byte[] messageBytes, bool asn1Encoding = true) + { + if (encodingECPrivateKey.Value is null) throw new ArgumentNullException(nameof(encodingECPrivateKey)); + if (uidBytes is null) throw new ArgumentNullException(nameof(uidBytes)); + if (messageBytes is null) throw new ArgumentNullException(nameof(messageBytes)); + + return SignWithSM3ByECPrivateKey( + ecPrivateKeyBytes: EncodedString.FromString(encodingECPrivateKey, fallbackEncodingKind: EncodingKinds.Hex), + uidBytes: uidBytes, + messageBytes: messageBytes, + asn1Encoding: asn1Encoding + ); + } + + /// + /// 使用 EC 私钥基于 SM3 算法生成签名。 + /// + /// 经过编码后的(通常为十六进制)EC 私钥。 + /// 待签名的数据字节数组。 + /// 指示签名结果是否为 ASN.1 编码的形式。(默认值:true) + /// 签名字节数组。 + public static byte[] SignWithSM3ByECPrivateKey(EncodedString encodingECPrivateKey, byte[] messageBytes, bool asn1Encoding = true) + { + return SignWithSM3ByECPrivateKey( + encodingECPrivateKey: encodingECPrivateKey, + uidBytes: SM2_DEFAULT_UID, + messageBytes: messageBytes, + asn1Encoding: asn1Encoding + ); + } + + /// + /// 使用公钥基于 SM3 算法验证签名。 + /// + /// PKCS#8 公钥字节数组。 + /// 用户标识符字节数组。 + /// 待验证的数据字节数组。 + /// 签名字节数组。 + /// 指示签名结果是否为 ASN.1 编码的形式。(默认值:true) + /// 验证结果。 + public static bool VerifyWithSM3(byte[] publicKeyBytes, byte[] uidBytes, byte[] messageBytes, byte[] signBytes, bool asn1Encoding = true) + { + if (publicKeyBytes is null) throw new ArgumentNullException(nameof(publicKeyBytes)); + if (uidBytes is null) throw new ArgumentNullException(nameof(uidBytes)); + if (messageBytes is null) throw new ArgumentNullException(nameof(messageBytes)); + if (signBytes is null) throw new ArgumentNullException(nameof(signBytes)); + + ECPublicKeyParameters sm2PublicKeyParams = ParsePublicKeyToParameters(publicKeyBytes); + return VerifyWithSM3(sm2PublicKeyParams, uidBytes, messageBytes, signBytes, asn1Encoding); + } + + /// + /// 使用公钥基于 SM3 算法验证签名。 + /// + /// PKCS#8 公钥字节数组。 + /// 待验证的数据字节数组。 + /// 签名字节数组。 + /// 指示签名结果是否为 ASN.1 编码的形式。(默认值:true) + /// 验证结果。 + public static bool VerifyWithSM3(byte[] publicKeyBytes, byte[] messageBytes, byte[] signBytes, bool asn1Encoding = true) + { + return VerifyWithSM3( + publicKeyBytes: publicKeyBytes, + uidBytes: SM2_DEFAULT_UID, + messageBytes: messageBytes, + signBytes: signBytes, + asn1Encoding: asn1Encoding + ); + } + + /// + /// 使用公钥基于 SM3 算法验证签名。 + /// + /// PKCS#8 公钥(PEM 格式)。 + /// 待验证的数据。 + /// 经过编码后的(通常为 Base64)签名。 + /// 指示签名结果是否为 ASN.1 编码的形式。(默认值:true) + /// 验证结果。 + public static bool VerifyWithSM3(string publicKeyPem, string messageData, EncodedString encodingSignature, bool asn1Encoding = true) + { + if (publicKeyPem is null) throw new ArgumentNullException(nameof(publicKeyPem)); + if (messageData is null) throw new ArgumentNullException(nameof(messageData)); + if (encodingSignature.Value is null) throw new ArgumentNullException(nameof(encodingSignature)); + + return VerifyWithSM3( + publicKeyBytes: ConvertPublicKeyPemToByteArray(publicKeyPem), + messageBytes: EncodedString.FromLiteralString(messageData), + signBytes: EncodedString.FromString(encodingSignature, fallbackEncodingKind: EncodingKinds.Base64), + asn1Encoding: asn1Encoding + ); + } + + /// + /// 使用 EC 公钥基于 SM3 算法生成签名。 + /// + /// EC 公钥字节数组。 + /// 用户标识符字节数组。 + /// 待验证的数据字节数组。 + /// 签名字节数组。 + /// 指示签名结果是否为 ASN.1 编码的形式。(默认值:true) + /// 签名字节数组。 + public static bool VerifyWithSM3ByECPublicKey(byte[] ecPublicKeyBytes, byte[] uidBytes, byte[] messageBytes, byte[] signBytes, bool asn1Encoding = true) + { + if (ecPublicKeyBytes is null) throw new ArgumentNullException(nameof(ecPublicKeyBytes)); + if (uidBytes is null) throw new ArgumentNullException(nameof(uidBytes)); + if (messageBytes is null) throw new ArgumentNullException(nameof(messageBytes)); + + ECPublicKeyParameters sm2PublicKeyParams = ParseECPublicKeyToParameters(ecPublicKeyBytes); + return VerifyWithSM3(sm2PublicKeyParams, uidBytes, messageBytes, signBytes, asn1Encoding); + } + + /// + /// 使用 EC 公钥基于 SM3 算法生成签名。 + /// + /// EC 公钥字节数组。 + /// 待验证的数据字节数组。 + /// 签名字节数组。 + /// 指示签名结果是否为 ASN.1 编码的形式。(默认值:true) + /// 签名字节数组。 + public static bool VerifyWithSM3ByECPublicKey(byte[] ecPublicKeyBytes, byte[] messageBytes, byte[] signBytes, bool asn1Encoding = true) + { + return VerifyWithSM3ByECPublicKey( + ecPublicKeyBytes: ecPublicKeyBytes, + uidBytes: SM2_DEFAULT_UID, + messageBytes: messageBytes, + signBytes: signBytes, + asn1Encoding: asn1Encoding + ); + } + + /// + /// 使用 EC 公钥基于 SM3 算法生成签名。 + /// + /// 经过编码后的(通常为十六进制)EC 公钥。 + /// 用户标识符字节数组。 + /// 待验证的数据字节数组。 + /// 签名字节数组。 + /// 指示签名结果是否为 ASN.1 编码的形式。(默认值:true) + /// 签名字节数组。 + public static bool VerifyWithSM3ByECPublicKey(EncodedString encodingECPublicKey, byte[] uidBytes, byte[] messageBytes, byte[] signBytes, bool asn1Encoding = true) + { + if (encodingECPublicKey.Value is null) throw new ArgumentNullException(nameof(encodingECPublicKey)); + if (uidBytes is null) throw new ArgumentNullException(nameof(uidBytes)); + if (messageBytes is null) throw new ArgumentNullException(nameof(messageBytes)); + + return VerifyWithSM3ByECPublicKey( + ecPublicKeyBytes: EncodedString.FromString(encodingECPublicKey, fallbackEncodingKind: EncodingKinds.Hex), + uidBytes: uidBytes, + messageBytes: messageBytes, + signBytes: signBytes, + asn1Encoding: asn1Encoding + ); + } + + /// + /// 使用 EC 公钥基于 SM3 算法生成签名。 + /// + /// 经过编码后的(通常为十六进制)EC 公钥。 + /// 待验证的数据字节数组。 + /// 签名字节数组。 + /// 指示签名结果是否为 ASN.1 编码的形式。(默认值:true) + /// 签名字节数组。 + public static bool VerifyWithSM3ByECPublicKey(EncodedString encodingECPublicKey, byte[] messageBytes, byte[] signBytes, bool asn1Encoding = true) + { + return VerifyWithSM3ByECPublicKey( + encodingECPublicKey: encodingECPublicKey, + uidBytes: SM2_DEFAULT_UID, + messageBytes: messageBytes, + signBytes: signBytes, + asn1Encoding: asn1Encoding + ); + } + } +} diff --git a/src/SKIT.FlurlHttpClient.Wechat.Api/Utilities/SM4Utility.cs b/src/SKIT.FlurlHttpClient.Wechat.Api/Utilities/SM4Utility.cs new file mode 100644 index 00000000..c68c6291 --- /dev/null +++ b/src/SKIT.FlurlHttpClient.Wechat.Api/Utilities/SM4Utility.cs @@ -0,0 +1,138 @@ +using System; +using System.Text; +using Org.BouncyCastle.Crypto; +using Org.BouncyCastle.Crypto.Parameters; +using Org.BouncyCastle.Security; + +namespace SKIT.FlurlHttpClient.Wechat.Api.Utilities +{ + using SKIT.FlurlHttpClient.Primitives; + + /// + /// SM4 算法工具类。 + /// + public static class SM4Utility + { + /// + /// 填充模式:NoPadding。 + /// + public const string PADDING_MODE_NOPADDING = "NoPadding"; + + /// + /// 基于 GCM 模式解密数据。 + /// + /// SM4 密钥字节数组。 + /// 初始化向量字节数组。 + /// 附加数据字节数组。 + /// 待解密数据字节数组。 + /// 填充模式。(默认值:) + /// 解密后的数据字节数组。 + public static byte[] DecryptWithGCM(byte[] keyBytes, byte[] nonceBytes, byte[]? associatedDataBytes, byte[] cipherBytes, string paddingMode = PADDING_MODE_NOPADDING) + { + const int KEY_LENGTH_BYTE = 16; + const int NONCE_LENGTH_BYTE = 12; + const int TAG_LENGTH_BYTE = 16; + + if (keyBytes is null) throw new ArgumentNullException(nameof(keyBytes)); + if (keyBytes.Length != KEY_LENGTH_BYTE) throw new ArgumentException($"Invalid key byte length (expected: {KEY_LENGTH_BYTE}, actual: {keyBytes.Length}).", nameof(keyBytes)); + if (nonceBytes is null) throw new ArgumentNullException(nameof(nonceBytes)); + if (nonceBytes.Length != NONCE_LENGTH_BYTE) throw new ArgumentException($"Invalid nonce byte length (expected: {NONCE_LENGTH_BYTE}, actual: {nonceBytes.Length}).", nameof(nonceBytes)); + if (cipherBytes is null) throw new ArgumentNullException(nameof(cipherBytes)); + if (cipherBytes.Length < TAG_LENGTH_BYTE) throw new ArgumentException($"Invalid cipher byte length (expected: more than {TAG_LENGTH_BYTE}, actual: {cipherBytes.Length}).", nameof(cipherBytes)); + + IBufferedCipher cipher = CipherUtilities.GetCipher($"SM4/GCM/{paddingMode}"); + ICipherParameters cipherParams = new AeadParameters( + new KeyParameter(keyBytes), + TAG_LENGTH_BYTE * 8, + nonceBytes, + associatedDataBytes + ); + cipher.Init(false, cipherParams); + byte[] plainBytes = new byte[cipher.GetOutputSize(cipherBytes.Length)]; + int len = cipher.ProcessBytes(cipherBytes, 0, cipherBytes.Length, plainBytes, 0); + cipher.DoFinal(plainBytes, len); + return plainBytes; + } + + /// + /// 基于 GCM 模式解密数据。 + /// + /// 经过编码后的(通常为 Base64)SM4 密钥。 + /// 经过编码后的(通常为 Base64)初始化向量。 + /// 经过编码后的(通常为 Base64)附加数据。 + /// 经过编码后的(通常为 Base64)待解密数据。 + /// 填充模式。(默认值:) + /// 解密后的数据。 + public static EncodedString DecryptWithGCM(EncodedString encodingKey, EncodedString encodingNonce, EncodedString encodingAssociatedData, EncodedString encodingCipher, string paddingMode = PADDING_MODE_NOPADDING) + { + if (encodingKey.Value is null) throw new ArgumentNullException(nameof(encodingKey)); + if (encodingNonce.Value is null) throw new ArgumentNullException(nameof(encodingNonce)); + if (encodingCipher.Value is null) throw new ArgumentNullException(nameof(encodingCipher)); + + byte[] plainBytes = DecryptWithGCM( + keyBytes: EncodedString.FromString(encodingKey, fallbackEncodingKind: EncodingKinds.Base64), + nonceBytes: EncodedString.FromString(encodingNonce, fallbackEncodingKind: EncodingKinds.Base64), + associatedDataBytes: encodingAssociatedData.Value is not null ? EncodedString.FromString(encodingAssociatedData, fallbackEncodingKind: EncodingKinds.Base64) : null, + cipherBytes: EncodedString.FromString(encodingCipher, fallbackEncodingKind: EncodingKinds.Base64), + paddingMode: paddingMode + ); + return EncodedString.ToLiteralString(plainBytes); + } + + /// + /// 基于 GCM 模式加密数据。 + /// + /// SM4 密钥字节数组。 + /// 初始化向量字节数组。 + /// 附加数据字节数组。 + /// 待加密数据字节数组。 + /// 填充模式。(默认值:) + /// 加密后的数据字节数组。 + public static byte[] EncryptWithGCM(byte[] keyBytes, byte[] nonceBytes, byte[]? associatedDataBytes, byte[] plainBytes, string paddingMode = PADDING_MODE_NOPADDING) + { + const int TAG_LENGTH_BYTE = 16; + + if (keyBytes is null) throw new ArgumentNullException(nameof(keyBytes)); + if (nonceBytes is null) throw new ArgumentNullException(nameof(nonceBytes)); + if (plainBytes is null) throw new ArgumentNullException(nameof(plainBytes)); + + IBufferedCipher cipher = CipherUtilities.GetCipher($"SM4/GCM/{paddingMode}"); + ICipherParameters cipherParams = new AeadParameters( + new KeyParameter(keyBytes), + TAG_LENGTH_BYTE * 8, + nonceBytes, + associatedDataBytes + ); + cipher.Init(true, cipherParams); + byte[] cipherBytes = new byte[cipher.GetOutputSize(plainBytes.Length)]; + int len = cipher.ProcessBytes(plainBytes, 0, plainBytes.Length, cipherBytes, 0); + cipher.DoFinal(cipherBytes, len); + return cipherBytes; + } + + /// + /// 基于 GCM 模式加密数据。 + /// + /// 经过编码后的(通常为 Base64)SM4 密钥。 + /// 经过编码后的(通常为 Base64)初始化向量。 + /// 经过编码后的(通常为 Base64)附加数据。 + /// 待加密数据。 + /// 填充模式。(默认值:) + /// 经过 Base64 编码的加密后的数据。 + public static EncodedString EncryptWithGCM(EncodedString encodingKey, EncodedString encodingNonce, EncodedString encodingAssociatedData, string plainData, string paddingMode = PADDING_MODE_NOPADDING) + { + if (encodingKey.Value is null) throw new ArgumentNullException(nameof(encodingKey)); + if (encodingNonce.Value is null) throw new ArgumentNullException(nameof(encodingNonce)); + if (plainData is null) throw new ArgumentNullException(nameof(plainData)); + + byte[] plainBytes = EncryptWithGCM( + keyBytes: EncodedString.FromString(encodingKey, fallbackEncodingKind: EncodingKinds.Base64), + nonceBytes: EncodedString.FromString(encodingNonce, fallbackEncodingKind: EncodingKinds.Base64), + associatedDataBytes: encodingAssociatedData.Value is not null ? EncodedString.FromString(encodingAssociatedData, fallbackEncodingKind: EncodingKinds.Base64) : null, + plainBytes: Encoding.UTF8.GetBytes(plainData), + paddingMode: paddingMode + ); + return EncodedString.ToBase64String(plainBytes); + } + } +} diff --git a/test/SKIT.FlurlHttpClient.Wechat.Api.UnitTests/TestCase_ToolsAESUtilityTests.cs b/test/SKIT.FlurlHttpClient.Wechat.Api.UnitTests/TestCase_ToolsAESUtilityTests.cs index 8635b9c8..a5a1b88a 100644 --- a/test/SKIT.FlurlHttpClient.Wechat.Api.UnitTests/TestCase_ToolsAESUtilityTests.cs +++ b/test/SKIT.FlurlHttpClient.Wechat.Api.UnitTests/TestCase_ToolsAESUtilityTests.cs @@ -18,5 +18,33 @@ namespace SKIT.FlurlHttpClient.Wechat.Api.UnitTests Assert.Equal(expectedPlainData, actualPlainData, ignoreCase: true); } + + [Fact(DisplayName = "测试用例:AES-GCM 加密")] + public void TestAESGCMEncrypt() + { + string key = "b302fd4719dd26523d555e6fe392b91a"; + string nonce = "b302fd4719dd"; + string aad = "26523d555e6fe392b91a"; + string plainText = "Awesome SKIT.FlurlHttpClient.Wechat.Api!"; + + string actualPlain = Utilities.AESUtility.EncryptWithGCM(encodingKey: new EncodedString(key, EncodingKinds.Literal), encodingNonce: new EncodedString(nonce, EncodingKinds.Literal), encodingAssociatedData: new EncodedString(aad, EncodingKinds.Literal), plainData: plainText)!; + string expectedPlain = "bUGHWv7V1+rMtfIx4pIhD95i3J72AK+K1I0vsxBNWAUq8JLjUJ1iSY79kdqp2LCDe4IuOpPxXC0="; + + Assert.Equal(expectedPlain, actualPlain); + } + + [Fact(DisplayName = "测试用例:AES-GCM 解密")] + public void TestAESGCMDecrypt() + { + string key = "b302fd4719dd26523d555e6fe392b91a"; + string nonce = "b302fd4719dd"; + string aad = "26523d555e6fe392b91a"; + string cipherText = "bUGHWv7V1+rMtfIx4pIhD95i3J72AK+K1I0vsxBNWAUq8JLjUJ1iSY79kdqp2LCDe4IuOpPxXC0="; + + string actualPlain = Utilities.AESUtility.DecryptWithGCM(encodingKey: new EncodedString(key, EncodingKinds.Literal), encodingNonce: new EncodedString(nonce, EncodingKinds.Literal), encodingAssociatedData: new EncodedString(aad, EncodingKinds.Literal), encodingCipher: new EncodedString(cipherText, EncodingKinds.Base64))!; + string expectedPlain = "Awesome SKIT.FlurlHttpClient.Wechat.Api!"; + + Assert.Equal(expectedPlain, actualPlain); + } } } diff --git a/test/SKIT.FlurlHttpClient.Wechat.Api.UnitTests/TestCase_ToolsRSAUtilityTests.cs b/test/SKIT.FlurlHttpClient.Wechat.Api.UnitTests/TestCase_ToolsRSAUtilityTests.cs new file mode 100644 index 00000000..d69b1909 --- /dev/null +++ b/test/SKIT.FlurlHttpClient.Wechat.Api.UnitTests/TestCase_ToolsRSAUtilityTests.cs @@ -0,0 +1,42 @@ +using Xunit; + +namespace SKIT.FlurlHttpClient.Wechat.Api.UnitTests +{ + using SKIT.FlurlHttpClient.Primitives; + + public class TestCase_ToolsRSAUtilityTests + { + // 此处测试的 RSA 证书/公钥/私钥是自签名生成的,仅供执行 RSA 相关的单元测试,不能用于调用微信 API。 + private const string RSA_PEM_CERTIFICATE = "-----BEGIN CERTIFICATE-----\nMIIFRzCCAy8CFDBQ9y4tzgPn7+SVV90jHRdmSa+9MA0GCSqGSIb3DQEBCwUAMGAx\nCzAJBgNVBAYTAkNOMREwDwYDVQQIDAhTaGFuZ2hhaTERMA8GA1UEBwwIU2hhbmdo\nYWkxDTALBgNVBAoMBFNLSVQxDTALBgNVBAsMBFNLSVQxDTALBgNVBAMMBFNLSVQw\nHhcNMjExMTI1MTgzNzQ4WhcNMjExMjI1MTgzNzQ4WjBgMQswCQYDVQQGEwJDTjER\nMA8GA1UECAwIU2hhbmdoYWkxETAPBgNVBAcMCFNoYW5naGFpMQ0wCwYDVQQKDART\nS0lUMQ0wCwYDVQQLDARTS0lUMQ0wCwYDVQQDDARTS0lUMIICIjANBgkqhkiG9w0B\nAQEFAAOCAg8AMIICCgKCAgEA52DszUZzPKPo1d9Hi5Hjlu7OINwADaeXifA4rvmJ\nJaA+jm4DCMwrAMzyS12EiW31xCAF8LZ/xkrFHO5CZgvK87Y+kY9DmhvNX6FVYsn4\nay7KER0zo87zqQjC+njUu1rYuKnio7MYb354PitwQ3SWNv2qTCbCNCXTN9pJXNhl\nCudWCEWrNrYc4/hKz3bqu1DjpY0oHuuKPk/iRr2TTUIAwahNkNQheQNB2a8hL7L2\nOG1Sn1vaDWe+5RJYlMRZ3NgYDTqoy8GMs+6q091MQMDlQ90jtW/JEoM5DUyI8zfQ\nfDLGnU7FuY0rrZ/+6OQT/o7ISf0OR5TISS0lqnDN3vVaph0ftDGRdGqJk2SJAHIo\nxp5gt410rfWS9kpSDFJs3Pvt4rtNZBYvkGD8obSm91brAkoX4+u1Y4p1qZpWJ4LI\nKw8oyeieqlLZtF/VGKOtKxe/IKn8GwoQJLx4dUGFOqM7HPwR9cyjMaC1o3V1NQG+\n1wD9TLtGh3WXUFJRYDmePaSp39GFPupTMlPRbD0RK80B6xv2rYTyYyd8s2LN6P6H\nh/nFIkc1rekIf9JhPy0WKzrXdmnfjSHKPxmz0WSYN8FxKasqcJhncOdhLTzzVEhj\n9xHSI8ejP2fJ4v+ARoD3GURPD9H7KMa7xmzRSAZ8A8LM3uvdJNhbKBwWqvo45ncz\n+7cCAwEAATANBgkqhkiG9w0BAQsFAAOCAgEAVTS6oMfDA3XTwEel0BvaXMCdo7yM\ns5ueM87151eywnPlConYDXeqhfF0OCSBnY2g7Fpmn+YAUoa/L+FNOx/gMC9QV/lP\nHhhAcWpiCRy52RX/IyTDxFD6OqtH0BaBtDTb+QBXZuFypMUkPy6EUYs5Cl9qYepy\nHcgGVomx7tcwWcvI4o/KZtj8hXC5wu/k4Y0GGUriTt8xmnJ+RTRedZ6hzAFVHtXm\n/YIT9Lc1IIYZuHVyCbX/HXwa0E4r8lghwZRg94HUvpbfabNA3obt5auwtJUfW1tK\n2ERgFrtBRBWf9EGb8TstXqksqYZ04U4OjLm/3ZJhSSYKNbriRLlSEzAlHikNVW+t\n6cTh+sasrGt/qNIRMs5PiipwmV/T3z1LbyoiU7fXZ4GqiWBnZARFC9KiPPTzLszh\nBKJGYHaC8wkGb3WfNWFBqVRfFL8kdME+shLB8/ETQ31gIFeudnW1QlujJ7ZSZtwz\nxT3HxzZIIbNEqLFP+d37kmuKjRmI4KWc+pKOUw9BOl4g/TJH6ySljSNs8LSDWwQY\n76Dsnr+ovz8ZVLNUCmedZCyumeJo2tLkJmsPo5GuMnXpL94mhqpCoUS4l4JbJl44\nT2lmqp1Ueoz+Qlkqyt2lj3heTv9bvB7NO9KHTsDy1hhWHOG1QyXzajyWETU+1XdW\nx1hGvYxtpQPLUE8=\n-----END CERTIFICATE-----"; + private const string RSA_PEM_PUBLIC_KEY_PKCS8 = "-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA52DszUZzPKPo1d9Hi5Hj\nlu7OINwADaeXifA4rvmJJaA+jm4DCMwrAMzyS12EiW31xCAF8LZ/xkrFHO5CZgvK\n87Y+kY9DmhvNX6FVYsn4ay7KER0zo87zqQjC+njUu1rYuKnio7MYb354PitwQ3SW\nNv2qTCbCNCXTN9pJXNhlCudWCEWrNrYc4/hKz3bqu1DjpY0oHuuKPk/iRr2TTUIA\nwahNkNQheQNB2a8hL7L2OG1Sn1vaDWe+5RJYlMRZ3NgYDTqoy8GMs+6q091MQMDl\nQ90jtW/JEoM5DUyI8zfQfDLGnU7FuY0rrZ/+6OQT/o7ISf0OR5TISS0lqnDN3vVa\nph0ftDGRdGqJk2SJAHIoxp5gt410rfWS9kpSDFJs3Pvt4rtNZBYvkGD8obSm91br\nAkoX4+u1Y4p1qZpWJ4LIKw8oyeieqlLZtF/VGKOtKxe/IKn8GwoQJLx4dUGFOqM7\nHPwR9cyjMaC1o3V1NQG+1wD9TLtGh3WXUFJRYDmePaSp39GFPupTMlPRbD0RK80B\n6xv2rYTyYyd8s2LN6P6Hh/nFIkc1rekIf9JhPy0WKzrXdmnfjSHKPxmz0WSYN8Fx\nKasqcJhncOdhLTzzVEhj9xHSI8ejP2fJ4v+ARoD3GURPD9H7KMa7xmzRSAZ8A8LM\n3uvdJNhbKBwWqvo45ncz+7cCAwEAAQ==\n-----END PUBLIC KEY-----"; + private const string RSA_PEM_PUBLIC_KEY_PKCS1 = "-----BEGIN RSA PUBLIC KEY-----\nMIICCgKCAgEA52DszUZzPKPo1d9Hi5Hjlu7OINwADaeXifA4rvmJJaA+jm4DCMwr\nAMzyS12EiW31xCAF8LZ/xkrFHO5CZgvK87Y+kY9DmhvNX6FVYsn4ay7KER0zo87z\nqQjC+njUu1rYuKnio7MYb354PitwQ3SWNv2qTCbCNCXTN9pJXNhlCudWCEWrNrYc\n4/hKz3bqu1DjpY0oHuuKPk/iRr2TTUIAwahNkNQheQNB2a8hL7L2OG1Sn1vaDWe+\n5RJYlMRZ3NgYDTqoy8GMs+6q091MQMDlQ90jtW/JEoM5DUyI8zfQfDLGnU7FuY0r\nrZ/+6OQT/o7ISf0OR5TISS0lqnDN3vVaph0ftDGRdGqJk2SJAHIoxp5gt410rfWS\n9kpSDFJs3Pvt4rtNZBYvkGD8obSm91brAkoX4+u1Y4p1qZpWJ4LIKw8oyeieqlLZ\ntF/VGKOtKxe/IKn8GwoQJLx4dUGFOqM7HPwR9cyjMaC1o3V1NQG+1wD9TLtGh3WX\nUFJRYDmePaSp39GFPupTMlPRbD0RK80B6xv2rYTyYyd8s2LN6P6Hh/nFIkc1rekI\nf9JhPy0WKzrXdmnfjSHKPxmz0WSYN8FxKasqcJhncOdhLTzzVEhj9xHSI8ejP2fJ\n4v+ARoD3GURPD9H7KMa7xmzRSAZ8A8LM3uvdJNhbKBwWqvo45ncz+7cCAwEAAQ==\n-----END RSA PUBLIC KEY-----\r\n"; + private const string RSA_PEM_PRIVATE_KEY_PKCS8 = "-----BEGIN PRIVATE KEY-----\n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\nhpUq0eKgdGHc2R4r57soRvGoGy2DtA==\n-----END PRIVATE KEY-----"; + private const string RSA_PEM_PRIVATE_KEY_PKCS1 = "-----BEGIN RSA PRIVATE KEY-----\n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\n-----END RSA PRIVATE KEY-----\r\n"; + + [Fact(DisplayName = "测试用例:SHA256WithRSA 签名生成")] + public void TestRSASignatureSHA256WithRSASign() + { + string msgText = "SHA256WithRSASignTest"; + string actualSignByPrivateKeyPkcs8 = Utilities.RSAUtility.SignWithSHA256(RSA_PEM_PRIVATE_KEY_PKCS8, msgText)!; + string actualSignByPrivateKeyPkcs1 = Utilities.RSAUtility.SignWithSHA256(RSA_PEM_PRIVATE_KEY_PKCS1, msgText)!; + string expectedSign = "EzeVEIoBhzOhXpwbXdJjIuGIGRc6ArKO7sVo2fuAdzYTDgorAEufEnw7lPPXV1GTfFcHOnsAJH9kGJmg7Orwlkh7x7TyOGrkMEhWGulA9SIdmou4iBsHpIZ/TERSgGt/VTmqmfpkzJqrvbQWIQENwo7Lr6uJSJBND0YT3nIBP8TzbO3cHnQb6chHIBHrDF5vOO7HHu+Cga2MZnAtRizhO8BhK0jOmyro32CgIML3EVX8yuPy0kOk6aN1R8xFblZUD4NU2M6zzQpydmxaHr9B1WNFoMwmpoAS5BuKJMYOHO5cc6DhB+0fAGxaWtKp6759KhKCf8M65zh3WKS4l262SGuWq4qG1+AKf2DOgCon769+A4z8flOmvl0iIwoH9FThGJoP156rpAJW7v/bWputSeC6WToUTBRmGWdwWySVwW5AZ26OAFFWs1CmrGp3jF5E2oUy1mQwgfM0QN6DW+wD769ggIYH9HLHqDHbF5UyF7eNh3s8Qy23xXEKZWNMAJ0IdtdMQ7FRRgLFSCai7HELLlBJSCz7P5WTeYZGQpbvnUShkRvzujjO6XlGiKKI0EwKb121z8N6KRpvs4SnRztWBGoXbzHZgnXKXU/BWWADemqB2cvaT3Bj0k3/N3sea0dAEtlNEklRWoyyNUUlscK9zg4LBlHrhbbFo66uuub8ySo="; + + Assert.Equal(expectedSign, actualSignByPrivateKeyPkcs8); + Assert.Equal(expectedSign, actualSignByPrivateKeyPkcs1); + Assert.True(Utilities.RSAUtility.VerifyWithSHA256(RSA_PEM_PUBLIC_KEY_PKCS8, msgText, (EncodedString)actualSignByPrivateKeyPkcs8)); + Assert.True(Utilities.RSAUtility.VerifyWithSHA256(RSA_PEM_PUBLIC_KEY_PKCS1, msgText, (EncodedString)actualSignByPrivateKeyPkcs1)); + } + + [Fact(DisplayName = "测试用例:SHA256WithRSA 签名验证")] + public void TestRSASignatureSHA256WithRSAVerify() + { + string msgText = "SHA256WithRSAVerifyTest"; + string signText = "aHX+MrmZHDEraMKBEPV2Vnps1B9b25lGbv/rdppx/S7+oaXtjKJprzCq5H7RCpvrKS3xYIeTEPwQGC3Vots7dCdLi8v8ew1vvtXf8qNAnd7CTMHqu3wSohXzgyASTmNbXE2ml9LbWYPPYMvPJXROQbGVjoOrsErWBPPJYXuO3lIckIfwI05OTdl4H3+BvpD/ZoljRp8Qgo9+paGvarBc++TaAh0FXnQf0TGNFUIeHHiAKBee5oCBTuZZM9J5RPw0oIq/g7Wun+e/zWiwVBPHltOgZrV46uagSAE6nBDHk+hlNxDivCxkJdBVCSIYFFmBXIcnGZ/u4ZfBui/k1jGoKibyvPK4z2+6GSlj41Yo81kuSBfzLiSsx33EPR1eIJJkwDTsvap0ymL9pfIqMiLuiteH5kGmL/dyONy9oAJywLEeITfoVyElM/CY6Dc+xDhRnjN7Hu54meYyXRZrnCtQ3YhzEr1immNBn6npgA/qi9aHsuWFOw8b8aSwOHDHTDmjmvV+axI8CVMrR0MjB9QNCWrKLq2B9iQX9MtLgcUyDsQvzAsxUJm/OEfzUjs9SHvmgmyAvzNAuTdO7wLQ+ZmKg0yZne6nvcrJVvfh3lD5ZPt7NY57Y6OIJluqKUT5H+a3H6W9Q1Z+cBMnHGYaaK7Tv8IcDdEYqTIG8hc5BqjFOzE="; + + Assert.True(Utilities.RSAUtility.VerifyWithSHA256(RSA_PEM_PUBLIC_KEY_PKCS8, msgText, (EncodedString)signText)); + Assert.True(Utilities.RSAUtility.VerifyWithSHA256(RSA_PEM_PUBLIC_KEY_PKCS1, msgText, (EncodedString)signText)); + Assert.False(Utilities.RSAUtility.VerifyWithSHA256(RSA_PEM_PUBLIC_KEY_PKCS8, msgText, (EncodedString)"FAKE SIGN")); + Assert.False(Utilities.RSAUtility.VerifyWithSHA256(RSA_PEM_PUBLIC_KEY_PKCS1, msgText, (EncodedString)"FAKE SIGN")); + } + } +} diff --git a/test/SKIT.FlurlHttpClient.Wechat.Api.UnitTests/TestCase_ToolsSM2UtilityTests.cs b/test/SKIT.FlurlHttpClient.Wechat.Api.UnitTests/TestCase_ToolsSM2UtilityTests.cs new file mode 100644 index 00000000..8887f791 --- /dev/null +++ b/test/SKIT.FlurlHttpClient.Wechat.Api.UnitTests/TestCase_ToolsSM2UtilityTests.cs @@ -0,0 +1,42 @@ +using System; +using System.Text; +using Xunit; + +namespace SKIT.FlurlHttpClient.Wechat.Api.UnitTests +{ + using SKIT.FlurlHttpClient.Primitives; + + public class TestCase_ToolsSM2UtilityTests + { + // 此处测试的 SM2 证书/公钥/私钥是自签名生成的,仅供执行 SM2 相关的单元测试,不能用于调用微信 API。 + private const string SM2_PEM_CERTIFICATE = "-----BEGIN CERTIFICATE-----\nMIICNzCCAdygAwIBAgIJAOWoGwJCnY0IMAoGCCqBHM9VAYN1MGcxCzAJBgNVBAYT\nAkNOMRAwDgYDVQQIDAdCZWlqaW5nMRAwDgYDVQQHDAdIYWlEaWFuMRMwEQYDVQQK\nDApHTUNlcnQub3JnMR8wHQYDVQQDDBZHTUNlcnQgR00gUm9vdCBDQSAtIDAxMB4X\nDTIyMTEwOTEzMTIyMFoXDTIzMTEwOTEzMTIyMFowSzEtMCsGA1UEAwwkU0tJVC5G\nbHVybEh0dHBDbGllbnQuV2VjaGF0LlRlbnBheVYzMQ0wCwYDVQQKDARTS0lUMQsw\nCQYDVQQGEwJDTjBZMBMGByqGSM49AgEGCCqBHM9VAYItA0IABMXP1hZc2zBzreRN\nZgOR9hklE01tw10RDUfj176EXcVoVOvITMENJ3HREQtDPlOfz8i1SXCQEwclYyxI\n2KcTdKqjgYwwgYkwDAYDVR0TAQH/BAIwADALBgNVHQ8EBAMCA/gwLAYJYIZIAYb4\nQgENBB8WHUdNQ2VydC5vcmcgU2lnbmVkIENlcnRpZmljYXRlMB0GA1UdDgQWBBRj\nIhoxmSgP84XT/scjkQNSWylMFTAfBgNVHSMEGDAWgBR/Wl47AIRZKg+YvqEObzmV\nQxBNBzAKBggqgRzPVQGDdQNJADBGAiEAnXykM0qDOWay2EMB6+c6YJ7h4n7Wbju7\nXuT5RkuM/3ICIQDAA3sLba/dQMhmKkCoJl31iZwYKz7NP+0aq6NhWDommQ==\n-----END CERTIFICATE-----"; + private const string SM2_PEM_PUBLIC_KEY = "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoEcz1UBgi0DQgAExc/WFlzbMHOt5E1mA5H2GSUTTW3D\nXRENR+PXvoRdxWhU68hMwQ0ncdERC0M+U5/PyLVJcJATByVjLEjYpxN0qg==\n-----END PUBLIC KEY-----"; + private const string SM2_PEM_PRIVATE_KEY = "-----BEGIN PRIVATE KEY-----\nMIGTAgEAMBMGByqGSM49AgEGCCqBHM9VAYItBHkwdwIBAQQg3WePog9R4UV/EVlk\nCw8YHu+rXC/imiB89jFmaAPeXz6gCgYIKoEcz1UBgi2hRANCAATFz9YWXNswc63k\nTWYDkfYZJRNNbcNdEQ1H49e+hF3FaFTryEzBDSdx0RELQz5Tn8/ItUlwkBMHJWMs\nSNinE3Sq\n-----END PRIVATE KEY-----"; + private const string SM2_HEX_EC_PRIVATE_KEY = "dd678fa20f51e1457f1159640b0f181eefab5c2fe29a207cf631666803de5f3e"; + private const string SM2_HEX_EC_PUBLIC_KEY = "04c5cfd6165cdb3073ade44d660391f61925134d6dc35d110d47e3d7be845dc56854ebc84cc10d2771d1110b433e539fcfc8b5497090130725632c48d8a71374aa"; + + [Fact(DisplayName = "测试用例:SM2WithSM3 签名生成")] + public void TestSM2SignatureSM2WithSM3Sign() + { + string msgText = "SM2WithSM3SignTest"; + string actualSignByPrivateKey = Utilities.SM2Utility.SignWithSM3(SM2_PEM_PRIVATE_KEY, msgText)!; + string actualSignByECPrivateKey = Convert.ToBase64String(Utilities.SM2Utility.SignWithSM3ByECPrivateKey((EncodedString)SM2_HEX_EC_PRIVATE_KEY, Encoding.UTF8.GetBytes(msgText))); + + Assert.NotNull(actualSignByPrivateKey); + Assert.NotNull(actualSignByECPrivateKey); + Assert.True(Utilities.SM2Utility.VerifyWithSM3(SM2_PEM_PUBLIC_KEY, msgText, (EncodedString)actualSignByPrivateKey)); + } + + [Fact(DisplayName = "测试用例:SM2WithSM3 签名验证")] + public void TestSM2SignatureSM2WithSM3Verify() + { + string msgText = "SM2WithSM3SignTest"; + string signText = "MEUCIQCDzgpF2Z//sbFzASVQnwme2phm4ho5cr8/1Pz0+MONTwIgeQvhoWOTk1rngYRSlHeqqwtNFVD/vf3qtgl9mecvERI="; + + Assert.True(Utilities.SM2Utility.VerifyWithSM3(SM2_PEM_PUBLIC_KEY, msgText, (EncodedString)signText)); + Assert.False(Utilities.SM2Utility.VerifyWithSM3(SM2_PEM_PUBLIC_KEY, msgText, (EncodedString)"FAKE SIGN")); + Assert.True(Utilities.SM2Utility.VerifyWithSM3ByECPublicKey((EncodedString)SM2_HEX_EC_PUBLIC_KEY, Encoding.UTF8.GetBytes(msgText), Convert.FromBase64String("MEUCIQCDzgpF2Z//sbFzASVQnwme2phm4ho5cr8/1Pz0+MONTwIgeQvhoWOTk1rngYRSlHeqqwtNFVD/vf3qtgl9mecvERI="))); + Assert.False(Utilities.SM2Utility.VerifyWithSM3ByECPublicKey((EncodedString)SM2_HEX_EC_PUBLIC_KEY, Encoding.UTF8.GetBytes(msgText), Encoding.UTF8.GetBytes("FAKE SIGN"))); + } + } +} diff --git a/test/SKIT.FlurlHttpClient.Wechat.Api.UnitTests/TestCase_ToolsSM4UtilityTests.cs b/test/SKIT.FlurlHttpClient.Wechat.Api.UnitTests/TestCase_ToolsSM4UtilityTests.cs new file mode 100644 index 00000000..867e8b5a --- /dev/null +++ b/test/SKIT.FlurlHttpClient.Wechat.Api.UnitTests/TestCase_ToolsSM4UtilityTests.cs @@ -0,0 +1,37 @@ +using Xunit; + +namespace SKIT.FlurlHttpClient.Wechat.Api.UnitTests +{ + using SKIT.FlurlHttpClient.Primitives; + + public class TestCase_ToolsSM4UtilityTests + { + [Fact(DisplayName = "测试用例:SM4-GCM 加密")] + public void TestSM4GCMEncrypt() + { + string key = "b302fd4719dd2652"; + string nonce = "b302fd4719dd"; + string aad = "26523d555e6fe392b91a"; + string plainText = "Awesome SKIT.FlurlHttpClient.Wechat.Api!"; + + string actualPlain = Utilities.SM4Utility.EncryptWithGCM(encodingKey: new EncodedString(key, EncodingKinds.Literal), encodingNonce: new EncodedString(nonce, EncodingKinds.Literal), encodingAssociatedData: new EncodedString(aad, EncodingKinds.Literal), plainData: plainText)!; + string expectedPlain = "FqW9TkELJ5soXCSksDvqNvvBIge3rRrJAc8FP8tGaQqIHx2BljkSIfXn3miUL2FBnkpBCJ96V88="; + + Assert.Equal(expectedPlain, actualPlain); + } + + [Fact(DisplayName = "测试用例:SM4-GCM 解密")] + public void TestSM4GCMDecrypt() + { + string key = "b302fd4719dd2652"; + string nonce = "b302fd4719dd"; + string aad = "26523d555e6fe392b91a"; + string cipherText = "FqW9TkELJ5soXCSksDvqNvvBIge3rRrJAc8FP8tGaQqIHx2BljkSIfXn3miUL2FBnkpBCJ96V88="; + + string actualPlain = Utilities.SM4Utility.DecryptWithGCM(encodingKey: new EncodedString(key, EncodingKinds.Literal), encodingNonce: new EncodedString(nonce, EncodingKinds.Literal), encodingAssociatedData: new EncodedString(aad, EncodingKinds.Literal), encodingCipher: new EncodedString(cipherText, EncodingKinds.Base64))!; + string expectedPlain = "Awesome SKIT.FlurlHttpClient.Wechat.Api!"; + + Assert.Equal(expectedPlain, actualPlain); + } + } +}