feat(tenpayv3): 废弃原有的解密响应中加密字段的相关扩展方法,重新基于反射和特性实现

This commit is contained in:
Fu Diwei 2021-11-25 18:05:22 +08:00
parent bfa6557314
commit 906c667117
25 changed files with 440 additions and 420 deletions

View File

@ -49,7 +49,7 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV3.Sample_Net5.Services.BackgroundSe
var response = await client.ExecuteQueryCertificatesAsync(request, cancellationToken: stoppingToken);
if (response.IsSuccessful())
{
client.DecryptResponseEncryptedData(ref response);
response = client.DecryptResponseSensitiveProperty(response);
foreach (var certificateModel in response.CertificateList)
{
_certificateManager.AddEntry(new CertificateEntry(certificateModel));

View File

@ -0,0 +1,3 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("SKIT.FlurlHttpClient.Wechat.TenpayV3.UnitTests")]

View File

@ -0,0 +1,15 @@
using System;
namespace SKIT.FlurlHttpClient.Wechat.TenpayV3
{
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public sealed class WechatTenpaySensitivePropertyAttribute : Attribute
{
public string Algorithm { get; }
public WechatTenpaySensitivePropertyAttribute(string algorithm)
{
Algorithm = algorithm;
}
}
}

View File

@ -1,9 +1,9 @@
using System;
namespace SKIT.FlurlHttpClient.Wechat.TenpayV3.Constants
namespace SKIT.FlurlHttpClient.Wechat.TenpayV3.Constants
{
public static class EncryptionAlgorithms
{
public const string AEAD_AES_256_GCM = "AEAD_AES_256_GCM";
public const string RSA_2048_PKCS8_ECB = "RSA_2048_PKCS18_ECB";
}
}

View File

@ -1,6 +1,4 @@
using System;
namespace SKIT.FlurlHttpClient.Wechat.TenpayV3.Constants
namespace SKIT.FlurlHttpClient.Wechat.TenpayV3.Constants
{
internal static class FormDataFields
{

View File

@ -1,6 +1,4 @@
using System;
namespace SKIT.FlurlHttpClient.Wechat.TenpayV3.Constants
namespace SKIT.FlurlHttpClient.Wechat.TenpayV3.Constants
{
public static class SignAlgorithms
{

View File

@ -1,6 +1,4 @@
using System;
namespace SKIT.FlurlHttpClient.Wechat.TenpayV3.Constants
namespace SKIT.FlurlHttpClient.Wechat.TenpayV3.Constants
{
public static class SignTypes
{

View File

@ -70,7 +70,7 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV3
}
else
{
throw new Exceptions.WechatTenpayEventDecryptionException("Unknown encrypt algorithm of the resource.");
throw new Exceptions.WechatTenpayEventDecryptionException("Unsupported encrypt algorithm of the resource.");
}
return client.JsonSerializer.Deserialize<T>(plainJson);

View File

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
namespace SKIT.FlurlHttpClient.Wechat.TenpayV3
{
@ -15,7 +16,8 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV3
/// <param name="client"></param>
/// <param name="response"></param>
/// <returns></returns>
public static Models.GetEcommerceApplymentByOutRequestNumberResponse DecryptResponseEncryptedData(this WechatTenpayClient client, ref Models.GetEcommerceApplymentByOutRequestNumberResponse response)
public static TResponse DecryptResponseSensitiveProperty<TResponse>(this WechatTenpayClient client, TResponse response)
where TResponse : WechatTenpayResponse
{
if (client == null) throw new ArgumentNullException(nameof(client));
if (response == null) throw new ArgumentNullException(nameof(response));
@ -26,159 +28,17 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV3
if (!response.IsSuccessful())
throw new Exceptions.WechatTenpayResponseDecryptionException("Decrypt response failed, because the response is not successful.");
if (response.AccountValidation != null)
{
IList<Exception> exceptions = new List<Exception>();
var accountValidationModel = response.AccountValidation;
if (!string.IsNullOrEmpty(accountValidationModel.AccountName))
{
try
{
accountValidationModel.AccountName = Utilities.RSAUtility.DecryptWithECB(
client.Credentials.MerchantCertPrivateKey,
accountValidationModel.AccountName
);
}
catch (Exception ex)
// [GET] /certificates 接口的响应模型需特殊处理
if (response is Models.QueryCertificatesResponse queryCertificatesResponse)
{
exceptions.Add(ex);
}
}
if (!string.IsNullOrEmpty(accountValidationModel.AccountNumber))
{
try
{
accountValidationModel.AccountNumber = Utilities.RSAUtility.DecryptWithECB(
client.Credentials.MerchantCertPrivateKey,
accountValidationModel.AccountNumber!
);
}
catch (Exception ex)
{
exceptions.Add(ex);
}
}
if (exceptions.Any())
throw new Exceptions.WechatTenpayResponseDecryptionException("Decrypt response failed.", new AggregateException(exceptions));
}
if (queryCertificatesResponse.CertificateList == null)
return response;
}
/// <summary>
/// <para>解密响应中返回的敏感数据。该方法会改变传入的响应信息。</para>
/// </summary>
/// <param name="client"></param>
/// <param name="response"></param>
/// <returns></returns>
public static Models.GetEcommerceBillFundflowBillResponse DecryptResponseEncryptedData(this WechatTenpayClient client, ref Models.GetEcommerceBillFundflowBillResponse response)
{
if (client == null) throw new ArgumentNullException(nameof(client));
if (response == null) throw new ArgumentNullException(nameof(response));
if (string.IsNullOrEmpty(client.Credentials.MerchantCertPrivateKey))
throw new Exceptions.WechatTenpayResponseDecryptionException("Decrypt response failed, because there is no merchant private key.");
if (!response.IsSuccessful())
throw new Exceptions.WechatTenpayResponseDecryptionException("Decrypt response failed, because the response is not successful.");
if (response.DownloadBillList != null)
{
IList<Exception> exceptions = new List<Exception>();
foreach (var downloadBillModel in response.DownloadBillList)
{
if (!string.IsNullOrEmpty(downloadBillModel.EncryptKey))
{
try
{
downloadBillModel.EncryptKey = Utilities.RSAUtility.DecryptWithECB(
client.Credentials.MerchantCertPrivateKey,
downloadBillModel.EncryptKey
);
}
catch (Exception ex)
{
exceptions.Add(ex);
}
}
}
if (exceptions.Any())
throw new Exceptions.WechatTenpayResponseDecryptionException("Decrypt response failed.", new AggregateException(exceptions));
}
return response;
}
/// <summary>
/// <para>解密响应中返回的敏感数据。该方法会改变传入的响应信息。</para>
/// </summary>
/// <param name="client"></param>
/// <param name="response"></param>
/// <returns></returns>
public static Models.GetMerchantServiceComplaintByComplaintIdResponse DecryptResponseEncryptedData(this WechatTenpayClient client, ref Models.GetMerchantServiceComplaintByComplaintIdResponse response)
{
if (client == null) throw new ArgumentNullException(nameof(client));
if (response == null) throw new ArgumentNullException(nameof(response));
if (string.IsNullOrEmpty(client.Credentials.MerchantCertPrivateKey))
throw new Exceptions.WechatTenpayResponseDecryptionException("Decrypt response failed, because there is no merchant private key.");
if (!response.IsSuccessful())
throw new Exceptions.WechatTenpayResponseDecryptionException("Decrypt response failed, because the response is not successful.");
if (response.ComplaintDetail != null)
{
if (!string.IsNullOrEmpty(response.PayerPhone))
{
try
{
response.PayerPhone = Utilities.RSAUtility.DecryptWithECB(
client.Credentials.MerchantCertPrivateKey,
response.PayerPhone!
);
}
catch (Exception ex)
{
throw new Exceptions.WechatTenpayResponseDecryptionException("Decrypt response failed.", ex);
}
}
}
return response;
}
/// <summary>
/// <para>解密响应中返回的敏感数据。该方法会改变传入的响应信息。</para>
/// </summary>
/// <param name="client"></param>
/// <param name="response"></param>
/// <returns></returns>
public static Models.QueryCertificatesResponse DecryptResponseEncryptedData(this WechatTenpayClient client, ref Models.QueryCertificatesResponse response)
{
if (client == null) throw new ArgumentNullException(nameof(client));
if (response == null) throw new ArgumentNullException(nameof(response));
if (string.IsNullOrEmpty(client.Credentials.MerchantV3Secret))
throw new Exceptions.WechatTenpayResponseDecryptionException("Decrypt response failed, because there is no merchant secret.");
if (!response.IsSuccessful())
throw new Exceptions.WechatTenpayResponseDecryptionException("Decrypt response failed, because the response is not successful.");
if (response.CertificateList != null)
{
IList<Exception> exceptions = new List<Exception>();
foreach (var certificateModel in response.CertificateList)
foreach (var certificateModel in queryCertificatesResponse.CertificateList)
{
if (Constants.EncryptionAlgorithms.AEAD_AES_256_GCM.Equals(certificateModel.EncryptCertificate?.Algorithm))
{
try
{
certificateModel.EncryptCertificate.CipherText = Utilities.AESUtility.DecryptWithGCM(
key: client.Credentials.MerchantV3Secret,
@ -187,266 +47,38 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV3
cipherText: certificateModel.EncryptCertificate.CipherText
);
}
catch (Exception ex)
else
{
exceptions.Add(ex);
throw new Exceptions.WechatTenpayResponseDecryptionException("Unsupported decryption algorithm.");
}
}
return response;
}
// 遍历并解密被标记为敏感数据的字段
Utilities.ReflectionUtility.ReplacePropertyStringValue(ref response, (obj, prop, value) =>
{
var attr = prop.GetCustomAttribute<WechatTenpaySensitivePropertyAttribute>();
if (attr == null)
return value;
if (Constants.EncryptionAlgorithms.RSA_2048_PKCS8_ECB.Equals(attr.Algorithm))
{
return Utilities.RSAUtility.DecryptWithECB(
privateKey: client.Credentials.MerchantCertPrivateKey,
cipherText: value
);
}
else
{
exceptions.Add(new Exception("Unknown encrypt algorithm of the certificate."));
throw new Exceptions.WechatTenpayResponseDecryptionException("Unsupported decryption algorithm.");
}
});
}
if (exceptions.Any())
throw new Exceptions.WechatTenpayResponseDecryptionException("Decrypt response failed.", new AggregateException(exceptions));
}
return response;
}
/// <summary>
/// <para>解密响应中返回的敏感数据。该方法会改变传入的响应信息。</para>
/// </summary>
/// <param name="client"></param>
/// <param name="response"></param>
/// <returns></returns>
public static Models.QueryMerchantServiceComplaintsResponse DecryptResponseEncryptedData(this WechatTenpayClient client, ref Models.QueryMerchantServiceComplaintsResponse response)
catch (Exception ex) when (!(ex is Exceptions.WechatTenpayResponseDecryptionException))
{
if (client == null) throw new ArgumentNullException(nameof(client));
if (response == null) throw new ArgumentNullException(nameof(response));
if (string.IsNullOrEmpty(client.Credentials.MerchantCertPrivateKey))
throw new Exceptions.WechatTenpayResponseDecryptionException("Decrypt response failed, because there is no merchant private key.");
if (!response.IsSuccessful())
throw new Exceptions.WechatTenpayResponseDecryptionException("Decrypt response failed, because the response is not successful.");
if (response.ComplaintList != null)
{
IList<Exception> exceptions = new List<Exception>();
foreach (var complaintModel in response.ComplaintList)
{
if (!string.IsNullOrEmpty(complaintModel.PayerPhone))
{
try
{
complaintModel.PayerPhone = Utilities.RSAUtility.DecryptWithECB(
client.Credentials.MerchantCertPrivateKey,
complaintModel.PayerPhone!
);
}
catch (Exception ex)
{
exceptions.Add(ex);
}
}
}
if (exceptions.Any())
throw new Exceptions.WechatTenpayResponseDecryptionException("Decrypt response failed.", new AggregateException(exceptions));
}
return response;
}
/// <summary>
/// <para>解密响应中返回的敏感数据。该方法会改变传入的响应信息。</para>
/// </summary>
/// <param name="client"></param>
/// <param name="response"></param>
/// <returns></returns>
public static Models.QuerySmartGuidesResponse DecryptResponseEncryptedData(this WechatTenpayClient client, ref Models.QuerySmartGuidesResponse response)
{
if (client == null) throw new ArgumentNullException(nameof(client));
if (response == null) throw new ArgumentNullException(nameof(response));
if (string.IsNullOrEmpty(client.Credentials.MerchantCertPrivateKey))
throw new Exceptions.WechatTenpayResponseDecryptionException("Decrypt response failed, because there is no merchant private key.");
if (!response.IsSuccessful())
throw new Exceptions.WechatTenpayResponseDecryptionException("Decrypt response failed, because the response is not successful.");
if (response.GuideList != null)
{
IList<Exception> exceptions = new List<Exception>();
foreach (var guideModel in response.GuideList)
{
if (!string.IsNullOrEmpty(guideModel.UserName))
{
try
{
guideModel.UserName = Utilities.RSAUtility.DecryptWithECB(
client.Credentials.MerchantCertPrivateKey,
guideModel.UserName
);
}
catch (Exception ex)
{
exceptions.Add(ex);
}
}
if (!string.IsNullOrEmpty(guideModel.UserMobile))
{
try
{
guideModel.UserMobile = Utilities.RSAUtility.DecryptWithECB(
client.Credentials.MerchantCertPrivateKey,
guideModel.UserMobile
);
}
catch (Exception ex)
{
exceptions.Add(ex);
}
}
}
if (exceptions.Any())
throw new Exceptions.WechatTenpayResponseDecryptionException("Decrypt response failed.", new AggregateException(exceptions));
}
return response;
}
/// <summary>
/// <para>解密响应中返回的敏感数据。该方法会改变传入的响应信息。</para>
/// </summary>
/// <param name="client"></param>
/// <param name="response"></param>
/// <returns></returns>
public static Models.GetTransferBatchDetailByOutDetailNumberResponse DecryptResponseEncryptedData(this WechatTenpayClient client, ref Models.GetTransferBatchDetailByOutDetailNumberResponse response)
{
if (client == null) throw new ArgumentNullException(nameof(client));
if (response == null) throw new ArgumentNullException(nameof(response));
if (string.IsNullOrEmpty(client.Credentials.MerchantCertPrivateKey))
throw new Exceptions.WechatTenpayResponseDecryptionException("Decrypt response failed, because there is no merchant private key.");
if (!response.IsSuccessful())
throw new Exceptions.WechatTenpayResponseDecryptionException("Decrypt response failed, because the response is not successful.");
if (!string.IsNullOrEmpty(response.UserName))
{
try
{
response.UserName = Utilities.RSAUtility.DecryptWithECB(
client.Credentials.MerchantCertPrivateKey,
response.UserName
);
}
catch (Exception ex)
{
throw new Exceptions.WechatTenpayResponseDecryptionException("Decrypt response failed.", ex);
}
}
return response;
}
/// <summary>
/// <para>解密响应中返回的敏感数据。该方法会改变传入的响应信息。</para>
/// </summary>
/// <param name="client"></param>
/// <param name="response"></param>
/// <returns></returns>
public static Models.GetTransferBatchDetailByDetailIdResponse DecryptResponseEncryptedData(this WechatTenpayClient client, ref Models.GetTransferBatchDetailByDetailIdResponse response)
{
if (client == null) throw new ArgumentNullException(nameof(client));
if (response == null) throw new ArgumentNullException(nameof(response));
if (string.IsNullOrEmpty(client.Credentials.MerchantCertPrivateKey))
throw new Exceptions.WechatTenpayResponseDecryptionException("Decrypt response failed, because there is no merchant private key.");
if (!response.IsSuccessful())
throw new Exceptions.WechatTenpayResponseDecryptionException("Decrypt response failed, because the response is not successful.");
if (!string.IsNullOrEmpty(response.UserName))
{
try
{
response.UserName = Utilities.RSAUtility.DecryptWithECB(
client.Credentials.MerchantCertPrivateKey,
response.UserName
);
}
catch (Exception ex)
{
throw new Exceptions.WechatTenpayResponseDecryptionException("Decrypt response failed.", ex);
}
}
return response;
}
/// <summary>
/// <para>解密响应中返回的敏感数据。该方法会改变传入的响应信息。</para>
/// </summary>
/// <param name="client"></param>
/// <param name="response"></param>
/// <returns></returns>
public static Models.GetPartnerTransferBatchDetailByOutDetailNumberResponse DecryptResponseEncryptedData(this WechatTenpayClient client, ref Models.GetPartnerTransferBatchDetailByOutDetailNumberResponse response)
{
if (client == null) throw new ArgumentNullException(nameof(client));
if (response == null) throw new ArgumentNullException(nameof(response));
if (string.IsNullOrEmpty(client.Credentials.MerchantCertPrivateKey))
throw new Exceptions.WechatTenpayResponseDecryptionException("Decrypt response failed, because there is no merchant private key.");
if (!response.IsSuccessful())
throw new Exceptions.WechatTenpayResponseDecryptionException("Decrypt response failed, because the response is not successful.");
if (!string.IsNullOrEmpty(response.UserName))
{
try
{
response.UserName = Utilities.RSAUtility.DecryptWithECB(
client.Credentials.MerchantCertPrivateKey,
response.UserName
);
}
catch (Exception ex)
{
throw new Exceptions.WechatTenpayResponseDecryptionException("Decrypt response failed.", ex);
}
}
return response;
}
/// <summary>
/// <para>解密响应中返回的敏感数据。该方法会改变传入的响应信息。</para>
/// </summary>
/// <param name="client"></param>
/// <param name="response"></param>
/// <returns></returns>
public static Models.GetPartnerTransferBatchDetailByDetailIdResponse DecryptResponseEncryptedData(this WechatTenpayClient client, ref Models.GetPartnerTransferBatchDetailByDetailIdResponse response)
{
if (client == null) throw new ArgumentNullException(nameof(client));
if (response == null) throw new ArgumentNullException(nameof(response));
if (string.IsNullOrEmpty(client.Credentials.MerchantCertPrivateKey))
throw new Exceptions.WechatTenpayResponseDecryptionException("Decrypt response failed, because there is no merchant private key.");
if (!response.IsSuccessful())
throw new Exceptions.WechatTenpayResponseDecryptionException("Decrypt response failed, because the response is not successful.");
if (!string.IsNullOrEmpty(response.UserName))
{
try
{
response.UserName = Utilities.RSAUtility.DecryptWithECB(
client.Credentials.MerchantCertPrivateKey,
response.UserName
);
}
catch (Exception ex)
{
throw new Exceptions.WechatTenpayResponseDecryptionException("Decrypt response failed.", ex);
}
throw new Exceptions.WechatTenpayResponseDecryptionException("Decrypt response failed. Please see the `InnerException` for more details.", ex);
}
return response;

View File

@ -45,6 +45,7 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV3.Models
/// </summary>
[Newtonsoft.Json.JsonProperty("encrypt_key")]
[System.Text.Json.Serialization.JsonPropertyName("encrypt_key")]
[WechatTenpaySensitiveProperty(algorithm: Constants.EncryptionAlgorithms.RSA_2048_PKCS8_ECB)]
public string EncryptKey { get; set; } = default!;
/// <summary>

View File

@ -42,6 +42,7 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV3.Models
/// </summary>
[Newtonsoft.Json.JsonProperty("ciphertext")]
[System.Text.Json.Serialization.JsonPropertyName("ciphertext")]
[WechatTenpaySensitiveProperty(algorithm: Constants.EncryptionAlgorithms.AEAD_AES_256_GCM)]
public string CipherText { get; set; } = default!;
}
}

View File

@ -17,6 +17,7 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV3.Models
/// </summary>
[Newtonsoft.Json.JsonProperty("account_name")]
[System.Text.Json.Serialization.JsonPropertyName("account_name")]
[WechatTenpaySensitiveProperty(algorithm: Constants.EncryptionAlgorithms.RSA_2048_PKCS8_ECB)]
public string AccountName { get; set; } = default!;
/// <summary>
@ -24,6 +25,7 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV3.Models
/// </summary>
[Newtonsoft.Json.JsonProperty("account_no")]
[System.Text.Json.Serialization.JsonPropertyName("account_no")]
[WechatTenpaySensitiveProperty(algorithm: Constants.EncryptionAlgorithms.RSA_2048_PKCS8_ECB)]
public string? AccountNumber { get; set; }
/// <summary>

View File

@ -45,6 +45,7 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV3.Models
/// </summary>
[Newtonsoft.Json.JsonProperty("encrypt_key")]
[System.Text.Json.Serialization.JsonPropertyName("encrypt_key")]
[WechatTenpaySensitiveProperty(algorithm: Constants.EncryptionAlgorithms.RSA_2048_PKCS8_ECB)]
public string EncryptKey { get; set; } = default!;
/// <summary>

View File

@ -94,6 +94,7 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV3.Models
/// </summary>
[Newtonsoft.Json.JsonProperty("payer_phone")]
[System.Text.Json.Serialization.JsonPropertyName("payer_phone")]
[WechatTenpaySensitiveProperty(algorithm: Constants.EncryptionAlgorithms.RSA_2048_PKCS8_ECB)]
public string? PayerPhone { get; set; }
/// <summary>

View File

@ -58,6 +58,7 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV3.Models
/// </summary>
[Newtonsoft.Json.JsonProperty("payer_phone")]
[System.Text.Json.Serialization.JsonPropertyName("payer_phone")]
[WechatTenpaySensitiveProperty(algorithm: Constants.EncryptionAlgorithms.RSA_2048_PKCS8_ECB)]
public string? PayerPhone { get; set; }
/// <summary>

View File

@ -20,6 +20,7 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV3.Models
/// </summary>
[Newtonsoft.Json.JsonProperty("username")]
[System.Text.Json.Serialization.JsonPropertyName("username")]
[WechatTenpaySensitiveProperty(algorithm: Constants.EncryptionAlgorithms.RSA_2048_PKCS8_ECB)]
public override string UserName { get; set; } = default!;
}
}

View File

@ -45,6 +45,7 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV3.Models
/// </summary>
[Newtonsoft.Json.JsonProperty("encrypt_key")]
[System.Text.Json.Serialization.JsonPropertyName("encrypt_key")]
[WechatTenpaySensitiveProperty(algorithm: Constants.EncryptionAlgorithms.RSA_2048_PKCS8_ECB)]
public string EncryptKey { get; set; } = default!;
/// <summary>

View File

@ -38,6 +38,7 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV3.Models
/// </summary>
[Newtonsoft.Json.JsonProperty("name")]
[System.Text.Json.Serialization.JsonPropertyName("name")]
[WechatTenpaySensitiveProperty(algorithm: Constants.EncryptionAlgorithms.RSA_2048_PKCS8_ECB)]
public string UserName { get; set; } = default!;
/// <summary>
@ -45,6 +46,7 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV3.Models
/// </summary>
[Newtonsoft.Json.JsonProperty("mobile")]
[System.Text.Json.Serialization.JsonPropertyName("mobile")]
[WechatTenpaySensitiveProperty(algorithm: Constants.EncryptionAlgorithms.RSA_2048_PKCS8_ECB)]
public string UserMobile { get; set; } = default!;
/// <summary>

View File

@ -83,6 +83,7 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV3.Models
/// </summary>
[Newtonsoft.Json.JsonProperty("user_name")]
[System.Text.Json.Serialization.JsonPropertyName("user_name")]
[WechatTenpaySensitiveProperty(algorithm: Constants.EncryptionAlgorithms.RSA_2048_PKCS8_ECB)]
public virtual string UserName { get; set; } = default!;
/// <summary>

View File

@ -1,5 +1,6 @@
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
namespace SKIT.FlurlHttpClient.Wechat.TenpayV3.Settings
{
@ -49,7 +50,7 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV3.Settings
public override IEnumerable<CertificateEntry> AllEntries()
{
return _dict.Values;
return _dict.Values.Where(e => e.IsAvailable()).ToArray();
}
public override void AddEntry(CertificateEntry entry)

View File

@ -0,0 +1,140 @@
using System;
using System.Collections;
using System.Reflection;
namespace SKIT.FlurlHttpClient.Wechat.TenpayV3.Utilities
{
internal static class ReflectionUtility
{
public delegate string ReplacePropertyStringValueReplacement(object obj, PropertyInfo prop, string value);
public static void ReplacePropertyStringValue<T>(ref T obj, ReplacePropertyStringValueReplacement replacement)
{
InnerReplacePropertyStringValue(ref obj, replacement, null);
}
private static void InnerReplacePropertyStringValue<T>(ref T obj, ReplacePropertyStringValueReplacement replacement, PropertyInfo? currentProp)
{
if (obj == null) throw new ArgumentNullException(nameof(obj));
if (replacement == null) throw new ArgumentNullException(nameof(replacement));
Type objType = obj.GetType();
if (!objType.IsClass)
throw new NotSupportedException();
if (objType.IsArray)
{
var array = (obj as Array)!;
for (int i = 0, len = array.Length; i < len; i++)
{
object? element = array.GetValue(i);
if (element is null)
continue;
Type elementType = element.GetType();
if (elementType == typeof(string))
{
if (currentProp == null)
continue;
string oldValue = (string)element!;
string newValue = replacement(obj, currentProp, oldValue);
array.SetValue(newValue, i);
}
else if (elementType.IsClass)
{
InnerReplacePropertyStringValue(ref element, replacement, currentProp);
array.SetValue(element, i);
}
else
{
continue;
}
}
}
else if (obj is IList list)
{
for (int i = 0, len = list.Count; i < len; i++)
{
object? element = list[i];
if (element is null)
continue;
Type elementType = element.GetType();
if (elementType == typeof(string))
{
if (currentProp == null)
continue;
string oldValue = (string)element!;
string newValue = replacement(obj, currentProp, oldValue);
list[i] = newValue;
}
else if (elementType.IsClass)
{
InnerReplacePropertyStringValue(ref element, replacement, currentProp);
list[i] = element;
}
else
{
continue;
}
}
}
else if (obj is IDictionary dict)
{
foreach (DictionaryEntry entry in dict)
{
object? entryValue = entry.Value;
if (entryValue is null)
continue;
Type entryValueType = entryValue.GetType();
if (entryValueType == typeof(string))
{
if (currentProp == null)
continue;
string oldValue = (string)entryValue!;
string newValue = replacement(obj, currentProp, oldValue);
dict[entry.Key] = newValue;
}
else if (entryValueType.IsClass)
{
InnerReplacePropertyStringValue(ref entryValue, replacement, currentProp);
dict[entry.Key] = entryValue;
}
else
{
continue;
}
}
}
else
{
foreach (var childProp in objType.GetProperties(BindingFlags.Public | BindingFlags.Instance))
{
if (!childProp.CanWrite)
continue;
Type propType = childProp.PropertyType;
if (propType == typeof(string))
{
string oldValue = (string)childProp.GetValue(obj, null)!;
string newValue = replacement(obj, childProp, oldValue);
childProp.SetValue(obj, newValue);
}
else if (propType.IsClass)
{
object? value = childProp.GetValue(obj, null);
if (value is null)
continue;
InnerReplacePropertyStringValue(ref value, replacement, childProp);
childProp.SetValue(obj, value);
}
}
}
}
}
}

View File

@ -23,7 +23,17 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV3
/// <summary>
/// 获取当前客户端使用的微信商户平台证书管理器。
/// </summary>
internal Settings.CertificateManager CertificateManager { get; }
public Settings.CertificateManager CertificateManager { get; }
/// <summary>
/// 获取是否自动加密请求中的敏感字段数据。
/// </summary>
protected bool AutoEncryptRequestSensitiveProperty { get; }
/// <summary>
/// 获取是否自动解密请求中的敏感字段数据。
/// </summary>
protected bool AutoDecryptResponseSensitiveProperty { get; }
/// <summary>
/// 用指定的配置项初始化 <see cref="WechatTenpayClient"/> 类的新实例。
@ -35,6 +45,8 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV3
Credentials = new Settings.Credentials(options);
CertificateManager = options.CertificateManager;
AutoEncryptRequestSensitiveProperty = options.AutoEncryptRequestSensitiveProperty;
AutoDecryptResponseSensitiveProperty = options.AutoDecryptResponseSensitiveProperty;
FlurlClient.BaseUrl = options.Endpoints ?? WechatTenpayEndpoints.DEFAULT;
FlurlClient.Headers.Remove("Accept");
@ -75,6 +87,12 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV3
flurlRequest.WithHeader("Wechatpay-Serial", request.WechatpayCertSerialNumber);
}
if (AutoDecryptResponseSensitiveProperty)
{
// this.EncryptRequestSensitiveProperty(request);
throw new NotImplementedException();
}
return flurlRequest;
}
@ -152,6 +170,12 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV3
result.WechatpayTimestamp = flurlResponse.Headers.GetAll("Wechatpay-Timestamp").FirstOrDefault() ?? string.Empty;
result.WechatpaySignature = flurlResponse.Headers.GetAll("Wechatpay-Signature").FirstOrDefault() ?? string.Empty;
result.WechatpayCertSerialNumber = flurlResponse.Headers.GetAll("Wechatpay-Serial").FirstOrDefault() ?? string.Empty;
if (AutoDecryptResponseSensitiveProperty)
{
this.DecryptResponseSensitiveProperty(result);
}
return result;
}
}

View File

@ -58,6 +58,16 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV3
/// </summary>
public string MerchantCertPrivateKey { get; set; } = default!;
/// <summary>
/// 获取或设置是否自动加密请求中的敏感字段数据。
/// </summary>
public bool AutoEncryptRequestSensitiveProperty { get; set; }
/// <summary>
/// 获取或设置是否自动解密请求中的敏感字段数据。
/// </summary>
public bool AutoDecryptResponseSensitiveProperty { get; set; }
/// <summary>
/// 获取或设置微信商户平台证书管理器。
/// <para>默认值:<see cref="Settings.InMemoryCertificateManager"/></para>

View File

@ -0,0 +1,189 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Xunit;
namespace SKIT.FlurlHttpClient.Wechat.TenpayV3.UnitTests
{
public class WechatTenpayResponseDecryptionTests
{
private const string RSA_PRIVATE_KEY = "-----BEGIN PRIVATE KEY-----MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCd094X2jr6wrL1pIBTSbKX5hVqxyCJhKLqaf31KQsw2LdzfXEOL45KiYr6IshwZaSPFok7Sq+1wLrO6/DZ7/GSmzVwdFIV/zjDbI3jaSHt69ZAJGBS212AzC4cwHvpGrHm3kqzVifJyUNxsoTvWklRGSE50pPWaXoxmdPgD5da8IPDHNzkPM0QYTOlHmIRsDQZpWiZRPaFdrN3i/qG9toCRCPVptCVNYrWLDEFXh3ioOqOdTk1fHxFczaNf4YvQhsqXlG/lGiqGbuPuFYfF/RvS9KfptAM1yycknTslkSeiOpA22sFbDdb+m7DHv+nc8DQPPmD7SxDwVIDTcIS7pEJAgMBAAECggEAM6E+czEZwDR3FLKGpDhkqxpCgVa4xyPTo7mesVeG6KjMgoRTup9F/g42n5NHXLVzereVlwbcqiltMwmuFa1PqnUJq0ryG24NZKibVfxrdAiFYyBDPneyg9LHdvJk1qG79tlbOIWDqJglbwlGQYKYn2YIH4FKiYahyZ4X2KFhEwc9mWydHSOyN8zOOJcRCy1FzUcUvITRKob+Q9epz9/4/VX0g1AnB6FwIR5pBqwMYLSv+g+JxfVKPRnSaIxq/2HOvhiyJ7fUqX1yGI+konJJHrn66JIux8xt4SxEnomZBveHlOMUTgTqovxpXisbvXQGBDV7dwW/qhGZet6h57qogQKBgQDNFLP9S6aPnwseeavxK0ygQpgJRbXbHCyqH/mVA/Pg9DaIhxl7+JCC1lS/JuZslow4t3JvjwixAzQEz9SkwUuiRO5vUWb5R+DZJ8HeqtMfoll6wxepu1TQT1pTPnSHliJQP15k5AQ1bkTZjcGA79iUmkksa4EP/GWtOooE8JhMEQKBgQDFA6oyBtbMyWlnGmdsieQuRsjCklZhPL93INX5VUfcfRqQdhqrmoaJg+OZTwVrARp6VHGEaURBTSj6bSoRbBckFNxjVsL6Utpgof+ZWmr5u8ZGHGHIPJMLt7GxI9SItpNNNY33OiUkkfSH4zHK5KZeG9sKKraQwITJCwLZUnnNeQKBgChHkLKHUUeULVLnAuZzYrF3YvUvQ/CtL/iaHyMti5D7Zlqabl7zCy8nea2xrkBVsWTSYx+WMFbUEjt/tnxFmt1cPJiQnHEJtRfxvxpE4wKrmHeMKfGkYZwoec0vzyNyUXsBd0DJqCn2Zn90YDU65ocJZqXa15aUNEQ54zHlL4SBAoGAHbve3OwBUSj4unHWuB/bi0xtkkgJt2U2tGEFSjsfvFw5PSJGBi4tLeX03Ld7ZtnkyB+kfkpw3bYqgBknpzd8CpsHZAq9JJCKmtj4PYnS6Vv4oa4458KUoskXjVeOBRAhDR8PDQf+gRVyJWwZoLh/j2Z+2Xr20MPthnYd+PSko2kCgYBra4rMhYx2Hg0rRe2O7ju+MPm+JK01VpbvwDTnEPnYgMImDmLAXF6GljCt3iy/8X1WcjMPxGjTJ/xfTMne/aqKwvPhZCBL4DdNLNRzppCovsaaMHzrQzy4cvg0IEhIprFeR7ED4eMs8zLUhl3vgNhHOkeQ7cyuEnTl5wB9xOkbSw==-----END PRIVATE KEY-----";
private const string RSA_PUBLIC_KEY = "-----BEGIN PUBLIC KEY-----MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAndPeF9o6+sKy9aSAU0myl+YVascgiYSi6mn99SkLMNi3c31xDi+OSomK+iLIcGWkjxaJO0qvtcC6zuvw2e/xkps1cHRSFf84w2yN42kh7evWQCRgUttdgMwuHMB76Rqx5t5Ks1YnyclDcbKE71pJURkhOdKT1ml6MZnT4A+XWvCDwxzc5DzNEGEzpR5iEbA0GaVomUT2hXazd4v6hvbaAkQj1abQlTWK1iwxBV4d4qDqjnU5NXx8RXM2jX+GL0IbKl5Rv5Roqhm7j7hWHxf0b0vSn6bQDNcsnJJ07JZEnojqQNtrBWw3W/puwx7/p3PA0Dz5g+0sQ8FSA03CEu6RCQIDAQAB-----END PUBLIC KEY-----";
private const string MockText = "mock_text";
private readonly Lazy<WechatTenpayClient> MockClientInstance = new Lazy<WechatTenpayClient>(() =>
{
return new WechatTenpayClient(new WechatTenpayClientOptions()
{
MerchantCertPrivateKey = RSA_PRIVATE_KEY
});
}, isThreadSafe: false);
[Fact(DisplayName = "解密响应中的敏感数据([GET] /ecommerce/applyments/out-request-no/{out_request_no}")]
public void DecryptResponseSensitiveProperty_GetEcommerceApplymentByOutRequestNumberResponseTest()
{
var mock = new Models.GetEcommerceApplymentByOutRequestNumberResponse()
{
RawStatus = 200,
ApplymentState = MockText,
AccountValidation = new Models.GetEcommerceApplymentByOutRequestNumberResponse.Types.AccountValidation()
{
AccountName = Utilities.RSAUtility.EncryptWithECB(RSA_PUBLIC_KEY, MockText),
AccountNumber = Utilities.RSAUtility.EncryptWithECB(RSA_PUBLIC_KEY, MockText),
DestinationAccountName = MockText,
DestinationAccountNumber = MockText
}
};
var data = MockClientInstance.Value.DecryptResponseSensitiveProperty(mock);
Assert.Equal(MockText, data.ApplymentState);
Assert.Equal(MockText, data.AccountValidation.AccountNumber);
Assert.Equal(MockText, data.AccountValidation.DestinationAccountName);
Assert.Equal(MockText, data.AccountValidation.DestinationAccountNumber);
Assert.Equal(MockText, data.AccountValidation.AccountName);
}
[Fact(DisplayName = "解密响应中的敏感数据([GET] /ecommerce/bill/fundflowbill")]
public void DecryptResponseSensitiveProperty_GetEcommerceBillFundflowBillResponse()
{
var mock = new Models.GetEcommerceBillFundflowBillResponse()
{
RawStatus = 200,
DownloadBillCount = 1,
DownloadBillList = new Models.GetEcommerceBillFundflowBillResponse.Types.DownloadBill[]
{
new Models.GetEcommerceBillFundflowBillResponse.Types.DownloadBill()
{
DownloadUrl = MockText,
EncryptKey = Utilities.RSAUtility.EncryptWithECB(RSA_PUBLIC_KEY, MockText)
},
new Models.GetEcommerceBillFundflowBillResponse.Types.DownloadBill()
{
DownloadUrl = MockText,
EncryptKey = Utilities.RSAUtility.EncryptWithECB(RSA_PUBLIC_KEY, MockText)
}
}
};
var data = MockClientInstance.Value.DecryptResponseSensitiveProperty(mock);
Assert.Equal(1, data.DownloadBillCount);
Assert.Equal(MockText, data.DownloadBillList[0].DownloadUrl);
Assert.Equal(MockText, data.DownloadBillList[0].EncryptKey);
Assert.Equal(MockText, data.DownloadBillList[1].DownloadUrl);
Assert.Equal(MockText, data.DownloadBillList[1].EncryptKey);
}
[Fact(DisplayName = "解密响应中的敏感数据([GET] /merchant-service/complaints-v2/{complaint_id}")]
public void DecryptResponseSensitiveProperty_GetMerchantServiceComplaintByComplaintIdResponse()
{
var mock = new Models.GetMerchantServiceComplaintByComplaintIdResponse()
{
RawStatus = 200,
ComplaintOrderList = new Models.GetMerchantServiceComplaintByComplaintIdResponse.Types.ComplaintOrder[]
{
new Models.GetMerchantServiceComplaintByComplaintIdResponse.Types.ComplaintOrder()
{
TransactionId = MockText
}
},
ComplaintDetail = MockText,
PayerPhone = Utilities.RSAUtility.EncryptWithECB(RSA_PUBLIC_KEY, MockText)
};
var data = MockClientInstance.Value.DecryptResponseSensitiveProperty(mock);
Assert.Equal(MockText, data.ComplaintDetail);
Assert.Equal(MockText, data.PayerPhone);
Assert.Equal(MockText, data.ComplaintOrderList[0].TransactionId);
}
[Fact(DisplayName = "解密响应中的敏感数据([GET] /merchant-service/complaints-v2")]
public void DecryptResponseSensitiveProperty_QueryMerchantServiceComplaintsResponse()
{
var mock = new Models.QueryMerchantServiceComplaintsResponse()
{
RawStatus = 200,
ComplaintList = new Models.QueryMerchantServiceComplaintsResponse.Types.Complaint[]
{
new Models.QueryMerchantServiceComplaintsResponse.Types.Complaint()
{
PayerPhone = Utilities.RSAUtility.EncryptWithECB(RSA_PUBLIC_KEY, MockText),
PayerOpenId = MockText
}
}
};
var data = MockClientInstance.Value.DecryptResponseSensitiveProperty(mock);
Assert.Equal(MockText, data.ComplaintList[0].PayerPhone);
Assert.Equal(MockText, data.ComplaintList[0].PayerOpenId);
}
[Fact(DisplayName = "解密响应中的敏感数据([GET] /smartguide/guides")]
public void DecryptResponseSensitiveProperty_QuerySmartGuidesResponse()
{
var mock = new Models.QuerySmartGuidesResponse()
{
RawStatus = 200,
GuideList = new Models.QuerySmartGuidesResponse.Types.Guide[]
{
new Models.QuerySmartGuidesResponse.Types.Guide()
{
GuideId = MockText,
UserName = Utilities.RSAUtility.EncryptWithECB(RSA_PUBLIC_KEY, MockText),
UserMobile = Utilities.RSAUtility.EncryptWithECB(RSA_PUBLIC_KEY, MockText)
}
}
};
var data = MockClientInstance.Value.DecryptResponseSensitiveProperty(mock);
Assert.Equal(MockText, data.GuideList[0].GuideId);
Assert.Equal(MockText, data.GuideList[0].UserName);
Assert.Equal(MockText, data.GuideList[0].UserMobile);
}
[Fact(DisplayName = "解密响应中的敏感数据([GET] /transfer/batches/out-batch-no/{out_batch_no}/details/out-detail-no/{out_detail_no}")]
public void DecryptResponseSensitiveProperty_GetTransferBatchDetailByOutDetailNumberResponse()
{
var mock = new Models.GetTransferBatchDetailByOutDetailNumberResponse()
{
RawStatus = 200,
UserName = Utilities.RSAUtility.EncryptWithECB(RSA_PUBLIC_KEY, MockText),
OpenId = MockText
};
var data = MockClientInstance.Value.DecryptResponseSensitiveProperty(mock);
Assert.Equal(MockText, data.UserName);
Assert.Equal(MockText, data.OpenId);
}
[Fact(DisplayName = "解密响应中的敏感数据([GET] /transfer/batches/batch-id/{batch_id}/details/detail-id/{detail_id}")]
public void DecryptResponseSensitiveProperty_GetTransferBatchDetailByDetailIdResponse()
{
var mock = new Models.GetTransferBatchDetailByDetailIdResponse()
{
RawStatus = 200,
UserName = Utilities.RSAUtility.EncryptWithECB(RSA_PUBLIC_KEY, MockText),
OpenId = MockText
};
var data = MockClientInstance.Value.DecryptResponseSensitiveProperty(mock);
Assert.Equal(MockText, data.UserName);
Assert.Equal(MockText, data.OpenId);
}
[Fact(DisplayName = "解密响应中的敏感数据([GET] /partner-transfer/batches/batch-id/{batch_id}/details/detail-id/{detail_id}")]
public void DecryptResponseSensitiveProperty_GetPartnerTransferBatchDetailByDetailIdResponse()
{
var mock = new Models.GetPartnerTransferBatchDetailByDetailIdResponse()
{
RawStatus = 200,
UserName = Utilities.RSAUtility.EncryptWithECB(RSA_PUBLIC_KEY, MockText),
OpenId = MockText
};
var data = MockClientInstance.Value.DecryptResponseSensitiveProperty(mock);
Assert.Equal(MockText, data.UserName);
Assert.Equal(MockText, data.OpenId);
}
}
}

View File

@ -21,7 +21,7 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV3.UnitTests
Assert.NotNull(response.WechatpayCertSerialNumber);
Assert.NotNull(response.WechatpaySignature);
TestClients.Instance.DecryptResponseEncryptedData(ref response);
response = TestClients.Instance.DecryptResponseSensitiveProperty(response);
foreach (var certificateModel in response.CertificateList)
{
TestClients.GlobalCertificateManager.AddEntry(new Settings.CertificateEntry(certificateModel));