using System.Security.Cryptography; using System.Text; using Milimoe.FunGame.Core.Api.Transmittal; using Milimoe.FunGame.Core.Library.Constant; namespace Milimoe.FunGame.Core.Api.Utility { /// /// Aka. 2FA 双重认证 双因素身份验证 /// public class TwoFactorAuthenticator { /// /// SQLHelper 允许为空 /// private readonly SQLHelper? SQLHelper; /// /// 不使用SQL模式 /// public TwoFactorAuthenticator() { } /// /// 使用SQL模式 记录对应账号的密文到数据库中 /// /// 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 KEY ----- /// private const string PUBLICKEY = "----- PUBLIC KEY -----\r\n"; /// /// ----- SECRET SIGN ----- /// private const string SECRETSIGN = "----- SECRET SIGN -----\r\n"; /// /// 创键私钥,用于绑定账号,并生成两个文件,需要用户保存 /// public void CreateSecretKey(string username) { // 秘钥文件路径 string keypath = "authenticator.key"; // 创建RSA实例 using RSACryptoServiceProvider rsa = new(); // 获取公钥和私钥 string publickey = rsa.ToXmlString(false); string privatekey = rsa.ToXmlString(true); // 要加密的明文 byte[] random = RandomNumberGenerator.GetBytes(10); string randomstring = General.DefaultEncoding.GetString(random); // TODO 记录对应账号的密文 SQLHelper?.Execute(); string plain = Base32Encode(random); // 加密明文,获得密文 string secret = Encryption.RSAEncrypt(plain, publickey); // 保存密文到文件 File.WriteAllText(keypath, PUBLICKEY + secret + "\r\n"); // 保存私钥到文件 File.AppendAllText(keypath, SECRETSIGN + 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; } /// /// 拆分字符串中的密文和私钥 /// /// /// /// public static bool SplitKeyFile(string content, out string[] strs) { strs = content.Split(SECRETSIGN); if (strs.Length == 2) { return true; } else { return false; } } } }