Проверка 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.

Вам может также понравиться...

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *