From e5f12b905d5cf3cae009460b002b0d73ca662997 Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Tue, 10 Oct 2023 15:38:09 +0800 Subject: [PATCH] =?UTF-8?q?feat(work):=20=E6=96=B0=E5=A2=9E=20AES=20?= =?UTF-8?q?=E7=AE=97=E6=B3=95=E5=B7=A5=E5=85=B7=E7=B1=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Utilities/AESUtility.cs | 2 +- .../Utilities/AESUtility.cs | 78 +++++++++++++++++++ .../TestCase_ToolsAESUtilityTests.cs | 21 +++++ 3 files changed, 100 insertions(+), 1 deletion(-) create mode 100644 src/SKIT.FlurlHttpClient.Wechat.Work/Utilities/AESUtility.cs create mode 100644 test/SKIT.FlurlHttpClient.Wechat.Work.UnitTests/TestCase_ToolsAESUtilityTests.cs diff --git a/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Utilities/AESUtility.cs b/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Utilities/AESUtility.cs index d2c28b51..2e2dba0e 100644 --- a/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Utilities/AESUtility.cs +++ b/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Utilities/AESUtility.cs @@ -68,7 +68,7 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV3.Utilities byte[] plainBytes = DecryptWithGCM( keyBytes: Encoding.UTF8.GetBytes(key), nonceBytes: Encoding.UTF8.GetBytes(nonce), - aadBytes: Encoding.UTF8.GetBytes(aad ?? string.Empty), + aadBytes: aad != null ? Encoding.UTF8.GetBytes(aad) : null, cipherBytes: Convert.FromBase64String(cipherText), paddingMode: paddingMode ); diff --git a/src/SKIT.FlurlHttpClient.Wechat.Work/Utilities/AESUtility.cs b/src/SKIT.FlurlHttpClient.Wechat.Work/Utilities/AESUtility.cs new file mode 100644 index 00000000..f87c4003 --- /dev/null +++ b/src/SKIT.FlurlHttpClient.Wechat.Work/Utilities/AESUtility.cs @@ -0,0 +1,78 @@ +using System; +using System.Text; +using Org.BouncyCastle.Crypto; +using Org.BouncyCastle.Crypto.Parameters; +using Org.BouncyCastle.Security; + +namespace SKIT.FlurlHttpClient.Wechat.Work.Utilities +{ + /// + /// AES 算法工具类。 + /// + public static class AESUtility + { + private const string AES_CIPHER_ALGORITHM_GCM = "AES/GCM"; + private const string AES_CIPHER_PADDING_NOPADDING = "NoPadding"; + + /// + /// 基于 GCM 模式解密数据。 + /// + /// AES 密钥字节数组。 + /// 加密使用的初始化向量字节数组。 + /// 加密使用的附加数据包字节数组。 + /// 待解密数据字节数组。 + /// 填充模式。(默认值:) + /// 解密后的数据字节数组。 + public static byte[] DecryptWithGCM(byte[] keyBytes, byte[] nonceBytes, byte[]? aadBytes, byte[] cipherBytes, string paddingMode = AES_CIPHER_PADDING_NOPADDING) + { + const int KEY_LENGTH_BYTE = 32; + const int NONCE_LENGTH_BYTE = 12; + const int TAG_LENGTH_BYTE = 16; + + if (keyBytes == null) throw new ArgumentNullException(nameof(keyBytes)); + if (keyBytes.Length != KEY_LENGTH_BYTE) throw new ArgumentException($"Invalid key byte length (expected: {KEY_LENGTH_BYTE}, actual: {keyBytes.Length}).", nameof(keyBytes)); + if (nonceBytes == null) throw new ArgumentNullException(nameof(nonceBytes)); + if (nonceBytes.Length != NONCE_LENGTH_BYTE) throw new ArgumentException($"Invalid nonce byte length (expected: {NONCE_LENGTH_BYTE}, actual: {nonceBytes.Length}).", nameof(nonceBytes)); + if (cipherBytes == null) throw new ArgumentNullException(nameof(cipherBytes)); + if (cipherBytes.Length < TAG_LENGTH_BYTE) throw new ArgumentException($"Invalid cipher byte length (expected: more than {TAG_LENGTH_BYTE}, actual: {cipherBytes.Length}).", nameof(cipherBytes)); + + IBufferedCipher cipher = CipherUtilities.GetCipher(string.Format("{0}/{1}", AES_CIPHER_ALGORITHM_GCM, paddingMode)); + ICipherParameters cipherParams = new AeadParameters( + new KeyParameter(keyBytes), + TAG_LENGTH_BYTE * 8, + nonceBytes, + aadBytes + ); + cipher.Init(false, cipherParams); + byte[] plainBytes = new byte[cipher.GetOutputSize(cipherBytes.Length)]; + int len = cipher.ProcessBytes(cipherBytes, 0, cipherBytes.Length, plainBytes, 0); + cipher.DoFinal(plainBytes, len); + return plainBytes; + } + + /// + /// 基于 GCM 模式解密数据。 + /// + /// AES 密钥。 + /// 加密使用的初始化向量。 + /// 加密使用的附加数据包。 + /// 经 Base64 编码后的待解密数据。 + /// 填充模式。(默认值:) + /// 解密后的文本数据。 + public static string DecryptWithGCM(string key, string nonce, string? aad, string cipherText, string paddingMode = AES_CIPHER_PADDING_NOPADDING) + { + if (key == null) throw new ArgumentNullException(nameof(key)); + if (nonce == null) throw new ArgumentNullException(nameof(nonce)); + if (cipherText == null) throw new ArgumentNullException(nameof(cipherText)); + + byte[] plainBytes = DecryptWithGCM( + keyBytes: Encoding.UTF8.GetBytes(key), + nonceBytes: Encoding.UTF8.GetBytes(nonce), + aadBytes: aad != null ? Encoding.UTF8.GetBytes(aad) : null, + cipherBytes: Convert.FromBase64String(cipherText), + paddingMode: paddingMode + ); + return Encoding.UTF8.GetString(plainBytes); + } + } +} diff --git a/test/SKIT.FlurlHttpClient.Wechat.Work.UnitTests/TestCase_ToolsAESUtilityTests.cs b/test/SKIT.FlurlHttpClient.Wechat.Work.UnitTests/TestCase_ToolsAESUtilityTests.cs new file mode 100644 index 00000000..b0ca8ce8 --- /dev/null +++ b/test/SKIT.FlurlHttpClient.Wechat.Work.UnitTests/TestCase_ToolsAESUtilityTests.cs @@ -0,0 +1,21 @@ +using Xunit; + +namespace SKIT.FlurlHttpClient.Wechat.Work.UnitTests +{ + public class TestCase_ToolsAESUtilityTests + { + [Fact(DisplayName = "测试用例:AES-GCM 解密")] + public void TestAESGCMDecrypt() + { + string key = "f09b03a7a1902b5b4913856f1fd07ab1"; + string nonce = "aae8c2e79c5b"; + string aad = "certificate"; + string cipherText = "x9kkL5w1JuaypcjhrYIP+kVNlN8o8uN4yJyJjy5lg+PyPnQL2Zn//ORaXAyzdaK/WBMVd3u/Y9hLaTBLMyRXzowsrkJ5PT37johye48N7BAJQ0PJwW++d1RdhOOPjoqfmws6rSV5Gv2qhfdKjmpxVjr8xr71dtBt8J2wu+bAV99HHQoAynm/Pp9OQYZgpOQ+1cyFHd43TAxOoFfmixKrXr3HP8lJot0XCUSq4qkr1Hs44FV2KuzntSk8eqKr5N17UcuPF3VYnnnF/AvQ7HuLKWwrHhUbaXfkwy0Q2n36UJMfBj7344S97E8BnS89ojgOPQi+olBPyNgrtDWHgsJAKu7HA6PV/FgmXcrZirje/AH1u25es4z5xItHscm//6rDvALgf7greV5OJzMsSl/KVDtbkjDSzim0j4ZTduIfzh7l6jfOz115ITcNILT9ef2KkcMyBBc89GZlMGeHTbgsBHzGeLawX3dXFjqt5aMnHM3VWCovA2aUM4c//rqkZGf+Va86OEFoJQLiSTFpkOrKxcIxcrbKPLTgiDWRT3wzmnUDg7kSPbluzt3ROvMFq9lB8bO/pBd7TD2w87sfUdKLj69FniX6s37SBeRVhw8GSIvBf5lpLUhqL5zKYlbuAWePuz0wVV4NTtkaVKZlmm8KTODyZDFpsyKPubDDcwT1ftRf5aSVM4x04I/1B7GkNz/TOy+zpJ0h0B7VHdxyO5JYiI/1qsatX/FE2aJdQYMYOtqfDH7ZH5UUKIqo538OKvE2M4MlBR/aVE4z4QDKfE/1kYrOfvVGfDzF/FWHfUrcqB8kdQMvk8vCoM8yYZsX4KE1aoJbNM2pWv2tpr9JE8b/VQgUyHOgPYAha+UOmZki4Sfl9H62687EIWdbM57ZxmwIiBp60SrJLiBfZon9JqXKdtJOKj0CRokQiBnONNXCVerLFeBNQfeKRw8tgJXf+QPohMGYkSDdc8hTgdbmhTwB1Vv01stlYK12QMNRCovlp/fcmpB72Phlq+/3p5pqMzknw+qm5QAz7JnpZJCFHit52gHwAkKRkVPB3HF2rfLrdTYz5c9Bok1ICAY9My47eLFdduIe99V52cjMLQuUmNFBPrDdyZKVqIHJ/wtWO/wIFpAVGSJMHctyEKmeJVc1IQN74Wm00PrpPackHdO3G41bBmkp5pqUdsSgSkwdfNVqv0cMcSe04NrRGNKMcZ7TA/CMaP1YnhxvVE+z8aksJqSJ+gdplvuwl40y5C8UEHeAi1V8Q0Bf4YvYRgOVIWm2Lzjdn2z9PWLGcStUj11/1hthk2li4V3mgm2Cr2IZme2sn4rZmJ6dexGP1nk+ZYOq0xLE8F7oex9gyDN+A/6zHqnuhO/X08qye0gochMr8U89Qvj2c0L3P2mjCea2H1mEriAJPqMPMKIinh1lQJEZufnfCcPxbZLKTtl6zHtHgOztejd1gV/nUyCVKD4MCMfBDy9C/Af8pWx6akOg/QSQNIGA2AI6zprHn9zEjpFIzXJYvruVI22Yt6oF9Xnt7Ki82wRK2M96r4kj6cwSs4exMPGv8fWMrFTm0Br6p6T+HZsxyyn2ChuPIgpfisnce/ZaU/0xCZhK/K79+TK2GeeChq5oEpua/1tx4+kDHi7H9381pLJmy2oXW060c2mmwA9+EpcuwEDhr8fsnghbv41u7b1NhEmWNVUy29Dwaz61PPGUdh5DsvaKLWC+raZ/6UEKPw+tiABJ5o6u2jAWgmEYmmJCKapNgtfPc6D+O0aHH9oqh6u4+8NRAhusPZzDGWBr6AT4pexgWFeEhZhn6bXM9HhpUe0IhOTw5D+tqXrTlNon4kjYibiMUFy1h2YyYS3IEdu1J4xqvo0rFyCxF1C+P6ubc0tClRPkXg=="; + + string actualPlain = Utilities.AESUtility.DecryptWithGCM(key: key, nonce: nonce, aad: aad, cipherText: cipherText); + string expectedPlain = "-----BEGIN CERTIFICATE-----\nMIID3DCCAsSgAwIBAgIUGZNrTcamx3sFSJsQli3v9C6gZe8wDQYJKoZIhvcNAQEL\nBQAwXjELMAkGA1UEBhMCQ04xEzARBgNVBAoTClRlbnBheS5jb20xHTAbBgNVBAsT\nFFRlbnBheS5jb20gQ0EgQ2VudGVyMRswGQYDVQQDExJUZW5wYXkuY29tIFJvb3Qg\nQ0EwHhcNMjAwODAxMDczNTE4WhcNMjUwNzMxMDczNTE4WjBuMRgwFgYDVQQDDA9U\nZW5wYXkuY29tIHNpZ24xEzARBgNVBAoMClRlbnBheS5jb20xHTAbBgNVBAsMFFRl\nbnBheS5jb20gQ0EgQ2VudGVyMQswCQYDVQQGDAJDTjERMA8GA1UEBwwIU2hlblpo\nZW4wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDOAYHqxCqaRzoTIvgV\nixaYJJvmvHbiczbx5MQ9XL1ITSFxkTsNsk7RKHnO7eBS5imheJgQwd22Ky+XclSe\n7B4odssu/l/+gHo2gooTYrrCpQrOkpvGMf8R8aI56BQIF+vsomDvVq1NojHV2Fql\njMwFXzhj2EmU6p6gDv9iL7q1NrfnxFx8iJe4OhIB5Ek4qn1xXxrTUhiULd2vXlbI\nXhRetZSNcJsLt5Rw7D7c8F+aX2JchfeqsZECwKW7bSjMbVWWC6M9MgkB/aId8P0y\n7qEiiXFJkfkg1I/E1ud2apopsid5tdCyRRR6+MhhX2EC8S04MN4soUT7haqNNxX2\nrKHnAgMBAAGjgYEwfzAJBgNVHRMEAjAAMAsGA1UdDwQEAwIE8DBlBgNVHR8EXjBc\nMFqgWKBWhlRodHRwOi8vZXZjYS5pdHJ1cy5jb20uY24vcHVibGljL2l0cnVzY3Js\nP0NBPTFCRDQyMjBFNTBEQkMwNEIwNkFEMzk3NTQ5ODQ2QzAxQzNFOEVCRDIwDQYJ\nKoZIhvcNAQELBQADggEBAJyg2z4oLQmPfftLQWyzbUc9ONhRMtfA+tVlVBgtLLKn\nWuDlsmEntheM07fu84F4pcfs3yHzjD7pAOFbO4Yt1yhQ50DK35sjbRWepPdWJZLl\nni7KBctcmm0o4zq37oB7vonmBEbFqYs9DaINYOjgI3J25iSBkPVC7dBbvFj2xB0L\ncIcXipq30tDdC/oUem27MNzwZAt49WthKhw6u3HSkcE5cO4LyYTsJhSyG/7LXwvV\nMgX4Jyzo0SSiGOU1/beaZssTVI8sTPJVlHWjhNE3Lc2SaAlKGfGwvt0X3cEZEF+7\noEZIFTkkAF2JhqfnpR3gST0G8Umq1SaVtCPP/zVI8x0=\n-----END CERTIFICATE-----"; + + Assert.Equal(expectedPlain, actualPlain); + } + } +}