"""Checking Garbo's launch data on your server, in Python 3.8 or newer. https://developers.garbo.ph/#check Your page sends Garbo.WebApp.initData to your server; never trust initDataUnsafe there. - check_hash(): with your app's secret (from Garbo; keep it on your server only). No packages. - check_signature(): with Garbo's public key below; needs the "cryptography" package (pip install cryptography). Both raise ValueError when the data isn't right, and return what's in it when it is. """ import hashlib import hmac import json import time from base64 import urlsafe_b64decode from urllib.parse import parse_qsl # Garbo's public key for the launch data's signature (Ed25519, hex). GARBO_PUBLIC_KEY = "a6b7e2bcbd35fa46f8040e430316409d1d6e24f98f5232e5d89b9c361f7525a0" def _fields(init_data): return parse_qsl(init_data, keep_blank_values=True, strict_parsing=True) def _check_string(init_data, leave_out): """Every field but the ones left out, sorted by name, as "name=value" lines.""" pairs = sorted((name, value) for name, value in _fields(init_data) if name not in leave_out) return "\n".join(f"{name}={value}" for name, value in pairs) def _read(init_data, max_age_seconds): data = dict(_fields(init_data)) data["auth_date"] = int(data.get("auth_date", "0")) data["user"] = json.loads(data["user"]) if data.get("user") else None if max_age_seconds and time.time() - data["auth_date"] > max_age_seconds: raise ValueError("The launch data is too old.") return data def check_hash(init_data, app_secret, max_age_seconds=86400): received = dict(_fields(init_data)).get("hash", "") secret_key = hmac.new(b"WebAppData", app_secret.encode(), hashlib.sha256).digest() expected = hmac.new(secret_key, _check_string(init_data, ["hash"]).encode(), hashlib.sha256).hexdigest() if not hmac.compare_digest(received, expected): raise ValueError("The launch data was not signed with this app's secret.") return _read(init_data, max_age_seconds) def check_signature(init_data, app_id, max_age_seconds=86400, public_key=GARBO_PUBLIC_KEY): from cryptography.exceptions import InvalidSignature from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey fields = dict(_fields(init_data)) if fields.get("app_id") != app_id: raise ValueError("The launch data is for another app.") signature = fields.get("signature", "") signed = f"{app_id}:WebAppData\n{_check_string(init_data, ['hash', 'signature'])}".encode() try: Ed25519PublicKey.from_public_bytes(bytes.fromhex(public_key)).verify( urlsafe_b64decode(signature + "=" * (-len(signature) % 4)), signed ) except (InvalidSignature, ValueError): raise ValueError("The launch data was not signed by Garbo.") return _read(init_data, max_age_seconds)