using NSubstitute; using ScadaLink.Commons.Entities.InboundApi; using ScadaLink.Commons.Interfaces.Repositories; using ScadaLink.Commons.Types.InboundApi; namespace ScadaLink.InboundAPI.Tests; /// /// ConfigurationDatabase-012: must authenticate by /// hashing the presented candidate with the same HMAC-SHA256 pepper used at /// creation, then comparing against the stored — never /// against a plaintext key. The comparison stays constant-time. /// public class ApiKeyHashValidationTests { private const string Pepper = "a-sufficiently-long-server-side-pepper-value"; private readonly IInboundApiRepository _repository = Substitute.For(); private static ApiKey StoredKey(ApiKeyHasher hasher, string liveKey, int id = 1, bool enabled = true) { var key = ApiKey.FromHash("MES-Production", hasher.Hash(liveKey)); key.Id = id; key.IsEnabled = enabled; return key; } [Fact] public async Task ValidateAsync_WithPepperedHasher_AcceptsKeyHashedWithSamePepper() { var hasher = new ApiKeyHasher(Pepper); var stored = StoredKey(hasher, "live-secret-key"); var method = new ApiMethod("ingest", "return 1;") { Id = 10 }; _repository.GetAllApiKeysAsync().Returns(new List { stored }); _repository.GetMethodByNameAsync("ingest").Returns(method); _repository.GetApprovedKeysForMethodAsync(10).Returns(new List { stored }); var validator = new ApiKeyValidator(_repository, hasher); var result = await validator.ValidateAsync("live-secret-key", "ingest"); Assert.True(result.IsValid); Assert.Equal(200, result.StatusCode); } [Fact] public async Task ValidateAsync_WrongKey_FailsEvenWhenItHashesToSomethingNonNull() { var hasher = new ApiKeyHasher(Pepper); var stored = StoredKey(hasher, "the-real-key"); _repository.GetAllApiKeysAsync().Returns(new List { stored }); var validator = new ApiKeyValidator(_repository, hasher); var result = await validator.ValidateAsync("a-wrong-key", "ingest"); Assert.False(result.IsValid); Assert.Equal(401, result.StatusCode); } [Fact] public async Task ValidateAsync_StoredHashIsNotThePlaintextKey() { // Sanity guard: the value the validator compares against must be a hash, not // the live secret — a DB dump must not yield a usable credential. var hasher = new ApiKeyHasher(Pepper); var stored = StoredKey(hasher, "live-secret-key"); Assert.NotEqual("live-secret-key", stored.KeyHash); _repository.GetAllApiKeysAsync().Returns(new List { stored }); var validator = new ApiKeyValidator(_repository, hasher); // Presenting the stored hash itself must NOT authenticate — only the live key does. var result = await validator.ValidateAsync(stored.KeyHash, "ingest"); Assert.False(result.IsValid); } [Fact] public async Task ValidateAsync_KeyHashedUnderADifferentPepper_DoesNotAuthenticate() { var creationHasher = new ApiKeyHasher(Pepper); var stored = StoredKey(creationHasher, "live-secret-key"); _repository.GetAllApiKeysAsync().Returns(new List { stored }); // A validator running with a different pepper cannot recognise the key. var otherHasher = new ApiKeyHasher("a-totally-different-server-side-pepper-val"); var validator = new ApiKeyValidator(_repository, otherHasher); var result = await validator.ValidateAsync("live-secret-key", "ingest"); Assert.False(result.IsValid); } }