3

I try to rewrite validation code to JS + cryptoJS:

var secret_key = CryptoJS.HmacSHA256(bot.token, "WebAppData");
var key = CryptoJS.HmacSHA256(initData, secret_key)
// initData it is - Telegram.WebApp.initData
if(key==hash){
    // validated
}
    
// I have also tried converting 'key' to hex:
key = key.toString(CryptoJS.enc.Hex);
key == hash // always false too

But my validation is always false.

What fixes are needed?

Nicholas Obert
  • 1,235
  • 1
  • 13
  • 29
mpz
  • 1,906
  • 3
  • 15
  • 23

6 Answers6

3

Solution

This is a ported version of the Python solution found in Aiogram

import HmacSHA256 from "crypto-js/hmac-sha256";
import Hex from "crypto-js/enc-hex";

function checkWebAppSignature(token, initData) {
  // It is not clear from the documentation weather is URL
  // escaped or not, maybe you will need to uncomment this
  // initData = decodeURIComponent(initData)
  // Parse URL Query
  const q = new URLSearchParams(initData);
  // Extract the hash
  const hash = q.get("hash");

  // Re encode in accordance to the documentation. Remember
  // to remove hash before.
  q.delete("hash");
  const v = Array.from(q.entries());
  v.sort(([aN, aV], [bN, bV]) => aN.localeCompare(bN));
  const data_chack_string = v.map(([n, v]) => `${n}=${v}`).join("\n");

  // Perform the algorithm provided with the documentation
  var secret_key = HmacSHA256(token, "WebAppData").toString(Hex);
  var key = HmacSHA256(data_chack_string, secret_key).toString(Hex);

  return key === hash;
}

Here is a Sandbox

Clarifying the documentation

Honestly, the documentation is quite twisted and can be improved. But there are some hints to what may be going on.

1) Format

To validate data received via the Web App, one should send the data from the Telegram.WebApp.initData field to the bot's backend. The data is a query string, which is composed of a series of field-value pairs.

Data-check-string is a chain of all received fields, sorted alphabetically, in the format key= with a line feed character ('\n', 0x0A) used as separator ...

So initData is a URL Query name=One&surname=Two where the expected data_check_string should be new line separated name=One\nsurname=Two

2) Hash is included

This is not explicated in the documentation, but initData includes the hash in the form name=One&surname=Two&...&hash=.... Being almost impossible to include the hash of the document in the document itself hints at the fact that the string initData is not the one being hashed. In Aiogram you find a confirmation of this.

Newbie
  • 4,462
  • 11
  • 23
0

You need to exclude hash parameter from initData before computing hash for comparison and order other parameters in alphabetical order. Also you do need to convert key(computed hash to hex), cause hash present in initData from telegram is in hex format.

terviv
  • 1
  • 2
0

Maybe this is not exactly what you need, but you can still check initData in PHP. Here I found a great solution TgWebValid

soul wolf
  • 1
  • 2
0

function transformInitData(initData) { return Object.fromEntries(new URLSearchParams(initData));} 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") 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('') return data.hash === hex }

  • As it’s currently written, your answer is unclear. Please [edit] to add additional details that will help others understand how this addresses the question asked. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Sparkling Marcel May 03 '23 at 14:43
0

This is what I am using:

// Transforms Telegram.WebApp.initData string into object
    function transformInitData(initData: string): TransformInitData {
      return Object.fromEntries(new URLSearchParams(initData));
    }
    
    async function generateHex(data: TransformInitData, botToken: string): Promise<string> {
      const encoder = new TextEncoder();
      const checkString = await Object.keys(data)
        .filter((key) => key !== 'hash')
        .map((key) => `${key}=${data[key]}`)
        .sort()
        .join('\n');
      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('');
    
      return hex;
    }
    
    export async function onRequest(context: any) {
      try {
        const payload = await context.request.json();
        const dataCheckString = payload.hash;
        const data = transformInitData(dataCheckString);
        const botToken = context.env.BOT_TOKEN;
      
        const hex = await generateHex(data, botToken);
        if(data['hash'] === hex) {
          return await context.next();
        }
      
        const json = JSON.stringify({ message: 'not authorized' });
        return new Response(json, { status: 403 });
        
      } catch (err) {
        return new Response('Server Error', { status: 500 });
      }
    }
0

Thank you @Newbie, your answer helped me a lot. In my case it looks slightly different tho (especially when it comes to secret hashing), so I'll publish my solution.

import { HmacSHA256, Hex } from "crypto-js";

const params = c.req.query();
const { hash } = params;

delete params["hash"];

// Sort params by key alphabetically and then join them with new line symbol (\n).
let dataCheckString = Object.keys(params)
  .sort()
  .map((key) => `${key}=${params[key]}`)
  .join("\n");

const secret = HmacSHA256(c.env.BOT_TOKEN, "WebAppData");
const calculatedHash = HmacSHA256(dataCheckString, secret).toString(Hex);
console.log(hash === calculatedHash);
mitenka
  • 1,365
  • 1
  • 10
  • 10