< Summary

Information
Class: LOCKnet.Core.Crypto.CredentialEnvelopeService
Assembly: LOCKnet.Core
File(s): /home/runner/work/LOCKnet/LOCKnet/src/LOCKnet.Core/Crypto/CredentialEnvelopeService.cs
Line coverage
99%
Covered lines: 156
Uncovered lines: 1
Coverable lines: 157
Total lines: 231
Line coverage: 99.3%
Branch coverage
92%
Covered branches: 26
Total branches: 28
Branch coverage: 92.8%
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_CurrentVersion()100%11100%
get_CurrentMetadataVersion()100%11100%
Encrypt(...)100%11100%
Decrypt(...)100%44100%
EncryptMetadata(...)100%11100%
DecryptMetadata(...)100%44100%
DecryptV1(...)100%44100%
DecryptV2(...)75%4490.9%
DecryptMetadataV1(...)83.33%66100%
BuildAssociatedDataV1(...)100%22100%
BuildAssociatedDataV2(...)50%2288.88%
ValidateCredentialContext(...)100%22100%
CloneRecord(...)100%11100%
get_Title()100%11100%
get_Username()100%11100%
get_Url()100%11100%
get_Notes()100%11100%
get_IconKey()100%11100%
get_CredentialType()100%11100%

File(s)

/home/runner/work/LOCKnet/LOCKnet/src/LOCKnet.Core/Crypto/CredentialEnvelopeService.cs

#LineLine coverage
 1using LOCKnet.Core.DataAbstractions;
 2using System.Security.Cryptography;
 3using System.Text.Json;
 4
 5namespace LOCKnet.Core.Crypto;
 6
 7/// <summary>
 8/// Versionierter Envelope fuer Credential-Secrets.
 9/// V1 speichert <c>[Version][Nonce][Tag][Ciphertext]</c> und bindet den Ciphertext per AAD an stabile Metadaten.
 10/// </summary>
 11public sealed class CredentialEnvelopeService : ICredentialEnvelopeService
 12{
 13  private const int EnvelopeHeaderBytes = 1;
 14  private const byte SecretFieldDiscriminator = 1;
 15  private const byte MetadataFieldDiscriminator = 2;
 16  private readonly IEncryptionService _encryption;
 17
 18  /// <summary>
 19  /// Initialisiert eine neue Instanz von <see cref="CredentialEnvelopeService"/>.
 20  /// </summary>
 10521  public CredentialEnvelopeService(IEncryptionService encryption)
 10522  {
 10523    ArgumentNullException.ThrowIfNull(encryption);
 10524    _encryption = encryption;
 10525  }
 26
 27  /// <inheritdoc/>
 5728  public int CurrentVersion => CredentialSecretFormatVersion.Current;
 29
 30  /// <inheritdoc/>
 6931  public int CurrentMetadataVersion => CredentialMetadataFormatVersion.Current;
 32
 33  /// <inheritdoc/>
 34  public byte[] Encrypt(byte[] plaintext, byte[] key, CredentialRecord credential, int vaultFormatVersion)
 3435  {
 3436    ArgumentNullException.ThrowIfNull(plaintext);
 3437    ArgumentNullException.ThrowIfNull(key);
 3438    ArgumentNullException.ThrowIfNull(credential);
 3439    ValidateCredentialContext(credential);
 40
 3341    var aad = BuildAssociatedDataV2(vaultFormatVersion, credential.CredentialUuid, SecretFieldDiscriminator);
 3342    var packet = _encryption.Encrypt(plaintext, key, aad);
 3343    var envelope = new byte[EnvelopeHeaderBytes + packet.Length];
 3344    envelope[0] = (byte)CurrentVersion;
 3345    packet.CopyTo(envelope, EnvelopeHeaderBytes);
 3346    return envelope;
 3347  }
 48
 49  /// <inheritdoc/>
 50  public byte[] Decrypt(CredentialRecord credential, byte[] key, int vaultFormatVersion)
 2451  {
 2452    ArgumentNullException.ThrowIfNull(credential);
 2453    ArgumentNullException.ThrowIfNull(key);
 54
 2455    return credential.SecretFormatVersion switch
 2456    {
 157      CredentialSecretFormatVersion.Legacy => _encryption.Decrypt(credential.EncryptedPassword, key),
 458      CredentialSecretFormatVersion.AesGcmV1 => DecryptV1(credential, key, vaultFormatVersion),
 1859      CredentialSecretFormatVersion.AesGcmV2 => DecryptV2(credential, key, vaultFormatVersion),
 160      _ => throw new InvalidOperationException($"Nicht unterstuetzte Secret-Formatversion: {credential.SecretFormatVersi
 2461    };
 1662  }
 63
 64  /// <inheritdoc/>
 65  public byte[] EncryptMetadata(CredentialRecord credential, byte[] key, int vaultFormatVersion)
 3666  {
 3667    ArgumentNullException.ThrowIfNull(credential);
 3668    ArgumentNullException.ThrowIfNull(key);
 3669    ValidateCredentialContext(credential);
 70
 3671    var metadataBytes = JsonSerializer.SerializeToUtf8Bytes(new CredentialMetadataPayload
 3672    {
 3673      Title = credential.Title,
 3674      Username = credential.Username,
 3675      Url = credential.Url,
 3676      Notes = credential.Notes,
 3677      IconKey = credential.IconKey,
 3678      CredentialType = credential.CredentialType,
 3679    });
 80
 81    try
 3682    {
 3683      var aad = BuildAssociatedDataV2(vaultFormatVersion, credential.CredentialUuid, MetadataFieldDiscriminator);
 3684      var packet = _encryption.Encrypt(metadataBytes, key, aad);
 3685      var envelope = new byte[EnvelopeHeaderBytes + packet.Length];
 3686      envelope[0] = (byte)CurrentMetadataVersion;
 3687      packet.CopyTo(envelope, EnvelopeHeaderBytes);
 3688      return envelope;
 89    }
 90    finally
 3691    {
 3692      CryptographicOperations.ZeroMemory(metadataBytes);
 3693    }
 3694  }
 95
 96  /// <inheritdoc/>
 97  public CredentialRecord DecryptMetadata(CredentialRecord credential, byte[] key, int vaultFormatVersion)
 3898  {
 3899    ArgumentNullException.ThrowIfNull(credential);
 38100    ArgumentNullException.ThrowIfNull(key);
 101
 38102    return credential.MetadataFormatVersion switch
 38103    {
 1104      CredentialMetadataFormatVersion.Legacy => CloneRecord(credential),
 36105      CredentialMetadataFormatVersion.AesGcmV1 => DecryptMetadataV1(credential, key, vaultFormatVersion),
 1106      _ => throw new InvalidOperationException($"Nicht unterstuetzte Metadaten-Formatversion: {credential.MetadataFormat
 38107    };
 33108  }
 109
 110  private byte[] DecryptV1(CredentialRecord credential, byte[] key, int vaultFormatVersion)
 4111  {
 4112    ValidateCredentialContext(credential);
 4113    if (credential.EncryptedPassword.Length <= EnvelopeHeaderBytes)
 1114      throw new InvalidOperationException("Credential-Envelope ist zu kurz.");
 115
 3116    var versionByte = credential.EncryptedPassword[0];
 3117    if (versionByte != CredentialSecretFormatVersion.AesGcmV1)
 1118      throw new InvalidOperationException($"Envelope-Version {versionByte} passt nicht zum gespeicherten Secret-Format."
 119
 2120    var packet = credential.EncryptedPassword[EnvelopeHeaderBytes..];
 2121    var aad = BuildAssociatedDataV1(vaultFormatVersion, credential.CredentialUuid, credential.CredentialType, SecretFiel
 1122    return _encryption.Decrypt(packet, key, aad);
 1123  }
 124
 125  private byte[] DecryptV2(CredentialRecord credential, byte[] key, int vaultFormatVersion)
 18126  {
 18127    ValidateCredentialContext(credential);
 17128    if (credential.EncryptedPassword.Length <= EnvelopeHeaderBytes)
 1129      throw new InvalidOperationException("Credential-Envelope ist zu kurz.");
 130
 16131    var versionByte = credential.EncryptedPassword[0];
 16132    if (versionByte != CredentialSecretFormatVersion.AesGcmV2)
 1133      throw new InvalidOperationException($"Envelope-Version {versionByte} passt nicht zum gespeicherten Secret-Format."
 134
 15135    var packet = credential.EncryptedPassword[EnvelopeHeaderBytes..];
 15136    var aad = BuildAssociatedDataV2(vaultFormatVersion, credential.CredentialUuid, SecretFieldDiscriminator);
 15137    return _encryption.Decrypt(packet, key, aad);
 14138  }
 139
 140  private CredentialRecord DecryptMetadataV1(CredentialRecord credential, byte[] key, int vaultFormatVersion)
 36141  {
 36142    ValidateCredentialContext(credential);
 36143    if (credential.EncryptedMetadata.Length <= EnvelopeHeaderBytes)
 2144      throw new InvalidOperationException("Metadaten-Envelope ist zu kurz.");
 145
 34146    var versionByte = credential.EncryptedMetadata[0];
 34147    if (versionByte != CredentialMetadataFormatVersion.AesGcmV1)
 1148      throw new InvalidOperationException($"Envelope-Version {versionByte} passt nicht zum gespeicherten Metadaten-Forma
 149
 33150    var packet = credential.EncryptedMetadata[EnvelopeHeaderBytes..];
 33151    var aad = BuildAssociatedDataV2(vaultFormatVersion, credential.CredentialUuid, MetadataFieldDiscriminator);
 33152    var plaintext = _encryption.Decrypt(packet, key, aad);
 153    try
 33154    {
 33155      var payload = JsonSerializer.Deserialize<CredentialMetadataPayload>(plaintext)
 33156        ?? throw new InvalidOperationException("Metadaten konnten nicht deserialisiert werden.");
 157
 32158      var clone = CloneRecord(credential);
 32159      clone.Title = payload.Title;
 32160      clone.Username = payload.Username;
 32161      clone.Url = payload.Url;
 32162      clone.Notes = payload.Notes;
 32163      clone.IconKey = payload.IconKey;
 32164      clone.CredentialType = payload.CredentialType;
 32165      return clone;
 166    }
 167    finally
 33168    {
 33169      CryptographicOperations.ZeroMemory(plaintext);
 33170    }
 32171  }
 172
 173  private static byte[] BuildAssociatedDataV1(int vaultFormatVersion, string credentialUuid, CredentialType credentialTy
 2174  {
 2175    if (vaultFormatVersion != VaultHeaderFormatVersion.Current)
 1176      throw new InvalidOperationException($"Envelope erwartet VaultHeader-Format {VaultHeaderFormatVersion.Current}, erh
 177
 1178    var aad = new byte[22];
 1179    BitConverter.GetBytes(vaultFormatVersion).CopyTo(aad, 0);
 1180    Guid.ParseExact(credentialUuid, "N").TryWriteBytes(aad.AsSpan(4, 16));
 1181    aad[20] = fieldDiscriminator;
 1182    aad[21] = (byte)credentialType;
 1183    return aad;
 1184  }
 185
 186  private static byte[] BuildAssociatedDataV2(int vaultFormatVersion, string credentialUuid, byte fieldDiscriminator)
 117187  {
 117188    if (vaultFormatVersion != VaultHeaderFormatVersion.Current)
 0189      throw new InvalidOperationException($"Envelope erwartet VaultHeader-Format {VaultHeaderFormatVersion.Current}, erh
 190
 117191    var aad = new byte[21];
 117192    BitConverter.GetBytes(vaultFormatVersion).CopyTo(aad, 0);
 117193    Guid.ParseExact(credentialUuid, "N").TryWriteBytes(aad.AsSpan(4, 16));
 117194    aad[20] = fieldDiscriminator;
 117195    return aad;
 117196  }
 197
 198  private static void ValidateCredentialContext(CredentialRecord credential)
 128199  {
 128200    if (!Guid.TryParseExact(credential.CredentialUuid, "N", out _))
 2201      throw new InvalidOperationException("CredentialUuid muss eine GUID im N-Format sein.");
 126202  }
 203
 33204  private static CredentialRecord CloneRecord(CredentialRecord credential) => new()
 33205  {
 33206    Id = credential.Id,
 33207    Title = credential.Title,
 33208    Username = credential.Username,
 33209    EncryptedPassword = credential.EncryptedPassword.ToArray(),
 33210    EncryptedMetadata = credential.EncryptedMetadata.ToArray(),
 33211    CredentialUuid = credential.CredentialUuid,
 33212    SecretFormatVersion = credential.SecretFormatVersion,
 33213    MetadataFormatVersion = credential.MetadataFormatVersion,
 33214    Url = credential.Url,
 33215    Notes = credential.Notes,
 33216    CreatedAt = credential.CreatedAt,
 33217    UpdatedAt = credential.UpdatedAt,
 33218    IconKey = credential.IconKey,
 33219    CredentialType = credential.CredentialType,
 33220  };
 221
 222  private sealed class CredentialMetadataPayload
 223  {
 204224    public string Title { get; set; } = string.Empty;
 136225    public string? Username { get; set; }
 136226    public string? Url { get; set; }
 136227    public string? Notes { get; set; }
 136228    public string? IconKey { get; set; }
 136229    public CredentialType CredentialType { get; set; }
 230  }
 231}