Проверка Telegram Mini App Init Data для Telegram API
У популярного мессенджера Telegram есть своё API (не удивительно). Которое позволяет писать ботов, автоматизировать какую-то работу и ещё много чего, а также создавать Мини приложения (Mini App)
Собственно одной из таких задачек я занимался для своего домашнего проекта. И появился у меня вопрос, как же авторизовать клиента, который ломится на мой сервер. Заставлять авторизовываться его отдельно на моём сервере – странная ситуация, с учётом того, что я и так знаю, что за клиент ко мне идёт. Но мне надо убедиться, что данные, которые мне пришли, это действительно данные того клиента, который ко мне пришёл.
В документации Telegram API есть по этому поводу описание:
To validate data received via the Mini 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.
You can verify the integrity of the data received by comparing the received hash parameter with the hexadecimal representation of the HMAC-SHA-256 signature of the data-check-string with the secret key, which is the HMAC-SHA-256 signature of the bot's token with the constant string WebAppData used as a key.
Data-check-string is a chain of all received fields, sorted alphabetically, in the format key=<value> with a line feed character ('\n', 0x0A) used as separator – e.g., 'auth_date=<auth_date>\nquery_id=<query_id>\nuser=<user>'.
То есть. Берём наш ключ от бота, берём фразу WebAppData в качестве ключа для HMAC SHA-256.
Далее берём все полученные данные из Web App Init Data, сортируем их в алфавитном порядке и соединяем их через перенос строки (\n
) , чтобы получить примерно такое:
aKey=aValue\nbkey=bValue <...>
И высчитать хэш HMAC SHA-256 от этих данных с ключом, полученным ранее. Выглядит всё очень просто. Но!
Но тут я словил проблему, которую пытался починить три вечера. Хэши не совпадали. В целом всё выглядело достаточно просто. При вызове url моего приложения мне действительно приходила url encoded строка. Но произошло несколько недопонимание с моей стороны. Данные, что приходят на сервер отличаются от тех, что есть в вызове
Telegram.WebApp.initData
Соответственно мне приходило в довесок много лишнего, например tgWebAppVersion и прочее, от чего я тоже считал хэш.
Второе, что я понял гораздо раньше: hash=
надо исключать из подсчёта контрольной суммы. Это я ещё с университета помню.
В-третьих, user=
нуждается в повторной операции url decode.
В итоге у меня был написал правильный код, но я брал не правильные данные для подсчёта хэша. Ниже код:
public boolean checkData(String initData) {
if (StringUtils.isBlank(initData)) {
return false;
}
String parsedInitData = URLDecoder.decode(initData, StandardCharsets.UTF_8);
String[] hashContainer = new String[1];
List<String> sortedUrlDecoded = extractAndSortData(parsedInitData, hashContainer);
if (hashContainer[0] == null) {
return false;
}
byte[] secretKeyForData = hmacH256(applicationProperties.botToken().getBytes(StandardCharsets.UTF_8), TG_WEB_APP_DATA);
if (secretKeyForData == null) {
log.error("Невозможно сформировать ключ для подписи начальных данных пользователя Mini App");
return false;
}
return validateHash(sortedUrlDecoded, hashContainer[0]);
}
private boolean validateHash(List<String> sortedData, String originalHash) {
byte[] secretKey = hmacH256(BOT_TOKEN.getBytes(StandardCharsets.UTF_8), TG_WEB_APP_DATA);
if (secretKey == null) {
return false;
}
byte[] dataHash = hmacH256(String.join("\n", sortedData).getBytes(StandardCharsets.UTF_8), secretKey);
if (dataHash == null) {
return false;
}
String computedHashString = bytesToHex(dataHash);
return Objects.equals(originalHash, computedHashString);
}
private List<String> extractAndSortData(String initData, String[] hashContainer) {
return Arrays.stream(initData.split("&"))
.map(s -> extractHash(s, hashContainer))
.filter(s -> !s.startsWith("hash="))
.map(s -> URLDecoder.decode(s, StandardCharsets.UTF_8))
.sorted()
.collect(Collectors.toList());
}
private String extractHash(String param, String[] hashContainer) {
if (param.startsWith("hash=")) {
hashContainer[0] = param.substring(5);
}
return param;
}
private byte[] hmacH256(byte[] data, byte[] key) {
try {
Mac sha256HMAC = Mac.getInstance(HMAC_SHA256_ALGO);
SecretKeySpec secretKeySpec = new SecretKeySpec(key, HMAC_SHA256_ALGO);
sha256HMAC.init(secretKeySpec);
return sha256HMAC.doFinal(data);
} catch (Exception e) {
return null;
}
}
private String bytesToHex(byte[] bytes) {
StringBuilder hexString = new StringBuilder();
for (byte b : bytes) {
String hex = Integer.toHexString(0xFF & b);
if (hex.length() == 1) {
hexString.append('0');
}
hexString.append(hex);
}
return hexString.toString();
}
Таким образом, если в метод checkData
передать строку, полученную через вызов JavaScript метода Telegram.WebApp.initData
мы сможем проверить данные на отправку их из Telegram.
Последние комменатрии