< Summary

Information
Class: LOCKnet.Data.PlainToEncryptedVaultMigrationRequest
Assembly: LOCKnet.Data
File(s): /home/runner/work/LOCKnet/LOCKnet/src/LOCKnet.Data/PlainToEncryptedVaultMigrationCoordinator.cs
Line coverage
100%
Covered lines: 5
Uncovered lines: 0
Coverable lines: 5
Total lines: 297
Line coverage: 100%
Branch coverage
N/A
Covered branches: 0
Total branches: 0
Branch coverage: N/A
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
get_Header()100%11100%
get_Credentials()100%11100%
get_TargetMode()100%11100%
get_SourceValidatedWithActiveVaultKey()100%11100%

File(s)

/home/runner/work/LOCKnet/LOCKnet/src/LOCKnet.Data/PlainToEncryptedVaultMigrationCoordinator.cs

#LineLine coverage
 1using LOCKnet.Core.DataAbstractions;
 2using LOCKnet.Data.Repositories;
 3using Microsoft.Data.Sqlite;
 4
 5namespace LOCKnet.Data;
 6
 7internal sealed class PlainToEncryptedVaultMigrationCoordinator
 8{
 9  private readonly ISqliteConnectionFactory _sourceConnectionFactory;
 10  private readonly MasterKeyRepository _headerRepository;
 11
 12  internal PlainToEncryptedVaultMigrationCoordinator(ISqliteConnectionFactory sourceConnectionFactory)
 13  {
 14    ArgumentNullException.ThrowIfNull(sourceConnectionFactory);
 15    _sourceConnectionFactory = sourceConnectionFactory;
 16    _headerRepository = new MasterKeyRepository(sourceConnectionFactory);
 17  }
 18
 19  internal PlainToEncryptedVaultMigrationExecutionResult Execute(PlainToEncryptedVaultMigrationRequest request, IEncrypt
 20  {
 21    ArgumentNullException.ThrowIfNull(exporter);
 22    if (exporter.TargetMode != request.TargetMode)
 23      throw new InvalidOperationException("Exporter-Zielmodus passt nicht zum angeforderten Storage-Migrationsziel.");
 24
 25    var utcNow = DateTime.UtcNow;
 26    var plan = Prepare(request);
 27    var inProgressHeader = MarkInProgress(request.Header, request.TargetMode, utcNow);
 28    _headerRepository.Update(inProgressHeader);
 29
 30    try
 31    {
 32      exporter.ExportPlaintextVault(_sourceConnectionFactory.Storage.ConnectionString, plan.EncryptedTempPath);
 33
 34      if (!File.Exists(plan.EncryptedTempPath))
 35        throw new InvalidOperationException("Exporter hat kein Zielartefakt erstellt.");
 36
 37      exporter.ValidateExportedVault(plan.EncryptedTempPath);
 38
 39      var finalizationPendingHeader = MarkFinalizationPending(inProgressHeader, request.TargetMode, DateTime.UtcNow);
 40      exporter.PersistMigratedHeader(plan.EncryptedTempPath, finalizationPendingHeader);
 41      StorageRewriteArtifacts.ReplacePrimaryDatabase(plan.EncryptedTempPath, plan.SourcePath, plan.PlainBackupPath);
 42
 43      if (!File.Exists(plan.PlainBackupPath))
 44      {
 45        var clearedHeader = ClearMigrationState(finalizationPendingHeader, DateTime.UtcNow);
 46        exporter.PersistMigratedHeader(plan.SourcePath, clearedHeader);
 47        return new PlainToEncryptedVaultMigrationExecutionResult(
 48          clearedHeader.StorageMigrationState,
 49          clearedHeader.StorageMigrationTargetMode,
 50          clearedHeader.LastStorageMigrationAttemptUtc,
 51          clearedHeader.LastStorageMigrationError,
 52          plan.EncryptedTempPath,
 53          plan.PlainBackupPath);
 54      }
 55
 56      return new PlainToEncryptedVaultMigrationExecutionResult(
 57        finalizationPendingHeader.StorageMigrationState,
 58        finalizationPendingHeader.StorageMigrationTargetMode,
 59        finalizationPendingHeader.LastStorageMigrationAttemptUtc,
 60        finalizationPendingHeader.LastStorageMigrationError,
 61        plan.EncryptedTempPath,
 62        plan.PlainBackupPath);
 63    }
 64    catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or InvalidOperationException or SqliteEx
 65    {
 66      var failedHeader = MarkFailed(_headerRepository.Get() ?? inProgressHeader, request.TargetMode, DateTime.UtcNow, ex
 67      _headerRepository.Update(failedHeader);
 68
 69      return new PlainToEncryptedVaultMigrationExecutionResult(
 70        failedHeader.StorageMigrationState,
 71        failedHeader.StorageMigrationTargetMode,
 72        failedHeader.LastStorageMigrationAttemptUtc,
 73        failedHeader.LastStorageMigrationError,
 74        plan.EncryptedTempPath,
 75        plan.PlainBackupPath);
 76    }
 77  }
 78
 79  internal PlainToEncryptedVaultMigrationExecutionResult FinalizeSuccessfulMigration(VaultHeader header, IEncryptedVault
 80  {
 81    ArgumentNullException.ThrowIfNull(header);
 82    ArgumentNullException.ThrowIfNull(exporter);
 83
 84    var sourcePath = _sourceConnectionFactory.Storage.DatabasePath
 85      ?? throw new InvalidOperationException("Finalisierung der Plain-zu-encrypted-Migration benoetigt eine dateibasiert
 86    var backupPath = PlainToEncryptedVaultMigrationArtifacts.GetPlainBackupPath(sourcePath);
 87    if (header.StorageMigrationState != VaultStorageMigrationState.FinalizationPending)
 88      throw new InvalidOperationException("Finalisierung erwartet einen Storage-Migrationszustand FinalizationPending.")
 89
 90    if (File.Exists(backupPath) && !StorageRewriteArtifacts.TryDeleteFile(backupPath))
 91      throw new IOException("Alte Plain-Sicherung konnte nicht entfernt werden.");
 92
 93    var cleared = ClearMigrationState(header, DateTime.UtcNow);
 94    exporter.PersistMigratedHeader(sourcePath, cleared);
 95
 96    return new PlainToEncryptedVaultMigrationExecutionResult(
 97      cleared.StorageMigrationState,
 98      cleared.StorageMigrationTargetMode,
 99      cleared.LastStorageMigrationAttemptUtc,
 100      cleared.LastStorageMigrationError,
 101      PlainToEncryptedVaultMigrationArtifacts.GetEncryptedTempPath(sourcePath),
 102      backupPath);
 103  }
 104
 105  internal PlainToEncryptedVaultMigrationPlan Prepare(PlainToEncryptedVaultMigrationRequest request)
 106  {
 107    ArgumentNullException.ThrowIfNull(request);
 108    ArgumentNullException.ThrowIfNull(request.Header);
 109    ArgumentNullException.ThrowIfNull(request.Credentials);
 110
 111    var storage = _sourceConnectionFactory.Storage;
 112    if (storage.Mode != VaultStorageMode.PlainSqlite)
 113      throw new InvalidOperationException("Plain-zu-encrypted-Migration darf nur von einer Plain-SQLite-Quelle starten."
 114
 115    if (storage.RequiresKeyAtOpen)
 116      throw new InvalidOperationException("Die Quell-Vault darf fuer den Plain-zu-encrypted-Export kein Open-Time-Keying
 117
 118    var sourcePath = storage.DatabasePath
 119      ?? throw new InvalidOperationException("Plain-zu-encrypted-Migration benoetigt eine dateibasierte Quell-Vault.");
 120
 121    if (!request.SourceValidatedWithActiveVaultKey)
 122      throw new InvalidOperationException("Plain-zu-encrypted-Migration setzt eine vorherige Quellvalidierung mit aktive
 123
 124    ValidateHeader(request.Header);
 125    ValidateSettingsState();
 126    foreach (var credential in request.Credentials)
 127      ValidateCredential(credential);
 128
 129    if (PlainToEncryptedVaultMigrationArtifacts.HasPendingArtifacts(sourcePath))
 130      throw new InvalidOperationException("Plain-zu-encrypted-Migration kann nicht starten, solange alte Migrationsartef
 131
 132    return new PlainToEncryptedVaultMigrationPlan(
 133      sourcePath,
 134      PlainToEncryptedVaultMigrationArtifacts.GetEncryptedTempPath(sourcePath),
 135      PlainToEncryptedVaultMigrationArtifacts.GetPlainBackupPath(sourcePath),
 136      request.TargetMode);
 137  }
 138
 139  internal VaultHeader MarkInProgress(VaultHeader header, VaultStorageMigrationTargetMode targetMode, DateTime utcNow)
 140  {
 141    var updated = CloneHeader(header);
 142    updated.StorageMigrationState = VaultStorageMigrationState.InProgress;
 143    updated.StorageMigrationTargetMode = targetMode;
 144    updated.LastStorageMigrationAttemptUtc = utcNow;
 145    updated.LastStorageMigrationError = null;
 146    updated.UpdatedAt = utcNow;
 147    return updated;
 148  }
 149
 150  internal VaultHeader MarkFinalizationPending(VaultHeader header, VaultStorageMigrationTargetMode targetMode, DateTime 
 151  {
 152    var updated = CloneHeader(header);
 153    updated.StorageMigrationState = VaultStorageMigrationState.FinalizationPending;
 154    updated.StorageMigrationTargetMode = targetMode;
 155    updated.LastStorageMigrationAttemptUtc = utcNow;
 156    updated.LastStorageMigrationError = null;
 157    updated.UpdatedAt = utcNow;
 158    return updated;
 159  }
 160
 161  internal VaultHeader MarkFailed(VaultHeader header, VaultStorageMigrationTargetMode targetMode, DateTime utcNow, strin
 162  {
 163    var updated = CloneHeader(header);
 164    updated.StorageMigrationState = VaultStorageMigrationState.Failed;
 165    updated.StorageMigrationTargetMode = targetMode;
 166    updated.LastStorageMigrationAttemptUtc = utcNow;
 167    updated.LastStorageMigrationError = error;
 168    updated.UpdatedAt = utcNow;
 169    return updated;
 170  }
 171
 172  internal VaultHeader ClearMigrationState(VaultHeader header, DateTime utcNow)
 173  {
 174    var updated = CloneHeader(header);
 175    updated.StorageMigrationState = VaultStorageMigrationState.None;
 176    updated.StorageMigrationTargetMode = VaultStorageMigrationTargetMode.None;
 177    updated.LastStorageMigrationAttemptUtc = null;
 178    updated.LastStorageMigrationError = null;
 179    updated.UpdatedAt = utcNow;
 180    return updated;
 181  }
 182
 183  internal PlainToEncryptedVaultMigrationRecoveryDecision GetRecoveryDecision(VaultHeader header)
 184    => PlainToEncryptedVaultMigrationArtifacts.Decide(_sourceConnectionFactory.Storage.DatabasePath, header);
 185
 186  private void ValidateSettingsState()
 187  {
 188    using var connection = _sourceConnectionFactory.OpenConnection();
 189    RequireTable(connection, "MasterKey");
 190    RequireTable(connection, "Credentials");
 191    RequireTable(connection, "Settings");
 192
 193    using var masterKeyCount = connection.CreateCommand();
 194    masterKeyCount.CommandText = "SELECT COUNT(*) FROM MasterKey;";
 195    if (Convert.ToInt64(masterKeyCount.ExecuteScalar() ?? 0L) != 1)
 196      throw new InvalidOperationException("Plain-zu-encrypted-Migration erwartet genau einen MasterKey-Datensatz.");
 197  }
 198
 199  private static void RequireTable(SqliteConnection connection, string tableName)
 200  {
 201    using var command = connection.CreateCommand();
 202    command.CommandText = "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name=$name;";
 203    command.Parameters.AddWithValue("$name", tableName);
 204    if (Convert.ToInt64(command.ExecuteScalar() ?? 0L) <= 0)
 205      throw new InvalidOperationException($"Plain-zu-encrypted-Migration erwartet die Tabelle '{tableName}'.");
 206  }
 207
 208  private static void ValidateHeader(VaultHeader header)
 209  {
 210    if (header.FormatVersion != VaultHeaderFormatVersion.Current)
 211      throw new InvalidOperationException("Plain-zu-encrypted-Migration erwartet einen aktuellen VaultHeader.");
 212
 213    if (header.UsesLegacyKeyMaterial)
 214      throw new InvalidOperationException("Plain-zu-encrypted-Migration darf nicht mit Legacy-Keymaterial starten.");
 215
 216    if (header.LegacyPasswordHash.Length > 0)
 217      throw new InvalidOperationException("Plain-zu-encrypted-Migration erwartet keinen Legacy-Passwort-Hash mehr.");
 218
 219    if (header.StorageMigrationState is not VaultStorageMigrationState.None and not VaultStorageMigrationState.Failed)
 220      throw new InvalidOperationException("Plain-zu-encrypted-Migration darf nicht parallel zu einer bereits laufenden S
 221  }
 222
 223  private static void ValidateCredential(CredentialRecord credential)
 224  {
 225    if (credential.SecretFormatVersion != CredentialSecretFormatVersion.Current)
 226      throw new InvalidOperationException($"Credential {credential.Id} verwendet noch ein Legacy-Secret-Format.");
 227
 228    if (credential.MetadataFormatVersion != CredentialMetadataFormatVersion.Current)
 229      throw new InvalidOperationException($"Credential {credential.Id} verwendet noch ein Legacy-Metadaten-Format.");
 230
 231    if (!Guid.TryParseExact(credential.CredentialUuid, "N", out _))
 232      throw new InvalidOperationException($"Credential {credential.Id} enthaelt eine ungueltige CredentialUuid.");
 233
 234    if (credential.EncryptedPassword.Length == 0)
 235      throw new InvalidOperationException($"Credential {credential.Id} enthaelt keine verschluesselten Secret-Daten.");
 236
 237    if (credential.EncryptedMetadata.Length == 0)
 238      throw new InvalidOperationException($"Credential {credential.Id} enthaelt keine verschluesselten Metadaten.");
 239
 240    if (!string.IsNullOrEmpty(credential.Title) ||
 241      !string.IsNullOrEmpty(credential.Username) ||
 242      !string.IsNullOrEmpty(credential.Url) ||
 243      !string.IsNullOrEmpty(credential.Notes) ||
 244      !string.IsNullOrEmpty(credential.IconKey) ||
 245      credential.CredentialType != CredentialType.Password)
 246    {
 247      throw new InvalidOperationException($"Credential {credential.Id} enthaelt unerwartete Klartext-Metadaten.");
 248    }
 249  }
 250
 251  private static VaultHeader CloneHeader(VaultHeader header) => new()
 252  {
 253    FormatVersion = header.FormatVersion,
 254    KdfIdentifier = header.KdfIdentifier,
 255    KdfParameters = new VaultKdfParameters
 256    {
 257      HashAlgorithm = header.KdfParameters.HashAlgorithm,
 258      Iterations = header.KdfParameters.Iterations,
 259      KeyLengthBytes = header.KdfParameters.KeyLengthBytes,
 260      SaltLengthBytes = header.KdfParameters.SaltLengthBytes,
 261    },
 262    Salt = header.Salt.ToArray(),
 263    WrappedVaultKey = header.WrappedVaultKey.ToArray(),
 264    LegacyPasswordHash = header.LegacyPasswordHash.ToArray(),
 265    UsesLegacyKeyMaterial = header.UsesLegacyKeyMaterial,
 266    RequiresStorageCompaction = header.RequiresStorageCompaction,
 267    LastStorageCompactionAttemptUtc = header.LastStorageCompactionAttemptUtc,
 268    LastStorageCompactionFailureKind = header.LastStorageCompactionFailureKind,
 269    LastStorageCompactionError = header.LastStorageCompactionError,
 270    StorageMigrationState = header.StorageMigrationState,
 271    StorageMigrationTargetMode = header.StorageMigrationTargetMode,
 272    LastStorageMigrationAttemptUtc = header.LastStorageMigrationAttemptUtc,
 273    LastStorageMigrationError = header.LastStorageMigrationError,
 274    CreatedAt = header.CreatedAt,
 275    UpdatedAt = header.UpdatedAt,
 276  };
 277}
 278
 20279internal sealed record PlainToEncryptedVaultMigrationRequest(
 44280  VaultHeader Header,
 31281  IReadOnlyList<CredentialRecord> Credentials,
 38282  VaultStorageMigrationTargetMode TargetMode,
 37283  bool SourceValidatedWithActiveVaultKey);
 284
 285internal sealed record PlainToEncryptedVaultMigrationPlan(
 286  string SourcePath,
 287  string EncryptedTempPath,
 288  string PlainBackupPath,
 289  VaultStorageMigrationTargetMode TargetMode);
 290
 291internal sealed record PlainToEncryptedVaultMigrationExecutionResult(
 292  VaultStorageMigrationState StorageMigrationState,
 293  VaultStorageMigrationTargetMode StorageMigrationTargetMode,
 294  DateTime? LastStorageMigrationAttemptUtc,
 295  string? LastStorageMigrationError,
 296  string EncryptedTempPath,
 297  string PlainBackupPath);