< Summary

Information
Class: LOCKnet.Core.Crypto.Pbkdf2KeyDerivationService
Assembly: LOCKnet.Core
File(s): /home/runner/work/LOCKnet/LOCKnet/src/LOCKnet.Core/Crypto/Pbkdf2KeyDerivationService.cs
Line coverage
95%
Covered lines: 61
Uncovered lines: 3
Coverable lines: 64
Total lines: 126
Line coverage: 95.3%
Branch coverage
68%
Covered branches: 11
Total branches: 16
Branch coverage: 68.7%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
get_Identifier()100%11100%
GetDefaultParameters()100%11100%
ValidateParameters(...)100%11100%
GenerateSalt(...)100%11100%
DeriveKey(...)100%11100%
DeriveKey(...)100%11100%
ComputePasswordHash(...)100%11100%
ComputePasswordHash(...)100%11100%
VerifyPassword(...)100%11100%
VerifyPassword(...)100%11100%
ResolveHashAlgorithm(...)50%2280%
ValidateParametersCore(...)71.42%161477.77%

File(s)

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

#LineLine coverage
 1using LOCKnet.Core.DataAbstractions;
 2using System.Security.Cryptography;
 3
 4namespace LOCKnet.Core.Crypto;
 5
 6/// <summary>
 7/// PBKDF2-Implementierung von <see cref="IKeyDerivationService"/>.
 8/// Nutzt HMAC-SHA256, 600.000 Iterationen (OWASP-Empfehlung 2023).
 9/// </summary>
 10public sealed class Pbkdf2KeyDerivationService : IKeyDerivationService
 11{
 12  private const string KdfIdentifier = "PBKDF2-SHA256";
 13  /// <summary>Anzahl der PBKDF2-Iterationen. OWASP empfiehlt ≥600.000 für HMAC-SHA256.</summary>
 14  private const int Iterations = 600_000;
 15
 16  /// <summary>Ausgabelänge des abgeleiteten Schlüssels in Bytes (256 Bit für AES-256).</summary>
 17  private const int KeyLengthBytes = 32;
 18
 19  /// <summary>Ausgabelänge des Passwort-Hashes in Bytes.</summary>
 20  private const int HashLengthBytes = 32;
 21
 22  /// <inheritdoc/>
 11723  public string Identifier => KdfIdentifier;
 24
 25  /// <inheritdoc/>
 7326  public VaultKdfParameters GetDefaultParameters() => new()
 7327  {
 7328    HashAlgorithm = "SHA256",
 7329    Iterations = Iterations,
 7330    KeyLengthBytes = KeyLengthBytes,
 7331    SaltLengthBytes = 32,
 7332  };
 33
 34  /// <inheritdoc/>
 35  public void ValidateParameters(VaultKdfParameters parameters)
 5436    => ValidateParametersCore(parameters);
 37
 38  /// <inheritdoc/>
 39  public byte[] GenerateSalt(int length = 32)
 7540  {
 7541    ArgumentOutOfRangeException.ThrowIfLessThan(length, 16, nameof(length));
 7442    return RandomNumberGenerator.GetBytes(length);
 7443  }
 44
 45  /// <inheritdoc/>
 46  public byte[] DeriveKey(byte[] password, byte[] salt)
 847    => DeriveKey(password, salt, GetDefaultParameters());
 48
 49  /// <inheritdoc/>
 50  public byte[] DeriveKey(byte[] password, byte[] salt, VaultKdfParameters parameters)
 11251  {
 11252    ArgumentNullException.ThrowIfNull(password);
 11253    ArgumentNullException.ThrowIfNull(salt);
 11254    ArgumentNullException.ThrowIfNull(parameters);
 11255    ValidateParametersCore(parameters);
 56
 11157    return Rfc2898DeriveBytes.Pbkdf2(
 11158      password,
 11159      salt,
 11160      parameters.Iterations,
 11161      ResolveHashAlgorithm(parameters.HashAlgorithm),
 11162      parameters.KeyLengthBytes);
 11163  }
 64
 65  /// <inheritdoc/>
 66  public byte[] ComputePasswordHash(byte[] password, byte[] salt)
 667    => ComputePasswordHash(password, salt, GetDefaultParameters());
 68
 69  /// <inheritdoc/>
 70  public byte[] ComputePasswordHash(byte[] password, byte[] salt, VaultKdfParameters parameters)
 2171  {
 2172    ArgumentNullException.ThrowIfNull(password);
 2173    ArgumentNullException.ThrowIfNull(salt);
 2174    ArgumentNullException.ThrowIfNull(parameters);
 2175    ValidateParametersCore(parameters);
 76
 77    // Separater Durchlauf mit anderem Kontext-Byte, damit
 78    // DeriveKey-Ausgabe und PasswordHash nie identisch sind.
 2179    var saltWithContext = new byte[salt.Length + 1];
 2180    salt.CopyTo(saltWithContext, 0);
 2181    saltWithContext[^1] = 0x01; // Kontext: "password verification"
 82
 2183    return Rfc2898DeriveBytes.Pbkdf2(
 2184      password,
 2185      saltWithContext,
 2186      parameters.Iterations,
 2187      ResolveHashAlgorithm(parameters.HashAlgorithm),
 2188      HashLengthBytes);
 2189  }
 90
 91  /// <inheritdoc/>
 92  public bool VerifyPassword(byte[] password, byte[] salt, byte[] storedHash)
 393    => VerifyPassword(password, salt, storedHash, GetDefaultParameters());
 94
 95  /// <inheritdoc/>
 96  public bool VerifyPassword(byte[] password, byte[] salt, byte[] storedHash, VaultKdfParameters parameters)
 897  {
 898    ArgumentNullException.ThrowIfNull(password);
 899    ArgumentNullException.ThrowIfNull(salt);
 8100    ArgumentNullException.ThrowIfNull(storedHash);
 101
 8102    var computed = ComputePasswordHash(password, salt, parameters);
 8103    return CryptographicOperations.FixedTimeEquals(computed, storedHash);
 8104  }
 105
 106  private static HashAlgorithmName ResolveHashAlgorithm(string algorithm)
 318107    => algorithm.ToUpperInvariant() switch
 318108    {
 318109      "SHA256" => HashAlgorithmName.SHA256,
 0110      _ => throw new NotSupportedException($"Nicht unterstuetzter PBKDF2-Hash-Algorithmus: {algorithm}"),
 318111    };
 112
 113  private static void ValidateParametersCore(VaultKdfParameters parameters)
 187114  {
 187115    if (parameters.Iterations is < 100_000 or > 5_000_000)
 1116      throw new InvalidOperationException("Persistierte PBKDF2-Iterationen liegen ausserhalb des erlaubten Bereichs.");
 117
 186118    if (parameters.KeyLengthBytes != KeyLengthBytes)
 0119      throw new InvalidOperationException($"Persistierte PBKDF2-Schluessellaenge muss {KeyLengthBytes} Bytes betragen.")
 120
 186121    if (parameters.SaltLengthBytes is < 16 or > 64)
 0122      throw new InvalidOperationException("Persistierte Salt-Laenge liegt ausserhalb des erlaubten Bereichs.");
 123
 186124    _ = ResolveHashAlgorithm(parameters.HashAlgorithm);
 186125  }
 126}