155 lines
5.3 KiB
C#
155 lines
5.3 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|