HMAC-SHA256 и Telegram Mini App

Мой практический опыт работы с хеш-функциями был ограничен лишь небольшим набором — это всего лишь то работа с ассоциативным массивом, шифрование паролей и, вроде бы, все. Согласен, опыт не богатый. Но эта статья есть результат моего сознательного знакомства с хеш-функциями и, что еще важнее, случайной необходимости аутентификации данных при разработке Telegram Mini App где собственно они мне сразу и пригодились. Да, это было очень удивительно, стоило мне только взяться за чтение книги о криптографии, только прочесть ту часть где говориться о хеш-функциях, аутентификации сообщения, как сразу же на практике, при разработке своего ПО, я столкнулся с проблемой, которая именно что и решается этой бравой двойкой. Это лишь подтвердило в моей голове слова автора книги, что хеш-функции везде и на их базе строится другие более сложные криптографические примитивы.

Забегая вперед я определю сразу, что кульминацией моей статьи будет практический опыт применения HMAC-SHA256, имитовставки с использование хеш функции, при аутентификации сообщения на стороне сервера, которое, как я ожидаю, я получил от своего Telegram Mini App. И нужно понимать, этот процесс один из самых важных при разработке данного ПО, так как внешний сервис, в данном случае Telegram, присылает мне важные данные пользователя, на основе которых уже я его авторизую у себя в системе. Но, прежде чем приступиться к этому, нужно разобраться или освежить свои знания в теоретическом плане.

Хеш-функции

Задача хеш-функций — это присвоение глобальных уникальных идентификаторов чему бы то ни было, то есть принимают на вход какой-то набор данных, а возвращают уникальную строку байтов. Результат данной функции называют, чаще всего, хешем или дайджестом.

Важно различать криптографические и некриптографические хеш-функции, так как с именно с последними в той или иной мере работает каждый программист

Криптографические хеш-функции

Криптографические хеш-функции предназначены для безопасного преобразования данных в хеш-код (выходное значение), которое можно использовать для проверки целостности данных и создания кодов аутентификации, таких как HMAC

Важные свойства данной функции:

— Устойчивость к атаке нахождения прообраза (или preimage resistance) — это означает, что для заданного хеш-значения очень сложно или практически невозможно найти входное сообщение (прообраз), которое его породило.

Пример: Допустим, у вас есть хеш H = SHA-256(«password123»), который равен какому-то значению. Устойчивость к нахождению прообраза означает, что для хеш-значения H злоумышленник не сможет легко восстановить исходную строку »password123».

— Устойчивость к атаке нахождения второго прообраза (или second preimage resistance) — это означает, что, имея одно сообщение и его хеш, должно быть очень сложно или практически невозможно найти другое сообщение, которое даст тот же самый хеш.

Пример: Предположим, у вас есть сообщение M1 = «password123» и его хеш H1 = SHA-256(M1). Устойчивость ко второму прообразу означает, что злоумышленнику, даже зная M1 и H1, будет очень трудно найти другое сообщение M2, которое также даст хеш H1, то есть такое, что SHA-256(M2) = H1.

— Устойчивость к коллизиям все просто — это означает, что хеш-функция должна быть устойчива к коллизиям! То есть должно быть крайне сложно найти две различные входные строки, которые дают одинаковое хеш-значение. Это важно для предотвращения подмены данных.

— Лавинный критерий — это важное свойство криптографических хеш-функций, которое гарантирует, что даже небольшое изменение входных данных приводит к значительному изменению выходного хеш-значения. Этот эффект критически важен для обеспечения безопасности и надежности хеш-функций.

Некриптографические хеш-функции

Некриптографические хеш-функции обычно применяются там где нет особой необходимости к безопасности, а важнее эффективность вычисления. Такие хеш-функции используются, к примеру, при создании того же словаря или Map в java.

— Высокая скорость вычисления — одно из ключевых требований к некриптографическим хеш-функциям — это очень высокая скорость вычисления. Они должны быстро обрабатывать данные для повышения эффективности работы алгоритмов, таких как хеш-таблицы или контрольные суммы.

— Нет необходимости в устойчивости к первому и второму прообразу — это значит что для некриптографических хеш-функций устойчивость к первому или ко второму прообразу не является критически важной. Задачи, в которых используются такие хеш-функции, обычно не требуют защиты от нахождения второго сообщения, имеющего тот же хеш. Основной акцент делается на быстроте и эффективности.

— Коллизии допустимы в ограниченных случаях — в отличие от криптографических хеш-функций, в некриптографических хеш-функциях коллизии допустимы в определённых рамках. Хотя они должны быть достаточно редкими для обеспечения адекватной производительности, наличие коллизий не ставит под угрозу безопасность, как в случае с криптографическими функциями.

Таким образом, некриптографические хеш-функции оптимизированы для скорости и удобства обработки информации, и они не требуют свойств, таких как устойчивость к нахождению прообраза или второго прообраза. Но, что бы вам не казалось, что вы можете наплевательски относиться к таким функциям со стороны безопасности, я советую вам ознакомиться с такими рода атак как «атака на хеш-таблицы» (hash table attack) или «атака с переполнением» (overflow attack).

Далее мы будем говорить только о криптографических хеш-функциях, так как именно они используются при создании аутентификационного сообщения или, как сокращенно это называется, MAC, но перед тем как перейти к описанию оного, давайте познакомимся с главными реализациями криптографических хеш-функций.

SHA-2 и SHA-3

Сразу же стоит упомянуть популярные криптографические хеш-функции: MD5 и SHA1. Они считались хорошими хеш-функциями, но с течением времени эксперты доказали их недостаточную устойчивость к коллизиям. Тем не менее вы все еще можете их встретить в некоторых системах, а мы продолжим к более безопасным хеш-функциям.

SHA-2

SHA-2 (алгоритм безопасного хеширования — 2, Secure Hash Algorithm 2) — широко распространенное семейство криптографических хеш-функций разработанный Национальным институтом стандартов и технологий США (NIST) в 2001 году, как более безопасная альтернатива предшествующему алгоритму SHA-1.

SHA-2 включает несколько различных хеш-функций, отличающихся длиной выходного хеш-значения. Основные из них -SHA-224, SHA-256, SHA-512/256.

Техническое составляющее алгоритмов не является темой данной статьи, но стоит упомянуть, что SHA-2 не подходит для хеширования секретов, так как он использует структуру Меркла-Дамгарда (алгоритм, который хеширует сообщение путем иетративного вызова функции сжатия), а данная структура имеет недостаток, который делает данную хеш-функцию уязвимой для атаки удлинением сообщения при использовании ее для хеширования секретов.

SHA-3

В связи с проблема в таких хеш-функциях как MD5, SHA-1 и SHA-2 в 2007 году NIST решил провести открытый конкурс на новый стандарт — SHA-3.

SHA-3 удовлетворяет всем свойства криптографических хеш-функций, о которых мы упомянули выше и обеспечивает такую же защиту как и SHA-2, но плюс к этому она не подверженна атакам удлинением сообщения и может применяться для хеширования секретов, по этой причине советуют выбирать именно ее.

SHA-3 включает несколько различных хеш-функций, отличающихся длиной выходного хеш-значения. Основные из них — SHA3–224, SHA3–256, SHA3–384.

Хоть производительность может варьироваться в зависимости от конкретной реализации и окружения — в общем, SHA-2 обычно быстрее, чем SHA-3, особенно в программной реализации. Выбор за вами. Мы увидем дальше, что SHA-2, при своих проблемах, хорошо подходит для работы в HMAC.

Вначале статьи я упомянул что непосредственно работал с хеш-функциями при шифровании паролей и по этой причине не могу не упомянуть что для этого лучше использовать соответствующие хеш-функции для хеширования паролей такие как Argon2, scrypt и тд. Советую ознакомиться с их особенностями самостоятельно.

Код аутентификации сообщения или имитовставка

Вот мы и добрались до второго криптографического примитива, понимания которого нам необходимо для реализации проверки подлинности данных переданных от Telegram.

MAC

В общем, если смешать хеш-функцию с секретным ключом, то получиться так называемый код аутентификации сообщения, или имитовставка (message authentication code, MAC) — это важная концепция в криптографии, которая помогает обеспечить целостность и подлинность данных.

Имитовставкой называется алгоритм с использованием секретного ключа, который принимает не только входные данные, как хеш-функция, но и сам секретный ключ. Результатом мы получаем уникальный аутентификационный тег. И, конечно же, данный процесс детерминирован.

6112c54a3436c5f9a88f5596e36fa74c.jpg

HMAC

Кто-то прочитав описание MAC, что я дал выше, может выразить свое недовольство, тем что я упоминаю конкретно хеш-функцию как тот самый криптографический примитив который будет на пару с ключём создавать нам нужный аутентификационный тег, будто бы только хеш-функция может нам с эти помочь, и он будет прав в своем недовольстве, хоть я и намеренно допустил такую ошибку. Теперь же я поправлюсь на счет MAC и добавлю, что он может быть основан на различных криптографических примитивах, таких как блочные шифры, хеш-функции и тд.

А вот HMAC, это уже конкретный вид MAC, который использует хеш-функцию (например, SHA-256) в комбинации с секретным ключом для создания аутентификационного кода.
Даже просто их полное название само о себе говорит: HMAC (Hash-based Message Authentication Code) и MAC (Message Authentication Code)

HMAC-SHA256

HMAC-SHA256 — это конкретная реализация алгоритма HMAC, которая использует хеш-функцию SHA-256 (Secure Hash Algorithm 256-bit) для вычисления хэш-кода аутентификации сообщения.

Вот та самая конкретная реализация которая вобрала в себя отдельно взятые примитивы для достижения конкретной задачи — обеспечении аутентификации и целостности данных с высокой степенью безопасности.

Опять повторюсь, что целью статьи не является погружение в технологический аспекты тех или иных примитивов, так как я думаю не большой экскурс по предыдущим составным частям дает нам общую картинку, и вселяет в нас некоторую веру в безопасность тех технологий которые мы используем или проще говоря мы понимаем как это работает, а что бы разбираться как это внутри реализовано — на то в интернете или в на бумаге есть много материала, терпения и сил вам, а мы же продолжим к кульминации — к практике, где мы увидим как все выше описанное помогает нам решать сложную, но насущную задачу.

Внимательный читатель может задаться вопросом — почему именно SHA256 мы используем в HMAC, разве я не писал выше что функции данного семейства подверженны атаке удлинением сообщения ? Верное замечание, и мне стоит в конце ответить на него — HMAC делает SHA256, и другие криптографические функции, устойчивыми к атаке удлинением сообщения и, более того, другим криптографическим атакам.

Telegram Mini Apps и HMAC-SHA256

Что такое Telegram Mini Apps? В общем это возможность запустить ваш веб-сервис внутри Telegram, тем самым получая многие удобства, которых очень много, но нас в данной статье интересует одно — доступ к пользователям телеграмма минуя сложности реализации внутренней системы регистрации и аутентификации, ну, по-крайней мере, я считаю это большим плюсом данной технологии.

Как об этом сказано в документации: с помощью Mini Apps разработчики могут использовать JavaScript для создания бесконечно гибких интерфейсов, которые могут быть запущены прямо в Telegram и могут полностью заменить любой веб-сайт.

Так вот, мы просто, следуя любому пособию делаем из нашего обычного сайта — Telegram Mini App. Потом мы внутри нашего «приложения» легким движением руки (из динамической части URL, в хэше) получаем в параметрах запуска данные инициализации (tgWebAppData) этот набор данных, в основном, и хранит данные о пользователе что запустил ваше приложение. Ура, вот так просто у нас уже в системе есть пользователь, на этом наверно можно было бы закончить, но… Как мы понимаем, не все что на фронте мы можем принять за чистую монету будет являться тем же самым на бэке. Вот тут-то и задача с каждым запросом из нашего «приложения» отправлять эти данные на бэк, и там не просто им слепо доверять, а как-нибудь убедиться что это точно было нам отправлено из нашего веб-сервиса запущенного из Telegram и это именно тот пользователь чьи данные мы присылаем с каждым запросом.

6b268c10848bcfe885a02b9398e66d76.jpg

В общем наш короткий план таков:

На счет первого пункта все просто, реализация данной части не является для нас существенным, но кратко обозначим: просто получаем те самые данные инициализации из URL, через интерсептор в каждый запрос от клиента добавляем заголовок — X-Telegram-Init-Data: ${initData}, я к тому же еще использую base64 перед тем как вставить данные в заголовок, но тут дело ваше.

Вторая часть нашего плана нам более интересно и вы тут спросите — А что же у нас на сервере? Так как мы используем Spring Boot, то нам просто нужен фильтр для Spring Security и я, перед тем как что-то объяснять, просто полностью продемонстрирую его:

  
public class TelegramAuthFilter extends OncePerRequestFilter {

  private static final Logger log = LoggerFactory.getLogger(TelegramAuthFilter.class);

  private final String botToken;

  private byte[] secretHashByInitData;

  private final AuthenticationService authenticationService;

  private final ObjectMapper objectMapper;

  public TelegramAuthFilter(AuthenticationService authenticationService, String botToken) {
    this.authenticationService = authenticationService;
    this.botToken = botToken;
    this.objectMapper = new ObjectMapper();
    this.objectMapper.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE);
  }

  @PostConstruct
  public void init() {
    try {
      this.secretHashByInitData = getSecretHashByInitData();
    } catch (Exception ex) {
      log.error("[TELEGRAM_AUTH_FILTER_ERROR] init error", ex);
    }
  }

  @Override
  protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
                                  FilterChain filterChain)
      throws ServletException, IOException {

    if (StringUtils.isBlank(request.getHeader("X-Telegram-Init-Data"))) {
      filterChain.doFilter(request, response);
      return;
    }

    try {
      var result = Base64.getDecoder().decode(request.getHeader("X-Telegram-Init-Data"));
      var params = parseQueryString(new String(result));

      var userBody = params.get("user");
      var hash = params.get("hash");

      if (StringUtils.isNotBlank(userBody) && StringUtils.isNotBlank(hash)) {
        // Проверка подписи
        if (validateTelegramAuth(params, hash)) {
          var user = objectMapper.readValue(userBody, UserInit.class);
          authenticationService.authenticateUserByTelegram(user.getId());
        }
      }
    } catch (Exception e) {
      log.error("[TELEGRAM_AUTH_FILTER_ERROR] telegram user ", e);
    }

    // Продолжаем фильтрацию цепочки
    filterChain.doFilter(request, response);
  }

  private boolean validateTelegramAuth(Map paramMap, String receivedHash) throws Exception {

    // Пункт 1. Убираем hash и сортируем оставшиеся параметры
    var dataString = paramMap.entrySet().stream()
        .filter(e -> !"hash".equals(e.getKey()))
        .sorted(Map.Entry.comparingByKey())
        .map(e -> e.getKey() + "=" + e.getValue()) // Берем первый элемент из массива параметров
        .collect(Collectors.joining("\n"));

    // Пункт 2. Создаем HMAC SHA-256 хеш
    var sha256HMAC = Mac.getInstance("HmacSHA256");
    var secretKeySpec = new SecretKeySpec(this.secretHashByInitData, "HmacSHA256");
    sha256HMAC.init(secretKeySpec);

    byte[] hash2 = sha256HMAC.doFinal(dataString.getBytes());

    // Пункт 3. Преобразуем байты хеша в строку в hex формате
    var calculatedHash = bytesToHex(hash2);

    // Пункт 4. Сравниваем полученный хеш с тем, что был в запросе
    return calculatedHash.equals(receivedHash);
  }

  private byte[] getSecretHashByInitData() throws InvalidKeyException, NoSuchAlgorithmException {
    var sha256HMAC = Mac.getInstance("HmacSHA256");
    var secretKeySpec = new SecretKeySpec("WebAppData".getBytes(), "HmacSHA256");
    sha256HMAC.init(secretKeySpec);

    return sha256HMAC.doFinal(botToken.getBytes());
  }

  private Map parseQueryString(String queryString) throws UnsupportedEncodingException {

    Map result = new HashMap<>();
    var pairs = queryString.split("&");

    for (String pair : pairs) {
      String[] keyValue = pair.split("=", 2);
      String key = URLDecoder.decode(keyValue[0], StandardCharsets.UTF_8);
      String value = URLDecoder.decode(keyValue.length > 1 ? keyValue[1] : "", StandardCharsets.UTF_8);
      result.put(key, value);
    }

    return result;
  }

  private String bytesToHex(byte[] bytes) {
    StringBuilder sb = new StringBuilder();
    for (byte b : bytes) {
      sb.append(String.format("%02x", b));
    }
    return sb.toString();
  }
}

Вы можете ознакомиться полностью с фильтром самостоятельно, но, а мы в данной статье уделим подробное внимание методу validateTelegramAuth. Данный метод возвращает boolean значение так как именно этот метод определяет подлинность присланных данных в заголовке с помощью HMAC-SHA256:

Пункт 1. Необходимо перебрать все поля полученного словаря, исключить от туда поле hash, отсортировать словарь по ключам в алфавитном порядке, трансформировать в строковое значение в формат key=value и объединяем все в одну цельную строку с помощью переноса строки.

Пункт 2. Создаем MAC инстанс, в нашем случае это HmacSHA256 и инициализируем его ключем, который сам является хешем.

Этот ключ-хеш был определен на этапе создания bean фильтра в init() методе из токена бота (так как Telegram Mini App должно быть прикреплено к своему Telegram боту, то токен бота тот самый секрет который доказывает добропорядочность отношений между вами и Telegram), и тут так же используя HmacSHA256, но в данном случае инициализурая его ключем который являет из себя просто строковую константу WebAppData — мы получаем хеш который и используем в пункте 2. Смотрите метод getSecretHashByInitData

Пункт 3. Преобразуем байты хеша в строку в hex формате

Пункт 4. Сравниваем результирующий хеш с тем что был получен в запросе, и относительно результата этого сравнения — аутентифицируем пользователя, либо нет.

Вот наглядный практичный пример решения столько важной задачи которую мы, не прибегая к внешним зависимостям, используя лишь то что уже есть у в нашем JDK арсенале, а именно Java Cryptography API, решили и можем с чистой душой использовать.

Я не буду более сыпать дополнениями, я думаю моя статья началась с относительно не малой теорией и закончилась лаконичной, но точным куском практической информации, которые в сумме дают общее понимание реализации проверки подлинности данных, а так же указывает направление куда далее вы можете направить свой огонь любопытства.

Но, последним я хотел бы теперь от вас получить ответ на один вопрос, на который я сам, честно говоря, и не искал ответ, хоть и имею некоторое свое убеждение на этот счет, думаю самое время получить его от вас в комментариях. Так вот, скажите пожалуйста зачем Telegram, и тем самым и нас, принуждает применить HMAC-SHA256 к токен бота используя при этом статичное строковое поле в виде ключа (смотрите пункт 2), когда можно было сразу применить токен бота как ключ к HMAC-SHA256 при хешировании данных инициализации ? У меня вот один ответ — Telegram возможно так себя и нас огораживает от опасности постоянного хранения токена бота в оперативной памяти, ведь нужно хранить просто хеш ? И возможно мне стоит удалить этот токен в своем коде сразу как я получу значения из метода getSecretHashByInitData? Я прав или выдумываю ?

Хм, а это не так просто отдать столько существенную точку входа в систему на откуп какой-то якобы безопасной штуке, название чье собранно из других непонятных сложных слов-аббревиатур, которое так круто звучит и выглядит как непобедимый супер-мупер трансформер, одно слово HMAC-SHA256! Это вы еще не слышали про других крипто-трансформер защинимках: ChaCha20Poly1305, AES-GCM-SIV, да что там, герой нашей статьи сам бескорыстно иной раз является составной частью иного доблестного трансформера — AES-CBC-HMAC (этот не только по названию, но и по своей сути, трансформер, вы уж поверьте). Ну так вот, вернемся к герою статьи — нам и вправду стоит ему доверять ? Ну если да, вы советуете просто скопировать столь короткий код, в сравнении с задачей что он решает, и радоваться жизни ? Хорошо. Через некоторое время где-то в дальнем углу офиса или в очередном важном чете:

Саша: Наша красная команда взломала нас.
Миша: У нас нет красной команды.

В общем, я к чему ввиду: понимаю что в общем разработка ПО это доверие той или иной реализации, но когда дело касается столь весовых вещей, желается быть более осведомленнее и увереннее в том кто или что охраняет твой покой и покой твоих пользователей. И в кибербезопасности так же как и в остальном, мне видеться, 4 типа решения задач:

  • Решать все самому.

  • Довериться стороннему решению.

  • Комбинировать разные сторонние решения.

  • Комбинировать разные сторонние решения плюс добавить свое ноу-хау.

Может я параноик, и это излишни, но если бы я создавал свою невероятно безопасную систему, я наверно все-таки обратился бы к последнему варианту. Ну, а что такого ? Пусть хоть это самопальная безделушка в руках уже созданного другими супер трансформера выглядит излишней, но я буду спокойнее спать. Ну да ладно, это я так — мысли в слух.

© Habrahabr.ru