mirror of
https://github.com/bitwarden/server
synced 2025-12-12 06:13:43 +00:00
[PM-15608] Create more KDF defaults for prelogin (#5122)
* kdf defaults on null map to email hash * cleanup code. add some randomness as well * remove null check * fix test * move to private method * remove random options * tests for random defaults * SetDefaultKdfHmacKey for old test
This commit is contained in:
@@ -82,6 +82,7 @@ public class GlobalSettings : IGlobalSettings
|
|||||||
public virtual ILaunchDarklySettings LaunchDarkly { get; set; } = new LaunchDarklySettings();
|
public virtual ILaunchDarklySettings LaunchDarkly { get; set; } = new LaunchDarklySettings();
|
||||||
public virtual string DevelopmentDirectory { get; set; }
|
public virtual string DevelopmentDirectory { get; set; }
|
||||||
public virtual bool EnableEmailVerification { get; set; }
|
public virtual bool EnableEmailVerification { get; set; }
|
||||||
|
public virtual string KdfDefaultHashKey { get; set; }
|
||||||
public virtual string PricingUri { get; set; }
|
public virtual string PricingUri { get; set; }
|
||||||
|
|
||||||
public string BuildExternalUri(string explicitValue, string name)
|
public string BuildExternalUri(string explicitValue, string name)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
|
using System.Text;
|
||||||
using Bit.Core;
|
using Bit.Core;
|
||||||
using Bit.Core.Auth.Enums;
|
using Bit.Core.Auth.Enums;
|
||||||
using Bit.Core.Auth.Models.Api.Request.Accounts;
|
using Bit.Core.Auth.Models.Api.Request.Accounts;
|
||||||
@@ -15,6 +16,7 @@ using Bit.Core.Exceptions;
|
|||||||
using Bit.Core.Models.Data;
|
using Bit.Core.Models.Data;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
|
using Bit.Core.Settings;
|
||||||
using Bit.Core.Tokens;
|
using Bit.Core.Tokens;
|
||||||
using Bit.Core.Tools.Enums;
|
using Bit.Core.Tools.Enums;
|
||||||
using Bit.Core.Tools.Models.Business;
|
using Bit.Core.Tools.Models.Business;
|
||||||
@@ -44,6 +46,41 @@ public class AccountsController : Controller
|
|||||||
private readonly IFeatureService _featureService;
|
private readonly IFeatureService _featureService;
|
||||||
private readonly IDataProtectorTokenFactory<RegistrationEmailVerificationTokenable> _registrationEmailVerificationTokenDataFactory;
|
private readonly IDataProtectorTokenFactory<RegistrationEmailVerificationTokenable> _registrationEmailVerificationTokenDataFactory;
|
||||||
|
|
||||||
|
private readonly byte[] _defaultKdfHmacKey = null;
|
||||||
|
private static readonly List<UserKdfInformation> _defaultKdfResults =
|
||||||
|
[
|
||||||
|
// The first result (index 0) should always return the "normal" default.
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Kdf = KdfType.PBKDF2_SHA256,
|
||||||
|
KdfIterations = AuthConstants.PBKDF2_ITERATIONS.Default,
|
||||||
|
},
|
||||||
|
// We want more weight for this default, so add it again
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Kdf = KdfType.PBKDF2_SHA256,
|
||||||
|
KdfIterations = AuthConstants.PBKDF2_ITERATIONS.Default,
|
||||||
|
},
|
||||||
|
// Add some other possible defaults...
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Kdf = KdfType.PBKDF2_SHA256,
|
||||||
|
KdfIterations = 100_000,
|
||||||
|
},
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Kdf = KdfType.PBKDF2_SHA256,
|
||||||
|
KdfIterations = 5_000,
|
||||||
|
},
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Kdf = KdfType.Argon2id,
|
||||||
|
KdfIterations = AuthConstants.ARGON2_ITERATIONS.Default,
|
||||||
|
KdfMemory = AuthConstants.ARGON2_MEMORY.Default,
|
||||||
|
KdfParallelism = AuthConstants.ARGON2_PARALLELISM.Default,
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
public AccountsController(
|
public AccountsController(
|
||||||
ICurrentContext currentContext,
|
ICurrentContext currentContext,
|
||||||
ILogger<AccountsController> logger,
|
ILogger<AccountsController> logger,
|
||||||
@@ -55,7 +92,8 @@ public class AccountsController : Controller
|
|||||||
ISendVerificationEmailForRegistrationCommand sendVerificationEmailForRegistrationCommand,
|
ISendVerificationEmailForRegistrationCommand sendVerificationEmailForRegistrationCommand,
|
||||||
IReferenceEventService referenceEventService,
|
IReferenceEventService referenceEventService,
|
||||||
IFeatureService featureService,
|
IFeatureService featureService,
|
||||||
IDataProtectorTokenFactory<RegistrationEmailVerificationTokenable> registrationEmailVerificationTokenDataFactory
|
IDataProtectorTokenFactory<RegistrationEmailVerificationTokenable> registrationEmailVerificationTokenDataFactory,
|
||||||
|
GlobalSettings globalSettings
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
_currentContext = currentContext;
|
_currentContext = currentContext;
|
||||||
@@ -69,6 +107,11 @@ public class AccountsController : Controller
|
|||||||
_referenceEventService = referenceEventService;
|
_referenceEventService = referenceEventService;
|
||||||
_featureService = featureService;
|
_featureService = featureService;
|
||||||
_registrationEmailVerificationTokenDataFactory = registrationEmailVerificationTokenDataFactory;
|
_registrationEmailVerificationTokenDataFactory = registrationEmailVerificationTokenDataFactory;
|
||||||
|
|
||||||
|
if (CoreHelpers.SettingHasValue(globalSettings.KdfDefaultHashKey))
|
||||||
|
{
|
||||||
|
_defaultKdfHmacKey = Encoding.UTF8.GetBytes(globalSettings.KdfDefaultHashKey);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("register")]
|
[HttpPost("register")]
|
||||||
@@ -217,11 +260,7 @@ public class AccountsController : Controller
|
|||||||
var kdfInformation = await _userRepository.GetKdfInformationByEmailAsync(model.Email);
|
var kdfInformation = await _userRepository.GetKdfInformationByEmailAsync(model.Email);
|
||||||
if (kdfInformation == null)
|
if (kdfInformation == null)
|
||||||
{
|
{
|
||||||
kdfInformation = new UserKdfInformation
|
kdfInformation = GetDefaultKdf(model.Email);
|
||||||
{
|
|
||||||
Kdf = KdfType.PBKDF2_SHA256,
|
|
||||||
KdfIterations = AuthConstants.PBKDF2_ITERATIONS.Default,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
return new PreloginResponseModel(kdfInformation);
|
return new PreloginResponseModel(kdfInformation);
|
||||||
}
|
}
|
||||||
@@ -240,4 +279,26 @@ public class AccountsController : Controller
|
|||||||
Token = token
|
Token = token
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private UserKdfInformation GetDefaultKdf(string email)
|
||||||
|
{
|
||||||
|
if (_defaultKdfHmacKey == null)
|
||||||
|
{
|
||||||
|
return _defaultKdfResults[0];
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Compute the HMAC hash of the email
|
||||||
|
var hmacMessage = Encoding.UTF8.GetBytes(email.Trim().ToLowerInvariant());
|
||||||
|
using var hmac = new System.Security.Cryptography.HMACSHA256(_defaultKdfHmacKey);
|
||||||
|
var hmacHash = hmac.ComputeHash(hmacMessage);
|
||||||
|
// Convert the hash to a number
|
||||||
|
var hashHex = BitConverter.ToString(hmacHash).Replace("-", string.Empty).ToLowerInvariant();
|
||||||
|
var hashFirst8Bytes = hashHex.Substring(0, 16);
|
||||||
|
var hashNumber = long.Parse(hashFirst8Bytes, System.Globalization.NumberStyles.HexNumber);
|
||||||
|
// Find the default KDF value for this hash number
|
||||||
|
var hashIndex = (int)(Math.Abs(hashNumber) % _defaultKdfResults.Count);
|
||||||
|
return _defaultKdfResults[hashIndex];
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
using Bit.Core;
|
using System.Reflection;
|
||||||
|
using System.Text;
|
||||||
|
using Bit.Core;
|
||||||
using Bit.Core.Auth.Models.Api.Request.Accounts;
|
using Bit.Core.Auth.Models.Api.Request.Accounts;
|
||||||
using Bit.Core.Auth.Models.Business.Tokenables;
|
using Bit.Core.Auth.Models.Business.Tokenables;
|
||||||
using Bit.Core.Auth.Services;
|
using Bit.Core.Auth.Services;
|
||||||
@@ -11,6 +13,7 @@ using Bit.Core.Exceptions;
|
|||||||
using Bit.Core.Models.Data;
|
using Bit.Core.Models.Data;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
|
using Bit.Core.Settings;
|
||||||
using Bit.Core.Tokens;
|
using Bit.Core.Tokens;
|
||||||
using Bit.Core.Tools.Enums;
|
using Bit.Core.Tools.Enums;
|
||||||
using Bit.Core.Tools.Models.Business;
|
using Bit.Core.Tools.Models.Business;
|
||||||
@@ -42,6 +45,7 @@ public class AccountsControllerTests : IDisposable
|
|||||||
private readonly IReferenceEventService _referenceEventService;
|
private readonly IReferenceEventService _referenceEventService;
|
||||||
private readonly IFeatureService _featureService;
|
private readonly IFeatureService _featureService;
|
||||||
private readonly IDataProtectorTokenFactory<RegistrationEmailVerificationTokenable> _registrationEmailVerificationTokenDataFactory;
|
private readonly IDataProtectorTokenFactory<RegistrationEmailVerificationTokenable> _registrationEmailVerificationTokenDataFactory;
|
||||||
|
private readonly GlobalSettings _globalSettings;
|
||||||
|
|
||||||
|
|
||||||
public AccountsControllerTests()
|
public AccountsControllerTests()
|
||||||
@@ -57,6 +61,7 @@ public class AccountsControllerTests : IDisposable
|
|||||||
_referenceEventService = Substitute.For<IReferenceEventService>();
|
_referenceEventService = Substitute.For<IReferenceEventService>();
|
||||||
_featureService = Substitute.For<IFeatureService>();
|
_featureService = Substitute.For<IFeatureService>();
|
||||||
_registrationEmailVerificationTokenDataFactory = Substitute.For<IDataProtectorTokenFactory<RegistrationEmailVerificationTokenable>>();
|
_registrationEmailVerificationTokenDataFactory = Substitute.For<IDataProtectorTokenFactory<RegistrationEmailVerificationTokenable>>();
|
||||||
|
_globalSettings = Substitute.For<GlobalSettings>();
|
||||||
|
|
||||||
_sut = new AccountsController(
|
_sut = new AccountsController(
|
||||||
_currentContext,
|
_currentContext,
|
||||||
@@ -69,7 +74,8 @@ public class AccountsControllerTests : IDisposable
|
|||||||
_sendVerificationEmailForRegistrationCommand,
|
_sendVerificationEmailForRegistrationCommand,
|
||||||
_referenceEventService,
|
_referenceEventService,
|
||||||
_featureService,
|
_featureService,
|
||||||
_registrationEmailVerificationTokenDataFactory
|
_registrationEmailVerificationTokenDataFactory,
|
||||||
|
_globalSettings
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,8 +101,9 @@ public class AccountsControllerTests : IDisposable
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task PostPrelogin_WhenUserDoesNotExist_ShouldDefaultToPBKDF()
|
public async Task PostPrelogin_WhenUserDoesNotExistAndNoDefaultKdfHmacKeySet_ShouldDefaultToPBKDF()
|
||||||
{
|
{
|
||||||
|
SetDefaultKdfHmacKey(null);
|
||||||
_userRepository.GetKdfInformationByEmailAsync(Arg.Any<string>()).Returns(Task.FromResult<UserKdfInformation?>(null));
|
_userRepository.GetKdfInformationByEmailAsync(Arg.Any<string>()).Returns(Task.FromResult<UserKdfInformation?>(null));
|
||||||
|
|
||||||
var response = await _sut.PostPrelogin(new PreloginRequestModel { Email = "user@example.com" });
|
var response = await _sut.PostPrelogin(new PreloginRequestModel { Email = "user@example.com" });
|
||||||
@@ -105,6 +112,38 @@ public class AccountsControllerTests : IDisposable
|
|||||||
Assert.Equal(AuthConstants.PBKDF2_ITERATIONS.Default, response.KdfIterations);
|
Assert.Equal(AuthConstants.PBKDF2_ITERATIONS.Default, response.KdfIterations);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task PostPrelogin_WhenUserDoesNotExistAndDefaultKdfHmacKeyIsSet_ShouldComputeHmacAndReturnExpectedKdf(string email)
|
||||||
|
{
|
||||||
|
// Arrange:
|
||||||
|
var defaultKey = Encoding.UTF8.GetBytes("my-secret-key");
|
||||||
|
SetDefaultKdfHmacKey(defaultKey);
|
||||||
|
|
||||||
|
_userRepository.GetKdfInformationByEmailAsync(Arg.Any<string>()).Returns(Task.FromResult<UserKdfInformation?>(null));
|
||||||
|
|
||||||
|
var fieldInfo = typeof(AccountsController).GetField("_defaultKdfResults", BindingFlags.NonPublic | BindingFlags.Static);
|
||||||
|
if (fieldInfo == null)
|
||||||
|
throw new InvalidOperationException("Field '_defaultKdfResults' not found.");
|
||||||
|
|
||||||
|
var defaultKdfResults = (List<UserKdfInformation>)fieldInfo.GetValue(null)!;
|
||||||
|
|
||||||
|
var expectedIndex = GetExpectedKdfIndex(email, defaultKey, defaultKdfResults);
|
||||||
|
var expectedKdf = defaultKdfResults[expectedIndex];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var response = await _sut.PostPrelogin(new PreloginRequestModel { Email = email });
|
||||||
|
|
||||||
|
// Assert: Ensure the returned KDF matches the expected one from the computed hash
|
||||||
|
Assert.Equal(expectedKdf.Kdf, response.Kdf);
|
||||||
|
Assert.Equal(expectedKdf.KdfIterations, response.KdfIterations);
|
||||||
|
if (expectedKdf.Kdf == KdfType.Argon2id)
|
||||||
|
{
|
||||||
|
Assert.Equal(expectedKdf.KdfMemory, response.KdfMemory);
|
||||||
|
Assert.Equal(expectedKdf.KdfParallelism, response.KdfParallelism);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task PostRegister_ShouldRegisterUser()
|
public async Task PostRegister_ShouldRegisterUser()
|
||||||
{
|
{
|
||||||
@@ -484,6 +523,28 @@ public class AccountsControllerTests : IDisposable
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void SetDefaultKdfHmacKey(byte[]? newKey)
|
||||||
|
{
|
||||||
|
var fieldInfo = typeof(AccountsController).GetField("_defaultKdfHmacKey", BindingFlags.NonPublic | BindingFlags.Instance);
|
||||||
|
if (fieldInfo == null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Field '_defaultKdfHmacKey' not found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldInfo.SetValue(_sut, newKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int GetExpectedKdfIndex(string email, byte[] defaultKey, List<UserKdfInformation> defaultKdfResults)
|
||||||
|
{
|
||||||
|
// Compute the HMAC hash of the email
|
||||||
|
var hmacMessage = Encoding.UTF8.GetBytes(email.Trim().ToLowerInvariant());
|
||||||
|
using var hmac = new System.Security.Cryptography.HMACSHA256(defaultKey);
|
||||||
|
var hmacHash = hmac.ComputeHash(hmacMessage);
|
||||||
|
|
||||||
|
// Convert the hash to a number and calculate the index
|
||||||
|
var hashHex = BitConverter.ToString(hmacHash).Replace("-", string.Empty).ToLowerInvariant();
|
||||||
|
var hashFirst8Bytes = hashHex.Substring(0, 16);
|
||||||
|
var hashNumber = long.Parse(hashFirst8Bytes, System.Globalization.NumberStyles.HexNumber);
|
||||||
|
return (int)(Math.Abs(hashNumber) % defaultKdfResults.Count);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user