mirror of
https://github.com/project-redbud/FunGame-Core.git
synced 2025-04-22 03:59:35 +08:00
完善2FA相关功能 第一部分 (#57)
This commit is contained in:
parent
94bad21e8d
commit
51ddb2f736
@ -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
|
||||
/// <summary>
|
||||
/// 使用HMACSHA512算法加密
|
||||
/// </summary>
|
||||
internal class Encryption
|
||||
public class Encryption
|
||||
{
|
||||
/// <summary>
|
||||
/// 使用HMACSHA512算法加密
|
||||
@ -299,6 +300,36 @@ namespace Milimoe.FunGame.Core.Api.Utility
|
||||
string Hmac = BitConverter.ToString(Hash).Replace("-", "");
|
||||
return Hmac.ToLower();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 使用RSA算法加密
|
||||
/// </summary>
|
||||
/// <param name="PlainText">明文</param>
|
||||
/// <param name="PublicKey">公钥</param>
|
||||
/// <returns></returns>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 使用RSA算法解密
|
||||
/// </summary>
|
||||
/// <param name="SecretText">密文</param>
|
||||
/// <param name="PrivateKey">私钥</param>
|
||||
/// <returns></returns>
|
||||
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
|
||||
|
@ -1,46 +0,0 @@
|
||||
namespace Milimoe.FunGame.Core.Api.Utility
|
||||
{
|
||||
/// <summary>
|
||||
/// aka 2FA
|
||||
/// </summary>
|
||||
public class TFA
|
||||
{
|
||||
private readonly Dictionary<string, string> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
185
Api/Utility/TwoFactorAuthenticator.cs
Normal file
185
Api/Utility/TwoFactorAuthenticator.cs
Normal file
@ -0,0 +1,185 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using Milimoe.FunGame.Core.Api.Transmittal;
|
||||
|
||||
namespace Milimoe.FunGame.Core.Api.Utility
|
||||
{
|
||||
/// <summary>
|
||||
/// Aka. 2FA 双重认证 双因素身份验证
|
||||
/// </summary>
|
||||
public class TwoFactorAuthenticator
|
||||
{
|
||||
private readonly SQLHelper SQLHelper;
|
||||
|
||||
public TwoFactorAuthenticator(SQLHelper SQLHelper)
|
||||
{
|
||||
this.SQLHelper = SQLHelper;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查账号是否需要2FA
|
||||
/// </summary>
|
||||
/// <param name="username"></param>
|
||||
/// <returns></returns>
|
||||
public virtual bool IsAvailable(string username)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 2FA验证
|
||||
/// </summary>
|
||||
/// <param name="username"></param>
|
||||
/// <param name="code"></param>
|
||||
/// <param name="msg"></param>
|
||||
/// <returns></returns>
|
||||
public bool Authenticate(string username, string code)
|
||||
{
|
||||
// TODO
|
||||
// 使用username获取此账号记录在案的2FAKey,获取此时间戳内的验证码是否一致。
|
||||
SQLHelper.Execute();
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 每30秒刷新
|
||||
/// </summary>
|
||||
private const int INTERVAL_SECONDS = 30;
|
||||
|
||||
/// <summary>
|
||||
/// 6位数字2FA验证码
|
||||
/// </summary>
|
||||
private const int DIGITS = 6;
|
||||
|
||||
/// <summary>
|
||||
/// 创键私钥,用于绑定账号,并生成两个文件,需要用户保存
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 生成随机秘钥字符串
|
||||
/// </summary>
|
||||
/// <param name="data"></param>
|
||||
/// <returns></returns>
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 生成基于当前时间戳的6位数字2FA验证码
|
||||
/// </summary>
|
||||
/// <param name="secretKey"></param>
|
||||
/// <returns></returns>
|
||||
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');
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前时间节点
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
private static long GetCurrentCounter()
|
||||
{
|
||||
TimeSpan timeSpan = DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||
return (long)(timeSpan.TotalSeconds / INTERVAL_SECONDS);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 生成验证码
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="ArgumentException"></exception>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user