HaniBlindBox/server/HoneyBox/tests/HoneyBox.Tests/Services/WechatPayV3DecryptionPropertyTests.cs
2026-01-25 20:28:11 +08:00

421 lines
14 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System.Security.Cryptography;
using System.Text;
using FsCheck;
using FsCheck.Xunit;
using HoneyBox.Core.Interfaces;
using HoneyBox.Core.Services;
using HoneyBox.Model.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Moq;
using Xunit;
namespace HoneyBox.Tests.Services;
/// <summary>
/// 微信支付 V3 解密属性测试
/// **Feature: wechat-pay-v3-upgrade**
/// </summary>
public class WechatPayV3DecryptionPropertyTests
{
/// <summary>
/// 生成有效的 32 字节 APIv3 密钥
/// </summary>
private static string GenerateApiV3Key()
{
const string chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
var random = new Random();
var result = new char[32];
for (int i = 0; i < 32; i++)
{
result[i] = chars[random.Next(chars.Length)];
}
return new string(result);
}
/// <summary>
/// 生成有效的 12 字节 nonce
/// </summary>
private static string GenerateNonce()
{
const string chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
var random = new Random();
var result = new char[12];
for (int i = 0; i < 12; i++)
{
result[i] = chars[random.Next(chars.Length)];
}
return new string(result);
}
private IWechatPayV3Service CreateService()
{
var options = new DbContextOptionsBuilder<HoneyBoxDbContext>()
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
.Options;
var dbContext = new HoneyBoxDbContext(options);
var httpClient = new HttpClient();
var logger = Mock.Of<ILogger<WechatPayV3Service>>();
var configService = Mock.Of<IWechatPayConfigService>();
return new WechatPayV3Service(dbContext, httpClient, logger, configService);
}
#region Property 9: V3 Round-Trip
/// <summary>
/// **Feature: wechat-pay-v3-upgrade, Property 9: V3 回调解密 Round-Trip**
/// *For any* 有效的支付结果数据,使用 AES-256-GCM 加密后再解密,
/// 应该得到与原始数据等价的结果。
/// **Validates: Requirements 4.3**
/// </summary>
[Property(MaxTest = 100)]
public bool DecryptRoundTrip_ShouldReturnOriginalData(NonEmptyString plaintext, PositiveInt seed)
{
var service = CreateService();
// 生成固定的密钥和 nonce基于 seed 确保可重复)
var random = new Random(seed.Get);
var apiV3Key = GenerateApiV3KeyWithSeed(random);
var nonce = GenerateNonceWithSeed(random);
var associatedData = "transaction";
// 清理输入(移除可能导致问题的字符)
var cleanPlaintext = plaintext.Get.Replace("\0", "");
if (string.IsNullOrEmpty(cleanPlaintext))
{
return true; // 跳过空字符串
}
try
{
// 加密
var ciphertext = service.EncryptNotifyResource(cleanPlaintext, nonce, associatedData, apiV3Key);
// 解密
var decrypted = service.DecryptNotifyResource(ciphertext, nonce, associatedData, apiV3Key);
// 验证 round-trip
return cleanPlaintext == decrypted;
}
catch
{
return false;
}
}
/// <summary>
/// **Feature: wechat-pay-v3-upgrade, Property 9: V3 回调解密 Round-Trip**
/// *For any* JSON 格式的支付结果数据,加密后再解密应该保持 JSON 结构不变。
/// **Validates: Requirements 4.3**
/// </summary>
[Property(MaxTest = 100)]
public bool DecryptRoundTrip_JsonData_ShouldPreserveStructure(
NonEmptyString orderNo,
NonEmptyString transactionId,
PositiveInt amount,
PositiveInt seed)
{
var service = CreateService();
var random = new Random(seed.Get);
var apiV3Key = GenerateApiV3KeyWithSeed(random);
var nonce = GenerateNonceWithSeed(random);
var associatedData = "transaction";
// 构建类似微信支付回调的 JSON 数据
var cleanOrderNo = orderNo.Get.Replace("\"", "").Replace("\\", "").Replace("\n", "").Replace("\r", "");
var cleanTransactionId = transactionId.Get.Replace("\"", "").Replace("\\", "").Replace("\n", "").Replace("\r", "");
var jsonData = $"{{\"out_trade_no\":\"{cleanOrderNo}\",\"transaction_id\":\"{cleanTransactionId}\",\"trade_state\":\"SUCCESS\",\"amount\":{{\"total\":{amount.Get}}}}}";
try
{
// 加密
var ciphertext = service.EncryptNotifyResource(jsonData, nonce, associatedData, apiV3Key);
// 解密
var decrypted = service.DecryptNotifyResource(ciphertext, nonce, associatedData, apiV3Key);
// 验证 round-trip
return jsonData == decrypted;
}
catch
{
return false;
}
}
/// <summary>
/// **Feature: wechat-pay-v3-upgrade, Property 9: V3 回调解密 Round-Trip**
/// *For any* 有效数据,使用不同的密钥解密应该失败。
/// **Validates: Requirements 4.3**
/// </summary>
[Property(MaxTest = 100)]
public bool Decrypt_WithWrongKey_ShouldFail(NonEmptyString plaintext, PositiveInt seed)
{
var service = CreateService();
var random = new Random(seed.Get);
var apiV3Key1 = GenerateApiV3KeyWithSeed(random);
var apiV3Key2 = GenerateApiV3KeyWithSeed(new Random(seed.Get + 1)); // 不同的密钥
var nonce = GenerateNonceWithSeed(random);
var associatedData = "transaction";
// 确保两个密钥不同
if (apiV3Key1 == apiV3Key2)
{
return true; // 跳过相同密钥的情况
}
var cleanPlaintext = plaintext.Get.Replace("\0", "");
if (string.IsNullOrEmpty(cleanPlaintext))
{
return true;
}
try
{
// 使用密钥1加密
var ciphertext = service.EncryptNotifyResource(cleanPlaintext, nonce, associatedData, apiV3Key1);
// 使用密钥2解密应该失败
try
{
service.DecryptNotifyResource(ciphertext, nonce, associatedData, apiV3Key2);
return false; // 如果没有抛出异常,测试失败
}
catch (InvalidOperationException)
{
return true; // 预期的异常
}
catch (CryptographicException)
{
return true; // 预期的异常
}
}
catch
{
return false;
}
}
/// <summary>
/// **Feature: wechat-pay-v3-upgrade, Property 9: V3 回调解密 Round-Trip**
/// *For any* 有效数据,使用不同的 nonce 解密应该失败。
/// **Validates: Requirements 4.3**
/// </summary>
[Property(MaxTest = 100)]
public bool Decrypt_WithWrongNonce_ShouldFail(NonEmptyString plaintext, PositiveInt seed)
{
var service = CreateService();
var random = new Random(seed.Get);
var apiV3Key = GenerateApiV3KeyWithSeed(random);
var nonce1 = GenerateNonceWithSeed(random);
var nonce2 = GenerateNonceWithSeed(new Random(seed.Get + 1)); // 不同的 nonce
var associatedData = "transaction";
// 确保两个 nonce 不同
if (nonce1 == nonce2)
{
return true;
}
var cleanPlaintext = plaintext.Get.Replace("\0", "");
if (string.IsNullOrEmpty(cleanPlaintext))
{
return true;
}
try
{
// 使用 nonce1 加密
var ciphertext = service.EncryptNotifyResource(cleanPlaintext, nonce1, associatedData, apiV3Key);
// 使用 nonce2 解密应该失败
try
{
service.DecryptNotifyResource(ciphertext, nonce2, associatedData, apiV3Key);
return false;
}
catch (InvalidOperationException)
{
return true;
}
catch (CryptographicException)
{
return true;
}
}
catch
{
return false;
}
}
/// <summary>
/// **Feature: wechat-pay-v3-upgrade, Property 9: V3 回调解密 Round-Trip**
/// *For any* 有效数据,篡改密文后解密应该失败。
/// **Validates: Requirements 4.3**
/// </summary>
[Property(MaxTest = 100)]
public bool Decrypt_WithTamperedCiphertext_ShouldFail(NonEmptyString plaintext, PositiveInt seed)
{
var service = CreateService();
var random = new Random(seed.Get);
var apiV3Key = GenerateApiV3KeyWithSeed(random);
var nonce = GenerateNonceWithSeed(random);
var associatedData = "transaction";
var cleanPlaintext = plaintext.Get.Replace("\0", "");
if (string.IsNullOrEmpty(cleanPlaintext))
{
return true;
}
try
{
// 加密
var ciphertext = service.EncryptNotifyResource(cleanPlaintext, nonce, associatedData, apiV3Key);
// 篡改密文(修改一个字符)
var ciphertextBytes = Convert.FromBase64String(ciphertext);
if (ciphertextBytes.Length > 0)
{
ciphertextBytes[0] = (byte)(ciphertextBytes[0] ^ 0xFF);
}
var tamperedCiphertext = Convert.ToBase64String(ciphertextBytes);
// 解密篡改后的密文应该失败
try
{
service.DecryptNotifyResource(tamperedCiphertext, nonce, associatedData, apiV3Key);
return false;
}
catch (InvalidOperationException)
{
return true;
}
catch (CryptographicException)
{
return true;
}
}
catch
{
return false;
}
}
#endregion
#region
private static string GenerateApiV3KeyWithSeed(Random random)
{
const string chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
var result = new char[32];
for (int i = 0; i < 32; i++)
{
result[i] = chars[random.Next(chars.Length)];
}
return new string(result);
}
private static string GenerateNonceWithSeed(Random random)
{
const string chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
var result = new char[12];
for (int i = 0; i < 12; i++)
{
result[i] = chars[random.Next(chars.Length)];
}
return new string(result);
}
#endregion
#region
/// <summary>
/// **Feature: wechat-pay-v3-upgrade, Property 9: V3 回调解密 Round-Trip**
/// 空的 associated data 应该正常工作。
/// **Validates: Requirements 4.3**
/// </summary>
[Fact]
public void DecryptRoundTrip_EmptyAssociatedData_ShouldWork()
{
var service = CreateService();
var apiV3Key = "d1cxc0vXCUH2984901DxddPJMYqcwcnd";
var nonce = "abcdefghijkl";
var plaintext = "{\"out_trade_no\":\"TEST123\",\"trade_state\":\"SUCCESS\"}";
// 加密(空 associated data
var ciphertext = service.EncryptNotifyResource(plaintext, nonce, "", apiV3Key);
// 解密
var decrypted = service.DecryptNotifyResource(ciphertext, nonce, "", apiV3Key);
Assert.Equal(plaintext, decrypted);
}
/// <summary>
/// **Feature: wechat-pay-v3-upgrade, Property 9: V3 回调解密 Round-Trip**
/// 中文内容应该正常加解密。
/// **Validates: Requirements 4.3**
/// </summary>
[Fact]
public void DecryptRoundTrip_ChineseContent_ShouldWork()
{
var service = CreateService();
var apiV3Key = "d1cxc0vXCUH2984901DxddPJMYqcwcnd";
var nonce = "abcdefghijkl";
var plaintext = "{\"description\":\"商品购买-测试商品\",\"trade_state_desc\":\"支付成功\"}";
// 加密
var ciphertext = service.EncryptNotifyResource(plaintext, nonce, "transaction", apiV3Key);
// 解密
var decrypted = service.DecryptNotifyResource(ciphertext, nonce, "transaction", apiV3Key);
Assert.Equal(plaintext, decrypted);
}
/// <summary>
/// **Feature: wechat-pay-v3-upgrade, Property 9: V3 回调解密 Round-Trip**
/// 无效的 APIv3 密钥长度应该抛出异常。
/// **Validates: Requirements 4.3**
/// </summary>
[Fact]
public void Decrypt_InvalidKeyLength_ShouldThrow()
{
var service = CreateService();
var invalidKey = "shortkey"; // 不是 32 字节
var nonce = "abcdefghijkl";
var ciphertext = "dGVzdA=="; // 随便一个 base64
Assert.Throws<ArgumentException>(() =>
service.DecryptNotifyResource(ciphertext, nonce, "transaction", invalidKey));
}
/// <summary>
/// **Feature: wechat-pay-v3-upgrade, Property 9: V3 回调解密 Round-Trip**
/// 空密文应该抛出异常。
/// **Validates: Requirements 4.3**
/// </summary>
[Fact]
public void Decrypt_EmptyCiphertext_ShouldThrow()
{
var service = CreateService();
var apiV3Key = "d1cxc0vXCUH2984901DxddPJMYqcwcnd";
var nonce = "abcdefghijkl";
Assert.Throws<ArgumentException>(() =>
service.DecryptNotifyResource("", nonce, "transaction", apiV3Key));
}
#endregion
}