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

230

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

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

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

user1206970
источник
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

472084
источник
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
@BBeta paragonie.com/blog/2015/09/...
Scott Арцишевского
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
Евгений Фиделин