From 51ddb2f7369c242d502d5525262e3fa8a91ca204 Mon Sep 17 00:00:00 2001
From: milimoe <110188673+milimoe@users.noreply.github.com>
Date: Sat, 21 Oct 2023 01:52:50 +0800
Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E5=96=842FA=E7=9B=B8=E5=85=B3?=
=?UTF-8?q?=E5=8A=9F=E8=83=BD=20=E7=AC=AC=E4=B8=80=E9=83=A8=E5=88=86=20(#5?=
=?UTF-8?q?7)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
Api/Utility/General.cs | 33 ++++-
Api/Utility/TFA.cs | 46 -------
Api/Utility/TwoFactorAuthenticator.cs | 185 ++++++++++++++++++++++++++
3 files changed, 217 insertions(+), 47 deletions(-)
delete mode 100644 Api/Utility/TFA.cs
create mode 100644 Api/Utility/TwoFactorAuthenticator.cs
diff --git a/Api/Utility/General.cs b/Api/Utility/General.cs
index 3541ec9..853a98e 100644
--- a/Api/Utility/General.cs
+++ b/Api/Utility/General.cs
@@ -1,6 +1,7 @@
using System.Collections;
using System.Net.NetworkInformation;
using System.Security.Cryptography;
+using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
using Milimoe.FunGame.Core.Library.Common.Architecture;
@@ -281,7 +282,7 @@ namespace Milimoe.FunGame.Core.Api.Utility
///
/// 使用HMACSHA512算法加密
///
- internal class Encryption
+ public class Encryption
{
///
/// 使用HMACSHA512算法加密
@@ -299,6 +300,36 @@ namespace Milimoe.FunGame.Core.Api.Utility
string Hmac = BitConverter.ToString(Hash).Replace("-", "");
return Hmac.ToLower();
}
+
+ ///
+ /// 使用RSA算法加密
+ ///
+ /// 明文
+ /// 公钥
+ ///
+ public static string RSAEncrypt(string PlainText, string PublicKey)
+ {
+ byte[] Plain = Encoding.UTF8.GetBytes(PlainText);
+ using RSACryptoServiceProvider RSA = new();
+ RSA.FromXmlString(PublicKey);
+ byte[] Encrypted = RSA.Encrypt(Plain, false);
+ return Convert.ToBase64String(Encrypted);
+ }
+
+ ///
+ /// 使用RSA算法解密
+ ///
+ /// 密文
+ /// 私钥
+ ///
+ public static string RSADecrypt(string SecretText, string PrivateKey)
+ {
+ byte[] Encrypted = Convert.FromBase64String(SecretText);
+ using RSACryptoServiceProvider RSA = new();
+ RSA.FromXmlString(PrivateKey);
+ byte[] Decrypted = RSA.Decrypt(Encrypted, false);
+ return Encoding.UTF8.GetString(Decrypted);
+ }
}
public static class StringExtension
diff --git a/Api/Utility/TFA.cs b/Api/Utility/TFA.cs
deleted file mode 100644
index c4ff64c..0000000
--- a/Api/Utility/TFA.cs
+++ /dev/null
@@ -1,46 +0,0 @@
-namespace Milimoe.FunGame.Core.Api.Utility
-{
- ///
- /// aka 2FA
- ///
- public class TFA
- {
- private readonly Dictionary TFACodes = new();
-
- public virtual bool IsAvailable(string username)
- {
- return true;
- }
-
- public string GetTFACode(string username)
- {
- string code = TFACodes.ContainsKey(username) ? TFACodes[username] : Verification.CreateVerifyCode(Library.Constant.VerifyCodeType.MixVerifyCode, 5);
- TaskUtility.RunTimer(() =>
- {
- // 十分钟后删除此码
- TFACodes.Remove(username, out _);
- }, 1000 * 10 * 60);
- return code;
- }
-
- public bool Authenticate(string username, string code, out string msg)
- {
- msg = "";
- if (!IsAvailable(username))
- {
- msg = "此账号不需要双重认证。";
- return false;
- }
- if (TFACodes.ContainsKey(username) && TFACodes.TryGetValue(username, out string? checkcode) && checkcode != null && checkcode == code)
- {
- TFACodes.Remove(username);
- return true;
- }
- else
- {
- msg = "验证码错误或已过期。";
- return false;
- }
- }
- }
-}
diff --git a/Api/Utility/TwoFactorAuthenticator.cs b/Api/Utility/TwoFactorAuthenticator.cs
new file mode 100644
index 0000000..b4a0755
--- /dev/null
+++ b/Api/Utility/TwoFactorAuthenticator.cs
@@ -0,0 +1,185 @@
+using System.Security.Cryptography;
+using System.Text;
+using Milimoe.FunGame.Core.Api.Transmittal;
+
+namespace Milimoe.FunGame.Core.Api.Utility
+{
+ ///
+ /// Aka. 2FA 双重认证 双因素身份验证
+ ///
+ public class TwoFactorAuthenticator
+ {
+ private readonly SQLHelper SQLHelper;
+
+ public TwoFactorAuthenticator(SQLHelper SQLHelper)
+ {
+ this.SQLHelper = SQLHelper;
+ }
+
+ ///
+ /// 检查账号是否需要2FA
+ ///
+ ///
+ ///
+ public virtual bool IsAvailable(string username)
+ {
+ return true;
+ }
+
+ ///
+ /// 2FA验证
+ ///
+ ///
+ ///
+ ///
+ ///
+ public bool Authenticate(string username, string code)
+ {
+ // TODO
+ // 使用username获取此账号记录在案的2FAKey,获取此时间戳内的验证码是否一致。
+ SQLHelper.Execute();
+ return true;
+ }
+
+ ///
+ /// 每30秒刷新
+ ///
+ private const int INTERVAL_SECONDS = 30;
+
+ ///
+ /// 6位数字2FA验证码
+ ///
+ private const int DIGITS = 6;
+
+ ///
+ /// 创键私钥,用于绑定账号,并生成两个文件,需要用户保存
+ ///
+ public static void CreateSecretKey()
+ {
+ string publicpath = "public.key"; // 公钥(密文)文件路径
+ string privatepath = "private.key"; // 私钥文件路径
+
+ // 创建RSA实例
+ using RSACryptoServiceProvider rsa = new();
+
+ // 获取公钥和私钥
+ string publickey = rsa.ToXmlString(false);
+ string privatekey = rsa.ToXmlString(true);
+
+ // 要加密的明文
+ string plain = Base32Encode(RandomNumberGenerator.GetBytes(10));
+
+ // 加密明文,获得密文
+ string secret = Encryption.RSAEncrypt(plain, publickey);
+
+ // 保存密文到文件
+ File.WriteAllText(publicpath, secret);
+
+ // 保存私钥到文件
+ File.WriteAllText(privatepath, privatekey);
+ }
+
+ ///
+ /// 生成随机秘钥字符串
+ ///
+ ///
+ ///
+ private static string Base32Encode(byte[] data)
+ {
+ const string alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
+ StringBuilder result = new((data.Length * 8 + 4) / 5);
+ int buffer = data[0];
+ int next = 1;
+ int bitsLeft = 8;
+ while (bitsLeft > 0 || next < data.Length)
+ {
+ if (bitsLeft < 5)
+ {
+ if (next < data.Length)
+ {
+ buffer <<= 8;
+ buffer |= data[next++] & 0xFF;
+ bitsLeft += 8;
+ }
+ else
+ {
+ int pad = 5 - bitsLeft;
+ buffer <<= pad;
+ bitsLeft += pad;
+ }
+ }
+ int index = 0x1F & (buffer >> (bitsLeft - 5));
+ bitsLeft -= 5;
+ result.Append(alphabet[index]);
+ }
+ return result.ToString();
+ }
+
+ ///
+ /// 生成基于当前时间戳的6位数字2FA验证码
+ ///
+ ///
+ ///
+ public static string GenerateCode(string secretKey)
+ {
+ byte[] key = Base32Decode(secretKey);
+ long counter = GetCurrentCounter();
+ byte[] counterBytes = BitConverter.GetBytes(counter);
+ if (BitConverter.IsLittleEndian)
+ {
+ Array.Reverse(counterBytes);
+ }
+ HMACSHA1 hmac = new(key);
+ byte[] hash = hmac.ComputeHash(counterBytes);
+ int offset = hash[^1] & 0x0F;
+ int code = ((hash[offset] & 0x7F) << 24 |
+ (hash[offset + 1] & 0xFF) << 16 |
+ (hash[offset + 2] & 0xFF) << 8 |
+ (hash[offset + 3] & 0xFF)) % (int)Math.Pow(10, DIGITS);
+ return code.ToString().PadLeft(DIGITS, '0');
+ }
+
+ ///
+ /// 获取当前时间节点
+ ///
+ ///
+ private static long GetCurrentCounter()
+ {
+ TimeSpan timeSpan = DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
+ return (long)(timeSpan.TotalSeconds / INTERVAL_SECONDS);
+ }
+
+ ///
+ /// 生成验证码
+ ///
+ ///
+ ///
+ ///
+ private static byte[] Base32Decode(string input)
+ {
+ const string alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
+ int length = input.Length;
+ int bitsLeft = 0;
+ int buffer = 0;
+ int next = 0;
+ byte[] result = new byte[length * 5 / 8];
+ foreach (char c in input)
+ {
+ int value = alphabet.IndexOf(c);
+ if (value < 0)
+ {
+ throw new ArgumentException("Invalid base32 character: " + c);
+ }
+ buffer <<= 5;
+ buffer |= value & 0x1F;
+ bitsLeft += 5;
+ if (bitsLeft >= 8)
+ {
+ result[next++] = (byte)(buffer >> (bitsLeft - 8));
+ bitsLeft -= 8;
+ }
+ }
+ return result;
+ }
+ }
+}