using System.Security.Cryptography; using System.Text; using System.Text.Json; using System.Text.Json.Serialization; 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); } public bool VerifyLoginPayload(TelegramLoginPayload payload, out long telegramId, out string name) { telegramId = 0; name = string.Empty; if (payload.Id <= 0 || string.IsNullOrWhiteSpace(payload.FirstName) || payload.AuthDate <= 0 || string.IsNullOrWhiteSpace(payload.Hash)) { return false; } var token = configuration["Telegram__BotToken"] ?? configuration["Telegram:BotToken"]; if (string.IsNullOrEmpty(token)) return false; var values = new SortedDictionary(StringComparer.Ordinal) { ["auth_date"] = payload.AuthDate.ToString(System.Globalization.CultureInfo.InvariantCulture), ["first_name"] = payload.FirstName, ["id"] = payload.Id.ToString(System.Globalization.CultureInfo.InvariantCulture) }; if (!string.IsNullOrWhiteSpace(payload.LastName)) values["last_name"] = payload.LastName; if (!string.IsNullOrWhiteSpace(payload.PhotoUrl)) values["photo_url"] = payload.PhotoUrl; if (!string.IsNullOrWhiteSpace(payload.Username)) values["username"] = payload.Username; var dataCheckString = string.Join("\n", values.Select(pair => $"{pair.Key}={pair.Value}")); var secretKey = SHA256.HashData(Encoding.UTF8.GetBytes(token)); var computedHashBytes = HMACSHA256.HashData(secretKey, Encoding.UTF8.GetBytes(dataCheckString)); byte[] hashBytes; try { hashBytes = Convert.FromHexString(payload.Hash); } catch (FormatException) { return false; } catch (ArgumentException) { return false; } if (!CryptographicOperations.FixedTimeEquals(computedHashBytes, hashBytes)) return false; var now = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); if (now - payload.AuthDate > 86400) return false; telegramId = payload.Id; name = string.IsNullOrWhiteSpace(payload.LastName) ? payload.FirstName : $"{payload.FirstName} {payload.LastName}"; return true; } 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; } } } public sealed record TelegramLoginPayload( [property: JsonPropertyName("id")] long Id, [property: JsonPropertyName("first_name")] string FirstName, [property: JsonPropertyName("last_name")] string? LastName, [property: JsonPropertyName("username")] string? Username, [property: JsonPropertyName("photo_url")] string? PhotoUrl, [property: JsonPropertyName("auth_date")] long AuthDate, [property: JsonPropertyName("hash")] string Hash);