mirror of
https://github.com/bitwarden/server.git
synced 2026-06-01 01:55:55 -05:00
* feat(kdf-settings-validator): Enforce salt cannot be empty string. * fix(kdf-settings-validator): Prefer IsNullOrWhitespace. * feat(salt): Make AllowEmptyStrings false for request models.
341 lines
14 KiB
C#
341 lines
14 KiB
C#
using Bit.Core.Enums;
|
|
using Bit.Core.KeyManagement.Models.Data;
|
|
using Bit.Core.Utilities;
|
|
using Xunit;
|
|
|
|
namespace Bit.Api.Test.Utilities;
|
|
|
|
public class KdfSettingsValidatorTests
|
|
{
|
|
[Theory]
|
|
[InlineData(KdfType.PBKDF2_SHA256, 1_000_000, null, null)] // Somewhere in the middle
|
|
[InlineData(KdfType.PBKDF2_SHA256, 600_000, null, null)] // Right on the lower boundary
|
|
[InlineData(KdfType.PBKDF2_SHA256, 2_000_000, null, null)] // Right on the upper boundary
|
|
[InlineData(KdfType.Argon2id, 5, 500, 8)] // Somewhere in the middle
|
|
[InlineData(KdfType.Argon2id, 2, 15, 1)] // Right on the lower boundary
|
|
[InlineData(KdfType.Argon2id, 10, 1024, 16)] // Right on the upper boundary
|
|
public void Validate_IsValid(KdfType kdfType, int kdfIterations, int? kdfMemory, int? kdfParallelism)
|
|
{
|
|
var results = KdfSettingsValidator.Validate(kdfType, kdfIterations, kdfMemory, kdfParallelism);
|
|
Assert.Empty(results);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(KdfType.PBKDF2_SHA256, 500_000, null, null, 1)] // Too few iterations
|
|
[InlineData(KdfType.PBKDF2_SHA256, 2_000_001, null, null, 1)] // Too many iterations
|
|
[InlineData(KdfType.Argon2id, 0, 30, 8, 1)] // Iterations must be greater than 0
|
|
[InlineData(KdfType.Argon2id, 10, 14, 8, 1)] // Too little memory
|
|
[InlineData(KdfType.Argon2id, 10, 14, 0, 1)] // Too small of a parallelism value
|
|
[InlineData(KdfType.Argon2id, 10, 1025, 8, 1)] // Too much memory
|
|
[InlineData(KdfType.Argon2id, 10, 512, 17, 1)] // Too big of a parallelism value
|
|
public void Validate_Fails(KdfType kdfType, int kdfIterations, int? kdfMemory, int? kdfParallelism, int expectedFailures)
|
|
{
|
|
var results = KdfSettingsValidator.Validate(kdfType, kdfIterations, kdfMemory, kdfParallelism);
|
|
Assert.NotEmpty(results);
|
|
Assert.Equal(expectedFailures, results.Count());
|
|
}
|
|
|
|
[Fact]
|
|
public void ValidateAuthenticationAndUnlockData_WhenMatchingKdfAndSalt_ReturnsNoErrors()
|
|
{
|
|
var kdf = new KdfSettings { KdfType = KdfType.PBKDF2_SHA256, Iterations = 600_000 };
|
|
var authentication = new MasterPasswordAuthenticationData
|
|
{
|
|
Kdf = kdf,
|
|
MasterPasswordAuthenticationHash = "hash",
|
|
Salt = "salt"
|
|
};
|
|
var unlock = new MasterPasswordUnlockData
|
|
{
|
|
Kdf = new KdfSettings { KdfType = KdfType.PBKDF2_SHA256, Iterations = 600_000 },
|
|
MasterKeyWrappedUserKey = "wrapped",
|
|
Salt = "salt"
|
|
};
|
|
|
|
var results = KdfSettingsValidator.ValidateAuthenticationAndUnlockData(authentication, unlock).ToList();
|
|
|
|
Assert.Empty(results);
|
|
}
|
|
|
|
[Fact]
|
|
public void ValidateAuthenticationAndUnlockData_WhenKdfMismatch_ReturnsKdfEqualityError()
|
|
{
|
|
var authentication = new MasterPasswordAuthenticationData
|
|
{
|
|
Kdf = new KdfSettings { KdfType = KdfType.PBKDF2_SHA256, Iterations = 600_000 },
|
|
MasterPasswordAuthenticationHash = "hash",
|
|
Salt = "salt"
|
|
};
|
|
var unlock = new MasterPasswordUnlockData
|
|
{
|
|
Kdf = new KdfSettings { KdfType = KdfType.Argon2id, Iterations = 3, Memory = 64, Parallelism = 4 },
|
|
MasterKeyWrappedUserKey = "wrapped",
|
|
Salt = "salt"
|
|
};
|
|
|
|
var results = KdfSettingsValidator.ValidateAuthenticationAndUnlockData(authentication, unlock).ToList();
|
|
|
|
Assert.Single(results);
|
|
Assert.Equal("AuthenticationData and UnlockData must have the same KDF configuration.", results[0].ErrorMessage);
|
|
}
|
|
|
|
[Fact]
|
|
public void ValidateAuthenticationAndUnlockData_WhenSaltMismatch_ReturnsSaltEqualityError()
|
|
{
|
|
var authentication = new MasterPasswordAuthenticationData
|
|
{
|
|
Kdf = new KdfSettings { KdfType = KdfType.PBKDF2_SHA256, Iterations = 600_000 },
|
|
MasterPasswordAuthenticationHash = "hash",
|
|
Salt = "salt-auth"
|
|
};
|
|
var unlock = new MasterPasswordUnlockData
|
|
{
|
|
Kdf = new KdfSettings { KdfType = KdfType.PBKDF2_SHA256, Iterations = 600_000 },
|
|
MasterKeyWrappedUserKey = "wrapped",
|
|
Salt = "salt-unlock"
|
|
};
|
|
|
|
var results = KdfSettingsValidator.ValidateAuthenticationAndUnlockData(authentication, unlock).ToList();
|
|
|
|
Assert.Single(results);
|
|
Assert.Equal("Invalid master password salt.", results[0].ErrorMessage);
|
|
}
|
|
|
|
[Fact]
|
|
public void ValidateAuthenticationAndUnlockData_WhenKdfInvalid_ReturnsKdfValidationError()
|
|
{
|
|
// Matching but out-of-range KDF (iterations below minimum for PBKDF2)
|
|
var authentication = new MasterPasswordAuthenticationData
|
|
{
|
|
Kdf = new KdfSettings { KdfType = KdfType.PBKDF2_SHA256, Iterations = 100 },
|
|
MasterPasswordAuthenticationHash = "hash",
|
|
Salt = "salt"
|
|
};
|
|
var unlock = new MasterPasswordUnlockData
|
|
{
|
|
Kdf = new KdfSettings { KdfType = KdfType.PBKDF2_SHA256, Iterations = 100 },
|
|
MasterKeyWrappedUserKey = "wrapped",
|
|
Salt = "salt"
|
|
};
|
|
|
|
var results = KdfSettingsValidator.ValidateAuthenticationAndUnlockData(authentication, unlock).ToList();
|
|
|
|
Assert.Single(results);
|
|
Assert.Contains("KDF iterations must be between", results[0].ErrorMessage);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("")]
|
|
[InlineData(" ")]
|
|
[InlineData("\t")]
|
|
[InlineData("\n")]
|
|
public void ValidateAuthenticationAndUnlockData_WhenAuthSaltIsEmptyOrWhitespace_ReturnsSaltError(string salt)
|
|
{
|
|
var authentication = new MasterPasswordAuthenticationData
|
|
{
|
|
Kdf = new KdfSettings { KdfType = KdfType.PBKDF2_SHA256, Iterations = 600_000 },
|
|
MasterPasswordAuthenticationHash = "hash",
|
|
Salt = salt
|
|
};
|
|
var unlock = new MasterPasswordUnlockData
|
|
{
|
|
Kdf = new KdfSettings { KdfType = KdfType.PBKDF2_SHA256, Iterations = 600_000 },
|
|
MasterKeyWrappedUserKey = "wrapped",
|
|
Salt = "salt"
|
|
};
|
|
|
|
var results = KdfSettingsValidator.ValidateAuthenticationAndUnlockData(authentication, unlock).ToList();
|
|
|
|
Assert.Single(results);
|
|
Assert.Equal("Master password salt must not be empty.", results[0].ErrorMessage);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("")]
|
|
[InlineData(" ")]
|
|
[InlineData("\t")]
|
|
[InlineData("\n")]
|
|
public void ValidateAuthenticationAndUnlockData_WhenUnlockSaltIsEmptyOrWhitespace_ReturnsSaltError(string salt)
|
|
{
|
|
var authentication = new MasterPasswordAuthenticationData
|
|
{
|
|
Kdf = new KdfSettings { KdfType = KdfType.PBKDF2_SHA256, Iterations = 600_000 },
|
|
MasterPasswordAuthenticationHash = "hash",
|
|
Salt = "salt"
|
|
};
|
|
var unlock = new MasterPasswordUnlockData
|
|
{
|
|
Kdf = new KdfSettings { KdfType = KdfType.PBKDF2_SHA256, Iterations = 600_000 },
|
|
MasterKeyWrappedUserKey = "wrapped",
|
|
Salt = salt
|
|
};
|
|
|
|
var results = KdfSettingsValidator.ValidateAuthenticationAndUnlockData(authentication, unlock).ToList();
|
|
|
|
Assert.Single(results);
|
|
Assert.Equal("Master password salt must not be empty.", results[0].ErrorMessage);
|
|
}
|
|
|
|
[Fact]
|
|
public void ValidateKdfAndSaltAgreement_WhenMatchingKdfAndSalt_ReturnsNoErrors()
|
|
{
|
|
var kdf = new KdfSettings { KdfType = KdfType.PBKDF2_SHA256, Iterations = 600_000 };
|
|
var authentication = new MasterPasswordAuthenticationData
|
|
{
|
|
Kdf = kdf,
|
|
MasterPasswordAuthenticationHash = "hash",
|
|
Salt = "salt"
|
|
};
|
|
var unlock = new MasterPasswordUnlockData
|
|
{
|
|
Kdf = new KdfSettings { KdfType = KdfType.PBKDF2_SHA256, Iterations = 600_000 },
|
|
MasterKeyWrappedUserKey = "wrapped",
|
|
Salt = "salt"
|
|
};
|
|
|
|
var results = KdfSettingsValidator.ValidateKdfAndSaltAgreement(authentication, unlock).ToList();
|
|
|
|
Assert.Empty(results);
|
|
}
|
|
|
|
[Fact]
|
|
public void ValidateKdfAndSaltAgreement_WhenKdfMismatch_ReturnsKdfEqualityErrorAndShortCircuits()
|
|
{
|
|
var authentication = new MasterPasswordAuthenticationData
|
|
{
|
|
Kdf = new KdfSettings { KdfType = KdfType.PBKDF2_SHA256, Iterations = 600_000 },
|
|
MasterPasswordAuthenticationHash = "hash",
|
|
Salt = "salt-auth"
|
|
};
|
|
var unlock = new MasterPasswordUnlockData
|
|
{
|
|
Kdf = new KdfSettings { KdfType = KdfType.Argon2id, Iterations = 3, Memory = 64, Parallelism = 4 },
|
|
MasterKeyWrappedUserKey = "wrapped",
|
|
Salt = "salt-unlock"
|
|
};
|
|
|
|
var results = KdfSettingsValidator.ValidateKdfAndSaltAgreement(authentication, unlock).ToList();
|
|
|
|
// Asserts both that the KDF mismatch is reported AND that the salt-mismatch
|
|
// check does not also fire — KDF mismatch should short-circuit the validator,
|
|
// matching the behavior of ValidateAuthenticationAndUnlockData.
|
|
Assert.Single(results);
|
|
Assert.Equal("AuthenticationData and UnlockData must have the same KDF configuration.", results[0].ErrorMessage);
|
|
}
|
|
|
|
[Fact]
|
|
public void ValidateKdfAndSaltAgreement_WhenSaltMismatch_ReturnsSaltEqualityError()
|
|
{
|
|
var authentication = new MasterPasswordAuthenticationData
|
|
{
|
|
Kdf = new KdfSettings { KdfType = KdfType.PBKDF2_SHA256, Iterations = 600_000 },
|
|
MasterPasswordAuthenticationHash = "hash",
|
|
Salt = "salt-auth"
|
|
};
|
|
var unlock = new MasterPasswordUnlockData
|
|
{
|
|
Kdf = new KdfSettings { KdfType = KdfType.PBKDF2_SHA256, Iterations = 600_000 },
|
|
MasterKeyWrappedUserKey = "wrapped",
|
|
Salt = "salt-unlock"
|
|
};
|
|
|
|
var results = KdfSettingsValidator.ValidateKdfAndSaltAgreement(authentication, unlock).ToList();
|
|
|
|
Assert.Single(results);
|
|
Assert.Equal("Invalid master password salt.", results[0].ErrorMessage);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("")]
|
|
[InlineData(" ")]
|
|
[InlineData("\t")]
|
|
[InlineData("\n")]
|
|
public void ValidateKdfAndSaltAgreement_WhenAuthSaltIsEmptyOrWhitespace_ReturnsSaltError(string salt)
|
|
{
|
|
var authentication = new MasterPasswordAuthenticationData
|
|
{
|
|
Kdf = new KdfSettings { KdfType = KdfType.PBKDF2_SHA256, Iterations = 600_000 },
|
|
MasterPasswordAuthenticationHash = "hash",
|
|
Salt = salt
|
|
};
|
|
var unlock = new MasterPasswordUnlockData
|
|
{
|
|
Kdf = new KdfSettings { KdfType = KdfType.PBKDF2_SHA256, Iterations = 600_000 },
|
|
MasterKeyWrappedUserKey = "wrapped",
|
|
Salt = "salt"
|
|
};
|
|
|
|
var results = KdfSettingsValidator.ValidateKdfAndSaltAgreement(authentication, unlock).ToList();
|
|
|
|
Assert.Single(results);
|
|
Assert.Equal("Master password salt must not be empty.", results[0].ErrorMessage);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("")]
|
|
[InlineData(" ")]
|
|
[InlineData("\t")]
|
|
[InlineData("\n")]
|
|
public void ValidateKdfAndSaltAgreement_WhenUnlockSaltIsEmptyOrWhitespace_ReturnsSaltError(string salt)
|
|
{
|
|
var authentication = new MasterPasswordAuthenticationData
|
|
{
|
|
Kdf = new KdfSettings { KdfType = KdfType.PBKDF2_SHA256, Iterations = 600_000 },
|
|
MasterPasswordAuthenticationHash = "hash",
|
|
Salt = "salt"
|
|
};
|
|
var unlock = new MasterPasswordUnlockData
|
|
{
|
|
Kdf = new KdfSettings { KdfType = KdfType.PBKDF2_SHA256, Iterations = 600_000 },
|
|
MasterKeyWrappedUserKey = "wrapped",
|
|
Salt = salt
|
|
};
|
|
|
|
var results = KdfSettingsValidator.ValidateKdfAndSaltAgreement(authentication, unlock).ToList();
|
|
|
|
Assert.Single(results);
|
|
Assert.Equal("Master password salt must not be empty.", results[0].ErrorMessage);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(KdfType.PBKDF2_SHA256, 1, null, null)] // far below minimum
|
|
[InlineData(KdfType.PBKDF2_SHA256, 100_000, null, null)] // sub-minimum but plausible legacy
|
|
[InlineData(KdfType.Argon2id, 1, 8, 1)] // sub-minimum Argon2id
|
|
public void ValidateKdfAndSaltAgreement_WhenKdfBelowMinimum_DoesNotEmitRangeError(
|
|
KdfType kdfType, int iterations, int? memory, int? parallelism)
|
|
{
|
|
// PM-27892 regression guard: this validator MUST NOT range-check the KDF — it
|
|
// exists specifically so legacy-KDF users (whose stored KDF predates current
|
|
// minimums) are not locked out of flows backed by UpdateExistingPasswordData
|
|
// (change password, replace temp password, emergency-access takeover for
|
|
// grantors with master password). Range enforcement at the boundary is the
|
|
// job of ValidateAuthenticationAndUnlockData and is reserved for KDF-being-
|
|
// chosen flows. If this test fails, the validator has been incorrectly
|
|
// expanded to perform range validation and legacy-KDF users will be silently
|
|
// rejected at the boundary.
|
|
var kdf = new KdfSettings
|
|
{
|
|
KdfType = kdfType,
|
|
Iterations = iterations,
|
|
Memory = memory,
|
|
Parallelism = parallelism
|
|
};
|
|
var authentication = new MasterPasswordAuthenticationData
|
|
{
|
|
Kdf = kdf,
|
|
MasterPasswordAuthenticationHash = "hash",
|
|
Salt = "salt"
|
|
};
|
|
var unlock = new MasterPasswordUnlockData
|
|
{
|
|
Kdf = kdf,
|
|
MasterKeyWrappedUserKey = "wrapped",
|
|
Salt = "salt"
|
|
};
|
|
|
|
var results = KdfSettingsValidator.ValidateKdfAndSaltAgreement(authentication, unlock).ToList();
|
|
|
|
Assert.Empty(results);
|
|
}
|
|
}
|