Skip to content

Commit 7909b86

Browse files
committed
feat(openai): 新增 AES 算法工具类
1 parent fceec1e commit 7909b86

File tree

4 files changed

+145
-1
lines changed

4 files changed

+145
-1
lines changed

docs/WechatOpenAI/README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
# SKIT.FlurlHttpClient.Wechat.OpenAI
22

3-
基于 `Flurl.Http`[微信对话开放平台](https://openai.weixin.qq.com/) HTTP API SDK。
3+
基于 `Flurl.Http`[微信对话开放平台](https://chatbot.weixin.qq.com/) HTTP API SDK。
44

55
---
66

77
## 功能
88

99
- 基于微信对话开放平台 API 封装。
10+
- 提供了微信对话开放平台所需的 AES、SHA-1 等算法工具类。
1011
- 提供了解析回调通知事件等扩展方法。
1112

1213
---

src/SKIT.FlurlHttpClient.Wechat.OpenAI/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
### 【功能特性】
1010

1111
- 基于微信对话开放平台 API 封装。
12+
- 提供了微信对话开放平台所需的 AES、SHA-1 等算法工具类。
1213
- 提供了解析回调通知事件等扩展方法。
1314

1415
---
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
using System;
2+
using System.Security.Cryptography;
3+
4+
namespace SKIT.FlurlHttpClient.Wechat.OpenAI.Utilities
5+
{
6+
using SKIT.FlurlHttpClient.Primitives;
7+
8+
/// <summary>
9+
/// AES 算法工具类。
10+
/// </summary>
11+
public static class AESUtility
12+
{
13+
/// <summary>
14+
/// 基于 CBC 模式解密数据。
15+
/// </summary>
16+
/// <param name="keyBytes">AES 密钥字节数组。</param>
17+
/// <param name="ivBytes">初始化向量字节数组。</param>
18+
/// <param name="cipherBytes">待解密数据字节数组。</param>
19+
/// <returns>解密后的数据字节数组。</returns>
20+
public static byte[] DecryptWithCBC(byte[] keyBytes, byte[] ivBytes, byte[] cipherBytes)
21+
{
22+
if (keyBytes is null) throw new ArgumentNullException(nameof(keyBytes));
23+
if (ivBytes is null) throw new ArgumentNullException(nameof(ivBytes));
24+
if (cipherBytes is null) throw new ArgumentNullException(nameof(cipherBytes));
25+
26+
using (SymmetricAlgorithm aes = Aes.Create())
27+
{
28+
aes.Mode = CipherMode.CBC;
29+
aes.Padding = PaddingMode.PKCS7;
30+
aes.Key = keyBytes;
31+
aes.IV = ivBytes;
32+
33+
using ICryptoTransform transform = aes.CreateDecryptor();
34+
return transform.TransformFinalBlock(cipherBytes, 0, cipherBytes.Length);
35+
}
36+
}
37+
38+
/// <summary>
39+
/// 基于 CBC 模式解密数据。
40+
/// </summary>
41+
/// <param name="encodingKey">经过编码后的(通常为 Base64)AES 密钥。</param>
42+
/// <param name="encodingIV">经过编码后的(通常为 Base64)初始化向量。</param>
43+
/// <param name="encodingCipher">经过编码后的(通常为 Base64)待解密数据。</param>
44+
/// <returns>解密后的数据。</returns>
45+
public static EncodedString DecryptWithCBC(EncodedString encodingKey, EncodedString encodingIV, EncodedString encodingCipher)
46+
{
47+
if (encodingKey.Value is null) throw new ArgumentNullException(nameof(encodingKey));
48+
if (encodingIV.Value is null) throw new ArgumentNullException(nameof(encodingIV));
49+
if (encodingCipher.Value is null) throw new ArgumentNullException(nameof(encodingCipher));
50+
51+
byte[] plainBytes = DecryptWithCBC(
52+
keyBytes: EncodedString.FromString(encodingKey, fallbackEncodingKind: EncodingKinds.Base64),
53+
ivBytes: EncodedString.FromString(encodingIV, fallbackEncodingKind: EncodingKinds.Base64),
54+
cipherBytes: EncodedString.FromString(encodingCipher, fallbackEncodingKind: EncodingKinds.Base64)
55+
);
56+
return EncodedString.ToLiteralString(plainBytes);
57+
}
58+
59+
/// <summary>
60+
/// 基于 CBC 模式加密数据。
61+
/// </summary>
62+
/// <param name="keyBytes">AES 密钥字节数组。</param>
63+
/// <param name="ivBytes">初始化向量字节数组。</param>
64+
/// <param name="plainBytes">待加密数据字节数组。</param>
65+
/// <returns>加密后的数据字节数组。</returns>
66+
public static byte[] EncryptWithCBC(byte[] keyBytes, byte[] ivBytes, byte[] plainBytes)
67+
{
68+
if (keyBytes is null) throw new ArgumentNullException(nameof(keyBytes));
69+
if (ivBytes is null) throw new ArgumentNullException(nameof(ivBytes));
70+
if (plainBytes is null) throw new ArgumentNullException(nameof(plainBytes));
71+
72+
using (SymmetricAlgorithm aes = Aes.Create())
73+
{
74+
aes.Mode = CipherMode.CBC;
75+
aes.Padding = PaddingMode.PKCS7;
76+
aes.Key = keyBytes;
77+
aes.IV = ivBytes;
78+
79+
using ICryptoTransform transform = aes.CreateEncryptor();
80+
return transform.TransformFinalBlock(plainBytes, 0, plainBytes.Length);
81+
}
82+
}
83+
84+
/// <summary>
85+
/// 基于 CBC 模式加密数据。
86+
/// </summary>
87+
/// <param name="encodingKey">经过编码后的(通常为 Base64)AES 密钥。</param>
88+
/// <param name="encodingIV">经过编码后的(通常为 Base64)初始化向量。</param>
89+
/// <param name="plainData">待加密数据。</param>
90+
/// <returns>经过 Base64 编码的加密后的数据。</returns>
91+
public static EncodedString EncryptWithCBC(EncodedString encodingKey, EncodedString encodingIV, string plainData)
92+
{
93+
if (encodingKey.Value is null) throw new ArgumentNullException(nameof(encodingKey));
94+
if (encodingIV.Value is null) throw new ArgumentNullException(nameof(encodingIV));
95+
if (plainData is null) throw new ArgumentNullException(nameof(plainData));
96+
97+
byte[] plainBytes = EncryptWithCBC(
98+
keyBytes: EncodedString.FromString(encodingKey, fallbackEncodingKind: EncodingKinds.Base64),
99+
ivBytes: EncodedString.FromString(encodingIV, fallbackEncodingKind: EncodingKinds.Base64),
100+
plainBytes: EncodedString.FromLiteralString(plainData)
101+
);
102+
return EncodedString.ToBase64String(plainBytes);
103+
}
104+
}
105+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
using System;
2+
using System.Text;
3+
using Xunit;
4+
5+
namespace SKIT.FlurlHttpClient.Wechat.OpenAI.UnitTests
6+
{
7+
using SKIT.FlurlHttpClient.Primitives;
8+
9+
public class TestCase_ToolsAESUtilityTests
10+
{
11+
[Fact(DisplayName = "测试用例:AES-CBC 加密")]
12+
public void TestAESCBCEncryption()
13+
{
14+
string key = "q1Os1ZMe0nG28KUEx9lg3HjK7V5QyXvi212fzsgDqgz=";
15+
string iv = "q1Os1ZMe0nG28KUEx9lg3A=="; // iv 是 key 的前 16 个字节
16+
string plainText = "{\"answer_type\":\"text\",\"text_info\":{\"short_answer\":\"answer\"}}";
17+
18+
string expectedCipherData = "aJhHfz6xc9iQiTLwusQe0HYKT6itYwq/YgQHltmLPf2UfpD+8ODJ8lrrxOMxy5NiALZqz1eYGtwD7cLQDP3ADg==";
19+
string actualCipherData = Utilities.AESUtility.EncryptWithCBC(encodingKey: new EncodedString(key, EncodingKinds.Base64), encodingIV: new EncodedString(iv, EncodingKinds.Base64), plainData: plainText)!;
20+
21+
Assert.Equal(expectedCipherData, actualCipherData, ignoreCase: true);
22+
}
23+
24+
[Fact(DisplayName = "测试用例:AES-CBC 解密")]
25+
public void TestAESCBCDecryption()
26+
{
27+
string key = "q1Os1ZMe0nG28KUEx9lg3HjK7V5QyXvi212fzsgDqgz=";
28+
string iv = "q1Os1ZMe0nG28KUEx9lg3A=="; // iv 是 key 的前 16 个字节
29+
string cipherText = "rUWkvTY9vRPOeVDSH/IdNXHmvgsUQtPkp7QtBQjSS1tcuTHGPWv8O3PlxbnsjCogsM7+EY+As4yF2kp4yxXpP2U7RmbDsU/luRO/EqkpFFsoxMZZArz2XH1YeSdnDyHYPWzjiicBYjNiqqpTMX8ekrqooN0cCEH7JBcbEe6btmiK8hZkysKTUJfG1DTpbONxON5+YuVPelVpzW5ry9sRYLDcqhImMb9FqI+BlIVAIXt5g+e70rheSqpeXz98pEROx7yPeRi3tXPAibuwg+vKDhoN6LuM0hzvyNzPjwK2gMmQB5yVuBZUalYIIZTVaMNGu4H6RK6MovLyM2cKfMUTphKaBBKpAvsV0o4/QRY0MvxeRYvZAQXEzOG3dJ7BRB2KEqBKttT7jMK8MO5HEXDE0CJxtNI4Rjww9XYmPhBM7lOZSF97YNEg1NhwcXvUc3YcrR334PhWJeu2dZCHaJzBqVXFxq/WprNHM0Gw06o6p5oWb4/nzXKYbpJWDyqTN/aztwo5sppHwlYrzNzF7gERP691qoabTHiCd0H+Ea3t65gTyNo2+ssvS1RVsKubApS4BkbZb/EaZCTKP20pcvDBoJk3QLi8ObyBq8sIcLwVjzelLMUgCDa059gBuao+S9qdHXebEZyS49BqAxngMWjHU5uCRO/x2b9w8nwfCCT8b0Q=";
30+
31+
string expectedPlainData = "{\"RequestId\":\"123123456456789789123456789\",\"SessionId\":\"12345678901234567_12345678909876543\",\"Query\":\"北京限行尾号是多少\",\"SkillName\":\"限行\",\"IntentName\":\"查限行尾号\",\"Slots\":[{\"SlotName\":\"from_loc\",\"SlotValue\":\"北京\",\"NormalizeValue\":\"{\\\"type\\\":\\\"LOC_CHINA_CITY\\\",\\\"city\\\":\\\"北京市\\\",\\\"city_simple\\\":\\\"北京\\\",\\\"loc_ori\\\":\\\"北京\\\"}\"}],\"Timestamp\":1704135845,\"Signature\":\"96f439043e1f7d2bb38162e35406f173\",\"ThirdApiId\":1234,\"ThirdApiName\":\"车辆限行\",\"UserId\":\"97f7e892\"}";
32+
string actualPlainData = Utilities.AESUtility.DecryptWithCBC(encodingKey: new EncodedString(key, EncodingKinds.Base64), encodingIV: new EncodedString(iv, EncodingKinds.Base64), encodingCipher: new EncodedString(cipherText, EncodingKinds.Base64))!;
33+
34+
Assert.Equal(expectedPlainData, actualPlainData, ignoreCase: true);
35+
}
36+
}
37+
}

0 commit comments

Comments
 (0)