Простейшее двустороннее шифрование с использованием PHP


230

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

Мне нужно иметь возможность шифровать данные строковым ключом и использовать тот же ключ для расшифровки на другом конце.

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



3
Для шифрования общего назначения используйте defuse / php-encryption / вместо собственного.
— Скотт Аркишевский

2
Отойдите от github.com/defuse/php-encryption - он на несколько порядков медленнее, чем mcrypt.
— Евгений Рик

1
@ Scott Думая о том, что «это, вероятно, не будет узким местом», мы принесли много плохого программного обеспечения
— Евгений Рик,

3
Если вы действительно зашифровываете / дешифруете много данных до такой степени, что миллисекунды, которые они стоят, сводят ваше приложение на нет, откусите пулю и переключитесь на libsodium. Sodium::crypto_secretbox()и Sodium::crypto_secretbox_open()безопасны и производительны.
— Скотт Аркишевский

Ответы:


196

Отредактировано:

Вы действительно должны использовать openssl_encrypt () & openssl_decrypt ()

Как говорит Скотт , Mcrypt не очень хорошая идея, поскольку она не обновлялась с 2007 года.

Существует даже RFC для удаления Mcrypt из PHP - https://wiki.php.net/rfc/mcrypt-viking-funeral


6
@EugenRieck Да, в этом все дело. Макрипт не получает патчи. OpenSSL получает исправления, как только обнаруживается какая-либо уязвимость, большая или маленькая.
— Грег

5
было бы лучше, если бы такой голос с высоким рейтингом был предоставлен в ответ на самые простые примеры. Спасибо, в любом случае.
— Т.Тодуа

ребята, только к вашему сведению => MCRYPT УСТАРЕЛО. так что все должны знать, чтобы не использовать его, поскольку это дало нам множество проблем. Это устарело с PHP 7.1, если я не ошибаюсь.
— clusterBuddy

Начиная с PHP 7, функция mcrypt удаляется из кодовой базы php. Поэтому при использовании последней версии php (которая должна быть стандартной) вы больше не сможете использовать эту устаревшую функцию.
— Александр Белинг

234

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

Быть информированным. Проектирование безопасных систем.

Портативное шифрование данных в PHP

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

Если ваши цели портативности не помешают требующему расширение PECL, libsodium является высоко рекомендуется над чем вы или я могу написать в PHP.

Обновление (2016-06-12): Теперь вы можете использовать sodium_compat и использовать одни и те же криптографические предложения libsodium без установки расширений PECL.

Если вы хотите попробовать себя в криптографии, читайте дальше.


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

  • Зашифрованные данные все еще могут быть подделаны злоумышленником.
  • Аутентификация зашифрованных данных предотвращает фальсификацию.
  • Аутентификация незашифрованных данных не предотвращает взлома.

Шифрование и дешифрование

Шифрование в PHP на самом деле простое (мы собираемся его использовать, openssl_encrypt()и как openssl_decrypt()только вы примете некоторые решения о том, как зашифровать вашу информацию. Обратитесь openssl_get_cipher_methods()к списку методов, поддерживаемых в вашей системе. Лучший выбор - AES в режиме CTR :

  • aes-128-ctr
  • aes-192-ctr
  • aes-256-ctr

В настоящее время нет оснований полагать, что размер ключа AES является серьезной проблемой, о которой нужно беспокоиться (чем больше, тем лучше - не из-за плохого планирования ключей в 256-битном режиме).

Примечание: мы не используем mcryptего, потому что он заброшен и содержит исправленные ошибки, которые могут повлиять на безопасность. По этим причинам я призываю других разработчиков PHP также избегать этого.

Simple Encryption / Decryption Wrapper с использованием OpenSSL

class UnsafeCrypto
{
    const METHOD = 'aes-256-ctr';

    /**
     * Encrypts (but does not authenticate) a message
     * 
     * @param string $message - plaintext message
     * @param string $key - encryption key (raw binary expected)
     * @param boolean $encode - set to TRUE to return a base64-encoded 
     * @return string (raw binary)
     */
    public static function encrypt($message, $key, $encode = false)
    {
        $nonceSize = openssl_cipher_iv_length(self::METHOD);
        $nonce = openssl_random_pseudo_bytes($nonceSize);

        $ciphertext = openssl_encrypt(
            $message,
            self::METHOD,
            $key,
            OPENSSL_RAW_DATA,
            $nonce
        );

        // Now let's pack the IV and the ciphertext together
        // Naively, we can just concatenate
        if ($encode) {
            return base64_encode($nonce.$ciphertext);
        }
        return $nonce.$ciphertext;
    }

    /**
     * Decrypts (but does not verify) a message
     * 
     * @param string $message - ciphertext message
     * @param string $key - encryption key (raw binary expected)
     * @param boolean $encoded - are we expecting an encoded string?
     * @return string
     */
    public static function decrypt($message, $key, $encoded = false)
    {
        if ($encoded) {
            $message = base64_decode($message, true);
            if ($message === false) {
                throw new Exception('Encryption failure');
            }
        }

        $nonceSize = openssl_cipher_iv_length(self::METHOD);
        $nonce = mb_substr($message, 0, $nonceSize, '8bit');
        $ciphertext = mb_substr($message, $nonceSize, null, '8bit');

        $plaintext = openssl_decrypt(
            $ciphertext,
            self::METHOD,
            $key,
            OPENSSL_RAW_DATA,
            $nonce
        );

        return $plaintext;
    }
}

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

$message = 'Ready your ammunition; we attack at dawn.';
$key = hex2bin('000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f');

$encrypted = UnsafeCrypto::encrypt($message, $key);
$decrypted = UnsafeCrypto::decrypt($encrypted, $key);

var_dump($encrypted, $decrypted);

Демо : https://3v4l.org/jl7qR


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

Примечание . По умолчанию UnsafeCrypto::encrypt()возвращается необработанная двоичная строка. Назовите это так, если вам нужно сохранить его в бинарно-безопасном формате (в кодировке base64):

$message = 'Ready your ammunition; we attack at dawn.';
$key = hex2bin('000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f');

$encrypted = UnsafeCrypto::encrypt($message, $key, true);
$decrypted = UnsafeCrypto::decrypt($encrypted, $key, true);

var_dump($encrypted, $decrypted);

Демо : http://3v4l.org/f5K93

Оболочка простой аутентификации

class SaferCrypto extends UnsafeCrypto
{
    const HASH_ALGO = 'sha256';

    /**
     * Encrypts then MACs a message
     * 
     * @param string $message - plaintext message
     * @param string $key - encryption key (raw binary expected)
     * @param boolean $encode - set to TRUE to return a base64-encoded string
     * @return string (raw binary)
     */
    public static function encrypt($message, $key, $encode = false)
    {
        list($encKey, $authKey) = self::splitKeys($key);

        // Pass to UnsafeCrypto::encrypt
        $ciphertext = parent::encrypt($message, $encKey);

        // Calculate a MAC of the IV and ciphertext
        $mac = hash_hmac(self::HASH_ALGO, $ciphertext, $authKey, true);

        if ($encode) {
            return base64_encode($mac.$ciphertext);
        }
        // Prepend MAC to the ciphertext and return to caller
        return $mac.$ciphertext;
    }

    /**
     * Decrypts a message (after verifying integrity)
     * 
     * @param string $message - ciphertext message
     * @param string $key - encryption key (raw binary expected)
     * @param boolean $encoded - are we expecting an encoded string?
     * @return string (raw binary)
     */
    public static function decrypt($message, $key, $encoded = false)
    {
        list($encKey, $authKey) = self::splitKeys($key);
        if ($encoded) {
            $message = base64_decode($message, true);
            if ($message === false) {
                throw new Exception('Encryption failure');
            }
        }

        // Hash Size -- in case HASH_ALGO is changed
        $hs = mb_strlen(hash(self::HASH_ALGO, '', true), '8bit');
        $mac = mb_substr($message, 0, $hs, '8bit');

        $ciphertext = mb_substr($message, $hs, null, '8bit');

        $calculated = hash_hmac(
            self::HASH_ALGO,
            $ciphertext,
            $authKey,
            true
        );

        if (!self::hashEquals($mac, $calculated)) {
            throw new Exception('Encryption failure');
        }

        // Pass to UnsafeCrypto::decrypt
        $plaintext = parent::decrypt($ciphertext, $encKey);

        return $plaintext;
    }

    /**
     * Splits a key into two separate keys; one for encryption
     * and the other for authenticaiton
     * 
     * @param string $masterKey (raw binary)
     * @return array (two raw binary strings)
     */
    protected static function splitKeys($masterKey)
    {
        // You really want to implement HKDF here instead!
        return [
            hash_hmac(self::HASH_ALGO, 'ENCRYPTION', $masterKey, true),
            hash_hmac(self::HASH_ALGO, 'AUTHENTICATION', $masterKey, true)
        ];
    }

    /**
     * Compare two strings without leaking timing information
     * 
     * @param string $a
     * @param string $b
     * @ref https://paragonie.com/b/WS1DLx6BnpsdaVQW
     * @return boolean
     */
    protected static function hashEquals($a, $b)
    {
        if (function_exists('hash_equals')) {
            return hash_equals($a, $b);
        }
        $nonce = openssl_random_pseudo_bytes(32);
        return hash_hmac(self::HASH_ALGO, $a, $nonce) === hash_hmac(self::HASH_ALGO, $b, $nonce);
    }
}

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

$message = 'Ready your ammunition; we attack at dawn.';
$key = hex2bin('000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f');

$encrypted = SaferCrypto::encrypt($message, $key);
$decrypted = SaferCrypto::decrypt($encrypted, $key);

var_dump($encrypted, $decrypted);

Демонстрации : сырой двоичный файл , закодированный в base64


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

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


3
Итак, я просто пытаюсь заставить UnsafeCrypto работать первым. Шифрование происходит нормально, но каждый раз, когда я запускаю дешифрование, я получаю «ложь» в качестве ответа. Я использую тот же ключ для дешифрования, и передаю истину при кодировании, а также при декодировании. В этом примере я предполагаю, что это типо, и мне интересно, откуда это моя проблема? Можете ли вы объяснить, откуда берется переменная $ mac, и должна ли она быть просто $ iv?
— Дэвид К

1
@EugenRieck Реализации OpenSSL, вероятно, являются единственными частями, которые не работают, и это единственный способ использовать AES-NI в ванильном PHP. Если вы устанавливаете на OpenBSD, PHP будет скомпилирован с LibreSSL без разницы кода PHP. Libsodium> OpenSSL в любой день. Кроме того, не используйте libmcrypt . Что бы вы посоветовали использовать PHP-разработчикам вместо OpenSSL?
— Скотт Аркишевский

2
Ни 5.2, ни 5.3 больше не поддерживаются . Вместо этого вам следует изучить обновление до поддерживаемой версии PHP , такой как 5.6.
— Скотт Аркишевский


1
Я просто сделал это для демонстрации того, что вы хотите, чтобы ваши ключи представляли собой двоичные строки, а не строки, читаемые человеком .
— Скотт

22

Используйте mcrypt_encrypt()и mcrypt_decrypt()с соответствующими параметрами. Действительно легко и просто, и вы используете проверенный в бою пакет шифрования.

РЕДАКТИРОВАТЬ

Через 5 лет и 4 месяца после этого ответа mcryptрасширение находится в процессе устаревания и возможного удаления из PHP.


34
Битва проверена и не обновляется уже более 8 лет?
— Мартен Бодьюз

2
Ну, mcrypt в PHP7 и не устарел - это достаточно хорошо для меня. Не весь код имеет ужасное качество OpenSSL и нуждается в исправлении каждые несколько дней.
— Евгений Рик

3
mcrypt не просто ужасен в отношении поддержки. Он также не реализует лучшие практики, такие как заполнение в соответствии с PKCS # 7, аутентифицированное шифрование. Он не будет поддерживать SHA-3 или любой другой новый алгоритм, так как никто не поддерживает его, лишая вас пути обновления. Кроме того, он принимал такие вещи, как частичные ключи, выполнение заполнения нулями и т. Д. Есть веская причина, по которой он постепенно удаляется из PHP.
— Мартен Бодьюз

2
В PHP 7.1 все функции mcrypt_ * будут вызывать уведомление E_DEPRECATED. В PHP 7.1 + 1 (будь то 7.2 или 8.0) расширение mcrypt будет перемещено из ядра в PECL, где люди, которые действительно хотят его установить, могут сделать это, если смогут установить расширения PHP из PECL.
— Младен Янджетович

4

PHP 7.2 полностью отошел Mcryptи шифрование теперь основано на поддерживаемой Libsodiumбиблиотеке.

Все ваши потребности в шифровании могут быть в основном решены с помощью Libsodiumбиблиотеки.

// On Alice's computer:
$msg = 'This comes from Alice.';
$signed_msg = sodium_crypto_sign($msg, $secret_sign_key);


// On Bob's computer:
$original_msg = sodium_crypto_sign_open($signed_msg, $alice_sign_publickey);
if ($original_msg === false) {
    throw new Exception('Invalid signature');
} else {
    echo $original_msg; // Displays "This comes from Alice."
}

Документация по Libsodium: https://github.com/paragonie/pecl-libsodium-doc


2
если вы вставите некоторый код, убедитесь, что все переменные покрыты. В вашем примере $ secret_sign_key и $ alice_sign_publickey имеют значение NULL
— undefinedman,

1
crypto_signAPI делает не шифровать сообщения - это потребует одна из crypto_aead_*_encryptфункций.
— Роджер Дуек

1

ВАЖНО этот ответ действителен только для PHP 5, в PHP 7 используются встроенные криптографические функции.

Вот простая, но достаточно безопасная реализация:

  • Шифрование AES-256 в режиме CBC
  • PBKDF2 для создания ключа шифрования из простого текстового пароля
  • HMAC для аутентификации зашифрованного сообщения.

Код и примеры здесь: https://stackoverflow.com/a/19445173/1387163


1
Я не специалист по криптографии, но иметь ключ, полученный непосредственно из пароля, кажется ужасной идеей. Радужные таблицы + слабый пароль и ушли - это ваша безопасность. Кроме того, ваша ссылка указывает на функции mcrypt, которые устарели с PHP 7.1
— Alph.Dev

@ Alph.Dev Вы правы, ответ выше действителен только для PHP 5
— Евгений Фиделин
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.