1e21e33ade
Move SecureStoreManager project and tests to Deprecated folder and remove from solution. SecureStore functionality is now integrated into ConfigManager.
292 lines
8.7 KiB
C#
292 lines
8.7 KiB
C#
using System.IO;
|
|
using System.Text.Json;
|
|
using Microsoft.Extensions.Logging;
|
|
using Microsoft.Extensions.Logging.Abstractions;
|
|
using NeoSmart.SecureStore;
|
|
|
|
namespace JdeScoping.SecureStoreManager.Services;
|
|
|
|
/// <summary>
|
|
/// Manages SecureStore encrypted secret stores for the Avalonia application.
|
|
/// </summary>
|
|
public class SecureStoreManager : ISecureStoreManager, IDisposable
|
|
{
|
|
private readonly ILogger<SecureStoreManager> _logger;
|
|
private SecretsManager? _secretsManager;
|
|
private string? _currentStorePath;
|
|
private readonly HashSet<string> _keys = new();
|
|
private bool _hasUnsavedChanges;
|
|
private bool _disposed;
|
|
|
|
private const string KeysMetadataKey = "__keys__";
|
|
|
|
private static readonly HashSet<string> ReservedKeys = new(StringComparer.OrdinalIgnoreCase)
|
|
{
|
|
KeysMetadataKey
|
|
};
|
|
|
|
/// <summary>
|
|
/// Creates a new SecureStoreManager with no logging.
|
|
/// </summary>
|
|
public SecureStoreManager() : this(NullLogger<SecureStoreManager>.Instance)
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a new SecureStoreManager with the specified logger.
|
|
/// </summary>
|
|
/// <param name="logger">Logger instance for diagnostic output.</param>
|
|
public SecureStoreManager(ILogger<SecureStoreManager> logger)
|
|
{
|
|
_logger = logger ?? NullLogger<SecureStoreManager>.Instance;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public bool IsStoreOpen => _secretsManager != null;
|
|
|
|
/// <inheritdoc />
|
|
public string? CurrentStorePath => _currentStorePath;
|
|
|
|
/// <inheritdoc />
|
|
public bool HasUnsavedChanges => _hasUnsavedChanges;
|
|
|
|
/// <inheritdoc />
|
|
public void CreateStore(string storePath, string keyFilePath)
|
|
{
|
|
ThrowIfDisposed();
|
|
_logger.LogInformation("Creating new store at {StorePath}", storePath);
|
|
CloseStoreInternal();
|
|
|
|
EnsureDirectory(storePath);
|
|
EnsureDirectory(keyFilePath);
|
|
|
|
_secretsManager = SecretsManager.CreateStore();
|
|
_secretsManager.GenerateKey();
|
|
_secretsManager.ExportKey(keyFilePath);
|
|
|
|
_currentStorePath = storePath;
|
|
_keys.Clear();
|
|
_hasUnsavedChanges = true;
|
|
|
|
Save();
|
|
_logger.LogInformation("Store created with key file: {KeyFilePath}", keyFilePath);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public void OpenStore(string storePath, string keyFilePath)
|
|
{
|
|
ThrowIfDisposed();
|
|
_logger.LogInformation("Opening store at {StorePath}", storePath);
|
|
CloseStoreInternal();
|
|
|
|
if (!File.Exists(storePath))
|
|
throw new FileNotFoundException("Store file not found.", storePath);
|
|
|
|
if (!File.Exists(keyFilePath))
|
|
throw new FileNotFoundException("Key file not found.", keyFilePath);
|
|
|
|
_secretsManager = SecretsManager.LoadStore(storePath);
|
|
_secretsManager.LoadKeyFromFile(keyFilePath);
|
|
|
|
_currentStorePath = storePath;
|
|
LoadKeysMetadata();
|
|
_hasUnsavedChanges = false;
|
|
_logger.LogDebug("Store opened with key file, contains {KeyCount} keys", _keys.Count);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public void CloseStore()
|
|
{
|
|
ThrowIfDisposed();
|
|
_logger.LogInformation("Closing store");
|
|
CloseStoreInternal();
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public void Save()
|
|
{
|
|
ThrowIfDisposed();
|
|
|
|
if (_secretsManager == null || _currentStorePath == null)
|
|
throw new InvalidOperationException("No store is currently open.");
|
|
|
|
_logger.LogInformation("Saving store changes");
|
|
SaveKeysMetadata();
|
|
_secretsManager.SaveStore(_currentStorePath);
|
|
_hasUnsavedChanges = false;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public IReadOnlyList<string> GetKeys()
|
|
{
|
|
ThrowIfDisposed();
|
|
|
|
if (_secretsManager == null)
|
|
throw new InvalidOperationException("No store is currently open.");
|
|
|
|
return _keys.Where(k => k != KeysMetadataKey).ToList().AsReadOnly();
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public string GetSecret(string key)
|
|
{
|
|
ThrowIfDisposed();
|
|
|
|
if (_secretsManager == null)
|
|
throw new InvalidOperationException("No store is currently open.");
|
|
|
|
if (string.IsNullOrEmpty(key))
|
|
throw new ArgumentException("Key cannot be empty.", nameof(key));
|
|
|
|
if (!_keys.Contains(key))
|
|
throw new KeyNotFoundException($"Secret '{key}' not found.");
|
|
|
|
return _secretsManager.Get(key);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public void SetSecret(string key, string value)
|
|
{
|
|
ThrowIfDisposed();
|
|
|
|
if (_secretsManager == null)
|
|
throw new InvalidOperationException("No store is currently open.");
|
|
|
|
if (string.IsNullOrWhiteSpace(key))
|
|
throw new ArgumentException("Key cannot be null or whitespace.", nameof(key));
|
|
|
|
if (ReservedKeys.Contains(key))
|
|
{
|
|
_logger.LogWarning("Attempted to access reserved key {Key}", key);
|
|
throw new ArgumentException($"The key '{key}' is reserved for internal use.", nameof(key));
|
|
}
|
|
|
|
_logger.LogDebug("Setting secret for key {Key}", key);
|
|
_secretsManager.Set(key, value ?? string.Empty);
|
|
_keys.Add(key);
|
|
_hasUnsavedChanges = true;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public void RemoveSecret(string key)
|
|
{
|
|
ThrowIfDisposed();
|
|
|
|
if (_secretsManager == null)
|
|
throw new InvalidOperationException("No store is currently open.");
|
|
|
|
if (string.IsNullOrWhiteSpace(key))
|
|
throw new ArgumentException("Key cannot be null or whitespace.", nameof(key));
|
|
|
|
if (ReservedKeys.Contains(key))
|
|
{
|
|
_logger.LogWarning("Attempted to access reserved key {Key}", key);
|
|
throw new ArgumentException($"The key '{key}' is reserved for internal use.", nameof(key));
|
|
}
|
|
|
|
if (!_keys.Remove(key))
|
|
throw new KeyNotFoundException($"Secret '{key}' not found.");
|
|
|
|
_logger.LogInformation("Removing secret for key {Key}", key);
|
|
_secretsManager.Delete(key);
|
|
_hasUnsavedChanges = true;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public void GenerateKeyFile(string path)
|
|
{
|
|
ThrowIfDisposed();
|
|
|
|
if (string.IsNullOrEmpty(path))
|
|
throw new ArgumentException("Path cannot be empty.", nameof(path));
|
|
|
|
EnsureDirectory(path);
|
|
|
|
using var tempManager = SecretsManager.CreateStore();
|
|
tempManager.GenerateKey();
|
|
tempManager.ExportKey(path);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public void ExportKey(string path)
|
|
{
|
|
ThrowIfDisposed();
|
|
|
|
if (_secretsManager == null)
|
|
throw new InvalidOperationException("No store is currently open.");
|
|
|
|
if (string.IsNullOrEmpty(path))
|
|
throw new ArgumentException("Path cannot be empty.", nameof(path));
|
|
|
|
EnsureDirectory(path);
|
|
_secretsManager.ExportKey(path);
|
|
}
|
|
|
|
private void LoadKeysMetadata()
|
|
{
|
|
_keys.Clear();
|
|
|
|
try
|
|
{
|
|
var keysJson = _secretsManager!.Get(KeysMetadataKey);
|
|
if (!string.IsNullOrEmpty(keysJson))
|
|
{
|
|
var keys = JsonSerializer.Deserialize<string[]>(keysJson);
|
|
if (keys != null)
|
|
{
|
|
foreach (var key in keys)
|
|
_keys.Add(key);
|
|
}
|
|
}
|
|
}
|
|
catch (KeyNotFoundException)
|
|
{
|
|
// No keys metadata yet
|
|
}
|
|
}
|
|
|
|
private void SaveKeysMetadata()
|
|
{
|
|
var keys = _keys.Where(k => k != KeysMetadataKey).ToArray();
|
|
var keysJson = JsonSerializer.Serialize(keys);
|
|
_secretsManager!.Set(KeysMetadataKey, keysJson);
|
|
_keys.Add(KeysMetadataKey);
|
|
}
|
|
|
|
private void CloseStoreInternal()
|
|
{
|
|
_secretsManager?.Dispose();
|
|
_secretsManager = null;
|
|
_currentStorePath = null;
|
|
_keys.Clear();
|
|
_hasUnsavedChanges = false;
|
|
}
|
|
|
|
private static void EnsureDirectory(string filePath)
|
|
{
|
|
var directory = Path.GetDirectoryName(filePath);
|
|
if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
|
|
{
|
|
Directory.CreateDirectory(directory);
|
|
}
|
|
}
|
|
|
|
private void ThrowIfDisposed()
|
|
{
|
|
ObjectDisposedException.ThrowIf(_disposed, this);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Releases the resources used by the <see cref="SecureStoreManager"/>.
|
|
/// </summary>
|
|
public void Dispose()
|
|
{
|
|
if (_disposed) return;
|
|
|
|
_secretsManager?.Dispose();
|
|
_secretsManager = null;
|
|
_disposed = true;
|
|
GC.SuppressFinalize(this);
|
|
}
|
|
}
|