Skip to content
Draft
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/Api/Vault/Models/Response/SyncResponseModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ public SyncResponseModel(
Parallelism = user.KdfParallelism
},
MasterKeyEncryptedUserKey = user.Key!,
Salt = user.Email.ToLowerInvariant()
Salt = user.GetMasterPasswordSalt()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ CRITICAL: GetMasterPasswordSalt() never reads User.MasterPasswordSalt -- it always returns Email.ToLowerInvariant().Trim(), so the new explicit-salt tests will fail and the feature does not work.

Details and fix

Commit 0240c94ad message says "add null coalesce to GetMasterPasswordSalt" but the method body in src/Core/Entities/User.cs line 117-120 was never updated:

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

It should be:

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

Without this change, both Get_HaveExplicitMasterPasswordSalt_SaltReturnedInSync and Constructor_UserWithExplicitSalt_UsesMasterPasswordSalt will fail because they set MasterPasswordSalt = "explicit-salt-value" but the method ignores it and returns the email.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚨 CRITICAL β€” GetMasterPasswordSalt() in src/Core/Entities/User.cs:117-120 never checks the MasterPasswordSalt property β€” it always returns Email.ToLowerInvariant().Trim(). The two new tests that set MasterPasswordSalt = "explicit-salt-value" will fail.

Suggested fix in User.cs:

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

}
: null,
WebAuthnPrfOptions = webAuthnPrfOptions.Length > 0 ? webAuthnPrfOptions : null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,28 @@ public async Task Get_HaveMasterPassword_UserDecryptionMasterPasswordUnlockNotNu
Assert.Equal(kdfMemory, syncResponseModel.UserDecryption.MasterPasswordUnlock.Kdf.Memory);
Assert.Equal(kdfParallelism, syncResponseModel.UserDecryption.MasterPasswordUnlock.Kdf.Parallelism);
Assert.Equal(user.Key, syncResponseModel.UserDecryption.MasterPasswordUnlock.MasterKeyEncryptedUserKey);
Assert.Equal(user.Email.ToLower(), syncResponseModel.UserDecryption.MasterPasswordUnlock.Salt);
Assert.Equal(user.GetMasterPasswordSalt(), syncResponseModel.UserDecryption.MasterPasswordUnlock.Salt);
}

[Fact]
public async Task Get_HaveExplicitMasterPasswordSalt_SaltReturnedInSync()
{
var tempEmail = $"integration-test{Guid.NewGuid()}@bitwarden.com";
await _factory.LoginWithNewAccount(tempEmail);
await _loginHelper.LoginAsync(tempEmail);

var user = await _userRepository.GetByEmailAsync(tempEmail);
Assert.NotNull(user);
user.MasterPasswordSalt = "explicit-salt-value";
await _userRepository.UpsertAsync(user);

var response = await _client.GetAsync("/sync");
response.EnsureSuccessStatusCode();

var syncResponseModel = await response.Content.ReadFromJsonAsync<SyncResponseModel>();

Assert.NotNull(syncResponseModel);
Assert.NotNull(syncResponseModel.UserDecryption?.MasterPasswordUnlock);
Assert.Equal("explicit-salt-value", syncResponseModel.UserDecryption.MasterPasswordUnlock.Salt);
}
}
2 changes: 1 addition & 1 deletion test/Api.Test/Vault/Controllers/SyncControllerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -402,7 +402,7 @@ public async Task Get_HaveMasterPassword_UserDecryptionMasterPasswordUnlockNotNu
Assert.Equal(kdfMemory, result.UserDecryption.MasterPasswordUnlock.Kdf.Memory);
Assert.Equal(kdfParallelism, result.UserDecryption.MasterPasswordUnlock.Kdf.Parallelism);
Assert.Equal(user.Key, result.UserDecryption.MasterPasswordUnlock.MasterKeyEncryptedUserKey);
Assert.Equal(user.Email.ToLower(), result.UserDecryption.MasterPasswordUnlock.Salt);
Assert.Equal(user.GetMasterPasswordSalt(), result.UserDecryption.MasterPasswordUnlock.Salt);
}

private async Task AssertMethodsCalledAsync(IUserService userService,
Expand Down
24 changes: 23 additions & 1 deletion test/Api.Test/Vault/Models/Response/SyncResponseModelTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ public void Constructor_UserWithMasterPassword_SetsMasterPasswordUnlock(User use
// Arrange
user.MasterPassword = "hashed-password";
user.Key = _mockEncryptedKey1;
user.MasterPasswordSalt = null;
user.Kdf = KdfType.Argon2id;
user.KdfIterations = 3;
user.KdfMemory = 64;
Expand All @@ -70,14 +71,35 @@ public void Constructor_UserWithMasterPassword_SetsMasterPasswordUnlock(User use
Assert.NotNull(result.UserDecryption);
Assert.NotNull(result.UserDecryption.MasterPasswordUnlock);
Assert.Equal(_mockEncryptedKey1, result.UserDecryption.MasterPasswordUnlock.MasterKeyEncryptedUserKey);
Assert.Equal(user.Email.ToLowerInvariant(), result.UserDecryption.MasterPasswordUnlock.Salt);
Assert.Equal(user.GetMasterPasswordSalt(), result.UserDecryption.MasterPasswordUnlock.Salt);
Assert.NotNull(result.UserDecryption.MasterPasswordUnlock.Kdf);
Assert.Equal(KdfType.Argon2id, result.UserDecryption.MasterPasswordUnlock.Kdf.KdfType);
Assert.Equal(3, result.UserDecryption.MasterPasswordUnlock.Kdf.Iterations);
Assert.Equal(64, result.UserDecryption.MasterPasswordUnlock.Kdf.Memory);
Assert.Equal(4, result.UserDecryption.MasterPasswordUnlock.Kdf.Parallelism);
}

[Theory]
[BitAutoData]
public void Constructor_UserWithExplicitSalt_UsesMasterPasswordSalt(User user)
{
// Arrange
user.MasterPassword = "hashed-password";
user.Key = _mockEncryptedKey1;
user.MasterPasswordSalt = "explicit-salt-value";
user.Kdf = KdfType.Argon2id;
user.KdfIterations = 3;
user.KdfMemory = 64;
user.KdfParallelism = 4;

// Act
var result = CreateSyncResponseModel(user);

// Assert
Assert.NotNull(result.UserDecryption?.MasterPasswordUnlock);
Assert.Equal("explicit-salt-value", result.UserDecryption.MasterPasswordUnlock.Salt);
}

[Theory]
[BitAutoData]
public void Constructor_UserWithoutMasterPassword_MasterPasswordUnlockIsNull(User user)
Expand Down
Loading