mirror of
https://gitee.com/fudiwei/DotNetCore.SKIT.FlurlHttpClient.Wechat.git
synced 2026-03-10 00:13:36 +08:00
refactor(work): 优化加解密及哈希算法工具类
This commit is contained in:
@@ -7,6 +7,7 @@ using System.Threading.Tasks;
|
||||
|
||||
namespace SKIT.FlurlHttpClient.Wechat.Work.ExtendedSDK.Finance
|
||||
{
|
||||
using SKIT.FlurlHttpClient.Primitives;
|
||||
using SKIT.FlurlHttpClient.Wechat.Work.ExtendedSDK.Finance.InteropServices;
|
||||
|
||||
/// <summary>
|
||||
@@ -200,9 +201,9 @@ namespace SKIT.FlurlHttpClient.Wechat.Work.ExtendedSDK.Finance
|
||||
throw new WechatWorkFinanceException($"Failed to decrypt random key of the encrypted chat data, because there is no private key matched the verion: \"{request.PublicKeyVersion}\".");
|
||||
|
||||
encryptKey = Utilities.RSAUtility.DecryptWithECB(
|
||||
privateKey: encryptionKeyEntry.Value.PrivateKey,
|
||||
cipherText: request.EncryptedRandomKey
|
||||
);
|
||||
privateKeyPem: encryptionKeyEntry.Value.PrivateKey,
|
||||
encodingCipher: new EncodedString(request.EncryptedRandomKey, EncodingKinds.Base64)
|
||||
)!;
|
||||
}
|
||||
catch (WechatWorkFinanceException)
|
||||
{
|
||||
|
||||
@@ -29,7 +29,7 @@ namespace SKIT.FlurlHttpClient.Wechat.Work
|
||||
|
||||
string timestamp = DateTimeOffset.Now.ToLocalTime().ToUnixTimeSeconds().ToString();
|
||||
string nonce = Guid.NewGuid().ToString("N");
|
||||
string sign = Utilities.SHA1Utility.Hash($"jsapi_ticket={jsapiTicket}&noncestr={nonce}×tamp={timestamp}&url={url.Split('#')[0]}").ToLower();
|
||||
string sign = Utilities.SHA1Utility.Hash($"jsapi_ticket={jsapiTicket}&noncestr={nonce}×tamp={timestamp}&url={url.Split('#')[0]}").Value!.ToLower();
|
||||
|
||||
return new ReadOnlyDictionary<string, string>(new Dictionary<string, string>()
|
||||
{
|
||||
@@ -58,7 +58,7 @@ namespace SKIT.FlurlHttpClient.Wechat.Work
|
||||
|
||||
string timestamp = DateTimeOffset.Now.ToLocalTime().ToUnixTimeSeconds().ToString();
|
||||
string nonce = Guid.NewGuid().ToString("N");
|
||||
string sign = Utilities.SHA1Utility.Hash($"jsapi_ticket={jsapiTicket}&noncestr={nonce}×tamp={timestamp}&url={url.Split('#')[0]}").ToLower();
|
||||
string sign = Utilities.SHA1Utility.Hash($"jsapi_ticket={jsapiTicket}&noncestr={nonce}×tamp={timestamp}&url={url.Split('#')[0]}").Value!.ToLower();
|
||||
|
||||
return new ReadOnlyDictionary<string, string>(new Dictionary<string, string>()
|
||||
{
|
||||
|
||||
@@ -1,29 +1,32 @@
|
||||
using System;
|
||||
using System.Text;
|
||||
using Org.BouncyCastle.Crypto;
|
||||
using Org.BouncyCastle.Crypto.Parameters;
|
||||
using Org.BouncyCastle.Security;
|
||||
|
||||
namespace SKIT.FlurlHttpClient.Wechat.Work.Utilities
|
||||
{
|
||||
using SKIT.FlurlHttpClient.Primitives;
|
||||
|
||||
/// <summary>
|
||||
/// AES 算法工具类。
|
||||
/// </summary>
|
||||
public static class AESUtility
|
||||
{
|
||||
private const string AES_CIPHER_ALGORITHM_GCM = "AES/GCM";
|
||||
private const string AES_CIPHER_PADDING_NOPADDING = "NoPadding";
|
||||
/// <summary>
|
||||
/// 填充模式:NoPadding。
|
||||
/// </summary>
|
||||
public const string PADDING_MODE_NOPADDING = "NoPadding";
|
||||
|
||||
/// <summary>
|
||||
/// 基于 GCM 模式解密数据。
|
||||
/// </summary>
|
||||
/// <param name="keyBytes">AES 密钥字节数组。</param>
|
||||
/// <param name="nonceBytes">加密使用的初始化向量字节数组。</param>
|
||||
/// <param name="aadBytes">加密使用的附加数据包字节数组。</param>
|
||||
/// <param name="nonceBytes">初始化向量字节数组。</param>
|
||||
/// <param name="associatedDataBytes">附加数据字节数组。</param>
|
||||
/// <param name="cipherBytes">待解密数据字节数组。</param>
|
||||
/// <param name="paddingMode">填充模式。(默认值:<see cref="AES_CIPHER_PADDING_NOPADDING"/>)</param>
|
||||
/// <param name="paddingMode">填充模式。(默认值:<see cref="PADDING_MODE_NOPADDING"/>)</param>
|
||||
/// <returns>解密后的数据字节数组。</returns>
|
||||
public static byte[] DecryptWithGCM(byte[] keyBytes, byte[] nonceBytes, byte[]? aadBytes, byte[] cipherBytes, string paddingMode = AES_CIPHER_PADDING_NOPADDING)
|
||||
public static byte[] DecryptWithGCM(byte[] keyBytes, byte[] nonceBytes, byte[]? associatedDataBytes, byte[] cipherBytes, string paddingMode = PADDING_MODE_NOPADDING)
|
||||
{
|
||||
const int KEY_LENGTH_BYTE = 32;
|
||||
const int NONCE_LENGTH_BYTE = 12;
|
||||
@@ -36,12 +39,12 @@ namespace SKIT.FlurlHttpClient.Wechat.Work.Utilities
|
||||
if (cipherBytes is 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));
|
||||
IBufferedCipher cipher = CipherUtilities.GetCipher($"AES/GCM/{paddingMode}");
|
||||
ICipherParameters cipherParams = new AeadParameters(
|
||||
new KeyParameter(keyBytes),
|
||||
TAG_LENGTH_BYTE * 8,
|
||||
nonceBytes,
|
||||
aadBytes
|
||||
associatedDataBytes
|
||||
);
|
||||
cipher.Init(false, cipherParams);
|
||||
byte[] plainBytes = new byte[cipher.GetOutputSize(cipherBytes.Length)];
|
||||
@@ -53,26 +56,26 @@ namespace SKIT.FlurlHttpClient.Wechat.Work.Utilities
|
||||
/// <summary>
|
||||
/// 基于 GCM 模式解密数据。
|
||||
/// </summary>
|
||||
/// <param name="key">AES 密钥。</param>
|
||||
/// <param name="nonce">加密使用的初始化向量。</param>
|
||||
/// <param name="aad">加密使用的附加数据包。</param>
|
||||
/// <param name="cipherText">经 Base64 编码后的待解密数据。</param>
|
||||
/// <param name="paddingMode">填充模式。(默认值:<see cref="AES_CIPHER_PADDING_NOPADDING"/>)</param>
|
||||
/// <returns>解密后的文本数据。</returns>
|
||||
public static string DecryptWithGCM(string key, string nonce, string? aad, string cipherText, string paddingMode = AES_CIPHER_PADDING_NOPADDING)
|
||||
/// <param name="encodingKey">经过编码后的(通常为 Base64)AES 密钥。</param>
|
||||
/// <param name="encodingNonce">经过编码后的(通常为 Base64)初始化向量。</param>
|
||||
/// <param name="encodingAssociatedData">经过编码后的(通常为 Base64)附加数据。</param>
|
||||
/// <param name="encodingCipher">经过编码后的(通常为 Base64)待解密数据。</param>
|
||||
/// <param name="paddingMode">填充模式。(默认值:<see cref="PADDING_MODE_NOPADDING"/>)</param>
|
||||
/// <returns>解密后的数据。</returns>
|
||||
public static EncodedString DecryptWithGCM(EncodedString encodingKey, EncodedString encodingNonce, EncodedString encodingAssociatedData, EncodedString encodingCipher, string paddingMode = PADDING_MODE_NOPADDING)
|
||||
{
|
||||
if (key is null) throw new ArgumentNullException(nameof(key));
|
||||
if (nonce is null) throw new ArgumentNullException(nameof(nonce));
|
||||
if (cipherText is null) throw new ArgumentNullException(nameof(cipherText));
|
||||
if (encodingKey.Value is null) throw new ArgumentNullException(nameof(encodingKey));
|
||||
if (encodingNonce.Value is null) throw new ArgumentNullException(nameof(encodingNonce));
|
||||
if (encodingCipher.Value is null) throw new ArgumentNullException(nameof(encodingCipher));
|
||||
|
||||
byte[] plainBytes = DecryptWithGCM(
|
||||
keyBytes: Encoding.UTF8.GetBytes(key),
|
||||
nonceBytes: Encoding.UTF8.GetBytes(nonce),
|
||||
aadBytes: aad is not null ? Encoding.UTF8.GetBytes(aad) : null,
|
||||
cipherBytes: Convert.FromBase64String(cipherText),
|
||||
keyBytes: EncodedString.FromString(encodingKey, fallbackEncodingKind: EncodingKinds.Base64),
|
||||
nonceBytes: EncodedString.FromString(encodingNonce, fallbackEncodingKind: EncodingKinds.Base64),
|
||||
associatedDataBytes: encodingAssociatedData.Value is not null ? EncodedString.FromString(encodingAssociatedData, fallbackEncodingKind: EncodingKinds.Base64) : null,
|
||||
cipherBytes: EncodedString.FromString(encodingCipher, fallbackEncodingKind: EncodingKinds.Base64),
|
||||
paddingMode: paddingMode
|
||||
);
|
||||
return Encoding.UTF8.GetString(plainBytes);
|
||||
return EncodedString.ToLiteralString(plainBytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using Org.BouncyCastle.Crypto;
|
||||
using Org.BouncyCastle.Crypto.Parameters;
|
||||
@@ -9,17 +8,21 @@ using Org.BouncyCastle.Security;
|
||||
|
||||
namespace SKIT.FlurlHttpClient.Wechat.Work.Utilities
|
||||
{
|
||||
using SKIT.FlurlHttpClient.Primitives;
|
||||
|
||||
/// <summary>
|
||||
/// RSA 算法工具类。
|
||||
/// </summary>
|
||||
public static class RSAUtility
|
||||
{
|
||||
private const string RSA_CIPHER_ALGORITHM_NONE = "RSA/ECB";
|
||||
private const string RSA_CIPHER_PADDING_PKCS1 = "PKCS1PADDING";
|
||||
/// <summary>
|
||||
/// 填充模式:PKCS1Padding。
|
||||
/// </summary>
|
||||
public const string PADDING_MODE_PKCS1 = "PKCS1PADDING";
|
||||
|
||||
private static byte[] ConvertPrivateKeyPkcs1PemToByteArray(string privateKey)
|
||||
private static byte[] ConvertPrivateKeyPemToByteArray(string privateKeyPem)
|
||||
{
|
||||
using (TextReader textReader = new StringReader(privateKey))
|
||||
using (TextReader textReader = new StringReader(privateKeyPem))
|
||||
using (PemReader pemReader = new PemReader(textReader))
|
||||
{
|
||||
AsymmetricCipherKeyPair cipherKeyPair = (AsymmetricCipherKeyPair)pemReader.ReadObject();
|
||||
@@ -30,24 +33,19 @@ namespace SKIT.FlurlHttpClient.Wechat.Work.Utilities
|
||||
pemWriter.WriteObject(pkcs8);
|
||||
pemWriter.Writer.Close();
|
||||
|
||||
privateKey = textWriter.ToString()!;
|
||||
privateKey = privateKey
|
||||
privateKeyPem = textWriter.ToString()!;
|
||||
privateKeyPem = privateKeyPem
|
||||
.Replace("-----BEGIN PRIVATE KEY-----", string.Empty)
|
||||
.Replace("-----END PRIVATE KEY-----", string.Empty);
|
||||
privateKey = Regex.Replace(privateKey, "\\s+", string.Empty);
|
||||
return Convert.FromBase64String(privateKey);
|
||||
privateKeyPem = Regex.Replace(privateKeyPem, "\\s+", string.Empty);
|
||||
return Convert.FromBase64String(privateKeyPem);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static RsaKeyParameters ParsePrivateKeyPemToPrivateKeyParameters(byte[] privateKeyBytes)
|
||||
{
|
||||
return (RsaKeyParameters)PrivateKeyFactory.CreateKey(privateKeyBytes);
|
||||
}
|
||||
|
||||
private static byte[] DecryptWithECB(RsaKeyParameters rsaPrivateKeyParams, byte[] cipherBytes, string paddingMode)
|
||||
{
|
||||
IBufferedCipher cipher = CipherUtilities.GetCipher($"{RSA_CIPHER_ALGORITHM_NONE}/{paddingMode}");
|
||||
IBufferedCipher cipher = CipherUtilities.GetCipher($"RSA/ECB/{paddingMode}");
|
||||
cipher.Init(false, rsaPrivateKeyParams);
|
||||
return cipher.DoFinal(cipherBytes);
|
||||
}
|
||||
@@ -55,35 +53,35 @@ namespace SKIT.FlurlHttpClient.Wechat.Work.Utilities
|
||||
/// <summary>
|
||||
/// 使用私钥基于 ECB 模式解密数据。
|
||||
/// </summary>
|
||||
/// <param name="privateKeyBytes">PKCS#1 私钥字节数据。</param>
|
||||
/// <param name="cipherBytes">待解密的数据字节数据。</param>
|
||||
/// <param name="paddingMode">填充模式。(默认值:<see cref="RSA_CIPHER_PADDING_PKCS1"/>)</param>
|
||||
/// <param name="privateKeyBytes">PKCS#1/PKCS#8 私钥字节数组。</param>
|
||||
/// <param name="cipherBytes">待解密的数据字节数组。</param>
|
||||
/// <param name="paddingMode">填充模式。(默认值:<see cref="PADDING_MODE_PKCS1"/>)</param>
|
||||
/// <returns>解密后的数据字节数组。</returns>
|
||||
public static byte[] DecryptWithECB(byte[] privateKeyBytes, byte[] cipherBytes, string paddingMode = RSA_CIPHER_PADDING_PKCS1)
|
||||
public static byte[] DecryptWithECB(byte[] privateKeyBytes, byte[] cipherBytes, string paddingMode = PADDING_MODE_PKCS1)
|
||||
{
|
||||
if (privateKeyBytes is null) throw new ArgumentNullException(nameof(privateKeyBytes));
|
||||
if (cipherBytes is null) throw new ArgumentNullException(nameof(cipherBytes));
|
||||
|
||||
RsaKeyParameters rsaPrivateKeyParams = ParsePrivateKeyPemToPrivateKeyParameters(privateKeyBytes);
|
||||
RsaKeyParameters rsaPrivateKeyParams = (RsaKeyParameters)PrivateKeyFactory.CreateKey(privateKeyBytes);
|
||||
return DecryptWithECB(rsaPrivateKeyParams, cipherBytes, paddingMode);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 使用私钥基于 ECB 模式解密数据。
|
||||
/// </summary>
|
||||
/// <param name="privateKey">PKCS#1 私钥(PEM 格式)。</param>
|
||||
/// <param name="cipherText">经 Base64 编码的待解密数据。</param>
|
||||
/// <param name="paddingMode">填充模式。(默认值:<see cref="RSA_CIPHER_PADDING_PKCS1"/>)</param>
|
||||
/// <returns>解密后的文本数据。</returns>
|
||||
public static string DecryptWithECB(string privateKey, string cipherText, string paddingMode = RSA_CIPHER_PADDING_PKCS1)
|
||||
/// <param name="privateKeyPem">PKCS#1/PKCS#8 私钥(PEM 格式)。</param>
|
||||
/// <param name="encodingCipher">经过编码后的(通常为 Base64)待解密数据。</param>
|
||||
/// <param name="paddingMode">填充模式。(默认值:<see cref="PADDING_MODE_PKCS1"/>)</param>
|
||||
/// <returns>解密后的数据。</returns>
|
||||
public static EncodedString DecryptWithECB(string privateKeyPem, EncodedString encodingCipher, string paddingMode = PADDING_MODE_PKCS1)
|
||||
{
|
||||
if (privateKey is null) throw new ArgumentNullException(nameof(privateKey));
|
||||
if (cipherText is null) throw new ArgumentNullException(nameof(cipherText));
|
||||
if (privateKeyPem is null) throw new ArgumentNullException(nameof(privateKeyPem));
|
||||
if (encodingCipher.Value is null) throw new ArgumentNullException(nameof(encodingCipher));
|
||||
|
||||
byte[] privateKeyBytes = ConvertPrivateKeyPkcs1PemToByteArray(privateKey);
|
||||
byte[] cipherBytes = Convert.FromBase64String(cipherText);
|
||||
byte[] privateKeyBytes = ConvertPrivateKeyPemToByteArray(privateKeyPem);
|
||||
byte[] cipherBytes = EncodedString.FromString(encodingCipher, fallbackEncodingKind: EncodingKinds.Base64);
|
||||
byte[] plainBytes = DecryptWithECB(privateKeyBytes, cipherBytes, paddingMode);
|
||||
return Encoding.UTF8.GetString(plainBytes);
|
||||
return EncodedString.ToLiteralString(plainBytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,39 +1,44 @@
|
||||
using System;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace SKIT.FlurlHttpClient.Wechat.Work.Utilities
|
||||
{
|
||||
using SKIT.FlurlHttpClient.Primitives;
|
||||
|
||||
/// <summary>
|
||||
/// SHA-1 算法工具类。
|
||||
/// </summary>
|
||||
public static class SHA1Utility
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取 SHA-1 信息摘要。
|
||||
/// 计算 SHA-1 哈希值。
|
||||
/// </summary>
|
||||
/// <param name="bytes">信息字节数组。</param>
|
||||
/// <returns>信息摘要字节数组。</returns>
|
||||
/// <param name="bytes">要计算哈希值的信息字节数组。</param>
|
||||
/// <returns>哈希值字节数组。</returns>
|
||||
public static byte[] Hash(byte[] bytes)
|
||||
{
|
||||
if (bytes is null) throw new ArgumentNullException(nameof(bytes));
|
||||
|
||||
using SHA1 sha = SHA1.Create();
|
||||
return sha.ComputeHash(bytes);
|
||||
#if NET5_0_OR_GREATER
|
||||
return SHA1.HashData(bytes);
|
||||
#else
|
||||
using SHA1 sha1 = SHA1.Create();
|
||||
return sha1.ComputeHash(bytes);
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取 SHA-1 信息摘要。
|
||||
/// 计算 SHA-1 哈希值。
|
||||
/// </summary>
|
||||
/// <param name="message">文本信息。</param>
|
||||
/// <returns>信息摘要。</returns>
|
||||
public static string Hash(string message)
|
||||
/// <param name="message">要计算哈希值的信息。</param>
|
||||
/// <returns>经过十六进制编码的哈希值。</returns>
|
||||
public static EncodedString Hash(string message)
|
||||
{
|
||||
if (message is null) throw new ArgumentNullException(nameof(message));
|
||||
|
||||
byte[] msgBytes = Encoding.UTF8.GetBytes(message);
|
||||
byte[] msgBytes = EncodedString.FromLiteralString(message);
|
||||
byte[] hashBytes = Hash(msgBytes);
|
||||
return BitConverter.ToString(hashBytes).Replace("-", string.Empty);
|
||||
return EncodedString.ToHexString(hashBytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -233,7 +233,7 @@ namespace SKIT.FlurlHttpClient.Wechat.Work.Utilities
|
||||
tmp.Sort(StringComparer.Ordinal);
|
||||
|
||||
string rawText = string.Join(string.Empty, tmp);
|
||||
string signText = SHA1Utility.Hash(rawText);
|
||||
string signText = SHA1Utility.Hash(rawText).Value!;
|
||||
return signText.ToLower();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user