Two-way encryption: I need to store passwords that can be retrieved

67,408

Solution 1

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...

Solution 2

How do I encrypt and decrypt a password in PHP? By implementing one of many encryption algorithms. (or using one of many libraries)

What is the safest algorithm to encrypt the passwords with? There are tons of different algorithms, none of which are 100% secure. But many of them are secure enough for commerce and even military purposes

Where do I store the private key? If you have decided to implement public key - cryptography algorithm(eg RSA), you don't store private key. user have private key. your system has public key which could be stored anywhere you wish.

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) Well if your user can remember ridiculously long prime numbers then - yes, why not. But generally you would need to come up with the system which will allow user to store their key somewhere.

In what ways can the password be stolen and decrypted? What do I need to be aware of? This depends on the algorithm used. However always make sure that you don't send password unencrypted to or from the user. Either encrypt/decrypt it on the client side, or use https(or user other cryptographic means to secure connection between server and client).

However if all you need is to store passwords in encrypted way, I would suggest you to use a simple XOR Cipher. The main problem with this algorithm is that it could be easily broken by frequency analysis. However as generally passwords are not made from long paragraphs of English text I don't think you should worry about it. The second problem with XOR Cipher is that if you have a message in both encrypted and decrypted form you could easily find out password with which it was encrypted. Again, not a big problem in your case as it only affects the user who already was compromised by other means.

Solution 3

  1. The PHP function you are after is Mcrypt (http://www.php.net/manual/en/intro.mcrypt.php).

The example from the manual is slightly edited for this example):

<?php
$iv_size = mcrypt_get_iv_size(MCRYPT_BLOWFISH, MCRYPT_MODE_ECB);
$iv = mcrypt_create_iv($iv_size, MCRYPT_RAND);
$key = "This is a very secret key";
$pass = "PasswordHere";
echo strlen($pass) . "\n";

$crypttext = mcrypt_encrypt(MCRYPT_BLOWFISH, $key, $pass, MCRYPT_MODE_ECB, $iv);
echo strlen($crypttext) . "\n";
?>

You would use mcrypt_decrypt to decrypt your password.

  1. The best algorithm is rather subjective - ask 5 people, get 5 answers. Personally if the the default (Blowfish) isn't good enough for you, you probably have bigger problems!

  2. Given that it is needed by PHP to encrypt - not sure you can hide it anywhere - welcome comments on this. Standard PHP best coding practices apply of course!

  3. Given that the encryption key will be in your code anyway, not sure what you will gain, providing the rest of your application is secure.

  4. Obviously, if the encrypted password and the encryption key are stolen, then game over.

I'd put a rider on my answer - I'm not a PHP crypto expert, but, I think what I have answered is standard practice - I welcome comments other may have.

Solution 4

A lot of users have suggested using mcrypt... which is correct, but I like to go a step further to make it easily stored and transfered (as sometimes encrypted values can make them hard to send using other technologies like curl, or json).

After you have successfully encrypted using mcrypt, run it through base64_encode and then convert it to hex code. Once in hex code it's easy to transfer in a variety of ways.

$td = mcrypt_module_open('tripledes', '', 'ecb', '');
$iv = mcrypt_create_iv (mcrypt_enc_get_iv_size($td), MCRYPT_RAND);
$key = substr("SUPERSECRETKEY",0,mcrypt_enc_get_key_size($td));
mcrypt_generic_init($td, $key, $iv);
$encrypted = mcrypt_generic($td, $unencrypted);
$encrypted = $ua."||||".$iv;
mcrypt_generic_deinit($td);
mcrypt_module_close($td);
$encrypted = base64_encode($encrypted);
$encrypted = array_shift(unpack('H*', $encrypted));

And on the other side:

$encrypted = pack('H*', $encrypted);
$encrypted = base64_decode($encrypted);
list($encrypted,$iv) = explode("||||",$encrypted,2);
$td = mcrypt_module_open('tripledes', '', 'ecb', '');
$key = substr("SUPERSECRETKEY",0,mcrypt_enc_get_key_size($td));
mcrypt_generic_init($td, $key, $iv);
$unencrypted = mdecrypt_generic($td, $encrypted);
mcrypt_generic_deinit($td);
mcrypt_module_close($td);

Solution 5

I'd only suggest public key encryption if you want the ability to set a user's password without their interaction (this can be handy for resets and shared passwords).

Public key

  1. The OpenSSL extension, specifically openssl_public_encrypt and openssl_private_decrypt
  2. This would be straight RSA assuming your passwords will fit in key size - padding, otherwise you need a symmetric layer
  3. Store both keys for each user, the private key's passphrase is their application password

Symmetric

  1. The Mcrypt extension
  2. AES-256 is probably a safe bet, but this could be a SO question in itself
  3. You don't - this would be their application password

Both

4. Yes - users would have to enter their application password every time, but storing it in the session would raise other issues

5.

  • If someone steals the application data, it's as secure as the symmetric cipher (for the public key scheme, it's used to protect the private key with the passphrase.)
  • Your application should definitely be only accessible over SSL, preferably using client certificates.
  • Consider adding a second factor for authentication which would only be used once per session, like a token sent via SMS.
Share:
67,408

Related videos on Youtube

HyderA
Author by

HyderA

Updated on September 25, 2020

Comments

  • HyderA
    HyderA over 3 years

    I am creating an application that will store passwords, which the user can retrieve and see. The passwords are for a hardware device, so checking against hashes are out of the question.

    What I need to know is:

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

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

    3. Where do I store the private key?

    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)

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

    • Exhibitioner
      Exhibitioner about 6 years
      Note: Libsodium is now compiled into the PHP core for >= 7.2. This would be the "go to" solution now as it's full of modern methods unlike mcrypt which is considered deprecated and has been removed.
  • HyderA
    HyderA about 13 years
    $pass = $text. I think he changed that to cater to the question, and didn't notice the second occurrence.
  • ircmaxell
    ircmaxell about 13 years
    Two things to note. First, MCRYPT_MODE_ECB doesn't use an IV. Second, if it did, you'd need to store the IV as you can't decrypt the data without it...
  • jcolebrand
    jcolebrand about 13 years
    Somebody doesn't understand what it means "break". @IRC nice job on the class, that's pretty damned nice code.
  • The Wavelength
    The Wavelength over 11 years
    The following returns false. Any idea why? $x = new Encryption(MCRYPT_BlOWFISH, MCRYPT_MODE_CBC); $test = $x->encrypt("test", "a"); echo var_dump($x->decrypt($test, "a"));
  • cosmorogers
    cosmorogers over 11 years
    I think I fixed the problem you are having @TheWavelength (i had the same problem). In the decrypt function the line after $dec = mcrypt_... is $data = $this->unpad($data);. If you change that to $data = $this->unpad($dec); it appears to work for me
  • cosmorogers
    cosmorogers over 11 years
    Oh and again in the decrypt function changing the two -64s to -128 helped (so you get $enc = substr($data, 128, -128) and $mac = substr($data, -128);
  • Gigala
    Gigala about 11 years
    is there any way to do this without the user input (the user enters his password only once and its encrypted, and later automatically decrypted without the users input)?
  • nt.bas
    nt.bas about 9 years
    @ircmaxell It's been quite a while since the code was last revised so I'm wondering if it's up to date. I need to use something similar for a financial application and it would be nice if you gave an okay with this class :-)
  • Scott Arciszewski
    Scott Arciszewski about 8 years
    Avoid mcrypt, be careful with openssl_private_decrypt().
  • Scott Arciszewski
    Scott Arciszewski about 8 years
    "The best algorithm is rather subjective - ask 5 people, get 5 answers. Personally if the the default (Blowfish) isn't good enough for you, you probably have bigger problems!" This is totally wrong. Any crypto expert will more-or-less agree with gist.github.com/tqbf/be58d2d39690c3b366ad which specifically excludes blowfish
  • Scott Arciszewski
    Scott Arciszewski about 8 years
  • Arsha
    Arsha over 7 years
    Warning! The mcrypt extension has been abandonware for nearly a decade now, and was also fairly complex to use. It has therefore been deprecated in favour of OpenSSL, where it will be removed from the core and into PECL in PHP 7.2. th1.php.net/manual/en/migration71.deprecated.php
  • Bradley
    Bradley about 7 years
    Well - it was in 2011 :P

Related