From 3e68ed3ddf7304b2e845ac380fd2b6506b0ab22d Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Mon, 7 Jun 2021 00:35:30 +0800 Subject: [PATCH] =?UTF-8?q?feat(wxapi):=20=E6=96=B0=E5=A2=9E=20WxBizMsgCry?= =?UTF-8?q?ptor=20=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/WxBizMsgCryptor.cs | 329 ++++++++++++++++++ 1 file changed, 329 insertions(+) create mode 100644 src/SKIT.FlurlHttpClient.Wechat.Api/Utilities/WxBizMsgCryptor.cs diff --git a/src/SKIT.FlurlHttpClient.Wechat.Api/Utilities/WxBizMsgCryptor.cs b/src/SKIT.FlurlHttpClient.Wechat.Api/Utilities/WxBizMsgCryptor.cs new file mode 100644 index 00000000..4cfc1e5c --- /dev/null +++ b/src/SKIT.FlurlHttpClient.Wechat.Api/Utilities/WxBizMsgCryptor.cs @@ -0,0 +1,329 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Security; +using System.Security.Cryptography; +using System.Text; +using System.Threading.Tasks; +using System.Xml; + +namespace SKIT.FlurlHttpClient.Wechat.Api.Utilities +{ + public static class WxBizMsgCryptor + { + private const int AES_KEY_SIZE = 256; + private const int AES_BLOCK_SIZE = 128; + + private static char ConvertToChar(int i) + { + byte ret = (byte)(i & 0xFF); + return (char)ret; + } + + private static byte[] KCS7Encoder(int len) + { + if (len <= 0) throw new ArgumentOutOfRangeException(nameof(len)); + + const int BLOCK_SIZE = 32; + + int paddingLength = BLOCK_SIZE - (len % BLOCK_SIZE); + if (paddingLength == 0) + { + paddingLength = BLOCK_SIZE; + } + + char paddingChar = ConvertToChar(paddingLength); + string tmp = string.Empty; + for (int index = 0; index < paddingLength; index++) + { + tmp += paddingChar; + } + + return Encoding.UTF8.GetBytes(tmp); + } + + private static string CreateRandCode(int len) + { + if (len <= 0) throw new ArgumentOutOfRangeException(nameof(len)); + + Random random = new Random(unchecked((int)DateTime.Now.Ticks)); + string[] source = "2,3,4,5,6,7,a,c,d,e,f,h,i,j,k,m,n,p,r,s,t,A,C,D,E,F,G,H,J,K,M,N,P,Q,R,S,U,V,W,X,Y,Z".Split(','); + string result = string.Empty; + for (int i = 0; i < len; i++) + { + int randval = random.Next(0, source.Length - 1); + result += source[randval]; + } + + return result; + } + + private static byte[] Decode2(byte[] decryptedBytes) + { + if (decryptedBytes == null) throw new ArgumentNullException(nameof(decryptedBytes)); + + int pad = (int)decryptedBytes[decryptedBytes.Length - 1]; + if (pad < 1 || pad > 32) + { + pad = 0; + } + + byte[] res = new byte[decryptedBytes.Length - pad]; + Array.Copy(decryptedBytes, 0, res, 0, decryptedBytes.Length - pad); + return res; + } + + private static byte[] AESDecrypt(byte[] keyBytes, byte[] ivBytes, byte[] chiperBytes) + { + if (keyBytes == null) throw new ArgumentNullException(nameof(keyBytes)); + if (ivBytes == null) throw new ArgumentNullException(nameof(ivBytes)); + if (chiperBytes == null) throw new ArgumentNullException(nameof(chiperBytes)); + + using RijndaelManaged aes = new RijndaelManaged(); + aes.KeySize = 256; + aes.BlockSize = 128; + aes.Mode = CipherMode.CBC; + //aes.Padding = PaddingMode.PKCS7; + aes.Padding = PaddingMode.None; + aes.Key = keyBytes; + aes.IV = ivBytes; + + using (var decryptor = aes.CreateDecryptor(aes.Key, aes.IV)) + using (var ms = new MemoryStream()) + using (var cs = new CryptoStream(ms, decryptor, CryptoStreamMode.Write)) + { + byte[] bMsg = new byte[chiperBytes.Length + 32 - chiperBytes.Length % 32]; + Array.Copy(chiperBytes, bMsg, chiperBytes.Length); + cs.Write(chiperBytes, 0, chiperBytes.Length); + + byte[] plainBytes = Decode2(ms.ToArray()); + return plainBytes; + } + } + + private static string AESEncrypt(byte[] keyBytes, byte[] ivBytes, byte[] plainBytes) + { + if (keyBytes == null) throw new ArgumentNullException(nameof(keyBytes)); + if (ivBytes == null) throw new ArgumentNullException(nameof(ivBytes)); + if (plainBytes == null) throw new ArgumentNullException(nameof(plainBytes)); + + using var aes = new RijndaelManaged(); + aes.KeySize = AES_KEY_SIZE; + aes.BlockSize = AES_BLOCK_SIZE; + //aes.Padding = PaddingMode.PKCS7; + aes.Padding = PaddingMode.None; + aes.Mode = CipherMode.CBC; + aes.Key = keyBytes; + aes.IV = ivBytes; + + byte[] msgBytes = new byte[plainBytes.Length + 32 - plainBytes.Length % 32]; + Array.Copy(plainBytes, msgBytes, plainBytes.Length); + byte[] padBytes = KCS7Encoder(plainBytes.Length); + Array.Copy(padBytes, 0, msgBytes, plainBytes.Length, padBytes.Length); + + using (var encryptor = aes.CreateEncryptor(aes.Key, aes.IV)) + using (var ms = new MemoryStream()) + using (var cs = new CryptoStream(ms, encryptor, CryptoStreamMode.Write)) + { + cs.Write(msgBytes, 0, msgBytes.Length); + byte[] chiperBytes = ms.ToArray(); + return Convert.ToBase64String(chiperBytes); + } + } + + /// + /// AES 解密微信回调加密数据。 + /// + /// 微信推送来的加密文本内容(即 `Encrypt` 字段的值)。 + /// 微信后台设置的 EncodingAESKey。 + /// 微信 AppId。 + /// 解密后的文本内容。 + public static string AESDecrypt(string chiperText, string encodingAESKey, out string appId) + { + if (chiperText == null) throw new ArgumentNullException(nameof(chiperText)); + if (encodingAESKey == null) throw new ArgumentNullException(nameof(encodingAESKey)); + + byte[] chiperBytes = Convert.FromBase64String(chiperText); + byte[] keyBytes = Convert.FromBase64String(encodingAESKey + "="); + byte[] ivBytes = new byte[16]; + Array.Copy(keyBytes, ivBytes, 16); + byte[] btmpMsg = AESDecrypt(chiperBytes: chiperBytes, ivBytes: ivBytes, keyBytes: keyBytes); + + int len = BitConverter.ToInt32(btmpMsg, 16); + len = IPAddress.NetworkToHostOrder(len); + + byte[] bMsg = new byte[len]; + byte[] bCorpId = new byte[btmpMsg.Length - 20 - len]; + Array.Copy(btmpMsg, 20, bMsg, 0, len); + Array.Copy(btmpMsg, 20 + len, bCorpId, 0, btmpMsg.Length - 20 - len); + + appId = Encoding.UTF8.GetString(bCorpId); + return Encoding.UTF8.GetString(bMsg); + } + + /// + /// AES 加密微信回调敏感数据。 + /// + /// 返回给微信的原始文本内容。 + /// 微信后台设置的 EncodingAESKey。 + /// 微信 AppId。 + /// 加密后的文本内容。 + public static string AESEncrypt(string plainText, string encodingAESKey, string appId) + { + if (plainText == null) throw new ArgumentNullException(nameof(plainText)); + if (encodingAESKey == null) throw new ArgumentNullException(nameof(encodingAESKey)); + if (appId == null) throw new ArgumentNullException(nameof(appId)); + + byte[] keyBytes = Convert.FromBase64String(encodingAESKey + "="); + byte[] ivBytes = new byte[16]; + Array.Copy(keyBytes, ivBytes, 16); + + string randCode = CreateRandCode(16); + byte[] bRand = Encoding.UTF8.GetBytes(randCode); + byte[] bCorpId = Encoding.UTF8.GetBytes(appId); + byte[] bMsgTmp = Encoding.UTF8.GetBytes(plainText); + byte[] bMsgLen = BitConverter.GetBytes(IPAddress.HostToNetworkOrder(bMsgTmp.Length)); + byte[] bMsg = new byte[bRand.Length + bMsgLen.Length + bCorpId.Length + bMsgTmp.Length]; + + Array.Copy(bRand, bMsg, bRand.Length); + Array.Copy(bMsgLen, 0, bMsg, bRand.Length, bMsgLen.Length); + Array.Copy(bMsgTmp, 0, bMsg, bRand.Length + bMsgLen.Length, bMsgTmp.Length); + Array.Copy(bCorpId, 0, bMsg, bRand.Length + bMsgLen.Length + bMsgTmp.Length, bCorpId.Length); + + return AESEncrypt(keyBytes: keyBytes, ivBytes: ivBytes, plainBytes: bMsg); + } + + /// + /// 验证微信回调签名。 + /// + /// 微信后台设置的 Token。 + /// 微信推送来的时间戳字符串。 + /// 微信推送来的随机字符串。 + /// 微信推送来的加密文本内容(即 `Encrypt` 字段的值)。 + /// 待验证的签名。 + /// 验证结果。 + public static bool VerifySignature(string sToken, string sTimestamp, string sNonce, string sMsgEncrypt, string sMsgSign) + { + if (sToken == null) throw new ArgumentNullException(nameof(sToken)); + if (sTimestamp == null) throw new ArgumentNullException(nameof(sTimestamp)); + if (sNonce == null) throw new ArgumentNullException(nameof(sNonce)); + if (sMsgEncrypt == null) throw new ArgumentNullException(nameof(sMsgEncrypt)); + if (sMsgSign == null) throw new ArgumentNullException(nameof(sMsgSign)); + + string expectedSign = GenerateSignature(sToken, sTimestamp, sNonce, sMsgEncrypt); + return string.Equals(expectedSign, sMsgSign, StringComparison.OrdinalIgnoreCase); + } + + /// + /// 生成微信回调签名。 + /// + /// 微信后台设置的 Token。 + /// 返回给微信的时间戳字符串。 + /// 返回给微信的随机字符串。 + /// 返回给微信的加密文本内容。 + /// 签名。 + public static string GenerateSignature(string sToken, string sTimestamp, string sNonce, string sMsgEncrypt) + { + if (sToken == null) throw new ArgumentNullException(nameof(sToken)); + if (sTimestamp == null) throw new ArgumentNullException(nameof(sTimestamp)); + if (sNonce == null) throw new ArgumentNullException(nameof(sNonce)); + if (sMsgEncrypt == null) throw new ArgumentNullException(nameof(sMsgEncrypt)); + + ISet set = new SortedSet(); + set.Add(sToken); + set.Add(sTimestamp); + set.Add(sNonce); + set.Add(sMsgEncrypt); + + string rawText = string.Join(string.Empty, set.ToArray()); + string signText = Security.SHA1Utility.Hash(rawText); + return signText.ToLower(); + } + + /// + /// 尝试解析微信推送过来的 XML 数据(仅适用于兼容模式或安全模式)。 + /// + /// 微信推送来的 XML 数据。 + /// 如果解析成功,将返回解析后的 `Encrypt` 字段的值。 + /// 指示是否是有效的 XML 内容。 + public static bool TryParseXml(string xml, out string? encryptedMsg) + { + return TryParseXml(xml, out encryptedMsg, out _); + } + + /// + /// 尝试解析微信推送过来的 XML 数据(仅适用于兼容模式或安全模式)。 + /// + /// 微信推送来的 XML 数据。 + /// 如果解析成功,将返回解析后的 `ToUserName` 字段的值。 + /// 如果解析成功,将返回解析后的 `Encrypt` 字段的值。 + /// 指示是否是有效的 XML 内容。 + public static bool TryParseXml(string xml, out string? encryptedMsg, out string? toUserName) + { + if (xml == null) throw new ArgumentNullException(nameof(xml)); + + encryptedMsg = null; + toUserName = null; + + try + { + XmlDocument xmlDoc = new XmlDocument(); + xmlDoc.LoadXml(xml); + + XmlNode xmlRoot = xmlDoc.FirstChild; + encryptedMsg = xmlRoot["Encrypt"]?.InnerText?.ToString(); + toUserName = xmlRoot["ToUserName"]?.InnerText?.ToString(); + + return !string.IsNullOrEmpty(encryptedMsg); + } + catch (XmlException) + { + return false; + } + } + + /// + /// 将返回给微信的加密文本内容包装成 XML 格式(仅适用于安全模式)。 + /// + /// 微信后台设置的 Token。 + /// 返回给微信的加密文本内容。 + /// + public static string WrapXml(string sToken, string sMsgEncrypt) + { + if (sToken == null) throw new ArgumentNullException(nameof(sToken)); + if (sMsgEncrypt == null) throw new ArgumentNullException(nameof(sMsgEncrypt)); + + string sTimestamp = DateTimeOffset.Now.ToLocalTime().ToUnixTimeSeconds().ToString(); + string sNonce = DateTimeOffset.Now.Ticks.ToString("x"); + string sMsgSign = GenerateSignature(sToken: sToken, sTimestamp: sTimestamp, sNonce: sNonce, sMsgEncrypt: sMsgEncrypt); + return WrapXml(sTimestamp: sTimestamp, sNonce: sNonce, sMsgEncrypt: sMsgEncrypt, sMsgSign: sMsgSign); + } + + /// + /// 将返回给微信的加密文本内容包装成 XML 格式(仅适用于安全模式)。 + /// + /// 返回给微信的时间戳字符串。 + /// 返回给微信的随机字符串。 + /// 返回给微信的加密文本内容。 + /// 返回给微信的签名。 + /// + public static string WrapXml(string sTimestamp, string sNonce, string sMsgEncrypt, string sMsgSign) + { + if (sTimestamp == null) throw new ArgumentNullException(nameof(sTimestamp)); + if (sNonce == null) throw new ArgumentNullException(nameof(sNonce)); + if (sMsgEncrypt == null) throw new ArgumentNullException(nameof(sMsgEncrypt)); + if (sMsgSign == null) throw new ArgumentNullException(nameof(sMsgSign)); + + StringBuilder builder = new StringBuilder(); + builder.AppendFormat(""); + builder.AppendFormat("", SecurityElement.Escape(sTimestamp)); + builder.AppendFormat("", SecurityElement.Escape(sNonce)); + builder.AppendFormat("", SecurityElement.Escape(sMsgEncrypt)); + builder.AppendFormat("", SecurityElement.Escape(sMsgSign)); + builder.AppendFormat(""); + return builder.ToString(); + } + } +}