using System.Security.Cryptography; using System.Text; using System.Text.Json; using Microsoft.AspNetCore.WebUtilities; namespace GmRelay.Web.Services; public sealed class TelegramAuthService(IConfiguration configuration) { public bool Verify(IQueryCollection query, out long telegramId, out string name) { telegramId = 0; name = string.Empty; if (!query.TryGetValue("hash", out var hash)) return false; var token = configuration["Telegram__BotToken"] ?? configuration["Telegram:BotToken"]; if (string.IsNullOrEmpty(token)) return false; // 1. Sort and join var dataCheckList = query .Where(x => x.Key != "hash") .OrderBy(x => x.Key) .Select(x => $"{x.Key}={x.Value}") .ToList(); var dataCheckString = string.Join("\n", dataCheckList); // 2. Compute Secret Key (static method — no IDisposable needed) var secretKey = SHA256.HashData(Encoding.UTF8.GetBytes(token)); // 3. Compute Hash (static method — no IDisposable needed) var computedHashBytes = HMACSHA256.HashData(secretKey, Encoding.UTF8.GetBytes(dataCheckString)); // 4. Timing-safe comparison to prevent timing attacks var hashBytes = Convert.FromHexString(hash.ToString()); if (!CryptographicOperations.FixedTimeEquals(computedHashBytes, hashBytes)) return false; // 5. Check expiration (auth_date) if (query.TryGetValue("auth_date", out var authDateStr) && long.TryParse(authDateStr, out var authDate)) { var now = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); if (now - authDate > 86400) // 24 hours return false; } if (query.TryGetValue("id", out var idStr) && long.TryParse(idStr, out telegramId)) { var firstName = query["first_name"].ToString(); var lastName = query["last_name"].ToString(); name = string.IsNullOrWhiteSpace(lastName) ? firstName : $"{firstName} {lastName}"; return true; } return false; } public bool VerifyWebAppInitData(string initData, out long telegramId, out string name) { telegramId = 0; name = string.Empty; if (string.IsNullOrWhiteSpace(initData)) return false; var token = configuration["Telegram__BotToken"] ?? configuration["Telegram:BotToken"]; if (string.IsNullOrEmpty(token)) return false; var values = QueryHelpers.ParseQuery(initData); if (!values.TryGetValue("hash", out var hash) || string.IsNullOrWhiteSpace(hash)) return false; var dataCheckString = string.Join( "\n", values .Where(pair => pair.Key != "hash") .OrderBy(pair => pair.Key, StringComparer.Ordinal) .Select(pair => $"{pair.Key}={pair.Value}")); var secretKey = HMACSHA256.HashData(Encoding.UTF8.GetBytes("WebAppData"), Encoding.UTF8.GetBytes(token)); var computedHashBytes = HMACSHA256.HashData(secretKey, Encoding.UTF8.GetBytes(dataCheckString)); byte[] hashBytes; try { hashBytes = Convert.FromHexString(hash.ToString()); } catch (FormatException) { return false; } if (!CryptographicOperations.FixedTimeEquals(computedHashBytes, hashBytes)) return false; if (!values.TryGetValue("auth_date", out var authDateStr) || !long.TryParse(authDateStr, out var authDate)) { return false; } var now = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); if (now - authDate > 86400) return false; if (!values.TryGetValue("user", out var userJson) || string.IsNullOrWhiteSpace(userJson)) return false; return TryReadWebAppUser(userJson.ToString(), out telegramId, out name); } private static bool TryReadWebAppUser(string userJson, out long telegramId, out string name) { telegramId = 0; name = string.Empty; try { using var document = JsonDocument.Parse(userJson); var root = document.RootElement; if (!root.TryGetProperty("id", out var idElement) || !idElement.TryGetInt64(out telegramId)) return false; var firstName = root.TryGetProperty("first_name", out var firstNameElement) ? firstNameElement.GetString() ?? string.Empty : string.Empty; var lastName = root.TryGetProperty("last_name", out var lastNameElement) ? lastNameElement.GetString() ?? string.Empty : string.Empty; var username = root.TryGetProperty("username", out var usernameElement) ? usernameElement.GetString() : null; name = (firstName, lastName) switch { ({ Length: > 0 }, { Length: > 0 }) => $"{firstName} {lastName}", ({ Length: > 0 }, _) => firstName, _ when !string.IsNullOrWhiteSpace(username) => "@" + username, _ => $"Telegram {telegramId.ToString(System.Globalization.CultureInfo.InvariantCulture)}" }; return true; } catch (JsonException) { return false; } } }