chore: deprecate standalone SecureStoreManager utility
Move SecureStoreManager project and tests to Deprecated folder and remove from solution. SecureStore functionality is now integrated into ConfigManager.
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
using Avalonia.Input.Platform;
|
||||
|
||||
namespace JdeScoping.SecureStoreManager.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Avalonia implementation of IClipboardService.
|
||||
/// </summary>
|
||||
public class AvaloniaClipboardService : IClipboardService
|
||||
{
|
||||
private readonly Func<IClipboard?> _getClipboard;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of AvaloniaClipboardService.
|
||||
/// </summary>
|
||||
/// <param name="getClipboard">Factory function to get the clipboard instance.</param>
|
||||
public AvaloniaClipboardService(Func<IClipboard?> getClipboard)
|
||||
{
|
||||
_getClipboard = getClipboard ?? throw new ArgumentNullException(nameof(getClipboard));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the clipboard text asynchronously.
|
||||
/// </summary>
|
||||
/// <param name="text">The text to set on the clipboard.</param>
|
||||
public async Task SetTextAsync(string text)
|
||||
{
|
||||
var clipboard = _getClipboard();
|
||||
if (clipboard != null)
|
||||
{
|
||||
await clipboard.SetTextAsync(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Platform.Storage;
|
||||
using JdeScoping.SecureStoreManager.Constants;
|
||||
using MsBox.Avalonia;
|
||||
using MsBox.Avalonia.Enums;
|
||||
|
||||
namespace JdeScoping.SecureStoreManager.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Avalonia implementation of IDialogService using MsBox.Avalonia and platform storage.
|
||||
/// </summary>
|
||||
public class AvaloniaDialogService : IDialogService
|
||||
{
|
||||
private readonly Func<Window?> _getOwnerWindow;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of AvaloniaDialogService.
|
||||
/// </summary>
|
||||
/// <param name="getOwnerWindow">Factory function to get the owner window for dialogs.</param>
|
||||
public AvaloniaDialogService(Func<Window?> getOwnerWindow)
|
||||
{
|
||||
_getOwnerWindow = getOwnerWindow ?? throw new ArgumentNullException(nameof(getOwnerWindow));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Displays an error dialog with the specified message and title.
|
||||
/// </summary>
|
||||
/// <param name="message">The error message to display.</param>
|
||||
/// <param name="title">The title of the error dialog.</param>
|
||||
/// <returns>A task that completes when the dialog is dismissed.</returns>
|
||||
public async Task ShowErrorAsync(string message, string title)
|
||||
{
|
||||
var box = MessageBoxManager.GetMessageBoxStandard(title, message, ButtonEnum.Ok, Icon.Error);
|
||||
var window = _getOwnerWindow();
|
||||
if (window != null)
|
||||
{
|
||||
await box.ShowWindowDialogAsync(window);
|
||||
}
|
||||
else
|
||||
{
|
||||
await box.ShowAsync();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Displays an informational dialog with the specified message and title.
|
||||
/// </summary>
|
||||
/// <param name="message">The information message to display.</param>
|
||||
/// <param name="title">The title of the information dialog.</param>
|
||||
/// <returns>A task that completes when the dialog is dismissed.</returns>
|
||||
public async Task ShowInfoAsync(string message, string title)
|
||||
{
|
||||
var box = MessageBoxManager.GetMessageBoxStandard(title, message, ButtonEnum.Ok, Icon.Info);
|
||||
var window = _getOwnerWindow();
|
||||
if (window != null)
|
||||
{
|
||||
await box.ShowWindowDialogAsync(window);
|
||||
}
|
||||
else
|
||||
{
|
||||
await box.ShowAsync();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Displays a confirmation dialog with Yes/No buttons.
|
||||
/// </summary>
|
||||
/// <param name="message">The confirmation message to display.</param>
|
||||
/// <param name="title">The title of the confirmation dialog.</param>
|
||||
/// <returns>A task that completes with true if Yes was clicked; otherwise, false.</returns>
|
||||
public async Task<bool> ShowConfirmationAsync(string message, string title)
|
||||
{
|
||||
var box = MessageBoxManager.GetMessageBoxStandard(title, message, ButtonEnum.YesNo, Icon.Warning);
|
||||
var window = _getOwnerWindow();
|
||||
ButtonResult result;
|
||||
if (window != null)
|
||||
{
|
||||
result = await box.ShowWindowDialogAsync(window);
|
||||
}
|
||||
else
|
||||
{
|
||||
result = await box.ShowAsync();
|
||||
}
|
||||
return result == ButtonResult.Yes;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Displays a prompt asking the user whether to save unsaved changes.
|
||||
/// </summary>
|
||||
/// <returns>A task that completes with the user's choice: Save, DontSave, or Cancel.</returns>
|
||||
public async Task<UnsavedChangesResult> ShowUnsavedChangesPromptAsync()
|
||||
{
|
||||
var box = MessageBoxManager.GetMessageBoxStandard(
|
||||
DialogStrings.UnsavedChangesTitle,
|
||||
DialogStrings.UnsavedChangesMessage,
|
||||
ButtonEnum.YesNoCancel,
|
||||
Icon.Warning);
|
||||
|
||||
var window = _getOwnerWindow();
|
||||
ButtonResult result;
|
||||
if (window != null)
|
||||
{
|
||||
result = await box.ShowWindowDialogAsync(window);
|
||||
}
|
||||
else
|
||||
{
|
||||
result = await box.ShowAsync();
|
||||
}
|
||||
|
||||
return result switch
|
||||
{
|
||||
ButtonResult.Yes => UnsavedChangesResult.Save,
|
||||
ButtonResult.No => UnsavedChangesResult.DontSave,
|
||||
_ => UnsavedChangesResult.Cancel
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Displays a save file dialog allowing the user to select a file path.
|
||||
/// </summary>
|
||||
/// <param name="title">The title of the save file dialog.</param>
|
||||
/// <param name="fileTypeName">The friendly name of the file type (e.g., "Excel Files").</param>
|
||||
/// <param name="pattern">The file extension pattern (e.g., "*.xlsx").</param>
|
||||
/// <param name="defaultExtension">The default file extension (e.g., ".xlsx").</param>
|
||||
/// <returns>A task that completes with the selected file path, or null if canceled.</returns>
|
||||
public async Task<string?> ShowSaveFileDialogAsync(string title, string fileTypeName, string pattern, string defaultExtension)
|
||||
{
|
||||
var window = _getOwnerWindow();
|
||||
if (window == null)
|
||||
return null;
|
||||
|
||||
var file = await window.StorageProvider.SaveFilePickerAsync(new FilePickerSaveOptions
|
||||
{
|
||||
Title = title,
|
||||
DefaultExtension = defaultExtension,
|
||||
FileTypeChoices = new[]
|
||||
{
|
||||
new FilePickerFileType(fileTypeName) { Patterns = new[] { pattern } },
|
||||
new FilePickerFileType(FileExtensions.AllFilesTypeName) { Patterns = new[] { FileExtensions.AllFilesPattern } }
|
||||
}
|
||||
});
|
||||
|
||||
return file?.Path.LocalPath;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Displays an open file dialog allowing the user to select a file to open.
|
||||
/// </summary>
|
||||
/// <param name="title">The title of the open file dialog.</param>
|
||||
/// <param name="fileTypeName">The friendly name of the file type (e.g., "Excel Files").</param>
|
||||
/// <param name="pattern">The file extension pattern (e.g., "*.xlsx").</param>
|
||||
/// <returns>A task that completes with the selected file path, or null if canceled.</returns>
|
||||
public async Task<string?> ShowOpenFileDialogAsync(string title, string fileTypeName, string pattern)
|
||||
{
|
||||
var window = _getOwnerWindow();
|
||||
if (window == null)
|
||||
return null;
|
||||
|
||||
var files = await window.StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
|
||||
{
|
||||
Title = title,
|
||||
AllowMultiple = false,
|
||||
FileTypeFilter = new[]
|
||||
{
|
||||
new FilePickerFileType(fileTypeName) { Patterns = new[] { pattern } },
|
||||
new FilePickerFileType(FileExtensions.AllFilesTypeName) { Patterns = new[] { FileExtensions.AllFilesPattern } }
|
||||
}
|
||||
});
|
||||
|
||||
return files.Count > 0 ? files[0].Path.LocalPath : null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
namespace JdeScoping.SecureStoreManager.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Abstraction for platform-specific clipboard operations.
|
||||
/// Enables unit testing of view models that need clipboard access.
|
||||
/// </summary>
|
||||
public interface IClipboardService
|
||||
{
|
||||
/// <summary>
|
||||
/// Copies text to the system clipboard.
|
||||
/// </summary>
|
||||
/// <param name="text">The text to copy.</param>
|
||||
Task SetTextAsync(string text);
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
namespace JdeScoping.SecureStoreManager.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Result from unsaved changes prompt.
|
||||
/// </summary>
|
||||
public enum UnsavedChangesResult
|
||||
{
|
||||
Save,
|
||||
DontSave,
|
||||
Cancel
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Abstraction for platform-specific dialog operations.
|
||||
/// Enables unit testing of view models that need to show dialogs.
|
||||
/// </summary>
|
||||
public interface IDialogService
|
||||
{
|
||||
/// <summary>
|
||||
/// Shows an error message dialog.
|
||||
/// </summary>
|
||||
/// <param name="message">The error message to display.</param>
|
||||
/// <param name="title">The dialog title.</param>
|
||||
Task ShowErrorAsync(string message, string title);
|
||||
|
||||
/// <summary>
|
||||
/// Shows an informational message dialog.
|
||||
/// </summary>
|
||||
/// <param name="message">The informational message to display.</param>
|
||||
/// <param name="title">The dialog title.</param>
|
||||
Task ShowInfoAsync(string message, string title);
|
||||
|
||||
/// <summary>
|
||||
/// Shows a confirmation dialog with Yes/No options.
|
||||
/// </summary>
|
||||
/// <param name="message">The confirmation message to display.</param>
|
||||
/// <param name="title">The dialog title.</param>
|
||||
/// <returns>True if user clicked Yes, false otherwise.</returns>
|
||||
Task<bool> ShowConfirmationAsync(string message, string title);
|
||||
|
||||
/// <summary>
|
||||
/// Shows a prompt for unsaved changes with Save/Don't Save/Cancel options.
|
||||
/// </summary>
|
||||
Task<UnsavedChangesResult> ShowUnsavedChangesPromptAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Shows a save file dialog.
|
||||
/// </summary>
|
||||
/// <param name="title">Dialog title.</param>
|
||||
/// <param name="fileTypeName">Display name for the file type (e.g., "Key Files").</param>
|
||||
/// <param name="pattern">File pattern (e.g., "*.key").</param>
|
||||
/// <param name="defaultExtension">Default extension (e.g., ".key").</param>
|
||||
/// <returns>Selected file path or null if cancelled.</returns>
|
||||
Task<string?> ShowSaveFileDialogAsync(string title, string fileTypeName, string pattern, string defaultExtension);
|
||||
|
||||
/// <summary>
|
||||
/// Shows an open file dialog.
|
||||
/// </summary>
|
||||
/// <param name="title">Dialog title.</param>
|
||||
/// <param name="fileTypeName">Display name for the file type (e.g., "Key Files").</param>
|
||||
/// <param name="pattern">File pattern (e.g., "*.key").</param>
|
||||
/// <returns>Selected file path or null if cancelled.</returns>
|
||||
Task<string?> ShowOpenFileDialogAsync(string title, string fileTypeName, string pattern);
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
namespace JdeScoping.SecureStoreManager.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Interface for managing SecureStore encrypted secret stores.
|
||||
/// </summary>
|
||||
public interface ISecureStoreManager
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets whether a store is currently open.
|
||||
/// </summary>
|
||||
bool IsStoreOpen { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the path to the currently open store, or null if no store is open.
|
||||
/// </summary>
|
||||
string? CurrentStorePath { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether there are unsaved changes to the current store.
|
||||
/// </summary>
|
||||
bool HasUnsavedChanges { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new store secured with a key file.
|
||||
/// </summary>
|
||||
/// <param name="storePath">Path for the new store file (.json).</param>
|
||||
/// <param name="keyFilePath">Path for the key file (.key).</param>
|
||||
void CreateStore(string storePath, string keyFilePath);
|
||||
|
||||
/// <summary>
|
||||
/// Opens an existing store using a key file.
|
||||
/// </summary>
|
||||
/// <param name="storePath">Path to the store file (.json).</param>
|
||||
/// <param name="keyFilePath">Path to the key file (.key).</param>
|
||||
void OpenStore(string storePath, string keyFilePath);
|
||||
|
||||
/// <summary>
|
||||
/// Closes the currently open store without saving.
|
||||
/// </summary>
|
||||
void CloseStore();
|
||||
|
||||
/// <summary>
|
||||
/// Saves changes to the currently open store.
|
||||
/// </summary>
|
||||
void Save();
|
||||
|
||||
/// <summary>
|
||||
/// Gets all secret keys in the current store.
|
||||
/// </summary>
|
||||
/// <returns>Collection of secret key names.</returns>
|
||||
IReadOnlyList<string> GetKeys();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the value of a secret.
|
||||
/// </summary>
|
||||
/// <param name="key">The secret key.</param>
|
||||
/// <returns>The decrypted secret value.</returns>
|
||||
string GetSecret(string key);
|
||||
|
||||
/// <summary>
|
||||
/// Sets or updates a secret value.
|
||||
/// </summary>
|
||||
/// <param name="key">The secret key.</param>
|
||||
/// <param name="value">The value to encrypt and store.</param>
|
||||
void SetSecret(string key, string value);
|
||||
|
||||
/// <summary>
|
||||
/// Removes a secret from the store.
|
||||
/// </summary>
|
||||
/// <param name="key">The secret key to remove.</param>
|
||||
void RemoveSecret(string key);
|
||||
|
||||
/// <summary>
|
||||
/// Generates a new key file for use with store encryption.
|
||||
/// </summary>
|
||||
/// <param name="path">Path where the key file will be created.</param>
|
||||
void GenerateKeyFile(string path);
|
||||
|
||||
/// <summary>
|
||||
/// Exports the current store's key to a file (for key file-based stores).
|
||||
/// </summary>
|
||||
/// <param name="path">Path where the key will be exported.</param>
|
||||
void ExportKey(string path);
|
||||
}
|
||||
@@ -0,0 +1,291 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user