От переводчика: в процессе программирования никогда не забываю о том, что я опасно некомпетентен в криптографии, и всем советую исходить из этого тезиса (ну, может быть кроме вас и еще вон того крутого парня). Однако, так или иначе, в процессе работы возникают задачи, связанные с защитой данных, и их надо решать. Поэтому я предлагаю вашему вниманию перевод статьи финского разрабочика Timo H, которая показалась мне достаточно интересной и полезной.
Важный update: в комментариях SamDark сделал замечание, что библиотека Mcrypt давно не поддерживается и имеет ряд недоработок, поэтому рекомендуется использовать OpenSSL. Если требуется переписывать имеющийся код, то может помочь эта статья. Кроме того, есть сведения, что Mcrypt может быть удален в PHP7.
Это краткое руководство о том, как избежать распространенных ошибок с симметричным шифрованием на PHP.
Будем рассматривать случай, когда данные обрабатываются на стороне сервера (в частности, шифрование происходит на сервере, а данные могут быть получены, например, от клиента в виде открытого текста, пароля и т.п.), что является типичным случаем для PHP-приложений.
Cведения из этого руководства не стоит использовать для создания шифрованных сетевых соединений, которые имеют более сложные требования. Для таких случаев надо использовать spiped или TLS.
Естественно, рекомендации, приведенные здесь, не являются «единственно возможным способом» организации шифрования на 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:
Пример использования 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, чтобы сохранить ключ, необходимый для расшифровки файла с фактическими ключами:
Теперь, если файлы в корне сайта и ниже, в том числе файлы с ключами, будут скомпрометированы (например, при утечке бэкапа), зашифрованные данные останутся в безопаности поскольку ключ, хранящийся в переменной окружения, не был скомпрометирован. Важно помнить, что файлы httpd.conf следует бэкапить отдельно, и не скомпрометировать переменную keyfile_key через, например, вывод phpinfo().
Если вместо параметра конфигурации вы используете файл, то возможно организовать ротацию ключей. В худшем случае, если противник заполучил ваши ключи шифрования и аутентификации, и этот факт остался незамеченным, то ротация ключей с какой-то периодичностью может ограничить его доступ (при условии, что он не может заполучить новые ключи). Такой прием поможет уменьшить ущерб, потому что противник не сможет использовать скомпрометированные ключи бесконечно.
В общем случае не стоит сжимать исходный текст до шифрования. Это может дать противнику дополнительный инструмент для анализа.
Например, если вы храните данные сессии в зашифрованных cookie, при этом некоторые из этих данных предоставлены пользователем, а некоторые представляют секретную информацию, противник может узнать дополнительную информацию о секрете, посылая как рядовой пользователь определенным образом сформированные данные и измеряя как меняется длина получаемых шифротекстов.
Текст сжимается более эффективно, если есть повторяющиеся участки. Манипулируя данными пользователя, можно подбирать так, чтоб они частично совпадали с секретными данными. Чем больше совпадение, тем меньший размер шифротекста будет на выходе. Такой тип атаки называется CRIME.
Если у вас нет жесткой необходимости сжимать данные, не сжимайте.
Как правило, не стоит размещать требовательные к безопасности приложения на разделяемом сервере. Например, на виртуальном хостинге, где противник может получить доступ к виртуалке на том же физическом сервере, что и вы.
Есть различные причины, делающие разделяемые сервера сомнительным местом для размещения критичных к безопасности приложений. Например, недавно были продемонстрированы атаки между виртуальными серверами: eprint.iacr.org/2014/248.pdf. Это хорошее напоминание, что техники нападения не деградируют, а наоборот оттачиваются и улучшаются со временем. Всегда надо учитывать такие подводные камни.
И последнее, но не по важности, проконсультируйтесь с экспертом, пусть он сделает ревью вашего кода, отвечающего за безопасность.
@rootlabs, 5 июня 2014:
Используйте источник случайноси предоставляемый ОС. В PHP, например, mcrypt_create_iv($count, MCRYPT_DEV_URANDOM) или напрямую читайте из /dev/urandom.
Всегда проверяйте, что из источника получено нужное количество байт. Если это не так, не пытайтесь исправить такую ошибку самостоятельно при помощи самодельных алгортимов псевдослучайности.
Важный 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.
Всегда проверяйте, что из источника получено нужное количество байт. Если это не так, не пытайтесь исправить такую ошибку самостоятельно при помощи самодельных алгортимов псевдослучайности.