Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/Core/Entities/User.cs
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ public class User : ITableObject<Guid>, IStorableSubscriber, IRevisable, ITwoFac

public string GetMasterPasswordSalt()
{
return Email.ToLowerInvariant().Trim();
return MasterPasswordSalt ?? Email.ToLowerInvariant().Trim();
}

public void SetNewId()
Expand Down
1 change: 1 addition & 0 deletions src/Core/Models/Data/UserKdfInformation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ public class UserKdfInformation
public required int KdfIterations { get; set; }
public int? KdfMemory { get; set; }
public int? KdfParallelism { get; set; }
public string? MasterPasswordSalt { get; set; }
}
2 changes: 0 additions & 2 deletions src/Core/Repositories/IUserRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@
using Bit.Core.KeyManagement.UserKey;
using Bit.Core.Models.Data;

#nullable enable

namespace Bit.Core.Repositories;

public interface IUserRepository : IRepository<User, Guid>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,8 @@ public UserRepository(IServiceScopeFactory serviceScopeFactory, IMapper mapper)
Kdf = e.Kdf,
KdfIterations = e.KdfIterations,
KdfMemory = e.KdfMemory,
KdfParallelism = e.KdfParallelism
KdfParallelism = e.KdfParallelism,
MasterPasswordSalt = e.MasterPasswordSalt
}).SingleOrDefaultAsync();
}
}
Expand Down Expand Up @@ -307,7 +308,7 @@ public async Task SetV2AccountCryptographicStateAsync(
userEntity.SecurityVersion = accountKeysData.SecurityStateData.SecurityVersion;
userEntity.SignedPublicKey = accountKeysData.PublicKeyEncryptionKeyPairData.SignedPublicKey;

// Replace existing keypair if it exists
// Replace existing key-pair if it exists
var existingKeyPair = await dbContext.UserSignatureKeyPairs
.FirstOrDefaultAsync(x => x.UserId == userId);
if (existingKeyPair != null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,17 @@ BEGIN
U.[KdfIterations],
U.[KdfMemory],
U.[KdfParallelism],
U.[MasterPasswordSalt],
OU.[ResetPasswordKey],
O.[PrivateKey] AS EncryptedPrivateKey
FROM @OrganizationUserIds AS OUIDs
INNER JOIN [dbo].[OrganizationUser] AS OU
ON OUIDs.[Id] = OU.[Id]
INNER JOIN [dbo].[Organization] AS O
ON OU.[OrganizationId] = O.[Id]
INNER JOIN [dbo].[User] U
ON U.[Id] = OU.[UserId]
WHERE OU.[OrganizationId] = @OrganizationId
FROM
@OrganizationUserIds AS OUIDs
INNER JOIN
[dbo].[OrganizationUser] AS OU ON OUIDs.[Id] = OU.[Id]
INNER JOIN
[dbo].[Organization] AS O ON OU.[OrganizationId] = O.[Id]
INNER JOIN
[dbo].[User] U ON U.[Id] = OU.[UserId]
WHERE
OU.[OrganizationId] = @OrganizationId
END
3 changes: 2 additions & 1 deletion src/Sql/dbo/Stored Procedures/User_ReadKdfByEmail.sql
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ BEGIN
[Kdf],
[KdfIterations],
[KdfMemory],
[KdfParallelism]
[KdfParallelism],
[MasterPasswordSalt]
FROM
[dbo].[User]
WHERE
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,18 +33,6 @@ public void Constructor_ValidInputs_SetsAllPropertiesCorrectly(
Assert.Equal(grantor.GetMasterPasswordSalt(), model.Salt);
}

[Theory]
[BitAutoData]
public void Constructor_Salt_EqualsGrantorEmailLowercasedAndTrimmed(
EmergencyAccess emergencyAccess, User grantor)
{
grantor.Email = " TEST@Example.COM ";

var model = new EmergencyAccessTakeoverResponseModel(emergencyAccess, grantor);

Assert.Equal("test@example.com", model.Salt);
}

[Theory]
[InlineData("user@domain.com", "user@domain.com")]
[InlineData("USER@DOMAIN.COM", "user@domain.com")]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -740,6 +740,103 @@ public async Task UpdateUserKeyAndEncryptedDataV2Async_InvokesUpdateDataActions(
Assert.True(actionWasInvoked);
}

[Theory, DatabaseData]
public async Task GetKdfInformationByEmailAsync_WithPbkdf2User_ReturnsKdfInformation(
IUserRepository userRepository)
{
// Arrange
var email = $"test+{Guid.NewGuid()}@example.com";
var salt = "test-salt-value";
await userRepository.CreateAsync(new User
{
Name = "Test User",
Email = email,
ApiKey = "TEST",
SecurityStamp = "stamp",
MasterPassword = "password_hash",
MasterPasswordSalt = salt,
Kdf = KdfType.PBKDF2_SHA256,
KdfIterations = AuthConstants.PBKDF2_ITERATIONS.Default,
});

// Act
var result = await userRepository.GetKdfInformationByEmailAsync(email);

// Assert
Assert.NotNull(result);
Assert.Equal(KdfType.PBKDF2_SHA256, result.Kdf);
Assert.Equal(AuthConstants.PBKDF2_ITERATIONS.Default, result.KdfIterations);
Assert.Null(result.KdfMemory);
Assert.Null(result.KdfParallelism);
Assert.Equal(salt, result.MasterPasswordSalt);
}

[Theory, DatabaseData]
public async Task GetKdfInformationByEmailAsync_WithArgon2idUser_ReturnsKdfInformationWithMemoryAndParallelism(
IUserRepository userRepository)
{
// Arrange
var email = $"test+{Guid.NewGuid()}@example.com";
var salt = "argon2-salt-value";
await userRepository.CreateAsync(new User
{
Name = "Test User",
Email = email,
ApiKey = "TEST",
SecurityStamp = "stamp",
MasterPassword = "password_hash",
MasterPasswordSalt = salt,
Kdf = KdfType.Argon2id,
KdfIterations = AuthConstants.ARGON2_ITERATIONS.Default,
KdfMemory = AuthConstants.ARGON2_MEMORY.Default,
KdfParallelism = AuthConstants.ARGON2_PARALLELISM.Default,
});

// Act
var result = await userRepository.GetKdfInformationByEmailAsync(email);

// Assert
Assert.NotNull(result);
Assert.Equal(KdfType.Argon2id, result.Kdf);
Assert.Equal(AuthConstants.ARGON2_ITERATIONS.Default, result.KdfIterations);
Assert.Equal(AuthConstants.ARGON2_MEMORY.Default, result.KdfMemory);
Assert.Equal(AuthConstants.ARGON2_PARALLELISM.Default, result.KdfParallelism);
Assert.Equal(salt, result.MasterPasswordSalt);
}

[Theory, DatabaseData]
public async Task GetKdfInformationByEmailAsync_WithNoMasterPassword_ReturnsNullSalt(
IUserRepository userRepository)
{
// Arrange
var email = $"test+{Guid.NewGuid()}@example.com";
await userRepository.CreateAsync(new User
{
Name = "Test User",
Email = email,
ApiKey = "TEST",
SecurityStamp = "stamp",
});

// Act
var result = await userRepository.GetKdfInformationByEmailAsync(email);

// Assert
Assert.NotNull(result);
Assert.Null(result.MasterPasswordSalt);
}

[Theory, DatabaseData]
public async Task GetKdfInformationByEmailAsync_WithNonExistentEmail_ReturnsNull(
IUserRepository userRepository)
{
// Act
var result = await userRepository.GetKdfInformationByEmailAsync($"nonexistent+{Guid.NewGuid()}@example.com");

// Assert
Assert.Null(result);
}

private static async Task RunUpdateUserDataAsync(UpdateUserData task, Database database)
{
if (database.Type == SupportedDatabaseProviders.SqlServer && !database.UseEf)
Expand Down
18 changes: 18 additions & 0 deletions util/Migrator/DbScripts/2026-03-16_00_AlterReadKdfByEmail.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
CREATE OR ALTER PROCEDURE [dbo].[User_ReadKdfByEmail]
@Email NVARCHAR(256)
AS
BEGIN
SET NOCOUNT ON

SELECT
[Kdf],
[KdfIterations],
[KdfMemory],
[KdfParallelism],
[MasterPasswordSalt]
FROM
[dbo].[User]
WHERE
[Email] = @Email
END
GO
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
CREATE OR ALTER PROCEDURE [dbo].[OrganizationUser_ReadManyAccountRecoveryDetailsByOrganizationUserIds]
@OrganizationId UNIQUEIDENTIFIER,
@OrganizationUserIds AS [dbo].[GuidIdArray] READONLY
AS
BEGIN
SET NOCOUNT ON

SELECT
OU.[Id] AS OrganizationUserId,
U.[Kdf],
U.[KdfIterations],
U.[KdfMemory],
U.[KdfParallelism],
U.[MasterPasswordSalt],
OU.[ResetPasswordKey],
O.[PrivateKey] AS EncryptedPrivateKey
FROM
@OrganizationUserIds AS OUIDs
INNER JOIN
[dbo].[OrganizationUser] AS OU ON OUIDs.[Id] = OU.[Id]
INNER JOIN
[dbo].[Organization] AS O ON OU.[OrganizationId] = O.[Id]
INNER JOIN
[dbo].[User] U ON U.[Id] = OU.[UserId]
WHERE
OU.[OrganizationId] = @OrganizationId
END
GO
Loading