Проверка 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 не будет опубликован. Обязательные поля помечены *