7

I need to encrypt some data and have it decrypted on a later point in time. The data is tied to specific users. I've gathered two possible solutions...

1: The first one is derived from the official docs (example #1 @ http://php.net/manual/en/function.mcrypt-encrypt.php):

function encrypt($toEncrypt)
{
    global $key;
    $iv_size = mcrypt_get_iv_size(MCRYPT_RIJNDAEL_256, MCRYPT_MODE_CBC);
    $iv = mcrypt_create_iv($iv_size, MCRYPT_RAND);
    return base64_encode($iv . mcrypt_encrypt(MCRYPT_RIJNDAEL_256, $key, $toEncrypt, MCRYPT_MODE_CBC, $iv));
}

function decrypt($toDecrypt)
{
    global $key;
    $iv_size = mcrypt_get_iv_size(MCRYPT_RIJNDAEL_256, MCRYPT_MODE_CBC);
    $toDecrypt = base64_decode($toDecrypt);
    return rtrim(mcrypt_decrypt(MCRYPT_RIJNDAEL_256, $key, substr($toDecrypt, $iv_size), MCRYPT_MODE_CBC, substr($toDecrypt, 0, $iv_size)));
}

The key is generated once using:

pack('H*', bin2hex(openssl_random_pseudo_bytes(mcrypt_get_iv_size(MCRYPT_RIJNDAEL_256, MCRYPT_MODE_CBC))));

1.1: I've noticed that the encrypted result always ends in two equal signs ('=='). Why? - Using bin2hex() and hex2bin() in encrypt() and decrypt() instead of base64_encode()/base64_decode() respectively does not yield these results.

1.2: Will using bin2hex()/hex2bin() have any consequence on the outcome (other than length)?

1.3: There seems to be some discussion whether or not to call a trim-function on the return result when decrypting (this applies to the solution below as well). Why would this be necessary?


2: Second solution comes from here, Stackoverflow (https://stackoverflow.com/questions/9262109/php-simplest-two-way-encryption):

function encrypt($key, $toEncrypt)
{
    return base64_encode(mcrypt_encrypt(MCRYPT_RIJNDAEL_256, md5($key), $toEncrypt, MCRYPT_MODE_CBC, md5(md5($key))));
}

function decrypt($key, $toDecrypt)
{
    return rtrim(mcrypt_decrypt(MCRYPT_RIJNDAEL_256, md5($key), base64_decode($toDecrypt), MCRYPT_MODE_CBC, md5(md5($key))), "\0");
}

I'm aware that both approaches to the key handling is interchangeable, I purposely made them different in that respect in order to highlight possible solutions, please feel free to mix and match.

Personally I feel that the first one offers tighter security since both key and initialization vector is properly randomized. The second solution however, does offer some form of non-predictability since the key is unique for each piece of encrypted data (even though it suffers under the weak randomization of md5()). The key could for example be the user's name.

3: So, which one is preferable? I'm slightly in the dark since the Stackoverflow answer got a whopping 105 votes. Other thoughts, tips?

4: Bonus question!: I'm not incredibly brainy on server security aspects, but obviously gaining access to the PHP files would expose the key, which as a direct result, would render the encryption useless, assuming the attacker also has access to the DB. Is there any way to obscure the key?

Thank you for reading and have a nice day!

1
  • Cross posted on SO. Cross posting is against SE policy.
    – mikeazo
    Commented Oct 9, 2014 at 19:52

2 Answers 2

1

The two encryptions are equivalent. You can add a wrapper to use one approach as a drop-in replacement of the other:

/**
* Uses the one-parameter method as if it was the 2-parameter one.
*/
function buhlencrypt($keya, $data) {
   global $key;
   $key = $keya;
   return encrypt($data);
}

/**
* Uses the two-parameter method as if it was the one-parameter.
*/
function weidencrypt($data) {
   global $key;
   return encrypt($key, $data);
}

The "predictability" of MD5 adds a negligible level if 'insecurity' - 99.99999% of users will have their data hacked because they chose a trivial password or wrote it down in their address book or left it on a yellow sticky note under the keyboard. A professional attack will hack the server and gather all the passwords rather than bruteforcing a MD5 (see below, in answer to question 4).

1.1: I've noticed that the encrypted result always ends in two equal signs ('=='). Why? - Using bin2hex() and hex2bin() in encrypt() and decrypt() instead of base64_encode()/base64_decode() respectively does not yield these results.

Base64 encoding works by replacing groups of three binary bytes (3 x 8 = 24 bits) with four ASCII-6 characters (4 x 6 = 24 bits). This means that the input text must have a length multiple of three and the output will always have a length multiple of four.

To ensure this, Base64 adds a special padding to its output to signify whether there are one or two "ignoreable" characters. And since Rijndael produces an output that is multiple of 16, the last 16-block is always padded to length 18.

1.2: Will using bin2hex()/hex2bin() have any consequence on the outcome (other than length)?

None whatsoever.

1.3: There seems to be some discussion whether or not to call a trim-function on the return result when decrypting

That is because Rijndael also uses a fixed size block, so if you were to encrypt 17 bytes, it would be divided in two 16-byte blocks, giving 32 bytes of which the last 15 are padding. Since Rijndael makes no provision to indicate pad stripping (e.g. it does not store the length of the cleartext), the output will have a zeroed padding that may need removal. In some contexts this may not be necessary since a zero-padding might be ignored no matter what.

4: Bonus question!: I'm not incredibly brainy on server security aspects, but obviously gaining access to the PHP files would expose the key, which as a direct result, would render the encryption useless, assuming the attacker also has access to the DB. Is there any way to obscure the key?

If you do that symmetrically, then access to the PHP files will also allow removing the obscuration. Verify what level of access might one have once in possession of the PHP files. If his access is near to complete, it makes little sense to obfuscate the key.

However, you can do this, albeit in a roundabout way.

Add to your User model three properties (i.e. two column tables) called UserKey, RootKey and RootKeyOK. Each user gets a triplet.

When an user registers, you generate a random IV, and encrypt it using the user's password, storing it in UserKey. You also store it in the clear in RootKey, and set RootKeyOK to FALSE.

When the user changes the password, you use the old password to decrypt the UserKey and reencrypt it with the new password. You needn't touch the RootKeys at all.

Whenever the admin logs in, it browses the user database for keys not OK; for each of them it encrypts the RootKey with the Admin's password, and sets RootKeyOK to TRUE. This can be done automatically.

Now in your database you have:

  • HashedPassword --- unusable to an attacker since it's a hash.
  • UserKey --- unusable since it's encrypted
  • RootKey --- usable, IF still in the clear, but for this user's data only

When the user is logged in and you need to encrypt something only he (and the admin) can read, use his password (he's logged in, so it's in memory) to decrypt the userKey, and use the userKey to encrypt the data.

Of course, if you need to encrypt something when neither the user nor the admin are logged in, this approach won't work - you will have to store it unencrypted with a flag that tells it needs be encrypted, and do it as soon as either the user or the admin log in. While you can do this transparently to both, in truth the data may remain in the clear for long periods on the server, vulnerable to database capture (from remote break-in to hard disk burglary).

0

I liked this openssl_encrypt, there are different ones, you can try:

http://www.the-art-of-web.com/php/two-way-encryption/

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .