From 0240c94ada998b86ba6c07cc2598e7fedaecfd0c Mon Sep 17 00:00:00 2001 From: Ike Kottlowski Date: Mon, 16 Mar 2026 16:50:44 -0400 Subject: [PATCH 1/3] feat: add null coalesce to GetMasterPasswordSalt and replace all email fallbacks to use GetMasterPasswordSalt --- src/Api/Vault/Models/Response/SyncResponseModel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Api/Vault/Models/Response/SyncResponseModel.cs b/src/Api/Vault/Models/Response/SyncResponseModel.cs index b292a10eb381..af5c86616024 100644 --- a/src/Api/Vault/Models/Response/SyncResponseModel.cs +++ b/src/Api/Vault/Models/Response/SyncResponseModel.cs @@ -84,7 +84,7 @@ public SyncResponseModel( Parallelism = user.KdfParallelism }, MasterKeyEncryptedUserKey = user.Key!, - Salt = user.Email.ToLowerInvariant() + Salt = user.GetMasterPasswordSalt() } : null, WebAuthnPrfOptions = webAuthnPrfOptions.Length > 0 ? webAuthnPrfOptions : null, From a323d75eb10a40f7c9bd684734a2b5c519ba82d9 Mon Sep 17 00:00:00 2001 From: Ike Kottlowski Date: Mon, 16 Mar 2026 16:50:46 -0400 Subject: [PATCH 2/3] test: add tests for --- .../Vault/Controllers/SyncControllerTests.cs | 24 ++++++++++++++++++- .../Models/Response/SyncResponseModelTests.cs | 24 ++++++++++++++++++- 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/test/Api.IntegrationTest/Vault/Controllers/SyncControllerTests.cs b/test/Api.IntegrationTest/Vault/Controllers/SyncControllerTests.cs index 16d4b0fb66d3..2e2a330366c0 100644 --- a/test/Api.IntegrationTest/Vault/Controllers/SyncControllerTests.cs +++ b/test/Api.IntegrationTest/Vault/Controllers/SyncControllerTests.cs @@ -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(); + + Assert.NotNull(syncResponseModel); + Assert.NotNull(syncResponseModel.UserDecryption?.MasterPasswordUnlock); + Assert.Equal("explicit-salt-value", syncResponseModel.UserDecryption.MasterPasswordUnlock.Salt); } } diff --git a/test/Api.Test/Vault/Models/Response/SyncResponseModelTests.cs b/test/Api.Test/Vault/Models/Response/SyncResponseModelTests.cs index f3f87eb673ac..c0bef82912dd 100644 --- a/test/Api.Test/Vault/Models/Response/SyncResponseModelTests.cs +++ b/test/Api.Test/Vault/Models/Response/SyncResponseModelTests.cs @@ -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; @@ -70,7 +71,7 @@ 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); @@ -78,6 +79,27 @@ public void Constructor_UserWithMasterPassword_SetsMasterPasswordUnlock(User use 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) From da8687ba55036e7ce908a3ab3b2ed72aeff0f6f2 Mon Sep 17 00:00:00 2001 From: Ike Kottlowski Date: Mon, 16 Mar 2026 16:50:49 -0400 Subject: [PATCH 3/3] test: update SyncControllerTest --- test/Api.Test/Vault/Controllers/SyncControllerTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/Api.Test/Vault/Controllers/SyncControllerTests.cs b/test/Api.Test/Vault/Controllers/SyncControllerTests.cs index e6d34592c7d1..85dc0e938bc7 100644 --- a/test/Api.Test/Vault/Controllers/SyncControllerTests.cs +++ b/test/Api.Test/Vault/Controllers/SyncControllerTests.cs @@ -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,