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); } }