using System.IO;
using System.Text.Json;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using NeoSmart.SecureStore;
namespace JdeScoping.SecureStoreManager.Services;
///
/// Manages SecureStore encrypted secret stores for the Avalonia application.
///
public class SecureStoreManager : ISecureStoreManager, IDisposable
{
private readonly ILogger _logger;
private SecretsManager? _secretsManager;
private string? _currentStorePath;
private readonly HashSet _keys = new();
private bool _hasUnsavedChanges;
private bool _disposed;
private const string KeysMetadataKey = "__keys__";
private static readonly HashSet ReservedKeys = new(StringComparer.OrdinalIgnoreCase)
{
KeysMetadataKey
};
///
/// Creates a new SecureStoreManager with no logging.
///
public SecureStoreManager() : this(NullLogger.Instance)
{
}
///
/// Creates a new SecureStoreManager with the specified logger.
///
/// Logger instance for diagnostic output.
public SecureStoreManager(ILogger logger)
{
_logger = logger ?? NullLogger.Instance;
}
///
public bool IsStoreOpen => _secretsManager != null;
///
public string? CurrentStorePath => _currentStorePath;
///
public bool HasUnsavedChanges => _hasUnsavedChanges;
///
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);
}
///
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);
}
///
public void CloseStore()
{
ThrowIfDisposed();
_logger.LogInformation("Closing store");
CloseStoreInternal();
}
///
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;
}
///
public IReadOnlyList GetKeys()
{
ThrowIfDisposed();
if (_secretsManager == null)
throw new InvalidOperationException("No store is currently open.");
return _keys.Where(k => k != KeysMetadataKey).ToList().AsReadOnly();
}
///
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);
}
///
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;
}
///
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;
}
///
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);
}
///
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(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);
}
///
/// Releases the resources used by the .
///
public void Dispose()
{
if (_disposed) return;
_secretsManager?.Dispose();
_secretsManager = null;
_disposed = true;
GC.SuppressFinalize(this);
}
}