mirror of
https://github.com/bitwarden/server
synced 2025-12-11 13:53:40 +00:00
[PM-22736] Send password hasher (#6112)
* feat: - Add SendPasswordHasher class and interface - DI for SendPasswordHasher to use Marker class allowing us to use custom options for the SendPasswordHasher without impacting other PasswordHashers. * test: Unit tests for SendPasswordHasher implementation * doc: docs for interface and comments Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,6 @@
|
|||||||
|
namespace Bit.Core.Auth.UserFeatures.PasswordValidation;
|
||||||
|
|
||||||
|
public static class PasswordValidationConstants
|
||||||
|
{
|
||||||
|
public const int PasswordHasherKdfIterations = 100000;
|
||||||
|
}
|
||||||
21
src/Core/KeyManagement/Sends/ISendPasswordHasher.cs
Normal file
21
src/Core/KeyManagement/Sends/ISendPasswordHasher.cs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
namespace Bit.Core.KeyManagement.Sends;
|
||||||
|
|
||||||
|
public interface ISendPasswordHasher
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Matches the send password hash against the user provided client password hash. The send password is server hashed and the client
|
||||||
|
/// password hash is hashed by the server for comparison <see cref="HashOfClientPasswordHash"/> in this method.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="sendPasswordHash">The send password that is hashed by the server.</param>
|
||||||
|
/// <param name="clientPasswordHash">The user provided password hash that has not yet been hashed by the server for comparison.</param>
|
||||||
|
/// <returns>true if hashes match false otherwise</returns>
|
||||||
|
/// <exception cref="InvalidOperationException">Thrown if the server password hash or client password hash is null or empty.</exception>
|
||||||
|
bool PasswordHashMatches(string sendPasswordHash, string clientPasswordHash);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Accepts a client hashed send password and returns a server hashed password.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="clientHashedPassword"></param>
|
||||||
|
/// <returns>server hashed password</returns>
|
||||||
|
string HashOfClientPasswordHash(string clientHashedPassword);
|
||||||
|
}
|
||||||
35
src/Core/KeyManagement/Sends/SendPasswordHasher.cs
Normal file
35
src/Core/KeyManagement/Sends/SendPasswordHasher.cs
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
|
||||||
|
namespace Bit.Core.KeyManagement.Sends;
|
||||||
|
|
||||||
|
internal class SendPasswordHasher(IPasswordHasher<SendPasswordHasherMarker> passwordHasher) : ISendPasswordHasher
|
||||||
|
{
|
||||||
|
private readonly IPasswordHasher<SendPasswordHasherMarker> _passwordHasher = passwordHasher;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// <inheritdoc cref="ISendPasswordHasher.PasswordHashMatches"/>
|
||||||
|
/// </summary>
|
||||||
|
public bool PasswordHashMatches(string sendPasswordHash, string inputPasswordHash)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(sendPasswordHash) || string.IsNullOrWhiteSpace(inputPasswordHash))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var passwordResult = _passwordHasher.VerifyHashedPassword(SendPasswordHasherMarker.Instance, sendPasswordHash, inputPasswordHash);
|
||||||
|
|
||||||
|
/*
|
||||||
|
In our use-case we input a high-entropy, pre-hashed secret sent by the client. Thus, we don't really care
|
||||||
|
about if the hash needs to be rehashed. Sends also only live for 30 days max.
|
||||||
|
*/
|
||||||
|
return passwordResult is PasswordVerificationResult.Success or PasswordVerificationResult.SuccessRehashNeeded;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// <inheritdoc cref="ISendPasswordHasher.HashOfClientPasswordHash"/>
|
||||||
|
/// </summary>
|
||||||
|
public string HashOfClientPasswordHash(string clientHashedPassword)
|
||||||
|
{
|
||||||
|
return _passwordHasher.HashPassword(SendPasswordHasherMarker.Instance, clientHashedPassword);
|
||||||
|
}
|
||||||
|
}
|
||||||
10
src/Core/KeyManagement/Sends/SendPasswordHasherMarker.cs
Normal file
10
src/Core/KeyManagement/Sends/SendPasswordHasherMarker.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
namespace Bit.Core.KeyManagement.Sends;
|
||||||
|
|
||||||
|
// This should not be used except for DI as open generic marker class for use with
|
||||||
|
// the SendPasswordHasher.
|
||||||
|
public class SendPasswordHasherMarker
|
||||||
|
{
|
||||||
|
// We know we will pass a single instance that isn't used to the PasswordHasher so we
|
||||||
|
// gain an efficiency benefit of not creating multiple marker classes.
|
||||||
|
public static readonly SendPasswordHasherMarker Instance = new();
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
using Bit.Core.Auth.UserFeatures.PasswordValidation;
|
||||||
|
using Bit.Core.KeyManagement.Sends;
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||||
|
|
||||||
|
namespace Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
public static class SendPasswordHasherServiceCollectionExtensions
|
||||||
|
{
|
||||||
|
public static void AddSendPasswordServices(this IServiceCollection services)
|
||||||
|
{
|
||||||
|
const string sendPasswordHasherMarkerName = "SendPasswordHasherMarker";
|
||||||
|
|
||||||
|
services.AddOptions<PasswordHasherOptions>(sendPasswordHasherMarkerName)
|
||||||
|
.Configure(options => options.IterationCount = PasswordValidationConstants.PasswordHasherKdfIterations);
|
||||||
|
|
||||||
|
services.TryAddScoped<IPasswordHasher<SendPasswordHasherMarker>>(sp =>
|
||||||
|
{
|
||||||
|
var opts = sp
|
||||||
|
.GetRequiredService<IOptionsMonitor<PasswordHasherOptions>>()
|
||||||
|
.Get(sendPasswordHasherMarkerName);
|
||||||
|
|
||||||
|
var optionsAccessor = Options.Create(opts);
|
||||||
|
|
||||||
|
return new PasswordHasher<SendPasswordHasherMarker>(optionsAccessor);
|
||||||
|
});
|
||||||
|
services.TryAddScoped<ISendPasswordHasher, SendPasswordHasher>();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,6 +23,7 @@ using Bit.Core.Auth.Repositories;
|
|||||||
using Bit.Core.Auth.Services;
|
using Bit.Core.Auth.Services;
|
||||||
using Bit.Core.Auth.Services.Implementations;
|
using Bit.Core.Auth.Services.Implementations;
|
||||||
using Bit.Core.Auth.UserFeatures;
|
using Bit.Core.Auth.UserFeatures;
|
||||||
|
using Bit.Core.Auth.UserFeatures.PasswordValidation;
|
||||||
using Bit.Core.Billing.Services;
|
using Bit.Core.Billing.Services;
|
||||||
using Bit.Core.Billing.Services.Implementations;
|
using Bit.Core.Billing.Services.Implementations;
|
||||||
using Bit.Core.Billing.TrialInitiation;
|
using Bit.Core.Billing.TrialInitiation;
|
||||||
@@ -381,7 +382,7 @@ public static class ServiceCollectionExtensions
|
|||||||
services.TryAddTransient(typeof(IOtpTokenProvider<>), typeof(OtpTokenProvider<>));
|
services.TryAddTransient(typeof(IOtpTokenProvider<>), typeof(OtpTokenProvider<>));
|
||||||
|
|
||||||
services.AddScoped<IOrganizationDuoUniversalTokenProvider, OrganizationDuoUniversalTokenProvider>();
|
services.AddScoped<IOrganizationDuoUniversalTokenProvider, OrganizationDuoUniversalTokenProvider>();
|
||||||
services.Configure<PasswordHasherOptions>(options => options.IterationCount = 100000);
|
services.Configure<PasswordHasherOptions>(options => options.IterationCount = PasswordValidationConstants.PasswordHasherKdfIterations);
|
||||||
services.Configure<TwoFactorRememberTokenProviderOptions>(options =>
|
services.Configure<TwoFactorRememberTokenProviderOptions>(options =>
|
||||||
{
|
{
|
||||||
options.TokenLifespan = TimeSpan.FromDays(30);
|
options.TokenLifespan = TimeSpan.FromDays(30);
|
||||||
|
|||||||
103
test/Core.Test/KeyManagement/SendPasswordHasherTests.cs
Normal file
103
test/Core.Test/KeyManagement/SendPasswordHasherTests.cs
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
using Bit.Core.KeyManagement.Sends;
|
||||||
|
using Bit.Test.Common.AutoFixture;
|
||||||
|
using Bit.Test.Common.AutoFixture.Attributes;
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
using NSubstitute;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Bit.Core.Test.KeyManagement.Sends;
|
||||||
|
|
||||||
|
[SutProviderCustomize]
|
||||||
|
public class SendPasswordHasherTests
|
||||||
|
{
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData(PasswordVerificationResult.Success)]
|
||||||
|
[BitAutoData(PasswordVerificationResult.SuccessRehashNeeded)]
|
||||||
|
void VerifyPasswordHash_WithValidMatching_ReturnsTrue(
|
||||||
|
PasswordVerificationResult passwordVerificationResult,
|
||||||
|
SutProvider<SendPasswordHasher> sutProvider,
|
||||||
|
string sendPasswordHash,
|
||||||
|
string inputPasswordHash)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
sutProvider.GetDependency<IPasswordHasher<SendPasswordHasherMarker>>()
|
||||||
|
.VerifyHashedPassword(Arg.Any<SendPasswordHasherMarker>(), sendPasswordHash, inputPasswordHash)
|
||||||
|
.Returns(passwordVerificationResult);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = sutProvider.Sut.PasswordHashMatches(sendPasswordHash, inputPasswordHash);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(result);
|
||||||
|
sutProvider.GetDependency<IPasswordHasher<SendPasswordHasherMarker>>()
|
||||||
|
.Received(1)
|
||||||
|
.VerifyHashedPassword(Arg.Any<SendPasswordHasherMarker>(), sendPasswordHash, inputPasswordHash);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
void VerifyPasswordHash_WithNonMatchingPasswords_ReturnsFalse(
|
||||||
|
SutProvider<SendPasswordHasher> sutProvider,
|
||||||
|
string sendPasswordHash,
|
||||||
|
string inputPasswordHash)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
sutProvider.GetDependency<IPasswordHasher<SendPasswordHasherMarker>>()
|
||||||
|
.VerifyHashedPassword(Arg.Any<SendPasswordHasherMarker>(), sendPasswordHash, inputPasswordHash)
|
||||||
|
.Returns(PasswordVerificationResult.Failed);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = sutProvider.Sut.PasswordHashMatches(sendPasswordHash, inputPasswordHash);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.False(result);
|
||||||
|
sutProvider.GetDependency<IPasswordHasher<SendPasswordHasherMarker>>()
|
||||||
|
.Received(1)
|
||||||
|
.VerifyHashedPassword(Arg.Any<SendPasswordHasherMarker>(), sendPasswordHash, inputPasswordHash);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(null, "inputPassword")]
|
||||||
|
[InlineData("", "inputPassword")]
|
||||||
|
[InlineData(" ", "inputPassword")]
|
||||||
|
[InlineData("sendPassword", null)]
|
||||||
|
[InlineData("sendPassword", "")]
|
||||||
|
[InlineData("sendPassword", " ")]
|
||||||
|
[InlineData(null, null)]
|
||||||
|
[InlineData("", "")]
|
||||||
|
public void VerifyPasswordHash_WithNullOrEmptyParameters_ReturnsFalse(
|
||||||
|
string? sendPasswordHash,
|
||||||
|
string? inputPasswordHash)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var passwordHasher = Substitute.For<IPasswordHasher<SendPasswordHasherMarker>>();
|
||||||
|
var sut = new SendPasswordHasher(passwordHasher);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = sut.PasswordHashMatches(sendPasswordHash, inputPasswordHash);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.False(result);
|
||||||
|
passwordHasher.DidNotReceive().VerifyHashedPassword(Arg.Any<SendPasswordHasherMarker>(), Arg.Any<string>(), Arg.Any<string>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
void HashPasswordHash_WithValidInput_ReturnsHashedPassword(
|
||||||
|
SutProvider<SendPasswordHasher> sutProvider,
|
||||||
|
string clientHashedPassword,
|
||||||
|
string expectedHashedResult)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
sutProvider.GetDependency<IPasswordHasher<SendPasswordHasherMarker>>()
|
||||||
|
.HashPassword(Arg.Any<SendPasswordHasherMarker>(), clientHashedPassword)
|
||||||
|
.Returns(expectedHashedResult);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = sutProvider.Sut.HashOfClientPasswordHash(clientHashedPassword);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(expectedHashedResult, result);
|
||||||
|
sutProvider.GetDependency<IPasswordHasher<SendPasswordHasherMarker>>()
|
||||||
|
.Received(1)
|
||||||
|
.HashPassword(Arg.Any<SendPasswordHasherMarker>(), clientHashedPassword);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user