refactor(work): 优化加解密及哈希算法工具类

This commit is contained in:
Fu Diwei 2024-02-04 22:53:47 +08:00 committed by RHQYZ
parent 69955a1f2b
commit a11df544e5
8 changed files with 86 additions and 75 deletions

View File

@ -7,6 +7,7 @@ using System.Threading.Tasks;
namespace SKIT.FlurlHttpClient.Wechat.Work.ExtendedSDK.Finance namespace SKIT.FlurlHttpClient.Wechat.Work.ExtendedSDK.Finance
{ {
using SKIT.FlurlHttpClient.Primitives;
using SKIT.FlurlHttpClient.Wechat.Work.ExtendedSDK.Finance.InteropServices; using SKIT.FlurlHttpClient.Wechat.Work.ExtendedSDK.Finance.InteropServices;
/// <summary> /// <summary>
@ -200,9 +201,9 @@ namespace SKIT.FlurlHttpClient.Wechat.Work.ExtendedSDK.Finance
throw new WechatWorkFinanceException($"Failed to decrypt random key of the encrypted chat data, because there is no private key matched the verion: \"{request.PublicKeyVersion}\"."); throw new WechatWorkFinanceException($"Failed to decrypt random key of the encrypted chat data, because there is no private key matched the verion: \"{request.PublicKeyVersion}\".");
encryptKey = Utilities.RSAUtility.DecryptWithECB( encryptKey = Utilities.RSAUtility.DecryptWithECB(
privateKey: encryptionKeyEntry.Value.PrivateKey, privateKeyPem: encryptionKeyEntry.Value.PrivateKey,
cipherText: request.EncryptedRandomKey encodingCipher: new EncodedString(request.EncryptedRandomKey, EncodingKinds.Base64)
); )!;
} }
catch (WechatWorkFinanceException) catch (WechatWorkFinanceException)
{ {

View File

@ -29,7 +29,7 @@ namespace SKIT.FlurlHttpClient.Wechat.Work
string timestamp = DateTimeOffset.Now.ToLocalTime().ToUnixTimeSeconds().ToString(); string timestamp = DateTimeOffset.Now.ToLocalTime().ToUnixTimeSeconds().ToString();
string nonce = Guid.NewGuid().ToString("N"); string nonce = Guid.NewGuid().ToString("N");
string sign = Utilities.SHA1Utility.Hash($"jsapi_ticket={jsapiTicket}&noncestr={nonce}&timestamp={timestamp}&url={url.Split('#')[0]}").ToLower(); string sign = Utilities.SHA1Utility.Hash($"jsapi_ticket={jsapiTicket}&noncestr={nonce}&timestamp={timestamp}&url={url.Split('#')[0]}").Value!.ToLower();
return new ReadOnlyDictionary<string, string>(new Dictionary<string, string>() return new ReadOnlyDictionary<string, string>(new Dictionary<string, string>()
{ {
@ -58,7 +58,7 @@ namespace SKIT.FlurlHttpClient.Wechat.Work
string timestamp = DateTimeOffset.Now.ToLocalTime().ToUnixTimeSeconds().ToString(); string timestamp = DateTimeOffset.Now.ToLocalTime().ToUnixTimeSeconds().ToString();
string nonce = Guid.NewGuid().ToString("N"); string nonce = Guid.NewGuid().ToString("N");
string sign = Utilities.SHA1Utility.Hash($"jsapi_ticket={jsapiTicket}&noncestr={nonce}&timestamp={timestamp}&url={url.Split('#')[0]}").ToLower(); string sign = Utilities.SHA1Utility.Hash($"jsapi_ticket={jsapiTicket}&noncestr={nonce}&timestamp={timestamp}&url={url.Split('#')[0]}").Value!.ToLower();
return new ReadOnlyDictionary<string, string>(new Dictionary<string, string>() return new ReadOnlyDictionary<string, string>(new Dictionary<string, string>()
{ {

View File

@ -1,29 +1,32 @@
using System; using System;
using System.Text;
using Org.BouncyCastle.Crypto; using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Crypto.Parameters; using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.Security; using Org.BouncyCastle.Security;
namespace SKIT.FlurlHttpClient.Wechat.Work.Utilities namespace SKIT.FlurlHttpClient.Wechat.Work.Utilities
{ {
using SKIT.FlurlHttpClient.Primitives;
/// <summary> /// <summary>
/// AES 算法工具类。 /// AES 算法工具类。
/// </summary> /// </summary>
public static class AESUtility public static class AESUtility
{ {
private const string AES_CIPHER_ALGORITHM_GCM = "AES/GCM"; /// <summary>
private const string AES_CIPHER_PADDING_NOPADDING = "NoPadding"; /// 填充模式NoPadding。
/// </summary>
public const string PADDING_MODE_NOPADDING = "NoPadding";
/// <summary> /// <summary>
/// 基于 GCM 模式解密数据。 /// 基于 GCM 模式解密数据。
/// </summary> /// </summary>
/// <param name="keyBytes">AES 密钥字节数组。</param> /// <param name="keyBytes">AES 密钥字节数组。</param>
/// <param name="nonceBytes">加密使用的初始化向量字节数组。</param> /// <param name="nonceBytes">初始化向量字节数组。</param>
/// <param name="aadBytes">加密使用的附加数据字节数组。</param> /// <param name="associatedDataBytes">附加数据字节数组。</param>
/// <param name="cipherBytes">待解密数据字节数组。</param> /// <param name="cipherBytes">待解密数据字节数组。</param>
/// <param name="paddingMode">填充模式。(默认值:<see cref="AES_CIPHER_PADDING_NOPADDING"/></param> /// <param name="paddingMode">填充模式。(默认值:<see cref="PADDING_MODE_NOPADDING"/></param>
/// <returns>解密后的数据字节数组。</returns> /// <returns>解密后的数据字节数组。</returns>
public static byte[] DecryptWithGCM(byte[] keyBytes, byte[] nonceBytes, byte[]? aadBytes, byte[] cipherBytes, string paddingMode = AES_CIPHER_PADDING_NOPADDING) 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 KEY_LENGTH_BYTE = 32;
const int NONCE_LENGTH_BYTE = 12; const int NONCE_LENGTH_BYTE = 12;
@ -36,12 +39,12 @@ namespace SKIT.FlurlHttpClient.Wechat.Work.Utilities
if (cipherBytes is null) throw new ArgumentNullException(nameof(cipherBytes)); 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 (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(string.Format("{0}/{1}", AES_CIPHER_ALGORITHM_GCM, paddingMode)); IBufferedCipher cipher = CipherUtilities.GetCipher($"AES/GCM/{paddingMode}");
ICipherParameters cipherParams = new AeadParameters( ICipherParameters cipherParams = new AeadParameters(
new KeyParameter(keyBytes), new KeyParameter(keyBytes),
TAG_LENGTH_BYTE * 8, TAG_LENGTH_BYTE * 8,
nonceBytes, nonceBytes,
aadBytes associatedDataBytes
); );
cipher.Init(false, cipherParams); cipher.Init(false, cipherParams);
byte[] plainBytes = new byte[cipher.GetOutputSize(cipherBytes.Length)]; byte[] plainBytes = new byte[cipher.GetOutputSize(cipherBytes.Length)];
@ -53,26 +56,26 @@ namespace SKIT.FlurlHttpClient.Wechat.Work.Utilities
/// <summary> /// <summary>
/// 基于 GCM 模式解密数据。 /// 基于 GCM 模式解密数据。
/// </summary> /// </summary>
/// <param name="key">AES 密钥。</param> /// <param name="encodingKey">经过编码后的(通常为 Base64AES 密钥。</param>
/// <param name="nonce">加密使用的初始化向量。</param> /// <param name="encodingNonce">经过编码后的(通常为 Base64初始化向量。</param>
/// <param name="aad">加密使用的附加数据包。</param> /// <param name="encodingAssociatedData">经过编码后的(通常为 Base64附加数据。</param>
/// <param name="cipherText">经 Base64 编码后的待解密数据。</param> /// <param name="encodingCipher">经过编码后的(通常为 Base64待解密数据。</param>
/// <param name="paddingMode">填充模式。(默认值:<see cref="AES_CIPHER_PADDING_NOPADDING"/></param> /// <param name="paddingMode">填充模式。(默认值:<see cref="PADDING_MODE_NOPADDING"/></param>
/// <returns>解密后的文本数据。</returns> /// <returns>解密后的数据。</returns>
public static string DecryptWithGCM(string key, string nonce, string? aad, string cipherText, string paddingMode = AES_CIPHER_PADDING_NOPADDING) public static EncodedString DecryptWithGCM(EncodedString encodingKey, EncodedString encodingNonce, EncodedString encodingAssociatedData, EncodedString encodingCipher, string paddingMode = PADDING_MODE_NOPADDING)
{ {
if (key is null) throw new ArgumentNullException(nameof(key)); if (encodingKey.Value is null) throw new ArgumentNullException(nameof(encodingKey));
if (nonce is null) throw new ArgumentNullException(nameof(nonce)); if (encodingNonce.Value is null) throw new ArgumentNullException(nameof(encodingNonce));
if (cipherText is null) throw new ArgumentNullException(nameof(cipherText)); if (encodingCipher.Value is null) throw new ArgumentNullException(nameof(encodingCipher));
byte[] plainBytes = DecryptWithGCM( byte[] plainBytes = DecryptWithGCM(
keyBytes: Encoding.UTF8.GetBytes(key), keyBytes: EncodedString.FromString(encodingKey, fallbackEncodingKind: EncodingKinds.Base64),
nonceBytes: Encoding.UTF8.GetBytes(nonce), nonceBytes: EncodedString.FromString(encodingNonce, fallbackEncodingKind: EncodingKinds.Base64),
aadBytes: aad is not null ? Encoding.UTF8.GetBytes(aad) : null, associatedDataBytes: encodingAssociatedData.Value is not null ? EncodedString.FromString(encodingAssociatedData, fallbackEncodingKind: EncodingKinds.Base64) : null,
cipherBytes: Convert.FromBase64String(cipherText), cipherBytes: EncodedString.FromString(encodingCipher, fallbackEncodingKind: EncodingKinds.Base64),
paddingMode: paddingMode paddingMode: paddingMode
); );
return Encoding.UTF8.GetString(plainBytes); return EncodedString.ToLiteralString(plainBytes);
} }
} }
} }

View File

@ -1,6 +1,5 @@
using System; using System;
using System.IO; using System.IO;
using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using Org.BouncyCastle.Crypto; using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Crypto.Parameters; using Org.BouncyCastle.Crypto.Parameters;
@ -9,17 +8,21 @@ using Org.BouncyCastle.Security;
namespace SKIT.FlurlHttpClient.Wechat.Work.Utilities namespace SKIT.FlurlHttpClient.Wechat.Work.Utilities
{ {
using SKIT.FlurlHttpClient.Primitives;
/// <summary> /// <summary>
/// RSA 算法工具类。 /// RSA 算法工具类。
/// </summary> /// </summary>
public static class RSAUtility public static class RSAUtility
{ {
private const string RSA_CIPHER_ALGORITHM_NONE = "RSA/ECB"; /// <summary>
private const string RSA_CIPHER_PADDING_PKCS1 = "PKCS1PADDING"; /// 填充模式PKCS1Padding。
/// </summary>
public const string PADDING_MODE_PKCS1 = "PKCS1PADDING";
private static byte[] ConvertPrivateKeyPkcs1PemToByteArray(string privateKey) private static byte[] ConvertPrivateKeyPemToByteArray(string privateKeyPem)
{ {
using (TextReader textReader = new StringReader(privateKey)) using (TextReader textReader = new StringReader(privateKeyPem))
using (PemReader pemReader = new PemReader(textReader)) using (PemReader pemReader = new PemReader(textReader))
{ {
AsymmetricCipherKeyPair cipherKeyPair = (AsymmetricCipherKeyPair)pemReader.ReadObject(); AsymmetricCipherKeyPair cipherKeyPair = (AsymmetricCipherKeyPair)pemReader.ReadObject();
@ -30,24 +33,19 @@ namespace SKIT.FlurlHttpClient.Wechat.Work.Utilities
pemWriter.WriteObject(pkcs8); pemWriter.WriteObject(pkcs8);
pemWriter.Writer.Close(); pemWriter.Writer.Close();
privateKey = textWriter.ToString()!; privateKeyPem = textWriter.ToString()!;
privateKey = privateKey privateKeyPem = privateKeyPem
.Replace("-----BEGIN PRIVATE KEY-----", string.Empty) .Replace("-----BEGIN PRIVATE KEY-----", string.Empty)
.Replace("-----END PRIVATE KEY-----", string.Empty); .Replace("-----END PRIVATE KEY-----", string.Empty);
privateKey = Regex.Replace(privateKey, "\\s+", string.Empty); privateKeyPem = Regex.Replace(privateKeyPem, "\\s+", string.Empty);
return Convert.FromBase64String(privateKey); return Convert.FromBase64String(privateKeyPem);
} }
} }
} }
private static RsaKeyParameters ParsePrivateKeyPemToPrivateKeyParameters(byte[] privateKeyBytes)
{
return (RsaKeyParameters)PrivateKeyFactory.CreateKey(privateKeyBytes);
}
private static byte[] DecryptWithECB(RsaKeyParameters rsaPrivateKeyParams, byte[] cipherBytes, string paddingMode) private static byte[] DecryptWithECB(RsaKeyParameters rsaPrivateKeyParams, byte[] cipherBytes, string paddingMode)
{ {
IBufferedCipher cipher = CipherUtilities.GetCipher($"{RSA_CIPHER_ALGORITHM_NONE}/{paddingMode}"); IBufferedCipher cipher = CipherUtilities.GetCipher($"RSA/ECB/{paddingMode}");
cipher.Init(false, rsaPrivateKeyParams); cipher.Init(false, rsaPrivateKeyParams);
return cipher.DoFinal(cipherBytes); return cipher.DoFinal(cipherBytes);
} }
@ -55,35 +53,35 @@ namespace SKIT.FlurlHttpClient.Wechat.Work.Utilities
/// <summary> /// <summary>
/// 使用私钥基于 ECB 模式解密数据。 /// 使用私钥基于 ECB 模式解密数据。
/// </summary> /// </summary>
/// <param name="privateKeyBytes">PKCS#1 私钥字节数据。</param> /// <param name="privateKeyBytes">PKCS#1/PKCS#8 私钥字节数组。</param>
/// <param name="cipherBytes">待解密的数据字节数。</param> /// <param name="cipherBytes">待解密的数据字节数。</param>
/// <param name="paddingMode">填充模式。(默认值:<see cref="RSA_CIPHER_PADDING_PKCS1"/></param> /// <param name="paddingMode">填充模式。(默认值:<see cref="PADDING_MODE_PKCS1"/></param>
/// <returns>解密后的数据字节数组。</returns> /// <returns>解密后的数据字节数组。</returns>
public static byte[] DecryptWithECB(byte[] privateKeyBytes, byte[] cipherBytes, string paddingMode = RSA_CIPHER_PADDING_PKCS1) public static byte[] DecryptWithECB(byte[] privateKeyBytes, byte[] cipherBytes, string paddingMode = PADDING_MODE_PKCS1)
{ {
if (privateKeyBytes is null) throw new ArgumentNullException(nameof(privateKeyBytes)); if (privateKeyBytes is null) throw new ArgumentNullException(nameof(privateKeyBytes));
if (cipherBytes is null) throw new ArgumentNullException(nameof(cipherBytes)); if (cipherBytes is null) throw new ArgumentNullException(nameof(cipherBytes));
RsaKeyParameters rsaPrivateKeyParams = ParsePrivateKeyPemToPrivateKeyParameters(privateKeyBytes); RsaKeyParameters rsaPrivateKeyParams = (RsaKeyParameters)PrivateKeyFactory.CreateKey(privateKeyBytes);
return DecryptWithECB(rsaPrivateKeyParams, cipherBytes, paddingMode); return DecryptWithECB(rsaPrivateKeyParams, cipherBytes, paddingMode);
} }
/// <summary> /// <summary>
/// 使用私钥基于 ECB 模式解密数据。 /// 使用私钥基于 ECB 模式解密数据。
/// </summary> /// </summary>
/// <param name="privateKey">PKCS#1 私钥PEM 格式)。</param> /// <param name="privateKeyPem">PKCS#1/PKCS#8 私钥PEM 格式)。</param>
/// <param name="cipherText">经 Base64 编码的待解密数据。</param> /// <param name="encodingCipher">经过编码后的(通常为 Base64待解密数据。</param>
/// <param name="paddingMode">填充模式。(默认值:<see cref="RSA_CIPHER_PADDING_PKCS1"/></param> /// <param name="paddingMode">填充模式。(默认值:<see cref="PADDING_MODE_PKCS1"/></param>
/// <returns>解密后的文本数据。</returns> /// <returns>解密后的数据。</returns>
public static string DecryptWithECB(string privateKey, string cipherText, string paddingMode = RSA_CIPHER_PADDING_PKCS1) public static EncodedString DecryptWithECB(string privateKeyPem, EncodedString encodingCipher, string paddingMode = PADDING_MODE_PKCS1)
{ {
if (privateKey is null) throw new ArgumentNullException(nameof(privateKey)); if (privateKeyPem is null) throw new ArgumentNullException(nameof(privateKeyPem));
if (cipherText is null) throw new ArgumentNullException(nameof(cipherText)); if (encodingCipher.Value is null) throw new ArgumentNullException(nameof(encodingCipher));
byte[] privateKeyBytes = ConvertPrivateKeyPkcs1PemToByteArray(privateKey); byte[] privateKeyBytes = ConvertPrivateKeyPemToByteArray(privateKeyPem);
byte[] cipherBytes = Convert.FromBase64String(cipherText); byte[] cipherBytes = EncodedString.FromString(encodingCipher, fallbackEncodingKind: EncodingKinds.Base64);
byte[] plainBytes = DecryptWithECB(privateKeyBytes, cipherBytes, paddingMode); byte[] plainBytes = DecryptWithECB(privateKeyBytes, cipherBytes, paddingMode);
return Encoding.UTF8.GetString(plainBytes); return EncodedString.ToLiteralString(plainBytes);
} }
} }
} }

View File

@ -1,39 +1,44 @@
using System; using System;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Text;
namespace SKIT.FlurlHttpClient.Wechat.Work.Utilities namespace SKIT.FlurlHttpClient.Wechat.Work.Utilities
{ {
using SKIT.FlurlHttpClient.Primitives;
/// <summary> /// <summary>
/// SHA-1 算法工具类。 /// SHA-1 算法工具类。
/// </summary> /// </summary>
public static class SHA1Utility public static class SHA1Utility
{ {
/// <summary> /// <summary>
/// 获取 SHA-1 信息摘要 /// 计算 SHA-1 哈希值
/// </summary> /// </summary>
/// <param name="bytes">信息字节数组。</param> /// <param name="bytes">要计算哈希值的信息字节数组。</param>
/// <returns>信息摘要字节数组。</returns> /// <returns>哈希值字节数组。</returns>
public static byte[] Hash(byte[] bytes) public static byte[] Hash(byte[] bytes)
{ {
if (bytes is null) throw new ArgumentNullException(nameof(bytes)); if (bytes is null) throw new ArgumentNullException(nameof(bytes));
using SHA1 sha = SHA1.Create(); #if NET5_0_OR_GREATER
return sha.ComputeHash(bytes); return SHA1.HashData(bytes);
#else
using SHA1 sha1 = SHA1.Create();
return sha1.ComputeHash(bytes);
#endif
} }
/// <summary> /// <summary>
/// 获取 SHA-1 信息摘要。 /// 计算 SHA-1 哈希值
/// </summary> /// </summary>
/// <param name="message">文本信息。</param> /// <param name="message">要计算哈希值的信息。</param>
/// <returns>信息摘要。</returns> /// <returns>经过十六进制编码的哈希值。</returns>
public static string Hash(string message) public static EncodedString Hash(string message)
{ {
if (message is null) throw new ArgumentNullException(nameof(message)); if (message is null) throw new ArgumentNullException(nameof(message));
byte[] msgBytes = Encoding.UTF8.GetBytes(message); byte[] msgBytes = EncodedString.FromLiteralString(message);
byte[] hashBytes = Hash(msgBytes); byte[] hashBytes = Hash(msgBytes);
return BitConverter.ToString(hashBytes).Replace("-", string.Empty); return EncodedString.ToHexString(hashBytes);
} }
} }
} }

View File

@ -233,7 +233,7 @@ namespace SKIT.FlurlHttpClient.Wechat.Work.Utilities
tmp.Sort(StringComparer.Ordinal); tmp.Sort(StringComparer.Ordinal);
string rawText = string.Join(string.Empty, tmp); string rawText = string.Join(string.Empty, tmp);
string signText = SHA1Utility.Hash(rawText); string signText = SHA1Utility.Hash(rawText).Value!;
return signText.ToLower(); return signText.ToLower();
} }

View File

@ -2,6 +2,8 @@ using Xunit;
namespace SKIT.FlurlHttpClient.Wechat.Work.UnitTests namespace SKIT.FlurlHttpClient.Wechat.Work.UnitTests
{ {
using SKIT.FlurlHttpClient.Primitives;
public class TestCase_ToolsAESUtilityTests public class TestCase_ToolsAESUtilityTests
{ {
[Fact(DisplayName = "测试用例AES-GCM 解密")] [Fact(DisplayName = "测试用例AES-GCM 解密")]
@ -9,10 +11,10 @@ namespace SKIT.FlurlHttpClient.Wechat.Work.UnitTests
{ {
string key = "f09b03a7a1902b5b4913856f1fd07ab1"; string key = "f09b03a7a1902b5b4913856f1fd07ab1";
string nonce = "aae8c2e79c5b"; string nonce = "aae8c2e79c5b";
string aad = "certificate"; string associatedData = "certificate";
string cipherText = "x9kkL5w1JuaypcjhrYIP+kVNlN8o8uN4yJyJjy5lg+PyPnQL2Zn//ORaXAyzdaK/WBMVd3u/Y9hLaTBLMyRXzowsrkJ5PT37johye48N7BAJQ0PJwW++d1RdhOOPjoqfmws6rSV5Gv2qhfdKjmpxVjr8xr71dtBt8J2wu+bAV99HHQoAynm/Pp9OQYZgpOQ+1cyFHd43TAxOoFfmixKrXr3HP8lJot0XCUSq4qkr1Hs44FV2KuzntSk8eqKr5N17UcuPF3VYnnnF/AvQ7HuLKWwrHhUbaXfkwy0Q2n36UJMfBj7344S97E8BnS89ojgOPQi+olBPyNgrtDWHgsJAKu7HA6PV/FgmXcrZirje/AH1u25es4z5xItHscm//6rDvALgf7greV5OJzMsSl/KVDtbkjDSzim0j4ZTduIfzh7l6jfOz115ITcNILT9ef2KkcMyBBc89GZlMGeHTbgsBHzGeLawX3dXFjqt5aMnHM3VWCovA2aUM4c//rqkZGf+Va86OEFoJQLiSTFpkOrKxcIxcrbKPLTgiDWRT3wzmnUDg7kSPbluzt3ROvMFq9lB8bO/pBd7TD2w87sfUdKLj69FniX6s37SBeRVhw8GSIvBf5lpLUhqL5zKYlbuAWePuz0wVV4NTtkaVKZlmm8KTODyZDFpsyKPubDDcwT1ftRf5aSVM4x04I/1B7GkNz/TOy+zpJ0h0B7VHdxyO5JYiI/1qsatX/FE2aJdQYMYOtqfDH7ZH5UUKIqo538OKvE2M4MlBR/aVE4z4QDKfE/1kYrOfvVGfDzF/FWHfUrcqB8kdQMvk8vCoM8yYZsX4KE1aoJbNM2pWv2tpr9JE8b/VQgUyHOgPYAha+UOmZki4Sfl9H62687EIWdbM57ZxmwIiBp60SrJLiBfZon9JqXKdtJOKj0CRokQiBnONNXCVerLFeBNQfeKRw8tgJXf+QPohMGYkSDdc8hTgdbmhTwB1Vv01stlYK12QMNRCovlp/fcmpB72Phlq+/3p5pqMzknw+qm5QAz7JnpZJCFHit52gHwAkKRkVPB3HF2rfLrdTYz5c9Bok1ICAY9My47eLFdduIe99V52cjMLQuUmNFBPrDdyZKVqIHJ/wtWO/wIFpAVGSJMHctyEKmeJVc1IQN74Wm00PrpPackHdO3G41bBmkp5pqUdsSgSkwdfNVqv0cMcSe04NrRGNKMcZ7TA/CMaP1YnhxvVE+z8aksJqSJ+gdplvuwl40y5C8UEHeAi1V8Q0Bf4YvYRgOVIWm2Lzjdn2z9PWLGcStUj11/1hthk2li4V3mgm2Cr2IZme2sn4rZmJ6dexGP1nk+ZYOq0xLE8F7oex9gyDN+A/6zHqnuhO/X08qye0gochMr8U89Qvj2c0L3P2mjCea2H1mEriAJPqMPMKIinh1lQJEZufnfCcPxbZLKTtl6zHtHgOztejd1gV/nUyCVKD4MCMfBDy9C/Af8pWx6akOg/QSQNIGA2AI6zprHn9zEjpFIzXJYvruVI22Yt6oF9Xnt7Ki82wRK2M96r4kj6cwSs4exMPGv8fWMrFTm0Br6p6T+HZsxyyn2ChuPIgpfisnce/ZaU/0xCZhK/K79+TK2GeeChq5oEpua/1tx4+kDHi7H9381pLJmy2oXW060c2mmwA9+EpcuwEDhr8fsnghbv41u7b1NhEmWNVUy29Dwaz61PPGUdh5DsvaKLWC+raZ/6UEKPw+tiABJ5o6u2jAWgmEYmmJCKapNgtfPc6D+O0aHH9oqh6u4+8NRAhusPZzDGWBr6AT4pexgWFeEhZhn6bXM9HhpUe0IhOTw5D+tqXrTlNon4kjYibiMUFy1h2YyYS3IEdu1J4xqvo0rFyCxF1C+P6ubc0tClRPkXg=="; string cipherText = "x9kkL5w1JuaypcjhrYIP+kVNlN8o8uN4yJyJjy5lg+PyPnQL2Zn//ORaXAyzdaK/WBMVd3u/Y9hLaTBLMyRXzowsrkJ5PT37johye48N7BAJQ0PJwW++d1RdhOOPjoqfmws6rSV5Gv2qhfdKjmpxVjr8xr71dtBt8J2wu+bAV99HHQoAynm/Pp9OQYZgpOQ+1cyFHd43TAxOoFfmixKrXr3HP8lJot0XCUSq4qkr1Hs44FV2KuzntSk8eqKr5N17UcuPF3VYnnnF/AvQ7HuLKWwrHhUbaXfkwy0Q2n36UJMfBj7344S97E8BnS89ojgOPQi+olBPyNgrtDWHgsJAKu7HA6PV/FgmXcrZirje/AH1u25es4z5xItHscm//6rDvALgf7greV5OJzMsSl/KVDtbkjDSzim0j4ZTduIfzh7l6jfOz115ITcNILT9ef2KkcMyBBc89GZlMGeHTbgsBHzGeLawX3dXFjqt5aMnHM3VWCovA2aUM4c//rqkZGf+Va86OEFoJQLiSTFpkOrKxcIxcrbKPLTgiDWRT3wzmnUDg7kSPbluzt3ROvMFq9lB8bO/pBd7TD2w87sfUdKLj69FniX6s37SBeRVhw8GSIvBf5lpLUhqL5zKYlbuAWePuz0wVV4NTtkaVKZlmm8KTODyZDFpsyKPubDDcwT1ftRf5aSVM4x04I/1B7GkNz/TOy+zpJ0h0B7VHdxyO5JYiI/1qsatX/FE2aJdQYMYOtqfDH7ZH5UUKIqo538OKvE2M4MlBR/aVE4z4QDKfE/1kYrOfvVGfDzF/FWHfUrcqB8kdQMvk8vCoM8yYZsX4KE1aoJbNM2pWv2tpr9JE8b/VQgUyHOgPYAha+UOmZki4Sfl9H62687EIWdbM57ZxmwIiBp60SrJLiBfZon9JqXKdtJOKj0CRokQiBnONNXCVerLFeBNQfeKRw8tgJXf+QPohMGYkSDdc8hTgdbmhTwB1Vv01stlYK12QMNRCovlp/fcmpB72Phlq+/3p5pqMzknw+qm5QAz7JnpZJCFHit52gHwAkKRkVPB3HF2rfLrdTYz5c9Bok1ICAY9My47eLFdduIe99V52cjMLQuUmNFBPrDdyZKVqIHJ/wtWO/wIFpAVGSJMHctyEKmeJVc1IQN74Wm00PrpPackHdO3G41bBmkp5pqUdsSgSkwdfNVqv0cMcSe04NrRGNKMcZ7TA/CMaP1YnhxvVE+z8aksJqSJ+gdplvuwl40y5C8UEHeAi1V8Q0Bf4YvYRgOVIWm2Lzjdn2z9PWLGcStUj11/1hthk2li4V3mgm2Cr2IZme2sn4rZmJ6dexGP1nk+ZYOq0xLE8F7oex9gyDN+A/6zHqnuhO/X08qye0gochMr8U89Qvj2c0L3P2mjCea2H1mEriAJPqMPMKIinh1lQJEZufnfCcPxbZLKTtl6zHtHgOztejd1gV/nUyCVKD4MCMfBDy9C/Af8pWx6akOg/QSQNIGA2AI6zprHn9zEjpFIzXJYvruVI22Yt6oF9Xnt7Ki82wRK2M96r4kj6cwSs4exMPGv8fWMrFTm0Br6p6T+HZsxyyn2ChuPIgpfisnce/ZaU/0xCZhK/K79+TK2GeeChq5oEpua/1tx4+kDHi7H9381pLJmy2oXW060c2mmwA9+EpcuwEDhr8fsnghbv41u7b1NhEmWNVUy29Dwaz61PPGUdh5DsvaKLWC+raZ/6UEKPw+tiABJ5o6u2jAWgmEYmmJCKapNgtfPc6D+O0aHH9oqh6u4+8NRAhusPZzDGWBr6AT4pexgWFeEhZhn6bXM9HhpUe0IhOTw5D+tqXrTlNon4kjYibiMUFy1h2YyYS3IEdu1J4xqvo0rFyCxF1C+P6ubc0tClRPkXg==";
string actualPlain = Utilities.AESUtility.DecryptWithGCM(key: key, nonce: nonce, aad: aad, cipherText: cipherText); string actualPlain = Utilities.AESUtility.DecryptWithGCM(encodingKey: new EncodedString(key, EncodingKinds.Literal), encodingNonce: new EncodedString(nonce, EncodingKinds.Literal), encodingAssociatedData: new EncodedString(associatedData, EncodingKinds.Literal), encodingCipher: new EncodedString(cipherText, EncodingKinds.Base64))!;
string expectedPlain = "-----BEGIN CERTIFICATE-----\nMIID3DCCAsSgAwIBAgIUGZNrTcamx3sFSJsQli3v9C6gZe8wDQYJKoZIhvcNAQEL\nBQAwXjELMAkGA1UEBhMCQ04xEzARBgNVBAoTClRlbnBheS5jb20xHTAbBgNVBAsT\nFFRlbnBheS5jb20gQ0EgQ2VudGVyMRswGQYDVQQDExJUZW5wYXkuY29tIFJvb3Qg\nQ0EwHhcNMjAwODAxMDczNTE4WhcNMjUwNzMxMDczNTE4WjBuMRgwFgYDVQQDDA9U\nZW5wYXkuY29tIHNpZ24xEzARBgNVBAoMClRlbnBheS5jb20xHTAbBgNVBAsMFFRl\nbnBheS5jb20gQ0EgQ2VudGVyMQswCQYDVQQGDAJDTjERMA8GA1UEBwwIU2hlblpo\nZW4wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDOAYHqxCqaRzoTIvgV\nixaYJJvmvHbiczbx5MQ9XL1ITSFxkTsNsk7RKHnO7eBS5imheJgQwd22Ky+XclSe\n7B4odssu/l/+gHo2gooTYrrCpQrOkpvGMf8R8aI56BQIF+vsomDvVq1NojHV2Fql\njMwFXzhj2EmU6p6gDv9iL7q1NrfnxFx8iJe4OhIB5Ek4qn1xXxrTUhiULd2vXlbI\nXhRetZSNcJsLt5Rw7D7c8F+aX2JchfeqsZECwKW7bSjMbVWWC6M9MgkB/aId8P0y\n7qEiiXFJkfkg1I/E1ud2apopsid5tdCyRRR6+MhhX2EC8S04MN4soUT7haqNNxX2\nrKHnAgMBAAGjgYEwfzAJBgNVHRMEAjAAMAsGA1UdDwQEAwIE8DBlBgNVHR8EXjBc\nMFqgWKBWhlRodHRwOi8vZXZjYS5pdHJ1cy5jb20uY24vcHVibGljL2l0cnVzY3Js\nP0NBPTFCRDQyMjBFNTBEQkMwNEIwNkFEMzk3NTQ5ODQ2QzAxQzNFOEVCRDIwDQYJ\nKoZIhvcNAQELBQADggEBAJyg2z4oLQmPfftLQWyzbUc9ONhRMtfA+tVlVBgtLLKn\nWuDlsmEntheM07fu84F4pcfs3yHzjD7pAOFbO4Yt1yhQ50DK35sjbRWepPdWJZLl\nni7KBctcmm0o4zq37oB7vonmBEbFqYs9DaINYOjgI3J25iSBkPVC7dBbvFj2xB0L\ncIcXipq30tDdC/oUem27MNzwZAt49WthKhw6u3HSkcE5cO4LyYTsJhSyG/7LXwvV\nMgX4Jyzo0SSiGOU1/beaZssTVI8sTPJVlHWjhNE3Lc2SaAlKGfGwvt0X3cEZEF+7\noEZIFTkkAF2JhqfnpR3gST0G8Umq1SaVtCPP/zVI8x0=\n-----END CERTIFICATE-----"; string expectedPlain = "-----BEGIN CERTIFICATE-----\nMIID3DCCAsSgAwIBAgIUGZNrTcamx3sFSJsQli3v9C6gZe8wDQYJKoZIhvcNAQEL\nBQAwXjELMAkGA1UEBhMCQ04xEzARBgNVBAoTClRlbnBheS5jb20xHTAbBgNVBAsT\nFFRlbnBheS5jb20gQ0EgQ2VudGVyMRswGQYDVQQDExJUZW5wYXkuY29tIFJvb3Qg\nQ0EwHhcNMjAwODAxMDczNTE4WhcNMjUwNzMxMDczNTE4WjBuMRgwFgYDVQQDDA9U\nZW5wYXkuY29tIHNpZ24xEzARBgNVBAoMClRlbnBheS5jb20xHTAbBgNVBAsMFFRl\nbnBheS5jb20gQ0EgQ2VudGVyMQswCQYDVQQGDAJDTjERMA8GA1UEBwwIU2hlblpo\nZW4wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDOAYHqxCqaRzoTIvgV\nixaYJJvmvHbiczbx5MQ9XL1ITSFxkTsNsk7RKHnO7eBS5imheJgQwd22Ky+XclSe\n7B4odssu/l/+gHo2gooTYrrCpQrOkpvGMf8R8aI56BQIF+vsomDvVq1NojHV2Fql\njMwFXzhj2EmU6p6gDv9iL7q1NrfnxFx8iJe4OhIB5Ek4qn1xXxrTUhiULd2vXlbI\nXhRetZSNcJsLt5Rw7D7c8F+aX2JchfeqsZECwKW7bSjMbVWWC6M9MgkB/aId8P0y\n7qEiiXFJkfkg1I/E1ud2apopsid5tdCyRRR6+MhhX2EC8S04MN4soUT7haqNNxX2\nrKHnAgMBAAGjgYEwfzAJBgNVHRMEAjAAMAsGA1UdDwQEAwIE8DBlBgNVHR8EXjBc\nMFqgWKBWhlRodHRwOi8vZXZjYS5pdHJ1cy5jb20uY24vcHVibGljL2l0cnVzY3Js\nP0NBPTFCRDQyMjBFNTBEQkMwNEIwNkFEMzk3NTQ5ODQ2QzAxQzNFOEVCRDIwDQYJ\nKoZIhvcNAQELBQADggEBAJyg2z4oLQmPfftLQWyzbUc9ONhRMtfA+tVlVBgtLLKn\nWuDlsmEntheM07fu84F4pcfs3yHzjD7pAOFbO4Yt1yhQ50DK35sjbRWepPdWJZLl\nni7KBctcmm0o4zq37oB7vonmBEbFqYs9DaINYOjgI3J25iSBkPVC7dBbvFj2xB0L\ncIcXipq30tDdC/oUem27MNzwZAt49WthKhw6u3HSkcE5cO4LyYTsJhSyG/7LXwvV\nMgX4Jyzo0SSiGOU1/beaZssTVI8sTPJVlHWjhNE3Lc2SaAlKGfGwvt0X3cEZEF+7\noEZIFTkkAF2JhqfnpR3gST0G8Umq1SaVtCPP/zVI8x0=\n-----END CERTIFICATE-----";
Assert.Equal(expectedPlain, actualPlain); Assert.Equal(expectedPlain, actualPlain);

View File

@ -2,6 +2,8 @@ using Xunit;
namespace SKIT.FlurlHttpClient.Wechat.Work.UnitTests namespace SKIT.FlurlHttpClient.Wechat.Work.UnitTests
{ {
using SKIT.FlurlHttpClient.Primitives;
public class TestCase_ToolsRSAUtilityTests public class TestCase_ToolsRSAUtilityTests
{ {
// 此处测试的 RSA 公钥/私钥是自签名生成的,仅供执行 RSA 相关的单元测试,不能用于调用企业微信 API // 此处测试的 RSA 公钥/私钥是自签名生成的,仅供执行 RSA 相关的单元测试,不能用于调用企业微信 API
@ -12,7 +14,7 @@ namespace SKIT.FlurlHttpClient.Wechat.Work.UnitTests
public void TestRSADecrypt() public void TestRSADecrypt()
{ {
string cipherText = "ewwZ8LmXVJpkJpj/JWcz16L4bePAGcf3Fi2EKyC6AS3JsF5u4aku7iOYqtcAczjoYwE1fqSadRd6YTrWr3tLP3uWFYmhqthQoaAcjmQS0vHYRFeS1V7q5hbziVLRp7C42S4YrvqXAmSmUyjPUXG5tXFVchARVkTr1F53HGoPP+iBg+i8y0uJK4FgiuKraFgdtKofv/k5/30xKzRHxdLFCFt1rF7wL+Hk/7Bl0tFZM/rfhmuvwbf46zWhxKKviAge+61tEot4QCSBLnAFpPuSQsTOOSOrlCl92DwW54dWdlWwhqkTVHdm6pXEdUE66y1yoZkXfpqjnONjta0njqN/Jw=="; string cipherText = "ewwZ8LmXVJpkJpj/JWcz16L4bePAGcf3Fi2EKyC6AS3JsF5u4aku7iOYqtcAczjoYwE1fqSadRd6YTrWr3tLP3uWFYmhqthQoaAcjmQS0vHYRFeS1V7q5hbziVLRp7C42S4YrvqXAmSmUyjPUXG5tXFVchARVkTr1F53HGoPP+iBg+i8y0uJK4FgiuKraFgdtKofv/k5/30xKzRHxdLFCFt1rF7wL+Hk/7Bl0tFZM/rfhmuvwbf46zWhxKKviAge+61tEot4QCSBLnAFpPuSQsTOOSOrlCl92DwW54dWdlWwhqkTVHdm6pXEdUE66y1yoZkXfpqjnONjta0njqN/Jw==";
string actualPlain = Utilities.RSAUtility.DecryptWithECB(RSA_PEM_PRIVATE_KEY, cipherText); string actualPlain = Utilities.RSAUtility.DecryptWithECB(RSA_PEM_PRIVATE_KEY, new EncodedString(cipherText, EncodingKinds.Base64))!;
string expectedPlain = "RsaDecryptTest"; string expectedPlain = "RsaDecryptTest";
Assert.Equal(expectedPlain, actualPlain); Assert.Equal(expectedPlain, actualPlain);