Viewed   237 times

What is the simplest way of doing two way encryption in common PHP installs?

I need to be able to encrypt data with a string key, and use the same key to decrypt on the other end.

The security isn't as big of a concern as the portability of the code, so I'd like to be able to keep things as simple as possible. Currently, I am using an RC4 implementation, but if I can find something natively supported I figure I can save a lot of unnecessary code.

 Answers

1

Edited:

You should really be using openssl_encrypt() & openssl_decrypt()

As Scott says, Mcrypt is not a good idea as it has not been updated since 2007.

There is even an RFC to remove Mcrypt from PHP - https://wiki.php.net/rfc/mcrypt-viking-funeral

Thursday, November 17, 2022
2

Foreword

Starting with your table definition:

- UserID
- Fname
- Lname
- Email
- Password
- IV

Here are the changes:

  1. The fields Fname, Lname and Email will be encrypted using a symmetric cipher, provided by OpenSSL,
  2. The IV field will store the initialisation vector used for encryption. The storage requirements depend on the cipher and mode used; more about this later.
  3. The Password field will be hashed using a one-way password hash,

Encryption

Cipher and mode

Choosing the best encryption cipher and mode is beyond the scope of this answer, but the final choice affects the size of both the encryption key and initialisation vector; for this post we will be using AES-256-CBC which has a fixed block size of 16 bytes and a key size of either 16, 24 or 32 bytes.

Encryption key

A good encryption key is a binary blob that's generated from a reliable random number generator. The following example would be recommended (>= 5.3):

$key_size = 32; // 256 bits
$encryption_key = openssl_random_pseudo_bytes($key_size, $strong);
// $strong will be true if the key is crypto safe

This can be done once or multiple times (if you wish to create a chain of encryption keys). Keep these as private as possible.

IV

The initialisation vector adds randomness to the encryption and required for CBC mode. These values should be ideally be used only once (technically once per encryption key), so an update to any part of a row should regenerate it.

A function is provided to help you generate the IV:

$iv_size = 16; // 128 bits
$iv = openssl_random_pseudo_bytes($iv_size, $strong);

Example

Let's encrypt the name field, using the earlier $encryption_key and $iv; to do this, we have to pad our data to the block size:

function pkcs7_pad($data, $size)
{
    $length = $size - strlen($data) % $size;
    return $data . str_repeat(chr($length), $length);
}

$name = 'Jack';
$enc_name = openssl_encrypt(
    pkcs7_pad($name, 16), // padded data
    'AES-256-CBC',        // cipher and mode
    $encryption_key,      // secret key
    0,                    // options (not used)
    $iv                   // initialisation vector
);

Storage requirements

The encrypted output, like the IV, is binary; storing these values in a database can be accomplished by using designated column types such as BINARY or VARBINARY.

The output value, like the IV, is binary; to store those values in MySQL, consider using BINARY or VARBINARY columns. If this is not an option, you can also convert the binary data into a textual representation using base64_encode() or bin2hex(), doing so requires between 33% to 100% more storage space.

Decryption

Decryption of the stored values is similar:

function pkcs7_unpad($data)
{
    return substr($data, 0, -ord($data[strlen($data) - 1]));
}

$row = $result->fetch(PDO::FETCH_ASSOC); // read from database result
// $enc_name = base64_decode($row['Name']);
// $enc_name = hex2bin($row['Name']);
$enc_name = $row['Name'];
// $iv = base64_decode($row['IV']);
// $iv = hex2bin($row['IV']);
$iv = $row['IV'];

$name = pkcs7_unpad(openssl_decrypt(
    $enc_name,
    'AES-256-CBC',
    $encryption_key,
    0,
    $iv
));

Authenticated encryption

You can further improve the integrity of the generated cipher text by appending a signature that's generated from a secret key (different from the encryption key) and the cipher text. Before the cipher text is decrypted, the signature is first verified (preferably with a constant-time comparison method).

Example

// generate once, keep safe
$auth_key = openssl_random_pseudo_bytes(32, $strong);

// authentication
$auth = hash_hmac('sha256', $enc_name, $auth_key, true);
$auth_enc_name = $auth . $enc_name;

// verification
$auth = substr($auth_enc_name, 0, 32);
$enc_name = substr($auth_enc_name, 32);
$actual_auth = hash_hmac('sha256', $enc_name, $auth_key, true);

if (hash_equals($auth, $actual_auth)) {
    // perform decryption
}

See also: hash_equals()

Hashing

Storing a reversible password in your database must be avoided as much as possible; you only wish to verify the password rather than knowing its contents. If a user loses their password, it's better to allow them to reset it rather than sending them their original one (make sure that password reset can only be done for a limited time).

Applying a hash function is a one-way operation; afterwards it can be safely used for verification without revealing the original data; for passwords, a brute force method is a feasible approach to uncover it due to its relatively short length and poor password choices of many people.

Hashing algorithms such as MD5 or SHA1 were made to verify file contents against a known hash value. They're greatly optimized to make this verification as fast as possible while still being accurate. Given their relatively limited output space it was easy to build a database with known passwords and their respective hash outputs, the rainbow tables.

Adding a salt to the password before hashing it would render a rainbow table useless, but recent hardware advancements made brute force lookups a viable approach. That's why you need a hashing algorithm that's deliberately slow and simply impossible to optimize. It should also be able to increase the load for faster hardware without affecting the ability to verify existing password hashes to make it future proof.

Currently there are two popular choices available:

  1. PBKDF2 (Password Based Key Derivation Function v2)
  2. bcrypt (aka Blowfish)

This answer will use an example with bcrypt.

Generation

A password hash can be generated like this:

$password = 'my password';
$random = openssl_random_pseudo_bytes(18);
$salt = sprintf('$2y$%02d$%s',
    13, // 2^n cost factor
    substr(strtr(base64_encode($random), '+', '.'), 0, 22)
);

$hash = crypt($password, $salt);

The salt is generated with openssl_random_pseudo_bytes() to form a random blob of data which is then run through base64_encode() and strtr() to match the required alphabet of [A-Za-z0-9/.].

The crypt() function performs the hashing based on the algorithm ($2y$ for Blowfish), the cost factor (a factor of 13 takes roughly 0.40s on a 3GHz machine) and the salt of 22 characters.

Validation

Once you have fetched the row containing the user information, you validate the password in this manner:

$given_password = $_POST['password']; // the submitted password
$db_hash = $row['Password']; // field with the password hash

$given_hash = crypt($given_password, $db_hash);

if (isEqual($given_hash, $db_hash)) {
    // user password verified
}

// constant time string compare
function isEqual($str1, $str2)
{
    $n1 = strlen($str1);
    if (strlen($str2) != $n1) {
        return false;
    }
    for ($i = 0, $diff = 0; $i != $n1; ++$i) {
        $diff |= ord($str1[$i]) ^ ord($str2[$i]);
    }
    return !$diff;
}

To verify a password, you call crypt() again but you pass the previously calculated hash as the salt value. The return value yields the same hash if the given password matches the hash. To verify the hash, it's often recommended to use a constant-time comparison function to avoid timing attacks.

Password hashing with PHP 5.5

PHP 5.5 introduced the password hashing functions that you can use to simplify the above method of hashing:

$hash = password_hash($password, PASSWORD_BCRYPT, ['cost' => 13]);

And verifying:

if (password_verify($given_password, $db_hash)) {
    // password valid
}

See also: password_hash(), password_verify()

Saturday, November 19, 2022
4

Personally, I would use mcrypt like others posted. But there is much more to note...

  1. How do I encrypt and decrypt a password in PHP?

    See below for a strong class that takes care of everything for you:

  2. What is the safest algorithm to encrypt the passwords with?

    safest? any of them. The safest method if you're going to encrypt is to protect against information disclosure vulnerabilities (XSS, remote inclusion, etc). If it gets out, the attacker can eventually crack the encryption (no encryption is 100% un-reversible without the key - As @NullUserException points out this is not entirely true. There are some encryption schemes that are impossible to crack such as OneTimePad).

  3. Where do I store the private key?

    What I would do is use 3 keys. One is user supplied, one is application specific and the other is user specific (like a salt). The application specific key can be stored anywhere (in a config file outside of the web-root, in an environmental variable, etc). The user specific one would be stored in a column in the db next to the encrypted password. The user supplied one would not be stored. Then, you'd do something like this:

    $key = $userKey . $serverKey . $userSuppliedKey;
    

    The benefit there, is that any 2 of the keys can be compromised without the data being compromised. If there's a SQL Injection attack, they can get the $userKey, but not the other 2. If there's a local server exploit, they can get $userKey and $serverKey, but not the third $userSuppliedKey. If they go beat the user with a wrench, they can get the $userSuppliedKey, but not the other 2 (but then again, if the user is beaten with a wrench, you're too late anyway).

  4. Instead of storing the private key, is it a good idea to require users to enter the private key any time they need a password decrypted? (Users of this application can be trusted)

    Absolutely. In fact, that's the only way I would do it. Otherwise you'd need to store an unencrypted version in a durable storage format (shared memory such as APC or memcached, or in a session file). That's exposing yourself to additional compromises. Never store the unencrypted version of the password in anything except a local variable.

  5. In what ways can the password be stolen and decrypted? What do I need to be aware of?

    Any form of compromise of your systems will let them view encrypted data. If they can inject code or get to your filesystem, they can view decrypted data (since they can edit the files that decrypt the data). Any form of Replay or MITM attack will also give them full access to the keys involved. Sniffing the raw HTTP traffic will also give them the keys.

    Use SSL for all traffic. And make sure nothing on the server has any kind of vulnerabilities (CSRF, XSS, SQL Injection, Privilege Escalation, Remote Code Execution, etc).

Edit: Here's a PHP class implementation of a strong encryption method:

/**
 * A class to handle secure encryption and decryption of arbitrary data
 *
 * Note that this is not just straight encryption.  It also has a few other
 *  features in it to make the encrypted data far more secure.  Note that any
 *  other implementations used to decrypt data will have to do the same exact
 *  operations.  
 *
 * Security Benefits:
 *
 * - Uses Key stretching
 * - Hides the Initialization Vector
 * - Does HMAC verification of source data
 *
 */
class Encryption {

    /**
     * @var string $cipher The mcrypt cipher to use for this instance
     */
    protected $cipher = '';

    /**
     * @var int $mode The mcrypt cipher mode to use
     */
    protected $mode = '';

    /**
     * @var int $rounds The number of rounds to feed into PBKDF2 for key generation
     */
    protected $rounds = 100;

    /**
     * Constructor!
     *
     * @param string $cipher The MCRYPT_* cypher to use for this instance
     * @param int    $mode   The MCRYPT_MODE_* mode to use for this instance
     * @param int    $rounds The number of PBKDF2 rounds to do on the key
     */
    public function __construct($cipher, $mode, $rounds = 100) {
        $this->cipher = $cipher;
        $this->mode = $mode;
        $this->rounds = (int) $rounds;
    }

    /**
     * Decrypt the data with the provided key
     *
     * @param string $data The encrypted datat to decrypt
     * @param string $key  The key to use for decryption
     * 
     * @returns string|false The returned string if decryption is successful
     *                           false if it is not
     */
    public function decrypt($data, $key) {
        $salt = substr($data, 0, 128);
        $enc = substr($data, 128, -64);
        $mac = substr($data, -64);

        list ($cipherKey, $macKey, $iv) = $this->getKeys($salt, $key);

        if (!hash_equals(hash_hmac('sha512', $enc, $macKey, true), $mac)) {
             return false;
        }

        $dec = mcrypt_decrypt($this->cipher, $cipherKey, $enc, $this->mode, $iv);

        $data = $this->unpad($dec);

        return $data;
    }

    /**
     * Encrypt the supplied data using the supplied key
     * 
     * @param string $data The data to encrypt
     * @param string $key  The key to encrypt with
     *
     * @returns string The encrypted data
     */
    public function encrypt($data, $key) {
        $salt = mcrypt_create_iv(128, MCRYPT_DEV_URANDOM);
        list ($cipherKey, $macKey, $iv) = $this->getKeys($salt, $key);

        $data = $this->pad($data);

        $enc = mcrypt_encrypt($this->cipher, $cipherKey, $data, $this->mode, $iv);

        $mac = hash_hmac('sha512', $enc, $macKey, true);
        return $salt . $enc . $mac;
    }

    /**
     * Generates a set of keys given a random salt and a master key
     *
     * @param string $salt A random string to change the keys each encryption
     * @param string $key  The supplied key to encrypt with
     *
     * @returns array An array of keys (a cipher key, a mac key, and a IV)
     */
    protected function getKeys($salt, $key) {
        $ivSize = mcrypt_get_iv_size($this->cipher, $this->mode);
        $keySize = mcrypt_get_key_size($this->cipher, $this->mode);
        $length = 2 * $keySize + $ivSize;

        $key = $this->pbkdf2('sha512', $key, $salt, $this->rounds, $length);

        $cipherKey = substr($key, 0, $keySize);
        $macKey = substr($key, $keySize, $keySize);
        $iv = substr($key, 2 * $keySize);
        return array($cipherKey, $macKey, $iv);
    }

    /**
     * Stretch the key using the PBKDF2 algorithm
     *
     * @see http://en.wikipedia.org/wiki/PBKDF2
     *
     * @param string $algo   The algorithm to use
     * @param string $key    The key to stretch
     * @param string $salt   A random salt
     * @param int    $rounds The number of rounds to derive
     * @param int    $length The length of the output key
     *
     * @returns string The derived key.
     */
    protected function pbkdf2($algo, $key, $salt, $rounds, $length) {
        $size   = strlen(hash($algo, '', true));
        $len    = ceil($length / $size);
        $result = '';
        for ($i = 1; $i <= $len; $i++) {
            $tmp = hash_hmac($algo, $salt . pack('N', $i), $key, true);
            $res = $tmp;
            for ($j = 1; $j < $rounds; $j++) {
                 $tmp  = hash_hmac($algo, $tmp, $key, true);
                 $res ^= $tmp;
            }
            $result .= $res;
        }
        return substr($result, 0, $length);
    }

    protected function pad($data) {
        $length = mcrypt_get_block_size($this->cipher, $this->mode);
        $padAmount = $length - strlen($data) % $length;
        if ($padAmount == 0) {
            $padAmount = $length;
        }
        return $data . str_repeat(chr($padAmount), $padAmount);
    }

    protected function unpad($data) {
        $length = mcrypt_get_block_size($this->cipher, $this->mode);
        $last = ord($data[strlen($data) - 1]);
        if ($last > $length) return false;
        if (substr($data, -1 * $last) !== str_repeat(chr($last), $last)) {
            return false;
        }
        return substr($data, 0, -1 * $last);
    }
}

Note that I'm using a function added in PHP 5.6: hash_equals. If you're on lower than 5.6, you can use this substitute function which implements a timing-safe comparison function using double HMAC verification:

function hash_equals($a, $b) {
    $key = mcrypt_create_iv(128, MCRYPT_DEV_URANDOM);
    return hash_hmac('sha512', $a, $key) === hash_hmac('sha512', $b, $key);
}

Usage:

$e = new Encryption(MCRYPT_BLOWFISH, MCRYPT_MODE_CBC);
$encryptedData = $e->encrypt($data, $key);

Then, to decrypt:

$e2 = new Encryption(MCRYPT_BLOWFISH, MCRYPT_MODE_CBC);
$data = $e2->decrypt($encryptedData, $key);

Note that I used $e2 the second time to show you different instances will still properly decrypt the data.

Now, how does it work/why use it over another solution:

  1. Keys

    • The keys are not directly used. Instead, the key is stretched by a standard PBKDF2 derivation.

    • The key used for encryption is unique for every encrypted block of text. The supplied key therefore becomes a "master key". This class therefore provides key rotation for cipher and auth keys.

    • IMPORTANT NOTE, the $rounds parameter is configured for true random keys of sufficient strength (128 bits of Cryptographically Secure random at a minimum). If you are going to use a password, or non-random key (or less random then 128 bits of CS random), you must increase this parameter. I would suggest a minimum of 10000 for passwords (the more you can afford, the better, but it will add to the runtime)...

  2. Data Integrity

    • The updated version uses ENCRYPT-THEN-MAC, which is a far better method for ensuring the authenticity of the encrypted data.
  3. Encryption:

    • It uses mcrypt to actually perform the encryption. I would suggest using either MCRYPT_BLOWFISH or MCRYPT_RIJNDAEL_128 cyphers and MCRYPT_MODE_CBC for the mode. It's strong enough, and still fairly fast (an encryption and decryption cycle takes about 1/2 second on my machine).

Now, as to point 3 from the first list, what that would give you is a function like this:

function makeKey($userKey, $serverKey, $userSuppliedKey) {
    $key = hash_hmac('sha512', $userKey, $serverKey);
    $key = hash_hmac('sha512', $key, $userSuppliedKey);
    return $key;
}

You could stretch it in the makeKey() function, but since it's going to be stretched later, there's not really a huge point to doing so.

As far as the storage size, it depends on the plain text. Blowfish uses a 8 byte block size, so you'll have:

  • 16 bytes for the salt
  • 64 bytes for the hmac
  • data length
  • Padding so that data length % 8 == 0

So for a 16 character data source, there will be 16 characters of data to be encrypted. So that means the actual encrypted data size is 16 bytes due to padding. Then add the 16 bytes for the salt and 64 bytes for the hmac and the total stored size is 96 bytes. So there's at best a 80 character overhead, and at worst a 87 character overhead...

I hope that helps...

Note: 12/11/12: I just updated this class with a MUCH better encryption method, using better derived keys, and fixing the MAC generation...

Sunday, November 27, 2022
2

Updated

PHP 7 ready version. It uses openssl_encrypt function from PHP OpenSSL Library.

class Openssl_EncryptDecrypt {
    function encrypt ($pure_string, $encryption_key) {
        $cipher     = 'AES-256-CBC';
        $options    = OPENSSL_RAW_DATA;
        $hash_algo  = 'sha256';
        $sha2len    = 32;
        $ivlen = openssl_cipher_iv_length($cipher);
        $iv = openssl_random_pseudo_bytes($ivlen);
        $ciphertext_raw = openssl_encrypt($pure_string, $cipher, $encryption_key, $options, $iv);
        $hmac = hash_hmac($hash_algo, $ciphertext_raw, $encryption_key, true);
        return $iv.$hmac.$ciphertext_raw;
    }
    function decrypt ($encrypted_string, $encryption_key) {
        $cipher     = 'AES-256-CBC';
        $options    = OPENSSL_RAW_DATA;
        $hash_algo  = 'sha256';
        $sha2len    = 32;
        $ivlen = openssl_cipher_iv_length($cipher);
        $iv = substr($encrypted_string, 0, $ivlen);
        $hmac = substr($encrypted_string, $ivlen, $sha2len);
        $ciphertext_raw = substr($encrypted_string, $ivlen+$sha2len);
        $original_plaintext = openssl_decrypt($ciphertext_raw, $cipher, $encryption_key, $options, $iv);
        $calcmac = hash_hmac($hash_algo, $ciphertext_raw, $encryption_key, true);
        if(function_exists('hash_equals')) {
            if (hash_equals($hmac, $calcmac)) return $original_plaintext;
        } else {
            if ($this->hash_equals_custom($hmac, $calcmac)) return $original_plaintext;
        }
    }
    /**
     * (Optional)
     * hash_equals() function polyfilling.
     * PHP 5.6+ timing attack safe comparison
     */
    function hash_equals_custom($knownString, $userString) {
        if (function_exists('mb_strlen')) {
            $kLen = mb_strlen($knownString, '8bit');
            $uLen = mb_strlen($userString, '8bit');
        } else {
            $kLen = strlen($knownString);
            $uLen = strlen($userString);
        }
        if ($kLen !== $uLen) {
            return false;
        }
        $result = 0;
        for ($i = 0; $i < $kLen; $i++) {
            $result |= (ord($knownString[$i]) ^ ord($userString[$i]));
        }
        return 0 === $result;
    }
}

define('ENCRYPTION_KEY', '__^%&Q@$&*!@#$%^&*^__');
$string = "This is the original string!";

$OpensslEncryption = new Openssl_EncryptDecrypt;
$encrypted = $OpensslEncryption->encrypt($string, ENCRYPTION_KEY);
$decrypted = $OpensslEncryption->decrypt($encrypted, ENCRYPTION_KEY);
Thursday, September 29, 2022
 
forge
 
4

Placing a hash next to the ID to ensure it's security, or padding the ID with extra data, or even converting the ID to hex would all work fairly well I think.

Friday, October 28, 2022
 
duncanm
 
Only authorized users can answer the search term. Please sign in first, or register a free account.
Not the answer you're looking for? Browse other questions tagged :