From d5aac9bac19ced44765c2bff79db703241a45a2a Mon Sep 17 00:00:00 2001 From: Patrick Pimentel Date: Mon, 2 Feb 2026 21:00:56 -0500 Subject: [PATCH 1/2] feat(emergency-access): [PM-31636] Emergency Access Takeover Salt - Added salt to response of emergency access takeover response. --- src/Api/Auth/Models/Response/EmergencyAccessResponseModel.cs | 2 ++ src/Core/Entities/User.cs | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Api/Auth/Models/Response/EmergencyAccessResponseModel.cs b/src/Api/Auth/Models/Response/EmergencyAccessResponseModel.cs index 640c9bb3e062..dff766da12dd 100644 --- a/src/Api/Auth/Models/Response/EmergencyAccessResponseModel.cs +++ b/src/Api/Auth/Models/Response/EmergencyAccessResponseModel.cs @@ -112,6 +112,7 @@ public EmergencyAccessTakeoverResponseModel(EmergencyAccess emergencyAccess, Use KdfIterations = grantor.KdfIterations; KdfMemory = grantor.KdfMemory; KdfParallelism = grantor.KdfParallelism; + Salt = grantor.GetMasterPasswordSalt(); } public int KdfIterations { get; private set; } @@ -119,6 +120,7 @@ public EmergencyAccessTakeoverResponseModel(EmergencyAccess emergencyAccess, Use public int? KdfParallelism { get; private set; } public KdfType Kdf { get; private set; } public string KeyEncrypted { get; private set; } + public string Salt { get; private set; } } public class EmergencyAccessViewResponseModel : ResponseModel diff --git a/src/Core/Entities/User.cs b/src/Core/Entities/User.cs index 669e32bcbe8b..4eb22f8f6f2d 100644 --- a/src/Core/Entities/User.cs +++ b/src/Core/Entities/User.cs @@ -51,7 +51,7 @@ public class User : ITableObject, IStorableSubscriber, IRevisable, ITwoFac public string? Key { get; set; } /// /// The raw public key, without a signature from the user's signature key. - /// + /// public string? PublicKey { get; set; } /// /// User key wrapped private key. From c4a601a54e9838e260e466dd78f1018ad0f4c54b Mon Sep 17 00:00:00 2001 From: Patrick Pimentel Date: Mon, 2 Feb 2026 21:08:33 -0500 Subject: [PATCH 2/2] test(emergency-access): [PM-31636] Emergency Access Takeover Salt - Added tests. --- ...ergencyAccessTakeoverResponseModelTests.cs | 129 ++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 test/Api.Test/Auth/Models/Response/EmergencyAccessTakeoverResponseModelTests.cs diff --git a/test/Api.Test/Auth/Models/Response/EmergencyAccessTakeoverResponseModelTests.cs b/test/Api.Test/Auth/Models/Response/EmergencyAccessTakeoverResponseModelTests.cs new file mode 100644 index 000000000000..1a46cb195655 --- /dev/null +++ b/test/Api.Test/Auth/Models/Response/EmergencyAccessTakeoverResponseModelTests.cs @@ -0,0 +1,129 @@ +using Bit.Api.Auth.Models.Response; +using Bit.Core.Auth.Entities; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Test.Common.AutoFixture.Attributes; +using Xunit; + +namespace Bit.Api.Test.Auth.Models.Response; + +public class EmergencyAccessTakeoverResponseModelTests +{ + [Theory] + [BitAutoData] + public void Constructor_EmergencyAccessNull_ThrowsArgumentNullException(User grantor) + { + var exception = Assert.Throws( + () => new EmergencyAccessTakeoverResponseModel(null, grantor)); + Assert.Equal("emergencyAccess", exception.ParamName); + } + + [Theory] + [BitAutoData] + public void Constructor_ValidInputs_SetsAllPropertiesCorrectly( + EmergencyAccess emergencyAccess, User grantor) + { + var model = new EmergencyAccessTakeoverResponseModel(emergencyAccess, grantor); + + Assert.Equal(emergencyAccess.KeyEncrypted, model.KeyEncrypted); + Assert.Equal(grantor.Kdf, model.Kdf); + Assert.Equal(grantor.KdfIterations, model.KdfIterations); + Assert.Equal(grantor.KdfMemory, model.KdfMemory); + Assert.Equal(grantor.KdfParallelism, model.KdfParallelism); + 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")] + [InlineData(" user@domain.com ", "user@domain.com")] + [InlineData(" USER@DOMAIN.COM ", "user@domain.com")] + public void Constructor_SaltWithVariousEmailFormats_NormalizesCorrectly( + string email, string expectedSalt) + { + var emergencyAccess = new EmergencyAccess + { + Id = Guid.NewGuid(), + KeyEncrypted = "test-key-encrypted" + }; + var grantor = new User + { + Id = Guid.NewGuid(), + Email = email, + SecurityStamp = "security-stamp", + ApiKey = "api-key" + }; + + var model = new EmergencyAccessTakeoverResponseModel(emergencyAccess, grantor); + + Assert.Equal(expectedSalt, model.Salt); + } + + [Theory] + [BitAutoData] + public void Constructor_WithPBKDF2_SetsKdfTypeCorrectly( + EmergencyAccess emergencyAccess, User grantor) + { + grantor.Kdf = KdfType.PBKDF2_SHA256; + grantor.KdfIterations = 600000; + grantor.KdfMemory = null; + grantor.KdfParallelism = null; + + var model = new EmergencyAccessTakeoverResponseModel(emergencyAccess, grantor); + + Assert.Equal(KdfType.PBKDF2_SHA256, model.Kdf); + Assert.Equal(600000, model.KdfIterations); + Assert.Null(model.KdfMemory); + Assert.Null(model.KdfParallelism); + } + + [Theory] + [BitAutoData] + public void Constructor_WithArgon2id_SetsAllKdfPropertiesCorrectly( + EmergencyAccess emergencyAccess, User grantor) + { + grantor.Kdf = KdfType.Argon2id; + grantor.KdfIterations = 3; + grantor.KdfMemory = 64; + grantor.KdfParallelism = 4; + + var model = new EmergencyAccessTakeoverResponseModel(emergencyAccess, grantor); + + Assert.Equal(KdfType.Argon2id, model.Kdf); + Assert.Equal(3, model.KdfIterations); + Assert.Equal(64, model.KdfMemory); + Assert.Equal(4, model.KdfParallelism); + } + + [Theory] + [BitAutoData] + public void Constructor_SetsObjectTypeCorrectly( + EmergencyAccess emergencyAccess, User grantor) + { + var model = new EmergencyAccessTakeoverResponseModel(emergencyAccess, grantor); + + Assert.Equal("emergencyAccessTakeover", model.Object); + } + + [Theory] + [BitAutoData] + public void Constructor_CustomObjectName_SetsObjectTypeCorrectly( + EmergencyAccess emergencyAccess, User grantor) + { + var model = new EmergencyAccessTakeoverResponseModel(emergencyAccess, grantor, "customObject"); + + Assert.Equal("customObject", model.Object); + } +}