безопасность — два подхода к PHP с двусторонним шифрованием — какой из них предпочтительнее?

Мне нужно зашифровать некоторые данные и расшифровать их на более поздний момент времени. Данные привязаны к конкретным пользователям. Я собрал два возможных решения …

1: Первый получен из официальных документов (пример № 1 @ http://php.net/manual/en/function.mcrypt-encrypt.php):

function encrypt($toEncrypt)
{
global $key;
$iv_size = mcrypt_get_iv_size(MCRYPT_RIJNDAEL_256, MCRYPT_MODE_CBC);
$iv = mcrypt_create_iv($iv_size, MCRYPT_RAND);
return base64_encode($iv . mcrypt_encrypt(MCRYPT_RIJNDAEL_256, $key, $toEncrypt, MCRYPT_MODE_CBC, $iv));
}

function decrypt($toDecrypt)
{
global $key;
$iv_size = mcrypt_get_iv_size(MCRYPT_RIJNDAEL_256, MCRYPT_MODE_CBC);
$toDecrypt = base64_decode($toDecrypt);
return rtrim(mcrypt_decrypt(MCRYPT_RIJNDAEL_256, $key, substr($toDecrypt, $iv_size), MCRYPT_MODE_CBC, substr($toDecrypt, 0, $iv_size)));
}

Ключ генерируется один раз с использованием:

echo bin2hex(openssl_random_pseudo_bytes(mcrypt_get_iv_size(MCRYPT_RIJNDAEL_256, MCRYPT_MODE_CBC)))

А потом позже упоминается как это:

$key = pack('H*', [result of above]);

1,1Я заметил, что зашифрованный результат всегда заканчивается двумя знаками равенства (‘==’). Зачем? — Использование bin2hex () и hex2bin () в encrypt () и decrypt () вместо base64_encode () / base64_decode () соответственно не дает этих результатов.

1.2: Будет ли использование bin2hex () / hex2bin () иметь какие-либо последствия для результата (кроме длины)?

1,3Похоже, что существует некоторое обсуждение, вызывать или нет функцию триммера для возвращаемого результата при расшифровке (это также относится и к решению, приведенному ниже). Зачем это нужно?


2: Второе решение отсюда, Stackoverflow (Простейшее двустороннее шифрование с использованием PHP):

function encrypt($key, $toEncrypt)
{
return base64_encode(mcrypt_encrypt(MCRYPT_RIJNDAEL_256, md5($key), $toEncrypt, MCRYPT_MODE_CBC, md5(md5($key))));
}

function decrypt($key, $toDecrypt)
{
return rtrim(mcrypt_decrypt(MCRYPT_RIJNDAEL_256, md5($key), base64_decode($toDecrypt), MCRYPT_MODE_CBC, md5(md5($key))), "\0");
}

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

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

3Итак, какой из них предпочтительнее? Я немного в неведении, так как ответ на Stackoverflow набрал колоссальные 105 голосов. Другие мысли, советы?

4: Бонусный вопрос !: Я не слишком умный в вопросах безопасности сервера, но, очевидно, получение доступа к файлам PHP может раскрыть ключ, что в результате приведет к тому, что шифрование станет бесполезным, если злоумышленник также получит доступ к БД , Есть ли способ скрыть ключ?

Спасибо за чтение и хорошего дня!

РЕДАКТИРОВАТЬ: Учитывая все обстоятельства, это, кажется, мой лучший выбор:

function encrypt($toEncrypt)
{
global $key;
$iv_size = mcrypt_get_iv_size(MCRYPT_RIJNDAEL_128, MCRYPT_MODE_CBC);
$iv = mcrypt_create_iv(mcrypt_get_iv_size(MCRYPT_RIJNDAEL_128, MCRYPT_MODE_CBC), MCRYPT_RAND);
return base64_encode($iv . mcrypt_encrypt(MCRYPT_RIJNDAEL_128, $key, $toEncrypt, MCRYPT_MODE_CBC, $iv));
}

function decrypt($toDecrypt)
{
global $key;
$iv_size = mcrypt_get_iv_size(MCRYPT_RIJNDAEL_128, MCRYPT_MODE_CBC);
$toDecrypt = base64_decode($toDecrypt);
return rtrim(mcrypt_decrypt(MCRYPT_RIJNDAEL_128, $key, substr($toDecrypt, $iv_size), MCRYPT_MODE_CBC, substr($toDecrypt, 0, $iv_size)));
}

Используя ключ, созданный один раз, используя следующее:

bin2hex(openssl_random_pseudo_bytes(32)));

2

Решение

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

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


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

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

$cipher = MCRYPT_RIJNDAEL_128;  // = AES-256
$mode   = MCRYPT_MODE_CBC;
$keylen = mcrypt_get_key_size( $cipher, $mode );

$salt   = mcrypt_create_iv( $keylen, MCRYPT_DEV_URANDOM );
$iterations = 10000;  // higher = slower; make this as high as you can tolerate

$key = hash_pbkdf2( 'sha256', $password, $salt, $iterations, $keylen, true );

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

(Кстати, это также довольно хороший способ хеширования пароля для проверки его правильности. Очевидно, что вы не должны использовать тот же $key значение для шифрования и проверки пароля, но вы можете безопасно хранить, скажем, hash( 'sha256', $key, true ) рядом с зашифрованным текстом, чтобы вы могли проверить правильность пароля / ключа.)


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

  • Оба фрагмента используют MCRYPT_RIJNDAEL_256что, по-видимому, не AES-256, скорее это нестандартный вариант Rijndael-256/256 с 256-битным размером блока (и размером ключа). Это наверное безопасный, но варианты Rijndael с 256-битным размером блока получили гораздо меньше криптоаналитического контроля, чем 128-битные размеры блока (которые были стандартизированы как AES), так что вы берете на себя немного более высокий риск при их использовании.

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

  • В вашем коде генерации ключей, pack( 'H*', bin2hex( ... ) ) это неоперация: bin2hex() преобразует ключ из двоичного в шестнадцатеричное, и pack( 'H*', ... ) затем делает обратное. Просто избавьтесь от обеих функций.

    Кроме того, вы генерируете ключ, а не IV, поэтому вы должны использовать mcrypt_get_key_size()не mcrypt_get_iv_size(), Как это бывает, для MCRYPT_RIJNDAEL_256 нет никакой разницы (поскольку размер IV и размер ключа составляют 32 байта = 256 бит), но для MCRYPT_RIJNDAEL_128 (и многие другие шифры) есть.

  • Как отмечает Олстед, реализация режима CBC в mcrypt, очевидно, использует нестандартную схему заполнения нулями. Ваш второй пример кода правильно удаляет заполнение с rtrim( $msg, "\0" ); первый просто звонит rtrim( $msg ), который также будет обрезать любые пробелы в конце сообщения.

    Также очевидно, что эта схема заполнения нулями не будет работать должным образом, если ваши данные могут законно содержать нулевые байты в конце. Вместо этого вы можете переключиться в другой режим шифрования, например MCRYPT_MODE_CFB или же MCRYPT_MODE_OFB, которые не требуют каких-либо дополнений. (Из этих двух я обычно рекомендовал бы CFB, так как случайное повторное использование IV очень плохо для OFB. Это не хорошо для CFB или CBC, но их режим неудачи намного менее катастрофичен.)

2

Другие решения

Q1: выберите это!

Раскрытие: я (пере) написал mcrypt_encrypt пример кода. Поэтому я выбираю 1.

Лично я бы не рекомендовал использовать MCRYPT_RIJNDAEL_256, Вы используете AES-256, используя ключ с размером ключа 32 байта (256 бит) для MCRYPT_RIJNDAEL_128 алгоритм, а не путем выбора Rijndael с размером блока 256. Я явно переписал образец для удаления MCRYPT_RIJNDAEL_256 — среди других ошибок — и укажите в комментариях, почему вы должны использовать MCRYPT_RIJNDAEL_128 вместо.

Q 1.1: Байт заполнения для base64

= является символом заполнения для кодировки base 64. Base64 кодирует 3 байта в 4 символа. Чтобы количество символов было точным кратным 4, они используют эти байты заполнения, если это необходимо.

В1.2: Будет ли использование bin2hex () / hex2bin () иметь какие-либо последствия для результата (кроме длины)?

Нет, поскольку и hex, и base64 являются детерминированными и полностью обратимыми.

Q1.3: на трим

То же самое касается rtrim, Это необходимо, так как PHP mcrypt использует нестандартное заполнение нулями вплоть до размера блока (он заполняет открытый текст 00 оцененные байты справа). Это хорошо для ASCII & Струны UTF-8, где 00 байт находится вне диапазона печатаемых символов, но вы можете посмотреть дальше, если хотите зашифровать двоичные данные. Есть примеры заполнения PKCS # 7 в разделе комментариев mcrypt_encrypt, Незначительное примечание: rtrim может работать только для некоторых языков, таких как PHP, другие реализации могут оставить после себя 00 символы как 00 не считается пробелом.

Q2: дисквалификация

Другой ответ SO использует MD5 для получения пароля и MD5 для пароля для расчета IV. Это полностью дисквалифицирует это как хороший ответ. Если у вас есть пароль вместо ключа, пожалуйста, проверьте это Q / A.

И он также не использует AES, выбирая для MCRYPT_RIJNDAEL_256,

Q3: по голосам

Пока SO сообщество продолжает голосовать за ответы, которые похоже, работает для определенного языка / конфигурации вместо того, чтобы голосовать за ответы, которые криптографически безопасны, вы найдете абсолютная ловушка как ответ в Q2. К сожалению, большинство людей, которые приходят сюда, не криптографы; другой ответ будет абсолютно пораженный на сайте crypto.stackexchange.com.

Обратите внимание, что только вчера я должен был объяснить кому-то на SO, почему это не возможно расшифровать MCRYPT_RIJNDAEL_256 используя CCCrypt на iOS, потому что доступен только AES.

Q4: запутывание

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

Либо вам нужно использовать открытый ключ (например, RSA) и гибридную криптографию, либо вам нужно хранить ключ где-нибудь в безопасности, например, в HSM или смарт-карте. Управление ключами является сложной частью криптографии, возможно, самый сложный часть.

2

Прежде всего, я прошу прощения за длину этого ответа.

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

Описание:

Этот класс сначала возьмет предоставленный ключ шифрования и запустит его через реализацию PBKDF2, используя алгоритм SHA-512 на 1000 итераций.

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

Новый IV генерируется с использованием dev / urandom перед каждой операцией шифрования. Если скрипт работает на компьютере с Windows, а версия PHP меньше 5.3, класс будет использовать MCRYPT_RAND для генерации IV.

В зависимости от того, имеет ли параметр $ raw_output значение true или false, метод шифрования будет возвращать шестнадцатеричный регистр по умолчанию в нижнем регистре или двоичный файл зашифрованных данных.

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

Этот класс использует Rijndael 128 в режиме CBC.

Этот класс будет работать кроссплатформенно и был протестирован на PHP 5.2, 5.3, 5.4, 5.5 и 5.6


Файл: AesEncryption.php

<?php

/**
* This file contains the class AesEncryption
*
* AesEncryption can safely encrypt and decrypt plain or binary data and
* uses verification to ensure decryption was successful.
*
* PHP version 5
*
* LICENSE: This source file is subject to version 2.0 of the Apache license
* that is available through the world-wide-web at the following URI:
* https://www.apache.org/licenses/LICENSE-2.0.html.
*
* @author     Michael Bush <michael(.)bush(@)hotmail(.)co(.)uk>
* @license    https://www.apache.org/licenses/LICENSE-2.0.html Apache 2.0
* @copyright  2015 Michael Bush
* @version    1.0.0
*/

/**
* @version    1.0.0
*/
final class AesEncryption
{
/**
* @var string
*/
private $key;

/**
* @var string
*/
private $iv;

/**
* @var resource
*/
private $mcrypt;

/**
* Construct the call optionally providing an encryption key
*
* @param string $key
* @return Encryption
* @throws RuntimeException if the PHP installation is missing critical requirements
*/
public function __construct($key = null) {
if (!extension_loaded ('mcrypt')) {
throw new RuntimeException('MCrypt library is not availble');
}
if (!extension_loaded ('hash')) {
throw new RuntimeException('Hash library is not availble');
}
if (!in_array('rijndael-128', mcrypt_list_algorithms(), true)) {
throw new RuntimeException('MCrypt library does not contain an implementation of rijndael-128');
}
if (!in_array('cbc', mcrypt_list_modes(), true)) {
throw new RuntimeException('MCrypt library does not support CBC encryption mode');
}
$this->mcrypt = mcrypt_module_open('rijndael-128', '', 'cbc', '');
if(isset($key)) {
$this->SetKey($key);
}
}

/**
* @return void
*/
public function __destruct() {
if (extension_loaded ('mcrypt')) {
if (isset($this->mcrypt)) {
mcrypt_module_close($this->mcrypt);
}
}
}

/**
* Set the key to be used for encryption and decryption operations.
*
* @param string $key
* @return void
*/
public function SetKey($key){
$this->key = $this->pbkdf2('sha512', $key, hash('sha512', $key, true), 1000, mcrypt_enc_get_key_size($this->mcrypt), true);
}

/**
* Encrypts data
*
* @param string $data
* @param bool $raw_output if false this method will return lowercase hexit, if true this method will return raw binary
* @return string
*/
public function Encrypt($data, $raw_output = false) {
$data = gzcompress($data, 9);
$hash = md5($data, true);
$datalen = strlen($data);
$datalen = pack('N', $datalen);
$data = $datalen . $hash . $data;
if (version_compare(PHP_VERSION, '5.3.0', '<=')) {
if (strtolower (substr (PHP_OS, 0, 3)) == 'win') {
$this->iv = mcrypt_create_iv(mcrypt_enc_get_iv_size($this->mcrypt), MCRYPT_RAND);
} else {
$this->iv = mcrypt_create_iv(mcrypt_enc_get_iv_size($this->mcrypt), MCRYPT_DEV_URANDOM);
}
} else {
$this->iv = mcrypt_create_iv(mcrypt_enc_get_iv_size($this->mcrypt), MCRYPT_DEV_URANDOM);
}
$this->initialize();
$data = mcrypt_generic($this->mcrypt, $data);
$this->deinitialize();
$data = $this->iv . $data;
$this->iv = null;
if ($raw_output) {
return $data;
}
$data = unpack('H*',$data);
$data = end($data);
return $data;
}

/**
* Decrypts data
*
* @param string $data
* @return string This method will return false if an error occurs
*/
public function Decrypt($data) {
if (ctype_xdigit($data)) {
$data = pack ('H*',$data);
}
$this->iv = substr ($data, 0, mcrypt_enc_get_iv_size($this->mcrypt));
$data = substr ($data, mcrypt_enc_get_iv_size($this->mcrypt));
$this->initialize();
$data = mdecrypt_generic($this->mcrypt, $data);
$this->deinitialize();
$datalen = substr($data, 0, 4);
$len = unpack('N', $datalen);
$len = end($len);
$hash = substr($data, 4, 16);
$data = substr($data, 20, $len);
$datahash = md5($data, true);
if ($this->compare($hash,$datahash)) {
$data = @gzuncompress($data);
return $data;
}
return false;
}

/**
* Initializes the mcrypt module
*
* @return void
*/
private function initialize() {
mcrypt_generic_init($this->mcrypt, $this->key, $this->iv);
}

/**
* Deinitializes the mcrypt module and releases memory.
*
* @return void
*/
private function deinitialize() {
mcrypt_generic_deinit($this->mcrypt);
}

/**
* Implementation of a timing-attack safe string comparison algorithm, it will use hash_equals if it is available
*
* @param string $safe
* @param string $supplied
* @return bool
*/
private function compare($safe, $supplied) {
if (function_exists('hash_equals')) {
return hash_equals($safe, $supplied);
}
$safe .= chr(0x00);
$supplied .= chr(0x00);
$safeLen = strlen($safe);
$suppliedLen = strlen($supplied);
$result = $safeLen - $suppliedLen;
for ($i = 0; $i < $suppliedLen; $i++) {
$result |= (ord($safe[$i % $safeLen]) ^ ord($supplied[$i]));
}
return $result === 0;
}

/**
* Implementation of the keyed-hash message authentication code algorithm, it will use hash_hmac if it is available
*
* @param string $algo
* @param string $data
* @param string $key
* @param bool $raw_output
* @return string
*
* @bug method returning wrong result for joaat algorithm
* @id 101275
* @affects PHP installations without the hash_hmac function but they do have the joaat algorithm
* @action wont fix
*/
private function hmac($algo, $data, $key, $raw_output = false) {
$algo = strtolower ($algo);
if (function_exists('hash_hmac')) {
return hash_hmac($algo, $data, $key, $raw_output);
}
switch ( $algo ) {
case 'joaat':
case 'crc32':
case 'crc32b':
case 'adler32':
case 'fnv132':
case 'fnv164':
case 'fnv1a32':
case 'fnv1a64':
$block_size = 4;
break;
case 'md2':
$block_size = 16;
break;
case 'gost':
case 'gost-crypto':
case 'snefru':
case 'snefru256':
$block_size = 32;
break;
case 'sha384':
case 'sha512':
case 'haval256,5':
case 'haval224,5':
case 'haval192,5':
case 'haval160,5':
case 'haval128,5':
case 'haval256,4':
case 'haval224,4':
case 'haval192,4':
case 'haval160,4':
case 'haval128,4':
case 'haval256,3':
case 'haval224,3':
case 'haval192,3':
case 'haval160,3':
case 'haval128,3':
$block_size = 128;
break;
default:
$block_size = 64;
break;
}
if (strlen($key) > $block_size) {
$key=hash($algo, $key, true);
} else {
$key=str_pad($key, $block_size, chr(0x00));
}
$ipad=str_repeat(chr(0x36), $block_size);
$opad=str_repeat(chr(0x5c), $block_size);
$hmac = hash($algo, ($key^$opad) . hash($algo, ($key^$ipad) . $data, true), $raw_output);
return $hmac;
}

/**
* Implementation of the pbkdf2 algorithm, it will use hash_pbkdf2 if it is available
*
* @param string $algorithm
* @param string $password
* @param string $salt
* @param int $count
* @param int $key_length
* @param bool $raw_output
* @return string
* @throws RuntimeException if the algorithm is not found
*/
private function pbkdf2($algorithm, $password, $salt, $count = 1000, $key_length = 0, $raw_output = false) {
$algorithm = strtolower ($algorithm);
if (!in_array($algorithm, hash_algos(), true)) {
throw new RuntimeException('Hash library does not contain an implementation of ' . $algorithm);
}
if (function_exists('hash_pbkdf2')) {
return hash_pbkdf2($algorithm, $password, $salt, $count, $key_length, $raw_output);
}
$hash_length = strlen(hash($algorithm, '', true));
if ($count <= 0) {
$count = 1000;
}
if($key_length <= 0) {
$key_length = $hash_length * 2;
}
$block_count = ceil($key_length / $hash_length);
$output = '';
for($i = 1; $i <= $block_count; $i++) {
$last = $salt . pack('N', $i);
$last = $xorsum = $this->hmac($algorithm, $last, $password, true);
for ($j = 1; $j < $count; $j++) {
$xorsum ^= ($last = $this->hmac($algorithm, $last, $password, true));
}
$output .= $xorsum;
}
if ($raw_output) {
return substr($output, 0, $key_length);
}
$output = unpack('H*',$output);
$output = end ($output);
return substr($output, 0, $key_length);
}
}

Пример использования:

<?php

include 'AesEncryption.php';

$key = 'my secret key';
$string = 'hello world';

try
{
$aes = new AesEncryption($key); // exception can be thrown here if the class is not supported

$data = $aes->Encrypt($string, true); // expecting return of a raw byte string
$decr = $aes->Decrypt($data); // expecting the return of "hello world"var_dump ($decr);

// encrypt something else with a different key
$aes->SetKey('my other secret key'); // exception can be thrown here if the class is not supported

$data2 = $aes->Encrypt($string); // return the return of a lowercase hexit string
$decr = $aes->Decrypt($data2); // expecting the return of "hello world"var_dump ($decr);

// proof that the key was changed
$decr = $aes->Decrypt($data); // expecting return of Boolean False
var_dump ($decr);

// reset the key back
$aes->SetKey($key); // exception can be thrown here if the class is not supported
$decr = $aes->Decrypt($data); // expecting hello world
var_dump ($decr);
}

catch (Exception $e)
{
print 'Error running AesEncryption class; reason: ' . $e->getMessage ();
}
2

1.1 Это просто набивка. Это происходит с большинством входных данных для base64, но не со всеми.

1.2 Без разницы. Держите с base64, это стандартно с шифрованием.

1.3 Я не вижу причин, почему это было бы необходимо. Люди иногда решают проблемы в неправильных местах. Вместо того, чтобы исправить ввод, они изменяют вывод. Где это обсуждение?

  1. Определенно не используйте это. Вы меняете свой ключ некоторой длины на 128-битный MD5. И это не безопасно.

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

0
По вопросам рекламы [email protected]