163 lines
5.9 KiB
C#
163 lines
5.9 KiB
C#
using System.Diagnostics;
|
|
using System.Security.Cryptography;
|
|
using Journal.Core.Services.Database;
|
|
|
|
namespace Journal.Core.Services.Vault;
|
|
|
|
public class VaultStorageService(IVaultCryptoService crypto, IJournalDatabaseService database) : IVaultStorageService
|
|
{
|
|
private readonly IVaultCryptoService _crypto = crypto;
|
|
private readonly IJournalDatabaseService _database = database;
|
|
private readonly object _vaultIoLock = new();
|
|
|
|
private const string DatabaseVaultPrefix = "_db_";
|
|
private const string DatabaseVaultSuffix = ".vault";
|
|
|
|
public bool LoadAllVaults(string password, string vaultDirectory, string dataDirectory)
|
|
{
|
|
EnsureRequiredArguments(password, vaultDirectory, dataDirectory);
|
|
|
|
lock (_vaultIoLock)
|
|
{
|
|
var dbDirectory = GetDatabaseDirectory();
|
|
Directory.CreateDirectory(dbDirectory);
|
|
if (!Directory.Exists(vaultDirectory))
|
|
return true;
|
|
|
|
return RestoreDatabaseVaults(password, vaultDirectory, dbDirectory);
|
|
}
|
|
}
|
|
|
|
public void RebuildAllVaults(string password, string vaultDirectory, string dataDirectory)
|
|
{
|
|
EnsureRequiredArguments(password, vaultDirectory, dataDirectory);
|
|
|
|
lock (_vaultIoLock)
|
|
{
|
|
Directory.CreateDirectory(vaultDirectory);
|
|
var dbDirectory = GetDatabaseDirectory();
|
|
if (!Directory.Exists(dbDirectory))
|
|
return;
|
|
|
|
SaveDatabaseVaults(password, vaultDirectory, dbDirectory);
|
|
}
|
|
}
|
|
|
|
public void ClearDataDirectory(string dataDirectory)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(dataDirectory))
|
|
throw new ArgumentException("Data directory is required.", nameof(dataDirectory));
|
|
|
|
lock (_vaultIoLock)
|
|
{
|
|
var normalizedDataDir = Path.GetFullPath(dataDirectory);
|
|
var dbDirectory = GetDatabaseDirectory();
|
|
if (string.Equals(normalizedDataDir, dbDirectory, StringComparison.OrdinalIgnoreCase))
|
|
return;
|
|
|
|
DeleteDirectoryWithRetries(normalizedDataDir);
|
|
}
|
|
}
|
|
|
|
private void SaveDatabaseVaults(string password, string vaultDirectory, string dataDirectory)
|
|
{
|
|
var dbFiles = Directory.GetFiles(dataDirectory, "*.db");
|
|
foreach (var dbPath in dbFiles)
|
|
{
|
|
try
|
|
{
|
|
var dbFileName = Path.GetFileName(dbPath);
|
|
var vaultFileName = $"{DatabaseVaultPrefix}{dbFileName}{DatabaseVaultSuffix}";
|
|
var vaultPath = Path.Combine(vaultDirectory, vaultFileName);
|
|
|
|
var dbBytes = File.ReadAllBytes(dbPath);
|
|
var encrypted = _crypto.EncryptData(dbBytes, password);
|
|
File.WriteAllBytes(vaultPath, encrypted);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Debug.WriteLine($"[VaultStorageService] Failed to save database vault for {Path.GetFileName(dbPath)}: {ex.Message}");
|
|
}
|
|
}
|
|
}
|
|
|
|
private bool RestoreDatabaseVaults(string password, string vaultDirectory, string dataDirectory)
|
|
{
|
|
var dbVaultFiles = Directory.GetFiles(vaultDirectory, $"{DatabaseVaultPrefix}*{DatabaseVaultSuffix}");
|
|
if (dbVaultFiles.Length == 0)
|
|
return true;
|
|
|
|
var anyRestored = false;
|
|
foreach (var vaultFile in dbVaultFiles)
|
|
{
|
|
try
|
|
{
|
|
var vaultFileName = Path.GetFileName(vaultFile);
|
|
var dbFileName = vaultFileName[DatabaseVaultPrefix.Length..^DatabaseVaultSuffix.Length];
|
|
if (string.IsNullOrWhiteSpace(dbFileName))
|
|
continue;
|
|
|
|
var encrypted = File.ReadAllBytes(vaultFile);
|
|
var dbBytes = _crypto.DecryptData(encrypted, password);
|
|
var targetPath = Path.Combine(dataDirectory, dbFileName);
|
|
File.WriteAllBytes(targetPath, dbBytes);
|
|
anyRestored = true;
|
|
}
|
|
catch (CryptographicException)
|
|
{
|
|
Debug.WriteLine($"[VaultStorageService] Database vault decryption failed for {Path.GetFileName(vaultFile)} (likely wrong password)");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Debug.WriteLine($"[VaultStorageService] Failed to restore database vault {Path.GetFileName(vaultFile)}: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
return anyRestored;
|
|
}
|
|
|
|
private static void EnsureRequiredArguments(string password, string vaultDirectory, string dataDirectory)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(password))
|
|
throw new ArgumentException("Password cannot be empty.", nameof(password));
|
|
if (string.IsNullOrWhiteSpace(vaultDirectory))
|
|
throw new ArgumentException("Vault directory is required.", nameof(vaultDirectory));
|
|
if (string.IsNullOrWhiteSpace(dataDirectory))
|
|
throw new ArgumentException("Data directory is required.", nameof(dataDirectory));
|
|
}
|
|
|
|
private string GetDatabaseDirectory()
|
|
{
|
|
var dbPath = _database.GetDatabasePath();
|
|
var directory = Path.GetDirectoryName(dbPath);
|
|
return string.IsNullOrWhiteSpace(directory)
|
|
? Path.GetFullPath(".")
|
|
: Path.GetFullPath(directory);
|
|
}
|
|
|
|
private static void DeleteDirectoryWithRetries(string dataDirectory, int retries = 5, int delayMs = 200)
|
|
{
|
|
if (!Directory.Exists(dataDirectory))
|
|
return;
|
|
|
|
for (var attempt = 0; attempt < retries; attempt++)
|
|
{
|
|
try
|
|
{
|
|
Directory.Delete(dataDirectory, recursive: true);
|
|
return;
|
|
}
|
|
catch (IOException) when (attempt < retries - 1)
|
|
{
|
|
Thread.Sleep(delayMs);
|
|
}
|
|
catch (UnauthorizedAccessException) when (attempt < retries - 1)
|
|
{
|
|
Thread.Sleep(delayMs);
|
|
}
|
|
}
|
|
|
|
Directory.Delete(dataDirectory, recursive: true);
|
|
}
|
|
}
|