< Summary

Information
Class: LOCKnet.Core.Services.CredentialService
Assembly: LOCKnet.Core
File(s): /home/runner/work/LOCKnet/LOCKnet/src/LOCKnet.Core/Services/CredentialService.cs
Line coverage
97%
Covered lines: 193
Uncovered lines: 4
Coverable lines: 197
Total lines: 280
Line coverage: 97.9%
Branch coverage
85%
Covered branches: 29
Total branches: 34
Branch coverage: 85.2%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
Add(...)100%11100%
GetAll()100%11100%
GetPassword(...)100%22100%
Update(...)100%44100%
Remove(...)100%11100%
RequireSessionKey()100%22100%
RequireUnlocked()100%22100%
RequireCurrentHeader()50%4485.71%
MaterializeRecord(...)100%11100%
ToPersistedRecord(...)100%11100%
EnsureCredentialUuid(...)50%22100%
ValidatePersistedCurrentRecord(...)88.88%201881.25%

File(s)

/home/runner/work/LOCKnet/LOCKnet/src/LOCKnet.Core/Services/CredentialService.cs

#LineLine coverage
 1using LOCKnet.Core.Crypto;
 2using LOCKnet.Core.DataAbstractions;
 3using LOCKnet.Core.Security;
 4using System.Security;
 5
 6namespace LOCKnet.Core.Services;
 7
 8/// <summary>
 9/// Implementierung von <see cref="ICredentialService"/>.
 10/// Kombiniert <see cref="ISessionManager"/> (Session-Key), <see cref="IEncryptionService"/> (AES-GCM)
 11/// und <see cref="ICredentialRepository"/> (Persistenz).
 12/// </summary>
 13public sealed class CredentialService : ICredentialService
 14{
 15  private readonly ICredentialRepository _repo;
 16  private readonly IMasterKeyRepository _masterKeyRepo;
 17  private readonly ICredentialEnvelopeService _credentialEnvelope;
 18  private readonly ISessionManager _session;
 19  private readonly ISecureStringService _secureStr;
 20
 21  /// <summary>
 22  /// Initialisiert eine neue Instanz von <see cref="CredentialService"/>.
 23  /// </summary>
 5524  public CredentialService(
 5525    ICredentialRepository repo,
 5526    IMasterKeyRepository masterKeyRepo,
 5527    IEncryptionService encryption,
 5528    ICredentialEnvelopeService credentialEnvelope,
 5529    ISessionManager session,
 5530    ISecureStringService secureStr)
 5531  {
 5532    ArgumentNullException.ThrowIfNull(repo);
 5533    ArgumentNullException.ThrowIfNull(masterKeyRepo);
 5534    ArgumentNullException.ThrowIfNull(credentialEnvelope);
 5535    ArgumentNullException.ThrowIfNull(session);
 5536    ArgumentNullException.ThrowIfNull(secureStr);
 5537    _repo = repo;
 5538    _masterKeyRepo = masterKeyRepo;
 5539    _credentialEnvelope = credentialEnvelope;
 5540    _session = session;
 5541    _secureStr = secureStr;
 5542  }
 43
 44  /// <inheritdoc/>
 45  public void Add(string title, string? username, SecureString password, string? url = null, string? notes = null, strin
 2646  {
 2647    ArgumentException.ThrowIfNullOrWhiteSpace(title);
 2448    ArgumentNullException.ThrowIfNull(password);
 49
 2450    var key = RequireSessionKey();
 2351    var header = RequireCurrentHeader();
 2352    var passwordBytes = _secureStr.ToByteArray(password);
 53    try
 2354    {
 2355      var record = new CredentialRecord
 2356      {
 2357        Title = title,
 2358        Username = username,
 2359        CredentialUuid = Guid.NewGuid().ToString("N"),
 2360        SecretFormatVersion = _credentialEnvelope.CurrentVersion,
 2361        MetadataFormatVersion = _credentialEnvelope.CurrentMetadataVersion,
 2362        Url = url,
 2363        Notes = notes,
 2364        IconKey = iconKey,
 2365        CredentialType = credentialType,
 2366        CreatedAt = DateTime.UtcNow,
 2367        UpdatedAt = DateTime.UtcNow
 2368      };
 2369      record.EncryptedPassword = _credentialEnvelope.Encrypt(passwordBytes, key, record, header.FormatVersion);
 2370      record.EncryptedMetadata = _credentialEnvelope.EncryptMetadata(record, key, header.FormatVersion);
 2371      _repo.Add(ToPersistedRecord(record));
 2372    }
 73    finally
 2374    {
 2375      _secureStr.ZeroMemory(key);
 2376      _secureStr.ZeroMemory(passwordBytes);
 2377    }
 2378  }
 79
 80  /// <inheritdoc/>
 81  public IReadOnlyList<CredentialRecord> GetAll()
 2582  {
 2583    var key = RequireSessionKey();
 2384    var header = RequireCurrentHeader();
 85    try
 2386    {
 2387      return _repo.GetAll()
 2388        .Select(record => MaterializeRecord(record, key, header.FormatVersion))
 2389        .ToList();
 90    }
 91    finally
 2392    {
 2393      _secureStr.ZeroMemory(key);
 2394    }
 2195  }
 96
 97  /// <inheritdoc/>
 98  public SecureString? GetPassword(int id)
 1099  {
 10100    var key = RequireSessionKey();
 7101    var header = RequireCurrentHeader();
 102    try
 7103    {
 7104      var record = _repo.GetById(id);
 9105      if (record is null) return null;
 106
 5107      var decrypted = _credentialEnvelope.Decrypt(record, key, header.FormatVersion);
 108      try
 3109      {
 3110        return _secureStr.FromByteArray(decrypted);
 111      }
 112      finally
 3113      {
 3114        _secureStr.ZeroMemory(decrypted);
 3115      }
 116    }
 117    finally
 7118    {
 7119      _secureStr.ZeroMemory(key);
 7120    }
 5121  }
 122
 123  /// <inheritdoc/>
 124  public void Update(int id, string title, string? username, SecureString? newPassword, string? url = null, string? note
 7125  {
 7126    ArgumentException.ThrowIfNullOrWhiteSpace(title);
 127
 7128    var key = RequireSessionKey();
 5129    var header = RequireCurrentHeader();
 130    try
 5131    {
 5132      var existing = _repo.GetById(id)
 5133        ?? throw new InvalidOperationException($"Credential mit ID {id} nicht gefunden.");
 134
 135      byte[] encryptedPassword;
 4136      var credentialUuid = EnsureCredentialUuid(existing.CredentialUuid);
 4137      var secretFormatVersion = existing.SecretFormatVersion;
 4138      if (newPassword is not null)
 1139      {
 1140        var passwordBytes = _secureStr.ToByteArray(newPassword);
 141        try
 1142        {
 1143          secretFormatVersion = _credentialEnvelope.CurrentVersion;
 1144          var encryptedRecord = new CredentialRecord
 1145          {
 1146            Id = id,
 1147            CredentialUuid = credentialUuid,
 1148            CredentialType = credentialType,
 1149          };
 1150          encryptedPassword = _credentialEnvelope.Encrypt(passwordBytes, key, encryptedRecord, header.FormatVersion);
 1151        }
 152        finally
 1153        {
 1154          _secureStr.ZeroMemory(passwordBytes);
 1155        }
 1156      }
 157      else
 3158      {
 3159        encryptedPassword = existing.EncryptedPassword;
 3160      }
 161
 4162      var materialized = new CredentialRecord
 4163      {
 4164        Id = id,
 4165        Title = title,
 4166        Username = username,
 4167        EncryptedPassword = encryptedPassword,
 4168        EncryptedMetadata = [],
 4169        CredentialUuid = credentialUuid,
 4170        SecretFormatVersion = secretFormatVersion,
 4171        MetadataFormatVersion = _credentialEnvelope.CurrentMetadataVersion,
 4172        Url = url,
 4173        Notes = notes,
 4174        IconKey = iconKey,
 4175        CredentialType = credentialType,
 4176        CreatedAt = existing.CreatedAt,
 4177        UpdatedAt = DateTime.UtcNow
 4178      };
 4179      materialized.EncryptedMetadata = _credentialEnvelope.EncryptMetadata(materialized, key, header.FormatVersion);
 4180      _repo.Update(ToPersistedRecord(materialized));
 4181    }
 182    finally
 5183    {
 5184      _secureStr.ZeroMemory(key);
 5185    }
 4186  }
 187
 188  /// <inheritdoc/>
 189  public void Remove(int id)
 4190  {
 4191    RequireUnlocked();
 2192    _repo.Remove(id);
 2193  }
 194
 195  // ── Helpers ───────────────────────────────────────────────────────────────
 196
 197  private byte[] RequireSessionKey()
 66198  {
 66199    var key = _session.GetSessionKey();
 66200    if (key is null)
 8201      throw new InvalidOperationException("Sitzung ist gesperrt. Bitte zuerst entsperren.");
 58202    return key;
 58203  }
 204
 205  private void RequireUnlocked()
 4206  {
 4207    if (!_session.IsUnlocked)
 2208      throw new InvalidOperationException("Sitzung ist gesperrt. Bitte zuerst entsperren.");
 2209  }
 210
 211  private VaultHeader RequireCurrentHeader()
 58212  {
 58213    var header = _masterKeyRepo.Get()
 58214      ?? throw new InvalidOperationException("VaultHeader konnte nicht geladen werden.");
 215
 58216    if (header.FormatVersion != VaultHeaderFormatVersion.Current)
 0217      throw new InvalidOperationException("Vault ist noch nicht auf das aktuelle Secret-Format migriert.");
 218
 58219    return header;
 58220  }
 221
 222  private CredentialRecord MaterializeRecord(CredentialRecord persisted, byte[] key, int vaultFormatVersion)
 23223  {
 23224    ValidatePersistedCurrentRecord(persisted);
 225
 22226    var materialized = _credentialEnvelope.DecryptMetadata(persisted, key, vaultFormatVersion);
 21227    materialized.EncryptedPassword = persisted.EncryptedPassword.ToArray();
 21228    materialized.EncryptedMetadata = persisted.EncryptedMetadata.ToArray();
 21229    materialized.SecretFormatVersion = persisted.SecretFormatVersion;
 21230    materialized.MetadataFormatVersion = persisted.MetadataFormatVersion;
 21231    materialized.CredentialUuid = persisted.CredentialUuid;
 21232    materialized.CreatedAt = persisted.CreatedAt;
 21233    materialized.UpdatedAt = persisted.UpdatedAt;
 21234    materialized.Id = persisted.Id;
 21235    return materialized;
 21236  }
 237
 27238  private static CredentialRecord ToPersistedRecord(CredentialRecord materialized) => new()
 27239  {
 27240    Id = materialized.Id,
 27241    Title = string.Empty,
 27242    Username = null,
 27243    EncryptedPassword = materialized.EncryptedPassword,
 27244    EncryptedMetadata = materialized.EncryptedMetadata,
 27245    CredentialUuid = materialized.CredentialUuid,
 27246    SecretFormatVersion = materialized.SecretFormatVersion,
 27247    MetadataFormatVersion = materialized.MetadataFormatVersion,
 27248    Url = null,
 27249    Notes = null,
 27250    CreatedAt = materialized.CreatedAt,
 27251    UpdatedAt = materialized.UpdatedAt,
 27252    IconKey = null,
 27253    CredentialType = CredentialType.Password,
 27254  };
 255
 256  private static string EnsureCredentialUuid(string credentialUuid)
 4257    => Guid.TryParseExact(credentialUuid, "N", out _) ? credentialUuid : Guid.NewGuid().ToString("N");
 258
 259  private static void ValidatePersistedCurrentRecord(CredentialRecord persisted)
 23260  {
 23261    if (persisted.MetadataFormatVersion != CredentialMetadataFormatVersion.Current)
 0262      return;
 263
 23264    if (!Guid.TryParseExact(persisted.CredentialUuid, "N", out _))
 0265      throw new InvalidOperationException("Aktuelle Records enthaelten eine ungueltige CredentialUuid.");
 266
 23267    if (persisted.EncryptedMetadata.Length == 0)
 0268      throw new InvalidOperationException("Aktuelle Records enthaelten keine verschluesselten Metadaten.");
 269
 23270    if (!string.IsNullOrEmpty(persisted.Title) ||
 23271      !string.IsNullOrEmpty(persisted.Username) ||
 23272      !string.IsNullOrEmpty(persisted.Url) ||
 23273      !string.IsNullOrEmpty(persisted.Notes) ||
 23274      !string.IsNullOrEmpty(persisted.IconKey) ||
 23275      persisted.CredentialType != CredentialType.Password)
 1276    {
 1277      throw new InvalidOperationException("Aktuelle Records enthalten unerwartete Klartext-Metadaten.");
 278    }
 22279  }
 280}