refactor(work): 优化加解密及哈希算法工具类

This commit is contained in:
Fu Diwei
2024-02-04 22:53:47 +08:00
committed by RHQYZ
parent 69955a1f2b
commit a11df544e5
8 changed files with 86 additions and 75 deletions

View File

@@ -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)
{

View File

@@ -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}&timestamp={timestamp}&url={url.Split('#')[0]}").ToLower();
string sign = Utilities.SHA1Utility.Hash($"jsapi_ticket={jsapiTicket}&noncestr={nonce}&timestamp={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}&timestamp={timestamp}&url={url.Split('#')[0]}").ToLower();
string sign = Utilities.SHA1Utility.Hash($"jsapi_ticket={jsapiTicket}&noncestr={nonce}&timestamp={timestamp}&url={url.Split('#')[0]}").Value!.ToLower();
return new ReadOnlyDictionary<string, string>(new Dictionary<string, string>()
{

View File

@@ -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">经过编码后的(通常为 Base64AES 密钥。</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);
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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();
}