完善2FA相关功能 第一部分 (#57)

This commit is contained in:
milimoe 2023-10-21 01:52:50 +08:00 committed by GitHub
parent 94bad21e8d
commit 51ddb2f736
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 217 additions and 47 deletions

View File

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

View File

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

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