mirror of
https://github.com/bitwarden/server
synced 2025-12-06 00:03:34 +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.Implementations;
|
||||
using Bit.Core.Auth.UserFeatures;
|
||||
using Bit.Core.Auth.UserFeatures.PasswordValidation;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Billing.Services.Implementations;
|
||||
using Bit.Core.Billing.TrialInitiation;
|
||||
@@ -381,7 +382,7 @@ public static class ServiceCollectionExtensions
|
||||
services.TryAddTransient(typeof(IOtpTokenProvider<>), typeof(OtpTokenProvider<>));
|
||||
|
||||
services.AddScoped<IOrganizationDuoUniversalTokenProvider, OrganizationDuoUniversalTokenProvider>();
|
||||
services.Configure<PasswordHasherOptions>(options => options.IterationCount = 100000);
|
||||
services.Configure<PasswordHasherOptions>(options => options.IterationCount = PasswordValidationConstants.PasswordHasherKdfIterations);
|
||||
services.Configure<TwoFactorRememberTokenProviderOptions>(options =>
|
||||
{
|
||||
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