Несмотря на бурное развитие технологий, по сегодняшний день многие разработчики гнушаются локальным хранением элементарной информации: используют Google fonts, загружают килобайтный JS-файл с сервера, расположенного на другом континенте и прочие топологические и логические неоднозначности. При таком подходе к разработке веб-приложений мысли о локально-генерируемой и этичной капче появляются крайне редко. А если и появляются, возникают вопросы: как генерировать, хранить и проверять ответы, ассоциировать картинки с конкретной сессией пользователя и прочее. Итог почти всегда один: использовать сторонние онлайн-сервисы вроде reCAPTCHA. На вкус и цвет товарищи всегда найдутся, но сейчас рассмотрим альтернативу.

Давайте познакомимся с Zero Storage Captcha, которая работает локально (возможно, в виде дополнительного класса в коде приложения), не обязывает хранить информацию на стороне сервера о сгенерированных картинках и в тот же момент позволяет проверить ответ любого пользователя со стопроцентной вероятностью.

Zero Storage Captcha Repository Cover
Zero Storage Captcha Repository Cover

Концепция

Если не хочется организовывать базу данных под капчи, почему бы не хранить ответ на капчу у самого пользователя? Чтобы реализовать эту идею и при этом исключить перехват ответа на картинку на стороне пользователя, нужно обратиться к базовой криптографии. Пусть пользователь хранит не ответ в чистом виде, а специальный токен, который будет передан серверу вместе с ответом на капчу и позволит проверить правильность ответа. Криптография поможет нам не хранить пару "правильный ответ - токен", а производить проверку через простые вычисления по факту поступления запроса от пользователя.

Встает несколько вопросов:

  1. Алгоритм генерации токена, защищенный от подделки;

  2. Ограничение времени жизни капчи;

  3. Невозможность использования одного верного ответа дважды.

Реализация

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

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

Токен базируется на следующих составляющих:

  1. Верный ответ на капчу;

  2. Хеш (SHA256);

  3. Подпись (X25519);

  4. Временной маркер.

В виде формулы алгоритм создания токена можно представить так:

SIGNATURE( HASH(answer value + time token) )

Фактически токен является подписью в кодировке base64. Чтобы сделать токен более компактным, принято решение сокращать подпись в три раза — в итоговую строку добавляется каждый третий символ. Также удаляются все спецсимволы вроде =, - и _. На выходе получается строка примерно такого вида: i2oefBw6mswaORIphgDcY7GwnS.

Временной маркер (time token) обновляется каждые полторы минуты, благодаря чему через небольшой промежуток времени проверочный токен для капчи с тем же ответом будет абсолютно новый.

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

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

Можно усомниться в нужности хеша, так как операция подписи даст не менее уникальную строку при прямом применении к сконкатенированной строке answer + time token. С этим сложно поспорить, поэтому оправдывать хеш сильно не стану: в изначальной реализации он есть и хлеб не просит. Возможно, в будущих реализация Zero Storage Captcha произойдет отказ от предварительного хеширования.

Получая от пользователя токен и ответ на капчу, сервер выводит из ответа пользователя новый токен. Если новый токен и изначальный проверочный совпадают, о верности ответа выносится положительный вердикт.

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

Использование

Сфера применения описанной технологии также широка, как и область применения любой капчи. В сценарии с веб-страницами Zero Storage Captcha может быть реализована при помощи JS, либо силами чистого HTML, если он генерируется на бекэнде и позволяет подставлять дополнительный уникальный ключ в <form> отправки ответа на капчу.

Если вы хотите использовать Zero Storage Captcha в проектах на C++, ознакомьтесь с заголовочной библиотекой на несколько сотен строк. Если интеграция плюсового класса в ваш продукт затруднительна, воспользуйтесь Zero Storage Captcha в виде отдельного приложения, которое предоставляет простой REST API и может работать как локально, так и на любом удобном для вас сервере.

Комментарии (14)


  1. andreymal
    25.04.2022 10:54
    +4

    проверочный токен заносится в специальный кеш

    Это уже не "Zero Storage", можно в том же самом кеше просто хранить ответ на капчу и не выпендриваться (собственно, именно так я на всех своих сайтах и делаю)


    1. pureacetone Автор
      25.04.2022 11:41

      Можете в деталях описать как вы храните капчи, чтобы разница стала очевидной?


      1. andreymal
        25.04.2022 12:03

        Ничего особенного, просто кладу в Redis (можно и memcached по вкусу) строку с ответом, ключ — рандомный токен, по таймауту редис автоматически удаляет старые капчи


        127.0.0.1:6379> keys *captcha_sol_*
         1) "captcha_sol_ZTcabneMEqKgjCq4"
         2) "captcha_sol_7y6sGkqsg7J5mCb8"
         3) "captcha_sol_w2LUY3wNkWHj3BVj"
        127.0.0.1:6379> get captcha_sol_ZTcabneMEqKgjCq4
        "YB2PB"


        1. pureacetone Автор
          25.04.2022 13:35
          +1

          Похожая механика, согласен. Остается оправдаться, что в моем случае пару минут хранятся только те токены, на которые пришел корректный ответ. Таким образом запрос тысячи капч не увеличит объем потребляемой памяти ни на йоту под хранение сгенерированной информации. Хранятся только использованные токены, чтобы не счесть их валидными дважды.


          1. andreymal
            25.04.2022 13:58
            +2

            Залил миллион ключей в Redis — он скушал 147 МБ оперативки. Наверное, каждый сам для себя решит, какой вариант лучше (ваш вариант тоже имеет свои плюсы), но я предполагаю, что большинству проектов не понадобится хранить миллион капч одновременно и такое потребление памяти можно считать незначительным


            1. pureacetone Автор
              25.04.2022 14:45

              Спасибо за дискуссию по сути темы


            1. Revertis
              25.04.2022 18:08
              +1

              большинству проектов не понадобится хранить миллион капч одновременно

              Но ведь это потенциальная уязвимость DoS, которую могут даже скрипт-кидди использовать. Просто в цикле, во много потоков, будут запрашивать у вас капчи сутками.


              1. andreymal
                25.04.2022 18:34

                Во-первых, Капитан Очевидность сообщает, что нужно ставить рейт-лимиты по айпишникам (и это касается вообще любых действий клиентов, не только капчи).

                Во-вторых, в такой ситуации сервак помрёт от перегрузки намного раньше, чем от нехватки памяти (и это относится к любым реализациям капчи, да и вообще к реализациям какой угодно CPU-bound задачи).


            1. select26
              27.04.2022 09:45
              +1

              Идея толковая.

              Но в моем случае я часто получаю DoS'ы по 1-2MRPM. И в моем случае подход автора топика оправдан - экономия памяти будет очень существенная.

              Хотя, при таких объемах можно наступить на CPU limit - вычисления то на каждый ВЫДАННЫЙ токен, не на проверенный.

              Буду тестировать.

              Спасибо всем за хорошую идею и дискуссию!


  1. mixsture
    25.04.2022 15:31

    Имхо, стойкость капчи низкая. Волна искажения во всех картинках к статье — константная => исключить ее легко. Эллипс с контрастом динамический, но я на вскидку не вижу больших сложностей в его вычислении, даже если использовать только 1 из его цветов (а основной цвет эллипса всегда противоположен основному преобладающему цвету картинки).
    После исключения этих 2 факторов останутся лишь точки и линии, которые очень тонкие и врятли сильно будут мешать OCR. Но даже если и будут, у них сильно маленькая толщина относительно букв, поэтому я бы применил какой-нибудь контурный механизм из openCV, посчитал их площадь и выкинул 1% самых маленьких объектов по площади, заменив их окружающим фоном — точки выкинет. Линии тоже относительно легко линейной регрессией вычислить.
    Так что, сугубо имхо, но содержимое этой капчи довольно легко восстанавливается до первоначального текста.


    1. pureacetone Автор
      25.04.2022 16:03

      Количество мелких точек (шум), количество мусорный линий и их ширина, а также эллипсы и их параметры, как и общие параметры искажения параметризуются уже реализованными в классе сеттерами. Идея по умолчанию рандомизировать волну искажения хорошая, реализую. Не задумывался.


  1. Revertis
    25.04.2022 18:13

    Логика построена таким образом, чтобы одновременно хранились два тайм-токена

    А где это хранение происходит?