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

Важный update: в комментариях SamDark сделал замечание, что библиотека Mcrypt давно не поддерживается и имеет ряд недоработок, поэтому рекомендуется использовать OpenSSL. Если требуется переписывать имеющийся код, то может помочь эта статья. Кроме того, есть сведения, что Mcrypt может быть удален в PHP7.

Это краткое руководство о том, как избежать распространенных ошибок с симметричным шифрованием на PHP.

Будем рассматривать случай, когда данные обрабатываются на стороне сервера (в частности, шифрование происходит на сервере, а данные могут быть получены, например, от клиента в виде открытого текста, пароля и т.п.), что является типичным случаем для PHP-приложений.

Cведения из этого руководства не стоит использовать для создания шифрованных сетевых соединений, которые имеют более сложные требования. Для таких случаев надо использовать spiped или TLS.

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

Функции шифрования в PHP


Используйте расширения Mcrypt или OpenSSL.

Алгоритм шифрования и его режим работы, одноразовый код (вектор инициализации)


Используйте AES-256 в режиме CTR со случайным одноразовым кодом (прим. перев.: nonce). AES это стандарт, поэтому можно использовать функции любого из расширений — Mcrypt или OpenSSL.

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

Одноразовый код должен быть длиной 128 бит (16 байт), просто строка байт без какого-либо кодирования.

В расширении Mcrypt AES известен как Rijndael-128 (прим. перев.: несмотря на то, что речь идет про AES-256, это не ошибка. AES-256 != Rijndael-256). В OpenSSL соответственно AES-256-CTR.

Пример использования Mcrypt:
<?php
// $key length must be exactly 256 bits (32 bytes).
// $nonce length must be exactly 128 bits (16 bytes).
$ciphertext = mcrypt_encrypt(MCRYPT_RIJNDAEL_128, $key, $plaintext, 'ctr', $nonce); // Mcrypt

Пример использования OpenSSL:
<?php
// $key length must be exactly 256 bits (32 bytes).
// $nonce length must be exactly 128 bits (16 bytes).
$ciphertext = openssl_encrypt($plaintext, 'AES-256-CTR', $key, true, $nonce); // OpenSSL

Убедитесь, что шифрование работает правильно с помощью тестовых векторов (прим. перев.: для AES-256-CTR см. пункт F.5.5 на странице 57).

Для режима CTR существуют некоторые ограничения на суммарный объем шифруемых данных. Возможно вы не встретитесь с этим на практике, но имейте ввиду, что не стоит шифровать более чем 2^64 байт данных одним ключом, безоносительно того одно ли это длинное сообщение или много коротких.

Режим CTR сохраняет стойкость только если не использовать один и тот же одноразовый код с одним и тем же ключом. По этой причине важно генерировать одноразовые коды при помощи криптографически стойкого источника случайности. Кроме того, это означает, что вы не должны шифровать более чем 2^64 сообщений с одним ключом. Поскольку длина одноразового кода 128 бит, важно ограничение на количество сообщений (и соответствующих им одноразовых кодов) 2^128/2 из-за парадокса Дней рождения (прим. перев.: подробнее про парадокс).

И помните, что шифрование не сможет скрыть тот факт, сколько данных вы посылаете. Как пример экстремального случая, если вы шифруете сообщения, содержащие только «да» или «нет», очевидно, шифрование не скроет эту информацию.

Аутентификация данных


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

При расшифровке сначала проверте HMAC, используя алгоритм сравнения устойчивый к атакам по времени. Не сравнивайте напрямую $user_submitted_mac и $calculated_mac, используя операторы сравнения == или ===. Лучше даже использовать "двойную проверку HMAC".

Если проверка HMAC удачна, можно безопасно производить расшифровку. Если же HMAC не подходит, немедленно завершайте работу.

Ключи шифрования и аутентификации


В идеале использовать ключи, полученные из криптографически стойкого источника случайности. Для AES-256 необходимы 32 байта случайных данных («сырая» строка – последовательность бит без использования какой-либо кодировки).

Если вы полагаетесь на ключ (прим. перев.: ключ вводимый пользователем назовем «пароль»), вводимый пользователем или заданный в конфигурации, то он нуждается в преобразовании перед использованием в качестве ключа шифрования. Используйте PBKDF2 для превращения пароля в ключ шифрования. Подробнее http://php.net/hash_pbkdf2.

Если приложение запущено под PHP версии ниже 5.5, где нет встроеной реализации PBKDF2, то придется использовать собственную реализацию на PHP, пример которой можно найти тут: https://defuse.ca/php-pbkdf2.htm. Имейте ввиду, что полагаясь на собственную реализацию, возможно не получится преобразовать ключ должным образом, как это делает встроенная функция hash_pbkdf2().

Не используйте один и тотже ключ для шифрования и аутентификации. Как сказано выше, необходимо 32 байта на ключ шифрования и 32 байта на ключ аутентификации (HMAC). С помощью PBKDF2 вы можете получить 64 байта из пароля и использовать, скажем, первые 32 байта в качестве ключа шифрования, и остальные 32 байта для ключа аутентификации.

Если у вас пароли хранятся в файле, например, в виде HEX-строки, не перекодируйте их перед тем как «скормить» функциям шифрования. Вместо этого используйте PBKDF2 для преобразования ключей из HEX-кодировки сразу в качественный ключ шифрования или аутентификации. Или используйте SHA-256 с выводом без дополнительного кодирования (просто строка в 32 байта) для хеширования паролей. Использование обычного хеширования паролей дает достаточно энтропии. Подробнее описано в следующих параграфах.

Растяжение ключа


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

Одним из параметров PBKDF2 является количество итераций хеширования. И чем оно выше, тем на большую безопасность ключа можно рассчитывать. Если ваш код работает на 64-битной платформе, используйте SHA-512 в качестве алгоритма хэширования для PBKDF2. В случае 32-битной платформы используйте SHA-256.

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

В случае, если вы можете использовать пароли с высокой энтропией, не обязательно проводить «растяжение», как для паролей с низкой энтропией. Например, если вы создаете «главный_ключ_шифрования» и «главный_ключ_аутентификации», используя /dev/urandom, то необходимость в PBKDF2 вообще отпадает. Только убедитесь, что используете ключи как последовательности бит, без какого-либо кодирования.

Кроме того, с помощью PBKDF2 несложно получить оба ключа и для шифрования, и для аутентификации от одного мастер-пароля (просто использовать небольшое количество итераций или даже одну). Это полезно, если у вас есть только один «мастер-пароль», используемый и для шифрования, и для аутентификации.

Хранение и управление ключами


Самое лучшее — это использовать отдельное специализированное устройство для хранения ключей (HSM).

Если это невозможно, то для усложнения атаки, можно использовать шифрование файла с ключами или файла конфигурации (в котором хранятся фактические ключи шифрования / аутентификации) с помощью ключа, хранящегося в отдельном месте (вне домашего каталога или корня сайта). Например, вы можете использовать переменную окружения Apache в httpd.conf, чтобы сохранить ключ, необходимый для расшифровки файла с фактическими ключами:
<VirtualHost *:80>
SetEnv keyfile_key crypto_strong_high_entropy_key
# You can access this variable in PHP using $_SERVER['keyfile_key']
# Rest of the config
</VirtualHost>

Теперь, если файлы в корне сайта и ниже, в том числе файлы с ключами, будут скомпрометированы (например, при утечке бэкапа), зашифрованные данные останутся в безопаности поскольку ключ, хранящийся в переменной окружения, не был скомпрометирован. Важно помнить, что файлы httpd.conf следует бэкапить отдельно, и не скомпрометировать переменную keyfile_key через, например, вывод phpinfo().

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

Сжатие данных


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

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

Текст сжимается более эффективно, если есть повторяющиеся участки. Манипулируя данными пользователя, можно подбирать так, чтоб они частично совпадали с секретными данными. Чем больше совпадение, тем меньший размер шифротекста будет на выходе. Такой тип атаки называется CRIME.

Если у вас нет жесткой необходимости сжимать данные, не сжимайте.

Серверное окружение


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

Есть различные причины, делающие разделяемые сервера сомнительным местом для размещения критичных к безопасности приложений. Например, недавно были продемонстрированы атаки между виртуальными серверами: eprint.iacr.org/2014/248.pdf. Это хорошее напоминание, что техники нападения не деградируют, а наоборот оттачиваются и улучшаются со временем. Всегда надо учитывать такие подводные камни.

Консультация эксперта


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

@rootlabs, 5 июня 2014:
@plo @veorq Я работаю в криптографии с 1997 года, и до сих пор все мои решения и реализации проходят ревью третьей стороной.

Криптографически стойкие случайные числа


Используйте источник случайноси предоставляемый ОС. В PHP, например, mcrypt_create_iv($count, MCRYPT_DEV_URANDOM) или напрямую читайте из /dev/urandom.

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

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


  1. SamDark
    12.05.2015 12:49
    +9

    Статья об ошибках учит плохому. mcrypt не обновлялся более десяти лет и не планирует. Авторы его забросили. В нём есть серьёзные недоработки. Мы в Yii от него ушли в сторону OpenSSL и я бы никому не советовал его использовать.


    1. maximw Автор
      12.05.2015 16:22
      +2

      Блин, да сколько ж можно, когда я уже перестану быть таким опасно некомпетентным! )
      Подскажите, пожалуйтса, где почитать разъяснения про недоработки в Mcrypt? Я просмотрел офф. сайты — не нашел подобного, кроме их багтрекера.
      Разберусь и вставлю тогда в начало статьи ссылку на это замечание.


      1. zapimir
        12.05.2015 23:28

        Да, например, то что в Mcrypt криво сделан вектор инициализации для AES-192/256 (из-за этого пишут что типа AES-256 != Rijndael-256). Вектор инициализации должен равняться длине блока, т.е. в случае AES это 128 бит/16 байт (независимо от длины ключа), а в Mcrypt Rijndael-192/256 вектор инициализации равен размеру ключа. В итоге никто кроме mcrypt такого шифротекста не понимает. В то время, как в других шифрах Mcrypt (Twofish, CAST-256, Serpent и т.п.) вектор инициализации равен размеру блока.

        Также OpenSSL банально в 2 раза быстрее, даже на процах не поддерживающих AES-NI (на процах поддерживающих новые инструкции OpenSSL уже на порядок быстрее), в то время, как Mcrypt об этих новых инструкциях ничего не знает.


        1. zapimir
          12.05.2015 23:34

          Пардон, там не только вектор инициализации, но и размер блока меняется, хотя в случае AES 192/256 размер блока и вектор инициализации должен быть равен 128 бит. В таком случае будет совместимость со всеми другими программами, хоть OpenSSL, хоть Crypto-JS…


          1. maximw Автор
            13.05.2015 10:31

            Ну да, поэтому и неравенство AES-256 != Rijndael-256 справедливо.
            AES-N — N = длина ключа.
            Rijndael-M — M = длина блока.

            AES это Rijndael-128, с вариациями длины ключа.


            1. zapimir
              13.05.2015 11:37

              Тут вопрос в том, зачем нужен такой шифр который везде реализован по другому? При том, что AES это вообще стандарт, а Rijndael можно сказать черновик этого стандарта. И самое фиговое, что в PHP и шифрующие фильтры сделаны на mcrypt.
              Кроме того в Mcrypt используются Zero padding, и приходится самому обрабатывать текст, чтобы добавлять PKCS7.
              В общем имхо лучше бы сделали Mcrypt deprecated, а вместо него прикрутили бы еще какую-то современную библиотеку шифрования на пару к OpenSSL.


              1. maximw Автор
                13.05.2015 12:43

                Тут вопрос в том, зачем нужен такой шифр который везде реализован по другому?
                Простите, не понял, что вы имеете ввиду? Какой шифр и где реализован по-другому?


      1. SamDark
        13.05.2015 03:59

        А багтрекер не торт?


        1. SamDark
          13.05.2015 04:04

          1. maximw Автор
            13.05.2015 10:33

            Да, именно это я и смотрел. Но чтоб там разобраться надо больше знаний.


      1. SamDark
        13.05.2015 04:05

        И ещё можно на тему Тома почитать: thefsb.tumblr.com


        1. maximw Автор
          13.05.2015 10:39

          Я вот и думал вы наведете на какую-нибудь статью, в которой в более доступной форме рассказано про недостатки Mcrypt. Спасибо, изучу внимательно.

          Кстати, после беглого чтения статьи, я так понял что OpenSSL не поддерживает режим AES CTR.
          Когда переводил статью для поста дополнительно гуглил тему «CBC vs CTR», и много где рекомендуют именно CTR.


          1. SamDark
            13.05.2015 13:37
            +1

            Начиная с OpenSSL v1.0.1 поддерживается AES-CTR-256. Проверить, какой у вас сейчас, можно при помощи php -r "echo OPENSSL_VERSION_TEXT;.


            1. maximw Автор
              13.05.2015 14:58

              Добавил апдейт по вашим комментариям.
              Перевод править, пожалуй, нехорошо, все же перевод, а не моя статья.