using System.ComponentModel.DataAnnotations; using Bit.Api.Auth.Models.Request.Accounts; using Bit.Api.KeyManagement.Models.Requests; using Bit.Core.Auth.Models.Api.Request.Accounts; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.KeyManagement.Models.Api.Request; using Bit.Test.Common.AutoFixture.Attributes; using Xunit; namespace Bit.Api.Test.Auth.Models.Request.Accounts; public class SetInitialPasswordRequestModelTests { #region V2 Validation Tests [Theory] [InlineData(KdfType.PBKDF2_SHA256, 600000, null, null)] [InlineData(KdfType.Argon2id, 3, 64, 4)] public void Validate_V2Request_WithMatchingKdf_ReturnsNoErrors(KdfType kdfType, int iterations, int? memory, int? parallelism) { // Arrange var kdf = new KdfRequestModel { KdfType = kdfType, Iterations = iterations, Memory = memory, Parallelism = parallelism }; var model = new SetInitialPasswordRequestModel { OrgIdentifier = "orgIdentifier", MasterPasswordAuthentication = new MasterPasswordAuthenticationDataRequestModel { Kdf = kdf, MasterPasswordAuthenticationHash = "authHash", Salt = "salt" }, MasterPasswordUnlock = new MasterPasswordUnlockDataRequestModel { Kdf = kdf, MasterKeyWrappedUserKey = "wrappedKey", Salt = "salt" }, AccountKeys = new AccountKeysRequestModel { UserKeyEncryptedAccountPrivateKey = "privateKey", AccountPublicKey = "publicKey" } }; // Act var result = model.Validate(new ValidationContext(model)); // Assert Assert.Empty(result); } [Theory] [BitAutoData] public void Validate_V2Request_WithMismatchedKdfSettings_ReturnsValidationError(string orgIdentifier) { // Arrange var model = new SetInitialPasswordRequestModel { OrgIdentifier = orgIdentifier, MasterPasswordAuthentication = new MasterPasswordAuthenticationDataRequestModel { Kdf = new KdfRequestModel { KdfType = KdfType.PBKDF2_SHA256, Iterations = 600000 }, MasterPasswordAuthenticationHash = "authHash", Salt = "salt" }, MasterPasswordUnlock = new MasterPasswordUnlockDataRequestModel { Kdf = new KdfRequestModel { KdfType = KdfType.PBKDF2_SHA256, Iterations = 650000 // Different iterations }, MasterKeyWrappedUserKey = "wrappedKey", Salt = "salt" } }; // Act var result = model.Validate(new ValidationContext(model)).ToList(); // Assert Assert.Single(result); Assert.Contains("KDF settings must be equal", result[0].ErrorMessage); var memberNames = result[0].MemberNames.ToList(); Assert.Equal(2, memberNames.Count); Assert.Contains("MasterPasswordAuthentication.Kdf", memberNames); Assert.Contains("MasterPasswordUnlock.Kdf", memberNames); } [Theory] [BitAutoData] public void Validate_V2Request_WithInvalidAuthenticationKdf_ReturnsValidationError(string orgIdentifier) { // Arrange var kdf = new KdfRequestModel { KdfType = KdfType.PBKDF2_SHA256, Iterations = 1 // Too low }; var model = new SetInitialPasswordRequestModel { OrgIdentifier = orgIdentifier, MasterPasswordAuthentication = new MasterPasswordAuthenticationDataRequestModel { Kdf = kdf, MasterPasswordAuthenticationHash = "authHash", Salt = "salt" }, MasterPasswordUnlock = new MasterPasswordUnlockDataRequestModel { Kdf = kdf, MasterKeyWrappedUserKey = "wrappedKey", Salt = "salt" } }; // Act var result = model.Validate(new ValidationContext(model)).ToList(); // Assert Assert.NotEmpty(result); Assert.Contains(result, r => r.ErrorMessage != null && r.ErrorMessage.Contains("KDF iterations must be between")); } #endregion #region V1 Validation Tests (Obsolete) [Theory] [BitAutoData] public void Validate_V1Request_WithMissingMasterPasswordHash_ReturnsValidationError(string orgIdentifier) { // Arrange var model = new SetInitialPasswordRequestModel { OrgIdentifier = orgIdentifier, Key = "key", Kdf = KdfType.PBKDF2_SHA256, KdfIterations = 600000 }; // Act var result = model.Validate(new ValidationContext(model)).ToList(); // Assert Assert.Contains(result, r => r.ErrorMessage.Contains("MasterPasswordHash must be supplied")); } [Theory] [BitAutoData] public void Validate_V1Request_WithMissingKey_ReturnsValidationError(string orgIdentifier) { // Arrange var model = new SetInitialPasswordRequestModel { OrgIdentifier = orgIdentifier, MasterPasswordHash = "hash", Kdf = KdfType.PBKDF2_SHA256, KdfIterations = 600000 }; // Act var result = model.Validate(new ValidationContext(model)).ToList(); // Assert Assert.Contains(result, r => r.ErrorMessage.Contains("Key must be supplied")); } [Theory] [BitAutoData] public void Validate_V1Request_WithMissingKdf_ReturnsValidationError(string orgIdentifier) { // Arrange var model = new SetInitialPasswordRequestModel { OrgIdentifier = orgIdentifier, MasterPasswordHash = "hash", Key = "key", KdfIterations = 600000 }; // Act var result = model.Validate(new ValidationContext(model)).ToList(); // Assert Assert.Contains(result, r => r.ErrorMessage != null && r.ErrorMessage.Contains("Kdf must be supplied")); } [Theory] [BitAutoData] public void Validate_V1Request_WithMissingKdfIterations_ReturnsValidationError(string orgIdentifier) { // Arrange var model = new SetInitialPasswordRequestModel { OrgIdentifier = orgIdentifier, MasterPasswordHash = "hash", Key = "key", Kdf = KdfType.PBKDF2_SHA256 }; // Act var result = model.Validate(new ValidationContext(model)).ToList(); // Assert Assert.Contains(result, r => r.ErrorMessage != null && r.ErrorMessage.Contains("KdfIterations must be supplied")); } [Theory] [BitAutoData] public void Validate_V1Request_WithArgon2idAndMissingMemory_ReturnsValidationError(string orgIdentifier) { // Arrange var model = new SetInitialPasswordRequestModel { OrgIdentifier = orgIdentifier, MasterPasswordHash = "hash", Key = "key", Kdf = KdfType.Argon2id, KdfIterations = 3, KdfParallelism = 4 }; // Act var result = model.Validate(new ValidationContext(model)).ToList(); // Assert Assert.Contains(result, r => r.ErrorMessage.Contains("KdfMemory must be supplied when Kdf is Argon2id")); } [Theory] [BitAutoData] public void Validate_V1Request_WithArgon2idAndMissingParallelism_ReturnsValidationError(string orgIdentifier) { // Arrange var model = new SetInitialPasswordRequestModel { OrgIdentifier = orgIdentifier, MasterPasswordHash = "hash", Key = "key", Kdf = KdfType.Argon2id, KdfIterations = 3, KdfMemory = 64 }; // Act var result = model.Validate(new ValidationContext(model)).ToList(); // Assert Assert.Contains(result, r => r.ErrorMessage.Contains("KdfParallelism must be supplied when Kdf is Argon2id")); } [Theory] [BitAutoData] public void Validate_V1Request_WithInvalidKdfSettings_ReturnsValidationError(string orgIdentifier) { // Arrange var model = new SetInitialPasswordRequestModel { OrgIdentifier = orgIdentifier, MasterPasswordHash = "hash", Key = "key", Kdf = KdfType.PBKDF2_SHA256, KdfIterations = 5000 // Too low }; // Act var result = model.Validate(new ValidationContext(model)).ToList(); // Assert Assert.NotEmpty(result); Assert.Contains(result, r => r.ErrorMessage != null && r.ErrorMessage.Contains("KDF iterations must be between")); } [Theory] [InlineData(KdfType.PBKDF2_SHA256, 600000, null, null)] [InlineData(KdfType.Argon2id, 3, 64, 4)] public void Validate_V1Request_WithValidSettings_ReturnsNoErrors(KdfType kdfType, int kdfIterations, int? kdfMemory, int? kdfParallelism) { // Arrange var model = new SetInitialPasswordRequestModel { OrgIdentifier = "orgIdentifier", MasterPasswordHash = "hash", Key = "key", Kdf = kdfType, KdfIterations = kdfIterations, KdfMemory = kdfMemory, KdfParallelism = kdfParallelism }; // Act var result = model.Validate(new ValidationContext(model)); // Assert Assert.Empty(result); } #endregion #region IsV2Request Tests [Theory] [BitAutoData] public void IsV2Request_WithV2Properties_ReturnsTrue(string orgIdentifier) { // Arrange var model = new SetInitialPasswordRequestModel { OrgIdentifier = orgIdentifier, MasterPasswordAuthentication = new MasterPasswordAuthenticationDataRequestModel { Kdf = new KdfRequestModel { KdfType = KdfType.PBKDF2_SHA256, Iterations = 600000 }, MasterPasswordAuthenticationHash = "authHash", Salt = "salt" }, MasterPasswordUnlock = new MasterPasswordUnlockDataRequestModel { Kdf = new KdfRequestModel { KdfType = KdfType.PBKDF2_SHA256, Iterations = 600000 }, MasterKeyWrappedUserKey = "wrappedKey", Salt = "salt" } }; // Act var result = model.IsV2Request(); // Assert Assert.True(result); } [Theory] [BitAutoData] public void IsV2Request_WithoutMasterPasswordAuthentication_ReturnsFalse(string orgIdentifier) { // Arrange var model = new SetInitialPasswordRequestModel { OrgIdentifier = orgIdentifier, MasterPasswordUnlock = new MasterPasswordUnlockDataRequestModel { Kdf = new KdfRequestModel { KdfType = KdfType.PBKDF2_SHA256, Iterations = 600000 }, MasterKeyWrappedUserKey = "wrappedKey", Salt = "salt" } }; // Act var result = model.IsV2Request(); // Assert Assert.False(result); } [Theory] [BitAutoData] public void IsV2Request_WithoutMasterPasswordUnlock_ReturnsFalse(string orgIdentifier) { // Arrange var model = new SetInitialPasswordRequestModel { OrgIdentifier = orgIdentifier, MasterPasswordAuthentication = new MasterPasswordAuthenticationDataRequestModel { Kdf = new KdfRequestModel { KdfType = KdfType.PBKDF2_SHA256, Iterations = 600000 }, MasterPasswordAuthenticationHash = "authHash", Salt = "salt" } }; // Act var result = model.IsV2Request(); // Assert Assert.False(result); } [Theory] [BitAutoData] public void IsV2Request_WithV1Properties_ReturnsFalse(string orgIdentifier) { // Arrange var model = new SetInitialPasswordRequestModel { OrgIdentifier = orgIdentifier, MasterPasswordHash = "hash", Key = "key", Kdf = KdfType.PBKDF2_SHA256, KdfIterations = 600000 }; // Act var result = model.IsV2Request(); // Assert Assert.False(result); } #endregion #region IsTdeSetPasswordRequest Tests [Theory] [BitAutoData] public void IsTdeSetPasswordRequest_WithNullAccountKeys_ReturnsTrue(string orgIdentifier) { // Arrange var model = new SetInitialPasswordRequestModel { OrgIdentifier = orgIdentifier, MasterPasswordAuthentication = new MasterPasswordAuthenticationDataRequestModel { Kdf = new KdfRequestModel { KdfType = KdfType.PBKDF2_SHA256, Iterations = 600000 }, MasterPasswordAuthenticationHash = "authHash", Salt = "salt" }, MasterPasswordUnlock = new MasterPasswordUnlockDataRequestModel { Kdf = new KdfRequestModel { KdfType = KdfType.PBKDF2_SHA256, Iterations = 600000 }, MasterKeyWrappedUserKey = "wrappedKey", Salt = "salt" }, AccountKeys = null }; // Act var result = model.IsTdeSetPasswordRequest(); // Assert Assert.True(result); } [Theory] [BitAutoData] public void IsTdeSetPasswordRequest_WithAccountKeys_ReturnsFalse(string orgIdentifier) { // Arrange var model = new SetInitialPasswordRequestModel { OrgIdentifier = orgIdentifier, MasterPasswordAuthentication = new MasterPasswordAuthenticationDataRequestModel { Kdf = new KdfRequestModel { KdfType = KdfType.PBKDF2_SHA256, Iterations = 600000 }, MasterPasswordAuthenticationHash = "authHash", Salt = "salt" }, MasterPasswordUnlock = new MasterPasswordUnlockDataRequestModel { Kdf = new KdfRequestModel { KdfType = KdfType.PBKDF2_SHA256, Iterations = 600000 }, MasterKeyWrappedUserKey = "wrappedKey", Salt = "salt" }, AccountKeys = new AccountKeysRequestModel { UserKeyEncryptedAccountPrivateKey = "privateKey", AccountPublicKey = "publicKey" } }; // Act var result = model.IsTdeSetPasswordRequest(); // Assert Assert.False(result); } #endregion #region ToUser Tests (Obsolete) [Theory] [InlineData(KdfType.PBKDF2_SHA256, 600000, null, null)] [InlineData(KdfType.Argon2id, 3, 64, 4)] public void ToUser_WithKeys_MapsPropertiesCorrectly(KdfType kdfType, int kdfIterations, int? kdfMemory, int? kdfParallelism) { // Arrange var existingUser = new User(); var model = new SetInitialPasswordRequestModel { OrgIdentifier = "orgIdentifier", MasterPasswordHash = "hash", MasterPasswordHint = "hint", Key = "key", Kdf = kdfType, KdfIterations = kdfIterations, KdfMemory = kdfMemory, KdfParallelism = kdfParallelism, Keys = new KeysRequestModel { PublicKey = "publicKey", EncryptedPrivateKey = "encryptedPrivateKey" } }; // Act var result = model.ToUser(existingUser); // Assert Assert.Same(existingUser, result); Assert.Equal("hint", result.MasterPasswordHint); Assert.Equal(kdfType, result.Kdf); Assert.Equal(kdfIterations, result.KdfIterations); Assert.Equal(kdfMemory, result.KdfMemory); Assert.Equal(kdfParallelism, result.KdfParallelism); Assert.Equal("key", result.Key); Assert.Equal("publicKey", result.PublicKey); Assert.Equal("encryptedPrivateKey", result.PrivateKey); } [Theory] [InlineData(KdfType.PBKDF2_SHA256, 600000, null, null)] [InlineData(KdfType.Argon2id, 3, 64, 4)] public void ToUser_WithoutKeys_MapsPropertiesCorrectly(KdfType kdfType, int kdfIterations, int? kdfMemory, int? kdfParallelism) { // Arrange var existingUser = new User(); var model = new SetInitialPasswordRequestModel { OrgIdentifier = "orgIdentifier", MasterPasswordHash = "hash", MasterPasswordHint = "hint", Key = "key", Kdf = kdfType, KdfIterations = kdfIterations, KdfMemory = kdfMemory, KdfParallelism = kdfParallelism, Keys = null }; // Act var result = model.ToUser(existingUser); // Assert Assert.Same(existingUser, result); Assert.Equal("hint", result.MasterPasswordHint); Assert.Equal(kdfType, result.Kdf); Assert.Equal(kdfIterations, result.KdfIterations); Assert.Equal(kdfMemory, result.KdfMemory); Assert.Equal(kdfParallelism, result.KdfParallelism); Assert.Equal("key", result.Key); Assert.Null(result.PublicKey); Assert.Null(result.PrivateKey); } #endregion #region ToData Tests [Theory] [BitAutoData] public void ToData_MapsPropertiesCorrectly(string orgIdentifier) { // Arrange var kdf = new KdfRequestModel { KdfType = KdfType.PBKDF2_SHA256, Iterations = 600000 }; var model = new SetInitialPasswordRequestModel { OrgIdentifier = orgIdentifier, MasterPasswordHint = "hint", MasterPasswordAuthentication = new MasterPasswordAuthenticationDataRequestModel { Kdf = kdf, MasterPasswordAuthenticationHash = "authHash", Salt = "salt" }, MasterPasswordUnlock = new MasterPasswordUnlockDataRequestModel { Kdf = kdf, MasterKeyWrappedUserKey = "wrappedKey", Salt = "salt" }, AccountKeys = new AccountKeysRequestModel { UserKeyEncryptedAccountPrivateKey = "privateKey", AccountPublicKey = "publicKey" } }; // Act var result = model.ToData(); // Assert Assert.NotNull(result); Assert.Equal(orgIdentifier, result.OrgSsoIdentifier); Assert.Equal("hint", result.MasterPasswordHint); Assert.NotNull(result.MasterPasswordAuthentication); Assert.NotNull(result.MasterPasswordUnlock); Assert.NotNull(result.AccountKeys); Assert.Equal("authHash", result.MasterPasswordAuthentication.MasterPasswordAuthenticationHash); Assert.Equal("wrappedKey", result.MasterPasswordUnlock.MasterKeyWrappedUserKey); } [Theory] [BitAutoData] public void ToData_WithNullAccountKeys_MapsCorrectly(string orgIdentifier) { // Arrange var kdf = new KdfRequestModel { KdfType = KdfType.PBKDF2_SHA256, Iterations = 600000 }; var model = new SetInitialPasswordRequestModel { OrgIdentifier = orgIdentifier, MasterPasswordHint = "hint", MasterPasswordAuthentication = new MasterPasswordAuthenticationDataRequestModel { Kdf = kdf, MasterPasswordAuthenticationHash = "authHash", Salt = "salt" }, MasterPasswordUnlock = new MasterPasswordUnlockDataRequestModel { Kdf = kdf, MasterKeyWrappedUserKey = "wrappedKey", Salt = "salt" }, AccountKeys = null }; // Act var result = model.ToData(); // Assert Assert.NotNull(result); Assert.Equal(orgIdentifier, result.OrgSsoIdentifier); Assert.Null(result.AccountKeys); } #endregion }