421 lines
14 KiB
C#
421 lines
14 KiB
C#
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
|
||
}
|