Files
ReLibreHardwareMonitor/LibreHardwareMonitor.Windows.WinUI.Tests/Services/PasswordHasherTests.cs
T
JMR-devandClaude Opus 4.8 15737ad639 Harden remote web server security
All changes are localized to RemoteWebServer plus two small helpers; the
Phase 0 characterization tests confirm routing, JSON/Prometheus shape, and
credential pass/fail semantics are otherwise unchanged.

- Password hashing: new PasswordHasher uses PBKDF2-HMAC-SHA256 with a
  per-credential random salt (self-describing pbkdf2$iters$salt$hash).
  Verify() still accepts the legacy unsalted SHA-256 hex hash and a
  successful legacy auth transparently upgrades the stored hash, persisted
  by the view model on save/shutdown. Property renamed PasswordSHA256 ->
  PasswordHash.
- Constant-time comparison: CredentialComparer.FixedTimeEquals for the user
  name and password hash; both are evaluated fully (no && short-circuit).
- No information disclosure: POST failures return a generic message instead
  of ex.ToString(); detail is logged server-side only.
- Bind intent respected: ResolveListenerIp no longer mutates ListenerIp or
  silently falls back to all-interfaces for a specific configured address
  (auto/'?'/wildcards still bind all). A bad address now fails Start().
- CORS: removed the Access-Control-Allow-Origin '*' wildcard; common
  response headers centralized in WriteCommonHeaders.
- Prometheus: label values are escaped (EscapePrometheusLabel).

185 tests pass (was 167).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 10:35:13 -05:00

71 lines
2.2 KiB
C#

// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
// If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
// Copyright (C) LibreHardwareMonitor and Contributors.
using LibreHardwareMonitor.Windows.WinUI.Services;
using Xunit;
namespace LibreHardwareMonitor.Windows.WinUI.Tests.Services;
public class PasswordHasherTests
{
[Fact]
public void Hash_ThenVerify_Succeeds()
{
string hash = PasswordHasher.Hash("correct horse");
Assert.True(PasswordHasher.Verify("correct horse", hash, out bool isLegacy));
Assert.False(isLegacy);
}
[Fact]
public void Verify_WrongPassword_Fails()
{
string hash = PasswordHasher.Hash("secret");
Assert.False(PasswordHasher.Verify("guess", hash, out _));
}
[Fact]
public void Hash_UsesSelfDescribingPbkdf2Format()
{
string hash = PasswordHasher.Hash("secret");
Assert.StartsWith("pbkdf2$", hash);
Assert.Equal(4, hash.Split('$').Length);
}
[Fact]
public void Hash_UsesRandomSalt_SoTwoHashesOfSamePasswordDiffer()
{
Assert.NotEqual(PasswordHasher.Hash("secret"), PasswordHasher.Hash("secret"));
}
[Fact]
public void Verify_AcceptsLegacySha256_AndReportsLegacy()
{
string legacy = PasswordHasher.ComputeLegacySha256("secret");
Assert.True(PasswordHasher.Verify("secret", legacy, out bool isLegacy));
Assert.True(isLegacy);
Assert.False(PasswordHasher.Verify("wrong", legacy, out _));
}
[Fact]
public void ComputeLegacySha256_KnownVector()
{
Assert.Equal("ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad", PasswordHasher.ComputeLegacySha256("abc"));
}
[Fact]
public void Verify_EmptyStoredHash_Fails()
{
Assert.False(PasswordHasher.Verify("anything", "", out _));
}
[Theory]
[InlineData("pbkdf2$notanumber$c2FsdA==$aGFzaA==")]
[InlineData("pbkdf2$100000$not-base64$aGFzaA==")]
[InlineData("pbkdf2$100000")]
public void Verify_MalformedPbkdf2_Fails(string storedHash)
{
Assert.False(PasswordHasher.Verify("secret", storedHash, out _));
}
}