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