djekl
5/25/2013 - 7:08 AM

Data encryption in PHP - https://defuse.ca/secure-php-encryption.htm

<?php

/*
 * Data encryption in PHP.
 *
 * This script is released into the public domain by Defuse Security.
 * You may use it for any purpose whatsoever and redistribute it with or without
 * modification.
 *
 * https://defuse.ca/secure-php-encryption.htm
 */

// Configuration constants. Don't change unless you know what you're doing.
// The block cipher to use for encryption.
define('CRYPTO_CIPHER_ALG', MCRYPT_RIJNDAEL_128);
// The hash function to use for HMAC.
define('CRYPTO_HMAC_ALG', 'sha256');
// The byte length of the encryption and HMAC keys.
define('CRYPTO_KEY_BYTE_SIZE', 16);
// The block cipher mode of operation to use.
define('CRYPTO_CIPHER_MODE', 'cbc');
// The length of an HMAC, so it can be extracted from the ciphertext.
define('CRYPTO_HMAC_BYTES', strlen(hash_hmac(CRYPTO_HMAC_ALG, '', '', TRUE)));

class Crypto
{
    // Ciphertext format: [____HMAC____][____IV____][____CIPHERTEXT____].

    public static function Encrypt($plaintext, $key)
    {
        $key = self::AdjustKey($key, 16);

        if(strlen($key) < 16)
        {
            trigger_error("Key too small.", E_USER_ERROR);
            return FALSE;
        }

        // Open the encryption module and get some parameters.
        $crypt = mcrypt_module_open(CRYPTO_CIPHER_ALG, "", CRYPTO_CIPHER_MODE, "");
        $keysize = mcrypt_enc_get_key_size($crypt);
        $ivsize = mcrypt_enc_get_iv_size($crypt);

        // Generate a sub-key for encryption.
        $ekey = self::CreateSubkey($key, "encryption", $keysize);
        // Generate a random initialization vector.
        $iv = self::SecureRandom($ivsize);

        // Pad the plaintext to a multiple of the block size (PKCS #7)
        $block = mcrypt_enc_get_block_size($crypt);
        $pad = $block - (strlen($plaintext) % $block);
        $plaintext .= str_repeat(chr($pad), $pad);

        // Do the encryption.
        mcrypt_generic_init($crypt, $ekey, $iv);
        $ciphertext = $iv . mcrypt_generic($crypt, $plaintext);
        mcrypt_generic_deinit($crypt);
        mcrypt_module_close($crypt);

        // Generate a sub-key for authentication.
        $akey = self::CreateSubkey($key, "authentication", CRYPTO_KEY_BYTE_SIZE);
        // Apply the HMAC.
        $auth = hash_hmac(CRYPTO_HMAC_ALG, $ciphertext, $akey, TRUE);
        $ciphertext = $auth . $ciphertext;

        return $ciphertext;
    }

    public static function Decrypt($ciphertext, $key)
    {
        // Extract the HMAC from the front of the ciphertext.
        if(strlen($ciphertext) <= CRYPTO_HMAC_BYTES)
            return FALSE;

        $key        = self::AdjustKey($key, 16);
        $hmac       = substr($ciphertext, 0, CRYPTO_HMAC_BYTES);
        $ciphertext = substr($ciphertext, CRYPTO_HMAC_BYTES);

        // Re-generate the same authentication sub-key.
        $akey = self::CreateSubkey($key, "authentication", CRYPTO_KEY_BYTE_SIZE);

        // Make sure the HMAC is correct. If not, the ciphertext has been changed.
        if($hmac === hash_hmac(CRYPTO_HMAC_ALG, $ciphertext, $akey, TRUE))
        {
            // Open the encryption module and get some parameters.
            $crypt = mcrypt_module_open(CRYPTO_CIPHER_ALG, "", CRYPTO_CIPHER_MODE, "");
            $keysize = mcrypt_enc_get_key_size($crypt);
            $ivsize = mcrypt_enc_get_iv_size($crypt);

            // Re-generate the same encryption sub-key.
            $ekey = self::CreateSubkey($key, "encryption", $keysize);

            // Extract the initialization vector from the ciphertext.
            if(strlen($ciphertext) <= $ivsize)
                return FALSE;
            $iv = substr($ciphertext, 0, $ivsize);
            $ciphertext = substr($ciphertext, $ivsize);

            // Do the decryption.
            mcrypt_generic_init($crypt, $ekey, $iv);
            $plaintext = mdecrypt_generic($crypt, $ciphertext);
            mcrypt_generic_deinit($crypt);
            mcrypt_module_close($crypt);

            // Remove the padding.
            $pad = ord($plaintext[strlen($plaintext) - 1]);
            $plaintext = substr($plaintext, 0, strlen($plaintext) - $pad);

            return $plaintext;
        }
        else
        {
            // If the ciphertext has been modified, refuse to decrypt it.
            return FALSE;
        }
    }

    /*
     * Creates a sub-key from a master key for a specific purpose.
     */
    public static function CreateSubkey($master, $purpose, $bytes)
    {
        $source = self::AdjustKey(hash_hmac("sha512", $purpose, $master, TRUE), $bytes);

        if(strlen($source) < $bytes) {
            trigger_error("Subkey too big.", E_USER_ERROR);
            return $source; // fail safe
        }

        return substr($source, 0, $bytes);
    }

    /*
     * Returns a random binary string of length $octets bytes.
     */
    public static function SecureRandom($octets)
    {
        return mcrypt_create_iv($octets, MCRYPT_DEV_URANDOM);
    }

    /*
     * A simple test and demonstration of how to use this class.
     */
    public static function Test()
    {
        echo "Running crypto test...\n";

        $key = mcrypt_create_iv(16, MCRYPT_DEV_URANDOM);
        $data = "EnCrYpT EvErYThInG\x00\x00";

        $ciphertext = Crypto::Encrypt($data, $key);
        echo "Ciphertext: " . bin2hex($ciphertext) . "\n";

        $decrypted = Crypto::Decrypt($ciphertext, $key);
        echo "Decrypted: " . $decrypted . "\n";

        if($decrypted != $data)
        {
            echo "FAIL: Decrypted data is not the same as the original.\n";
            return FALSE;
        }

        if(Crypto::Decrypt($ciphertext . "a", $key) !== FALSE)
        {
            echo "FAIL: Ciphertext tampering not detected.\n";
            return FALSE;
        }

        $ciphertext[0] = chr((ord($ciphertext[0]) + 1) % 256);
        if(Crypto::Decrypt($ciphertext, $key) !== FALSE)
        {
            echo "FAIL: Ciphertext tampering not detected.\n";
            return FALSE;
        }

        $key = mcrypt_create_iv(16, MCRYPT_DEV_URANDOM);
        $data = "abcdef";
        $ciphertext = Crypto::Encrypt($data, $key);
        $wrong_key = mcrypt_create_iv(16, MCRYPT_DEV_URANDOM);
        if (Crypto::Decrypt($ciphertext, $wrong_key))
        {
            echo "FAIL: Ciphertext decrypts with an incorrect key.\n";
            return FALSE;
        }

        echo "PASS\n";
        return TRUE;
    }

    private static function AdjustKey($key, $length = 16)
    {
        if(strlen($key) > $length)
            $key = str_pad(sha1($key, TRUE), $length, chr(0));
        if(strlen($key) < $length)
            $key = str_pad($key, $length, chr(0));

        return $key;
    }
}

// Run the test when and only when this script is executed on the command line.
if(isset($argv) && realpath($argv[0]) == __FILE__)
{
    Crypto::Test();
}