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; + } + } +}