From f539455e10ac9be744e7f4f0bf63d11350940f28 Mon Sep 17 00:00:00 2001 From: Eveldee Date: Mon, 7 Jun 2021 01:44:47 +0200 Subject: [PATCH] Implement IKeyManager --- .../Services/ApplicationsManager.cs | 152 ++++++++++++------ .../Services/AuthManager.cs | 49 +++++- .../Services/IApplicationsManager.cs | 12 +- .../Services/IAuthManager.cs | 1 + .../Services/IKeyManager.cs | 8 + Akari.Prototype.Server/Services/KeyManager.cs | 108 ++++++++++++- Akari.Prototype.Server/Utils/Security.cs | 56 +++++++ 7 files changed, 331 insertions(+), 55 deletions(-) diff --git a/Akari.Prototype.Server/Services/ApplicationsManager.cs b/Akari.Prototype.Server/Services/ApplicationsManager.cs index 411bfc0..4c44c18 100644 --- a/Akari.Prototype.Server/Services/ApplicationsManager.cs +++ b/Akari.Prototype.Server/Services/ApplicationsManager.cs @@ -3,6 +3,7 @@ using Akari.Prototype.Server.Utils; using Isopoh.Cryptography.Argon2; using Microsoft.Extensions.Logging; using System; +using System.Collections; using System.Collections.Generic; using System.IO; using System.Linq; @@ -12,7 +13,7 @@ using System.Threading.Tasks; namespace Akari.Prototype.Server.Services { - public class ApplicationsManager : IApplicationsManager + public sealed class ApplicationsManager : IApplicationsManager { public const string ApplicationsPath = "applications.json"; public const int ApplicationTokenLength = 18; @@ -20,15 +21,17 @@ namespace Akari.Prototype.Server.Services private readonly ILogger _logger; private readonly IKeyManager _keyManager; private readonly IAuthManager _authManager; + private readonly IMasterKeyService _masterKeyService; private readonly AkariPath _akariPath; private IDictionary _applications; - public ApplicationsManager(ILogger logger, IKeyManager keyManager, IAuthManager authManager, AkariPath akariPath) + public ApplicationsManager(ILogger logger, IKeyManager keyManager, IAuthManager authManager, IMasterKeyService masterKeyService, AkariPath akariPath) { _logger = logger; _keyManager = keyManager; _authManager = authManager; + _masterKeyService = masterKeyService; _akariPath = akariPath; LoadApplications(); @@ -57,56 +60,27 @@ namespace Akari.Prototype.Server.Services File.WriteAllText(path, JsonSerializer.Serialize(_applications.Values)); } - public bool AddFingerprint(string applicationName, string applicationToken, string fingerprintName, string masterPassword) - { - if (!VerifyToken(applicationName, applicationToken)) - { - _logger.LogDebug($"Wrong token provided for {applicationName}"); - - return false; - } - - // Verify master password - - // Try get fingerprint key - if (!_authManager.TryGetKey(fingerprintName, out var key)) - { - _logger.LogDebug($"Fingerprint '{fingerprintName}' has not been auth"); - - return false; - } - - // Encrypt key - //_keyManager.AddFingerprint(applicationName, fingerprintName, key, masterPassword); - - _applications[applicationName].Fingerprints.Add(fingerprintName); - - SaveApplications(); - - return true; - } - - private bool VerifyToken(string applicationName, string token) - { - if (!_applications.TryGetValue(applicationName, out var application)) - { - return false; - } - - return Argon2.Verify(application.TokenHash, Convert.FromBase64String(token)); - } - - public bool TryCreate(string applicationName, out string token) + public bool TryCreate(string applicationName, out string applicationToken) { if (_applications.ContainsKey(applicationName)) { _logger.LogDebug($"Can't create '{applicationName}' as it already exists"); - token = null; + applicationToken = null; return false; } + if (!_masterKeyService.IsLoggedIn) + { + _logger.LogDebug("Can't create an application if master service is not logged in"); + + applicationToken = null; + + return false; + } + + // Application token var tokenData = new byte[ApplicationTokenLength]; RandomNumberGenerator.Fill(tokenData); @@ -117,7 +91,10 @@ namespace Akari.Prototype.Server.Services _applications[applicationName] = application; - token = Convert.ToBase64String(tokenData); + applicationToken = Convert.ToBase64String(tokenData); + + // Application key + _keyManager.Create(applicationName); SaveApplications(); @@ -126,23 +103,102 @@ namespace Akari.Prototype.Server.Services return true; } - public bool TryRetrieveKey(string applicationName, string token, out AesGcm key) + public bool Contains(string applicationName) { - key = null; + return _applications.ContainsKey(applicationName); + } + public bool Remove(string applicationName) + { + if (!_applications.ContainsKey(applicationName)) + { + _logger.LogDebug($"Can't remove non existing application: {applicationName}"); + + return false; + } + + // Clear keys + _keyManager.Clear(applicationName); + + _applications.Remove(applicationName); + + SaveApplications(); + + return true; + } + + public bool AddFingerprint(string applicationName, string fingerprintName) + { + // Verify master password + if (!_masterKeyService.IsLoggedIn) + { + _logger.LogDebug($"Can't add a fingerprint for {applicationName} if master service is not logged in"); + } + + // Try get fingerprint key + if (!_authManager.TryGetKey(fingerprintName, out var key)) + { + _logger.LogDebug($"Fingerprint '{fingerprintName}' has not been auth"); + + return false; + } + + // Encrypt key + _keyManager.AddFingerprint(applicationName, fingerprintName, key); + + _applications[applicationName].Fingerprints.Add(fingerprintName); + + SaveApplications(); + + return true; + } + + private bool VerifyToken(string applicationName, string applicationToken) + { if (!_applications.TryGetValue(applicationName, out var application)) { return false; } - if (!VerifyToken(applicationName, token)) + return Argon2.Verify(application.TokenHash, Convert.FromBase64String(applicationToken)); + } + + public bool TryRetrieveKey(string applicationName, string applicationToken, out AesGcm applicationKey) + { + applicationKey = null; + + if (!_applications.TryGetValue(applicationName, out var application)) { + _logger.LogDebug($"Tried to retrieve key for non existing application: {applicationName}"); + return false; } - //key = _authManager.Retrieve(applicationName); + if (!VerifyToken(applicationName, applicationToken)) + { + _logger.LogDebug($"Can't retrieve '{applicationName}' key, wrong token"); + + return false; + } + + if (!_authManager.TryGetKey(application.Fingerprints, out var fingerprintName, out var fingerprintKey)) + { + _logger.LogDebug($"Can't retrieve '{applicationName}' key, no fingerprint auth found"); + } + + applicationKey = _keyManager.RetrieveKey(applicationName, fingerprintName, fingerprintKey); return true; } + + public IEnumerator GetEnumerator() + { + return _applications.Values.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } } } diff --git a/Akari.Prototype.Server/Services/AuthManager.cs b/Akari.Prototype.Server/Services/AuthManager.cs index 0f6217f..62a0f3f 100644 --- a/Akari.Prototype.Server/Services/AuthManager.cs +++ b/Akari.Prototype.Server/Services/AuthManager.cs @@ -26,8 +26,8 @@ namespace Akari.Prototype.Server.Services private readonly ILogger _logger; private readonly AkariPath _akariPath; + private readonly IDictionary> _keys; - private IDictionary> _keys; private IDictionary _salts; public AuthManager(ILogger logger, AkariPath akariPath) @@ -91,9 +91,49 @@ namespace Akari.Prototype.Server.Services 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) { - return _keys.Remove(name); + if (_keys.TryGetValue(name, out var entry)) + { + entry.Value.Dispose(); + + return _keys.Remove(name); + } + + return false; } private void SetKey(string name, AesGcm aesGcm) @@ -114,6 +154,11 @@ namespace Akari.Prototype.Server.Services public void Dispose() { + foreach (var entry in _keys.Values) + { + entry.Value.Dispose(); + } + _keys.Clear(); } } diff --git a/Akari.Prototype.Server/Services/IApplicationsManager.cs b/Akari.Prototype.Server/Services/IApplicationsManager.cs index 3408662..c6f0ae7 100644 --- a/Akari.Prototype.Server/Services/IApplicationsManager.cs +++ b/Akari.Prototype.Server/Services/IApplicationsManager.cs @@ -7,12 +7,16 @@ using System.Threading.Tasks; namespace Akari.Prototype.Server.Services { - public interface IApplicationsManager + public interface IApplicationsManager : IEnumerable { - bool TryCreate(string applicationName, out string token); + bool TryCreate(string applicationName, out string applicationToken); - bool AddFingerprint(string applicationName, string applicationToken, string fingerprintName, string masterPassword); + bool Contains(string applicationName); - bool TryRetrieveKey(string applicationName, string token, out AesGcm key); + bool Remove(string applicationName); + + bool AddFingerprint(string applicationName, string fingerprintName); + + bool TryRetrieveKey(string applicationName, string applicationToken, out AesGcm key); } } diff --git a/Akari.Prototype.Server/Services/IAuthManager.cs b/Akari.Prototype.Server/Services/IAuthManager.cs index b32a23f..1f92c20 100644 --- a/Akari.Prototype.Server/Services/IAuthManager.cs +++ b/Akari.Prototype.Server/Services/IAuthManager.cs @@ -16,6 +16,7 @@ namespace Akari.Prototype.Server.Services void Auth(string name, byte[] token); bool TryGetKey(string name, out AesGcm key); + bool TryGetKey(IEnumerable names, out string name, out AesGcm key); bool Remove(string name); } diff --git a/Akari.Prototype.Server/Services/IKeyManager.cs b/Akari.Prototype.Server/Services/IKeyManager.cs index d8d9d15..fbe4961 100644 --- a/Akari.Prototype.Server/Services/IKeyManager.cs +++ b/Akari.Prototype.Server/Services/IKeyManager.cs @@ -1,11 +1,19 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Security.Cryptography; using System.Threading.Tasks; namespace Akari.Prototype.Server.Services { public interface IKeyManager { + public bool AddFingerprint(string applicationName, string fingerprintName, AesGcm fingerprintKey); + + public void Clear(string applicationName); + + public bool Create(string applicationName); + + public AesGcm RetrieveKey(string applicationName, string fingerprintName, AesGcm fingerprintKey); } } diff --git a/Akari.Prototype.Server/Services/KeyManager.cs b/Akari.Prototype.Server/Services/KeyManager.cs index 6aa06be..dc04e39 100644 --- a/Akari.Prototype.Server/Services/KeyManager.cs +++ b/Akari.Prototype.Server/Services/KeyManager.cs @@ -1,11 +1,117 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; +using System.Security.Cryptography; using System.Threading.Tasks; +using Akari.Prototype.Server.Utils; +using Microsoft.Extensions.Logging; namespace Akari.Prototype.Server.Services { - public class KeyManager : IKeyManager + public sealed class KeyManager : IKeyManager { + public const string KeysPath = "Keys"; + public const int KeyLength = 256 / 8; + + private readonly ILogger _logger; + private readonly IMasterKeyService _masterKeyService; + private readonly AkariPath _akariPath; + + public KeyManager(ILogger logger, IMasterKeyService masterKeyService, AkariPath akariPath) + { + _logger = logger; + _masterKeyService = masterKeyService; + _akariPath = akariPath; + + CheckConfig(); + } + + private void CheckConfig() + { + string path = _akariPath.GetPath(KeysPath); + + if (!Directory.Exists(path)) + { + Directory.CreateDirectory(path); + } + } + + public bool AddFingerprint(string applicationName, string fingerprintName, AesGcm fingerprintKey) + { + if (!Directory.Exists(GetKeyDirectoryPath(applicationName)) || !_masterKeyService.TryGetKey(out var masterKey)) + { + return false; + } + + // Read key + var encryptedKeyBytes = File.ReadAllBytes(GetMasterKeyPath(applicationName)); + + var decryptedKeyBytes = Security.AesGcmDecrypt(masterKey, encryptedKeyBytes); + + var newEncryptedKeyBytes = Security.AesGcmEncrypt(fingerprintKey, decryptedKeyBytes); + + File.WriteAllBytes(GetKeyPath(applicationName, fingerprintName), newEncryptedKeyBytes); + + _logger.LogDebug($"Fingerprint '{fingerprintName}' added for {applicationName}"); + + return true; + } + + public void Clear(string applicationName) + { + _logger.LogDebug($"Deleted keys for {applicationName}"); + + _logger.LogDebug($"Going to delete {GetKeyDirectoryPath(applicationName)}"); + //Directory.Delete(GetKeyDirectoryPath(applicationName)); + } + + public bool Create(string applicationName) + { + if (!_masterKeyService.TryGetKey(out var masterKey)) + { + _logger.LogDebug("Can't create key if master not logged in"); + + return false; + } + + Directory.CreateDirectory(GetKeyDirectoryPath(applicationName)); + + Span keyBytes = KeyLength <= 1024 ? stackalloc byte[KeyLength] + : new byte[KeyLength]; + + RandomNumberGenerator.Fill(keyBytes); + + var encryptedKeyBytes = Security.AesGcmEncrypt(masterKey, keyBytes); + + File.WriteAllBytes(GetMasterKeyPath(applicationName), encryptedKeyBytes); + + _logger.LogDebug($"Key created for {applicationName}"); + + return true; + } + + public AesGcm RetrieveKey(string applicationName, string fingerprintName, AesGcm fingerprintKey) + { + if (!Directory.Exists(GetKeyDirectoryPath(applicationName))) + { + throw new IOException($"Can't find key files for {applicationName}"); + } + + // Read key + var encryptedKeyBytes = File.ReadAllBytes(GetKeyPath(applicationName, fingerprintName)); + + var decryptedKeyBytes = Security.AesGcmDecrypt(fingerprintKey, encryptedKeyBytes); + + _logger.LogDebug($"Key retrieved for {applicationName} using {fingerprintName}"); + + return new AesGcm(decryptedKeyBytes); + } + + private string GetKeyDirectoryPath(string applicationName) => Path.Combine(_akariPath.GetPath(KeysPath), applicationName); + + private string GetMasterKeyPath(string applicationName) => Path.Combine(GetKeyDirectoryPath(applicationName), "key"); + + private string GetKeyPath(string applicationName, string fingerprintName) => $"{GetMasterKeyPath(applicationName)}-{fingerprintName}"; } } diff --git a/Akari.Prototype.Server/Utils/Security.cs b/Akari.Prototype.Server/Utils/Security.cs index 49e3f40..9b427f4 100644 --- a/Akari.Prototype.Server/Utils/Security.cs +++ b/Akari.Prototype.Server/Utils/Security.cs @@ -1,6 +1,7 @@ using Isopoh.Cryptography.Argon2; using Isopoh.Cryptography.SecureArray; using System; +using System.Buffers.Binary; using System.Collections.Generic; using System.Linq; using System.Security.Cryptography; @@ -75,5 +76,60 @@ namespace Akari.Prototype.Server.Utils { return Argon2idDeriveBytes(Encoding.UTF8.GetBytes(password), Encoding.UTF8.GetBytes(salt), length, clear, threads); } + + // Source: https://stackoverflow.com/a/60891115/7465768 + public static byte[] AesGcmEncrypt(AesGcm key, Span plain) + { + // Get parameter sizes + int nonceSize = AesGcm.NonceByteSizes.MaxSize; + int tagSize = AesGcm.TagByteSizes.MaxSize; + int cipherSize = plain.Length; + + // We write everything into one big array for easier encoding + int encryptedDataLength = 4 + nonceSize + 4 + tagSize + cipherSize; + Span encryptedData = encryptedDataLength < 1024 + ? stackalloc byte[encryptedDataLength] + : new byte[encryptedDataLength]; + + // Copy parameters + BinaryPrimitives.WriteInt32LittleEndian(encryptedData.Slice(0, 4), nonceSize); + BinaryPrimitives.WriteInt32LittleEndian(encryptedData.Slice(4 + nonceSize, 4), tagSize); + + var nonce = encryptedData.Slice(4, nonceSize); + var tag = encryptedData.Slice(4 + nonceSize + 4, tagSize); + var cipherBytes = encryptedData.Slice(4 + nonceSize + 4 + tagSize, cipherSize); + + // Generate secure nonce + RandomNumberGenerator.Fill(nonce); + + // Encrypt + key.Encrypt(nonce, plain, cipherBytes, tag); + + // Encode for transmission + return encryptedData.ToArray(); + } + + public static byte[] AesGcmDecrypt(AesGcm key, Span encryptedData) + { + // Extract parameter sizes + int nonceSize = BinaryPrimitives.ReadInt32LittleEndian(encryptedData.Slice(0, 4)); + int tagSize = BinaryPrimitives.ReadInt32LittleEndian(encryptedData.Slice(4 + nonceSize, 4)); + int cipherSize = encryptedData.Length - 4 - nonceSize - 4 - tagSize; + + // Extract parameters + var nonce = encryptedData.Slice(4, nonceSize); + var tag = encryptedData.Slice(4 + nonceSize + 4, tagSize); + var cipherBytes = encryptedData.Slice(4 + nonceSize + 4 + tagSize, cipherSize); + + // Decrypt + Span plainBytes = cipherSize < 1024 + ? stackalloc byte[cipherSize] + : new byte[cipherSize]; + + key.Decrypt(nonce, cipherBytes, tag, plainBytes); + + // Convert plain bytes back into string + return plainBytes.ToArray(); + } } }