Browse Source

AES 对称加解密

龙腾猫跃 4 months ago
parent
commit
7bf59d7f74

+ 14 - 12
Shared/General/Config.cs

@@ -1,6 +1,9 @@
 namespace MeloongCore;
 
 public interface IConfigProvider {
+    /// <summary>
+    /// 写入加密后的原始数据。
+    /// </summary>
     void Write(string key, JToken value);
     JToken? Read(string key, JToken? defaultValue = null);
     bool HasValue(string key);
@@ -31,27 +34,26 @@ public static class Configs {
     public static readonly JsonConfigProvider AppData = new(Path.Combine(Paths.AppDataThenName, "config.json"));
 
     /// <summary>
-    /// 从 secret.json 中读取特定密钥,如果不存在则抛出异常。
+    /// 从 secret.json 中读取特定密钥,如果未找到对应密钥则抛出异常。
     /// </summary>
     public static string GetSecret(string key) => secret.Read(key)?.ToString() ?? throw new KeyNotFoundException($"Secret not found: {key}");
     private static readonly JsonConfigProvider secret = new(Path.Combine(Paths.AppData, "secret.json"));
 }
 
 public class ConfigEntry<T>(string key, T? defaultValue, IConfigProvider? defaultProvider = null, bool encrypted = false) {
-    private readonly IConfigProvider defaultProvider = defaultProvider ?? Configs.AppData;
-    public string Key = key;
-    public bool Encrypted = encrypted;
-    public T? DefaultValue = defaultValue;
-
     /// <summary>
     /// 当设置项的值实际被改变时触发,参数为新值。
     /// </summary>
     public event Action<T?>? OnChanged;
 
-    public bool HasValue(IConfigProvider? provider = null) 
-        => (provider ?? defaultProvider).HasValue(Key);
-    public void Remove(IConfigProvider? provider = null) 
-        => (provider ?? defaultProvider).Remove(Key);
-    public void Save(IConfigProvider? provider = null) 
-        => (provider ?? defaultProvider).Save();
+
+    private IConfigProvider GetProvider(IConfigProvider? provider) 
+        => provider ?? defaultProvider ?? Configs.AppData;
+
+    public bool HasValue(IConfigProvider? provider = null)
+        => GetProvider(provider).HasValue(key);
+    public void Remove(IConfigProvider? provider = null)
+        => GetProvider(provider).Remove(key);
+    public void Save(IConfigProvider? provider = null)
+        => GetProvider(provider).Save();
 }

+ 63 - 2
Shared/Utils/CryptographyUtils.cs

@@ -58,10 +58,71 @@ public static class CryptographyUtils {
 
     #endregion
 
-    #region ECDSA
+    #region AES 对称加解密
+
+    private const byte AesDataVersion = 1;
+
+    /// <summary>
+    /// 使用 AES-256-CBC 对称加密字符串。
+    /// </summary>
+    /// <returns>加密后的 Base64 字符串。</returns>
+    public static string AesEncrypt(string sourceString, string key = "EncryptKey") {
+        byte[] iv = new byte[16];
+        using var rng = RandomNumberGenerator.Create();
+        rng.GetBytes(iv);
+        byte[] cipherBytes;
+        using var sha256 = SHA256.Create();
+        using Aes aes = Aes.Create();
+        aes.Mode = CipherMode.CBC;
+        aes.Padding = PaddingMode.PKCS7;
+        using ICryptoTransform encryptor = aes.CreateEncryptor(sha256.ComputeHash(Encoding.UTF8.GetBytes(key)), iv);
+        using var ms = new MemoryStream();
+        using var cs = new CryptoStream(ms, encryptor, CryptoStreamMode.Write);
+        byte[] sourceBytes = Encoding.UTF8.GetBytes(sourceString);
+        cs.Write(sourceBytes, 0, sourceBytes.Length);
+        cs.FlushFinalBlock();
+        cipherBytes = ms.ToArray();
+        byte[] encryptedBytes = new byte[1 + iv.Length + cipherBytes.Length];
+        encryptedBytes[0] = AesDataVersion;
+        Array.Copy(iv, 0, encryptedBytes, 1, iv.Length);
+        Array.Copy(cipherBytes, 0, encryptedBytes, 1 + iv.Length, cipherBytes.Length);
+        return Convert.ToBase64String(encryptedBytes);
+    }
+
+    /// <summary>
+    /// 使用 AES-256-CBC 对称解密字符串。
+    /// 如果密钥错误或数据格式有误,则抛出 <see cref="CryptographicException"/>。
+    /// </summary>
+    public static string AesDecrypt(string encryptedString, string key = "EncryptKey") {
+        byte[] encryptedBytes;
+        try {
+            encryptedBytes = Convert.FromBase64String(encryptedString);
+        } catch (FormatException ex) {
+            throw new CryptographicException($"数据不是一个有效的 Base64 字符串({encryptedString})", ex);
+        }
+        int cipherSize = encryptedBytes.Length - 1 - 16;
+        if (encryptedBytes.Length < 1 + 16 + 16 || encryptedBytes[0] != AesDataVersion || cipherSize % 16 != 0)
+            throw new CryptographicException($"对称加密数据长度有误({encryptedString})");
+        byte[] iv = new byte[16];
+        Array.Copy(encryptedBytes, 1, iv, 0, iv.Length);
+        using var sha256 = SHA256.Create();
+        using Aes aes = Aes.Create();
+        aes.Mode = CipherMode.CBC;
+        aes.Padding = PaddingMode.PKCS7;
+        using ICryptoTransform decryptor = aes.CreateDecryptor(sha256.ComputeHash(Encoding.UTF8.GetBytes(key)), iv);
+        using var ms = new MemoryStream(encryptedBytes, 1 + iv.Length, cipherSize);
+        using var cs = new CryptoStream(ms, decryptor, CryptoStreamMode.Read);
+        using var output = new MemoryStream();
+        cs.CopyTo(output);
+        return Encoding.UTF8.GetString(output.ToArray());
+    }
+
+    #endregion
+
+    #region ECDSA 非对称加密签名
 
     /// <summary>
-    /// 进行 ECDSA P-256 非对称加密签名验证。
+    /// 使用 ECDSA 算法,验证一个字符串的签名是否使用私钥生成。
     /// 如果验证失败则抛出 <see cref="CryptographicException"/>。
     /// </summary>
     public static void EcdsaVerify(string sourceString, string sign, string publicKey) {

+ 40 - 0
Tests/Utils/CryptographyUtilsTest.cs

@@ -0,0 +1,40 @@
+using System.Security.Cryptography;
+
+namespace MeloongCore.Tests;
+public class CryptographyUtilsTest : TestBase {
+
+    #region 对称加密
+
+    [Test]
+    [Arguments("")]
+    [Arguments("PCL")]
+    [Arguments("中文内容 Test 123 !@#$%^&*()")]
+    public async Task 对称加密_往返(string sourceString) {
+        string encryptedString = CryptographyUtils.AesEncrypt(sourceString, "PCL-Key-中文");
+        await Assert.That(encryptedString != sourceString).IsTrue();
+        await Assert.That(CryptographyUtils.AesDecrypt(encryptedString, "PCL-Key-中文")).IsEqualTo(sourceString);
+    }
+
+    [Test]
+    public async Task 对称加密_随机化()
+        => await Assert.That(CryptographyUtils.AesEncrypt("same source", "same key") != CryptographyUtils.AesEncrypt("same source", "same key")).IsTrue();
+
+    [Test]
+    public void 对称加密_篡改失败() {
+        byte[] encryptedBytes = Convert.FromBase64String(CryptographyUtils.AesEncrypt("source", "key"));
+        encryptedBytes[encryptedBytes.Length - 1] ^= 1;
+        Assert.Throws<CryptographicException>(() => CryptographyUtils.AesDecrypt(Convert.ToBase64String(encryptedBytes), "key"));
+    }
+
+    [Test]
+    public void 对称加密_错误密钥失败() {
+        Assert.Throws<CryptographicException>(() => CryptographyUtils.AesDecrypt(CryptographyUtils.AesEncrypt("source", "key"), "bad key"));
+    }
+
+    [Test]
+    public void 对称加密_格式错误失败()
+        => Assert.Throws<CryptographicException>(() => CryptographyUtils.AesDecrypt("not base64", "key"));
+
+    #endregion
+
+}

+ 1 - 1
Tests/Utils/DirectoryUtilsTest.cs

@@ -73,7 +73,7 @@ public class DirectoryUtilsTest : TestWithFolder {
 
     [Test]
     public void 剪切_安全检查() 
-        => Assert.Throws<UnauthorizedAccessException>(() => DirectoryUtils.Move(PathUtils.CurrentFolder, Path.Combine(tempFolder, "SafetyCheckDest")));
+        => Assert.Throws<UnauthorizedAccessException>(() => DirectoryUtils.Move(Paths.Base, Path.Combine(tempFolder, "SafetyCheckDest")));
 
     #endregion