< Summary

Information
Class: LOCKnet.Core.Security.MasterKeyManager
Assembly: LOCKnet.Core
File(s): /home/runner/work/LOCKnet/LOCKnet/src/LOCKnet.Core/Security/MasterKeyManager.cs
Line coverage
94%
Covered lines: 458
Uncovered lines: 29
Coverable lines: 487
Total lines: 653
Line coverage: 94%
Branch coverage
83%
Covered branches: 134
Total branches: 160
Branch coverage: 83.7%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

File(s)

/home/runner/work/LOCKnet/LOCKnet/src/LOCKnet.Core/Security/MasterKeyManager.cs

#LineLine coverage
 1using LOCKnet.Core.Crypto;
 2using LOCKnet.Core.DataAbstractions;
 3using System.Security;
 4using System.Security.Cryptography;
 5using System.Text.Json;
 6
 7namespace LOCKnet.Core.Security;
 8
 9/// <summary>
 10/// Implementierung von <see cref="IMasterKeyManager"/>.
 11/// Delegiert Schlüsselableitung an <see cref="IKeyDerivationService"/> und
 12/// Persistenz an <see cref="IMasterKeyRepository"/>.
 13/// </summary>
 14public sealed class MasterKeyManager : IMasterKeyManager
 15{
 16  private const int WrappedVaultKeyPacketBytes = 60;
 217  private static readonly TimeSpan AutomaticStorageCompactionRetryDelay = TimeSpan.FromMinutes(10);
 18  private readonly IKeyDerivationService _kdf;
 19  private readonly IMasterKeyRepository _repo;
 20  private readonly IVaultMigrationRepository _vaultMigrationRepo;
 21  private readonly IEncryptionService _encryption;
 22  private readonly ICredentialEnvelopeService _credentialEnvelope;
 23  private readonly ISessionManager _session;
 24  private readonly ISecureStringService _secureStr;
 25
 26  /// <summary>
 27  /// Initialisiert eine neue Instanz von <see cref="MasterKeyManager"/>.
 28  /// </summary>
 6229  public MasterKeyManager(
 6230    IKeyDerivationService kdf,
 6231    IMasterKeyRepository repo,
 6232    IVaultMigrationRepository vaultMigrationRepo,
 6233    IEncryptionService encryption,
 6234    ICredentialEnvelopeService credentialEnvelope,
 6235    ISessionManager session,
 6236    ISecureStringService secureStr)
 6237  {
 6238    ArgumentNullException.ThrowIfNull(kdf);
 6239    ArgumentNullException.ThrowIfNull(repo);
 6240    ArgumentNullException.ThrowIfNull(vaultMigrationRepo);
 6241    ArgumentNullException.ThrowIfNull(encryption);
 6242    ArgumentNullException.ThrowIfNull(credentialEnvelope);
 6243    ArgumentNullException.ThrowIfNull(session);
 6244    ArgumentNullException.ThrowIfNull(secureStr);
 6245    _kdf = kdf;
 6246    _repo = repo;
 6247    _vaultMigrationRepo = vaultMigrationRepo;
 6248    _encryption = encryption;
 6249    _credentialEnvelope = credentialEnvelope;
 6250    _session = session;
 6251    _secureStr = secureStr;
 6252  }
 53
 54  /// <inheritdoc/>
 7055  public bool IsInitialized => _repo.Get() is not null;
 56
 57  /// <inheritdoc/>
 58  public void Initialize(SecureString password)
 4959  {
 4960    ArgumentNullException.ThrowIfNull(password);
 4861    if (IsInitialized)
 262      throw new InvalidOperationException("Master-Key ist bereits initialisiert.");
 63
 4664    var passwordBytes = _secureStr.ToByteArray(password);
 4665    byte[]? kek = null;
 4666    byte[]? vaultKey = null;
 67    try
 4668    {
 4669      var parameters = _kdf.GetDefaultParameters();
 4670      var salt = _kdf.GenerateSalt(parameters.SaltLengthBytes);
 4671      kek = _kdf.DeriveKey(passwordBytes, salt, parameters);
 4672      vaultKey = RandomNumberGenerator.GetBytes(32);
 4673      var wrappedVaultKey = _encryption.Encrypt(vaultKey, kek);
 74
 4675      _repo.Create(new VaultHeader
 4676      {
 4677        FormatVersion = VaultHeaderFormatVersion.Current,
 4678        KdfIdentifier = _kdf.Identifier,
 4679        KdfParameters = parameters,
 4680        Salt = salt,
 4681        WrappedVaultKey = wrappedVaultKey,
 4682        LegacyPasswordHash = [],
 4683        UsesLegacyKeyMaterial = false,
 4684        RequiresStorageCompaction = false,
 4685        CreatedAt = DateTime.UtcNow,
 4686        UpdatedAt = DateTime.UtcNow
 4687      });
 4688    }
 89    finally
 4690    {
 4691      if (kek is not null)
 4692        CryptographicOperations.ZeroMemory(kek);
 4693      if (vaultKey is not null)
 4694        CryptographicOperations.ZeroMemory(vaultKey);
 4695      _secureStr.ZeroMemory(passwordBytes);
 4696    }
 4697  }
 98
 99  /// <inheritdoc/>
 100  public UnlockResult? Unlock(SecureString password)
 53101  {
 53102    ArgumentNullException.ThrowIfNull(password);
 103
 52104    var record = _repo.Get();
 52105    if (record is null)
 3106      throw new InvalidOperationException("Kein Master-Key vorhanden. Bitte zuerst Initialize() aufrufen.");
 49107    ValidateHeader(record);
 108
 43109    var passwordBytes = _secureStr.ToByteArray(password);
 43110    byte[]? kek = null;
 43111    byte[]? currentVaultKey = null;
 43112    byte[]? targetVaultKey = null;
 43113    byte[]? resultKey = null;
 114    try
 43115    {
 43116      var parameters = record.KdfParameters;
 43117      kek = _kdf.DeriveKey(passwordBytes, record.Salt, parameters);
 118
 43119      if (record.WrappedVaultKey.Length > 0)
 38120      {
 121        try
 38122        {
 38123          currentVaultKey = _encryption.Decrypt(record.WrappedVaultKey, kek);
 30124        }
 8125        catch (CryptographicException)
 8126        {
 8127          return null;
 128        }
 129
 30130        var usesLegacyKey = record.UsesLegacyKeyMaterial ||
 30131          (record.FormatVersion < VaultHeaderFormatVersion.Current &&
 30132          CryptographicOperations.FixedTimeEquals(currentVaultKey, kek));
 133
 30134        var migration = BuildMigrationPlan(record, currentVaultKey, kek, usesLegacyKey);
 30135        if (migration is not null)
 2136        {
 2137          _vaultMigrationRepo.ApplyMigration(migration.Header, migration.Credentials);
 2138          var storageInfo = CompleteStorageCompactionIfRequired(migration.Header, migration.ActiveVaultKey, automaticRet
 2139          targetVaultKey = migration.ActiveVaultKey;
 2140          resultKey = targetVaultKey;
 2141          targetVaultKey = null;
 2142          return new UnlockResult { VaultKey = resultKey, StorageCompaction = storageInfo };
 143        }
 144
 28145        var currentStorageInfo = CompleteStorageCompactionIfRequired(record, currentVaultKey, automaticRetry: true);
 146
 28147        resultKey = currentVaultKey;
 28148        currentVaultKey = null;
 28149        return new UnlockResult { VaultKey = resultKey, StorageCompaction = currentStorageInfo };
 150      }
 151
 5152      if (record.LegacyPasswordHash.Length == 0 ||
 5153        !_kdf.VerifyPassword(passwordBytes, record.Salt, record.LegacyPasswordHash, parameters))
 1154      {
 1155        return null;
 156      }
 157
 4158      currentVaultKey = _kdf.DeriveKey(passwordBytes, record.Salt, parameters);
 4159      var legacyMigration = BuildMigrationPlan(record, currentVaultKey, kek, true)
 4160        ?? throw new InvalidOperationException("Legacy-Vault konnte nicht in das aktuelle Format migriert werden.");
 161
 4162      _vaultMigrationRepo.ApplyMigration(legacyMigration.Header, legacyMigration.Credentials);
 3163      var legacyStorageInfo = CompleteStorageCompactionIfRequired(legacyMigration.Header, legacyMigration.ActiveVaultKey
 3164      targetVaultKey = legacyMigration.ActiveVaultKey;
 3165      resultKey = targetVaultKey;
 3166      targetVaultKey = null;
 3167      return new UnlockResult { VaultKey = resultKey, StorageCompaction = legacyStorageInfo };
 168    }
 169    finally
 43170    {
 43171      if (kek is not null)
 43172        CryptographicOperations.ZeroMemory(kek);
 43173      if (currentVaultKey is not null)
 6174        CryptographicOperations.ZeroMemory(currentVaultKey);
 43175      if (targetVaultKey is not null)
 0176        CryptographicOperations.ZeroMemory(targetVaultKey);
 43177      _secureStr.ZeroMemory(passwordBytes);
 43178    }
 42179  }
 180
 181  /// <inheritdoc/>
 182  public void ChangePassword(SecureString currentPassword, SecureString newPassword)
 5183  {
 5184    ArgumentNullException.ThrowIfNull(currentPassword);
 5185    ArgumentNullException.ThrowIfNull(newPassword);
 186
 5187    var unlock = Unlock(currentPassword);
 4188    if (unlock is null)
 1189      throw new UnauthorizedAccessException("Das aktuelle Passwort ist falsch.");
 3190    var vaultKey = unlock.VaultKey;
 191
 3192    var record = _repo.Get();
 3193    if (record is null)
 0194      throw new InvalidOperationException("Kein Master-Key vorhanden.");
 195
 3196    var newBytes = _secureStr.ToByteArray(newPassword);
 3197    byte[]? newKek = null;
 198    try
 3199    {
 3200      var parameters = _kdf.GetDefaultParameters();
 3201      var newSalt = _kdf.GenerateSalt(parameters.SaltLengthBytes);
 3202      newKek = _kdf.DeriveKey(newBytes, newSalt, parameters);
 3203      var wrappedVaultKey = _encryption.Encrypt(vaultKey, newKek);
 204
 3205      _repo.Update(new VaultHeader
 3206      {
 3207        FormatVersion = VaultHeaderFormatVersion.Current,
 3208        KdfIdentifier = _kdf.Identifier,
 3209        KdfParameters = parameters,
 3210        Salt = newSalt,
 3211        WrappedVaultKey = wrappedVaultKey,
 3212        LegacyPasswordHash = [],
 3213        UsesLegacyKeyMaterial = false,
 3214        RequiresStorageCompaction = record.RequiresStorageCompaction,
 3215        LastStorageCompactionAttemptUtc = record.LastStorageCompactionAttemptUtc,
 3216        LastStorageCompactionFailureKind = record.LastStorageCompactionFailureKind,
 3217        LastStorageCompactionError = record.LastStorageCompactionError,
 3218        CreatedAt = record.CreatedAt,
 3219        UpdatedAt = DateTime.UtcNow
 3220      });
 3221    }
 222    finally
 3223    {
 3224      CryptographicOperations.ZeroMemory(vaultKey);
 3225      if (newKek is not null)
 3226        CryptographicOperations.ZeroMemory(newKek);
 3227      _secureStr.ZeroMemory(newBytes);
 3228    }
 3229  }
 230
 231  public StorageCompactionInfo GetStorageCompactionInfo()
 14232  {
 14233    var header = _repo.Get();
 14234    return header is null ? BuildNoPendingStorageInfo() : BuildStorageCompactionInfo(header, autoRetryDeferred: false, o
 14235  }
 236
 237  public StorageCompactionInfo RetryPendingStorageCompaction()
 7238  {
 7239    var header = _repo.Get()
 7240      ?? throw new InvalidOperationException("Kein Master-Key vorhanden.");
 241
 7242    ValidateHeader(header);
 243
 7244    var sessionKey = _session.GetSessionKey();
 7245    if (sessionKey is null)
 1246    {
 1247      var info = BuildStorageCompactionInfo(header, autoRetryDeferred: false, overrideMessage: "Speicherbereinigung noch
 1248      return new StorageCompactionInfo
 1249      {
 1250        IsPending = info.IsPending,
 1251        AutoRetryDeferred = info.AutoRetryDeferred,
 1252        LastAttemptUtc = info.LastAttemptUtc,
 1253        NextAutomaticRetryUtc = info.NextAutomaticRetryUtc,
 1254        FailureKind = StorageCompactionFailureKind.BusyOrLocked,
 1255        UserMessage = info.UserMessage,
 1256        LastError = info.LastError,
 1257      };
 258    }
 259
 260    try
 6261    {
 6262      return CompleteStorageCompactionIfRequired(header, sessionKey, automaticRetry: false);
 263    }
 264    finally
 6265    {
 6266      CryptographicOperations.ZeroMemory(sessionKey);
 6267    }
 7268  }
 269
 270  private CredentialMigrationPlan? BuildMigrationPlan(VaultHeader header, byte[] currentVaultKey, byte[] kek, bool usesL
 34271  {
 34272    var credentials = _vaultMigrationRepo.GetAllCredentials();
 34273    var migratedCredentials = new List<CredentialRecord>();
 34274    var targetVaultKey = usesLegacyKey ? RandomNumberGenerator.GetBytes(32) : currentVaultKey.ToArray();
 34275    var requiresStorageCompaction = header.RequiresStorageCompaction;
 34276    var headerNeedsUpgrade = header.FormatVersion != VaultHeaderFormatVersion.Current ||
 34277      header.UsesLegacyKeyMaterial != usesLegacyKey ||
 34278      header.LegacyPasswordHash.Length > 0 ||
 34279      header.WrappedVaultKey.Length != WrappedVaultKeyPacketBytes;
 280
 281    try
 34282    {
 130283      foreach (var credential in credentials)
 14284      {
 14285        if (!NeedsCredentialMigration(credential, usesLegacyKey))
 8286          continue;
 287
 6288        var needsSecretMigration = NeedsSecretMigration(credential) || usesLegacyKey;
 6289        var needsMetadataMigration = NeedsMetadataMigration(credential) || usesLegacyKey;
 6290        requiresStorageCompaction |= HasPlaintextMetadataResidue(credential);
 6291        byte[]? secretPlaintext = null;
 6292        CredentialRecord? metadataRecord = null;
 293        try
 6294        {
 6295          var migrated = CloneCredential(credential);
 6296          migrated.CredentialUuid = EnsureCredentialUuid(migrated.CredentialUuid);
 297
 6298          if (needsSecretMigration)
 6299          {
 6300            secretPlaintext = DecryptSecretForMigration(credential, currentVaultKey, header.FormatVersion);
 6301            migrated.SecretFormatVersion = CredentialSecretFormatVersion.Current;
 6302            migrated.EncryptedPassword = _credentialEnvelope.Encrypt(secretPlaintext, targetVaultKey, migrated, VaultHea
 6303          }
 304
 6305          if (needsMetadataMigration)
 6306          {
 6307            metadataRecord = DecryptMetadataForMigration(credential, currentVaultKey, header.FormatVersion);
 6308            migrated.Title = metadataRecord.Title;
 6309            migrated.Username = metadataRecord.Username;
 6310            migrated.Url = metadataRecord.Url;
 6311            migrated.Notes = metadataRecord.Notes;
 6312            migrated.IconKey = metadataRecord.IconKey;
 6313            migrated.CredentialType = metadataRecord.CredentialType;
 6314            migrated.MetadataFormatVersion = _credentialEnvelope.CurrentMetadataVersion;
 6315            migrated.EncryptedMetadata = _credentialEnvelope.EncryptMetadata(migrated, targetVaultKey, VaultHeaderFormat
 6316            migrated = SanitizePersistedMetadata(migrated);
 6317          }
 318
 6319          migrated.UpdatedAt = DateTime.UtcNow;
 6320          migratedCredentials.Add(migrated);
 6321        }
 322        finally
 6323        {
 6324          if (secretPlaintext is not null)
 6325            CryptographicOperations.ZeroMemory(secretPlaintext);
 6326          if (metadataRecord is not null)
 6327            metadataRecord.EncryptedMetadata = [];
 6328        }
 6329      }
 330
 34331      if (!headerNeedsUpgrade && migratedCredentials.Count == 0)
 28332      {
 28333        CryptographicOperations.ZeroMemory(targetVaultKey);
 28334        return null;
 335      }
 336
 6337      var migratedHeader = CloneHeader(header);
 6338      migratedHeader.FormatVersion = VaultHeaderFormatVersion.Current;
 6339      migratedHeader.KdfIdentifier = _kdf.Identifier;
 6340      migratedHeader.KdfParameters = CloneParameters(header.KdfParameters);
 6341      migratedHeader.LegacyPasswordHash = [];
 6342      migratedHeader.WrappedVaultKey = _encryption.Encrypt(targetVaultKey, kek);
 6343      migratedHeader.UsesLegacyKeyMaterial = false;
 6344      migratedHeader.RequiresStorageCompaction = requiresStorageCompaction;
 6345      migratedHeader.LastStorageCompactionAttemptUtc = null;
 6346      migratedHeader.LastStorageCompactionFailureKind = StorageCompactionFailureKind.None;
 6347      migratedHeader.LastStorageCompactionError = null;
 6348      migratedHeader.UpdatedAt = DateTime.UtcNow;
 349
 6350      return new CredentialMigrationPlan(migratedHeader, migratedCredentials, targetVaultKey);
 351    }
 0352    catch
 0353    {
 0354      CryptographicOperations.ZeroMemory(targetVaultKey);
 0355      throw;
 356    }
 34357  }
 358
 359  private byte[] DecryptForMigration(CredentialRecord credential, byte[] currentVaultKey, int vaultFormatVersion)
 0360    => DecryptSecretForMigration(credential, currentVaultKey, vaultFormatVersion);
 361
 362  private byte[] DecryptSecretForMigration(CredentialRecord credential, byte[] currentVaultKey, int vaultFormatVersion)
 6363    => credential.SecretFormatVersion switch
 6364    {
 6365      CredentialSecretFormatVersion.Legacy => _encryption.Decrypt(credential.EncryptedPassword, currentVaultKey),
 0366      CredentialSecretFormatVersion.AesGcmV1 => _credentialEnvelope.Decrypt(credential, currentVaultKey, vaultFormatVers
 0367      CredentialSecretFormatVersion.AesGcmV2 => _credentialEnvelope.Decrypt(credential, currentVaultKey, vaultFormatVers
 0368      _ => throw new InvalidOperationException($"Nicht unterstuetzte Secret-Formatversion: {credential.SecretFormatVersi
 6369    };
 370
 371  private CredentialRecord DecryptMetadataForMigration(CredentialRecord credential, byte[] currentVaultKey, int vaultFor
 6372    => credential.MetadataFormatVersion switch
 6373    {
 6374      CredentialMetadataFormatVersion.Legacy => CloneCredential(credential),
 0375      CredentialMetadataFormatVersion.AesGcmV1 => _credentialEnvelope.DecryptMetadata(credential, currentVaultKey, vault
 0376      _ => throw new InvalidOperationException($"Nicht unterstuetzte Metadaten-Formatversion: {credential.MetadataFormat
 6377    };
 378
 379  private void ValidateHeader(VaultHeader header)
 56380  {
 56381    ArgumentNullException.ThrowIfNull(header);
 382
 56383    if (header.FormatVersion < VaultHeaderFormatVersion.Legacy || header.FormatVersion > VaultHeaderFormatVersion.Curren
 1384      throw new InvalidOperationException($"Nicht unterstuetzte VaultHeader-Version: {header.FormatVersion}");
 385
 55386    if (!string.Equals(header.KdfIdentifier, _kdf.Identifier, StringComparison.Ordinal))
 1387      throw new InvalidOperationException($"Nicht unterstuetzter KDF-Identifier: {header.KdfIdentifier}");
 388
 54389    if (header.KdfParameters is null)
 0390      throw new InvalidOperationException("VaultHeader enthaelt keine KDF-Parameter.");
 391
 54392    _kdf.ValidateParameters(header.KdfParameters);
 393
 54394    if (header.Salt.Length != header.KdfParameters.SaltLengthBytes)
 1395      throw new InvalidOperationException("VaultHeader enthaelt einen ungueltigen Salt.");
 396
 53397    if (header.FormatVersion == VaultHeaderFormatVersion.Legacy)
 7398    {
 7399      if (header.WrappedVaultKey.Length != 0)
 1400        throw new InvalidOperationException("Legacy-VaultHeader darf keinen WrappedVaultKey enthalten.");
 401
 6402      if (header.LegacyPasswordHash.Length == 0)
 1403        throw new InvalidOperationException("Legacy-VaultHeader enthaelt keinen Passwort-Hash.");
 404
 5405      return;
 406    }
 407
 46408    if (header.WrappedVaultKey.Length != WrappedVaultKeyPacketBytes)
 1409      throw new InvalidOperationException("VaultHeader enthaelt einen ungueltigen WrappedVaultKey.");
 50410  }
 411
 412  private StorageCompactionInfo CompleteStorageCompactionIfRequired(VaultHeader header, byte[] activeVaultKey, bool auto
 39413  {
 39414    if (!header.RequiresStorageCompaction)
 25415      return BuildNoPendingStorageInfo();
 416
 14417    var now = DateTime.UtcNow;
 14418    if (automaticRetry && header.LastStorageCompactionAttemptUtc is DateTime lastAttemptUtc)
 3419    {
 3420      var nextRetryUtc = lastAttemptUtc + AutomaticStorageCompactionRetryDelay;
 3421      if (nextRetryUtc > now)
 3422        return BuildStorageCompactionInfo(header, autoRetryDeferred: true, overrideMessage: $"Speicherbereinigung noch o
 0423    }
 424
 11425    if (!_vaultMigrationRepo.HasPendingStorageArtifacts())
 11426    {
 11427      var validationFailure = ValidateStorageRewriteReadiness(header, activeVaultKey);
 11428      if (validationFailure is not null)
 1429        return PersistStorageCompactionFailure(header, validationFailure.Value.failureKind, validationFailure.Value.user
 10430    }
 431
 10432    var result = _vaultMigrationRepo.CompactStorage();
 10433    var updated = CloneHeader(header);
 10434    updated.LastStorageCompactionAttemptUtc = now;
 10435    updated.LastStorageCompactionFailureKind = result.FailureKind;
 10436    updated.LastStorageCompactionError = result.LastError;
 437
 10438    if (!result.IsPending)
 7439    {
 7440      updated.RequiresStorageCompaction = false;
 7441      updated.LastStorageCompactionAttemptUtc = null;
 7442      updated.LastStorageCompactionFailureKind = StorageCompactionFailureKind.None;
 7443      updated.LastStorageCompactionError = null;
 7444      updated.UpdatedAt = DateTime.UtcNow;
 7445      _repo.Update(updated);
 7446      return result;
 447    }
 448
 3449    updated.RequiresStorageCompaction = true;
 3450    updated.UpdatedAt = DateTime.UtcNow;
 3451    _repo.Update(updated);
 3452    return BuildStorageCompactionInfo(updated, autoRetryDeferred: false, overrideMessage: result.UserMessage);
 39453  }
 454
 455  private (StorageCompactionFailureKind failureKind, string userMessage, string lastError)? ValidateStorageRewriteReadin
 11456  {
 457    try
 11458    {
 11459      ValidateHeaderForStorageRewrite(header);
 50460      foreach (var credential in _vaultMigrationRepo.GetAllCredentials())
 9461        ValidateCredentialForStorageRewrite(credential, activeVaultKey, header.FormatVersion);
 462
 10463      return null;
 464    }
 0465    catch (CryptographicException ex)
 0466    {
 0467      return (StorageCompactionFailureKind.Corruption, "Speicherbereinigung noch offen: Aktuelle Vault-Daten konnten nic
 468    }
 1469    catch (InvalidOperationException ex)
 1470    {
 1471      return (StorageCompactionFailureKind.Corruption, "Speicherbereinigung noch offen: Aktuelle Vault-Daten sind inkons
 472    }
 0473    catch (JsonException ex)
 0474    {
 0475      return (StorageCompactionFailureKind.Corruption, "Speicherbereinigung noch offen: Aktuelle Vault-Metadaten sind in
 476    }
 11477  }
 478
 479  private StorageCompactionInfo PersistStorageCompactionFailure(VaultHeader header, StorageCompactionFailureKind failure
 1480  {
 1481    var updated = CloneHeader(header);
 1482    updated.RequiresStorageCompaction = true;
 1483    updated.LastStorageCompactionAttemptUtc = DateTime.UtcNow;
 1484    updated.LastStorageCompactionFailureKind = failureKind;
 1485    updated.LastStorageCompactionError = lastError;
 1486    updated.UpdatedAt = DateTime.UtcNow;
 1487    _repo.Update(updated);
 1488    return BuildStorageCompactionInfo(updated, autoRetryDeferred: false, overrideMessage: userMessage);
 1489  }
 490
 491  private void ValidateHeaderForStorageRewrite(VaultHeader header)
 11492  {
 11493    if (header.FormatVersion != VaultHeaderFormatVersion.Current)
 0494      throw new InvalidOperationException("Storage-Rewrite erwartet einen aktuellen VaultHeader.");
 495
 11496    if (header.UsesLegacyKeyMaterial)
 0497      throw new InvalidOperationException("Storage-Rewrite darf nicht mit Legacy-Keymaterial ausgefuehrt werden.");
 498
 11499    if (header.LegacyPasswordHash.Length > 0)
 0500      throw new InvalidOperationException("Storage-Rewrite erwartet keinen Legacy-Passwort-Hash mehr im Header.");
 11501  }
 502
 503  private void ValidateCredentialForStorageRewrite(CredentialRecord credential, byte[] activeVaultKey, int vaultFormatVe
 9504  {
 9505    if (credential.SecretFormatVersion != CredentialSecretFormatVersion.Current)
 0506      throw new InvalidOperationException($"Credential {credential.Id} verwendet noch ein Legacy-Secret-Format.");
 507
 9508    if (credential.MetadataFormatVersion != CredentialMetadataFormatVersion.Current)
 0509      throw new InvalidOperationException($"Credential {credential.Id} verwendet noch ein Legacy-Metadaten-Format.");
 510
 9511    if (!Guid.TryParseExact(credential.CredentialUuid, "N", out _))
 0512      throw new InvalidOperationException($"Credential {credential.Id} enthaelt eine ungueltige CredentialUuid.");
 513
 9514    if (credential.EncryptedPassword.Length == 0)
 0515      throw new InvalidOperationException($"Credential {credential.Id} enthaelt keine verschluesselten Secret-Daten.");
 516
 9517    if (credential.EncryptedMetadata.Length == 0)
 0518      throw new InvalidOperationException($"Credential {credential.Id} enthaelt keine verschluesselten Metadaten.");
 519
 9520    if (HasPlaintextMetadataResidue(credential))
 0521      throw new InvalidOperationException($"Credential {credential.Id} enthaelt unerwartete Klartext-Metadaten.");
 522
 9523    var secret = _credentialEnvelope.Decrypt(credential, activeVaultKey, vaultFormatVersion);
 524    try
 8525    {
 8526    }
 527    finally
 8528    {
 8529      CryptographicOperations.ZeroMemory(secret);
 8530    }
 531
 8532    _ = _credentialEnvelope.DecryptMetadata(credential, activeVaultKey, vaultFormatVersion);
 8533  }
 534
 535  private static bool NeedsCredentialMigration(CredentialRecord credential, bool usesLegacyKey)
 14536    => usesLegacyKey || NeedsSecretMigration(credential) || NeedsMetadataMigration(credential);
 537
 538  private static bool NeedsSecretMigration(CredentialRecord credential)
 15539    => credential.SecretFormatVersion != CredentialSecretFormatVersion.Current || !Guid.TryParseExact(credential.Credent
 540
 541  private static bool NeedsMetadataMigration(CredentialRecord credential)
 14542    => credential.MetadataFormatVersion != CredentialMetadataFormatVersion.Current ||
 14543      !Guid.TryParseExact(credential.CredentialUuid, "N", out _) ||
 14544      HasPlaintextMetadataResidue(credential);
 545
 546  private static bool HasPlaintextMetadataResidue(CredentialRecord credential)
 23547    => !string.IsNullOrEmpty(credential.Title) ||
 23548      !string.IsNullOrEmpty(credential.Username) ||
 23549      !string.IsNullOrEmpty(credential.Url) ||
 23550      !string.IsNullOrEmpty(credential.Notes) ||
 23551      !string.IsNullOrEmpty(credential.IconKey) ||
 23552      credential.CredentialType != CredentialType.Password;
 553
 554  private static string EnsureCredentialUuid(string credentialUuid)
 6555    => Guid.TryParseExact(credentialUuid, "N", out _) ? credentialUuid : Guid.NewGuid().ToString("N");
 556
 23557  private static VaultKdfParameters CloneParameters(VaultKdfParameters parameters) => new()
 23558  {
 23559    HashAlgorithm = parameters.HashAlgorithm,
 23560    Iterations = parameters.Iterations,
 23561    KeyLengthBytes = parameters.KeyLengthBytes,
 23562    SaltLengthBytes = parameters.SaltLengthBytes,
 23563  };
 564
 12565  private static CredentialRecord CloneCredential(CredentialRecord credential) => new()
 12566  {
 12567    Id = credential.Id,
 12568    Title = credential.Title,
 12569    Username = credential.Username,
 12570    EncryptedPassword = credential.EncryptedPassword.ToArray(),
 12571    EncryptedMetadata = credential.EncryptedMetadata.ToArray(),
 12572    CredentialUuid = credential.CredentialUuid,
 12573    SecretFormatVersion = credential.SecretFormatVersion,
 12574    MetadataFormatVersion = credential.MetadataFormatVersion,
 12575    Url = credential.Url,
 12576    Notes = credential.Notes,
 12577    CreatedAt = credential.CreatedAt,
 12578    UpdatedAt = credential.UpdatedAt,
 12579    IconKey = credential.IconKey,
 12580    CredentialType = credential.CredentialType,
 12581  };
 582
 583  private static CredentialRecord SanitizePersistedMetadata(CredentialRecord credential)
 6584  {
 6585    credential.Title = string.Empty;
 6586    credential.Username = null;
 6587    credential.Url = null;
 6588    credential.Notes = null;
 6589    credential.IconKey = null;
 6590    credential.CredentialType = CredentialType.Password;
 6591    return credential;
 6592  }
 593
 17594  private static VaultHeader CloneHeader(VaultHeader header) => new()
 17595  {
 17596    FormatVersion = header.FormatVersion,
 17597    KdfIdentifier = header.KdfIdentifier,
 17598    KdfParameters = CloneParameters(header.KdfParameters),
 17599    Salt = header.Salt.ToArray(),
 17600    WrappedVaultKey = header.WrappedVaultKey.ToArray(),
 17601    LegacyPasswordHash = header.LegacyPasswordHash.ToArray(),
 17602    UsesLegacyKeyMaterial = header.UsesLegacyKeyMaterial,
 17603    RequiresStorageCompaction = header.RequiresStorageCompaction,
 17604    LastStorageCompactionAttemptUtc = header.LastStorageCompactionAttemptUtc,
 17605    LastStorageCompactionFailureKind = header.LastStorageCompactionFailureKind,
 17606    LastStorageCompactionError = header.LastStorageCompactionError,
 17607    CreatedAt = header.CreatedAt,
 17608    UpdatedAt = header.UpdatedAt,
 17609  };
 610
 33611  private static StorageCompactionInfo BuildNoPendingStorageInfo() => new()
 33612  {
 33613    IsPending = false,
 33614    FailureKind = StorageCompactionFailureKind.None,
 33615    UserMessage = string.Empty,
 33616  };
 617
 618  private static StorageCompactionInfo BuildStorageCompactionInfo(VaultHeader header, bool autoRetryDeferred, string? ov
 22619  {
 22620    if (!header.RequiresStorageCompaction)
 8621      return BuildNoPendingStorageInfo();
 622
 14623    DateTime? nextRetryUtc = header.LastStorageCompactionAttemptUtc is DateTime lastAttemptUtc
 14624      ? lastAttemptUtc + AutomaticStorageCompactionRetryDelay
 14625      : null;
 14626    var message = overrideMessage;
 14627    if (string.IsNullOrWhiteSpace(message))
 6628    {
 6629      message = header.LastStorageCompactionFailureKind switch
 6630      {
 1631        StorageCompactionFailureKind.None => "Speicherbereinigung steht noch aus. Die Vault ist entsperrt, aber alte Spe
 1632        StorageCompactionFailureKind.BusyOrLocked => "Speicherbereinigung noch offen: Die Vault-Datei oder ein Rewrite-A
 1633        StorageCompactionFailureKind.InsufficientSpace => "Speicherbereinigung noch offen: Fuer den Vault-Rewrite ist ni
 1634        StorageCompactionFailureKind.Io => "Speicherbereinigung noch offen: Beim Rewrite der Vault-Datei ist ein I/O-Feh
 1635        StorageCompactionFailureKind.Corruption => "Speicherbereinigung noch offen: Aktuelle Vault-Daten sind inkonsiste
 1636        _ => "Speicherbereinigung noch offen: Der Vault-Rewrite konnte nicht abgeschlossen werden.",
 6637      };
 6638    }
 639
 14640    return new StorageCompactionInfo
 14641    {
 14642      IsPending = true,
 14643      AutoRetryDeferred = autoRetryDeferred,
 14644      LastAttemptUtc = header.LastStorageCompactionAttemptUtc,
 14645      NextAutomaticRetryUtc = nextRetryUtc,
 14646      FailureKind = header.LastStorageCompactionFailureKind,
 14647      LastError = header.LastStorageCompactionError,
 14648      UserMessage = message,
 14649    };
 22650  }
 651
 33652  private sealed record CredentialMigrationPlan(VaultHeader Header, IReadOnlyList<CredentialRecord> Credentials, byte[] 
 653}

Methods/Properties

.cctor()
.ctor(LOCKnet.Core.Crypto.IKeyDerivationService,LOCKnet.Core.DataAbstractions.IMasterKeyRepository,LOCKnet.Core.DataAbstractions.IVaultMigrationRepository,LOCKnet.Core.Crypto.IEncryptionService,LOCKnet.Core.Crypto.ICredentialEnvelopeService,LOCKnet.Core.Security.ISessionManager,LOCKnet.Core.Crypto.ISecureStringService)
get_IsInitialized()
Initialize(System.Security.SecureString)
Unlock(System.Security.SecureString)
ChangePassword(System.Security.SecureString,System.Security.SecureString)
GetStorageCompactionInfo()
RetryPendingStorageCompaction()
BuildMigrationPlan(LOCKnet.Core.DataAbstractions.VaultHeader,System.Byte[],System.Byte[],System.Boolean)
DecryptForMigration(LOCKnet.Core.DataAbstractions.CredentialRecord,System.Byte[],System.Int32)
DecryptSecretForMigration(LOCKnet.Core.DataAbstractions.CredentialRecord,System.Byte[],System.Int32)
DecryptMetadataForMigration(LOCKnet.Core.DataAbstractions.CredentialRecord,System.Byte[],System.Int32)
ValidateHeader(LOCKnet.Core.DataAbstractions.VaultHeader)
CompleteStorageCompactionIfRequired(LOCKnet.Core.DataAbstractions.VaultHeader,System.Byte[],System.Boolean)
ValidateStorageRewriteReadiness(LOCKnet.Core.DataAbstractions.VaultHeader,System.Byte[])
PersistStorageCompactionFailure(LOCKnet.Core.DataAbstractions.VaultHeader,LOCKnet.Core.DataAbstractions.StorageCompactionFailureKind,System.String,System.String)
ValidateHeaderForStorageRewrite(LOCKnet.Core.DataAbstractions.VaultHeader)
ValidateCredentialForStorageRewrite(LOCKnet.Core.DataAbstractions.CredentialRecord,System.Byte[],System.Int32)
NeedsCredentialMigration(LOCKnet.Core.DataAbstractions.CredentialRecord,System.Boolean)
NeedsSecretMigration(LOCKnet.Core.DataAbstractions.CredentialRecord)
NeedsMetadataMigration(LOCKnet.Core.DataAbstractions.CredentialRecord)
HasPlaintextMetadataResidue(LOCKnet.Core.DataAbstractions.CredentialRecord)
EnsureCredentialUuid(System.String)
CloneParameters(LOCKnet.Core.DataAbstractions.VaultKdfParameters)
CloneCredential(LOCKnet.Core.DataAbstractions.CredentialRecord)
SanitizePersistedMetadata(LOCKnet.Core.DataAbstractions.CredentialRecord)
CloneHeader(LOCKnet.Core.DataAbstractions.VaultHeader)
BuildNoPendingStorageInfo()
BuildStorageCompactionInfo(LOCKnet.Core.DataAbstractions.VaultHeader,System.Boolean,System.String)
get_Header()