Skip to content

Instantly share code, notes, and snippets.

@zubiden
Created April 17, 2022 03:33
Show Gist options
  • Save zubiden/175bfed36ac186664de41f54c55e4327 to your computer and use it in GitHub Desktop.
Save zubiden/175bfed36ac186664de41f54c55e4327 to your computer and use it in GitHub Desktop.
Telegram Web Bots data validation in JavaScript via Web Crypto API (dependency-free)
// Thanks to @MarvinMiles for Telegram Widget Login check function https://gist.github.com/MarvinMiles/f041205d872b0d8547d054eafeafe2a5
// This function validates Web App input https://core.telegram.org/bots/webapps#validating-data-received-via-the-web-app
// Transforms Telegram.WebApp.initData string into object
function transformInitData(initData) {
return Object.fromEntries(new URLSearchParams(initData));
}
// Accepts init data object and bot token
async function validate(data, botToken) {
const encoder = new TextEncoder()
const checkString = await Object.keys(data)
.filter((key) => key !== "hash")
.map((key) => `${key}=${data[key]}`)
.sort()
.join("\n")
// console.log('computed string:', checkString)
const secretKey = await crypto.subtle.importKey("raw", encoder.encode('WebAppData'), { name: "HMAC", hash: "SHA-256" }, true, ["sign"])
const secret = await crypto.subtle.sign("HMAC", secretKey, encoder.encode(botToken))
const signatureKey = await crypto.subtle.importKey("raw", secret, { name: "HMAC", hash: "SHA-256" }, true, ["sign"])
const signature = await crypto.subtle.sign("HMAC", signatureKey, encoder.encode(checkString))
const hex = [...new Uint8Array(signature)].map(b => b.toString(16).padStart(2, '0')).join('')
// console.log('original hash:', data.hash)
// console.log('computed hash:', hex)
return data.hash === hex
}
@designervoid
Copy link

NextJS Typescript Implementation:

import { NextApiRequest, NextApiResponse } from "next";

const { subtle } = require("crypto").webcrypto;

type TransformInitData = {
  [k: string]: string;
};

function transformInitData(initData: string): TransformInitData {
  return Object.fromEntries(new URLSearchParams(initData));
}

async function validate(data: TransformInitData, botToken: string) {
  const encoder = new TextEncoder();

  const checkString = Object.keys(data)
    .filter((key) => key !== "hash")
    .map((key) => `${key}=${data[key]}`)
    .sort()
    .join("\n");

  const secretKey = await subtle.importKey(
    "raw",
    encoder.encode("WebAppData"),
    { name: "HMAC", hash: "SHA-256" },
    true,
    ["sign"]
  );
  const secret = await subtle.sign("HMAC", secretKey, encoder.encode(botToken));
  const signatureKey = await subtle.importKey(
    "raw",
    secret,
    { name: "HMAC", hash: "SHA-256" },
    true,
    ["sign"]
  );
  const signature = await subtle.sign(
    "HMAC",
    signatureKey,
    encoder.encode(checkString)
  );

  const hex = [...new Uint8Array(signature)]
    .map((b) => b.toString(16).padStart(2, "0"))
    .join("");

  return data.hash === hex;
}

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  const initData = req.body._auth;
  
  if (!initData) {
    res.status(400);
    return;
  }

  const data = transformInitData(initData);
  const isOk = await validate(
    data,
    process.env.BOT_TOKEN!
  );

  if (isOk) {
    res.status(200).send({
      ok: isOk,
    });
  } else {
    res.status(403).send({
      error: "Invalid hash",
    });
  }
}

@albanleong
Copy link

const secret = await CryptoJS.HmacSHA256(botToken, 'WebAppData');
const signature = await CryptoJS.HmacSHA256(checkString, secret);
const hex = await signature.toString(CryptoJS.enc.Hex);

Thanks for sharing this - it worked great for me!!

@TakhirKudusov
Copy link

Thank you so much for this! It helped a lot.

@nhd98z
Copy link

nhd98z commented Jun 25, 2024

Thanks for the code.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment