Skip to content

Commit

Permalink
Fix and implement cross engine decryption.
Browse files Browse the repository at this point in the history
Sort out the nasty problems that were preventing Mcrypt and OpenSSL
backends from decrypting each others ciphertext.

Add a test to show how each engine can decode its own and the other's
cipher texts.
  • Loading branch information
markstory committed Dec 24, 2014
1 parent d7d1277 commit cf69477
Show file tree
Hide file tree
Showing 4 changed files with 79 additions and 38 deletions.
33 changes: 17 additions & 16 deletions src/Utility/Crypto/Mcrypt.php
Expand Up @@ -16,6 +16,11 @@

/**
* Mcrypt implementation of crypto features for Cake\Utility\Security
*
* This class is not intended to be used directly and should only
* be used in the context of Cake\Utility\Security.
*
* @internal
*/
class Mcrypt {

Expand Down Expand Up @@ -55,15 +60,18 @@ public static function rijndael($text, $key, $operation) {
* @return string Encrypted data.
* @throws \InvalidArgumentException On invalid data or key.
*/
public static function encrypt($plain, $key, $hmacSalt = null) {
public static function encrypt($plain, $key) {
$algorithm = MCRYPT_RIJNDAEL_128;
$mode = MCRYPT_MODE_CBC;

$ivSize = mcrypt_get_iv_size($algorithm, $mode);
$iv = mcrypt_create_iv($ivSize, MCRYPT_DEV_URANDOM);
$ciphertext = $iv . mcrypt_encrypt($algorithm, $key, $plain, $mode, $iv);
$hmac = hash_hmac('sha256', $ciphertext, $key);
return $hmac . $ciphertext;

// Pad out plain to make it AES compatible.
$pad = ($ivSize - (strlen($plain) % $ivSize));
$plain .= str_repeat(chr($pad), $pad);

return $iv . mcrypt_encrypt($algorithm, $key, $plain, $mode, $iv);
}

/**
Expand All @@ -75,24 +83,17 @@ public static function encrypt($plain, $key, $hmacSalt = null) {
* @throws InvalidArgumentException On invalid data or key.
*/
public static function decrypt($cipher, $key) {
// Split out hmac for comparison
$macSize = 64;
$hmac = substr($cipher, 0, $macSize);
$cipher = substr($cipher, $macSize);

$compareHmac = hash_hmac('sha256', $cipher, $key);
// TODO time constant comparison?
if ($hmac !== $compareHmac) {
return false;
}

$algorithm = MCRYPT_RIJNDAEL_128;
$mode = MCRYPT_MODE_CBC;
$ivSize = mcrypt_get_iv_size($algorithm, $mode);

$iv = substr($cipher, 0, $ivSize);
$cipher = substr($cipher, $ivSize);
$plain = mcrypt_decrypt($algorithm, $key, $cipher, $mode, $iv);
return rtrim($plain, "\0");

// Remove PKCS#7 padding
$padChar = substr($plain, -1);
$padLen = ord($padChar);
return substr($plain, 0, -$padLen);
}
}
28 changes: 11 additions & 17 deletions src/Utility/Crypto/OpenSsl.php
Expand Up @@ -19,6 +19,11 @@
*
* OpenSSL should be favored over mcrypt as it is actively maintained and
* more widely available.
*
* This class is not intended to be used directly and should only
* be used in the context of Cake\Utility\Security.
*
* @internal
*/
class OpenSsl {

Expand Down Expand Up @@ -49,12 +54,11 @@ public static function rijndael($text, $key, $operation) {
* @throws \InvalidArgumentException On invalid data or key.
*/
public static function encrypt($plain, $key, $hmacSalt = null) {
$method = 'AES-128-CBC';
$method = 'AES-256-CBC';
$ivSize = openssl_cipher_iv_length($method);

$iv = openssl_random_pseudo_bytes($ivSize);
$ciphertext = $iv . openssl_encrypt($plain, $method, $key, 0, $iv);
$hmac = hash_hmac('sha256', $ciphertext, $key);
return $hmac . $ciphertext;
return $iv . openssl_encrypt($plain, $method, $key, true, $iv);
}

/**
Expand All @@ -66,23 +70,13 @@ public static function encrypt($plain, $key, $hmacSalt = null) {
* @throws InvalidArgumentException On invalid data or key.
*/
public static function decrypt($cipher, $key) {
// Split out hmac for comparison
$macSize = 64;
$hmac = substr($cipher, 0, $macSize);
$cipher = substr($cipher, $macSize);

$compareHmac = hash_hmac('sha256', $cipher, $key);
// TODO time constant comparison?
if ($hmac !== $compareHmac) {
return false;
}
$method = 'AES-128-CBC';
$method = 'aes-256-cbc';
$ivSize = openssl_cipher_iv_length($method);

$iv = substr($cipher, 0, $ivSize);

$cipher = substr($cipher, $ivSize);
$plain = openssl_decrypt($cipher, $method, $key, 0, $iv);
return rtrim($plain, "\0");
return openssl_decrypt($cipher, $method, $key, true, $iv);
}
}

20 changes: 18 additions & 2 deletions src/Utility/Security.php
Expand Up @@ -98,6 +98,8 @@ public static function setHash($hash) {
/**
* Get the crypto implementation based on the loaded extensions.
*
* You can use this method to forcibly decide between mcrypt/openssl/custom implementations.
*
* @param object $instance The crypto instance to use.
* @return object Crypto instance.
*/
Expand All @@ -115,7 +117,9 @@ public static function engine($instance = null) {
if (isset(static::$_instance)) {
return static::$_instance;
}
throw new InvalidArgumentException('No compatible crypto engine loaded. Load either mcrypt or openssl');
throw new InvalidArgumentException(
'No compatible crypto engine available. ' .
'Load either the openssl or mcrypt extensions');
}

/**
Expand Down Expand Up @@ -164,7 +168,9 @@ public static function encrypt($plain, $key, $hmacSalt = null) {
$key = substr(hash('sha256', $key . $hmacSalt), 0, 32);

$crypto = static::engine();
return $crypto->encrypt($plain, $key);
$ciphertext = $crypto->encrypt($plain, $key);
$hmac = hash_hmac('sha256', $ciphertext, $key);
return $hmac . $ciphertext;
}

/**
Expand Down Expand Up @@ -204,6 +210,16 @@ public static function decrypt($cipher, $key, $hmacSalt = null) {
// Generate the encryption and hmac key.
$key = substr(hash('sha256', $key . $hmacSalt), 0, 32);

// Split out hmac for comparison
$macSize = 64;
$hmac = substr($cipher, 0, $macSize);
$cipher = substr($cipher, $macSize);

$compareHmac = hash_hmac('sha256', $cipher, $key);
if ($hmac !== $compareHmac) {
return false;
}

$crypto = static::engine();
return $crypto->decrypt($cipher, $key);
}
Expand Down
36 changes: 33 additions & 3 deletions tests/TestCase/Utility/SecurityTest.php
Expand Up @@ -15,6 +15,8 @@
namespace Cake\Test\TestCase\Utility;

use Cake\TestSuite\TestCase;
use Cake\Utility\Crypto\Mcrypt;
use Cake\Utility\Crypto\OpenSsl;
use Cake\Utility\Security;

/**
Expand Down Expand Up @@ -82,21 +84,23 @@ public function testHash() {
*/
public function testRijndael() {
$this->skipIf(!function_exists('mcrypt_encrypt'));
$engine = Security::engine();

Security::engine(new Mcrypt());
$txt = 'The quick brown fox jumped over the lazy dog.';
$key = 'DYhG93b0qyJfIxfs2guVoUubWwvniR2G0FgaC9mi';

$result = Security::rijndael($txt, $key, 'encrypt');
$this->assertEquals($txt, Security::rijndael($result, $key, 'decrypt'));

$result = Security::rijndael($key, $txt, 'encrypt');
$this->assertEquals($key, Security::rijndael($result, $txt, 'decrypt'));

$result = Security::rijndael('', $key, 'encrypt');
$this->assertEquals('', Security::rijndael($result, $key, 'decrypt'));

$key = 'this is my key of over 32 chars, yes it is';
$result = Security::rijndael($txt, $key, 'encrypt');
$this->assertEquals($txt, Security::rijndael($result, $key, 'decrypt'));

Security::engine($engine);
}

/**
Expand Down Expand Up @@ -245,6 +249,32 @@ public function testDecryptInvalidData() {
Security::decrypt($txt, $key);
}

/**
* Test that values encrypted with open ssl can be decrypted with mcrypt and the reverse.
*
* @return void
*/
public function testEngineEquivalence() {
$restore = Security::engine();
$txt = "Obi-wan you're our only hope";
$key = 'This is my secret key phrase it is quite long.';
$salt = 'A tasty salt that is delicious';

Security::engine(new Mcrypt());
$cipher = Security::encrypt($txt, $key, $salt);
$this->assertEquals($txt, Security::decrypt($cipher, $key, $salt));

Security::engine(new OpenSsl());
$this->assertEquals($txt, Security::decrypt($cipher, $key, $salt));

Security::engine(new OpenSsl());
$cipher = Security::encrypt($txt, $key, $salt);
$this->assertEquals($txt, Security::decrypt($cipher, $key, $salt));

Security::engine(new Mcrypt());
$this->assertEquals($txt, Security::decrypt($cipher, $key, $salt));
}

/**
* Tests that the salt can be set and retrieved
*
Expand Down

0 comments on commit cf69477

Please sign in to comment.