using System.Security.Cryptography; using System.Text; namespace NATS.Server.Auth; /// /// Authenticates a single username/password pair configured on the server. /// Supports plain-text and bcrypt-hashed passwords. /// Uses constant-time comparison for both username and password to prevent timing attacks. /// Reference: golang/nats-server/server/auth.go checkClientAuth for single user. /// public sealed class SimpleUserPasswordAuthenticator : IAuthenticator { private readonly byte[] _expectedUsername; private readonly string _serverPassword; public SimpleUserPasswordAuthenticator(string username, string password) { _expectedUsername = Encoding.UTF8.GetBytes(username); _serverPassword = password; } public AuthResult? Authenticate(ClientAuthContext context) { var clientUsername = context.Opts.Username; if (string.IsNullOrEmpty(clientUsername)) return null; var clientUsernameBytes = Encoding.UTF8.GetBytes(clientUsername); if (!CryptographicOperations.FixedTimeEquals(clientUsernameBytes, _expectedUsername)) return null; var clientPassword = context.Opts.Password ?? string.Empty; if (!ComparePasswords(_serverPassword, clientPassword)) return null; return new AuthResult { Identity = clientUsername }; } private static bool ComparePasswords(string serverPassword, string clientPassword) { // Bcrypt hashes start with "$2" (e.g., $2a$, $2b$, $2y$) if (serverPassword.StartsWith("$2")) { try { return BCrypt.Net.BCrypt.Verify(clientPassword, serverPassword); } catch { return false; } } // Plain-text: constant-time comparison to prevent timing attacks var serverBytes = Encoding.UTF8.GetBytes(serverPassword); var clientBytes = Encoding.UTF8.GetBytes(clientPassword); return CryptographicOperations.FixedTimeEquals(serverBytes, clientBytes); } }