using Akari.Prototype.Server.Models; using Akari.Prototype.Server.Utils; using Isopoh.Cryptography.Argon2; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Linq; using System.Runtime.InteropServices; using System.Security.Cryptography; using System.Text; using System.Text.Json; using System.Threading; using System.Threading.Tasks; namespace Akari.Prototype.Server.Services { public sealed class AuthManager : IAuthManager, IDisposable { public const int AuthKeysLength = 256 / 8; public const string SaltsPath = "auth_salts.json"; public IEnumerable>> Pairs => _keys; private readonly ILogger _logger; private readonly AkariPath _akariPath; private readonly IDictionary> _keys; private IDictionary _salts; public AuthManager(ILogger logger, AkariPath akariPath) { _logger = logger; _akariPath = akariPath; _keys = new ConcurrentDictionary>(); LoadSalts(); } private void LoadSalts() { var path = _akariPath.GetPath(SaltsPath); // Create new if (!File.Exists(path)) { _salts = new Dictionary(); File.WriteAllText(path, JsonSerializer.Serialize(_salts)); } // Load else { _salts = JsonSerializer.Deserialize>(File.ReadAllText(path)); } } private void SaveSalts() { var path = _akariPath.GetPath(SaltsPath); File.WriteAllText(path, JsonSerializer.Serialize(_salts)); } public void GenerateSalt(string name) { Span salt = Security.DefaultSaltLength <= 1024 ? stackalloc byte[Security.DefaultSaltLength] : new byte[Security.DefaultSaltLength]; RandomNumberGenerator.Fill(salt); _salts[name] = Convert.ToBase64String(salt); SaveSalts(); } public void Auth(string name, byte[] token) { // Retrieve salt if (!_salts.TryGetValue(name, out var salt)) { throw new Exception($"Can't find the salt for '{name}', try to register the fingerprint again"); } // Derive key and store it using var key = Security.Argon2idDeriveBytes(token, Convert.FromBase64String(salt), AuthKeysLength, clear: true); SetKey(name, new AesGcm(key.Buffer)); } public bool TryGetKey(string name, out AesGcm key) { if (_keys.TryGetValue(name, out var entry)) { key = entry.Value; return true; } key = null; return false; } public bool TryGetKey(IEnumerable names, out string name, out AesGcm key) { foreach (var n in names) { if (TryGetKey(n, out var k)) { name = n; key = k; return true; } } name = null; key = null; return false; } public bool Remove(string name) { if (_keys.TryGetValue(name, out var entry)) { entry.Value.Dispose(); return _keys.Remove(name); } return false; } private void SetKey(string name, AesGcm aesGcm) { _logger.LogDebug($"New fingerprint auth: {name}"); if (_keys.TryGetValue(name, out var oldEntry)) { _logger.LogDebug($"Old auth were present for '{name}', clearing it"); oldEntry.Value.Dispose(); } _keys[name] = new TimedEntry(DateTime.Now, aesGcm); _logger.LogDebug($"New auth '{name}' at [{_keys[name].CreationDate}], expires at [{_keys[name].CreationDate + AuthLifetimeService.AuthLifetime}]"); } public void Dispose() { foreach (var entry in _keys.Values) { entry.Value.Dispose(); } _keys.Clear(); } } }