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?
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
Honestly, the documentation is quite twisted and can be improved. But there are some hints to what may be going on.
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
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.
Here is working variant with "node:crypto" module. Thanks to @Newbie and @mitenka answers.
import { createHmac } from "node:crypto";
function parseInitData(initData: string) {
const q = new URLSearchParams(initData);
const hash = q.get("hash");
q.delete("hash");
const v = Array.from(q.entries());
v.sort(([aN], [bN]) => aN.localeCompare(bN));
const data_check_string = v.map(([n, v]) => `${n}=${v}`).join("\n");
return { hash, data_check_string };
}
function checkSignature(bot_token: string, initData: string) {
const { hash, data_check_string } = parseInitData(initData);
const secret_key = createHmac("sha256", "WebAppData").update(bot_token).digest();
const key = createHmac("sha256", secret_key)
.update(data_check_string)
.digest("hex");
return key === hash;
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With