Skip to content

Commit aa0b35a

Browse files
authored
[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
1 parent 730f83b commit aa0b35a

File tree

3 files changed

+132
-9
lines changed

3 files changed

+132
-9
lines changed

src/Core/Settings/GlobalSettings.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ public virtual string LicenseDirectory
8282
public virtual ILaunchDarklySettings LaunchDarkly { get; set; } = new LaunchDarklySettings();
8383
public virtual string DevelopmentDirectory { get; set; }
8484
public virtual bool EnableEmailVerification { get; set; }
85+
public virtual string KdfDefaultHashKey { get; set; }
8586
public virtual string PricingUri { get; set; }
8687

8788
public string BuildExternalUri(string explicitValue, string name)

src/Identity/Controllers/AccountsController.cs

Lines changed: 67 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System.Diagnostics;
2+
using System.Text;
23
using Bit.Core;
34
using Bit.Core.Auth.Enums;
45
using Bit.Core.Auth.Models.Api.Request.Accounts;
@@ -15,6 +16,7 @@
1516
using Bit.Core.Models.Data;
1617
using Bit.Core.Repositories;
1718
using Bit.Core.Services;
19+
using Bit.Core.Settings;
1820
using Bit.Core.Tokens;
1921
using Bit.Core.Tools.Enums;
2022
using Bit.Core.Tools.Models.Business;
@@ -44,6 +46,41 @@ public class AccountsController : Controller
4446
private readonly IFeatureService _featureService;
4547
private readonly IDataProtectorTokenFactory<RegistrationEmailVerificationTokenable> _registrationEmailVerificationTokenDataFactory;
4648

49+
private readonly byte[] _defaultKdfHmacKey = null;
50+
private static readonly List<UserKdfInformation> _defaultKdfResults =
51+
[
52+
// The first result (index 0) should always return the "normal" default.
53+
new()
54+
{
55+
Kdf = KdfType.PBKDF2_SHA256,
56+
KdfIterations = AuthConstants.PBKDF2_ITERATIONS.Default,
57+
},
58+
// We want more weight for this default, so add it again
59+
new()
60+
{
61+
Kdf = KdfType.PBKDF2_SHA256,
62+
KdfIterations = AuthConstants.PBKDF2_ITERATIONS.Default,
63+
},
64+
// Add some other possible defaults...
65+
new()
66+
{
67+
Kdf = KdfType.PBKDF2_SHA256,
68+
KdfIterations = 100_000,
69+
},
70+
new()
71+
{
72+
Kdf = KdfType.PBKDF2_SHA256,
73+
KdfIterations = 5_000,
74+
},
75+
new()
76+
{
77+
Kdf = KdfType.Argon2id,
78+
KdfIterations = AuthConstants.ARGON2_ITERATIONS.Default,
79+
KdfMemory = AuthConstants.ARGON2_MEMORY.Default,
80+
KdfParallelism = AuthConstants.ARGON2_PARALLELISM.Default,
81+
}
82+
];
83+
4784
public AccountsController(
4885
ICurrentContext currentContext,
4986
ILogger<AccountsController> logger,
@@ -55,7 +92,8 @@ public AccountsController(
5592
ISendVerificationEmailForRegistrationCommand sendVerificationEmailForRegistrationCommand,
5693
IReferenceEventService referenceEventService,
5794
IFeatureService featureService,
58-
IDataProtectorTokenFactory<RegistrationEmailVerificationTokenable> registrationEmailVerificationTokenDataFactory
95+
IDataProtectorTokenFactory<RegistrationEmailVerificationTokenable> registrationEmailVerificationTokenDataFactory,
96+
GlobalSettings globalSettings
5997
)
6098
{
6199
_currentContext = currentContext;
@@ -69,6 +107,11 @@ IDataProtectorTokenFactory<RegistrationEmailVerificationTokenable> registrationE
69107
_referenceEventService = referenceEventService;
70108
_featureService = featureService;
71109
_registrationEmailVerificationTokenDataFactory = registrationEmailVerificationTokenDataFactory;
110+
111+
if (CoreHelpers.SettingHasValue(globalSettings.KdfDefaultHashKey))
112+
{
113+
_defaultKdfHmacKey = Encoding.UTF8.GetBytes(globalSettings.KdfDefaultHashKey);
114+
}
72115
}
73116

74117
[HttpPost("register")]
@@ -217,11 +260,7 @@ public async Task<PreloginResponseModel> PostPrelogin([FromBody] PreloginRequest
217260
var kdfInformation = await _userRepository.GetKdfInformationByEmailAsync(model.Email);
218261
if (kdfInformation == null)
219262
{
220-
kdfInformation = new UserKdfInformation
221-
{
222-
Kdf = KdfType.PBKDF2_SHA256,
223-
KdfIterations = AuthConstants.PBKDF2_ITERATIONS.Default,
224-
};
263+
kdfInformation = GetDefaultKdf(model.Email);
225264
}
226265
return new PreloginResponseModel(kdfInformation);
227266
}
@@ -240,4 +279,26 @@ public WebAuthnLoginAssertionOptionsResponseModel GetWebAuthnLoginAssertionOptio
240279
Token = token
241280
};
242281
}
282+
283+
private UserKdfInformation GetDefaultKdf(string email)
284+
{
285+
if (_defaultKdfHmacKey == null)
286+
{
287+
return _defaultKdfResults[0];
288+
}
289+
else
290+
{
291+
// Compute the HMAC hash of the email
292+
var hmacMessage = Encoding.UTF8.GetBytes(email.Trim().ToLowerInvariant());
293+
using var hmac = new System.Security.Cryptography.HMACSHA256(_defaultKdfHmacKey);
294+
var hmacHash = hmac.ComputeHash(hmacMessage);
295+
// Convert the hash to a number
296+
var hashHex = BitConverter.ToString(hmacHash).Replace("-", string.Empty).ToLowerInvariant();
297+
var hashFirst8Bytes = hashHex.Substring(0, 16);
298+
var hashNumber = long.Parse(hashFirst8Bytes, System.Globalization.NumberStyles.HexNumber);
299+
// Find the default KDF value for this hash number
300+
var hashIndex = (int)(Math.Abs(hashNumber) % _defaultKdfResults.Count);
301+
return _defaultKdfResults[hashIndex];
302+
}
303+
}
243304
}

test/Identity.Test/Controllers/AccountsControllerTests.cs

Lines changed: 64 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
using Bit.Core;
1+
using System.Reflection;
2+
using System.Text;
3+
using Bit.Core;
24
using Bit.Core.Auth.Models.Api.Request.Accounts;
35
using Bit.Core.Auth.Models.Business.Tokenables;
46
using Bit.Core.Auth.Services;
@@ -11,6 +13,7 @@
1113
using Bit.Core.Models.Data;
1214
using Bit.Core.Repositories;
1315
using Bit.Core.Services;
16+
using Bit.Core.Settings;
1417
using Bit.Core.Tokens;
1518
using Bit.Core.Tools.Enums;
1619
using Bit.Core.Tools.Models.Business;
@@ -42,6 +45,7 @@ public class AccountsControllerTests : IDisposable
4245
private readonly IReferenceEventService _referenceEventService;
4346
private readonly IFeatureService _featureService;
4447
private readonly IDataProtectorTokenFactory<RegistrationEmailVerificationTokenable> _registrationEmailVerificationTokenDataFactory;
48+
private readonly GlobalSettings _globalSettings;
4549

4650

4751
public AccountsControllerTests()
@@ -57,6 +61,7 @@ public AccountsControllerTests()
5761
_referenceEventService = Substitute.For<IReferenceEventService>();
5862
_featureService = Substitute.For<IFeatureService>();
5963
_registrationEmailVerificationTokenDataFactory = Substitute.For<IDataProtectorTokenFactory<RegistrationEmailVerificationTokenable>>();
64+
_globalSettings = Substitute.For<GlobalSettings>();
6065

6166
_sut = new AccountsController(
6267
_currentContext,
@@ -69,7 +74,8 @@ public AccountsControllerTests()
6974
_sendVerificationEmailForRegistrationCommand,
7075
_referenceEventService,
7176
_featureService,
72-
_registrationEmailVerificationTokenDataFactory
77+
_registrationEmailVerificationTokenDataFactory,
78+
_globalSettings
7379
);
7480
}
7581

@@ -95,8 +101,9 @@ public async Task PostPrelogin_WhenUserExists_ShouldReturnUserKdfInfo()
95101
}
96102

97103
[Fact]
98-
public async Task PostPrelogin_WhenUserDoesNotExist_ShouldDefaultToPBKDF()
104+
public async Task PostPrelogin_WhenUserDoesNotExistAndNoDefaultKdfHmacKeySet_ShouldDefaultToPBKDF()
99105
{
106+
SetDefaultKdfHmacKey(null);
100107
_userRepository.GetKdfInformationByEmailAsync(Arg.Any<string>()).Returns(Task.FromResult<UserKdfInformation?>(null));
101108

102109
var response = await _sut.PostPrelogin(new PreloginRequestModel { Email = "user@example.com" });
@@ -105,6 +112,38 @@ public async Task PostPrelogin_WhenUserDoesNotExist_ShouldDefaultToPBKDF()
105112
Assert.Equal(AuthConstants.PBKDF2_ITERATIONS.Default, response.KdfIterations);
106113
}
107114

115+
[Theory]
116+
[BitAutoData]
117+
public async Task PostPrelogin_WhenUserDoesNotExistAndDefaultKdfHmacKeyIsSet_ShouldComputeHmacAndReturnExpectedKdf(string email)
118+
{
119+
// Arrange:
120+
var defaultKey = Encoding.UTF8.GetBytes("my-secret-key");
121+
SetDefaultKdfHmacKey(defaultKey);
122+
123+
_userRepository.GetKdfInformationByEmailAsync(Arg.Any<string>()).Returns(Task.FromResult<UserKdfInformation?>(null));
124+
125+
var fieldInfo = typeof(AccountsController).GetField("_defaultKdfResults", BindingFlags.NonPublic | BindingFlags.Static);
126+
if (fieldInfo == null)
127+
throw new InvalidOperationException("Field '_defaultKdfResults' not found.");
128+
129+
var defaultKdfResults = (List<UserKdfInformation>)fieldInfo.GetValue(null)!;
130+
131+
var expectedIndex = GetExpectedKdfIndex(email, defaultKey, defaultKdfResults);
132+
var expectedKdf = defaultKdfResults[expectedIndex];
133+
134+
// Act
135+
var response = await _sut.PostPrelogin(new PreloginRequestModel { Email = email });
136+
137+
// Assert: Ensure the returned KDF matches the expected one from the computed hash
138+
Assert.Equal(expectedKdf.Kdf, response.Kdf);
139+
Assert.Equal(expectedKdf.KdfIterations, response.KdfIterations);
140+
if (expectedKdf.Kdf == KdfType.Argon2id)
141+
{
142+
Assert.Equal(expectedKdf.KdfMemory, response.KdfMemory);
143+
Assert.Equal(expectedKdf.KdfParallelism, response.KdfParallelism);
144+
}
145+
}
146+
108147
[Fact]
109148
public async Task PostRegister_ShouldRegisterUser()
110149
{
@@ -484,6 +523,28 @@ await _referenceEventService.Received(1).RaiseEventAsync(Arg.Is<ReferenceEvent>(
484523
));
485524
}
486525

526+
private void SetDefaultKdfHmacKey(byte[]? newKey)
527+
{
528+
var fieldInfo = typeof(AccountsController).GetField("_defaultKdfHmacKey", BindingFlags.NonPublic | BindingFlags.Instance);
529+
if (fieldInfo == null)
530+
{
531+
throw new InvalidOperationException("Field '_defaultKdfHmacKey' not found.");
532+
}
487533

534+
fieldInfo.SetValue(_sut, newKey);
535+
}
488536

537+
private int GetExpectedKdfIndex(string email, byte[] defaultKey, List<UserKdfInformation> defaultKdfResults)
538+
{
539+
// Compute the HMAC hash of the email
540+
var hmacMessage = Encoding.UTF8.GetBytes(email.Trim().ToLowerInvariant());
541+
using var hmac = new System.Security.Cryptography.HMACSHA256(defaultKey);
542+
var hmacHash = hmac.ComputeHash(hmacMessage);
543+
544+
// Convert the hash to a number and calculate the index
545+
var hashHex = BitConverter.ToString(hmacHash).Replace("-", string.Empty).ToLowerInvariant();
546+
var hashFirst8Bytes = hashHex.Substring(0, 16);
547+
var hashNumber = long.Parse(hashFirst8Bytes, System.Globalization.NumberStyles.HexNumber);
548+
return (int)(Math.Abs(hashNumber) % defaultKdfResults.Count);
549+
}
489550
}

0 commit comments

Comments
 (0)