using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Security.Cryptography; using System.Text; using System.Text.Json; using System.Threading.Tasks; using Akari.Prototype.Server.Utils; using Isopoh.Cryptography.Argon2; using Microsoft.Extensions.Logging; namespace Akari.Prototype.Server.Services { public class MasterKeyService : IMasterKeyService, IDisposable { private record Config(string Hash, string KeySalt); public const string ConfigPath = "master"; public const int MasterKeyLength = 256 / 8; public bool IsLoggedIn => _key is not null; private readonly ILogger _logger; private readonly AkariPath _akariPath; private Config _config; private AesGcm _key; public MasterKeyService(ILogger logger, AkariPath akariPath) { _logger = logger; _akariPath = akariPath; } public bool CheckConfig() => File.Exists(_akariPath.GetPath(ConfigPath)); public bool Login(string password) { if (_key is not null) { return true; } if (_config is null && !LoadConfig()) { _logger.LogDebug("Can't load config, has a master password been set?"); return false; } if (!Argon2.Verify(_config.Hash, password)) { _logger.LogDebug("Wrong password"); return false; } using var keyBytes = Security.Argon2idDeriveBytes(Encoding.UTF8.GetBytes(password), Convert.FromBase64String(_config.KeySalt), MasterKeyLength, clear: true); _key = new AesGcm(keyBytes.Buffer); return true; } public bool TryGetKey(out AesGcm key) { if (!IsLoggedIn) { _logger.LogDebug("Can't get key, not logged in"); key = null; return false; } key = _key; return true; } public bool CreatePassword(string password) { if (CheckConfig()) { _logger.LogDebug("Tried to create password but it's already set."); return false; } var hash = Security.NewArgon2idHash(password); Span salt = Security.DefaultSaltLength <= 1024 ? stackalloc byte[Security.DefaultSaltLength] : new byte[Security.DefaultSaltLength]; RandomNumberGenerator.Fill(salt); _config = new Config(hash, Convert.ToBase64String(salt)); SaveConfig(); return Login(password); } private bool LoadConfig() { if (!CheckConfig()) { return false; } string path = _akariPath.GetPath(ConfigPath); _config = JsonSerializer.Deserialize(File.ReadAllText(path)); _logger.LogDebug("Config loaded"); return true; } private void SaveConfig() { string path = _akariPath.GetPath(ConfigPath); File.WriteAllText(path, JsonSerializer.Serialize(_config)); } public void Dispose() { _key?.Dispose(); } } }