124 lines
4.1 KiB
C#
124 lines
4.1 KiB
C#
using System.Security.Cryptography;
|
|
using System.Text;
|
|
using KronaStaffTerminal.Models;
|
|
using KronaStaffTerminal.Services;
|
|
using Xunit;
|
|
|
|
namespace KronaStaffTerminal.Tests;
|
|
|
|
public class AdminPinServiceTests
|
|
{
|
|
[Fact]
|
|
public void HashAndVerify_ValidPin_ReturnsTrue()
|
|
{
|
|
var pin = "1234";
|
|
var (salt, hash, iterations) = AdminPinService.Hash(pin);
|
|
|
|
Assert.NotNull(salt);
|
|
Assert.StartsWith(AdminPinService.Argon2Prefix, hash);
|
|
Assert.True(iterations >= 1);
|
|
|
|
var verified = AdminPinService.Verify(pin, salt, hash, iterations);
|
|
Assert.True(verified);
|
|
}
|
|
|
|
[Fact]
|
|
public void Verify_InvalidPin_ReturnsFalse()
|
|
{
|
|
var (salt, hash, iterations) = AdminPinService.Hash("5678");
|
|
|
|
var verified = AdminPinService.Verify("0000", salt, hash, iterations);
|
|
Assert.False(verified);
|
|
}
|
|
|
|
[Fact]
|
|
public void Verify_LegacyPbkdf2Hash_VerifiesSuccessfully()
|
|
{
|
|
var pin = "9876";
|
|
var salt = RandomNumberGenerator.GetBytes(16);
|
|
var saltBase64 = Convert.ToBase64String(salt);
|
|
var legacyIterations = 120_000;
|
|
var hashBytes = Rfc2898DeriveBytes.Pbkdf2(
|
|
Encoding.UTF8.GetBytes(pin),
|
|
salt,
|
|
legacyIterations,
|
|
HashAlgorithmName.SHA256,
|
|
32);
|
|
var hashBase64 = Convert.ToBase64String(hashBytes);
|
|
|
|
var verified = AdminPinService.Verify(pin, saltBase64, hashBase64, legacyIterations);
|
|
Assert.True(verified);
|
|
}
|
|
|
|
[Fact]
|
|
public void VerifyAndRecord_LegacyHashUpgradesToArgon2id()
|
|
{
|
|
var pin = "4321";
|
|
var salt = RandomNumberGenerator.GetBytes(16);
|
|
var legacyIterations = 120_000;
|
|
var hashBytes = Rfc2898DeriveBytes.Pbkdf2(
|
|
Encoding.UTF8.GetBytes(pin),
|
|
salt,
|
|
legacyIterations,
|
|
HashAlgorithmName.SHA256,
|
|
32);
|
|
|
|
var settings = new TerminalSettings
|
|
{
|
|
AdminPinSalt = Convert.ToBase64String(salt),
|
|
AdminPinHash = Convert.ToBase64String(hashBytes),
|
|
AdminPinIterations = legacyIterations,
|
|
FailedAdminPinAttempts = 0
|
|
};
|
|
|
|
var now = DateTimeOffset.UtcNow;
|
|
var success = AdminPinService.VerifyAndRecord(pin, settings, now, out var error, out var rehashNeeded);
|
|
|
|
Assert.True(success);
|
|
Assert.Null(error);
|
|
Assert.True(rehashNeeded);
|
|
Assert.StartsWith(AdminPinService.Argon2Prefix, settings.AdminPinHash);
|
|
}
|
|
|
|
[Fact]
|
|
public void VerifyAndRecord_ThreeFailedAttempts_TriggersLockout()
|
|
{
|
|
var (salt, hash, iterations) = AdminPinService.Hash("1111");
|
|
var settings = new TerminalSettings
|
|
{
|
|
AdminPinSalt = salt,
|
|
AdminPinHash = hash,
|
|
AdminPinIterations = iterations,
|
|
FailedAdminPinAttempts = 0
|
|
};
|
|
|
|
var now = DateTimeOffset.UtcNow;
|
|
|
|
// Attempt 1: Fail
|
|
var r1 = AdminPinService.VerifyAndRecord("0000", settings, now, out var err1, out _);
|
|
Assert.False(r1);
|
|
Assert.Contains("до блокировки: 2", err1);
|
|
|
|
// Attempt 2: Fail
|
|
var r2 = AdminPinService.VerifyAndRecord("0000", settings, now, out var err2, out _);
|
|
Assert.False(r2);
|
|
Assert.Contains("до блокировки: 1", err2);
|
|
|
|
// Attempt 3: Lockout triggered (30s)
|
|
var r3 = AdminPinService.VerifyAndRecord("0000", settings, now, out var err3, out _);
|
|
Assert.False(r3);
|
|
Assert.Contains("заблокирован на 30 сек", err3);
|
|
Assert.True(settings.AdminPinBlockedUntilUtc > now);
|
|
|
|
// Attempt 4: Even with correct PIN, blocked during lockout
|
|
var r4 = AdminPinService.VerifyAndRecord("1111", settings, now.AddSeconds(5), out var err4, out _);
|
|
Assert.False(r4);
|
|
Assert.Contains("временно заблокирован", err4);
|
|
|
|
// Attempt 5: After lockout expires, correct PIN succeeds
|
|
var r5 = AdminPinService.VerifyAndRecord("1111", settings, now.AddSeconds(35), out var err5, out _);
|
|
Assert.True(r5);
|
|
Assert.Null(err5);
|
|
Assert.Equal(0, settings.FailedAdminPinAttempts);
|
|
}
|
|
}
|