diff --git a/Akari.Prototype.Server/Services/IMasterKeyService.cs b/Akari.Prototype.Server/Services/IMasterKeyService.cs new file mode 100644 index 0000000..e80d255 --- /dev/null +++ b/Akari.Prototype.Server/Services/IMasterKeyService.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Cryptography; +using System.Threading.Tasks; + +namespace Akari.Prototype.Server.Services +{ + public interface IMasterKeyService + { + public bool IsLoggedIn { get; } + + public bool CheckConfig(); + + public bool Login(string password); + + public bool TryGetKey(out AesGcm key); + + public bool CreatePassword(string password); + } +} diff --git a/Akari.Prototype.Server/Services/MasterKeyService.cs b/Akari.Prototype.Server/Services/MasterKeyService.cs new file mode 100644 index 0000000..1761af6 --- /dev/null +++ b/Akari.Prototype.Server/Services/MasterKeyService.cs @@ -0,0 +1,132 @@ +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(); + } + } +}