Cryptage bidirectionnel le plus simple avec PHP

230

Quelle est la façon la plus simple de procéder au chiffrement bidirectionnel dans les installations PHP courantes?

J'ai besoin de pouvoir chiffrer les données avec une clé de chaîne et utiliser la même clé pour déchiffrer à l'autre extrémité.

La sécurité n'est pas aussi importante que la portabilité du code, donc j'aimerais pouvoir garder les choses aussi simples que possible. Actuellement, j'utilise une implémentation RC4, mais si je peux trouver quelque chose pris en charge nativement, je pense que je peux économiser beaucoup de code inutile.

user1206970
la source
3
Pour le chiffrement à usage général, utilisez defuse / php-encryption / au lieu de lancer le vôtre.
Scott Arciszewski,
2
Mains loin de github.com/defuse/php-encryption - il est plus lent par ordre de grandeur que mcrypt.
Eugen Rieck
1
@Scott Penser dans le sens de "ce ne sera probablement pas le goulot d'étranglement" est ce qui nous a apporté beaucoup de mauvais logiciels.
Eugen Rieck
3
Si vous cryptez / décryptez vraiment beaucoup de données au point que les millisecondes qu'il en coûte gâchent votre application, mordez la balle et passez à libsodium. Sodium::crypto_secretbox()et Sodium::crypto_secretbox_open()sont sûrs et performants.
Scott Arciszewski

Réponses:

196

Édité:

Vous devriez vraiment utiliser openssl_encrypt () & openssl_decrypt ()

Comme le dit Scott , Mcrypt n'est pas une bonne idée car il n'a pas été mis à jour depuis 2007.

Il existe même un RFC pour supprimer Mcrypt de PHP - https://wiki.php.net/rfc/mcrypt-viking-funeral

472084
la source
6
@EugenRieck Oui, c'est le point. Mcrypt ne reçoit pas de correctifs. OpenSSL reçoit des correctifs dès qu'une vulnérabilité est découverte, grande ou petite.
Greg
5
il serait préférable qu'une telle réponse à vote élevé soit également fournie dans la réponse. Merci quand même.
T.Todua
les gars, juste FYI => MCRYPT est obsolète. capsuler donc tout le monde devrait savoir ne pas l'utiliser car cela nous a donné une myriade de problèmes. Il est obsolète depuis PHP 7.1 si je ne me trompe pas.
clusterBuddy
Depuis PHP 7, la fonction mcrypt est supprimée de la base de code php. Ainsi, lorsque vous utilisez la dernière version de php (qui devrait être standard), vous ne pouvez plus utiliser cette fonction obsolète.
Alexander Behling
234

Important : à moins que vous n'ayez un cas d'utilisation très particulier, ne cryptez pas les mots de passe , utilisez plutôt un algorithme de hachage de mot de passe. Lorsque quelqu'un dit chiffrer ses mots de passe dans une application côté serveur, il n'est pas informé ou décrit une conception de système dangereuse. Le stockage sécurisé des mots de passe est un problème totalement distinct du cryptage.

Être informé. Concevez des systèmes sûrs.

Cryptage de données portable en PHP

Si vous utilisez PHP 5.4 ou une version plus récente et que vous ne voulez pas écrire un module de cryptographie vous-même, je vous recommande d'utiliser une bibliothèque existante qui fournit un cryptage authentifié . La bibliothèque que j'ai liée ne dépend que de ce que PHP fournit et est régulièrement revue par une poignée de chercheurs en sécurité. (Moi inclus.)

Si vos objectifs de portabilité n'empêchent pas d'exiger des extensions PECL, libsodium est fortement recommandé par rapport à tout ce que vous ou moi pouvons écrire en PHP.

Mise à jour (2016-06-12): Vous pouvez maintenant utiliser sodium_compat et utiliser les mêmes offres de crypto libsodium sans installer d'extensions PECL.

Si vous voulez vous essayer à l'ingénierie de la cryptographie, lisez la suite.


Tout d'abord, vous devriez prendre le temps d'apprendre les dangers du chiffrement non authentifié et le principe du destin cryptographique .

  • Les données chiffrées peuvent toujours être falsifiées par un utilisateur malveillant.
  • L'authentification des données chiffrées empêche toute falsification.
  • L'authentification des données non cryptées n'empêche pas la falsification.

Cryptage et décryptage

Le cryptage en PHP est en fait simple (nous allons utiliser openssl_encrypt()et openssl_decrypt()une fois que vous aurez pris certaines décisions sur la façon de crypter vos informations. Consultez openssl_get_cipher_methods()pour une liste des méthodes prises en charge sur votre système. Le meilleur choix est AES en mode CTR :

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

Il n'y a actuellement aucune raison de penser que la taille de la clé AES est un problème important à craindre (une taille plus grande n'est probablement pas meilleure, en raison d'une mauvaise programmation des clés en mode 256 bits).

Remarque: nous ne l'utilisons pas mcryptcar il s'agit d'un abandonware et de bogues non corrigés qui pourraient affecter la sécurité. Pour ces raisons, j'encourage les autres développeurs PHP à l'éviter également.

Wrapper de chiffrement / déchiffrement simple à l'aide d'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;
    }
}

Exemple d'utilisation

$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);

Démo : https://3v4l.org/jl7qR


La bibliothèque de cryptographie simple ci-dessus n'est toujours pas sûre à utiliser. Nous devons authentifier les textes chiffrés et les vérifier avant de déchiffrer .

Remarque : par défaut, UnsafeCrypto::encrypt()renvoie une chaîne binaire brute. Appelez-le comme ceci si vous avez besoin de le stocker dans un format binaire (codé en 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);

Démo : http://3v4l.org/f5K93

Wrapper d'authentification simple

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);
    }
}

Exemple d'utilisation

$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);

Démos : binaire brut , encodé en base64


Si quelqu'un souhaite utiliser cette SaferCryptobibliothèque dans un environnement de production, ou votre propre implémentation des mêmes concepts, je recommande fortement de contacter vos cryptographes résidents pour un deuxième avis avant de le faire. Ils pourront vous parler d'erreurs que je ne connais peut-être même pas.

Vous serez beaucoup mieux en utilisant une bibliothèque de cryptographie réputée .

Scott Arciszewski
la source
3
Donc, j'essaie simplement de faire fonctionner UnsafeCrypto en premier. Le chiffrement se passe bien, mais chaque fois que je lance le déchiffrement, je reçois «faux» comme réponse. J'utilise la même clé pour déchiffrer, et je passe true sur l'encodage, ainsi que le décodage. Il y a, ce que je suppose est un typeo dans l'exemple, je me demande si c'est de là que vient mon problème. Pouvez-vous expliquer d'où vient la variable $ mac, et devrait-elle simplement être $ iv?
David C
1
@EugenRieck Les implémentations de chiffrement OpenSSL sont probablement les seules parties qui ne sont pas nulles, et c'est le seul moyen de tirer parti d'AES-NI en PHP vanille. Si vous installez sur OpenBSD, PHP sera compilé avec LibreSSL sans que le code PHP ne remarque de différence. Libsodium> OpenSSL tous les jours. En outre, ne pas utiliser libmcrypt . Que recommanderiez-vous aux développeurs PHP d'utiliser au lieu d'OpenSSL?
Scott Arciszewski du
2
Ni 5.2 ni 5.3 ne sont plus pris en charge . Vous devriez plutôt vous pencher sur la mise à jour vers une version prise en charge de PHP , telle que 5.6.
Scott Arciszewski
1
@BBeta paragonie.com/blog/2015/09/…
Scott Arciszewski
1
Je viens de le faire pour vous montrer que vous voulez des chaînes binaires, pas des chaînes à lecture humaine, pour vos clés .
Scott Arciszewski
22

Utilisez mcrypt_encrypt()et mcrypt_decrypt()avec les paramètres correspondants. Vraiment facile et simple, et vous utilisez un package de cryptage éprouvé.

ÉDITER

5 ans et 4 mois après cette réponse, l' mcryptextension est désormais en cours de dépréciation et de suppression éventuelle de PHP.

Eugen Rieck
la source
34
Testé en combat et non mis à jour depuis plus de 8 ans?
Maarten Bodewes,
2
Eh bien, mcrypt est en PHP7 et n'est pas obsolète - c'est assez bon pour moi. Tout le code n'est pas de la qualité horrible d'OpenSSL et doit être corrigé tous les quelques jours.
Eugen Rieck
3
mcrypt n'est pas seulement horrible en termes de support. Il n'implémente pas non plus les meilleures pratiques comme le remplissage conforme PKCS # 7, le chiffrement authentifié. Il ne prendra pas en charge SHA-3 ou tout autre nouvel algorithme car personne ne le maintient, vous privant d'un chemin de mise à niveau. De plus, il acceptait des choses comme les clés partielles, le remplissage nul, etc. Il y a une bonne raison pour laquelle il est en train d'être progressivement supprimé de PHP.
Maarten Bodewes
2
En PHP 7.1, toutes les fonctions mcrypt_ * déclencheront une notification E_DEPRECATED. En PHP 7.1 + 1 (que ce soit 7.2 ou 8.0), l'extension mcrypt sera déplacée hors du noyau et dans PECL, où les gens qui veulent vraiment l' installer pourront toujours le faire s'ils peuvent installer des extensions PHP à partir de PECL.
Mladen Janjetovic
4

PHP 7.2 s'est complètement éloigné Mcryptet le cryptage est désormais basé sur la Libsodiumbibliothèque maintenable .

Tous vos besoins de chiffrement peuvent être essentiellement résolus via la Libsodiumbibliothèque.

// 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."
}

Documentation Libsodium: https://github.com/paragonie/pecl-libsodium-doc

Hemerson Varela
la source
2
si vous collez du code, assurez-vous que toutes les variables sont couvertes. Dans votre exemple, $ secret_sign_key et $ alice_sign_publickey sont NULL
undefinedman
1
L' crypto_signAPI ne crypte pas les messages - cela nécessitera l'une des crypto_aead_*_encryptfonctions.
Roger Dueck
1

IMPORTANT cette réponse n'est valable que pour PHP 5, en PHP 7 utilisez des fonctions cryptographiques intégrées.

Voici une implémentation simple mais suffisamment sécurisée:

  • Cryptage AES-256 en mode CBC
  • PBKDF2 pour créer une clé de chiffrement à partir d'un mot de passe en texte brut
  • HMAC pour authentifier le message crypté.

Le code et les exemples sont ici: https://stackoverflow.com/a/19445173/1387163

Eugene Fidelin
la source
1
Je ne suis pas un expert en cryptographie, mais avoir une clé dérivée directement d'un mot de passe semble être une idée terrible. Tables arc-en-ciel + mot de passe faible et disparu est votre sécurité. Votre lien pointe également vers les fonctions mcrypt, obsolètes depuis PHP 7.1
Alph.Dev
@ Alph.Dev vous avez raison la réponse ci-dessus n'est valable que pour PHP 5
Eugene Fidelin