Implement IKeyManager
This commit is contained in:
@@ -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<ApplicationsManager> _logger;
|
||||
private readonly IKeyManager _keyManager;
|
||||
private readonly IAuthManager _authManager;
|
||||
private readonly IMasterKeyService _masterKeyService;
|
||||
private readonly AkariPath _akariPath;
|
||||
|
||||
private IDictionary<string, Application> _applications;
|
||||
|
||||
public ApplicationsManager(ILogger<ApplicationsManager> logger, IKeyManager keyManager, IAuthManager authManager, AkariPath akariPath)
|
||||
public ApplicationsManager(ILogger<ApplicationsManager> 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<Application> GetEnumerator()
|
||||
{
|
||||
return _applications.Values.GetEnumerator();
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
{
|
||||
return GetEnumerator();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,8 +26,8 @@ namespace Akari.Prototype.Server.Services
|
||||
|
||||
private readonly ILogger<AuthManager> _logger;
|
||||
private readonly AkariPath _akariPath;
|
||||
private readonly IDictionary<string, TimedEntry<AesGcm>> _keys;
|
||||
|
||||
private IDictionary<string, TimedEntry<AesGcm>> _keys;
|
||||
private IDictionary<string, string> _salts;
|
||||
|
||||
public AuthManager(ILogger<AuthManager> 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<string> 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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,12 +7,16 @@ using System.Threading.Tasks;
|
||||
|
||||
namespace Akari.Prototype.Server.Services
|
||||
{
|
||||
public interface IApplicationsManager
|
||||
public interface IApplicationsManager : IEnumerable<Application>
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<string> names, out string name, out AesGcm key);
|
||||
|
||||
bool Remove(string name);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<KeyManager> _logger;
|
||||
private readonly IMasterKeyService _masterKeyService;
|
||||
private readonly AkariPath _akariPath;
|
||||
|
||||
public KeyManager(ILogger<KeyManager> 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<byte> 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}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<byte> 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<byte> 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<byte> 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<byte> plainBytes = cipherSize < 1024
|
||||
? stackalloc byte[cipherSize]
|
||||
: new byte[cipherSize];
|
||||
|
||||
key.Decrypt(nonce, cipherBytes, tag, plainBytes);
|
||||
|
||||
// Convert plain bytes back into string
|
||||
return plainBytes.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user