using System.Security.Cryptography; using System.Text; using FsCheck; using FsCheck.Xunit; using MiAssessment.Core.Interfaces; using MiAssessment.Core.Services; using MiAssessment.Model.Data; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Moq; using Xunit; namespace MiAssessment.Tests.Services; /// /// 微信支付 V3 解密属性测试 /// **Feature: wechat-pay-v3-upgrade** /// public class WechatPayV3DecryptionPropertyTests { /// /// 生成有效的 32 字节 APIv3 密钥 /// 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); } /// /// 生成有效的 12 字节 nonce /// 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() .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()) .Options; var dbContext = new MiAssessmentDbContext(options); var httpClient = new HttpClient(); var logger = Mock.Of>(); var configService = Mock.Of(); return new WechatPayV3Service(dbContext, httpClient, logger, configService); } #region Property 9: V3 回调解密 Round-Trip /// /// **Feature: wechat-pay-v3-upgrade, Property 9: V3 回调解密 Round-Trip** /// *For any* 有效的支付结果数据,使用 AES-256-GCM 加密后再解密, /// 应该得到与原始数据等价的结果。 /// **Validates: Requirements 4.3** /// [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; } } /// /// **Feature: wechat-pay-v3-upgrade, Property 9: V3 回调解密 Round-Trip** /// *For any* JSON 格式的支付结果数据,加密后再解密应该保持 JSON 结构不变。 /// **Validates: Requirements 4.3** /// [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; } } /// /// **Feature: wechat-pay-v3-upgrade, Property 9: V3 回调解密 Round-Trip** /// *For any* 有效数据,使用不同的密钥解密应该失败。 /// **Validates: Requirements 4.3** /// [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; } } /// /// **Feature: wechat-pay-v3-upgrade, Property 9: V3 回调解密 Round-Trip** /// *For any* 有效数据,使用不同的 nonce 解密应该失败。 /// **Validates: Requirements 4.3** /// [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; } } /// /// **Feature: wechat-pay-v3-upgrade, Property 9: V3 回调解密 Round-Trip** /// *For any* 有效数据,篡改密文后解密应该失败。 /// **Validates: Requirements 4.3** /// [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 边界情况测试 /// /// **Feature: wechat-pay-v3-upgrade, Property 9: V3 回调解密 Round-Trip** /// 空的 associated data 应该正常工作。 /// **Validates: Requirements 4.3** /// [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); } /// /// **Feature: wechat-pay-v3-upgrade, Property 9: V3 回调解密 Round-Trip** /// 中文内容应该正常加解密。 /// **Validates: Requirements 4.3** /// [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); } /// /// **Feature: wechat-pay-v3-upgrade, Property 9: V3 回调解密 Round-Trip** /// 无效的 APIv3 密钥长度应该抛出异常。 /// **Validates: Requirements 4.3** /// [Fact] public void Decrypt_InvalidKeyLength_ShouldThrow() { var service = CreateService(); var invalidKey = "shortkey"; // 不是 32 字节 var nonce = "abcdefghijkl"; var ciphertext = "dGVzdA=="; // 随便一个 base64 Assert.Throws(() => service.DecryptNotifyResource(ciphertext, nonce, "transaction", invalidKey)); } /// /// **Feature: wechat-pay-v3-upgrade, Property 9: V3 回调解密 Round-Trip** /// 空密文应该抛出异常。 /// **Validates: Requirements 4.3** /// [Fact] public void Decrypt_EmptyCiphertext_ShouldThrow() { var service = CreateService(); var apiV3Key = "d1cxc0vXCUH2984901DxddPJMYqcwcnd"; var nonce = "abcdefghijkl"; Assert.Throws(() => service.DecryptNotifyResource("", nonce, "transaction", apiV3Key)); } #endregion }