using System.Security.Cryptography; namespace Server.WatchBody.Web.Services; /// /// Аутентификация владельца консоли (spec 01 m08): PBKDF2-проверка /// Admin.PasswordHash → in-memory Bearer-сессии (TTL 8 ч; рестарт /// сервера сбрасывает). 🛡 Токены/пароли не логируются и не /// возвращаются; Admin.Login пуст — консоль отключена. /// public class AdminAuthService(string login, string passwordHash) { private static readonly TimeSpan Ttl = TimeSpan.FromHours(8); private readonly Dictionary _sessions = new(); public bool Enabled => login.Length > 0 && passwordHash.StartsWith("pbkdf2$", StringComparison.Ordinal); /// Вход → токен (base64url) или null. public string? Login(string candidateLogin, string candidatePassword) { if (!Enabled || !FixedTimeEquals(candidateLogin, login) || !VerifyPassword(candidatePassword, passwordHash)) return null; var token = Convert.ToBase64String(RandomNumberGenerator.GetBytes(32)) .TrimEnd('=').Replace('+', '-').Replace('/', '_'); lock (_sessions) { _sessions[token] = DateTime.UtcNow.Add(Ttl); } return token; } public bool Validate(string? token) { if (token == null || token.Length == 0) return false; lock (_sessions) { if (!_sessions.TryGetValue(token, out var expiresAt)) return false; if (expiresAt < DateTime.UtcNow) { _sessions.Remove(token); return false; } return true; } } public void Logout(string token) { lock (_sessions) _sessions.Remove(token); } internal static bool VerifyPassword(string password, string stored) { // pbkdf2$100000$$ var parts = stored.Split('$'); if (parts.Length != 4 || parts[0] != "pbkdf2" || !int.TryParse(parts[1], out var iterations) || iterations < 1) return false; byte[] salt, expected; try { salt = Convert.FromBase64String(parts[2]); expected = Convert.FromBase64String(parts[3]); } catch (FormatException) { return false; } var actual = Rfc2898DeriveBytes.Pbkdf2(password, salt, iterations, HashAlgorithmName.SHA256, expected.Length); return CryptographicOperations.FixedTimeEquals(actual, expected); } /// Хэш пароля в формате конфига (тесты; владельцу — /// python-однострочник из spec 01). public static string HashPassword(string password, byte[]? salt = null) { salt ??= RandomNumberGenerator.GetBytes(16); var hash = Rfc2898DeriveBytes.Pbkdf2(password, salt, 100_000, HashAlgorithmName.SHA256, 32); return $"pbkdf2$100000${Convert.ToBase64String(salt)}${Convert.ToBase64String(hash)}"; } private static bool FixedTimeEquals(string a, string b) { var bytesA = System.Text.Encoding.UTF8.GetBytes(a); var bytesB = System.Text.Encoding.UTF8.GetBytes(b); return bytesA.Length == bytesB.Length && CryptographicOperations.FixedTimeEquals(bytesA, bytesB); } }