Files
Abyss/Abyss/Components/Services/UserService.cs
2025-09-09 12:11:30 +08:00

182 lines
6.4 KiB
C#

// UserService.cs
using System.Security.Cryptography;
using System.Text;
using Abyss.Model;
using Microsoft.Extensions.Caching.Memory;
using NSec.Cryptography;
using SQLite;
using Task = System.Threading.Tasks.Task;
namespace Abyss.Components.Services;
public class UserService
{
private readonly ILogger<UserService> _logger;
private readonly ConfigureService _config;
private readonly IMemoryCache _cache;
private readonly SQLiteAsyncConnection _database;
public UserService(ILogger<UserService> logger, ConfigureService config, IMemoryCache cache)
{
_logger = logger;
_config = config;
_cache = cache;
_database = new SQLiteAsyncConnection(config.UserDatabase, SQLiteOpenFlags.ReadWrite | SQLiteOpenFlags.Create);
_database.CreateTableAsync<User>().Wait();
var rootUser = _database.Table<User>().Where(x => x.Name == "root").FirstOrDefaultAsync().Result;
if (_config.DebugMode == "Debug")
_cache.Set("root", $"root@127.0.0.1", DateTimeOffset.Now.AddHours(1));
// Test token, can only be used locally. Will be destroyed in one hour.
if (rootUser == null)
{
var key = GenerateKeyPair();
string privateKeyBase64 = Convert.ToBase64String(key.Export(KeyBlobFormat.RawPrivateKey));
string publicKeyBase64 = Convert.ToBase64String(key.Export(KeyBlobFormat.RawPublicKey));
var s = GenerateRandomAsciiString(8);
Console.WriteLine($"Enter the following string to create a root user: '{s}'");
if (Console.ReadLine() != s)
{
throw (new Exception("Invalid Input"));
}
Console.WriteLine($"Created root user. Please keep the key safe.");
Console.WriteLine("key: '" + privateKeyBase64 + "'");
_database.InsertAsync(new User()
{
Name = "root",
Parent = "root",
PublicKey = publicKeyBase64,
Privilege = 1145141919,
}).Wait();
Console.ReadKey();
}
}
public async Task<string?> Challenge(string user)
{
var u = await _database.Table<User>().Where(x => x.Name == user).FirstOrDefaultAsync();
if (u == null) // Error: User not exists
return null;
if (_cache.TryGetValue(u.Name, out var challenge)) // The previous challenge has not yet expired
_cache.Remove(u.Name);
var c = Convert.ToBase64String(Encoding.UTF8.GetBytes(GenerateRandomAsciiString(32)));
_cache.Set(u.Name,c, DateTimeOffset.Now.AddMinutes(1));
return c;
}
// The challenge source and response source are not necessarily required to be the same,
// but the source that obtains the token must be the same as the source that uses the token in the future
public async Task<string?> Verify(string user, string response, string ip)
{
var u = await _database.Table<User>().Where(x => x.Name == user).FirstOrDefaultAsync();
if (u == null) // Error: User not exists
{
return null;
}
if (_cache.TryGetValue(u.Name, out string? challenge))
{
bool isVerified = VerifySignature(
PublicKey.Import(
SignatureAlgorithm.Ed25519,
Convert.FromBase64String(u.PublicKey),
KeyBlobFormat.RawPublicKey),
Convert.FromBase64String(challenge ?? ""),
Convert.FromBase64String(response));
if (!isVerified)
{
// Verification failed, set the challenge string to random to prevent duplicate verification
_cache.Set(u.Name, $"failed : {GenerateRandomAsciiString(32)}", DateTimeOffset.Now.AddMinutes(1));
return null;
}
else
{
// Remove the challenge string and create a session
_cache.Remove(u.Name);
var s = GenerateRandomAsciiString(64);
_cache.Set(s, $"{u.Name}@{ip}", DateTimeOffset.Now.AddDays(1));
_logger.LogInformation($"Verified {u.Name}@{ip}");
return s;
}
}
return null;
}
public string? Validate(string token, string ip)
{
if (_cache.TryGetValue(token, out string? userAndIp))
{
if (ip != userAndIp?.Split('@')[1])
{
_logger.LogError($"Token used from another Host: {token}");
Destroy(token);
return null;
}
// _logger.LogInformation($"Validated {userAndIp}");
return userAndIp?.Split('@')[0];
}
_logger.LogWarning($"Validation failed {token}");
return null;
}
public void Destroy(string token)
{
_cache.Remove(token);
}
public async Task<User?> QueryUser(string user)
{
var u = await _database.Table<User>().Where(x => x.Name == user).FirstOrDefaultAsync();
return u;
}
public async Task CreateUser(User user)
{
await _database.InsertAsync(user);
_logger.LogInformation($"Created user: {user.Name}, Parent: {user.Parent}, Privilege: {user.Privilege}");
}
static Key GenerateKeyPair()
{
var algorithm = SignatureAlgorithm.Ed25519;
var creationParameters = new KeyCreationParameters
{
ExportPolicy = KeyExportPolicies.AllowPlaintextExport
};
return Key.Create(algorithm, creationParameters);
}
public static string GenerateRandomAsciiString(int length)
{
const string asciiChars = "-0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
using (var rng = RandomNumberGenerator.Create())
{
byte[] randomBytes = new byte[length];
rng.GetBytes(randomBytes);
char[] result = new char[length];
for (int i = 0; i < length; i++)
{
result[i] = asciiChars[randomBytes[i] % asciiChars.Length];
}
return new string(result);
}
}
static bool VerifySignature(PublicKey publicKey, byte[] data, byte[] signature)
{
var algorithm = SignatureAlgorithm.Ed25519;
return algorithm.Verify(publicKey, data, signature);
}
}