From 2b81f3e5ba24e58a4a4cdce589501a89c54df6ee Mon Sep 17 00:00:00 2001 From: Eveldee Date: Fri, 4 Jun 2021 18:32:47 +0200 Subject: [PATCH] Add AuthLifetimeService Refactor AuthManager --- Akari.Prototype.Server/Models/TimedEntry.cs | 9 ++ Akari.Prototype.Server/Program.cs | 1 + Akari.Prototype.Server/Services/AkariPath.cs | 22 +++++ .../Services/AuthLifetimeService.cs | 86 +++++++++++++++++++ .../Services/AuthManager.cs | 51 ++++++++++- .../Services/FingerprintManager.cs | 53 ++++++++---- .../Services/IAuthManager.cs | 2 +- .../Services/IFingerprintManager.cs | 3 +- .../Services/TcpProviderService.cs | 58 +++++++++---- Akari.Prototype.Server/Startup.cs | 6 ++ Akari.Prototype.Server/Utils/AkariPath.cs | 15 ---- Akari.Prototype.Server/Utils/Security.cs | 31 +++++++ Akari.Prototype.Server/akari.json | 2 +- 13 files changed, 281 insertions(+), 58 deletions(-) create mode 100644 Akari.Prototype.Server/Models/TimedEntry.cs create mode 100644 Akari.Prototype.Server/Services/AkariPath.cs create mode 100644 Akari.Prototype.Server/Services/AuthLifetimeService.cs delete mode 100644 Akari.Prototype.Server/Utils/AkariPath.cs diff --git a/Akari.Prototype.Server/Models/TimedEntry.cs b/Akari.Prototype.Server/Models/TimedEntry.cs new file mode 100644 index 0000000..af2dd7f --- /dev/null +++ b/Akari.Prototype.Server/Models/TimedEntry.cs @@ -0,0 +1,9 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Akari.Prototype.Server.Models +{ + public record TimedEntry(DateTime CreationDate, T Value); +} diff --git a/Akari.Prototype.Server/Program.cs b/Akari.Prototype.Server/Program.cs index 0152a79..b9867dd 100644 --- a/Akari.Prototype.Server/Program.cs +++ b/Akari.Prototype.Server/Program.cs @@ -37,6 +37,7 @@ namespace Akari.Prototype.Server .ConfigureHostConfiguration(configuration => { configuration.AddJsonFile(TcpProviderOptions.FilePath, true); + configuration.AddJsonFile(AkariOptions.FilePath, false); }) .ConfigureWebHostDefaults(webBuilder => { diff --git a/Akari.Prototype.Server/Services/AkariPath.cs b/Akari.Prototype.Server/Services/AkariPath.cs new file mode 100644 index 0000000..e40f319 --- /dev/null +++ b/Akari.Prototype.Server/Services/AkariPath.cs @@ -0,0 +1,22 @@ +using Akari.Prototype.Server.Options; +using Microsoft.Extensions.Options; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; + +namespace Akari.Prototype.Server.Services +{ + public class AkariPath + { + private readonly AkariOptions _options; + + public AkariPath(IOptions options) + { + _options = options.Value; + } + + public string GetPath(string path) => Path.Combine(_options.DataPath, path); + } +} diff --git a/Akari.Prototype.Server/Services/AuthLifetimeService.cs b/Akari.Prototype.Server/Services/AuthLifetimeService.cs new file mode 100644 index 0000000..e43c252 --- /dev/null +++ b/Akari.Prototype.Server/Services/AuthLifetimeService.cs @@ -0,0 +1,86 @@ +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace Akari.Prototype.Server.Services +{ + public class AuthLifetimeService : IHostedService + { + public static TimeSpan CleanupInterval => TimeSpan.FromSeconds(5); + public static TimeSpan AuthLifetime => TimeSpan.FromSeconds(30); + + private readonly ILogger _logger; + private readonly IAuthManager _authManager; + private CancellationTokenSource _cleanupCancellationTokenSource; + private Timer _timer; + + public AuthLifetimeService(ILogger logger, IAuthManager authManager) + { + _logger = logger; + _authManager = authManager; + } + + public Task StartAsync(CancellationToken cancellationToken) + { + _cleanupCancellationTokenSource = new CancellationTokenSource(); + + _timer = new Timer(AuthCleanup, _cleanupCancellationTokenSource.Token, CleanupInterval, CleanupInterval); + + _logger.LogDebug("Cleanup timer is running"); + + return Task.CompletedTask; + } + + public Task StopAsync(CancellationToken cancellationToken) + { + _logger.LogDebug("Stopping"); + + _timer?.Change(Timeout.Infinite, Timeout.Infinite); + + _cleanupCancellationTokenSource?.Cancel(); + + return Task.CompletedTask; + } + + private void AuthCleanup(object state) + { + var cancellationToken = (CancellationToken)state; + var date = DateTime.Now; + + if (cancellationToken.IsCancellationRequested) + { + _logger.LogDebug("Cancellation requested, aborted cleanup"); + + return; + } + + _logger.LogDebug($"Running cleanup at: {date}"); + + foreach (var (key, value) in _authManager.Pairs) + { + if (cancellationToken.IsCancellationRequested) + { + _logger.LogDebug("Cancellation requested, aborted cleanup"); + + return; + } + + if (date - value.CreationDate >= AuthLifetime) + { + _logger.LogDebug($"'{key}' auth expired"); + + _authManager.Remove(key); + } + } + } + + public void Dispose() + { + _timer?.Dispose(); + } + } +} diff --git a/Akari.Prototype.Server/Services/AuthManager.cs b/Akari.Prototype.Server/Services/AuthManager.cs index aec60b1..532fcc5 100644 --- a/Akari.Prototype.Server/Services/AuthManager.cs +++ b/Akari.Prototype.Server/Services/AuthManager.cs @@ -1,25 +1,70 @@ -using Microsoft.Extensions.Logging; +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.Linq; +using System.Runtime.InteropServices; +using System.Security.Cryptography; +using System.Text; +using System.Threading; using System.Threading.Tasks; namespace Akari.Prototype.Server.Services { - public class AuthManager : IAuthManager + public sealed class AuthManager : IAuthManager, IDisposable { + public const int AuthKeysLength = 256 / 8; + + public IEnumerable>> Pairs => _keys; + private readonly ILogger _logger; private readonly IKeyManager _keyManager; + private IDictionary> _keys; + public AuthManager(ILogger logger, IKeyManager keyManager) { _logger = logger; _keyManager = keyManager; + _keys = new ConcurrentDictionary>(); } - public void Auth(byte[] token, string name) + public void Auth(string name, string token) { + // Derive key and store it + using var key = Security.Argon2idDeriveBytes(token, name, AuthKeysLength, clear: true); + SetKey(name, new AesGcm(key.Buffer)); + } + + public bool Remove(string name) + { + return _keys.Remove(name); + } + + 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() + { + _keys.Clear(); } } } diff --git a/Akari.Prototype.Server/Services/FingerprintManager.cs b/Akari.Prototype.Server/Services/FingerprintManager.cs index 77dfd3d..c63ad24 100644 --- a/Akari.Prototype.Server/Services/FingerprintManager.cs +++ b/Akari.Prototype.Server/Services/FingerprintManager.cs @@ -1,12 +1,15 @@ using Akari.Prototype.Server.Options; using Akari.Prototype.Server.Utils; +using Isopoh.Cryptography.Argon2; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Runtime.InteropServices; using System.Text.Json; +using System.Threading; using System.Threading.Tasks; namespace Akari.Prototype.Server.Services @@ -16,56 +19,70 @@ namespace Akari.Prototype.Server.Services public const string FingerprintsPath = "fingerprints.json"; private readonly ILogger _logger; - private readonly IOptions _akariOptions; private readonly IAuthManager _authManager; + private readonly AkariPath _akariPath; - /// - /// Map token hash to user-friendly name - /// - private IDictionary _fingerprintNames; + private IDictionary _tokensHash; - public FingerprintManager(ILogger logger, IOptions akariOptions, - IAuthManager authManager) + public FingerprintManager(ILogger logger, IAuthManager authManager, AkariPath akariPath) { _logger = logger; - _akariOptions = akariOptions; _authManager = authManager; + _akariPath = akariPath; LoadFingerprints(); } private void LoadFingerprints() { - var path = AkariPath.GetPath(FingerprintsPath); + var path = _akariPath.GetPath(FingerprintsPath); // Create new if (!File.Exists(path)) { - _fingerprintNames = new Dictionary(); + _tokensHash = new Dictionary(); - File.WriteAllText(path, JsonSerializer.Serialize(_fingerprintNames)); + File.WriteAllText(path, JsonSerializer.Serialize(_tokensHash)); } // Load else { - _fingerprintNames = JsonSerializer.Deserialize>(File.ReadAllText(path)); + _tokensHash = JsonSerializer.Deserialize>(File.ReadAllText(path)); } } - public async Task VerifyToken(byte[] token) + public void VerifyFingerprint(string name, string token) { - var hash = Security.Argon2idHash(token); + _logger.LogDebug($"Verifying hash for {name}"); - _logger.LogDebug($"Verifying hash: {hash}"); + var handle = GCHandle.Alloc(token, GCHandleType.Pinned); - if (!_fingerprintNames.TryGetValue(hash, out var name)) + if (!_tokensHash.TryGetValue(name, out var hash)) { - _logger.LogDebug($"Unknown hash, aborting: {hash}"); + _logger.LogDebug($"No fingerprint exist with the name: {name}"); + + handle.Free(); return; } - _authManager.Auth(token, name); + if (!Argon2.Verify(hash, token)) + { + _logger.LogDebug($"Token doesn't match stored hash: {name}"); + + handle.Free(); + + return; + } + + try + { + _authManager.Auth(name, token); + } + finally + { + handle.Free(); + } } } } diff --git a/Akari.Prototype.Server/Services/IAuthManager.cs b/Akari.Prototype.Server/Services/IAuthManager.cs index 4aa31b9..aa1ab0b 100644 --- a/Akari.Prototype.Server/Services/IAuthManager.cs +++ b/Akari.Prototype.Server/Services/IAuthManager.cs @@ -7,6 +7,6 @@ namespace Akari.Prototype.Server.Services { public interface IAuthManager { - void Auth(byte[] token, string name); + void Auth(string name, string token); } } diff --git a/Akari.Prototype.Server/Services/IFingerprintManager.cs b/Akari.Prototype.Server/Services/IFingerprintManager.cs index 4c06fdd..e5401fa 100644 --- a/Akari.Prototype.Server/Services/IFingerprintManager.cs +++ b/Akari.Prototype.Server/Services/IFingerprintManager.cs @@ -1,12 +1,13 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; namespace Akari.Prototype.Server.Services { public interface IFingerprintManager { - Task VerifyToken(byte[] vs); + void VerifyFingerprint(string name, string token); } } diff --git a/Akari.Prototype.Server/Services/TcpProviderService.cs b/Akari.Prototype.Server/Services/TcpProviderService.cs index d5445ad..dfef2bf 100644 --- a/Akari.Prototype.Server/Services/TcpProviderService.cs +++ b/Akari.Prototype.Server/Services/TcpProviderService.cs @@ -9,22 +9,25 @@ using System.IO; using System.Linq; using System.Net; using System.Net.Sockets; +using System.Text; using System.Threading; using System.Threading.Tasks; namespace Akari.Prototype.Server.Services { - public class TcpProviderService : BackgroundService + // TODO Process multiple clients simultaneously + public sealed class TcpProviderService : BackgroundService { public const int BufferLength = 4096; + public const int MaxDataLength = MaxTokenLength + 1 + MaxNameLength; public const int MaxTokenLength = 24; - public const int ReceiveTimeout = 500_000; + public const int MaxNameLength = 200; + public const int ReceiveTimeout = 5_000; private readonly ILogger _logger; private readonly TcpProviderOptions _options; private readonly IHostApplicationLifetime _hostApplicationLifetime; private readonly IFingerprintManager _fingerprintManager; - private Task _backgroundTask; private TcpListener _listener; public TcpProviderService(ILogger logger, IOptions options, @@ -36,6 +39,16 @@ namespace Akari.Prototype.Server.Services _fingerprintManager = fingerprintManager; } + public override Task StartAsync(CancellationToken cancellationToken) + { + _listener = new TcpListener(IPAddress.Parse(_options.ListeningAddress), _options.Port); + _listener.Start(); + + _logger.LogInformation($"Now listening on: {_listener.LocalEndpoint}"); + + return Task.CompletedTask; + } + protected override async Task ExecuteAsync(CancellationToken stoppingToken) { await TcpLoop(stoppingToken); @@ -50,11 +63,6 @@ namespace Akari.Prototype.Server.Services try { - _listener = new TcpListener(IPAddress.Parse(_options.ListeningAddress), _options.Port); - _listener.Start(); - - _logger.LogInformation($"Now listening on: {_listener.LocalEndpoint}"); - while (!cancellationToken.IsCancellationRequested) { _logger.LogDebug("Waiting for client..."); @@ -88,17 +96,14 @@ namespace Akari.Prototype.Server.Services Environment.Exit(1); } - finally - { - _listener?.Stop(); - } } private async Task ReceiveAuth(TcpClient client, CancellationToken cancellationToken) { using var stream = client.GetStream(); - var data = new Memory(new byte[MaxTokenLength]); - var buffer = new Memory(new byte[BufferLength]); + + Memory data = new byte[MaxDataLength]; + Memory buffer = new byte[BufferLength]; // Receive token int position = 0; @@ -106,12 +111,12 @@ namespace Akari.Prototype.Server.Services var timeoutToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, new CancellationTokenSource(ReceiveTimeout).Token).Token; - _logger.LogDebug("Waiting for token..."); - while (position < MaxTokenLength && (read = await stream.ReadAsync(buffer, timeoutToken)) != 0) + _logger.LogDebug("Waiting for data..."); + while (position < MaxDataLength && (read = await stream.ReadAsync(buffer, timeoutToken)) != 0) { if (position + read > data.Length) { - // Invalid token + // Invalid data return; } @@ -120,9 +125,24 @@ namespace Akari.Prototype.Server.Services position += read; } - _logger.LogDebug($"Received token: {Convert.ToBase64String(data[..position].Span)}"); + var text = Encoding.UTF8.GetString(data.Span); + var splitIndex = text.IndexOf('$'); - await _fingerprintManager.VerifyToken(data[..position].ToArray()); + _logger.LogDebug($"Received text: {text}"); + + if (cancellationToken.IsCancellationRequested) + { + return; + } + + _fingerprintManager.VerifyFingerprint(text[..splitIndex], text[(splitIndex + 1)..]); + } + + public override void Dispose() + { + _listener?.Stop(); + + base.Dispose(); } } } diff --git a/Akari.Prototype.Server/Startup.cs b/Akari.Prototype.Server/Startup.cs index 80693d6..f204021 100644 --- a/Akari.Prototype.Server/Startup.cs +++ b/Akari.Prototype.Server/Startup.cs @@ -32,11 +32,17 @@ namespace Akari.Prototype.Server .Bind(Configuration.GetSection(TcpProviderOptions.SectionPath)) .ValidateDataAnnotations(); + services.AddOptions() + .Bind(Configuration.GetSection(AkariOptions.SectionPath)) + .ValidateDataAnnotations(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); services.AddHostedService(); + services.AddHostedService(); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. diff --git a/Akari.Prototype.Server/Utils/AkariPath.cs b/Akari.Prototype.Server/Utils/AkariPath.cs deleted file mode 100644 index 424d597..0000000 --- a/Akari.Prototype.Server/Utils/AkariPath.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading.Tasks; - -namespace Akari.Prototype.Server.Utils -{ - public static class AkariPath - { - public const string BasePath = "Data"; - - public static string GetPath(string path) => Path.Combine(BasePath, path); - } -} diff --git a/Akari.Prototype.Server/Utils/Security.cs b/Akari.Prototype.Server/Utils/Security.cs index 46f68a2..581b6ce 100644 --- a/Akari.Prototype.Server/Utils/Security.cs +++ b/Akari.Prototype.Server/Utils/Security.cs @@ -1,8 +1,10 @@ using Isopoh.Cryptography.Argon2; +using Isopoh.Cryptography.SecureArray; using System; using System.Collections.Generic; using System.Linq; using System.Security.Cryptography; +using System.Text; using System.Threading.Tasks; namespace Akari.Prototype.Server.Utils @@ -31,5 +33,34 @@ namespace Akari.Prototype.Server.Utils return Argon2.Hash(config); } + + public static SecureArray Argon2idDeriveBytes(byte[] password, byte[] salt, int length, bool clear = false, int? threads = null) + { + int t = threads ?? Environment.ProcessorCount / 2; + + if (t < 1) + { + t = 1; + } + + var config = new Argon2Config() + { + HashLength = length, + Password = password, + Salt = salt, + Lanes = t, + Threads = t, + ClearPassword = clear, + ClearSecret = clear, + Type = Argon2Type.HybridAddressing, + Version = Argon2Version.Nineteen + }; + + return new Argon2(config).Hash(); + } + public static SecureArray Argon2idDeriveBytes(string password, string salt, int length, bool clear = false, int? threads = null) + { + return Argon2idDeriveBytes(Encoding.UTF8.GetBytes(password), Encoding.UTF8.GetBytes(salt), length, clear, threads); + } } } diff --git a/Akari.Prototype.Server/akari.json b/Akari.Prototype.Server/akari.json index 6dadfaa..5ddcb9d 100644 --- a/Akari.Prototype.Server/akari.json +++ b/Akari.Prototype.Server/akari.json @@ -1,6 +1,6 @@ { "Akari": { - "DataPath": "Data" + "DataPath": "Data" } }