60 lines
1.7 KiB
Python
60 lines
1.7 KiB
Python
import hmac
|
|
import hashlib
|
|
import time
|
|
from urllib.parse import parse_qsl
|
|
|
|
from app.core.config import settings
|
|
|
|
# Reject initData older than this many seconds (24 hours)
|
|
_MAX_AGE_SECONDS = 86_400
|
|
|
|
|
|
def validate_init_data(init_data: str) -> dict | None:
|
|
"""Validate Telegram WebApp initData using HMAC-SHA256.
|
|
|
|
Also rejects payloads whose auth_date is older than _MAX_AGE_SECONDS to
|
|
prevent indefinite replay of leaked initData strings.
|
|
"""
|
|
try:
|
|
# parse_qsl already URL-decodes each value. Decoding the whole query
|
|
# string first can turn escaped separators inside JSON values into real
|
|
# separators and make Telegram's HMAC check fail.
|
|
parsed = dict(parse_qsl(init_data, keep_blank_values=True))
|
|
hash_value = parsed.pop("hash", "")
|
|
if not hash_value:
|
|
return None
|
|
|
|
# Validate auth_date freshness before doing any crypto work
|
|
auth_date_str = parsed.get("auth_date")
|
|
if not auth_date_str:
|
|
return None
|
|
try:
|
|
age = time.time() - int(auth_date_str)
|
|
if age > _MAX_AGE_SECONDS:
|
|
return None
|
|
except ValueError:
|
|
return None
|
|
|
|
data_check_string = "\n".join(
|
|
f"{k}={v}" for k, v in sorted(parsed.items())
|
|
)
|
|
|
|
secret_key = hmac.new(
|
|
b"WebAppData",
|
|
settings.bot_token.encode(),
|
|
hashlib.sha256,
|
|
).digest()
|
|
|
|
expected = hmac.new(
|
|
secret_key,
|
|
data_check_string.encode(),
|
|
hashlib.sha256,
|
|
).hexdigest()
|
|
|
|
if not hmac.compare_digest(expected, hash_value):
|
|
return None
|
|
|
|
return parsed
|
|
except Exception:
|
|
return None
|