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>