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

Я делаю это не для того, чтобы навлечь позор на людей, которые написали что-то не так. Я просто хочу приложить руку к решению проблемы. За время своей работы в AppSec я очень устал от бесконечных однообразных споров. Я делаю всё, что в моих силах, чтобы люди могли без особых сложностей реализовать всё правильно, в частности, указываю на код, который можно использовать смело – например, из репозитория Secure Compatible Encryption Examples от Люка Парка. И, тем не менее, попадаются команды, которые продолжают упорствовать, даже если код еще не ушел в продакшн – а ведь это самое лучшее время для исправления ошибок. Это усложняет всем жизнь: я теряю время на объяснения, что с кодом не всё в порядке, а команде потом приходится делать значительно больше работы, потому что после того как код уходит в продакшн, для исправления криптографии приходится сначала составлять план миграции.

Я убежден: есть основания надеяться, что в будущем проблем с безопасностью, связанных с криптографией, станет меньше. У многих криптографических имплементаций появляются усовершенствованные API, во многом благодаря NaCl Дена Бернштейна. Кроме того, StackOverflow больше не закрепляет принятый ответ наверху. Это дает людям возможность выводить наверх ответы с лучшими решениями, чем то, которое было принято изначально. Можно руководствоваться этим. Давайте будем друг с другом любезны: ведь ставить плюсы хорошим ответам полезнее, чем ставить минусы плохим.

А теперь перейдем к конкретике.

Пример №1: AES-128 в режиме CBC на Java


Код находится здесь. На момент написания статьи у этого ответа 248 плюсов: он и самый популярный, и выбран автором. Чтобы вникнуть, что в нём не так, взгляните на типы ключа и вектора инициализации: и тот, и другой – строки. А должны быть массивами байтов.



То, что ключ оказывается строкой – проблема, по моему опыту, распространенная. Если вы используете строки, это значит, что у вас пароль. Пароли криптографическими ключами не являются, но могут быть в них конвертированы при помощи функции деривации ключа на основе пароля. Ну а здесь делается именно то, чего делать нельзя: строка с паролем (который неверно определили как ключ) копируется и вставляется в структуру SecretKeySpec.

Использование строки для вектора инициализации – ситуация менее распространенная, но тоже неправильная. Прежде чем я начну рассказывать, как это исправить, давайте посмотрим на вызывающую функцию:



Здесь мы видим следующие проблемы:

  • Жестко прописанный пароль (который ошибочно обозначен как ключ)
  • Жестко прописанный вектор инициализации строкового типа

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

Чтобы яснее показать разницу между паролем и ключом: у пароля размером в 128 бит должно быть 128 бит энтропии, соответственно, чтобы его взломать потребуется 2 в 128 степени попыток. В местной реализации пароль составляется из больших и маленьких букв, а также символов. Если пароль сформирован из этого набора абсолютно произвольным образом, то возможных комбинаций получается 66 в 16 степени, что соответствует порядку 2 в 96 степени. Это значит, что на взлом уйдет максимум 2 в 96 степени попыток, но на самом деле, всё хуже, потому что люди редко составляют пароли произвольным образом. Очень часто я вижу, что на шифрование уходят крайне предсказуемые пароли с финальным аккордом в духе ‘123!’. Печально, что людей приучили следовать древним рекомендациям по выбору надежных паролей, потому что это якобы даст больше безопасности. Не даст и не надо делать то, что там написано.

Важный момент: не используйте один и тот же вектор инициализации дважды – это убивает все защитные свойства. Я объяснял эту ошибку в другой статье, и это вызвало очень много споров. Когда кто-нибудь говорит: «Да ладно, поместим вектор инициализации в vault», раздражение у меня доходит до пика. Нет, не ладно: векторы инициализации не должны быть секретными, и даже если их спрятать, проблемы с безопасностью от этого никуда не денутся. Хватит создавать собственные криптолагоритмы! К сожалению, очень многие даже не осознают, что именно это и делают.

В этом коде есть еще один недочет: он производит шифрование без аутентификации. В своей статье Top 10 Developer Crypto Mistakes я поместил под седьмым номером ошибку «Предполагать, что шифрование обеспечивает целостность сообщения». Теперь я принял философскую позицию, которую рекомендует проект NaCl: разработчикам нет необходимости понимать разницу, достаточно просто всегда использовать шифрование с аутентификацией – и волки сыты, и овцы целы. В нашем случае алгоритм AES решает проблему в режиме GCM, но в режиме CBC проблема остается.

Гораздо лучший ответ предлагает в той же ветке пользователь Патрик Фэвр. Пожалуйста, ознакомьтесь с ним сами и, если решение покажется вам удачным, как показалось мне, поставьте плюс.

Пример №2: AES-256 в режиме CBC на C#


Код находится здесь. На момент написания статьи это второй по популярности ответ, у него 117 голосов. Он не отмечен автором, но всё-таки пользуется успехом у пользователей.

Одна проблема в этом коде бросается в глаза, остальные менее очевидны. Взгляните на скриншот:



Да, еще один случай жестко прописанного пароля, который ошибочно назван ключом. Но есть и хорошие новости: тут используется функция деривации ключа (Rfc2898DeriveBytes, то есть PBKDF2 с применением HMAC_SHA1), которая превращает его в настоящий ключ. Неплохо, но учитывая, что число итераций не оговорено (на странице StackOverflow нужно прокрутить вправо, чтобы в этом убедиться), будет использоваться принятое по умолчанию значение – тысяча, а это, по современным стандартам, очень мало и не может обеспечить должную безопасность.

Теперь прокрутим вправо и посмотрим, что загружается в Rfc2898DeriveBytes в качестве соли хэш-функции.



Жестко прописанная соль! Солям необязательно быть секретными, но во многих случаях их не стоит использовать больше одного раза. В любом случае, с этой солью разумнее избегать жесткого кодирования.

Вот вам реальная история: я видел этот самый кусок, когда проводил инспекцию кода для клиента. Помню, посмотрел на соль и подумал: «Что-то наводит на мысли об ASCII». Тогда я написал программу на C, чтобы вывести ее строкой и получил следующее: «Иван Медведев». Затем я загуглил вектор инициализации и вышел на тот самый ответ на StackOverflow. Поневоле задумался, кто этот бедный Иван Медведев, имя которого увековечили в ненадежном коде.

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

Если бы автор просто скопировал пример из Rfc2898DeriveBytes от Microsoft, то оказался бы намного ближе к правильному ответу. Однако остается еще проблема шифрования без аутентификации, связанная с режимом CBC, да и итераций у Microsoft тоже маловато.

Несколько радует то, что выбранный автором ответ на StackOverflow лучше и плюсуют его чаще. Но и там те же проблемы: число итераций для Rfc2898DeriveBytes( ) слишком низкое и они используют шифрование без аутентификации (в режиме CBC).

Относительно того, что считать подходящим количеством итераций, единого мнения нет. NIST полагает, что «число итераций должно быть настолько большим, насколько позволяет производительность сервера верификации, как правило, не меньше 10 000». OWASP ставит планку в минимум 720 000 итераций. Разброс между этими двумя значениями немалый; возможно, стоит ознакомиться с тем, что говорит Томас Порнин. Так или иначе, любое число ниже 10 000 однозначно следует считать не соответствующим современным стандартам.

Пример №3: Triple DES на Java


Код находится здесь.

Вопрос задавали в 2008 году, так что некоторые недочеты можно простить. На тот момент NIST рекомендовал AES, но все-таки допускал Triple DES в отдельных случаях. Сегодня Triple DES считается абсолютно устаревшим из-за слишком маленького размера блоков, однако я по-прежнему время от времени сталкиваюсь с ним в коде.

Давайте повнимательнее изучим топовый ответ, у которого на момент написания статьи 68 плюсов:



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

В коде используется нулевой вектор инициализации – это очень распространенная проблема, которую я часто вижу в кодовых базах. Аутентификации тоже нет, но это простительно: в то время само понятие «шифрование без аутентификации» было не слишком широко известно.

Пожалуйста, никогда никуда не применяйте код такого вида.

Пример №4: AES на Java


Код находится здесь. На момент написания статьи у него было 15 плюсов.



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

У более достойного ответа в той же ветке всего 11 плюсов. Там режим CBC и вектор инициализации прописаны как надо. Аутентификации нет, но в остальном всё выглядит вполне разумно.

Пример №5: пожалуйста, не делайте так


Ответ находится здесь. Скриншот делать не буду, там всё слишком плохо.

Вопрос был о том, как зашифровать строку на Java. Выбранный автором ответ с 23 голосами предпринимает попытку реализации при помощи шифра Вернама. Над сообщением висит предупреждение, предостерегающее никогда не применять этот метод. И совершенно справедливо: проблема с этим шифром, который называют одноразовым блокнотом, в том, что на деле он скорее n-разовый, где n > 1. А когда блокнот используется больше одного раза, взломать шифр становится очень просто.

Это хороший пример того, как новая система StackOverflow, выводящая ответ с наибольшим числом голосов наверх, приносит хороший результат. Ответ, у которого на текущий момент больше всего голосов (187), дает очень толковый ознакомительный обзор по шифрованию – его однозначно стоит прочитать. Придраться могу только к некоторой путанице с SecureRandom( ), которую автор и сам признает. Риск, конечно, невелик, но вот как правильно использовать SecureRandom( ) – это очень просто.

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

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

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

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

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


  1. Suvitruf
    30.09.2021 10:20

    Это хороший пример того, как новая система StackOverflow, выводящая ответ с наибольшим числом голосов наверх, приносит хороший результат.
    О какой «новой системе» речь, если сортировке сто лет в обед? ????
    image


    1. Browning
      01.10.2021 10:52
      +1

      Речь об изменении порядка ответов на странице вопроса, которое произошло менее месяца назад (автор пишет о нём во втором абзаце): раньше ответ, принятый автором вопроса, всегда был сверху независимо от рейтинга, а теперь нет.


      1. Suvitruf
        01.10.2021 12:00
        +1

        О как, интересное нововведение. Спасибо за объяснение, а то не так понял)


  1. Vamp
    30.09.2021 14:12
    +6

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

    Благо, прогресс не стоит на месте и появляются простые криптографические API для использования не только профессиональными крипторафами.


  1. Mingun
    30.09.2021 18:11

    Хватит создавать собственные криптолагоритмы!

    Великолепная описка!


  1. darkdaskin
    30.09.2021 20:30

    Передаю привет Ивану Медведеву, видел этот фрагмент в унаследованном коде, с которым работал пару лет назад и тоже нагуглил исходный ответ из любопытства. Поскольку этот модуль планировалось в будущем переписать целиком, оставил всё как есть.