diff --git a/src/Core/Models/Data/UserKdfInformation.cs b/src/Core/Models/Data/UserKdfInformation.cs index 0e5696e581..14f525bb82 100644 --- a/src/Core/Models/Data/UserKdfInformation.cs +++ b/src/Core/Models/Data/UserKdfInformation.cs @@ -4,8 +4,8 @@ namespace Bit.Core.Models.Data; public class UserKdfInformation { - public required KdfType Kdf { get; set; } - public required int KdfIterations { get; set; } + public KdfType Kdf { get; set; } + public int KdfIterations { get; set; } public int? KdfMemory { get; set; } public int? KdfParallelism { get; set; } } diff --git a/src/Identity/Controllers/AccountsController.cs b/src/Identity/Controllers/AccountsController.cs index 108efe79ba..cc146800af 100644 --- a/src/Identity/Controllers/AccountsController.cs +++ b/src/Identity/Controllers/AccountsController.cs @@ -195,35 +195,16 @@ public class AccountsController : Controller throw new BadRequestException(ModelState); } + // Moved from API, If you modify this endpoint, please update API as well. Self hosted installs still use the API endpoints. [HttpPost("prelogin")] - [Obsolete("Migrating to use a more descriptive endpoint that would support different types of prelogins. " + - "Use prelogin/password instead. This endpoint has no EOL at the time of writing.")] - public async Task PostPrelogin([FromBody] PasswordPreloginRequestModel model) - { - // Same as PostPasswordPrelogin to maintain compatibility. Do not make changes in this function body, - // only make changes in MakePasswordPreloginCall - return await MakePasswordPreloginCall(model); - } - - // There are two functions done this way because the open api docs that get generated in our build pipeline - // cannot handle two of the same post attributes on the same function call. That is why there is a - // PostPrelogin and the more appropriate PostPasswordPrelogin. - [HttpPost("prelogin/password")] - public async Task PostPasswordPrelogin([FromBody] PasswordPreloginRequestModel model) - { - // Same as PostPrelogin to maintain backwards compatibility. Do not make changes in this function body, - // only make changes in MakePasswordPreloginCall - return await MakePasswordPreloginCall(model); - } - - private async Task MakePasswordPreloginCall(PasswordPreloginRequestModel model) + public async Task PostPrelogin([FromBody] PreloginRequestModel model) { var kdfInformation = await _userRepository.GetKdfInformationByEmailAsync(model.Email); if (kdfInformation == null) { kdfInformation = GetDefaultKdf(model.Email); } - return new PasswordPreloginResponseModel(kdfInformation, model.Email); + return new PreloginResponseModel(kdfInformation); } [HttpGet("webauthn/assertion-options")] @@ -247,17 +228,19 @@ public class AccountsController : Controller { return _defaultKdfResults[0]; } - - // 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]; + 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]; + } } } diff --git a/src/Identity/Models/Request/Accounts/PasswordPreloginRequestModel.cs b/src/Identity/Models/Request/Accounts/PreloginRequestModel.cs similarity index 87% rename from src/Identity/Models/Request/Accounts/PasswordPreloginRequestModel.cs rename to src/Identity/Models/Request/Accounts/PreloginRequestModel.cs index b5cf5699c4..a7dba7ce1d 100644 --- a/src/Identity/Models/Request/Accounts/PasswordPreloginRequestModel.cs +++ b/src/Identity/Models/Request/Accounts/PreloginRequestModel.cs @@ -5,7 +5,7 @@ using System.ComponentModel.DataAnnotations; namespace Bit.Identity.Models.Request.Accounts; -public class PasswordPreloginRequestModel +public class PreloginRequestModel { [Required] [EmailAddress] diff --git a/src/Identity/Models/Response/Accounts/PasswordPreloginResponseModel.cs b/src/Identity/Models/Response/Accounts/PasswordPreloginResponseModel.cs deleted file mode 100644 index 747b268088..0000000000 --- a/src/Identity/Models/Response/Accounts/PasswordPreloginResponseModel.cs +++ /dev/null @@ -1,38 +0,0 @@ -using Bit.Core.Enums; -using Bit.Core.KeyManagement.Models.Data; -using Bit.Core.Models.Data; - -namespace Bit.Identity.Models.Response.Accounts; - -public class PasswordPreloginResponseModel -{ - public PasswordPreloginResponseModel(UserKdfInformation kdfInformation, string? salt = null) - { - // PM-28143 Cleanup - Kdf = kdfInformation.Kdf; - KdfIterations = kdfInformation.KdfIterations; - KdfMemory = kdfInformation.KdfMemory; - KdfParallelism = kdfInformation.KdfParallelism; - // End Cleanup - - KdfSettings = new KdfSettings() - { - KdfType = kdfInformation.Kdf, - Iterations = kdfInformation.KdfIterations, - Memory = kdfInformation.KdfMemory, - Parallelism = kdfInformation.KdfParallelism, - }; - Salt = salt; - } - - // Old Data Types - public KdfType? Kdf { get; set; } // PM-28143 Remove with cleanup - public int? KdfIterations { get; set; } // PM-28143 Remove with cleanup - public int? KdfMemory { get; set; } // PM-28143 Remove with cleanup - public int? KdfParallelism { get; set; } // PM-28143 Remove with cleanup - - // New Data Types - public KdfSettings? KdfSettings { get; set; } // PM-28143 With cleanup make this not nullish - public string? Salt { get; set; } // PM-28143 With cleanup make this not nullish. Not used yet, - // just the email from the request at this time. -} diff --git a/src/Identity/Models/Response/Accounts/PreloginResponseModel.cs b/src/Identity/Models/Response/Accounts/PreloginResponseModel.cs new file mode 100644 index 0000000000..129aa3e7a9 --- /dev/null +++ b/src/Identity/Models/Response/Accounts/PreloginResponseModel.cs @@ -0,0 +1,20 @@ +using Bit.Core.Enums; +using Bit.Core.Models.Data; + +namespace Bit.Identity.Models.Response.Accounts; + +public class PreloginResponseModel +{ + public PreloginResponseModel(UserKdfInformation kdfInformation) + { + Kdf = kdfInformation.Kdf; + KdfIterations = kdfInformation.KdfIterations; + KdfMemory = kdfInformation.KdfMemory; + KdfParallelism = kdfInformation.KdfParallelism; + } + + public KdfType Kdf { get; set; } + public int KdfIterations { get; set; } + public int? KdfMemory { get; set; } + public int? KdfParallelism { get; set; } +} diff --git a/test/Identity.Test/Controllers/AccountsControllerTests.cs b/test/Identity.Test/Controllers/AccountsControllerTests.cs index d089c8ec57..bff33cc679 100644 --- a/test/Identity.Test/Controllers/AccountsControllerTests.cs +++ b/test/Identity.Test/Controllers/AccountsControllerTests.cs @@ -75,7 +75,7 @@ public class AccountsControllerTests : IDisposable } [Fact] - public async Task PostPasswordPrelogin_WhenUserExists_ShouldReturnUserKdfInfo() + public async Task PostPrelogin_WhenUserExists_ShouldReturnUserKdfInfo() { var userKdfInfo = new UserKdfInformation { @@ -84,113 +84,30 @@ public class AccountsControllerTests : IDisposable }; _userRepository.GetKdfInformationByEmailAsync(Arg.Any()).Returns(userKdfInfo); - var response = await _sut.PostPasswordPrelogin(new PasswordPreloginRequestModel { Email = "user@example.com" }); + var response = await _sut.PostPrelogin(new PreloginRequestModel { Email = "user@example.com" }); Assert.Equal(userKdfInfo.Kdf, response.Kdf); Assert.Equal(userKdfInfo.KdfIterations, response.KdfIterations); } [Fact] - public async Task PostPrelogin_And_PostPasswordPrelogin_ShouldUseSamePreloginLogic() - { - // Arrange: No user exists and no default HMAC key to force default path - var email = "same-user@example.com"; - SetDefaultKdfHmacKey(null); - _userRepository.GetKdfInformationByEmailAsync(Arg.Any()).Returns(Task.FromResult(null)); - - // Act - var legacyResponse = await _sut.PostPrelogin(new PasswordPreloginRequestModel { Email = email }); - var newResponse = await _sut.PostPasswordPrelogin(new PasswordPreloginRequestModel { Email = email }); - - // Assert: Both endpoints yield identical results, implying shared logic path - Assert.Equal(legacyResponse.Kdf, newResponse.Kdf); - Assert.Equal(legacyResponse.KdfIterations, newResponse.KdfIterations); - Assert.Equal(legacyResponse.KdfMemory, newResponse.KdfMemory); - Assert.Equal(legacyResponse.KdfParallelism, newResponse.KdfParallelism); - Assert.Equal(legacyResponse.Salt, newResponse.Salt); - Assert.NotNull(legacyResponse.KdfSettings); - Assert.NotNull(newResponse.KdfSettings); - Assert.Equal(legacyResponse.KdfSettings!.KdfType, newResponse.KdfSettings!.KdfType); - Assert.Equal(legacyResponse.KdfSettings!.Iterations, newResponse.KdfSettings!.Iterations); - Assert.Equal(legacyResponse.KdfSettings!.Memory, newResponse.KdfSettings!.Memory); - Assert.Equal(legacyResponse.KdfSettings!.Parallelism, newResponse.KdfSettings!.Parallelism); - - // Both methods should consult the repository once each with the same email - await _userRepository.Received(2).GetKdfInformationByEmailAsync(Arg.Is(e => e == email)); - } - - [Fact] - public async Task PostPasswordPrelogin_WhenUserExists_ReturnsNewFieldsAlignedWithLegacy_Argon2() - { - var email = "user@example.com"; - var userKdfInfo = new UserKdfInformation - { - Kdf = KdfType.Argon2id, - KdfIterations = AuthConstants.ARGON2_ITERATIONS.Default, - KdfMemory = AuthConstants.ARGON2_MEMORY.Default, - KdfParallelism = AuthConstants.ARGON2_PARALLELISM.Default - }; - _userRepository.GetKdfInformationByEmailAsync(Arg.Any()).Returns(userKdfInfo); - - var response = await _sut.PostPasswordPrelogin(new PasswordPreloginRequestModel { Email = email }); - - // New fields exist and match repository values - Assert.NotNull(response.KdfSettings); - Assert.Equal(userKdfInfo.Kdf, response.KdfSettings!.KdfType); - Assert.Equal(userKdfInfo.KdfIterations, response.KdfSettings!.Iterations); - Assert.Equal(userKdfInfo.KdfMemory, response.KdfSettings!.Memory); - Assert.Equal(userKdfInfo.KdfParallelism, response.KdfSettings!.Parallelism); - - // New and legacy fields are aligned during migration - Assert.Equal(response.Kdf, response.KdfSettings!.KdfType); - Assert.Equal(response.KdfIterations, response.KdfSettings!.Iterations); - Assert.Equal(response.KdfMemory, response.KdfSettings!.Memory); - Assert.Equal(response.KdfParallelism, response.KdfSettings!.Parallelism); - - // Salt is set to the input email during migration - Assert.Equal(email, response.Salt); - } - - [Fact] - public async Task PostPasswordPrelogin_WhenUserDoesNotExistAndNoDefaultKdfHmacKeySet_ShouldDefaultToPBKDF() + public async Task PostPrelogin_WhenUserDoesNotExistAndNoDefaultKdfHmacKeySet_ShouldDefaultToPBKDF() { SetDefaultKdfHmacKey(null); _userRepository.GetKdfInformationByEmailAsync(Arg.Any()).Returns(Task.FromResult(null)); - var response = await _sut.PostPasswordPrelogin(new PasswordPreloginRequestModel { Email = "user@example.com" }); + var response = await _sut.PostPrelogin(new PreloginRequestModel { Email = "user@example.com" }); Assert.Equal(KdfType.PBKDF2_SHA256, response.Kdf); Assert.Equal(AuthConstants.PBKDF2_ITERATIONS.Default, response.KdfIterations); } - [Fact] - public async Task PostPasswordPrelogin_NoUser_NoDefaultHmacKey_ReturnsAlignedNewFieldsAndSalt() - { - var email = "user@example.com"; - SetDefaultKdfHmacKey(null); - _userRepository.GetKdfInformationByEmailAsync(Arg.Any()).Returns(Task.FromResult(null)); - - var response = await _sut.PostPasswordPrelogin(new PasswordPreloginRequestModel { Email = email }); - - // New fields exist - Assert.NotNull(response.KdfSettings); - - // New and legacy fields are aligned during migration - Assert.Equal(response.Kdf, response.KdfSettings!.KdfType); - Assert.Equal(response.KdfIterations, response.KdfSettings!.Iterations); - Assert.Equal(response.KdfMemory, response.KdfSettings!.Memory); - Assert.Equal(response.KdfParallelism, response.KdfSettings!.Parallelism); - - // Salt is set to the input email during migration - Assert.Equal(email, response.Salt); - } - [Theory] [BitAutoData] - public async Task PostPasswordPrelogin_WhenUserDoesNotExistAndDefaultKdfHmacKeyIsSet_ShouldComputeHmacAndReturnExpectedKdf(string email) + public async Task PostPrelogin_WhenUserDoesNotExistAndDefaultKdfHmacKeyIsSet_ShouldComputeHmacAndReturnExpectedKdf(string email) { // Arrange: - var defaultKey = "my-secret-key"u8.ToArray(); + var defaultKey = Encoding.UTF8.GetBytes("my-secret-key"); SetDefaultKdfHmacKey(defaultKey); _userRepository.GetKdfInformationByEmailAsync(Arg.Any()).Returns(Task.FromResult(null)); @@ -205,7 +122,7 @@ public class AccountsControllerTests : IDisposable var expectedKdf = defaultKdfResults[expectedIndex]; // Act - var response = await _sut.PostPasswordPrelogin(new PasswordPreloginRequestModel { Email = email }); + 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); @@ -215,16 +132,6 @@ public class AccountsControllerTests : IDisposable Assert.Equal(expectedKdf.KdfMemory, response.KdfMemory); Assert.Equal(expectedKdf.KdfParallelism, response.KdfParallelism); } - - // New and legacy fields are aligned during migration - Assert.NotNull(response.KdfSettings); - Assert.Equal(response.Kdf, response.KdfSettings!.KdfType); - Assert.Equal(response.KdfIterations, response.KdfSettings!.Iterations); - Assert.Equal(response.KdfMemory, response.KdfSettings!.Memory); - Assert.Equal(response.KdfParallelism, response.KdfSettings!.Parallelism); - - // Salt is set to the input email during migration - Assert.Equal(email, response.Salt); } [Theory]