< Summary

Information
Class: LOCKnet.Data.PlainToEncryptedVaultMigrationCoordinator
Assembly: LOCKnet.Data
File(s): /home/runner/work/LOCKnet/LOCKnet/src/LOCKnet.Data/PlainToEncryptedVaultMigrationCoordinator.cs
Line coverage
86%
Covered lines: 180
Uncovered lines: 27
Coverable lines: 207
Total lines: 297
Line coverage: 86.9%
Branch coverage
72%
Covered branches: 49
Total branches: 68
Branch coverage: 72%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
Execute(...)75%8884.78%
FinalizeSuccessfulMigration(...)25%22840%
Prepare(...)83.33%121296%
MarkInProgress(...)100%11100%
MarkFinalizationPending(...)100%11100%
MarkFailed(...)100%11100%
ClearMigrationState(...)100%11100%
GetRecoveryDecision(...)100%11100%
ValidateSettingsState()50%4490%
RequireTable(...)75%44100%
ValidateHeader(...)80%111080%
ValidateCredential(...)81.81%262280%
CloneHeader(...)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
 2712  internal PlainToEncryptedVaultMigrationCoordinator(ISqliteConnectionFactory sourceConnectionFactory)
 2713  {
 2714    ArgumentNullException.ThrowIfNull(sourceConnectionFactory);
 2715    _sourceConnectionFactory = sourceConnectionFactory;
 2716    _headerRepository = new MasterKeyRepository(sourceConnectionFactory);
 2717  }
 18
 19  internal PlainToEncryptedVaultMigrationExecutionResult Execute(PlainToEncryptedVaultMigrationRequest request, IEncrypt
 1020  {
 1021    ArgumentNullException.ThrowIfNull(exporter);
 1022    if (exporter.TargetMode != request.TargetMode)
 123      throw new InvalidOperationException("Exporter-Zielmodus passt nicht zum angeforderten Storage-Migrationsziel.");
 24
 925    var utcNow = DateTime.UtcNow;
 926    var plan = Prepare(request);
 927    var inProgressHeader = MarkInProgress(request.Header, request.TargetMode, utcNow);
 928    _headerRepository.Update(inProgressHeader);
 29
 30    try
 931    {
 932      exporter.ExportPlaintextVault(_sourceConnectionFactory.Storage.ConnectionString, plan.EncryptedTempPath);
 33
 834      if (!File.Exists(plan.EncryptedTempPath))
 135        throw new InvalidOperationException("Exporter hat kein Zielartefakt erstellt.");
 36
 737      exporter.ValidateExportedVault(plan.EncryptedTempPath);
 38
 539      var finalizationPendingHeader = MarkFinalizationPending(inProgressHeader, request.TargetMode, DateTime.UtcNow);
 540      exporter.PersistMigratedHeader(plan.EncryptedTempPath, finalizationPendingHeader);
 541      StorageRewriteArtifacts.ReplacePrimaryDatabase(plan.EncryptedTempPath, plan.SourcePath, plan.PlainBackupPath);
 42
 543      if (!File.Exists(plan.PlainBackupPath))
 544      {
 545        var clearedHeader = ClearMigrationState(finalizationPendingHeader, DateTime.UtcNow);
 546        exporter.PersistMigratedHeader(plan.SourcePath, clearedHeader);
 547        return new PlainToEncryptedVaultMigrationExecutionResult(
 548          clearedHeader.StorageMigrationState,
 549          clearedHeader.StorageMigrationTargetMode,
 550          clearedHeader.LastStorageMigrationAttemptUtc,
 551          clearedHeader.LastStorageMigrationError,
 552          plan.EncryptedTempPath,
 553          plan.PlainBackupPath);
 54      }
 55
 056      return new PlainToEncryptedVaultMigrationExecutionResult(
 057        finalizationPendingHeader.StorageMigrationState,
 058        finalizationPendingHeader.StorageMigrationTargetMode,
 059        finalizationPendingHeader.LastStorageMigrationAttemptUtc,
 060        finalizationPendingHeader.LastStorageMigrationError,
 061        plan.EncryptedTempPath,
 062        plan.PlainBackupPath);
 63    }
 464    catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or InvalidOperationException or SqliteEx
 465    {
 466      var failedHeader = MarkFailed(_headerRepository.Get() ?? inProgressHeader, request.TargetMode, DateTime.UtcNow, ex
 467      _headerRepository.Update(failedHeader);
 68
 469      return new PlainToEncryptedVaultMigrationExecutionResult(
 470        failedHeader.StorageMigrationState,
 471        failedHeader.StorageMigrationTargetMode,
 472        failedHeader.LastStorageMigrationAttemptUtc,
 473        failedHeader.LastStorageMigrationError,
 474        plan.EncryptedTempPath,
 475        plan.PlainBackupPath);
 76    }
 977  }
 78
 79  internal PlainToEncryptedVaultMigrationExecutionResult FinalizeSuccessfulMigration(VaultHeader header, IEncryptedVault
 180  {
 181    ArgumentNullException.ThrowIfNull(header);
 182    ArgumentNullException.ThrowIfNull(exporter);
 83
 184    var sourcePath = _sourceConnectionFactory.Storage.DatabasePath
 185      ?? throw new InvalidOperationException("Finalisierung der Plain-zu-encrypted-Migration benoetigt eine dateibasiert
 186    var backupPath = PlainToEncryptedVaultMigrationArtifacts.GetPlainBackupPath(sourcePath);
 187    if (header.StorageMigrationState != VaultStorageMigrationState.FinalizationPending)
 188      throw new InvalidOperationException("Finalisierung erwartet einen Storage-Migrationszustand FinalizationPending.")
 89
 090    if (File.Exists(backupPath) && !StorageRewriteArtifacts.TryDeleteFile(backupPath))
 091      throw new IOException("Alte Plain-Sicherung konnte nicht entfernt werden.");
 92
 093    var cleared = ClearMigrationState(header, DateTime.UtcNow);
 094    exporter.PersistMigratedHeader(sourcePath, cleared);
 95
 096    return new PlainToEncryptedVaultMigrationExecutionResult(
 097      cleared.StorageMigrationState,
 098      cleared.StorageMigrationTargetMode,
 099      cleared.LastStorageMigrationAttemptUtc,
 0100      cleared.LastStorageMigrationError,
 0101      PlainToEncryptedVaultMigrationArtifacts.GetEncryptedTempPath(sourcePath),
 0102      backupPath);
 0103  }
 104
 105  internal PlainToEncryptedVaultMigrationPlan Prepare(PlainToEncryptedVaultMigrationRequest request)
 19106  {
 19107    ArgumentNullException.ThrowIfNull(request);
 19108    ArgumentNullException.ThrowIfNull(request.Header);
 19109    ArgumentNullException.ThrowIfNull(request.Credentials);
 110
 19111    var storage = _sourceConnectionFactory.Storage;
 19112    if (storage.Mode != VaultStorageMode.PlainSqlite)
 1113      throw new InvalidOperationException("Plain-zu-encrypted-Migration darf nur von einer Plain-SQLite-Quelle starten."
 114
 18115    if (storage.RequiresKeyAtOpen)
 1116      throw new InvalidOperationException("Die Quell-Vault darf fuer den Plain-zu-encrypted-Export kein Open-Time-Keying
 117
 17118    var sourcePath = storage.DatabasePath
 17119      ?? throw new InvalidOperationException("Plain-zu-encrypted-Migration benoetigt eine dateibasierte Quell-Vault.");
 120
 17121    if (!request.SourceValidatedWithActiveVaultKey)
 1122      throw new InvalidOperationException("Plain-zu-encrypted-Migration setzt eine vorherige Quellvalidierung mit aktive
 123
 16124    ValidateHeader(request.Header);
 13125    ValidateSettingsState();
 58126    foreach (var credential in request.Credentials)
 12127      ValidateCredential(credential);
 128
 10129    if (PlainToEncryptedVaultMigrationArtifacts.HasPendingArtifacts(sourcePath))
 0130      throw new InvalidOperationException("Plain-zu-encrypted-Migration kann nicht starten, solange alte Migrationsartef
 131
 10132    return new PlainToEncryptedVaultMigrationPlan(
 10133      sourcePath,
 10134      PlainToEncryptedVaultMigrationArtifacts.GetEncryptedTempPath(sourcePath),
 10135      PlainToEncryptedVaultMigrationArtifacts.GetPlainBackupPath(sourcePath),
 10136      request.TargetMode);
 10137  }
 138
 139  internal VaultHeader MarkInProgress(VaultHeader header, VaultStorageMigrationTargetMode targetMode, DateTime utcNow)
 11140  {
 11141    var updated = CloneHeader(header);
 11142    updated.StorageMigrationState = VaultStorageMigrationState.InProgress;
 11143    updated.StorageMigrationTargetMode = targetMode;
 11144    updated.LastStorageMigrationAttemptUtc = utcNow;
 11145    updated.LastStorageMigrationError = null;
 11146    updated.UpdatedAt = utcNow;
 11147    return updated;
 11148  }
 149
 150  internal VaultHeader MarkFinalizationPending(VaultHeader header, VaultStorageMigrationTargetMode targetMode, DateTime 
 7151  {
 7152    var updated = CloneHeader(header);
 7153    updated.StorageMigrationState = VaultStorageMigrationState.FinalizationPending;
 7154    updated.StorageMigrationTargetMode = targetMode;
 7155    updated.LastStorageMigrationAttemptUtc = utcNow;
 7156    updated.LastStorageMigrationError = null;
 7157    updated.UpdatedAt = utcNow;
 7158    return updated;
 7159  }
 160
 161  internal VaultHeader MarkFailed(VaultHeader header, VaultStorageMigrationTargetMode targetMode, DateTime utcNow, strin
 6162  {
 6163    var updated = CloneHeader(header);
 6164    updated.StorageMigrationState = VaultStorageMigrationState.Failed;
 6165    updated.StorageMigrationTargetMode = targetMode;
 6166    updated.LastStorageMigrationAttemptUtc = utcNow;
 6167    updated.LastStorageMigrationError = error;
 6168    updated.UpdatedAt = utcNow;
 6169    return updated;
 6170  }
 171
 172  internal VaultHeader ClearMigrationState(VaultHeader header, DateTime utcNow)
 6173  {
 6174    var updated = CloneHeader(header);
 6175    updated.StorageMigrationState = VaultStorageMigrationState.None;
 6176    updated.StorageMigrationTargetMode = VaultStorageMigrationTargetMode.None;
 6177    updated.LastStorageMigrationAttemptUtc = null;
 6178    updated.LastStorageMigrationError = null;
 6179    updated.UpdatedAt = utcNow;
 6180    return updated;
 6181  }
 182
 183  internal PlainToEncryptedVaultMigrationRecoveryDecision GetRecoveryDecision(VaultHeader header)
 5184    => PlainToEncryptedVaultMigrationArtifacts.Decide(_sourceConnectionFactory.Storage.DatabasePath, header);
 185
 186  private void ValidateSettingsState()
 13187  {
 13188    using var connection = _sourceConnectionFactory.OpenConnection();
 13189    RequireTable(connection, "MasterKey");
 13190    RequireTable(connection, "Credentials");
 13191    RequireTable(connection, "Settings");
 192
 12193    using var masterKeyCount = connection.CreateCommand();
 12194    masterKeyCount.CommandText = "SELECT COUNT(*) FROM MasterKey;";
 12195    if (Convert.ToInt64(masterKeyCount.ExecuteScalar() ?? 0L) != 1)
 0196      throw new InvalidOperationException("Plain-zu-encrypted-Migration erwartet genau einen MasterKey-Datensatz.");
 24197  }
 198
 199  private static void RequireTable(SqliteConnection connection, string tableName)
 39200  {
 39201    using var command = connection.CreateCommand();
 39202    command.CommandText = "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name=$name;";
 39203    command.Parameters.AddWithValue("$name", tableName);
 39204    if (Convert.ToInt64(command.ExecuteScalar() ?? 0L) <= 0)
 1205      throw new InvalidOperationException($"Plain-zu-encrypted-Migration erwartet die Tabelle '{tableName}'.");
 76206  }
 207
 208  private static void ValidateHeader(VaultHeader header)
 16209  {
 16210    if (header.FormatVersion != VaultHeaderFormatVersion.Current)
 0211      throw new InvalidOperationException("Plain-zu-encrypted-Migration erwartet einen aktuellen VaultHeader.");
 212
 16213    if (header.UsesLegacyKeyMaterial)
 0214      throw new InvalidOperationException("Plain-zu-encrypted-Migration darf nicht mit Legacy-Keymaterial starten.");
 215
 16216    if (header.LegacyPasswordHash.Length > 0)
 1217      throw new InvalidOperationException("Plain-zu-encrypted-Migration erwartet keinen Legacy-Passwort-Hash mehr.");
 218
 15219    if (header.StorageMigrationState is not VaultStorageMigrationState.None and not VaultStorageMigrationState.Failed)
 2220      throw new InvalidOperationException("Plain-zu-encrypted-Migration darf nicht parallel zu einer bereits laufenden S
 13221  }
 222
 223  private static void ValidateCredential(CredentialRecord credential)
 12224  {
 12225    if (credential.SecretFormatVersion != CredentialSecretFormatVersion.Current)
 0226      throw new InvalidOperationException($"Credential {credential.Id} verwendet noch ein Legacy-Secret-Format.");
 227
 12228    if (credential.MetadataFormatVersion != CredentialMetadataFormatVersion.Current)
 0229      throw new InvalidOperationException($"Credential {credential.Id} verwendet noch ein Legacy-Metadaten-Format.");
 230
 12231    if (!Guid.TryParseExact(credential.CredentialUuid, "N", out _))
 1232      throw new InvalidOperationException($"Credential {credential.Id} enthaelt eine ungueltige CredentialUuid.");
 233
 11234    if (credential.EncryptedPassword.Length == 0)
 0235      throw new InvalidOperationException($"Credential {credential.Id} enthaelt keine verschluesselten Secret-Daten.");
 236
 11237    if (credential.EncryptedMetadata.Length == 0)
 0238      throw new InvalidOperationException($"Credential {credential.Id} enthaelt keine verschluesselten Metadaten.");
 239
 11240    if (!string.IsNullOrEmpty(credential.Title) ||
 11241      !string.IsNullOrEmpty(credential.Username) ||
 11242      !string.IsNullOrEmpty(credential.Url) ||
 11243      !string.IsNullOrEmpty(credential.Notes) ||
 11244      !string.IsNullOrEmpty(credential.IconKey) ||
 11245      credential.CredentialType != CredentialType.Password)
 1246    {
 1247      throw new InvalidOperationException($"Credential {credential.Id} enthaelt unerwartete Klartext-Metadaten.");
 248    }
 10249  }
 250
 30251  private static VaultHeader CloneHeader(VaultHeader header) => new()
 30252  {
 30253    FormatVersion = header.FormatVersion,
 30254    KdfIdentifier = header.KdfIdentifier,
 30255    KdfParameters = new VaultKdfParameters
 30256    {
 30257      HashAlgorithm = header.KdfParameters.HashAlgorithm,
 30258      Iterations = header.KdfParameters.Iterations,
 30259      KeyLengthBytes = header.KdfParameters.KeyLengthBytes,
 30260      SaltLengthBytes = header.KdfParameters.SaltLengthBytes,
 30261    },
 30262    Salt = header.Salt.ToArray(),
 30263    WrappedVaultKey = header.WrappedVaultKey.ToArray(),
 30264    LegacyPasswordHash = header.LegacyPasswordHash.ToArray(),
 30265    UsesLegacyKeyMaterial = header.UsesLegacyKeyMaterial,
 30266    RequiresStorageCompaction = header.RequiresStorageCompaction,
 30267    LastStorageCompactionAttemptUtc = header.LastStorageCompactionAttemptUtc,
 30268    LastStorageCompactionFailureKind = header.LastStorageCompactionFailureKind,
 30269    LastStorageCompactionError = header.LastStorageCompactionError,
 30270    StorageMigrationState = header.StorageMigrationState,
 30271    StorageMigrationTargetMode = header.StorageMigrationTargetMode,
 30272    LastStorageMigrationAttemptUtc = header.LastStorageMigrationAttemptUtc,
 30273    LastStorageMigrationError = header.LastStorageMigrationError,
 30274    CreatedAt = header.CreatedAt,
 30275    UpdatedAt = header.UpdatedAt,
 30276  };
 277}
 278
 279internal sealed record PlainToEncryptedVaultMigrationRequest(
 280  VaultHeader Header,
 281  IReadOnlyList<CredentialRecord> Credentials,
 282  VaultStorageMigrationTargetMode TargetMode,
 283  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);