From 5a729eda8c5a67b6aec77951f3beded97fa9565b Mon Sep 17 00:00:00 2001 From: Taylor Hornby Date: Fri, 16 Oct 2015 14:59:49 -0600 Subject: [PATCH 01/35] Whitespace fix. --- benchmark.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/benchmark.php b/benchmark.php index c7b58ba..0d1f6f3 100644 --- a/benchmark.php +++ b/benchmark.php @@ -1,7 +1,7 @@ Date: Fri, 16 Oct 2015 15:34:03 -0600 Subject: [PATCH 02/35] Refactors, and changes the protcol a bit: Refactoring =================== Removes unnecessary inheritance. Using inheritance adds an extra mental burden of remembering where methods and variables are defined. With static inheritance, 'self::' can either mean "the file you're looking at" or "some other file." Moves the encoding functions into Encoding.php. I'm guessing the purpose of the inheritance was to be able to use 'protected' to prevent the user of the library from calling internal functions. This patch uses a different technique: There are only public and private methods, and the user is allowed to any public method of "user-usable" classes. So: We tell the user about Crypto and the new Encoding, but we don't tell them Core exists. The user should never be calling functions on Core, but our classes (and test cases) are allowed to. PHP doesn't let you make classes private to a namespace, so there's currently nothing *stopping* the user from doing that, but I think it's a better trade-off than the extra mental burden of using static inheritance. De-duplicates the magic number by defining Core::HEADER_MAGIC and Core::HEADER_MAGIC_FILE constants. The normal encryption and file encryption's config getters are now completely independent. This reduces coupling and increases cohesion, at the cost of duplication of the settings (but, conceptually, we may want to vary the file and normal encryption settings independently.) Increases security to downgrade attacks. The old code would simply fail if the version header was legacy. The new code allows you to pass a $min_ver_header to getVersionConfigFromHeader(), which means "I'm giving you a header, and I want you to give me the settings for that header, but I also want you to check that the version in that header is bigger than $min_ver_header." This is used by decrypt to ensure that it isn't decrypting a legacy ciphertext (i.e. the user has to call legacyDecrypt). This will be useful in the future if, say, the 2.0 configuration has a security vulnerability such that changing a 2.1 ciphertext's header to say 2.0 would make 2.1 ciphertexts vulnerable. (This downgrade prevention hasn't been implemented for the File class, but it should be soon.) Currently, there is no API for checking whether a ciphertext is legacy. That should be added in the future. In a couple places in the old code, $config was set to some hard-coded version if it wasn't passed in to the function. For each of those cases, I removed the code that did that, and ensured that a valid $config is always passed in. Protocol changes: =================== Different HKDF infos are used for the three different things: - Legacy encryption and decryption (keeps the old values) - 2.0 encryption and decryption (adds "V2") - File encryption and decryption (adds "V2File") This ensures that if the same key is re-used for all three, the same keys aren't re-used (potentially avoiding subtle interactions that could lead to a break). I got rid of the timing-safe code in getVersionConfig(). The differences in configuration returned (CBC vs CTR, etc.) reveal which version it is anyway, so there's no point trying to timing-safe-ize those functions. --- src/Core.php | 151 +++------------------------- src/Crypto.php | 214 ++++++++++++++++++++++------------------ src/Encoding.php | 66 +++++++++++++ src/File.php | 101 ++++++++++--------- tests/encode.php | 13 +-- tests/legacy.php | 5 +- tests/stream/keygen.php | 4 +- 7 files changed, 267 insertions(+), 287 deletions(-) create mode 100644 src/Encoding.php diff --git a/src/Core.php b/src/Core.php index 2b0bdf1..970df2b 100644 --- a/src/Core.php +++ b/src/Core.php @@ -3,79 +3,15 @@ use \Defuse\Crypto\Exception as Ex; -class Core +final class Core { - const VERSION = "\xD3\xF5\x02\x00"; - const HEADER_VERSION_SIZE = 4; // This should never change + const HEADER_MAGIC = "\xDE\xF5"; + const HEADER_MAGIC_FILE = "\xDE\xF4"; - /** - * Use this to generate a random encryption key. - * - * @return string - */ - public static function createNewRandomKey() - { - $valid = 0; - $config = self::getCoreVersionConfig(1, 0, $valid); - return self::secureRandom($config['KEY_BYTE_SIZE']); - } - /** - * Convert a binary string into a hexadecimal string without cache-timing - * leaks - * - * @param string $bin_string (raw binary) - * @return string - */ - public static function binToHex($bin_string) - { - $hex = ''; - $len = self::ourStrlen($bin_string); - for ($i = 0; $i < $len; ++$i) { - $c = \ord($bin_string[$i]) & 0xf; - $b = \ord($bin_string[$i]) >> 4; - $hex .= \chr(87 + $b + ((($b - 10) >> 8) & ~38)); - $hex .= \chr(87 + $c + ((($c - 10) >> 8) & ~38)); - } - return $hex; - } - - /** - * Convert a hexadecimal string into a binary string without cache-timing - * leaks - * - * @param string $hex_string - * @return string (raw binary) - */ - public static function hexToBin($hex_string) - { - $hex_pos = 0; - $bin = ''; - $hex_len = self::ourStrlen($hex_string); - $state = 0; - $c_acc = 0; - - while ($hex_pos < $hex_len) { - $c = \ord($hex_string[$hex_pos]); - $c_num = $c ^ 48; - $c_num0 = ($c_num - 10) >> 8; - $c_alpha = ($c & ~32) - 55; - $c_alpha0 = (($c_alpha - 10) ^ ($c_alpha - 16)) >> 8; - if (($c_num0 | $c_alpha0) === 0) { - throw new \RangeException( - 'Crypto::hexToBin() only expects hexadecimal characters' - ); - } - $c_val = ($c_num0 & $c_num) | ($c_alpha & $c_alpha0); - if ($state === 0) { - $c_acc = $c_val * 16; - } else { - $bin .= \chr($c_acc | $c_val); - } - $state = $state ? 0 : 1; - ++$hex_pos; - } - return $bin; - } + const CURRENT_VERSION = "\xDE\xF5\x02\x00"; + const CURRENT_FILE_VERSION = "\xDE\xF4\x02\x00"; + const LEGACY_VERSION = "\xDE\xF5\x01\x00"; + const HEADER_VERSION_SIZE = 4; /* This must never change. */ /** * Increment a counter (prevent nonce reuse) @@ -118,7 +54,7 @@ public static function incrementCounter($ctr, $inc, &$config) * @return string (raw binary) * @throws Ex\CannotPerformOperationException */ - protected static function secureRandom($octets) + public static function secureRandom($octets) { self::ensureFunctionExists('openssl_random_pseudo_bytes'); $secure = false; @@ -142,12 +78,8 @@ protected static function secureRandom($octets) * @return string * @throws Ex\CannotPerformOperationException */ - protected static function HKDF($hash, $ikm, $length, $info = '', $salt = null, $config = null) + public static function HKDF($hash, $ikm, $length, $info = '', $salt = null, $config = null) { - if (empty($config)) { - $valid = 0; - $config = self::getVersionConfig(1, 0, $valid); - } // Find the correct digest length as quickly as we can. $digest_length = $config['MAC_BYTE_SIZE']; if ($hash != $config['HASH_FUNCTION']) { @@ -210,7 +142,7 @@ protected static function HKDF($hash, $ikm, $length, $info = '', $salt = null, $ * @return boolean * @throws Ex\CannotPerformOperation */ - protected static function hashEquals($expected, $given) + public static function hashEquals($expected, $given) { static $native = null; if ($native === null) { @@ -248,7 +180,7 @@ protected static function hashEquals($expected, $given) * @param string $name * @throws Ex\CannotPerformOperationException */ - protected static function ensureConstantExists($name) + public static function ensureConstantExists($name) { if (!\defined($name)) { throw new Ex\CannotPerformOperationException(); @@ -261,7 +193,7 @@ protected static function ensureConstantExists($name) * @param string $name Function name * @throws Ex\CannotPerformOperationException */ - protected static function ensureFunctionExists($name) + public static function ensureFunctionExists($name) { if (!\function_exists($name)) { throw new Ex\CannotPerformOperationException(); @@ -281,7 +213,7 @@ protected static function ensureFunctionExists($name) * @param string $str * @return int */ - protected static function ourStrlen($str) + public static function ourStrlen($str) { static $exists = null; if ($exists === null) { @@ -307,7 +239,7 @@ protected static function ourStrlen($str) * @param int $length * @return string */ - protected static function ourSubstr($str, $start, $length = null) + public static function ourSubstr($str, $start, $length = null) { static $exists = null; if ($exists === null) { @@ -336,59 +268,4 @@ protected static function ourSubstr($str, $start, $length = null) } } - /** - * Take a 4-byte header and get meaningful version information out of it. - * Common configuration options should go in Core.php - * - * DO NOT CHANGE THESE VALUES! - * - * We spent *weeks* testing this code, making sure it is as perfect and - * correct as possible. Are you going to do the same after making your - * changes? Probably not. Besides, any change to these constants will break - * the runtime tests, which are extremely important for your security. - * You're literally millions of times more likely to screw up your own - * security by changing something here than you are to fall victim to an - * 128-bit key brute-force attack. You're also breaking your own - * compatibility with future updates to this library, so you'll be left - * vulnerable if we ever find a security bug and release a fix. - * - * So, PLEASE, do not change these constants. - * - * @param int $major - * @param int $minor - * @param ref $valid - * @return type - */ - protected static function getCoreVersionConfig($major, $minor, &$valid) - { - if ($major === 2) { - switch ($minor) { - case 0: - return [ - 'BLOCK_SIZE' => 16, - 'KEY_BYTE_SIZE' => 16, - 'SALT_SIZE' => 16, - 'HASH_FUNCTION' => 'sha256', - 'MAC_BYTE_SIZE' => 32, - 'ENCRYPTION_INFO' => 'DefusePHP|KeyForEncryption', - 'AUTHENTICATION_INFO' => 'DefusePHP|KeyForAuthentication' - ]; - default: - $valid |= 0xFF; - break; - } - } elseif ($major === 1) { - $valid |= 0xFF; // Set to a nonzero value to mark it as invalid - return [ - 'BLOCK_SIZE' => 16, - 'KEY_BYTE_SIZE' => 16, - 'SALT_SIZE' => null, - 'HASH_FUNCTION' => 'sha256', - 'MAC_BYTE_SIZE' => 32, - 'ENCRYPTION_INFO' => 'DefusePHP|KeyForEncryption', - 'AUTHENTICATION_INFO' => 'DefusePHP|KeyForAuthentication' - ]; - } - $valid |= 0xFF; // Set to a nonzero value to mark it as invalid - } } diff --git a/src/Crypto.php b/src/Crypto.php index 6d6cad4..9b3b3dd 100755 --- a/src/Crypto.php +++ b/src/Crypto.php @@ -3,6 +3,9 @@ use \Defuse\Crypto\Exception as Ex; +use \Defuse\Crypto\Core; +use \Defuse\Crypto\Encoding; + /* * PHP Encryption Library * Copyright (c) 2014-2015, Taylor Hornby @@ -30,13 +33,10 @@ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ -final class Crypto extends Core +final class Crypto { // Ciphertext format: [____VERSION____][____HMAC____][____IV____][____CIPHERTEXT____]. // Legacy format: [____HMAC____][____IV____][____CIPHERTEXT____]. - const CIPHER_METHOD = 'aes-128-cbc'; - - const LEGACY_VERSION = "\xD3\xF5\x01\x00"; /** * Use this to generate a random encryption key. @@ -45,8 +45,8 @@ final class Crypto extends Core */ public static function createNewRandomKey() { - $config = self::getVersionConfig(self::VERSION); - return self::secureRandom($config['KEY_BYTE_SIZE']); + $config = self::getVersionConfigFromHeader(Core::CURRENT_VERSION, Core::CURRENT_VERSION); + return Core::secureRandom($config['KEY_BYTE_SIZE']); } /** @@ -65,15 +65,15 @@ public static function createNewRandomKey() public static function encrypt($plaintext, $key, $raw_binary = false) { self::runtimeTest(); - $config = self::getVersionConfig(parent::VERSION); + $config = self::getVersionConfigFromHeader(Core::CURRENT_VERSION, Core::CURRENT_VERSION); - if (self::ourStrlen($key) !== $config['KEY_BYTE_SIZE']) { + if (Core::ourStrlen($key) !== $config['KEY_BYTE_SIZE']) { throw new Ex\CannotPerformOperationException("Key is the wrong size."); } - $salt = self::secureRandom($config['SALT_SIZE']); + $salt = Core::secureRandom($config['SALT_SIZE']); // Generate a sub-key for encryption. - $ekey = self::HKDF( + $ekey = Core::HKDF( $config['HASH_FUNCTION'], $key, $config['KEY_BYTE_SIZE'], @@ -83,7 +83,7 @@ public static function encrypt($plaintext, $key, $raw_binary = false) ); // Generate a sub-key for authentication and apply the HMAC. - $akey = self::HKDF( + $akey = Core::HKDF( $config['HASH_FUNCTION'], $key, $config['KEY_BYTE_SIZE'], @@ -93,25 +93,25 @@ public static function encrypt($plaintext, $key, $raw_binary = false) ); // Generate a random initialization vector. - self::ensureFunctionExists("openssl_cipher_iv_length"); + Core::ensureFunctionExists("openssl_cipher_iv_length"); $ivsize = \openssl_cipher_iv_length($config['CIPHER_METHOD']); if ($ivsize === false || $ivsize <= 0) { throw new Ex\CannotPerformOperationException( "Could not get the IV length from OpenSSL" ); } - $iv = self::secureRandom($ivsize); + $iv = Core::secureRandom($ivsize); $ciphertext = $salt . $iv . self::plainEncrypt($plaintext, $ekey, $iv, $config); - $auth = \hash_hmac($config['HASH_FUNCTION'], parent::VERSION . $ciphertext, $akey, true); + $auth = \hash_hmac($config['HASH_FUNCTION'], Core::CURRENT_VERSION . $ciphertext, $akey, true); // We're now appending the header as of 2.00 - $ciphertext = parent::VERSION . $auth . $ciphertext; + $ciphertext = Core::CURRENT_VERSION . $auth . $ciphertext; if ($raw_binary) { return $ciphertext; } - return self::binToHex($ciphertext); + return Encoding::binToHex($ciphertext); } /** @@ -131,25 +131,25 @@ public static function decrypt($ciphertext, $key, $raw_binary = false) { self::runtimeTest(); if (!$raw_binary) { - $ciphertext = self::hexToBin($ciphertext); + $ciphertext = Encoding::hexToBin($ciphertext); } // Grab the header tag - $version = self::ourSubstr($ciphertext, 0, parent::HEADER_VERSION_SIZE); + $version = Core::ourSubstr($ciphertext, 0, Core::HEADER_VERSION_SIZE); // Load the configuration for this version - $config = self::getVersionConfig($version); + $config = self::getVersionConfigFromHeader($version, Core::CURRENT_VERSION); // Now let's operate on the remainder of the ciphertext as normal - $ciphertext = self::ourSubstr($ciphertext, parent::HEADER_VERSION_SIZE, null); + $ciphertext = Core::ourSubstr($ciphertext, Core::HEADER_VERSION_SIZE, null); // Extract the HMAC from the front of the ciphertext. - if (self::ourStrlen($ciphertext) <= $config['MAC_BYTE_SIZE']) { + if (Core::ourStrlen($ciphertext) <= $config['MAC_BYTE_SIZE']) { throw new Ex\InvalidCiphertextException( "Ciphertext is too short." ); } - $hmac = self::ourSubstr( + $hmac = Core::ourSubstr( $ciphertext, 0, $config['MAC_BYTE_SIZE'] @@ -157,7 +157,7 @@ public static function decrypt($ciphertext, $key, $raw_binary = false) if ($hmac === false) { throw new Ex\CannotPerformOperationException(); } - $salt = self::ourSubstr( + $salt = Core::ourSubstr( $ciphertext, $config['MAC_BYTE_SIZE'], $config['SALT_SIZE'] @@ -166,7 +166,7 @@ public static function decrypt($ciphertext, $key, $raw_binary = false) throw new Ex\CannotPerformOperationException(); } - $ciphertext = self::ourSubstr( + $ciphertext = Core::ourSubstr( $ciphertext, $config['MAC_BYTE_SIZE'] + $config['SALT_SIZE'] ); @@ -175,30 +175,30 @@ public static function decrypt($ciphertext, $key, $raw_binary = false) } // Regenerate the same authentication sub-key. - $akey = self::HKDF($config['HASH_FUNCTION'], $key, $config['KEY_BYTE_SIZE'], $config['AUTHENTICATION_INFO'], $salt, $config); + $akey = Core::HKDF($config['HASH_FUNCTION'], $key, $config['KEY_BYTE_SIZE'], $config['AUTHENTICATION_INFO'], $salt, $config); - if (self::verifyHMAC($hmac, $version . $salt . $ciphertext, $akey)) { + if (self::verifyHMAC($hmac, $version . $salt . $ciphertext, $akey, $config)) { // Regenerate the same encryption sub-key. - $ekey = self::HKDF($config['HASH_FUNCTION'], $key, $config['KEY_BYTE_SIZE'], $config['ENCRYPTION_INFO'], $salt, $config); + $ekey = Core::HKDF($config['HASH_FUNCTION'], $key, $config['KEY_BYTE_SIZE'], $config['ENCRYPTION_INFO'], $salt, $config); // Extract the initialization vector from the ciphertext. - self::EnsureFunctionExists("openssl_cipher_iv_length"); + Core::EnsureFunctionExists("openssl_cipher_iv_length"); $ivsize = \openssl_cipher_iv_length($config['CIPHER_METHOD']); if ($ivsize === false || $ivsize <= 0) { throw new Ex\CannotPerformOperationException( "Could not get the IV length from OpenSSL" ); } - if (self::ourStrlen($ciphertext) <= $ivsize) { + if (Core::ourStrlen($ciphertext) <= $ivsize) { throw new Ex\InvalidCiphertextException( "Ciphertext is too short." ); } - $iv = self::ourSubstr($ciphertext, 0, $ivsize); + $iv = Core::ourSubstr($ciphertext, 0, $ivsize); if ($iv === false) { throw new Ex\CannotPerformOperationException(); } - $ciphertext = self::ourSubstr($ciphertext, $ivsize); + $ciphertext = Core::ourSubstr($ciphertext, $ivsize); if ($ciphertext === false) { throw new Ex\CannotPerformOperationException(); } @@ -234,25 +234,25 @@ public static function decrypt($ciphertext, $key, $raw_binary = false) public static function legacyDecrypt($ciphertext, $key) { self::runtimeTest(); - $config = self::getVersionConfig(self::LEGACY_VERSION); + $config = self::getVersionConfigFromHeader(Core::LEGACY_VERSION, Core::LEGACY_VERSION); // Extract the HMAC from the front of the ciphertext. - if (self::ourStrlen($ciphertext) <= $config['MAC_BYTE_SIZE']) { + if (Core::ourStrlen($ciphertext) <= $config['MAC_BYTE_SIZE']) { throw new Ex\InvalidCiphertextException( "Ciphertext is too short." ); } - $hmac = self::ourSubstr($ciphertext, 0, $config['MAC_BYTE_SIZE']); + $hmac = Core::ourSubstr($ciphertext, 0, $config['MAC_BYTE_SIZE']); if ($hmac === false) { throw new Ex\CannotPerformOperationException(); } - $ciphertext = self::ourSubstr($ciphertext, $config['MAC_BYTE_SIZE']); + $ciphertext = Core::ourSubstr($ciphertext, $config['MAC_BYTE_SIZE']); if ($ciphertext === false) { throw new Ex\CannotPerformOperationException(); } // Regenerate the same authentication sub-key. - $akey = self::HKDF( + $akey = Core::HKDF( $config['HASH_FUNCTION'], $key, $config['KEY_BYTE_SIZE'], @@ -261,9 +261,9 @@ public static function legacyDecrypt($ciphertext, $key) $config ); - if (self::verifyHMAC($hmac, $ciphertext, $akey)) { + if (self::verifyHMAC($hmac, $ciphertext, $akey, $config)) { // Regenerate the same encryption sub-key. - $ekey = self::HKDF( + $ekey = Core::HKDF( $config['HASH_FUNCTION'], $key, $config['KEY_BYTE_SIZE'], @@ -273,23 +273,23 @@ public static function legacyDecrypt($ciphertext, $key) ); // Extract the initialization vector from the ciphertext. - self::EnsureFunctionExists("openssl_cipher_iv_length"); + Core::EnsureFunctionExists("openssl_cipher_iv_length"); $ivsize = \openssl_cipher_iv_length($config['CIPHER_METHOD']); if ($ivsize === false || $ivsize <= 0) { throw new Ex\CannotPerformOperationException( "Could not get the IV length from OpenSSL" ); } - if (self::ourStrlen($ciphertext) <= $ivsize) { + if (Core::ourStrlen($ciphertext) <= $ivsize) { throw new Ex\InvalidCiphertextException( "Ciphertext is too short." ); } - $iv = self::ourSubstr($ciphertext, 0, $ivsize); + $iv = Core::ourSubstr($ciphertext, 0, $ivsize); if ($iv === false) { throw new Ex\CannotPerformOperationException(); } - $ciphertext = self::ourSubstr($ciphertext, $ivsize); + $ciphertext = Core::ourSubstr($ciphertext, $ivsize); if ($ciphertext === false) { throw new Ex\CannotPerformOperationException(); } @@ -323,7 +323,7 @@ public static function runtimeTest() // 3: Tests have failed. static $test_state = 0; - $config = self::getVersionConfig(parent::VERSION); + $config = self::getVersionConfigFromHeader(Core::CURRENT_VERSION, Core::CURRENT_VERSION); if ($test_state === 1 || $test_state === 2) { return; @@ -342,7 +342,7 @@ public static function runtimeTest() try { $test_state = 2; - self::ensureFunctionExists('openssl_get_cipher_methods'); + Core::ensureFunctionExists('openssl_get_cipher_methods'); if (\in_array($config['CIPHER_METHOD'], \openssl_get_cipher_methods()) === false) { throw new Ex\CryptoTestFailedException("Cipher method not supported."); } @@ -352,7 +352,7 @@ public static function runtimeTest() self::HKDFTestVector($config); self::testEncryptDecrypt($config); - if (self::ourStrlen(self::createNewRandomKey()) != $config['KEY_BYTE_SIZE']) { + if (Core::ourStrlen(self::createNewRandomKey()) != $config['KEY_BYTE_SIZE']) { throw new Ex\CryptoTestFailedException(); } @@ -383,8 +383,8 @@ public static function runtimeTest() */ private static function plainEncrypt($plaintext, $key, $iv, $config) { - self::ensureConstantExists("OPENSSL_RAW_DATA"); - self::ensureFunctionExists("openssl_encrypt"); + Core::ensureConstantExists("OPENSSL_RAW_DATA"); + Core::ensureFunctionExists("openssl_encrypt"); $ciphertext = \openssl_encrypt( $plaintext, $config['CIPHER_METHOD'], @@ -415,8 +415,8 @@ private static function plainEncrypt($plaintext, $key, $iv, $config) */ private static function plainDecrypt($ciphertext, $key, $iv, $config) { - self::ensureConstantExists("OPENSSL_RAW_DATA"); - self::ensureFunctionExists("openssl_decrypt"); + Core::ensureConstantExists("OPENSSL_RAW_DATA"); + Core::ensureFunctionExists("openssl_decrypt"); $plaintext = \openssl_decrypt( $ciphertext, $config['CIPHER_METHOD'], @@ -443,14 +443,10 @@ private static function plainDecrypt($ciphertext, $key, $iv, $config) * @return boolean * @throws Ex\CannotPerformOperationException */ - private static function verifyHMAC($correct_hmac, $message, $key) + private static function verifyHMAC($correct_hmac, $message, $key, $config) { - static $config = null; - if ($config === null) { - $config = self::getVersionConfig(parent::VERSION); - } $message_hmac = \hash_hmac($config['HASH_FUNCTION'], $message, $key, true); - return self::hashEquals($correct_hmac, $message_hmac); + return Core::hashEquals($correct_hmac, $message_hmac); } private static function testEncryptDecrypt($config) @@ -458,7 +454,7 @@ private static function testEncryptDecrypt($config) $key = self::createNewRandomKey(); $data = "EnCrYpT EvErYThInG\x00\x00"; if (empty($config)) { - $config = self::getVersionConfig(parent::VERSION); + $config = self::getVersionConfigFromHeader(Core::CURRENT_VERSION, Core::CURRENT_VERSION); } // Make sure encrypting then decrypting doesn't change the message. @@ -516,20 +512,20 @@ private static function HKDFTestVector($config) { // HKDF test vectors from RFC 5869 if (empty($config)) { - $config = self::getVersionConfig(parent::VERSION); + $config = self::getVersionConfigFromHeader(Core::CURRENT_VERSION, Core::CURRENT_VERSION); } // Test Case 1 $ikm = \str_repeat("\x0b", 22); - $salt = self::hexToBin("000102030405060708090a0b0c"); - $info = self::hexToBin("f0f1f2f3f4f5f6f7f8f9"); + $salt = Encoding::hexToBin("000102030405060708090a0b0c"); + $info = Encoding::hexToBin("f0f1f2f3f4f5f6f7f8f9"); $length = 42; - $okm = self::hexToBin( + $okm = Encoding::hexToBin( "3cb25f25faacd57a90434f64d0362f2a" . "2d2d0a90cf1a5a4c5db02d56ecc4c5bf" . "34007208d5b887185865" ); - $computed_okm = self::HKDF("sha256", $ikm, $length, $info, $salt, $config); + $computed_okm = Core::HKDF("sha256", $ikm, $length, $info, $salt, $config); if ($computed_okm !== $okm) { throw new Ex\CryptoTestFailedException(); } @@ -537,12 +533,12 @@ private static function HKDFTestVector($config) // Test Case 7 $ikm = \str_repeat("\x0c", 22); $length = 42; - $okm = self::hexToBin( + $okm = Encoding::hexToBin( "2c91117204d745f3500d636a62f64f0a" . "b3bae548aa53d423b0d1f27ebba6f5e5" . "673a081d70cce7acfc48" ); - $computed_okm = self::HKDF("sha1", $ikm, $length, '', null, $config); + $computed_okm = Core::HKDF("sha1", $ikm, $length, '', null, $config); if ($computed_okm !== $okm) { throw new Ex\CryptoTestFailedException(); } @@ -557,7 +553,7 @@ private static function HKDFTestVector($config) private static function HMACTestVector($config) { if (empty($config)) { - $config = self::getVersionConfig(parent::VERSION); + $config = self::getVersionConfigFromHeader(Core::CURRENT_VERSION, Core::CURRENT_VERSION); } // HMAC test vector From RFC 4231 (Test Case 1) $key = \str_repeat("\x0b", 20); @@ -576,22 +572,22 @@ private static function HMACTestVector($config) private static function AESTestVector($config) { // AES CTR mode test vector from NIST SP 800-38A - $key = self::hexToBin("2b7e151628aed2a6abf7158809cf4f3c"); - $iv = self::hexToBin("f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff"); - $plaintext = self::hexToBin( + $key = Encoding::hexToBin("2b7e151628aed2a6abf7158809cf4f3c"); + $iv = Encoding::hexToBin("f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff"); + $plaintext = Encoding::hexToBin( "6bc1bee22e409f96e93d7e117393172a" . "ae2d8a571e03ac9c9eb76fac45af8e51" . "30c81c46a35ce411e5fbc1191a0a52ef" . "f69f2445df4f9b17ad2b417be66c3710" ); - $ciphertext = self::hexToBin( + $ciphertext = Encoding::hexToBin( "874d6191b620e3261bef6864990db6ce" . "9806f66b7970fdff8617187bb9fffdff" . "5ae4df3edbd5d35e5b4f09020db03eab" . "1e031dda2fbe03d1792170a0f3009cee" ); - $config = self::getVersionConfig(parent::VERSION); + $config = self::getVersionConfigFromHeader(Core::CURRENT_VERSION, Core::CURRENT_VERSION); $computed_ciphertext = self::plainEncrypt($plaintext, $key, $iv, $config); if ($computed_ciphertext !== $ciphertext) { @@ -604,43 +600,69 @@ private static function AESTestVector($config) } } - /** - * Take a 4-byte header and get meaningful version information out of it. - * Common configuration options should go in Core.php - * - * @param string $header - */ - protected static function getVersionConfig($header) + private static function getVersionConfigFromHeader($header, $min_ver_header) { - $valid = 0; - $valid |= $header[0] ^ "\xDE"; - $valid |= $header[1] ^ "\xF5"; + if ($header[0] !== Core::HEADER_MAGIC[0] || $header[1] !== Core::HEADER_MAGIC[1]) { + throw new Ex\InvalidCiphertextException( + "Ciphertext has a bad magic number." + ); + } + $major = \ord($header[2]); $minor = \ord($header[3]); - if ($major === 1) { - return [ - 'CIPHER_METHOD' => 'aes-128-cbc', - 'KEY_BYTE_SIZE' => 16, - 'HASH_FUNCTION' => 'sha256', - 'MAC_BYTE_SIZE' => 32, - 'ENCRYPTION_INFO' => 'DefusePHP|KeyForEncryption', - 'AUTHENTICATION_INFO' => 'DefusePHP|KeyForAuthentication' - ]; + $min_major = \ord($min_ver_header[2]); + $min_minor = \ord($min_ver_header[3]); + + if ($major < $min_major || ($major === $min_major && $minor < $min_minor) ) { + throw new Ex\InvalidCiphertextException( + "Ciphertext is requesting an insecure fallback." + ); } - $config = parent::getCoreVersionConfig($major, $minor, $valid); + $config = self::getVersionConfigFromMajorMinor($major, $minor); + + return $config; + } + + private static function getVersionConfigFromMajorMinor($major, $minor) + { if ($major === 2) { switch ($minor) { case 0: - $config['CIPHER_METHOD'] = 'aes-128-ctr'; - break; + return [ + 'CIPHER_METHOD' => 'aes-128-ctr', + 'BLOCK_SIZE' => 16, + 'KEY_BYTE_SIZE' => 16, + 'SALT_SIZE' => 16, + 'HASH_FUNCTION' => 'sha256', + 'MAC_BYTE_SIZE' => 32, + 'ENCRYPTION_INFO' => 'DefusePHP|V2|KeyForEncryption', + 'AUTHENTICATION_INFO' => 'DefusePHP|V2|KeyForAuthentication' + ]; + default: + throw new Ex\InvalidCiphertextException( + "Unsupported ciphertext version." + ); + } + } elseif ($major === 1) { + switch ($minor) { + case 0: + return [ + 'CIPHER_METHOD' => 'aes-128-cbc', + 'BLOCK_SIZE' => 16, + 'KEY_BYTE_SIZE' => 16, + 'HASH_FUNCTION' => 'sha256', + 'MAC_BYTE_SIZE' => 32, + 'ENCRYPTION_INFO' => 'DefusePHP|KeyForEncryption', + 'AUTHENTICATION_INFO' => 'DefusePHP|KeyForAuthentication' + ]; + default: + throw new Ex\InvalidCiphertextException( + "Unsupported ciphertext version." + ); } } - - if ($valid !== 0) { - throw new Ex\InvalidCiphertextException('Unknown ciphertext version'); - } - return $config; } + } diff --git a/src/Encoding.php b/src/Encoding.php new file mode 100644 index 0000000..66655f2 --- /dev/null +++ b/src/Encoding.php @@ -0,0 +1,66 @@ +> 4; + $hex .= \chr(87 + $b + ((($b - 10) >> 8) & ~38)); + $hex .= \chr(87 + $c + ((($c - 10) >> 8) & ~38)); + } + return $hex; + } + + /** + * Convert a hexadecimal string into a binary string without cache-timing + * leaks + * + * @param string $hex_string + * @return string (raw binary) + */ + public static function hexToBin($hex_string) + { + $hex_pos = 0; + $bin = ''; + $hex_len = self::ourStrlen($hex_string); + $state = 0; + $c_acc = 0; + + while ($hex_pos < $hex_len) { + $c = \ord($hex_string[$hex_pos]); + $c_num = $c ^ 48; + $c_num0 = ($c_num - 10) >> 8; + $c_alpha = ($c & ~32) - 55; + $c_alpha0 = (($c_alpha - 10) ^ ($c_alpha - 16)) >> 8; + if (($c_num0 | $c_alpha0) === 0) { + throw new \RangeException( + 'Crypto::hexToBin() only expects hexadecimal characters' + ); + } + $c_val = ($c_num0 & $c_num) | ($c_alpha & $c_alpha0); + if ($state === 0) { + $c_acc = $c_val * 16; + } else { + $bin .= \chr($c_acc | $c_val); + } + $state = $state ? 0 : 1; + ++$hex_pos; + } + return $bin; + } + +} diff --git a/src/File.php b/src/File.php index 9199389..19bbde4 100644 --- a/src/File.php +++ b/src/File.php @@ -3,6 +3,8 @@ use \Defuse\Crypto\Exception as Ex; +use \Defuse\Crypto\Core; + /* * PHP Encryption Library * Copyright (c) 2014-2015, Taylor Hornby @@ -34,7 +36,7 @@ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ -final class File extends Core implements StreamInterface +final class File implements StreamInterface { /** * Use this to generate a random encryption key. @@ -43,8 +45,8 @@ final class File extends Core implements StreamInterface */ public static function createNewRandomKey() { - $config = self::getVersionConfig(self::VERSION); - return self::secureRandom($config['KEY_BYTE_SIZE']); + $config = self::getFileVersionConfigFromHeader(Core::CURRENT_FILE_VERSION); + return Core::secureRandom($config['KEY_BYTE_SIZE']); } /** @@ -198,7 +200,7 @@ public static function decryptFile($inputFilename, $outputFilename, $key) * @param string $key * @return boolean */ - public static function encryptResource($inputHandle, $outputHandle, $key) + private static function encryptResource($inputHandle, $outputHandle, $key) { // Because we don't have strict typing in PHP 5 if (!\is_resource($inputHandle)) { @@ -211,7 +213,7 @@ public static function encryptResource($inputHandle, $outputHandle, $key) 'Output handle must be a resource!' ); } - $config = self::getVersionConfig(parent::VERSION); + $config = self::getFileVersionConfigFromHeader(Core::CURRENT_FILE_VERSION); // Let's add this check before anything if (!\in_array($config['HASH_FUNCTION'], \hash_algos())) { @@ -221,7 +223,7 @@ public static function encryptResource($inputHandle, $outputHandle, $key) } // Sanity check; key must be the appropriate length! - if (self::ourStrlen($key) !== $config['KEY_BYTE_SIZE']) { + if (Core::ourStrlen($key) !== $config['KEY_BYTE_SIZE']) { throw new Ex\InvalidInput( 'Invalid key length. Keys should be '.$config['KEY_BYTE_SIZE'].' bytes long.' ); @@ -230,10 +232,10 @@ public static function encryptResource($inputHandle, $outputHandle, $key) /** * Let's split our keys */ - $file_salt = self::secureRandom($config['SALT_SIZE']); + $file_salt = Core::secureRandom($config['SALT_SIZE']); // $ekey -- Encryption Key -- used for AES - $ekey = self::HKDF( + $ekey = Core::HKDF( $config['HASH_FUNCTION'], $key, $config['KEY_BYTE_SIZE'], @@ -243,7 +245,7 @@ public static function encryptResource($inputHandle, $outputHandle, $key) ); // $akey -- Authentication Key -- used for HMAC - $akey = self::HKDF( + $akey = Core::HKDF( $config['HASH_FUNCTION'], $key, $config['KEY_BYTE_SIZE'], @@ -255,22 +257,22 @@ public static function encryptResource($inputHandle, $outputHandle, $key) /** * Generate a random initialization vector. */ - self::ensureFunctionExists("openssl_cipher_iv_length"); + Core::ensureFunctionExists("openssl_cipher_iv_length"); $ivsize = \openssl_cipher_iv_length($config['CIPHER_METHOD']); if ($ivsize === false || $ivsize <= 0) { throw new Ex\CannotPerformOperation( 'Improper IV size' ); } - $iv = self::secureRandom($ivsize); + $iv = Core::secureRandom($ivsize); /** * First let's write our header, file salt, and IV to the first N blocks of the output file */ if (\fwrite( $outputHandle, - parent::VERSION . $file_salt . $iv, - parent::HEADER_VERSION_SIZE + $config['SALT_SIZE'] + $ivsize + Core::CURRENT_FILE_VERSION . $file_salt . $iv, + Core::HEADER_VERSION_SIZE + $config['SALT_SIZE'] + $ivsize ) === false) { throw new Ex\CannotPerformOperation( 'Cannot write to output file' @@ -303,7 +305,7 @@ public static function encryptResource($inputHandle, $outputHandle, $key) /** * Let's MAC our salt and IV/nonce */ - \hash_update($hmac, parent::VERSION); + \hash_update($hmac, Core::CURRENT_FILE_VERSION); \hash_update($hmac, $file_salt); \hash_update($hmac, $iv); @@ -317,7 +319,7 @@ public static function encryptResource($inputHandle, $outputHandle, $key) 'Cannot read input file' ); } - $thisIv = self::incrementCounter($thisIv, $inc, $config); + $thisIv = Core::incrementCounter($thisIv, $inc, $config); /** * Perform the AES encryption. Encrypts the plaintext. @@ -341,7 +343,7 @@ public static function encryptResource($inputHandle, $outputHandle, $key) /** * Write the ciphertext to the output file */ - if (\fwrite($outputHandle, $encrypted, self::ourStrlen($encrypted)) === false) { + if (\fwrite($outputHandle, $encrypted, Core::ourStrlen($encrypted)) === false) { throw new Ex\CannotPerformOperation( 'Cannot write to output file during encryption' ); @@ -374,7 +376,7 @@ public static function encryptResource($inputHandle, $outputHandle, $key) * @param string $key * @return boolean */ - public static function decryptResource($inputHandle, $outputHandle, $key) + private static function decryptResource($inputHandle, $outputHandle, $key) { // Because we don't have strict typing in PHP 5 if (!\is_resource($inputHandle)) { @@ -393,10 +395,10 @@ public static function decryptResource($inputHandle, $outputHandle, $key) $remaining = 4; do { $header .= \fread($inputHandle, $remaining); - $remaining = 4 - self::ourStrlen($header); + $remaining = 4 - Core::ourStrlen($header); } while ($remaining > 0); - $config = self::getVersionConfig($header); + $config = self::getFileVersionConfigFromHeader($header); // Let's add this check before anything if (!\in_array($config['HASH_FUNCTION'], \hash_algos())) { @@ -406,7 +408,7 @@ public static function decryptResource($inputHandle, $outputHandle, $key) } // Sanity check; key must be the appropriate length! - if (self::ourStrlen($key) !== $config['KEY_BYTE_SIZE']) { + if (Core::ourStrlen($key) !== $config['KEY_BYTE_SIZE']) { throw new Ex\InvalidInput( 'Invalid key length. Keys should be '.$config['KEY_BYTE_SIZE'].' bytes long.' ); @@ -430,7 +432,7 @@ public static function decryptResource($inputHandle, $outputHandle, $key) * * $ekey -- Encryption Key -- used for AES */ - $ekey = self::HKDF( + $ekey = Core::HKDF( $config['HASH_FUNCTION'], $key, $config['KEY_BYTE_SIZE'], @@ -442,7 +444,7 @@ public static function decryptResource($inputHandle, $outputHandle, $key) /** * $akey -- Authentication Key -- used for HMAC */ - $akey = self::HKDF( + $akey = Core::HKDF( $config['HASH_FUNCTION'], $key, $config['KEY_BYTE_SIZE'], @@ -510,7 +512,7 @@ public static function decryptResource($inputHandle, $outputHandle, $key) /** * Reset file pointer to the beginning of the file after the header */ - if (\fseek($inputHandle, parent::HEADER_VERSION_SIZE, SEEK_SET) === false) { + if (\fseek($inputHandle, Core::HEADER_VERSION_SIZE, SEEK_SET) === false) { throw new Ex\CannotPerformOperation( 'Cannot read seek within input file' ); @@ -553,6 +555,8 @@ public static function decryptResource($inputHandle, $outputHandle, $key) */ if ($pos + $config['BUFFER'] >= $cipher_end) { $break = true; + echo "C end: " . $cipher_end . "\n"; + echo $pos . "\n"; $read = \fread($inputHandle, $cipher_end - $pos + 1); } else { $read = \fread($inputHandle, $config['BUFFER']); @@ -585,7 +589,7 @@ public static function decryptResource($inputHandle, $outputHandle, $key) /** * 3. Did we match? */ - if (!self::hashEquals($finalHMAC, $stored_mac)) { + if (!Core::hashEquals($finalHMAC, $stored_mac)) { throw new Ex\InvalidCiphertext( 'Message Authentication failure; tampering detected.' ); @@ -596,7 +600,7 @@ public static function decryptResource($inputHandle, $outputHandle, $key) /** * Return file pointer to the first non-header, non-IV byte in the file */ - if (\fseek($inputHandle, $config['SALT_SIZE'] + $ivsize + self::HEADER_VERSION_SIZE, SEEK_SET) === false) { + if (\fseek($inputHandle, $config['SALT_SIZE'] + $ivsize + Core::HEADER_VERSION_SIZE, SEEK_SET) === false) { throw new Ex\CannotPerformOperation( 'Could not move the input file pointer during decryption' ); @@ -656,13 +660,13 @@ public static function decryptResource($inputHandle, $outputHandle, $key) throw new Ex\InvalidCiphertext( 'File was modified after MAC verification' ); - } elseif (!self::hashEquals(\array_shift($macs), $calc)) { + } elseif (!Core::hashEquals(\array_shift($macs), $calc)) { throw new Ex\InvalidCiphertextException( 'File was modified after MAC verification' ); } - $thisIv = self::incrementCounter($thisIv, $inc, $config); + $thisIv = Core::incrementCounter($thisIv, $inc, $config); /** * Perform the AES decryption. Decrypts the message. @@ -690,7 +694,7 @@ public static function decryptResource($inputHandle, $outputHandle, $key) $result = \fwrite( $outputHandle, $decrypted, - self::ourStrlen($decrypted) + Core::ourStrlen($decrypted) ); /** @@ -711,32 +715,41 @@ public static function decryptResource($inputHandle, $outputHandle, $key) * * @param string $header */ - protected static function getVersionConfig($header) + private static function getFileVersionConfigFromHeader($header) { $valid = 0; - $valid |= $header[0] ^ "\xDE"; - $valid |= $header[1] ^ "\xF5"; + $valid |= ord($header[0]) ^ ord(Core::CURRENT_FILE_VERSION[0]); + $valid |= ord($header[1]) ^ ord(Core::CURRENT_FILE_VERSION[1]); $major = \ord($header[2]); $minor = \ord($header[3]); - $config = parent::getCoreVersionConfig($major, $minor, $valid); + $config = self::getFileVersionConfigFromMajorMinor($major, $minor, $valid); + if ($valid !== 0) { + throw new Ex\InvalidCiphertextException('Unknown ciphertext version'); + } + return $config; + } + private static function getFileVersionConfigFromMajorMinor($major, $minor, &$valid) + { if ($major === 2) { switch ($minor) { - case 0: - $config['CIPHER_METHOD'] = 'aes-128-ctr'; - $config['BUFFER'] = 1048576; - break; - default: - $valid |= 0xFF; - break; + case 0: + return [ + 'CIPHER_METHOD' => 'aes-128-ctr', + 'BLOCK_SIZE' => 16, + 'KEY_BYTE_SIZE' => 16, + 'SALT_SIZE' => 16, + 'HASH_FUNCTION' => 'sha256', + 'MAC_BYTE_SIZE' => 32, + 'ENCRYPTION_INFO' => 'DefusePHP|V2File|KeyForEncryption', + 'AUTHENTICATION_INFO' => 'DefusePHP|V2File|KeyForAuthentication', + 'BUFFER' => 1048576 + ]; + default: + $valid |= 0xFF; } } else { $valid |= 0xFF; } - - if ($valid !== 0) { - throw new Ex\InvalidCiphertextException('Unknown ciphertext version'); - } - return $config; } } diff --git a/tests/encode.php b/tests/encode.php index 67f0f06..56fe9b7 100644 --- a/tests/encode.php +++ b/tests/encode.php @@ -1,12 +1,12 @@ Date: Fri, 16 Oct 2015 16:03:45 -0600 Subject: [PATCH 03/35] Fix silly mistakes to make the tests pass. --- src/Encoding.php | 5 +++-- src/File.php | 4 ++-- tests/legacy.php | 4 ++-- tests/stream/decrypt.php | 2 +- tests/stream/encrypt.php | 2 +- tests/stream/keygen.php | 2 +- 6 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/Encoding.php b/src/Encoding.php index 66655f2..4095844 100644 --- a/src/Encoding.php +++ b/src/Encoding.php @@ -2,6 +2,7 @@ namespace Defuse\Crypto; use \Defuse\Crypto\Exception as Ex; +use \Defuse\Crypto\Core; final class Encoding { @@ -15,7 +16,7 @@ final class Encoding { public static function binToHex($bin_string) { $hex = ''; - $len = self::ourStrlen($bin_string); + $len = Core::ourStrlen($bin_string); for ($i = 0; $i < $len; ++$i) { $c = \ord($bin_string[$i]) & 0xf; $b = \ord($bin_string[$i]) >> 4; @@ -36,7 +37,7 @@ public static function hexToBin($hex_string) { $hex_pos = 0; $bin = ''; - $hex_len = self::ourStrlen($hex_string); + $hex_len = Core::ourStrlen($hex_string); $state = 0; $c_acc = 0; diff --git a/src/File.php b/src/File.php index 19bbde4..dbe0b37 100644 --- a/src/File.php +++ b/src/File.php @@ -200,7 +200,7 @@ public static function decryptFile($inputFilename, $outputFilename, $key) * @param string $key * @return boolean */ - private static function encryptResource($inputHandle, $outputHandle, $key) + public static function encryptResource($inputHandle, $outputHandle, $key) { // Because we don't have strict typing in PHP 5 if (!\is_resource($inputHandle)) { @@ -376,7 +376,7 @@ private static function encryptResource($inputHandle, $outputHandle, $key) * @param string $key * @return boolean */ - private static function decryptResource($inputHandle, $outputHandle, $key) + public static function decryptResource($inputHandle, $outputHandle, $key) { // Because we don't have strict typing in PHP 5 if (!\is_resource($inputHandle)) { diff --git a/tests/legacy.php b/tests/legacy.php index dec3e8e..a962f45 100644 --- a/tests/legacy.php +++ b/tests/legacy.php @@ -2,9 +2,9 @@ require_once \dirname(__DIR__).'/autoload.php'; use \Defuse\Crypto\Crypto; -use \Defuse\Crypto\Core; +use \Defuse\Crypto\Encoding; -$cipher = Core::hexToBin('cfdad83ebd506d2c9ada8d48030d0bca2ff94760e6d39c186adb1290d6c47e35821e262673c5631c42ebbaf70774d6ef29aa5eee0e412d646ae380e08189c85d024b5e2009106870f1db25d8b85fd01f'); +$cipher = Encoding::hexToBin('cfdad83ebd506d2c9ada8d48030d0bca2ff94760e6d39c186adb1290d6c47e35821e262673c5631c42ebbaf70774d6ef29aa5eee0e412d646ae380e08189c85d024b5e2009106870f1db25d8b85fd01f'); $plain = Crypto::legacyDecrypt($cipher, "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F"); if ($plain !== 'This is a test message') { diff --git a/tests/stream/decrypt.php b/tests/stream/decrypt.php index ebb2d5a..b9c6a4e 100644 --- a/tests/stream/decrypt.php +++ b/tests/stream/decrypt.php @@ -8,7 +8,7 @@ $mem = 0; $start_time = $end_time = \microtime(true); -$key = \Defuse\Crypto\Core::hexToBin(\file_get_contents('key.txt')); +$key = \Defuse\Crypto\Encoding::hexToBin(\file_get_contents('key.txt')); echo 'Decrypting', "\n", str_repeat('-', 50), "\n\n"; echo "Load Key:\n\t"; diff --git a/tests/stream/encrypt.php b/tests/stream/encrypt.php index 2b80ede..467b994 100644 --- a/tests/stream/encrypt.php +++ b/tests/stream/encrypt.php @@ -8,7 +8,7 @@ $mem = 0; $start_time = $end_time = \microtime(true); -$key = \Defuse\Crypto\Core::hexToBin(\file_get_contents('key.txt')); +$key = \Defuse\Crypto\Encoding::hexToBin(\file_get_contents('key.txt')); echo 'Encrypting', "\n", str_repeat('-', 50), "\n\n"; echo "Load Key:\n\t"; diff --git a/tests/stream/keygen.php b/tests/stream/keygen.php index 28a3ca3..4a7a413 100644 --- a/tests/stream/keygen.php +++ b/tests/stream/keygen.php @@ -1,6 +1,6 @@ Date: Fri, 16 Oct 2015 16:05:03 -0600 Subject: [PATCH 04/35] Remove debug echos I accidentally left in. --- src/File.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/File.php b/src/File.php index dbe0b37..b51b816 100644 --- a/src/File.php +++ b/src/File.php @@ -555,8 +555,6 @@ public static function decryptResource($inputHandle, $outputHandle, $key) */ if ($pos + $config['BUFFER'] >= $cipher_end) { $break = true; - echo "C end: " . $cipher_end . "\n"; - echo $pos . "\n"; $read = \fread($inputHandle, $cipher_end - $pos + 1); } else { $read = \fread($inputHandle, $config['BUFFER']); From 0202401971113648068d969b344b7873f0f33361 Mon Sep 17 00:00:00 2001 From: Taylor Hornby Date: Fri, 16 Oct 2015 16:05:40 -0600 Subject: [PATCH 05/35] Add backwards compatibility attack prevention to File. --- src/File.php | 50 +++++++++++++++++++++++++++++++++++++------------- 1 file changed, 37 insertions(+), 13 deletions(-) diff --git a/src/File.php b/src/File.php index b51b816..863c87a 100644 --- a/src/File.php +++ b/src/File.php @@ -45,7 +45,10 @@ final class File implements StreamInterface */ public static function createNewRandomKey() { - $config = self::getFileVersionConfigFromHeader(Core::CURRENT_FILE_VERSION); + $config = self::getFileVersionConfigFromHeader( + Core::CURRENT_FILE_VERSION, + Core::CURRENT_FILE_VERSION + ); return Core::secureRandom($config['KEY_BYTE_SIZE']); } @@ -213,7 +216,10 @@ public static function encryptResource($inputHandle, $outputHandle, $key) 'Output handle must be a resource!' ); } - $config = self::getFileVersionConfigFromHeader(Core::CURRENT_FILE_VERSION); + $config = self::getFileVersionConfigFromHeader( + Core::CURRENT_FILE_VERSION, + Core::CURRENT_FILE_VERSION + ); // Let's add this check before anything if (!\in_array($config['HASH_FUNCTION'], \hash_algos())) { @@ -398,7 +404,10 @@ public static function decryptResource($inputHandle, $outputHandle, $key) $remaining = 4 - Core::ourStrlen($header); } while ($remaining > 0); - $config = self::getFileVersionConfigFromHeader($header); + $config = self::getFileVersionConfigFromHeader( + $header, + Core::CURRENT_FILE_VERSION + ); // Let's add this check before anything if (!\in_array($config['HASH_FUNCTION'], \hash_algos())) { @@ -713,21 +722,32 @@ public static function decryptResource($inputHandle, $outputHandle, $key) * * @param string $header */ - private static function getFileVersionConfigFromHeader($header) + private static function getFileVersionConfigFromHeader($header, $min_ver_header) { - $valid = 0; - $valid |= ord($header[0]) ^ ord(Core::CURRENT_FILE_VERSION[0]); - $valid |= ord($header[1]) ^ ord(Core::CURRENT_FILE_VERSION[1]); + if ($header[0] !== Core::CURRENT_FILE_VERSION[0] || $header[1] !== Core::CURRENT_FILE_VERSION[1]) { + throw new Ex\InvalidCiphertextException( + "Ciphertext file has a bad magic number." + ); + } + $major = \ord($header[2]); $minor = \ord($header[3]); - $config = self::getFileVersionConfigFromMajorMinor($major, $minor, $valid); - if ($valid !== 0) { - throw new Ex\InvalidCiphertextException('Unknown ciphertext version'); + + $min_major = \ord($min_ver_header[2]); + $min_minor = \ord($min_ver_header[3]); + + if ($major < $min_major || ($major === $min_major && $minor < $min_minor)) { + throw new Ex\InvalidCiphertextException( + "Ciphertext is requesting an insecure fallback." + ); } + + $config = self::getFileVersionConfigFromMajorMinor($major, $minor); + return $config; } - private static function getFileVersionConfigFromMajorMinor($major, $minor, &$valid) + private static function getFileVersionConfigFromMajorMinor($major, $minor) { if ($major === 2) { switch ($minor) { @@ -744,10 +764,14 @@ private static function getFileVersionConfigFromMajorMinor($major, $minor, &$val 'BUFFER' => 1048576 ]; default: - $valid |= 0xFF; + throw new Ex\InvalidCiphertextException( + "Unsupported file ciphertext version." + ); } } else { - $valid |= 0xFF; + throw new Ex\InvalidCiphertextException( + "Unsupported file ciphertext version." + ); } } } From 3d84dbc73602e0d7826b0d59f7996a3a2f374d29 Mon Sep 17 00:00:00 2001 From: Taylor Hornby Date: Fri, 16 Oct 2015 16:15:25 -0600 Subject: [PATCH 06/35] Move the runtime tests into their own file. --- src/Crypto.php | 230 ++----------------------------------------- src/RuntimeTests.php | 228 ++++++++++++++++++++++++++++++++++++++++++ tests/runtime.php | 2 +- 3 files changed, 239 insertions(+), 221 deletions(-) create mode 100644 src/RuntimeTests.php diff --git a/src/Crypto.php b/src/Crypto.php index 9b3b3dd..fdd0d62 100755 --- a/src/Crypto.php +++ b/src/Crypto.php @@ -5,6 +5,7 @@ use \Defuse\Crypto\Core; use \Defuse\Crypto\Encoding; +use \Defuse\Crypto\RuntimeTests; /* * PHP Encryption Library @@ -33,7 +34,7 @@ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ -final class Crypto +class Crypto { // Ciphertext format: [____VERSION____][____HMAC____][____IV____][____CIPHERTEXT____]. // Legacy format: [____HMAC____][____IV____][____CIPHERTEXT____]. @@ -64,7 +65,7 @@ public static function createNewRandomKey() */ public static function encrypt($plaintext, $key, $raw_binary = false) { - self::runtimeTest(); + RuntimeTests::runtimeTest(); $config = self::getVersionConfigFromHeader(Core::CURRENT_VERSION, Core::CURRENT_VERSION); if (Core::ourStrlen($key) !== $config['KEY_BYTE_SIZE']) { @@ -129,7 +130,7 @@ public static function encrypt($plaintext, $key, $raw_binary = false) */ public static function decrypt($ciphertext, $key, $raw_binary = false) { - self::runtimeTest(); + RuntimeTests::runtimeTest(); if (!$raw_binary) { $ciphertext = Encoding::hexToBin($ciphertext); } @@ -233,7 +234,7 @@ public static function decrypt($ciphertext, $key, $raw_binary = false) */ public static function legacyDecrypt($ciphertext, $key) { - self::runtimeTest(); + RuntimeTests::runtimeTest(); $config = self::getVersionConfigFromHeader(Core::LEGACY_VERSION, Core::LEGACY_VERSION); // Extract the HMAC from the front of the ciphertext. @@ -309,66 +310,6 @@ public static function legacyDecrypt($ciphertext, $key) } } - /* - * Runs tests. - * Raises CannotPerformOperationExceptionException or CryptoTestFailedExceptionException if - * one of the tests fail. If any tests fails, your system is not capable of - * performing encryption, so make sure you fail safe in that case. - */ - public static function runtimeTest() - { - // 0: Tests haven't been run yet. - // 1: Tests have passed. - // 2: Tests are running right now. - // 3: Tests have failed. - static $test_state = 0; - - $config = self::getVersionConfigFromHeader(Core::CURRENT_VERSION, Core::CURRENT_VERSION); - - if ($test_state === 1 || $test_state === 2) { - return; - } - - if ($test_state === 3) { - /* If an intermittent problem caused a test to fail previously, we - * want that to be indicated to the user with every call to this - * library. This way, if the user first does something they really - * don't care about, and just ignores all exceptions, they won't get - * screwed when they then start to use the library for something - * they do care about. */ - throw new Ex\CryptoTestFailedException("Tests failed previously."); - } - - try { - $test_state = 2; - - Core::ensureFunctionExists('openssl_get_cipher_methods'); - if (\in_array($config['CIPHER_METHOD'], \openssl_get_cipher_methods()) === false) { - throw new Ex\CryptoTestFailedException("Cipher method not supported."); - } - - self::AESTestVector($config); - self::HMACTestVector($config); - self::HKDFTestVector($config); - - self::testEncryptDecrypt($config); - if (Core::ourStrlen(self::createNewRandomKey()) != $config['KEY_BYTE_SIZE']) { - throw new Ex\CryptoTestFailedException(); - } - - if ($config['ENCRYPTION_INFO'] == $config['AUTHENTICATION_INFO']) { - throw new Ex\CryptoTestFailedException(); - } - } catch (Ex\CryptoTestFailedException $ex) { - // Do this, otherwise it will stay in the "tests are running" state. - $test_state = 3; - throw $ex; - } - - // Change this to '0' make the tests always re-run (for benchmarking). - $test_state = 1; - } - /** * Never call this method directly! * @@ -381,7 +322,7 @@ public static function runtimeTest() * @return string * @throws Ex\CannotPerformOperationException */ - private static function plainEncrypt($plaintext, $key, $iv, $config) + protected static function plainEncrypt($plaintext, $key, $iv, $config) { Core::ensureConstantExists("OPENSSL_RAW_DATA"); Core::ensureFunctionExists("openssl_encrypt"); @@ -413,7 +354,7 @@ private static function plainEncrypt($plaintext, $key, $iv, $config) * @return string * @throws Ex\CannotPerformOperationException */ - private static function plainDecrypt($ciphertext, $key, $iv, $config) + protected static function plainDecrypt($ciphertext, $key, $iv, $config) { Core::ensureConstantExists("OPENSSL_RAW_DATA"); Core::ensureFunctionExists("openssl_decrypt"); @@ -443,164 +384,13 @@ private static function plainDecrypt($ciphertext, $key, $iv, $config) * @return boolean * @throws Ex\CannotPerformOperationException */ - private static function verifyHMAC($correct_hmac, $message, $key, $config) + protected static function verifyHMAC($correct_hmac, $message, $key, $config) { $message_hmac = \hash_hmac($config['HASH_FUNCTION'], $message, $key, true); return Core::hashEquals($correct_hmac, $message_hmac); } - private static function testEncryptDecrypt($config) - { - $key = self::createNewRandomKey(); - $data = "EnCrYpT EvErYThInG\x00\x00"; - if (empty($config)) { - $config = self::getVersionConfigFromHeader(Core::CURRENT_VERSION, Core::CURRENT_VERSION); - } - - // Make sure encrypting then decrypting doesn't change the message. - $ciphertext = self::encrypt($data, $key, true); - try { - $decrypted = self::decrypt($ciphertext, $key, true); - } catch (Ex\InvalidCiphertextException $ex) { - // It's important to catch this and change it into a - // CryptoTestFailedExceptionException, otherwise a test failure could trick - // the user into thinking it's just an invalid ciphertext! - throw new Ex\CryptoTestFailedException(); - } - if ($decrypted !== $data) { - throw new Ex\CryptoTestFailedException(); - } - - // Modifying the ciphertext: Appending a string. - try { - self::decrypt($ciphertext . "a", $key, true); - throw new Ex\CryptoTestFailedException(); - } catch (Ex\InvalidCiphertextException $e) { /* expected */ } - - // Modifying the ciphertext: Changing an IV byte. - try { - $ciphertext[4] = chr((ord($ciphertext[4]) + 1) % 256); - self::decrypt($ciphertext, $key, true); - throw new Ex\CryptoTestFailedException(); - } catch (Ex\InvalidCiphertextException $e) { /* expected */ } - - // Decrypting with the wrong key. - $key = self::createNewRandomKey(); - $data = "abcdef"; - $ciphertext = self::encrypt($data, $key, true); - $wrong_key = self::createNewRandomKey(); - try { - self::decrypt($ciphertext, $wrong_key, true); - throw new Ex\CryptoTestFailedException(); - } catch (Ex\InvalidCiphertextException $e) { /* expected */ } - - // Ciphertext too small (shorter than HMAC). - $key = self::createNewRandomKey(); - $ciphertext = \str_repeat("A", $config['MAC_BYTE_SIZE'] - 1); - try { - self::decrypt($ciphertext, $key, true); - throw new Ex\CryptoTestFailedException(); - } catch (Ex\InvalidCiphertextException $e) { /* expected */ } - } - - /** - * Run-time testing - * - * @throws Ex\CryptoTestFailedException - */ - private static function HKDFTestVector($config) - { - // HKDF test vectors from RFC 5869 - if (empty($config)) { - $config = self::getVersionConfigFromHeader(Core::CURRENT_VERSION, Core::CURRENT_VERSION); - } - - // Test Case 1 - $ikm = \str_repeat("\x0b", 22); - $salt = Encoding::hexToBin("000102030405060708090a0b0c"); - $info = Encoding::hexToBin("f0f1f2f3f4f5f6f7f8f9"); - $length = 42; - $okm = Encoding::hexToBin( - "3cb25f25faacd57a90434f64d0362f2a" . - "2d2d0a90cf1a5a4c5db02d56ecc4c5bf" . - "34007208d5b887185865" - ); - $computed_okm = Core::HKDF("sha256", $ikm, $length, $info, $salt, $config); - if ($computed_okm !== $okm) { - throw new Ex\CryptoTestFailedException(); - } - - // Test Case 7 - $ikm = \str_repeat("\x0c", 22); - $length = 42; - $okm = Encoding::hexToBin( - "2c91117204d745f3500d636a62f64f0a" . - "b3bae548aa53d423b0d1f27ebba6f5e5" . - "673a081d70cce7acfc48" - ); - $computed_okm = Core::HKDF("sha1", $ikm, $length, '', null, $config); - if ($computed_okm !== $okm) { - throw new Ex\CryptoTestFailedException(); - } - - } - - /** - * Run-Time tests - * - * @throws Ex\CryptoTestFailedException - */ - private static function HMACTestVector($config) - { - if (empty($config)) { - $config = self::getVersionConfigFromHeader(Core::CURRENT_VERSION, Core::CURRENT_VERSION); - } - // HMAC test vector From RFC 4231 (Test Case 1) - $key = \str_repeat("\x0b", 20); - $data = "Hi There"; - $correct = "b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7"; - if (\hash_hmac($config['HASH_FUNCTION'], $data, $key) !== $correct) { - throw new Ex\CryptoTestFailedException(); - } - } - - /** - * Run-time tests - * - * @throws Ex\CryptoTestFailedException - */ - private static function AESTestVector($config) - { - // AES CTR mode test vector from NIST SP 800-38A - $key = Encoding::hexToBin("2b7e151628aed2a6abf7158809cf4f3c"); - $iv = Encoding::hexToBin("f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff"); - $plaintext = Encoding::hexToBin( - "6bc1bee22e409f96e93d7e117393172a" . - "ae2d8a571e03ac9c9eb76fac45af8e51" . - "30c81c46a35ce411e5fbc1191a0a52ef" . - "f69f2445df4f9b17ad2b417be66c3710" - ); - $ciphertext = Encoding::hexToBin( - "874d6191b620e3261bef6864990db6ce" . - "9806f66b7970fdff8617187bb9fffdff" . - "5ae4df3edbd5d35e5b4f09020db03eab" . - "1e031dda2fbe03d1792170a0f3009cee" - ); - - $config = self::getVersionConfigFromHeader(Core::CURRENT_VERSION, Core::CURRENT_VERSION); - - $computed_ciphertext = self::plainEncrypt($plaintext, $key, $iv, $config); - if ($computed_ciphertext !== $ciphertext) { - throw new Ex\CryptoTestFailedException(); - } - - $computed_plaintext = self::plainDecrypt($ciphertext, $key, $iv, $config); - if ($computed_plaintext !== $plaintext) { - throw new Ex\CryptoTestFailedException(); - } - } - - private static function getVersionConfigFromHeader($header, $min_ver_header) + protected static function getVersionConfigFromHeader($header, $min_ver_header) { if ($header[0] !== Core::HEADER_MAGIC[0] || $header[1] !== Core::HEADER_MAGIC[1]) { throw new Ex\InvalidCiphertextException( @@ -625,7 +415,7 @@ private static function getVersionConfigFromHeader($header, $min_ver_header) return $config; } - private static function getVersionConfigFromMajorMinor($major, $minor) + protected static function getVersionConfigFromMajorMinor($major, $minor) { if ($major === 2) { switch ($minor) { diff --git a/src/RuntimeTests.php b/src/RuntimeTests.php new file mode 100644 index 0000000..a1df3c1 --- /dev/null +++ b/src/RuntimeTests.php @@ -0,0 +1,228 @@ + Date: Fri, 16 Oct 2015 16:20:07 -0600 Subject: [PATCH 07/35] Fix documentation of get*Config() methods. --- src/Crypto.php | 15 +++++++++++++++ src/File.php | 14 ++++++++++++-- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/src/Crypto.php b/src/Crypto.php index fdd0d62..6ee1f3a 100755 --- a/src/Crypto.php +++ b/src/Crypto.php @@ -390,6 +390,14 @@ protected static function verifyHMAC($correct_hmac, $message, $key, $config) return Core::hashEquals($correct_hmac, $message_hmac); } + /** + * Get the encryption configuration based on the version in a header. + * + * @param string $header The header to read the version number from. + * @param string $min_ver_header The header of the minimum version number allowed. + * @return array + * @throws Ex\InvalidCiphertextException + */ protected static function getVersionConfigFromHeader($header, $min_ver_header) { if ($header[0] !== Core::HEADER_MAGIC[0] || $header[1] !== Core::HEADER_MAGIC[1]) { @@ -415,6 +423,13 @@ protected static function getVersionConfigFromHeader($header, $min_ver_header) return $config; } + /** + * + * @param int $major The major version number. + * @param int $minor The minor version number. + * @return array + * @throws Ex\InvalidCiphertextException + */ protected static function getVersionConfigFromMajorMinor($major, $minor) { if ($major === 2) { diff --git a/src/File.php b/src/File.php index 863c87a..dc9c68b 100644 --- a/src/File.php +++ b/src/File.php @@ -718,9 +718,12 @@ public static function decryptResource($inputHandle, $outputHandle, $key) } /** - * Take a 4-byte header and get meaningful version information out of it + * Get the encryption configuration based on the version in a header. * - * @param string $header + * @param string $header The header to read the version number from. + * @param string $min_ver_header The header of the minimum version number allowed. + * @return array + * @throws Ex\InvalidCiphertextException */ private static function getFileVersionConfigFromHeader($header, $min_ver_header) { @@ -747,6 +750,13 @@ private static function getFileVersionConfigFromHeader($header, $min_ver_header) return $config; } + /** + * + * @param int $major The major version number. + * @param int $minor The minor version number. + * @return array + * @throws Ex\InvalidCiphertextException + */ private static function getFileVersionConfigFromMajorMinor($major, $minor) { if ($major === 2) { From aa6a5d41f5b856bbe49fe178dda9e862a949a73d Mon Sep 17 00:00:00 2001 From: Taylor Hornby Date: Fri, 16 Oct 2015 16:25:20 -0600 Subject: [PATCH 08/35] Fix method documentation. --- src/Crypto.php | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/Crypto.php b/src/Crypto.php index 6ee1f3a..0753b4f 100755 --- a/src/Crypto.php +++ b/src/Crypto.php @@ -55,13 +55,14 @@ public static function createNewRandomKey() * * $plaintext is the message to encrypt. * $key is the encryption key, a value generated by CreateNewRandomKey(). - * You MUST catch exceptions thrown by this function. See docs above. + * You MUST catch exceptions thrown by this function. Read the docs. * * @param string $plaintext * @param string $key * @param boolean $raw_binary * @return string * @throws Ex\CannotPerformOperationException + * @throws Ex\CryptoTestFailedException */ public static function encrypt($plaintext, $key, $raw_binary = false) { @@ -119,13 +120,14 @@ public static function encrypt($plaintext, $key, $raw_binary = false) * Decrypts a ciphertext. * $ciphertext is the ciphertext to decrypt. * $key is the key that the ciphertext was encrypted with. - * You MUST catch exceptions thrown by this function. See docs above. + * You MUST catch exceptions thrown by this function. Read the docs. * * @param string $ciphertext * @param string $key * @param boolean $raw_binary - * @return type + * @return string * @throws Ex\CannotPerformOperationException + * @throws Ex\CryptoTestFailedException * @throws Ex\InvalidCiphertextException */ public static function decrypt($ciphertext, $key, $raw_binary = false) @@ -224,12 +226,13 @@ public static function decrypt($ciphertext, $key, $raw_binary = false) * * $ciphertext is the ciphertext to decrypt. * $key is the key that the ciphertext was encrypted with. - * You MUST catch exceptions thrown by this function. See docs above. + * You MUST catch exceptions thrown by this function. Read the docs. * * @param string $ciphertext * @param string $key - * @return type + * @return string * @throws Ex\CannotPerformOperationException + * @throws Ex\CryptoTestFailedException * @throws Ex\InvalidCiphertextException */ public static function legacyDecrypt($ciphertext, $key) @@ -311,7 +314,7 @@ public static function legacyDecrypt($ciphertext, $key) } /** - * Never call this method directly! + * You MUST NOT call this method directly. * * Unauthenticated message encryption. * @@ -344,13 +347,14 @@ protected static function plainEncrypt($plaintext, $key, $iv, $config) } /** - * Never call this method directly! + * You MUST NOT call this method directly. * * Unauthenticated message deryption. * * @param string $ciphertext * @param string $key * @param string $iv + * @param array $config * @return string * @throws Ex\CannotPerformOperationException */ @@ -375,9 +379,8 @@ protected static function plainDecrypt($ciphertext, $key, $iv, $config) } /** - * Verify a HMAC without crypto side-channels + * Verify an HMAC without timing/cache side-channel leakage. * - * @staticvar boolean $native Use native hash_equals()? * @param string $correct_hmac HMAC string (raw binary) * @param string $message Ciphertext (raw binary) * @param string $key Authentication key (raw binary) From 907441140e885b7985c3a6244bc7960f8fed73bf Mon Sep 17 00:00:00 2001 From: Taylor Hornby Date: Fri, 16 Oct 2015 17:12:01 -0600 Subject: [PATCH 09/35] SECURITY CRITICAL: Fix CTR mode nonce incrementor. --- src/Core.php | 15 +++----- test/phpunit.sh | 72 +++++++++++++++++++++++++++++++++++ test/unit/RuntimeTestTest.php | 11 ++++++ tests/encode.php | 38 ------------------ tests/runtime.php | 30 --------------- 5 files changed, 88 insertions(+), 78 deletions(-) create mode 100755 test/phpunit.sh create mode 100644 test/unit/RuntimeTestTest.php delete mode 100644 tests/encode.php delete mode 100644 tests/runtime.php diff --git a/src/Core.php b/src/Core.php index 970df2b..16a65eb 100644 --- a/src/Core.php +++ b/src/Core.php @@ -28,21 +28,16 @@ public static function incrementCounter($ctr, $inc, &$config) $ivsize = \openssl_cipher_iv_length($config['CIPHER_METHOD']); } + // XXX: check the range and type of $ctr, $inc + /** * We start at the rightmost byte (big-endian) * So, too, does OpenSSL: http://stackoverflow.com/a/3146214/2224584 */ - for ($i = $ivsize - 1; $i >= 0; --$i) { - $c = \ord($ctr[$i]); - - $ctr[$i] = \chr(($c + $inc) & 0xFF); - if (($c + $inc) <= 255) { - // We don't need to keep incrementing to the left unless we exceed 255 - break; - } - $inc = ($inc >> 8) & ~0; - ++$inc; + $sum = \ord($ctr[$i]) + $inc; + $ctr[$i] = \chr($sum & 0xFF); + $inc = $sum >> 8; } return $ctr; } diff --git a/test/phpunit.sh b/test/phpunit.sh new file mode 100755 index 0000000..5fa1e05 --- /dev/null +++ b/test/phpunit.sh @@ -0,0 +1,72 @@ +#!/usr/bin/env bash + +origdir=`pwd` +cdir=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) +cd $origdir +parentdir="$(dirname $cdir)" + +clean=0 # Clean up? +gpg --fingerprint D8406D0D82947747293778314AA394086372C20A +if [ $? -ne 0 ]; then + echo -e "\033[33mDownloading PGP Public Key...\033[0m" + gpg --keyserver pgp.mit.edu --recv-keys D8406D0D82947747293778314AA394086372C20A + # Sebastian Bergmann + gpg --fingerprint D8406D0D82947747293778314AA394086372C20A + if [ $? -ne 0 ]; then + echo -e "\033[31mCould not download PGP public key for verification\033[0m" + exit 1 + fi +fi + +if [ "$clean" -eq 1 ]; then + # Let's clean them up, if they exist + if [ -f phpunit.phar ]; then + rm -f phpunit.phar + fi + if [ -f phpunit.phar.asc ]; then + rm -f phpunit.phar.asc + fi +fi + +# Let's grab the latest release and its signature +if [ ! -f phpunit.phar ]; then + wget https://phar.phpunit.de/phpunit.phar + if [ $? -ne 0 ]; then + echo "wget phpunit.phar was unsuccessful" + exit 1 + fi +fi +if [ ! -f phpunit.phar.asc ]; then + wget https://phar.phpunit.de/phpunit.phar.asc + if [ $? -ne 0 ]; then + echo "wget phpunit.phar.asc was unsuccessful" + exit 1 + fi +fi + +# What are the major/minor versions? +# php -r "var_dump([\Sodium\library_version_major(), \Sodium\library_version_minor()]);" + +# Verify before running +gpg --verify phpunit.phar.asc phpunit.phar +if [ $? -eq 0 ]; then + echo + echo -e "\033[33mBegin Unit Testing\033[0m" + # Run the testing suite + php phpunit.phar --bootstrap "$parentdir/autoload.php" "$parentdir/test/unit" + EXITCODE=$? + # Cleanup + if [ "$clean" -eq 1 ]; then + echo -e "\033[32mCleaning Up!\033[0m" + rm -f phpunit.phar + rm -f phpunit.phar.asc + fi + exit $EXITCODE +else + echo + chmod -x phpunit.phar + mv phpunit.phar /tmp/bad-phpunit.phar + mv phpunit.phar.asc /tmp/bad-phpunit.phar.asc + echo -e "\033[31mSignature did not match! Check /tmp/bad-phpunit.phar for trojans\033[0m" + exit 1 +fi diff --git a/test/unit/RuntimeTestTest.php b/test/unit/RuntimeTestTest.php new file mode 100644 index 0000000..7790323 --- /dev/null +++ b/test/unit/RuntimeTestTest.php @@ -0,0 +1,11 @@ + Date: Fri, 16 Oct 2015 18:17:06 -0600 Subject: [PATCH 10/35] Make incrementCounter more robust, improve tests. --- src/Core.php | 31 ++++- test/unit/CtrModeTest.php | 230 +++++++++++++++++++++++++++++++++++++ test/unit/EncodingTest.php | 27 +++++ tests/ctr.php | 46 -------- 4 files changed, 287 insertions(+), 47 deletions(-) create mode 100644 test/unit/CtrModeTest.php create mode 100644 test/unit/EncodingTest.php delete mode 100644 tests/ctr.php diff --git a/src/Core.php b/src/Core.php index 16a65eb..31aecd6 100644 --- a/src/Core.php +++ b/src/Core.php @@ -26,9 +26,30 @@ public static function incrementCounter($ctr, $inc, &$config) static $ivsize = null; if ($ivsize === null) { $ivsize = \openssl_cipher_iv_length($config['CIPHER_METHOD']); + if ($ivsize === false) { + throw new Ex\CannotPerformOperationException( + "Problem obtaining the correct nonce length." + ); + } + } + + if (strlen($ctr) !== $ivsize) { + throw new Ex\CannotPerformOperationException( + "Trying to increment a nonce of the wrong size." + ); } - // XXX: check the range and type of $ctr, $inc + if (!is_int($inc)) { + throw new Ex\CannotPerformOperationException( + "Trying to increment nonce by a non-integer." + ); + } + + if ($inc < 0) { + throw new Ex\CannotPerformOperationException( + "Trying to increment nonce by a negative amount." + ); + } /** * We start at the rightmost byte (big-endian) @@ -36,6 +57,14 @@ public static function incrementCounter($ctr, $inc, &$config) */ for ($i = $ivsize - 1; $i >= 0; --$i) { $sum = \ord($ctr[$i]) + $inc; + + /* Detect integer overflow and fail. */ + if (!is_int($sum)) { + throw new Ex\CannotPerformOperationException( + "Integer overflow in CTR mode nonce increment." + ); + } + $ctr[$i] = \chr($sum & 0xFF); $inc = $sum >> 8; } diff --git a/test/unit/CtrModeTest.php b/test/unit/CtrModeTest.php new file mode 100644 index 0000000..4b92267 --- /dev/null +++ b/test/unit/CtrModeTest.php @@ -0,0 +1,230 @@ + 'aes-128-ctr', + ]; + + $actual_end = \Defuse\Crypto\Core::incrementCounter(\hex2bin($start), $inc, $config); + $this->assertEquals( + $end, + \bin2hex($actual_end), + $start . " + " . $inc + ); + } + + public function testFuzzIncrementCounter() + { + $config = [ + 'CIPHER_METHOD' => 'aes-128-ctr', + ]; + + /* Test carry propagation. */ + for ($offset = 0; $offset < 16; $offset++) { + /* + * If we start with... + * FF FF FF FF FE FF FF ... FF + * ^- offset + * + * And add 1, we should get... + * + * FF FF FF FF FF 00 00 ... 00 + ^- offset + */ + $start = str_repeat("\xFF", $offset) . "\xFE" . str_repeat("\xFF", 16 - $offset - 1); + $expected_end = str_repeat("\xFF", $offset + 1) . str_repeat("\x00", 16 - $offset - 1); + $actual_end = \Defuse\Crypto\Core::incrementCounter($start, 1, $config); + $this->assertEquals( + \bin2hex($expected_end), + \bin2hex($actual_end), + \bin2hex($start) . " + " . 1 + ); + } + + /* Try using it to add random 24-bit integers, and check the result. */ + for ($trial = 0; $trial < 1000; $trial++) { + $rand_a = mt_rand() & 0x00ffffff; + $rand_b = mt_rand() & 0x00ffffff; + + $prefix = openssl_random_pseudo_bytes(12); + + $start = $prefix . + chr(($rand_a >> 24) & 0xff) . + chr(($rand_a >> 16) & 0xff) . + chr(($rand_a >> 8) & 0xff) . + chr(($rand_a >> 0) & 0xff); + + $sum = $rand_a + $rand_b; + + $expected_end = $prefix . + chr(($sum >> 24) & 0xff) . + chr(($sum >> 16) & 0xff) . + chr(($sum >> 8) & 0xff) . + chr(($sum >> 0) & 0xff); + $actual_end = \Defuse\Crypto\Core::incrementCounter($start, $rand_b, $config); + + $this->assertEquals( + \bin2hex($expected_end), + \bin2hex($actual_end), + \bin2hex($start) . " + " . $rand_b + ); + } + } + + /** + * @expectedException \Defuse\Crypto\Exception\CannotPerformOperationException + */ + public function testIncrementByNegativeValue() + { + $config = [ + 'CIPHER_METHOD' => 'aes-128-ctr', + ]; + + \Defuse\Crypto\Core::incrementCounter( + str_repeat("\x00", 16), + -1, + $config + ); + } + + + public function allNonZeroByteValuesProvider() + { + $all_bytes = array(); + for ($i = 1; $i <= 0xff; $i++) { + $all_bytes[] = array($i); + } + return $all_bytes; + } + + /** + * @dataProvider allNonZeroByteValuesProvider + * @expectedException \Defuse\Crypto\Exception\CannotPerformOperationException + */ + public function testIncrementCausingOverflowInFirstByte($lsb) + { + $config = [ + 'CIPHER_METHOD' => 'aes-128-ctr', + ]; + /* Smallest value that will overflow. */ + $increment = (PHP_INT_MAX - $lsb) + 1; + $start = str_repeat("\x00", 15) . chr($lsb); + \Defuse\Crypto\Core::incrementCounter($start, $increment, $config); + } + + /** + * @expectedException \Defuse\Crypto\Exception\CannotPerformOperationException + */ + public function testIncrementWithShortIvLength() + { + $config = [ + 'CIPHER_METHOD' => 'aes-128-ctr', + ]; + \Defuse\Crypto\Core::incrementCounter( + str_repeat("\x00", 15), + 1, + $config + ); + } + + /** + * @expectedException \Defuse\Crypto\Exception\CannotPerformOperationException + */ + public function testIncrementWithLongIvLength() + { + $config = [ + 'CIPHER_METHOD' => 'aes-128-ctr', + ]; + \Defuse\Crypto\Core::incrementCounter( + str_repeat("\x00", 17), + 1, + $config + ); + } + + /** + * @expectedException \Defuse\Crypto\Exception\CannotPerformOperationException + */ + public function testIncrementByNonInteger() + { + $config = [ + 'CIPHER_METHOD' => 'aes-128-ctr', + ]; + \Defuse\Crypto\Core::incrementCounter( + str_repeat("\x00", 16), + 1.0, + $config + ); + } +} diff --git a/test/unit/EncodingTest.php b/test/unit/EncodingTest.php new file mode 100644 index 0000000..6ed7015 --- /dev/null +++ b/test/unit/EncodingTest.php @@ -0,0 +1,27 @@ +assertEquals($encode_b, $encode_a); + + $decode_a = Encoding::hexToBin($encode_a); + $decode_b = \hex2bin($encode_b); + + $this->assertEquals($decode_b, $decode_a); + // Just in case. + $this->assertEquals($random, $decode_b); + } + } + } +} diff --git a/tests/ctr.php b/tests/ctr.php deleted file mode 100644 index ad17beb..0000000 --- a/tests/ctr.php +++ /dev/null @@ -1,46 +0,0 @@ - 16, - 'KEY_BYTE_SIZE' => 16, - 'HASH_FUNCTION' => 'sha256', - 'MAC_BYTE_SIZE' => 32, - 'ENCRYPTION_INFO' => 'DefusePHP|KeyForEncryption', - 'AUTHENTICATION_INFO' => 'DefusePHP|KeyForAuthentication', - 'CIPHER_METHOD' => 'aes-128-ctr', - 'BUFFER' => 1048576 -]; - -$ctr = [ - str_repeat("\0", \openssl_cipher_iv_length('aes-128-ctr')), - str_repeat("\0", \openssl_cipher_iv_length('aes-128-ctr') - 2) . "\x00\x40" -]; -$test = \Defuse\Crypto\Core::incrementCounter($ctr[0], 64, $config); -if ($test !== $ctr[1]) { - echo "Counter mode malfunction\n"; - exit(255); -} -$a = str_repeat('a', 2048); - -/** - * Let's verify that our counter is behaving properly - */ -$cipher = openssl_encrypt($a, $config['CIPHER_METHOD'], 'YELLOW SUBMARINE', OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING, $ctr[0]); -$slice = mb_substr($cipher, 1024, 1024, '8bit'); -$decrypt = openssl_decrypt($slice, $config['CIPHER_METHOD'], 'YELLOW SUBMARINE', OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING, $test); - -if (!preg_match('/^a{1024}$/', $decrypt)) { - echo "Counter mode calculation error\n"; - exit(255); -} - -/** - * We should carry - */ -$start = str_repeat("\xFF", \openssl_cipher_iv_length('aes-128-ctr')); -$end = \Defuse\Crypto\Core::incrementCounter($start, 1, $config); -if ($end !== $ctr[0]) { - echo "Carry error\n"; - exit(255); -} \ No newline at end of file From 5551dba7cf029ebba2915e017f26d9573a514365 Mon Sep 17 00:00:00 2001 From: Taylor Hornby Date: Fri, 16 Oct 2015 18:31:29 -0600 Subject: [PATCH 11/35] Move legacy test to PHPUnit. --- src/Encoding.php | 2 +- test/unit/LegacyDecryptTest.php | 45 +++++++++++++++++++++++++++++++++ tests/legacy.php | 12 --------- 3 files changed, 46 insertions(+), 13 deletions(-) create mode 100644 test/unit/LegacyDecryptTest.php delete mode 100644 tests/legacy.php diff --git a/src/Encoding.php b/src/Encoding.php index 4095844..3e4617a 100644 --- a/src/Encoding.php +++ b/src/Encoding.php @@ -49,7 +49,7 @@ public static function hexToBin($hex_string) $c_alpha0 = (($c_alpha - 10) ^ ($c_alpha - 16)) >> 8; if (($c_num0 | $c_alpha0) === 0) { throw new \RangeException( - 'Crypto::hexToBin() only expects hexadecimal characters' + 'Encoding::hexToBin() only expects hexadecimal characters' ); } $c_val = ($c_num0 & $c_num) | ($c_alpha & $c_alpha0); diff --git a/test/unit/LegacyDecryptTest.php b/test/unit/LegacyDecryptTest.php new file mode 100644 index 0000000..cea37cf --- /dev/null +++ b/test/unit/LegacyDecryptTest.php @@ -0,0 +1,45 @@ +assertEquals($plain, 'This is a test message'); + } + + /** + * @expectedException \Defuse\Crypto\Exception\InvalidCiphertextException + */ + function testDecryptLegacyCiphertextWrongKey() + { + $cipher = Encoding::hexToBin( + 'cfdad83ebd506d2c9ada8d48030d0bca'. + '2ff94760e6d39c186adb1290d6c47e35'. + '821e262673c5631c42ebbaf70774d6ef'. + '29aa5eee0e412d646ae380e08189c85d'. + '024b5e2009106870f1db25d8b85fd01f' + ); + + $plain = Crypto::legacyDecrypt( + $cipher, + "\x01\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F" + // ^- I changed that byte + ); + } +} diff --git a/tests/legacy.php b/tests/legacy.php deleted file mode 100644 index a962f45..0000000 --- a/tests/legacy.php +++ /dev/null @@ -1,12 +0,0 @@ - Date: Fri, 16 Oct 2015 18:33:31 -0600 Subject: [PATCH 12/35] Remove the empty output test (no longer checking accurately) --- tests/empty.php | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 tests/empty.php diff --git a/tests/empty.php b/tests/empty.php deleted file mode 100644 index a7feabe..0000000 --- a/tests/empty.php +++ /dev/null @@ -1,4 +0,0 @@ - Date: Fri, 16 Oct 2015 18:40:14 -0600 Subject: [PATCH 13/35] Fix bug uncovered by testing with mbstring.func_overload=7 --- src/Core.php | 2 +- {tests => test}/stream/decrypt.php | 0 {tests => test}/stream/encrypt.php | 0 {tests => test}/stream/error.php | 0 {tests => test}/stream/get_large.sh | 0 {tests => test}/stream/keygen.php | 0 {tests => test}/stream/large.jpg | Bin {tests => test}/stream/wat-gigantic-duck.jpg | Bin 8 files changed, 1 insertion(+), 1 deletion(-) rename {tests => test}/stream/decrypt.php (100%) rename {tests => test}/stream/encrypt.php (100%) rename {tests => test}/stream/error.php (100%) rename {tests => test}/stream/get_large.sh (100%) rename {tests => test}/stream/keygen.php (100%) rename {tests => test}/stream/large.jpg (100%) rename {tests => test}/stream/wat-gigantic-duck.jpg (100%) diff --git a/src/Core.php b/src/Core.php index 31aecd6..caaccf4 100644 --- a/src/Core.php +++ b/src/Core.php @@ -33,7 +33,7 @@ public static function incrementCounter($ctr, $inc, &$config) } } - if (strlen($ctr) !== $ivsize) { + if (self::ourStrlen($ctr) !== $ivsize) { throw new Ex\CannotPerformOperationException( "Trying to increment a nonce of the wrong size." ); diff --git a/tests/stream/decrypt.php b/test/stream/decrypt.php similarity index 100% rename from tests/stream/decrypt.php rename to test/stream/decrypt.php diff --git a/tests/stream/encrypt.php b/test/stream/encrypt.php similarity index 100% rename from tests/stream/encrypt.php rename to test/stream/encrypt.php diff --git a/tests/stream/error.php b/test/stream/error.php similarity index 100% rename from tests/stream/error.php rename to test/stream/error.php diff --git a/tests/stream/get_large.sh b/test/stream/get_large.sh similarity index 100% rename from tests/stream/get_large.sh rename to test/stream/get_large.sh diff --git a/tests/stream/keygen.php b/test/stream/keygen.php similarity index 100% rename from tests/stream/keygen.php rename to test/stream/keygen.php diff --git a/tests/stream/large.jpg b/test/stream/large.jpg similarity index 100% rename from tests/stream/large.jpg rename to test/stream/large.jpg diff --git a/tests/stream/wat-gigantic-duck.jpg b/test/stream/wat-gigantic-duck.jpg similarity index 100% rename from tests/stream/wat-gigantic-duck.jpg rename to test/stream/wat-gigantic-duck.jpg From 5c15b101c9bc538a9a06c3ee5ab5d431db6a5415 Mon Sep 17 00:00:00 2001 From: Taylor Hornby Date: Fri, 16 Oct 2015 18:41:57 -0600 Subject: [PATCH 14/35] Run tests properly. --- test.sh | 80 +++---------------------------------------------- test/phpunit.sh | 6 ++-- 2 files changed, 8 insertions(+), 78 deletions(-) diff --git a/test.sh b/test.sh index fd436c5..1a55aa7 100755 --- a/test.sh +++ b/test.sh @@ -1,82 +1,10 @@ #!/bin/bash - -echo "Normal" -echo "--------------------------------------------------" -php -d mbstring.func_overload=0 tests/runtime.php -if [ $? -ne 0 ]; then - echo "FAIL." - exit 1 -fi -echo "--------------------------------------------------" - -echo "" - -echo "Multibyte" -echo "--------------------------------------------------" -php -d mbstring.func_overload=7 tests/runtime.php -if [ $? -ne 0 ]; then - echo "FAIL." - exit 1 -fi -echo "--------------------------------------------------" - -echo "" - -if [ -z "$(php tests/empty.php)" ]; then - echo "PASS: Crypto.php output is empty." -else - echo "FAIL: Crypto.php output is not empty." - exit 1 -fi - -echo "--------------------------------------------------" - -echo "" - -echo "Hex Encoding" -echo "--------------------------------------------------" -php tests/encode.php -if [ $? -ne 0 ]; then - echo "FAIL." - exit 1 -else - echo "PASS: Hex encoding is working correctly" -fi - -echo "--------------------------------------------------" - -echo "" - -echo "Legacy Decryption" -echo "--------------------------------------------------" -php tests/legacy.php -if [ $? -ne 0 ]; then - echo "FAIL." - exit 1 -else - echo "PASS: Legacy decryption is working correctly" -fi - -echo "--------------------------------------------------" - -echo "" - -echo "AES COUNTER MODE" -echo "--------------------------------------------------" -php tests/ctr.php -if [ $? -ne 0 ]; then - echo "FAIL." - exit 1 -else - echo "PASS: Counter incrementing is working correctly" -fi - -echo "--------------------------------------------------" - +set -e +./test/phpunit.sh echo "" ORIGDIR=`pwd` -cd tests/stream +cd test/stream php keygen.php php encrypt.php php decrypt.php -cd $ORIGDIR \ No newline at end of file +cd $ORIGDIR diff --git a/test/phpunit.sh b/test/phpunit.sh index 5fa1e05..08ebd6d 100755 --- a/test/phpunit.sh +++ b/test/phpunit.sh @@ -52,8 +52,10 @@ gpg --verify phpunit.phar.asc phpunit.phar if [ $? -eq 0 ]; then echo echo -e "\033[33mBegin Unit Testing\033[0m" - # Run the testing suite - php phpunit.phar --bootstrap "$parentdir/autoload.php" "$parentdir/test/unit" + # Run the test suite with normal func_overload. + php -d mbstring.func_overload=0 phpunit.phar --bootstrap "$parentdir/autoload.php" "$parentdir/test/unit" && \ + # Run the test suite again with funky func_overload. + php -d mbstring.func_overload=7 phpunit.phar --bootstrap "$parentdir/autoload.php" "$parentdir/test/unit" EXITCODE=$? # Cleanup if [ "$clean" -eq 1 ]; then From bbcc1aee6e62239f76f0c7ee0d6ee258ec18977b Mon Sep 17 00:00:00 2001 From: Taylor Hornby Date: Fri, 16 Oct 2015 18:43:13 -0600 Subject: [PATCH 15/35] Move annoying files into an 'other/' directory. --- benchmark.php => other/benchmark.php | 0 example.php => other/example.php | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename benchmark.php => other/benchmark.php (100%) rename example.php => other/example.php (100%) diff --git a/benchmark.php b/other/benchmark.php similarity index 100% rename from benchmark.php rename to other/benchmark.php diff --git a/example.php b/other/example.php similarity index 100% rename from example.php rename to other/example.php From ee6abfd868b89381abd4cca54efb7903a2bb3bb0 Mon Sep 17 00:00:00 2001 From: Taylor Hornby Date: Fri, 16 Oct 2015 18:52:01 -0600 Subject: [PATCH 16/35] Put proper credit to Scott in phpunit.sh --- test/phpunit.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/phpunit.sh b/test/phpunit.sh index 08ebd6d..3e405fb 100755 --- a/test/phpunit.sh +++ b/test/phpunit.sh @@ -1,5 +1,8 @@ #!/usr/bin/env bash +# This was written by Scott Arciszewski. I copied it from his Halite project: +# https://github.com/paragonie/halite + origdir=`pwd` cdir=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) cd $origdir From 7fca4aef7b9d2f54263d5a7a922b1920079bab4a Mon Sep 17 00:00:00 2001 From: Taylor Hornby Date: Fri, 16 Oct 2015 19:04:54 -0600 Subject: [PATCH 17/35] Make all exceptions use consistent names and namespacing. --- doc/02-Crypto.php.md | 22 +++++++------- doc/03-File.php.md | 22 +++++++------- src/Core.php | 2 +- src/File.php | 72 ++++++++++++++++++++++---------------------- src/RuntimeTests.php | 4 +-- 5 files changed, 61 insertions(+), 61 deletions(-) diff --git a/doc/02-Crypto.php.md b/doc/02-Crypto.php.md index 79ddeb1..9dbab09 100644 --- a/doc/02-Crypto.php.md +++ b/doc/02-Crypto.php.md @@ -37,9 +37,9 @@ try { // \Defuse\Crypto\Crypto\binToHex() // \Defuse\Crypto\Crypto\hexToBin() // -} catch (\Defuse\Crypto\Exception\CryptoTestFailed $ex) { +} catch (\Defuse\Crypto\Exception\CryptoTestFailedException $ex) { die('Cannot safely create a key'); -} catch (\Defuse\Crypto\Exception\CannotPerformOperation $ex) { +} catch (\Defuse\Crypto\Exception\CannotPerformOperationException $ex) { die('Cannot safely create a key'); } ``` @@ -51,9 +51,9 @@ Encrypting a Message $message = 'ATTACK AT DAWN'; try { $ciphertext = \Defuse\Crypto\Crypto::Encrypt($message, $key); -} catch (\Defuse\Crypto\Exception\CryptoTestFailed $ex) { +} catch (\Defuse\Crypto\Exception\CryptoTestFailedException $ex) { die('Cannot safely perform encryption'); -} catch (\Defuse\Crypto\Exception\CannotPerformOperation $ex) { +} catch (\Defuse\Crypto\Exception\CannotPerformOperationException $ex) { die('Cannot safely perform encryption'); } ``` @@ -64,16 +64,16 @@ Decrypting a Message ```php try { $decrypted = self::Decrypt($ciphertext, $key); -} catch (\Defuse\Crypto\Exception\InvalidCiphertext $ex) { // VERY IMPORTANT +} catch (\Defuse\Crypto\Exception\InvalidCiphertextException $ex) { // VERY IMPORTANT // Either: // 1. The ciphertext was modified by the attacker, // 2. The key is wrong, or // 3. $ciphertext is not a valid ciphertext or was corrupted. // Assume the worst. die('DANGER! DANGER! The ciphertext has been tampered with!'); -} catch (CryptoTestFailedException $ex) { +} catch (\Defuse\Crypto\Exception\CryptoTestFailedException $ex) { die('Cannot safely perform decryption'); -} catch (CannotPerformOperationException $ex) { +} catch (\Defuse\Crypto\Exception\CannotPerformOperationException $ex) { die('Cannot safely perform decryption'); } ``` @@ -86,16 +86,16 @@ Decrypting a Message Encrypted with version 1 of this Library ```php try { $decrypted = self::legacyDecrypt($ciphertext, $key); -} catch (\Defuse\Crypto\Exception\InvalidCiphertext $ex) { // VERY IMPORTANT +} catch (\Defuse\Crypto\Exception\InvalidCiphertextException $ex) { // VERY IMPORTANT // Either: // 1. The ciphertext was modified by the attacker, // 2. The key is wrong, or // 3. $ciphertext is not a valid ciphertext or was corrupted. // Assume the worst. die('DANGER! DANGER! The ciphertext has been tampered with!'); -} catch (CryptoTestFailedException $ex) { +} catch (\Defuse\Crypto\Exception\CryptoTestFailedException $ex) { die('Cannot safely perform decryption'); -} catch (CannotPerformOperationException $ex) { +} catch (\Defuse\Crypto\Exception\CannotPerformOperationException $ex) { die('Cannot safely perform decryption'); } -``` \ No newline at end of file +``` diff --git a/doc/03-File.php.md b/doc/03-File.php.md index a502fe0..e4b02d5 100644 --- a/doc/03-File.php.md +++ b/doc/03-File.php.md @@ -37,9 +37,9 @@ try { // \Defuse\Crypto\File\binToHex() // \Defuse\Crypto\File\hexToBin() // -} catch (\Defuse\Crypto\Exception\CryptoTestFailed $ex) { +} catch (\Defuse\Crypto\Exception\CryptoTestFailedException $ex) { die('Cannot safely create a key'); -} catch (\Defuse\Crypto\Exception\CannotPerformOperation $ex) { +} catch (\Defuse\Crypto\Exception\CannotPerformOperationException $ex) { die('Cannot safely create a key'); } ``` @@ -62,9 +62,9 @@ try { $outputFilename, $key ); -} catch (\Defuse\Crypto\Exception\CryptoTestFailed $ex) { +} catch (\Defuse\Crypto\Exception\CryptoTestFailedException $ex) { die('Cannot safely perform encryption'); -} catch (\Defuse\Crypto\Exception\CannotPerformOperation $ex) { +} catch (\Defuse\Crypto\Exception\CannotPerformOperationException $ex) { die('Cannot safely perform encryption'); } ``` @@ -87,9 +87,9 @@ try { $outputFilename, $key ); -} catch (\Defuse\Crypto\Exception\CryptoTestFailed $ex) { +} catch (\Defuse\Crypto\Exception\CryptoTestFailedException $ex) { die('Cannot safely perform decryption'); -} catch (\Defuse\Crypto\Exception\CannotPerformOperation $ex) { +} catch (\Defuse\Crypto\Exception\CannotPerformOperationException $ex) { die('Cannot safely perform decryption'); } ``` @@ -108,9 +108,9 @@ $oFile = \fopen('image2.enc.jpg', 'wb'); try { \Defuse\Crypto\File::encryptResource($iFile, $oFile, $key); -} catch (\Defuse\Crypto\Exception\CryptoTestFailed $ex) { +} catch (\Defuse\Crypto\Exception\CryptoTestFailedException $ex) { die('Cannot safely perform encryption'); -} catch (\Defuse\Crypto\Exception\CannotPerformOperation $ex) { +} catch (\Defuse\Crypto\Exception\CannotPerformOperationException $ex) { die('Cannot safely perform encryption'); } ``` @@ -129,9 +129,9 @@ $oFile = \fopen('image2.dec.jpg', 'wb'); try { \Defuse\Crypto\File::decryptResource($iFile, $oFile, $key); -} catch (\Defuse\Crypto\Exception\CryptoTestFailed $ex) { +} catch (\Defuse\Crypto\Exception\CryptoTestFailedException $ex) { die('Cannot safely perform decryption'); -} catch (\Defuse\Crypto\Exception\CannotPerformOperation $ex) { +} catch (\Defuse\Crypto\Exception\CannotPerformOperationException $ex) { die('Cannot safely perform decryption'); } -``` \ No newline at end of file +``` diff --git a/src/Core.php b/src/Core.php index caaccf4..0bc706c 100644 --- a/src/Core.php +++ b/src/Core.php @@ -164,7 +164,7 @@ public static function HKDF($hash, $ikm, $length, $info = '', $salt = null, $con * @param string $expected string (raw binary) * @param string $given string (raw binary) * @return boolean - * @throws Ex\CannotPerformOperation + * @throws Ex\CannotPerformOperationException */ public static function hashEquals($expected, $given) { diff --git a/src/File.php b/src/File.php index dc9c68b..ba23982 100644 --- a/src/File.php +++ b/src/File.php @@ -81,7 +81,7 @@ public static function encryptFile($inputFilename, $outputFilename, $key) */ $if = \fopen($inputFilename, 'rb'); if ($if === false) { - throw new Ex\CannotPerformOperation( + throw new Ex\CannotPerformOperationException( 'Cannot open input file for encrypting' ); } @@ -92,7 +92,7 @@ public static function encryptFile($inputFilename, $outputFilename, $key) */ $of = \fopen($outputFilename, 'wb'); if ($of === false) { - throw new Ex\CannotPerformOperation( + throw new Ex\CannotPerformOperationException( 'Cannot open output file for encrypting' ); } @@ -107,12 +107,12 @@ public static function encryptFile($inputFilename, $outputFilename, $key) * Close handles */ if (\fclose($if) === false) { - throw new Ex\CannotPerformOperation( + throw new Ex\CannotPerformOperationException( 'Cannot close input file for encrypting' ); } if (\fclose($of) === false) { - throw new Ex\CannotPerformOperation( + throw new Ex\CannotPerformOperationException( 'Cannot close input file for encrypting' ); } @@ -152,7 +152,7 @@ public static function decryptFile($inputFilename, $outputFilename, $key) */ $if = \fopen($inputFilename, 'rb'); if ($if === false) { - throw new Ex\CannotPerformOperation( + throw new Ex\CannotPerformOperationException( 'Cannot open input file for decrypting' ); } @@ -163,7 +163,7 @@ public static function decryptFile($inputFilename, $outputFilename, $key) */ $of = \fopen($outputFilename, 'wb'); if ($of === false) { - throw new Ex\CannotPerformOperation( + throw new Ex\CannotPerformOperationException( 'Cannot open output file for decrypting' ); } @@ -178,12 +178,12 @@ public static function decryptFile($inputFilename, $outputFilename, $key) * Close handles */ if (\fclose($if) === false) { - throw new Ex\CannotPerformOperation( + throw new Ex\CannotPerformOperationException( 'Cannot close input file for decrypting' ); } if (\fclose($of) === false) { - throw new Ex\CannotPerformOperation( + throw new Ex\CannotPerformOperationException( 'Cannot close input file for decrypting' ); } @@ -223,7 +223,7 @@ public static function encryptResource($inputHandle, $outputHandle, $key) // Let's add this check before anything if (!\in_array($config['HASH_FUNCTION'], \hash_algos())) { - throw new Ex\CannotPerformOperation( + throw new Ex\CannotPerformOperationException( 'The specified hash function does not exist' ); } @@ -266,7 +266,7 @@ public static function encryptResource($inputHandle, $outputHandle, $key) Core::ensureFunctionExists("openssl_cipher_iv_length"); $ivsize = \openssl_cipher_iv_length($config['CIPHER_METHOD']); if ($ivsize === false || $ivsize <= 0) { - throw new Ex\CannotPerformOperation( + throw new Ex\CannotPerformOperationException( 'Improper IV size' ); } @@ -280,7 +280,7 @@ public static function encryptResource($inputHandle, $outputHandle, $key) Core::CURRENT_FILE_VERSION . $file_salt . $iv, Core::HEADER_VERSION_SIZE + $config['SALT_SIZE'] + $ivsize ) === false) { - throw new Ex\CannotPerformOperation( + throw new Ex\CannotPerformOperationException( 'Cannot write to output file' ); } @@ -291,7 +291,7 @@ public static function encryptResource($inputHandle, $outputHandle, $key) */ $hmac = \hash_init($config['HASH_FUNCTION'], HASH_HMAC, $akey); if ($hmac === false) { - throw new Ex\CannotPerformOperation( + throw new Ex\CannotPerformOperationException( 'Cannot initialize a hash context' ); } @@ -321,7 +321,7 @@ public static function encryptResource($inputHandle, $outputHandle, $key) while (!\feof($inputHandle)) { $read = \fread($inputHandle, $config['BUFFER']); if ($read === false) { - throw new Ex\CannotPerformOperation( + throw new Ex\CannotPerformOperationException( 'Cannot read input file' ); } @@ -341,7 +341,7 @@ public static function encryptResource($inputHandle, $outputHandle, $key) * Check that the encryption was performed successfully */ if ($encrypted === false) { - throw new Ex\CannotPerformOperation( + throw new Ex\CannotPerformOperationException( 'OpenSSL encryption error' ); } @@ -350,7 +350,7 @@ public static function encryptResource($inputHandle, $outputHandle, $key) * Write the ciphertext to the output file */ if (\fwrite($outputHandle, $encrypted, Core::ourStrlen($encrypted)) === false) { - throw new Ex\CannotPerformOperation( + throw new Ex\CannotPerformOperationException( 'Cannot write to output file during encryption' ); } @@ -366,7 +366,7 @@ public static function encryptResource($inputHandle, $outputHandle, $key) $appended = \fwrite($outputHandle, $finalHMAC, $config['MAC_BYTE_SIZE']); if ($appended === false) { - throw new Ex\CannotPerformOperation( + throw new Ex\CannotPerformOperationException( 'Cannot write to output file' ); } @@ -411,7 +411,7 @@ public static function decryptResource($inputHandle, $outputHandle, $key) // Let's add this check before anything if (!\in_array($config['HASH_FUNCTION'], \hash_algos())) { - throw new Ex\CannotPerformOperation( + throw new Ex\CannotPerformOperationException( 'The specified hash function does not exist' ); } @@ -425,7 +425,7 @@ public static function decryptResource($inputHandle, $outputHandle, $key) // Let's grab the file salt. $file_salt = \fread($inputHandle, $config['SALT_SIZE']); if ($file_salt === false ) { - throw new Ex\CannotPerformOperation( + throw new Ex\CannotPerformOperationException( 'Cannot read input file' ); } @@ -470,7 +470,7 @@ public static function decryptResource($inputHandle, $outputHandle, $key) $ivsize = \openssl_cipher_iv_length($config['CIPHER_METHOD']); $iv = \fread($inputHandle, $ivsize); if ($iv === false ) { - throw new Ex\CannotPerformOperation( + throw new Ex\CannotPerformOperationException( 'Cannot read input file' ); } @@ -486,7 +486,7 @@ public static function decryptResource($inputHandle, $outputHandle, $key) * It should be the last N blocks of the file (N = 32) */ if (\fseek($inputHandle, (-1 * $config['MAC_BYTE_SIZE']), SEEK_END) === false) { - throw new Ex\CannotPerformOperation( + throw new Ex\CannotPerformOperationException( 'Cannot seek to beginning of MAC within input file' ); } @@ -494,7 +494,7 @@ public static function decryptResource($inputHandle, $outputHandle, $key) // Grab our last position of ciphertext before we read the MAC $cipher_end = \ftell($inputHandle); if ($cipher_end === false) { - throw new Ex\CannotPerformOperation( + throw new Ex\CannotPerformOperationException( 'Cannot read input file' ); } @@ -503,7 +503,7 @@ public static function decryptResource($inputHandle, $outputHandle, $key) // We keep our MAC stored in this variable $stored_mac = \fread($inputHandle, $config['MAC_BYTE_SIZE']); if ($stored_mac === false) { - throw new Ex\CannotPerformOperation( + throw new Ex\CannotPerformOperationException( 'Cannot read input file' ); } @@ -513,7 +513,7 @@ public static function decryptResource($inputHandle, $outputHandle, $key) */ $hmac = \hash_init($config['HASH_FUNCTION'], HASH_HMAC, $akey); if ($hmac === false) { - throw new Ex\CannotPerformOperation( + throw new Ex\CannotPerformOperationException( 'Cannot initialize a hash context' ); } @@ -522,7 +522,7 @@ public static function decryptResource($inputHandle, $outputHandle, $key) * Reset file pointer to the beginning of the file after the header */ if (\fseek($inputHandle, Core::HEADER_VERSION_SIZE, SEEK_SET) === false) { - throw new Ex\CannotPerformOperation( + throw new Ex\CannotPerformOperationException( 'Cannot read seek within input file' ); } @@ -531,7 +531,7 @@ public static function decryptResource($inputHandle, $outputHandle, $key) * Set it to the first non-salt and non-IV byte */ if (\fseek($inputHandle, $config['SALT_SIZE'] + $ivsize, SEEK_CUR) === false) { - throw new Ex\CannotPerformOperation( + throw new Ex\CannotPerformOperationException( 'Cannot read seek input file to beginning of ciphertext' ); } @@ -553,7 +553,7 @@ public static function decryptResource($inputHandle, $outputHandle, $key) */ $pos = \ftell($inputHandle); if ($pos === false) { - throw new Ex\CannotPerformOperation( + throw new Ex\CannotPerformOperationException( 'Could not get current position in input file during decryption' ); } @@ -569,7 +569,7 @@ public static function decryptResource($inputHandle, $outputHandle, $key) $read = \fread($inputHandle, $config['BUFFER']); } if ($read === false) { - throw new Ex\CannotPerformOperation( + throw new Ex\CannotPerformOperationException( 'Could not read input file during decryption' ); } @@ -583,7 +583,7 @@ public static function decryptResource($inputHandle, $outputHandle, $key) */ $chunkMAC = \hash_copy($hmac); if ($chunkMAC === false) { - throw new Ex\CannotPerformOperation( + throw new Ex\CannotPerformOperationException( 'Cannot duplicate a hash context' ); } @@ -597,7 +597,7 @@ public static function decryptResource($inputHandle, $outputHandle, $key) * 3. Did we match? */ if (!Core::hashEquals($finalHMAC, $stored_mac)) { - throw new Ex\InvalidCiphertext( + throw new Ex\InvalidCiphertextException( 'Message Authentication failure; tampering detected.' ); } @@ -608,7 +608,7 @@ public static function decryptResource($inputHandle, $outputHandle, $key) * Return file pointer to the first non-header, non-IV byte in the file */ if (\fseek($inputHandle, $config['SALT_SIZE'] + $ivsize + Core::HEADER_VERSION_SIZE, SEEK_SET) === false) { - throw new Ex\CannotPerformOperation( + throw new Ex\CannotPerformOperationException( 'Could not move the input file pointer during decryption' ); } @@ -628,7 +628,7 @@ public static function decryptResource($inputHandle, $outputHandle, $key) */ $pos = \ftell($inputHandle); if ($pos === false) { - throw new Ex\CannotPerformOperation( + throw new Ex\CannotPerformOperationException( 'Could not get current position in input file during decryption' ); } @@ -644,7 +644,7 @@ public static function decryptResource($inputHandle, $outputHandle, $key) $read = \fread($inputHandle, $config['BUFFER']); } if ($read === false) { - throw new Ex\CannotPerformOperation( + throw new Ex\CannotPerformOperationException( 'Could not read input file during decryption' ); } @@ -657,14 +657,14 @@ public static function decryptResource($inputHandle, $outputHandle, $key) \hash_update($hmac2, $read); $calcMAC = \hash_copy($hmac2); if ($calcMAC === false) { - throw new Ex\CannotPerformOperation( + throw new Ex\CannotPerformOperationException( 'Cannot duplicate a hash context' ); } $calc = \hash_final($calcMAC); if (empty($macs)) { - throw new Ex\InvalidCiphertext( + throw new Ex\InvalidCiphertextException( 'File was modified after MAC verification' ); } elseif (!Core::hashEquals(\array_shift($macs), $calc)) { @@ -690,7 +690,7 @@ public static function decryptResource($inputHandle, $outputHandle, $key) * Test for decryption faulure */ if ($decrypted === false) { - throw new Ex\CannotPerformOperation( + throw new Ex\CannotPerformOperationException( 'OpenSSL decryption error' ); } @@ -708,7 +708,7 @@ public static function decryptResource($inputHandle, $outputHandle, $key) * Check result */ if ($result === false) { - throw new Ex\CannotPerformOperation( + throw new Ex\CannotPerformOperationException( 'Could not write to output file during decryption.' ); } diff --git a/src/RuntimeTests.php b/src/RuntimeTests.php index a1df3c1..17d710e 100644 --- a/src/RuntimeTests.php +++ b/src/RuntimeTests.php @@ -16,7 +16,7 @@ class RuntimeTests extends Crypto { /* * Runs tests. - * Raises CannotPerformOperationExceptionException or CryptoTestFailedExceptionException if + * Raises Ex\CannotPerformOperationException or Ex\CryptoTestFailedException if * one of the tests fail. If any tests fails, your system is not capable of * performing encryption, so make sure you fail safe in that case. */ @@ -88,7 +88,7 @@ private static function testEncryptDecrypt($config) $decrypted = Crypto::decrypt($ciphertext, $key, true); } catch (Ex\InvalidCiphertextException $ex) { // It's important to catch this and change it into a - // CryptoTestFailedExceptionException, otherwise a test failure could trick + // Ex\CryptoTestFailedException, otherwise a test failure could trick // the user into thinking it's just an invalid ciphertext! throw new Ex\CryptoTestFailedException(); } From b310ac9fc29a0c5022cb69ac42bd14f8aeeebcd2 Mon Sep 17 00:00:00 2001 From: Taylor Hornby Date: Fri, 16 Oct 2015 19:18:04 -0600 Subject: [PATCH 18/35] Test that our nonce format is the same as OpenSSL's --- test/unit/CtrModeTest.php | 46 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/test/unit/CtrModeTest.php b/test/unit/CtrModeTest.php index 4b92267..deea3f5 100644 --- a/test/unit/CtrModeTest.php +++ b/test/unit/CtrModeTest.php @@ -227,4 +227,50 @@ public function testIncrementByNonInteger() $config ); } + + public function testCompatibilityWithOpenSSL() + { + $config = [ + 'CIPHER_METHOD' => 'aes-128-ctr', + ]; + + /* Plaintext is 0x300 blocks. */ + $plaintext = str_repeat('a', 0x300 * 16); + + /* Start at zero. */ + $starting_nonce = str_repeat("\x00", 16); + + $ciphertext = openssl_encrypt( + $plaintext, + $config['CIPHER_METHOD'], + 'YELLOW SUBMARINE', + OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING, + $starting_nonce + ); + + /* Take the second half, the last 0x150 blocks. */ + $cipher_lasthalf = mb_substr($ciphertext, 0x150 * 16, 0x150 * 16, '8bit'); + + /* Compute what the nonce should be at the start of the last half. */ + $computed_nonce = \Defuse\Crypto\Core::incrementCounter( + $starting_nonce, + 0x150, + $config + ); + + /* Try to decrypt it using that nonce. */ + $decrypt = openssl_decrypt( + $cipher_lasthalf, + $config['CIPHER_METHOD'], + 'YELLOW SUBMARINE', + OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING, + $computed_nonce + ); + + /* If it decrypts properly, we computed the nonce the same way. */ + $this->assertEquals( + str_repeat('a', 0x150 * 16), + $decrypt + ); + } } From 7647408ceae888b16332c19f98675e34b64021d5 Mon Sep 17 00:00:00 2001 From: Taylor Hornby Date: Fri, 16 Oct 2015 19:25:00 -0600 Subject: [PATCH 19/35] Fix file handle leak. --- src/File.php | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/File.php b/src/File.php index ba23982..492ff96 100644 --- a/src/File.php +++ b/src/File.php @@ -92,6 +92,7 @@ public static function encryptFile($inputFilename, $outputFilename, $key) */ $of = \fopen($outputFilename, 'wb'); if ($of === false) { + fclose($if); throw new Ex\CannotPerformOperationException( 'Cannot open output file for encrypting' ); @@ -101,7 +102,13 @@ public static function encryptFile($inputFilename, $outputFilename, $key) /** * Use encryptResource() to actually write the encrypted data to $of */ - $encrypted = self::encryptResource($if, $of, $key); + try { + $encrypted = self::encryptResource($if, $of, $key); + } catch (\Ex\CryptoException $ex) { + fclose($if); + fclose($of); + throw $ex; + } /** * Close handles @@ -163,6 +170,7 @@ public static function decryptFile($inputFilename, $outputFilename, $key) */ $of = \fopen($outputFilename, 'wb'); if ($of === false) { + fclose($if); throw new Ex\CannotPerformOperationException( 'Cannot open output file for decrypting' ); @@ -172,7 +180,13 @@ public static function decryptFile($inputFilename, $outputFilename, $key) /** * Use decryptResource() to actually write the decrypted data to $of */ - $decrypted = self::decryptResource($if, $of, $key); + try { + $decrypted = self::decryptResource($if, $of, $key); + } catch (\Ex\CryptoException $ex) { + fclose($if); + fclose($of); + throw $ex; + } /** * Close handles From 26a1067227130e2749012ddec3f1df9fbf634265 Mon Sep 17 00:00:00 2001 From: Taylor Hornby Date: Fri, 16 Oct 2015 19:28:12 -0600 Subject: [PATCH 20/35] Fix documentation now that we use CTR mode. --- README.md | 5 ++--- doc/02-Crypto.php.md | 4 ++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 039efca..0e9e90d 100644 --- a/README.md +++ b/README.md @@ -8,9 +8,8 @@ This is a class for doing symmetric encryption in PHP. **Requires PHP 5.4 or new Implementation -------------- -Messages are encrypted with AES-128 in CBC mode and are authenticated with -HMAC-SHA256 (Encrypt-then-Mac). PKCS7 padding is used to pad the message to -a multiple of the block size. HKDF is used to split the user-provided key into +Messages are encrypted with AES-128 in CTR mode and are authenticated with +HMAC-SHA256 (Encrypt-then-Mac). HKDF is used to split the user-provided key into two keys: one for encryption, and the other for authentication. It is implemented using the `openssl_` and `hash_hmac` functions. diff --git a/doc/02-Crypto.php.md b/doc/02-Crypto.php.md index 9dbab09..efccc7b 100644 --- a/doc/02-Crypto.php.md +++ b/doc/02-Crypto.php.md @@ -3,8 +3,8 @@ Symmetric Key Encryption At a glance: -* **Cipher and Mode**: `AES-128-CBC` -* **Padding**: `PKCS#7` +* **Cipher and Mode**: `AES-128-CTR` +* **Padding**: None (CTR mode doesn't pad) * **Authentication**: `HMAC-SHA-256` * **Construction**: `Encrypt then MAC` * **Algorithm Backend**: `ext/openssl` From 3b4150984c3a73adec24bc7a28bc88fe14509028 Mon Sep 17 00:00:00 2001 From: Taylor Hornby Date: Fri, 16 Oct 2015 19:29:52 -0600 Subject: [PATCH 21/35] Get rid of the terrible ExceptionHandler hack. --- autoload.php | 2 -- src/ExceptionHandler.php | 46 ---------------------------------------- 2 files changed, 48 deletions(-) delete mode 100644 src/ExceptionHandler.php diff --git a/autoload.php b/autoload.php index d73b52a..309cc98 100644 --- a/autoload.php +++ b/autoload.php @@ -34,5 +34,3 @@ require $file; } }); - -$crypto_exception_handler_object_dont_touch_me = new \Defuse\Crypto\ExceptionHandler; diff --git a/src/ExceptionHandler.php b/src/ExceptionHandler.php deleted file mode 100644 index 8ef4a45..0000000 --- a/src/ExceptionHandler.php +++ /dev/null @@ -1,46 +0,0 @@ -rethrow = $ex; - } - } - - public function __destruct() { - if ($this->rethrow) { - throw $this->rethrow; - } - } -} From aa6810ad7d0c3d1685e5aab7ca0009f8dc844c7c Mon Sep 17 00:00:00 2001 From: Taylor Hornby Date: Fri, 16 Oct 2015 19:59:40 -0600 Subject: [PATCH 22/35] Switch to AES-256. --- doc/02-Crypto.php.md | 2 +- doc/03-File.php.md | 2 +- src/Crypto.php | 4 ++-- src/File.php | 4 ++-- src/RuntimeTests.php | 19 ++++++++++++++----- 5 files changed, 20 insertions(+), 11 deletions(-) diff --git a/doc/02-Crypto.php.md b/doc/02-Crypto.php.md index efccc7b..b9fb7eb 100644 --- a/doc/02-Crypto.php.md +++ b/doc/02-Crypto.php.md @@ -3,7 +3,7 @@ Symmetric Key Encryption At a glance: -* **Cipher and Mode**: `AES-128-CTR` +* **Cipher and Mode**: `AES-256-CTR` * **Padding**: None (CTR mode doesn't pad) * **Authentication**: `HMAC-SHA-256` * **Construction**: `Encrypt then MAC` diff --git a/doc/03-File.php.md b/doc/03-File.php.md index e4b02d5..5983401 100644 --- a/doc/03-File.php.md +++ b/doc/03-File.php.md @@ -3,7 +3,7 @@ Symmetric Key File Encryption At a glance: -* **Cipher and Mode**: `AES-128-CTR` +* **Cipher and Mode**: `AES-256-CTR` * **Padding**: None (CTR mode doesn't pad) * **Authentication**: `HMAC-SHA-256` * **Construction**: `Encrypt then MAC` diff --git a/src/Crypto.php b/src/Crypto.php index 0753b4f..9cf9752 100755 --- a/src/Crypto.php +++ b/src/Crypto.php @@ -439,9 +439,9 @@ protected static function getVersionConfigFromMajorMinor($major, $minor) switch ($minor) { case 0: return [ - 'CIPHER_METHOD' => 'aes-128-ctr', + 'CIPHER_METHOD' => 'aes-256-ctr', 'BLOCK_SIZE' => 16, - 'KEY_BYTE_SIZE' => 16, + 'KEY_BYTE_SIZE' => 32, 'SALT_SIZE' => 16, 'HASH_FUNCTION' => 'sha256', 'MAC_BYTE_SIZE' => 32, diff --git a/src/File.php b/src/File.php index 492ff96..ea3fdc1 100644 --- a/src/File.php +++ b/src/File.php @@ -777,9 +777,9 @@ private static function getFileVersionConfigFromMajorMinor($major, $minor) switch ($minor) { case 0: return [ - 'CIPHER_METHOD' => 'aes-128-ctr', + 'CIPHER_METHOD' => 'aes-256-ctr', 'BLOCK_SIZE' => 16, - 'KEY_BYTE_SIZE' => 16, + 'KEY_BYTE_SIZE' => 32, 'SALT_SIZE' => 16, 'HASH_FUNCTION' => 'sha256', 'MAC_BYTE_SIZE' => 32, diff --git a/src/RuntimeTests.php b/src/RuntimeTests.php index 17d710e..dbb3ed4 100644 --- a/src/RuntimeTests.php +++ b/src/RuntimeTests.php @@ -197,7 +197,10 @@ private static function HMACTestVector($config) private static function AESTestVector($config) { // AES CTR mode test vector from NIST SP 800-38A - $key = Encoding::hexToBin("2b7e151628aed2a6abf7158809cf4f3c"); + $key = Encoding::hexToBin( + "603deb1015ca71be2b73aef0857d7781". + "1f352c073b6108d72d9810a30914dff4" + ); $iv = Encoding::hexToBin("f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff"); $plaintext = Encoding::hexToBin( "6bc1bee22e409f96e93d7e117393172a" . @@ -206,16 +209,22 @@ private static function AESTestVector($config) "f69f2445df4f9b17ad2b417be66c3710" ); $ciphertext = Encoding::hexToBin( - "874d6191b620e3261bef6864990db6ce" . - "9806f66b7970fdff8617187bb9fffdff" . - "5ae4df3edbd5d35e5b4f09020db03eab" . - "1e031dda2fbe03d1792170a0f3009cee" + "601ec313775789a5b7a7f504bbf3d228". + "f443e3ca4d62b59aca84e990cacaf5c5". + "2b0930daa23de94ce87017ba2d84988d". + "dfc9c58db67aada613c2dd08457941a6" ); $config = Crypto::getVersionConfigFromHeader(Core::CURRENT_VERSION, Core::CURRENT_VERSION); $computed_ciphertext = Crypto::plainEncrypt($plaintext, $key, $iv, $config); if ($computed_ciphertext !== $ciphertext) { + echo str_repeat("\n", 30); + var_dump($config); + echo \bin2hex($computed_ciphertext); + echo "\n---\n"; + echo \bin2hex($ciphertext); + echo str_repeat("\n", 30); throw new Ex\CryptoTestFailedException(); } From 533ad7d82735feffdae63f69f9d33018fb9d5514 Mon Sep 17 00:00:00 2001 From: Taylor Hornby Date: Fri, 16 Oct 2015 21:20:08 -0600 Subject: [PATCH 23/35] Get older PHPUnit on older versions of PHP. --- test/phpunit.sh | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/test/phpunit.sh b/test/phpunit.sh index 3e405fb..87e5b68 100755 --- a/test/phpunit.sh +++ b/test/phpunit.sh @@ -33,17 +33,17 @@ fi # Let's grab the latest release and its signature if [ ! -f phpunit.phar ]; then - wget https://phar.phpunit.de/phpunit.phar - if [ $? -ne 0 ]; then - echo "wget phpunit.phar was unsuccessful" - exit 1 + if [[ $PHP_VERSION -ge 50600 ]]; then + wget https://phar.phpunit.de/phpunit.phar + else + wget -O phpunit.phar https://phar.phpunit.de/phpunit-old.phar fi fi if [ ! -f phpunit.phar.asc ]; then - wget https://phar.phpunit.de/phpunit.phar.asc - if [ $? -ne 0 ]; then - echo "wget phpunit.phar.asc was unsuccessful" - exit 1 + if [[ $PHP_VERSION -ge 50600 ]]; then + wget https://phar.phpunit.de/phpunit.phar.asc + else + wget -O phpunit.phar.asc https://phar.phpunit.de/phpunit-old.phar.asc fi fi From 82a7599b586145c8f23d6cbceaf8b3d825e68f50 Mon Sep 17 00:00:00 2001 From: Taylor Hornby Date: Fri, 16 Oct 2015 21:24:43 -0600 Subject: [PATCH 24/35] Fix syntax to parse on older versions of PHP. --- src/Core.php | 9 +++++---- src/Crypto.php | 2 +- src/File.php | 2 +- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/Core.php b/src/Core.php index 0bc706c..93b53e4 100644 --- a/src/Core.php +++ b/src/Core.php @@ -5,13 +5,14 @@ final class Core { - const HEADER_MAGIC = "\xDE\xF5"; - const HEADER_MAGIC_FILE = "\xDE\xF4"; + const HEADER_VERSION_SIZE = 4; /* This must never change. */ + const HEADER_MAGIC = "\xDE\xF5"; const CURRENT_VERSION = "\xDE\xF5\x02\x00"; - const CURRENT_FILE_VERSION = "\xDE\xF4\x02\x00"; const LEGACY_VERSION = "\xDE\xF5\x01\x00"; - const HEADER_VERSION_SIZE = 4; /* This must never change. */ + + const HEADER_MAGIC_FILE = "\xDE\xF4"; + const CURRENT_FILE_VERSION = "\xDE\xF4\x02\x00"; /** * Increment a counter (prevent nonce reuse) diff --git a/src/Crypto.php b/src/Crypto.php index 9cf9752..9064afb 100755 --- a/src/Crypto.php +++ b/src/Crypto.php @@ -403,7 +403,7 @@ protected static function verifyHMAC($correct_hmac, $message, $key, $config) */ protected static function getVersionConfigFromHeader($header, $min_ver_header) { - if ($header[0] !== Core::HEADER_MAGIC[0] || $header[1] !== Core::HEADER_MAGIC[1]) { + if (Core::ourSubstr($header, 0, 2) !== Core::ourSubstr(Core::HEADER_MAGIC, 0, 2)) { throw new Ex\InvalidCiphertextException( "Ciphertext has a bad magic number." ); diff --git a/src/File.php b/src/File.php index ea3fdc1..08dc198 100644 --- a/src/File.php +++ b/src/File.php @@ -741,7 +741,7 @@ public static function decryptResource($inputHandle, $outputHandle, $key) */ private static function getFileVersionConfigFromHeader($header, $min_ver_header) { - if ($header[0] !== Core::CURRENT_FILE_VERSION[0] || $header[1] !== Core::CURRENT_FILE_VERSION[1]) { + if (Core::ourSubstr($header, 0, 2) !== Core::ourSubstr(Core::HEADER_MAGIC_FILE, 0, 2)) { throw new Ex\InvalidCiphertextException( "Ciphertext file has a bad magic number." ); From 26db090f6f699b219bc5ce0593f929d20223c745 Mon Sep 17 00:00:00 2001 From: Taylor Hornby Date: Fri, 16 Oct 2015 21:28:05 -0600 Subject: [PATCH 25/35] Fix bug by baking 'sha256' in the mac-the-mac comparison. --- src/Core.php | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/Core.php b/src/Core.php index 93b53e4..9fde43b 100644 --- a/src/Core.php +++ b/src/Core.php @@ -176,11 +176,6 @@ public static function hashEquals($expected, $given) if ($native) { return \hash_equals($expected, $given); } - static $config = null; - if ($config === null) { - $valid = 0; - $config = self::getCoreVersionConfig(1, 0, $valid); - } // We can't just compare the strings with '==', since it would make // timing attacks possible. We could use the XOR-OR constant-time @@ -195,8 +190,8 @@ public static function hashEquals($expected, $given) } $blind = self::createNewRandomKey(); - $message_compare = hash_hmac($config['HASH_FUNCTION'], $given, $blind); - $correct_compare = hash_hmac($config['HASH_FUNCTION'], $expected, $blind); + $message_compare = hash_hmac('sha256', $given, $blind); + $correct_compare = hash_hmac('sha256', $expected, $blind); return $correct_compare === $message_compare; } /** From 2986ab1cfb736d5bed053ba2ce9d4ae1bf19608e Mon Sep 17 00:00:00 2001 From: Taylor Hornby Date: Fri, 16 Oct 2015 21:31:40 -0600 Subject: [PATCH 26/35] Fix bug caused by moving createNewRandomKey() to Crypto. --- src/Core.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Core.php b/src/Core.php index 9fde43b..58edf9f 100644 --- a/src/Core.php +++ b/src/Core.php @@ -2,6 +2,7 @@ namespace Defuse\Crypto; use \Defuse\Crypto\Exception as Ex; +use \Defuse\Crypto\Crypto; final class Core { @@ -189,7 +190,7 @@ public static function hashEquals($expected, $given) throw new Ex\CannotPerformOperationException(); } - $blind = self::createNewRandomKey(); + $blind = self::secureRandom(32); $message_compare = hash_hmac('sha256', $given, $blind); $correct_compare = hash_hmac('sha256', $expected, $blind); return $correct_compare === $message_compare; From 9b9a732877b42cc3a0f80734818d05d5a338a0f6 Mon Sep 17 00:00:00 2001 From: Taylor Hornby Date: Fri, 16 Oct 2015 21:34:07 -0600 Subject: [PATCH 27/35] Don't allow Travis-CI tests of PHP 7.0 to fail. --- .travis.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 32b30dd..db09559 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,7 +9,5 @@ sudo: false matrix: fast_finish: true - allow_failures: - - php: "7.0" script: ./test.sh From 02112029603dcafee429bde6a9932a1e84078641 Mon Sep 17 00:00:00 2001 From: Taylor Hornby Date: Fri, 16 Oct 2015 23:08:19 -0600 Subject: [PATCH 28/35] Add a 'Key' "firewall" to prevent use of insecurely-generated keys. --- src/Crypto.php | 22 +++++- src/Key.php | 173 +++++++++++++++++++++++++++++++++++++++++ src/RuntimeTests.php | 2 +- test/stream/keygen.php | 2 +- test/unit/KeyTest.php | 52 +++++++++++++ 5 files changed, 247 insertions(+), 4 deletions(-) create mode 100644 src/Key.php create mode 100644 test/unit/KeyTest.php diff --git a/src/Crypto.php b/src/Crypto.php index 9064afb..7115672 100755 --- a/src/Crypto.php +++ b/src/Crypto.php @@ -4,6 +4,7 @@ use \Defuse\Crypto\Exception as Ex; use \Defuse\Crypto\Core; +use \Defuse\Crypto\Key; use \Defuse\Crypto\Encoding; use \Defuse\Crypto\RuntimeTests; @@ -46,8 +47,7 @@ class Crypto */ public static function createNewRandomKey() { - $config = self::getVersionConfigFromHeader(Core::CURRENT_VERSION, Core::CURRENT_VERSION); - return Core::secureRandom($config['KEY_BYTE_SIZE']); + return Key::CreateNewRandomKey(Core::CURRENT_VERSION); } /** @@ -67,6 +67,15 @@ public static function createNewRandomKey() public static function encrypt($plaintext, $key, $raw_binary = false) { RuntimeTests::runtimeTest(); + + /* Attempt to validate that the key was generated safely. */ + if (!is_a($key, "\Defuse\Crypto\Key")) { + throw new Ex\CannotPerformOperationException( + "The given key is not a valid Key object." + ); + } + $key = $key->getRawBytes(); + $config = self::getVersionConfigFromHeader(Core::CURRENT_VERSION, Core::CURRENT_VERSION); if (Core::ourStrlen($key) !== $config['KEY_BYTE_SIZE']) { @@ -133,6 +142,15 @@ public static function encrypt($plaintext, $key, $raw_binary = false) public static function decrypt($ciphertext, $key, $raw_binary = false) { RuntimeTests::runtimeTest(); + + /* Attempt to validate that the key was generated safely. */ + if (!is_a($key, "\Defuse\Crypto\Key")) { + throw new Ex\CannotPerformOperationException( + "The given key is not a valid Key object." + ); + } + $key = $key->getRawBytes(); + if (!$raw_binary) { $ciphertext = Encoding::hexToBin($ciphertext); } diff --git a/src/Key.php b/src/Key.php new file mode 100644 index 0000000..3866434 --- /dev/null +++ b/src/Key.php @@ -0,0 +1,173 @@ +key_version_header = $version_header; + $this->key_bytes = $bytes; + $this->config = self::GetKeyVersionConfigFromKeyHeader($this->key_version_header); + } + + public function saveToAsciiSafeString() + { + return Encoding::binToHex( + $this->key_version_header . + $this->key_bytes . + hash( + $this->config['CHECKSUM_HASH_FUNCTION'], + $this->key_version_header . $this->key_bytes, + true + ) + ); + } + + public function isSafeForCipherTextVersion($major, $minor) + { + /* Legacy decryption uses raw key byte strings, not Key. */ + return $major == 2 && $minor == 0; + } + + public function getRawBytes() + { + if (is_null($this->key_bytes) || Core::ourStrlen($this->key_bytes) < self::MIN_SAFE_KEY_BYTE_SIZE) { + throw new CannotPerformOperationException( + "An attempt was made to use an uninitialzied or too-short key" + ); + } + return $this->key_bytes; + } + + private static function GetKeyVersionConfigFromKeyHeader($key_header) { + if ($key_header === self::KEY_CURRENT_VERSION) { + return [ + 'KEY_BYTE_SIZE' => 32, + 'CHECKSUM_HASH_FUNCTION' => 'sha256', + 'CHECKSUM_BYTE_SIZE' => 32 + ]; + } + throw new Ex\CannotPerformOperationException( + "Invalid key version header." + ); + } +} diff --git a/src/RuntimeTests.php b/src/RuntimeTests.php index dbb3ed4..1514d95 100644 --- a/src/RuntimeTests.php +++ b/src/RuntimeTests.php @@ -57,7 +57,7 @@ public static function runtimeTest() RuntimeTests::HKDFTestVector($config); RuntimeTests::testEncryptDecrypt($config); - if (Core::ourStrlen(Crypto::createNewRandomKey()) != $config['KEY_BYTE_SIZE']) { + if (Core::ourStrlen(Crypto::createNewRandomKey()->getRawBytes()) != $config['KEY_BYTE_SIZE']) { throw new Ex\CryptoTestFailedException(); } diff --git a/test/stream/keygen.php b/test/stream/keygen.php index 4a7a413..098f05c 100644 --- a/test/stream/keygen.php +++ b/test/stream/keygen.php @@ -3,4 +3,4 @@ $key = \Defuse\Crypto\Crypto::createNewRandomKey(); -\file_put_contents('key.txt', \Defuse\Crypto\Encoding::binToHex($key)); +\file_put_contents('key.txt', \Defuse\Crypto\Encoding::binToHex($key->getRawBytes())); diff --git a/test/unit/KeyTest.php b/test/unit/KeyTest.php new file mode 100644 index 0000000..4680cdf --- /dev/null +++ b/test/unit/KeyTest.php @@ -0,0 +1,52 @@ +assertEquals(32, Core::ourStrlen($key->getRawBytes())); + } + + function testSaveAndLoadKey() + { + $key1 = Key::CreateNewRandomKey(); + $str = $key1->saveToAsciiSafeString(); + $key2 = Key::LoadFromAsciiSafeString($str); + $this->assertEquals($key1->getRawBytes(), $key2->getRawBytes()); + } + + /** + * @expectedException \Defuse\Crypto\Exception\CannotPerformOperationException + * @excpectedExceptionMessage key version header + */ + function testIncorrectHeader() + { + $key = Key::CreateNewRandomKey(); + $str = $key->saveToAsciiSafeString(); + $str[0] = 'f'; + Key::LoadFromAsciiSafeString($str); + } + + /** + * @expectedException \Defuse\Crypto\Exception\CannotPerformOperationException + * @expectedExceptionMessage checksums don't match + */ + function testIncorrectChecksum() + { + $key = Key::CreateNewRandomKey(); + $str = $key->saveToAsciiSafeString(); + $str[2*Key::KEY_HEADER_SIZE + 0] = 'f'; + $str[2*Key::KEY_HEADER_SIZE + 1] = 'f'; + $str[2*Key::KEY_HEADER_SIZE + 3] = 'f'; + $str[2*Key::KEY_HEADER_SIZE + 4] = 'f'; + $str[2*Key::KEY_HEADER_SIZE + 5] = 'f'; + $str[2*Key::KEY_HEADER_SIZE + 6] = 'f'; + $str[2*Key::KEY_HEADER_SIZE + 7] = 'f'; + $str[2*Key::KEY_HEADER_SIZE + 8] = 'f'; + Key::LoadFromAsciiSafeString($str); + } +} From 6e72941a8a96e237cc1a493dd48031e10c0b7047 Mon Sep 17 00:00:00 2001 From: Taylor Hornby Date: Sat, 17 Oct 2015 12:19:07 -0600 Subject: [PATCH 29/35] SECURITY CRITICAL: Fix bug where bad version header could cause invalid config to be returned. --- src/Crypto.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Crypto.php b/src/Crypto.php index 7115672..960082d 100755 --- a/src/Crypto.php +++ b/src/Crypto.php @@ -488,6 +488,10 @@ protected static function getVersionConfigFromMajorMinor($major, $minor) "Unsupported ciphertext version." ); } + } else { + throw new Ex\InvalidCiphertextException( + "Unsupported ciphertext version." + ); } } From 56dd2d2b25286f9f066d64867ead2fcd8cd106db Mon Sep 17 00:00:00 2001 From: Taylor Hornby Date: Sat, 17 Oct 2015 12:22:13 -0600 Subject: [PATCH 30/35] Turn RangeExceptions into our exceptions upon hex decoding failure. --- src/Crypto.php | 8 +++++++- src/Key.php | 8 +++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/Crypto.php b/src/Crypto.php index 960082d..e1d1f92 100755 --- a/src/Crypto.php +++ b/src/Crypto.php @@ -152,7 +152,13 @@ public static function decrypt($ciphertext, $key, $raw_binary = false) $key = $key->getRawBytes(); if (!$raw_binary) { - $ciphertext = Encoding::hexToBin($ciphertext); + try { + $ciphertext = Encoding::hexToBin($ciphertext); + } catch (\RangeException $ex) { + throw new Ex\InvalidCiphertextException( + "Ciphertext has invalid hex encoding." + ); + } } // Grab the header tag diff --git a/src/Key.php b/src/Key.php index 3866434..1cc0101 100644 --- a/src/Key.php +++ b/src/Key.php @@ -65,7 +65,13 @@ public static function CreateNewRandomKey() public static function LoadFromAsciiSafeString($savedKeyString) { - $bytes = Encoding::hexToBin($savedKeyString); + try { + $bytes = Encoding::hexToBin($savedKeyString); + } catch (\RangeException $ex) { + throw new Ex\CannotPerformOperationException( + "Key has invalid hex encoding." + ); + } /* Make sure we have enough bytes to get the version header. */ if (Core::ourStrlen($bytes) < self::KEY_HEADER_SIZE) { From fec7f5f3f2fb346b9c367c1dbd25de44d51a2ac8 Mon Sep 17 00:00:00 2001 From: Taylor Hornby Date: Sat, 17 Oct 2015 12:25:32 -0600 Subject: [PATCH 31/35] Add test for backwards compatibility attacks. --- src/Crypto.php | 2 +- src/Key.php | 9 ++ test/unit/BackwardsCompatibilityTest.php | 166 +++++++++++++++++++++++ test/unit/KeyTest.php | 12 ++ 4 files changed, 188 insertions(+), 1 deletion(-) create mode 100644 test/unit/BackwardsCompatibilityTest.php diff --git a/src/Crypto.php b/src/Crypto.php index e1d1f92..5a46afc 100755 --- a/src/Crypto.php +++ b/src/Crypto.php @@ -425,7 +425,7 @@ protected static function verifyHMAC($correct_hmac, $message, $key, $config) * @return array * @throws Ex\InvalidCiphertextException */ - protected static function getVersionConfigFromHeader($header, $min_ver_header) + public static function getVersionConfigFromHeader($header, $min_ver_header) { if (Core::ourSubstr($header, 0, 2) !== Core::ourSubstr(Core::HEADER_MAGIC, 0, 2)) { throw new Ex\InvalidCiphertextException( diff --git a/src/Key.php b/src/Key.php index 1cc0101..5946110 100644 --- a/src/Key.php +++ b/src/Key.php @@ -176,4 +176,13 @@ private static function GetKeyVersionConfigFromKeyHeader($key_header) { "Invalid key version header." ); } + + /* + * NEVER use this, exept for testing. + */ + public static function LoadFromRawBytesForTestingPurposesOnlyInsecure($bytes) + { + return new Key(self::KEY_CURRENT_VERSION, $bytes); + } + } diff --git a/test/unit/BackwardsCompatibilityTest.php b/test/unit/BackwardsCompatibilityTest.php new file mode 100644 index 0000000..ed837f0 --- /dev/null +++ b/test/unit/BackwardsCompatibilityTest.php @@ -0,0 +1,166 @@ +saveToAsciiSafeString(); + $str[0] = "Z"; + Key::LoadFromAsciiSafeString($str); + } } From 4691ab20731956f2c9fd7eabe8a523fa885ec646 Mon Sep 17 00:00:00 2001 From: Taylor Hornby Date: Sat, 17 Oct 2015 12:40:50 -0600 Subject: [PATCH 32/35] Add more invalid version number tests. --- test/unit/BackwardsCompatibilityTest.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/unit/BackwardsCompatibilityTest.php b/test/unit/BackwardsCompatibilityTest.php index ed837f0..6051a2c 100644 --- a/test/unit/BackwardsCompatibilityTest.php +++ b/test/unit/BackwardsCompatibilityTest.php @@ -148,7 +148,9 @@ function unsupportedVersionProvider() array(2, 1), array(2, 2), array(3, 0), - array(4, 0) + array(4, 0), + array(255, 0), + array(255, 255) ); } From 6f99c97220d8350a2cb09024de631f957af68c0a Mon Sep 17 00:00:00 2001 From: Taylor Hornby Date: Sat, 17 Oct 2015 14:02:19 -0600 Subject: [PATCH 33/35] Use an object for config instead of an array. --- src/Config.php | 149 ++++++++++++++++++++++++++++++++++++++ src/Core.php | 6 +- src/Crypto.php | 102 +++++++++++++------------- src/File.php | 103 +++++++++++++------------- src/FileConfig.php | 34 +++++++++ src/RuntimeTests.php | 10 +-- test/unit/CtrModeTest.php | 53 +++++++------- 7 files changed, 321 insertions(+), 136 deletions(-) create mode 100644 src/Config.php create mode 100644 src/FileConfig.php diff --git a/src/Config.php b/src/Config.php new file mode 100644 index 0000000..4052577 --- /dev/null +++ b/src/Config.php @@ -0,0 +1,149 @@ +cipher_method = $config_array["cipher_method"]; + $this->block_byte_size = $config_array["block_byte_size"]; + $this->key_byte_size = $config_array["key_byte_size"]; + $this->salt_byte_size = $config_array["salt_byte_size"]; + $this->mac_byte_size = $config_array["mac_byte_size"]; + $this->hash_function_name = $config_array["hash_function_name"]; + $this->encryption_info_string = $config_array["encryption_info_string"]; + $this->authentication_info_string = $config_array["authentication_info_string"]; + + Core::ensureFunctionExists('openssl_get_cipher_methods'); + if (\in_array($this->cipher_method, \openssl_get_cipher_methods()) === false) { + throw new Ex\CannotPerformOperationException( + "Configuration contains an invalid OpenSSL cipher method." + ); + } + + if (!is_int($this->block_byte_size) || $this->block_byte_size <= 0) { + throw new Ex\CannotPerformOperationException( + "Configuration contains an invalid block byte size." + ); + } + + if (!is_int($this->key_byte_size) || $this->key_byte_size <= 0) { + throw new Ex\CannotPerformOperationException( + "Configuration contains an invalid key byte size." + ); + } + + if ($this->salt_byte_size !== false) { + if (!is_int($this->salt_byte_size) || $this->salt_byte_size <= 0) { + throw new Ex\CannotPerformOperationException( + "Configuration contains an invalid salt byte size." + ); + } + } + + if (!is_int($this->mac_byte_size) || $this->mac_byte_size <= 0) { + throw new Ex\CannotPerformOperationException( + "Configuration contains an invalid MAC byte size." + ); + } + + if (\in_array($this->hash_function_name, \hash_algos()) === false) { + throw new Ex\CannotPerformOperationException( + "Configuration contains an invalid hash function name." + ); + } + + if (!is_string($this->encryption_info_string) || $this->encryption_info_string === "") { + throw new Ex\CannotPerformOperationException( + "Configuration contains an invalid encryption info string." + ); + } + + if (!is_string($this->authentication_info_string) || $this->authentication_info_string === "") { + throw new Ex\CannotPerformOperationException( + "Configuration contains an invalid authentication info string." + ); + } + } + + public function cipherMethod() + { + return $this->cipher_method; + } + + public function blockByteSize() + { + return $this->block_byte_size; + } + + public function keyByteSize() + { + return $this->key_byte_size; + } + + public function saltByteSize() + { + return $this->salt_byte_size; + } + + public function macByteSize() + { + return $this->mac_byte_size; + } + + public function hashFunctionName() + { + return $this->hash_function_name; + } + + public function encryptionInfoString() + { + return $this->encryption_info_string; + } + + public function authenticationInfoString() + { + return $this->authentication_info_string; + } +} diff --git a/src/Core.php b/src/Core.php index 58edf9f..0c2822f 100644 --- a/src/Core.php +++ b/src/Core.php @@ -27,7 +27,7 @@ public static function incrementCounter($ctr, $inc, &$config) { static $ivsize = null; if ($ivsize === null) { - $ivsize = \openssl_cipher_iv_length($config['CIPHER_METHOD']); + $ivsize = \openssl_cipher_iv_length($config->cipherMethod()); if ($ivsize === false) { throw new Ex\CannotPerformOperationException( "Problem obtaining the correct nonce length." @@ -107,8 +107,8 @@ public static function secureRandom($octets) public static function HKDF($hash, $ikm, $length, $info = '', $salt = null, $config = null) { // Find the correct digest length as quickly as we can. - $digest_length = $config['MAC_BYTE_SIZE']; - if ($hash != $config['HASH_FUNCTION']) { + $digest_length = $config->macByteSize(); + if ($hash != $config->hashFunctionName()) { $digest_length = self::ourStrlen(\hash_hmac($hash, '', '', true)); } // Sanity-check the desired output length. diff --git a/src/Crypto.php b/src/Crypto.php index 5a46afc..6a38bd4 100755 --- a/src/Crypto.php +++ b/src/Crypto.php @@ -7,6 +7,7 @@ use \Defuse\Crypto\Key; use \Defuse\Crypto\Encoding; use \Defuse\Crypto\RuntimeTests; +use \Defuse\Crypto\Config; /* * PHP Encryption Library @@ -78,34 +79,34 @@ public static function encrypt($plaintext, $key, $raw_binary = false) $config = self::getVersionConfigFromHeader(Core::CURRENT_VERSION, Core::CURRENT_VERSION); - if (Core::ourStrlen($key) !== $config['KEY_BYTE_SIZE']) { + if (Core::ourStrlen($key) !== $config->keyByteSize()) { throw new Ex\CannotPerformOperationException("Key is the wrong size."); } - $salt = Core::secureRandom($config['SALT_SIZE']); + $salt = Core::secureRandom($config->saltByteSize()); // Generate a sub-key for encryption. $ekey = Core::HKDF( - $config['HASH_FUNCTION'], + $config->hashFunctionName(), $key, - $config['KEY_BYTE_SIZE'], - $config['ENCRYPTION_INFO'], + $config->keyByteSize(), + $config->encryptionInfoString(), $salt, $config ); // Generate a sub-key for authentication and apply the HMAC. $akey = Core::HKDF( - $config['HASH_FUNCTION'], + $config->hashFunctionName(), $key, - $config['KEY_BYTE_SIZE'], - $config['AUTHENTICATION_INFO'], + $config->keyByteSize(), + $config->authenticationInfoString(), $salt, $config ); // Generate a random initialization vector. Core::ensureFunctionExists("openssl_cipher_iv_length"); - $ivsize = \openssl_cipher_iv_length($config['CIPHER_METHOD']); + $ivsize = \openssl_cipher_iv_length($config->cipherMethod()); if ($ivsize === false || $ivsize <= 0) { throw new Ex\CannotPerformOperationException( "Could not get the IV length from OpenSSL" @@ -114,7 +115,7 @@ public static function encrypt($plaintext, $key, $raw_binary = false) $iv = Core::secureRandom($ivsize); $ciphertext = $salt . $iv . self::plainEncrypt($plaintext, $ekey, $iv, $config); - $auth = \hash_hmac($config['HASH_FUNCTION'], Core::CURRENT_VERSION . $ciphertext, $akey, true); + $auth = \hash_hmac($config->hashFunctionName(), Core::CURRENT_VERSION . $ciphertext, $akey, true); // We're now appending the header as of 2.00 $ciphertext = Core::CURRENT_VERSION . $auth . $ciphertext; @@ -171,7 +172,7 @@ public static function decrypt($ciphertext, $key, $raw_binary = false) $ciphertext = Core::ourSubstr($ciphertext, Core::HEADER_VERSION_SIZE, null); // Extract the HMAC from the front of the ciphertext. - if (Core::ourStrlen($ciphertext) <= $config['MAC_BYTE_SIZE']) { + if (Core::ourStrlen($ciphertext) <= $config->macByteSize()) { throw new Ex\InvalidCiphertextException( "Ciphertext is too short." ); @@ -179,15 +180,15 @@ public static function decrypt($ciphertext, $key, $raw_binary = false) $hmac = Core::ourSubstr( $ciphertext, 0, - $config['MAC_BYTE_SIZE'] + $config->macByteSize() ); if ($hmac === false) { throw new Ex\CannotPerformOperationException(); } $salt = Core::ourSubstr( $ciphertext, - $config['MAC_BYTE_SIZE'], - $config['SALT_SIZE'] + $config->macByteSize(), + $config->saltByteSize() ); if ($salt === false) { throw new Ex\CannotPerformOperationException(); @@ -195,22 +196,22 @@ public static function decrypt($ciphertext, $key, $raw_binary = false) $ciphertext = Core::ourSubstr( $ciphertext, - $config['MAC_BYTE_SIZE'] + $config['SALT_SIZE'] + $config->macByteSize() + $config->saltByteSize() ); if ($ciphertext === false) { throw new Ex\CannotPerformOperationException(); } // Regenerate the same authentication sub-key. - $akey = Core::HKDF($config['HASH_FUNCTION'], $key, $config['KEY_BYTE_SIZE'], $config['AUTHENTICATION_INFO'], $salt, $config); + $akey = Core::HKDF($config->hashFunctionName(), $key, $config->keyByteSize(), $config->authenticationInfoString(), $salt, $config); if (self::verifyHMAC($hmac, $version . $salt . $ciphertext, $akey, $config)) { // Regenerate the same encryption sub-key. - $ekey = Core::HKDF($config['HASH_FUNCTION'], $key, $config['KEY_BYTE_SIZE'], $config['ENCRYPTION_INFO'], $salt, $config); + $ekey = Core::HKDF($config->hashFunctionName(), $key, $config->keyByteSize(), $config->encryptionInfoString(), $salt, $config); // Extract the initialization vector from the ciphertext. Core::EnsureFunctionExists("openssl_cipher_iv_length"); - $ivsize = \openssl_cipher_iv_length($config['CIPHER_METHOD']); + $ivsize = \openssl_cipher_iv_length($config->cipherMethod()); if ($ivsize === false || $ivsize <= 0) { throw new Ex\CannotPerformOperationException( "Could not get the IV length from OpenSSL" @@ -265,26 +266,26 @@ public static function legacyDecrypt($ciphertext, $key) $config = self::getVersionConfigFromHeader(Core::LEGACY_VERSION, Core::LEGACY_VERSION); // Extract the HMAC from the front of the ciphertext. - if (Core::ourStrlen($ciphertext) <= $config['MAC_BYTE_SIZE']) { + if (Core::ourStrlen($ciphertext) <= $config->macByteSize()) { throw new Ex\InvalidCiphertextException( "Ciphertext is too short." ); } - $hmac = Core::ourSubstr($ciphertext, 0, $config['MAC_BYTE_SIZE']); + $hmac = Core::ourSubstr($ciphertext, 0, $config->macByteSize()); if ($hmac === false) { throw new Ex\CannotPerformOperationException(); } - $ciphertext = Core::ourSubstr($ciphertext, $config['MAC_BYTE_SIZE']); + $ciphertext = Core::ourSubstr($ciphertext, $config->macByteSize()); if ($ciphertext === false) { throw new Ex\CannotPerformOperationException(); } // Regenerate the same authentication sub-key. $akey = Core::HKDF( - $config['HASH_FUNCTION'], + $config->hashFunctionName(), $key, - $config['KEY_BYTE_SIZE'], - $config['AUTHENTICATION_INFO'], + $config->keyByteSize(), + $config->authenticationInfoString(), null, $config ); @@ -292,17 +293,17 @@ public static function legacyDecrypt($ciphertext, $key) if (self::verifyHMAC($hmac, $ciphertext, $akey, $config)) { // Regenerate the same encryption sub-key. $ekey = Core::HKDF( - $config['HASH_FUNCTION'], + $config->hashFunctionName(), $key, - $config['KEY_BYTE_SIZE'], - $config['ENCRYPTION_INFO'], + $config->keyByteSize(), + $config->encryptionInfoString(), null, $config ); // Extract the initialization vector from the ciphertext. Core::EnsureFunctionExists("openssl_cipher_iv_length"); - $ivsize = \openssl_cipher_iv_length($config['CIPHER_METHOD']); + $ivsize = \openssl_cipher_iv_length($config->cipherMethod()); if ($ivsize === false || $ivsize <= 0) { throw new Ex\CannotPerformOperationException( "Could not get the IV length from OpenSSL" @@ -355,7 +356,7 @@ protected static function plainEncrypt($plaintext, $key, $iv, $config) Core::ensureFunctionExists("openssl_encrypt"); $ciphertext = \openssl_encrypt( $plaintext, - $config['CIPHER_METHOD'], + $config->cipherMethod(), $key, OPENSSL_RAW_DATA, $iv @@ -388,7 +389,7 @@ protected static function plainDecrypt($ciphertext, $key, $iv, $config) Core::ensureFunctionExists("openssl_decrypt"); $plaintext = \openssl_decrypt( $ciphertext, - $config['CIPHER_METHOD'], + $config->cipherMethod(), $key, OPENSSL_RAW_DATA, $iv @@ -413,7 +414,7 @@ protected static function plainDecrypt($ciphertext, $key, $iv, $config) */ protected static function verifyHMAC($correct_hmac, $message, $key, $config) { - $message_hmac = \hash_hmac($config['HASH_FUNCTION'], $message, $key, true); + $message_hmac = \hash_hmac($config->hashFunctionName(), $message, $key, true); return Core::hashEquals($correct_hmac, $message_hmac); } @@ -462,16 +463,16 @@ protected static function getVersionConfigFromMajorMinor($major, $minor) if ($major === 2) { switch ($minor) { case 0: - return [ - 'CIPHER_METHOD' => 'aes-256-ctr', - 'BLOCK_SIZE' => 16, - 'KEY_BYTE_SIZE' => 32, - 'SALT_SIZE' => 16, - 'HASH_FUNCTION' => 'sha256', - 'MAC_BYTE_SIZE' => 32, - 'ENCRYPTION_INFO' => 'DefusePHP|V2|KeyForEncryption', - 'AUTHENTICATION_INFO' => 'DefusePHP|V2|KeyForAuthentication' - ]; + return new Config([ + 'cipher_method' => 'aes-256-ctr', + 'block_byte_size' => 16, + 'key_byte_size' => 32, + 'salt_byte_size' => 16, + 'hash_function_name' => 'sha256', + 'mac_byte_size' => 32, + 'encryption_info_string' => 'DefusePHP|V2|KeyForEncryption', + 'authentication_info_string' => 'DefusePHP|V2|KeyForAuthentication' + ]); default: throw new Ex\InvalidCiphertextException( "Unsupported ciphertext version." @@ -480,15 +481,16 @@ protected static function getVersionConfigFromMajorMinor($major, $minor) } elseif ($major === 1) { switch ($minor) { case 0: - return [ - 'CIPHER_METHOD' => 'aes-128-cbc', - 'BLOCK_SIZE' => 16, - 'KEY_BYTE_SIZE' => 16, - 'HASH_FUNCTION' => 'sha256', - 'MAC_BYTE_SIZE' => 32, - 'ENCRYPTION_INFO' => 'DefusePHP|KeyForEncryption', - 'AUTHENTICATION_INFO' => 'DefusePHP|KeyForAuthentication' - ]; + return new Config([ + 'cipher_method' => 'aes-128-cbc', + 'block_byte_size' => 16, + 'key_byte_size' => 16, + 'salt_byte_size' => false, + 'hash_function_name' => 'sha256', + 'mac_byte_size' => 32, + 'encryption_info_string' => 'DefusePHP|KeyForEncryption', + 'authentication_info_string' => 'DefusePHP|KeyForAuthentication' + ]); default: throw new Ex\InvalidCiphertextException( "Unsupported ciphertext version." diff --git a/src/File.php b/src/File.php index 08dc198..c07f031 100644 --- a/src/File.php +++ b/src/File.php @@ -4,6 +4,7 @@ use \Defuse\Crypto\Exception as Ex; use \Defuse\Crypto\Core; +use \Defuse\Crypto\FileConfig; /* * PHP Encryption Library @@ -49,7 +50,7 @@ public static function createNewRandomKey() Core::CURRENT_FILE_VERSION, Core::CURRENT_FILE_VERSION ); - return Core::secureRandom($config['KEY_BYTE_SIZE']); + return Core::secureRandom($config->keyByteSize()); } /** @@ -236,40 +237,40 @@ public static function encryptResource($inputHandle, $outputHandle, $key) ); // Let's add this check before anything - if (!\in_array($config['HASH_FUNCTION'], \hash_algos())) { + if (!\in_array($config->hashFunctionName(), \hash_algos())) { throw new Ex\CannotPerformOperationException( 'The specified hash function does not exist' ); } // Sanity check; key must be the appropriate length! - if (Core::ourStrlen($key) !== $config['KEY_BYTE_SIZE']) { + if (Core::ourStrlen($key) !== $config->keyByteSize()) { throw new Ex\InvalidInput( - 'Invalid key length. Keys should be '.$config['KEY_BYTE_SIZE'].' bytes long.' + 'Invalid key length. Keys should be '.$config->keyByteSize().' bytes long.' ); } /** * Let's split our keys */ - $file_salt = Core::secureRandom($config['SALT_SIZE']); + $file_salt = Core::secureRandom($config->saltByteSize()); // $ekey -- Encryption Key -- used for AES $ekey = Core::HKDF( - $config['HASH_FUNCTION'], + $config->hashFunctionName(), $key, - $config['KEY_BYTE_SIZE'], - $config['ENCRYPTION_INFO'], + $config->keyByteSize(), + $config->encryptionInfoString(), $file_salt, $config ); // $akey -- Authentication Key -- used for HMAC $akey = Core::HKDF( - $config['HASH_FUNCTION'], + $config->hashFunctionName(), $key, - $config['KEY_BYTE_SIZE'], - $config['AUTHENTICATION_INFO'], + $config->keyByteSize(), + $config->authenticationInfoString(), $file_salt, $config ); @@ -278,7 +279,7 @@ public static function encryptResource($inputHandle, $outputHandle, $key) * Generate a random initialization vector. */ Core::ensureFunctionExists("openssl_cipher_iv_length"); - $ivsize = \openssl_cipher_iv_length($config['CIPHER_METHOD']); + $ivsize = \openssl_cipher_iv_length($config->cipherMethod()); if ($ivsize === false || $ivsize <= 0) { throw new Ex\CannotPerformOperationException( 'Improper IV size' @@ -292,7 +293,7 @@ public static function encryptResource($inputHandle, $outputHandle, $key) if (\fwrite( $outputHandle, Core::CURRENT_FILE_VERSION . $file_salt . $iv, - Core::HEADER_VERSION_SIZE + $config['SALT_SIZE'] + $ivsize + Core::HEADER_VERSION_SIZE + $config->saltByteSize() + $ivsize ) === false) { throw new Ex\CannotPerformOperationException( 'Cannot write to output file' @@ -303,7 +304,7 @@ public static function encryptResource($inputHandle, $outputHandle, $key) * We're going to initialize a HMAC-SHA256 with the given $akey * and update it with each ciphertext chunk */ - $hmac = \hash_init($config['HASH_FUNCTION'], HASH_HMAC, $akey); + $hmac = \hash_init($config->hashFunctionName(), HASH_HMAC, $akey); if ($hmac === false) { throw new Ex\CannotPerformOperationException( 'Cannot initialize a hash context' @@ -320,7 +321,7 @@ public static function encryptResource($inputHandle, $outputHandle, $key) * How much do we increase the counter after each buffered encryption to * prevent nonce reuse? */ - $inc = $config['BUFFER'] / $config['BLOCK_SIZE']; + $inc = $config->bufferByteSize() / $config->blockByteSize(); /** * Let's MAC our salt and IV/nonce @@ -333,7 +334,7 @@ public static function encryptResource($inputHandle, $outputHandle, $key) * Iterate until we reach the end of the input file */ while (!\feof($inputHandle)) { - $read = \fread($inputHandle, $config['BUFFER']); + $read = \fread($inputHandle, $config->bufferByteSize()); if ($read === false) { throw new Ex\CannotPerformOperationException( 'Cannot read input file' @@ -346,7 +347,7 @@ public static function encryptResource($inputHandle, $outputHandle, $key) */ $encrypted = \openssl_encrypt( $read, - $config['CIPHER_METHOD'], + $config->cipherMethod(), $ekey, OPENSSL_RAW_DATA, $thisIv @@ -378,7 +379,7 @@ public static function encryptResource($inputHandle, $outputHandle, $key) // Now let's get our HMAC and append it $finalHMAC = \hash_final($hmac, true); - $appended = \fwrite($outputHandle, $finalHMAC, $config['MAC_BYTE_SIZE']); + $appended = \fwrite($outputHandle, $finalHMAC, $config->macByteSize()); if ($appended === false) { throw new Ex\CannotPerformOperationException( 'Cannot write to output file' @@ -424,20 +425,20 @@ public static function decryptResource($inputHandle, $outputHandle, $key) ); // Let's add this check before anything - if (!\in_array($config['HASH_FUNCTION'], \hash_algos())) { + if (!\in_array($config->hashFunctionName(), \hash_algos())) { throw new Ex\CannotPerformOperationException( 'The specified hash function does not exist' ); } // Sanity check; key must be the appropriate length! - if (Core::ourStrlen($key) !== $config['KEY_BYTE_SIZE']) { + if (Core::ourStrlen($key) !== $config->keyByteSize()) { throw new Ex\InvalidInput( - 'Invalid key length. Keys should be '.$config['KEY_BYTE_SIZE'].' bytes long.' + 'Invalid key length. Keys should be '.$config->keyByteSize().' bytes long.' ); } // Let's grab the file salt. - $file_salt = \fread($inputHandle, $config['SALT_SIZE']); + $file_salt = \fread($inputHandle, $config->saltByteSize()); if ($file_salt === false ) { throw new Ex\CannotPerformOperationException( 'Cannot read input file' @@ -456,10 +457,10 @@ public static function decryptResource($inputHandle, $outputHandle, $key) * $ekey -- Encryption Key -- used for AES */ $ekey = Core::HKDF( - $config['HASH_FUNCTION'], + $config->hashFunctionName(), $key, - $config['KEY_BYTE_SIZE'], - $config['ENCRYPTION_INFO'], + $config->keyByteSize(), + $config->encryptionInfoString(), $file_salt, $config ); @@ -468,10 +469,10 @@ public static function decryptResource($inputHandle, $outputHandle, $key) * $akey -- Authentication Key -- used for HMAC */ $akey = Core::HKDF( - $config['HASH_FUNCTION'], + $config->hashFunctionName(), $key, - $config['KEY_BYTE_SIZE'], - $config['AUTHENTICATION_INFO'], + $config->keyByteSize(), + $config->authenticationInfoString(), $file_salt, $config ); @@ -481,7 +482,7 @@ public static function decryptResource($inputHandle, $outputHandle, $key) * * It should be the first N blocks of the file (N = 16) */ - $ivsize = \openssl_cipher_iv_length($config['CIPHER_METHOD']); + $ivsize = \openssl_cipher_iv_length($config->cipherMethod()); $iv = \fread($inputHandle, $ivsize); if ($iv === false ) { throw new Ex\CannotPerformOperationException( @@ -490,7 +491,7 @@ public static function decryptResource($inputHandle, $outputHandle, $key) } // How much do we increase the counter after each buffered encryption to prevent nonce reuse - $inc = $config['BUFFER'] / $config['BLOCK_SIZE']; + $inc = $config->bufferByteSize() / $config->blockByteSize(); $thisIv = $iv; @@ -499,7 +500,7 @@ public static function decryptResource($inputHandle, $outputHandle, $key) * * It should be the last N blocks of the file (N = 32) */ - if (\fseek($inputHandle, (-1 * $config['MAC_BYTE_SIZE']), SEEK_END) === false) { + if (\fseek($inputHandle, (-1 * $config->macByteSize()), SEEK_END) === false) { throw new Ex\CannotPerformOperationException( 'Cannot seek to beginning of MAC within input file' ); @@ -515,7 +516,7 @@ public static function decryptResource($inputHandle, $outputHandle, $key) --$cipher_end; // We need to subtract one // We keep our MAC stored in this variable - $stored_mac = \fread($inputHandle, $config['MAC_BYTE_SIZE']); + $stored_mac = \fread($inputHandle, $config->macByteSize()); if ($stored_mac === false) { throw new Ex\CannotPerformOperationException( 'Cannot read input file' @@ -525,7 +526,7 @@ public static function decryptResource($inputHandle, $outputHandle, $key) /** * We begin recalculating the HMAC for the entire file... */ - $hmac = \hash_init($config['HASH_FUNCTION'], HASH_HMAC, $akey); + $hmac = \hash_init($config->hashFunctionName(), HASH_HMAC, $akey); if ($hmac === false) { throw new Ex\CannotPerformOperationException( 'Cannot initialize a hash context' @@ -544,7 +545,7 @@ public static function decryptResource($inputHandle, $outputHandle, $key) /** * Set it to the first non-salt and non-IV byte */ - if (\fseek($inputHandle, $config['SALT_SIZE'] + $ivsize, SEEK_CUR) === false) { + if (\fseek($inputHandle, $config->saltByteSize() + $ivsize, SEEK_CUR) === false) { throw new Ex\CannotPerformOperationException( 'Cannot read seek input file to beginning of ciphertext' ); @@ -576,11 +577,11 @@ public static function decryptResource($inputHandle, $outputHandle, $key) * Would a full DBUFFER read put it past the end of the * ciphertext? If so, only return a portion of the file. */ - if ($pos + $config['BUFFER'] >= $cipher_end) { + if ($pos + $config->bufferByteSize() >= $cipher_end) { $break = true; $read = \fread($inputHandle, $cipher_end - $pos + 1); } else { - $read = \fread($inputHandle, $config['BUFFER']); + $read = \fread($inputHandle, $config->bufferByteSize()); } if ($read === false) { throw new Ex\CannotPerformOperationException( @@ -621,7 +622,7 @@ public static function decryptResource($inputHandle, $outputHandle, $key) /** * Return file pointer to the first non-header, non-IV byte in the file */ - if (\fseek($inputHandle, $config['SALT_SIZE'] + $ivsize + Core::HEADER_VERSION_SIZE, SEEK_SET) === false) { + if (\fseek($inputHandle, $config->saltByteSize() + $ivsize + Core::HEADER_VERSION_SIZE, SEEK_SET) === false) { throw new Ex\CannotPerformOperationException( 'Could not move the input file pointer during decryption' ); @@ -651,11 +652,11 @@ public static function decryptResource($inputHandle, $outputHandle, $key) * Would a full BUFFER read put it past the end of the * ciphertext? If so, only return a portion of the file. */ - if ($pos + $config['BUFFER'] >= $cipher_end) { + if ($pos + $config->bufferByteSize() >= $cipher_end) { $breakW = true; $read = \fread($inputHandle, $cipher_end - $pos + 1); } else { - $read = \fread($inputHandle, $config['BUFFER']); + $read = \fread($inputHandle, $config->bufferByteSize()); } if ($read === false) { throw new Ex\CannotPerformOperationException( @@ -694,7 +695,7 @@ public static function decryptResource($inputHandle, $outputHandle, $key) */ $decrypted = \openssl_decrypt( $read, - $config['CIPHER_METHOD'], + $config->cipherMethod(), $ekey, OPENSSL_RAW_DATA, $thisIv @@ -776,17 +777,17 @@ private static function getFileVersionConfigFromMajorMinor($major, $minor) if ($major === 2) { switch ($minor) { case 0: - return [ - 'CIPHER_METHOD' => 'aes-256-ctr', - 'BLOCK_SIZE' => 16, - 'KEY_BYTE_SIZE' => 32, - 'SALT_SIZE' => 16, - 'HASH_FUNCTION' => 'sha256', - 'MAC_BYTE_SIZE' => 32, - 'ENCRYPTION_INFO' => 'DefusePHP|V2File|KeyForEncryption', - 'AUTHENTICATION_INFO' => 'DefusePHP|V2File|KeyForAuthentication', - 'BUFFER' => 1048576 - ]; + return new FileConfig([ + 'cipher_method' => 'aes-256-ctr', + 'block_byte_size' => 16, + 'key_byte_size' => 32, + 'salt_byte_size' => 16, + 'hash_function_name' => 'sha256', + 'mac_byte_size' => 32, + 'encryption_info_string' => 'DefusePHP|V2File|KeyForEncryption', + 'authentication_info_string' => 'DefusePHP|V2File|KeyForAuthentication', + 'buffer_byte_size' => 1048576 + ]); default: throw new Ex\InvalidCiphertextException( "Unsupported file ciphertext version." diff --git a/src/FileConfig.php b/src/FileConfig.php new file mode 100644 index 0000000..5bf98ac --- /dev/null +++ b/src/FileConfig.php @@ -0,0 +1,34 @@ +buffer_byte_size = $config_array["buffer_byte_size"]; + if (!is_int($this->buffer_byte_size) || $this->buffer_byte_size <= 0) { + throw new Ex\CannotPerformOperationException( + "Configuration contains an invalid buffer byte size." + ); + } + + unset($config_array["buffer_byte_size"]); + parent::__construct($config_array); + } + + public function bufferByteSize() + { + return $this->buffer_byte_size; + } +} diff --git a/src/RuntimeTests.php b/src/RuntimeTests.php index 1514d95..770c6a5 100644 --- a/src/RuntimeTests.php +++ b/src/RuntimeTests.php @@ -48,7 +48,7 @@ public static function runtimeTest() $test_state = 2; Core::ensureFunctionExists('openssl_get_cipher_methods'); - if (\in_array($config['CIPHER_METHOD'], \openssl_get_cipher_methods()) === false) { + if (\in_array($config->cipherMethod(), \openssl_get_cipher_methods()) === false) { throw new Ex\CryptoTestFailedException("Cipher method not supported."); } @@ -57,11 +57,11 @@ public static function runtimeTest() RuntimeTests::HKDFTestVector($config); RuntimeTests::testEncryptDecrypt($config); - if (Core::ourStrlen(Crypto::createNewRandomKey()->getRawBytes()) != $config['KEY_BYTE_SIZE']) { + if (Core::ourStrlen(Crypto::createNewRandomKey()->getRawBytes()) != $config->keyByteSize()) { throw new Ex\CryptoTestFailedException(); } - if ($config['ENCRYPTION_INFO'] == $config['AUTHENTICATION_INFO']) { + if ($config->encryptionInfoString() == $config->authenticationInfoString()) { throw new Ex\CryptoTestFailedException(); } } catch (Ex\CryptoTestFailedException $ex) { @@ -121,7 +121,7 @@ private static function testEncryptDecrypt($config) // Ciphertext too small (shorter than HMAC). $key = Crypto::createNewRandomKey(); - $ciphertext = \str_repeat("A", $config['MAC_BYTE_SIZE'] - 1); + $ciphertext = \str_repeat("A", $config->macByteSize() - 1); try { Crypto::decrypt($ciphertext, $key, true); throw new Ex\CryptoTestFailedException(); @@ -184,7 +184,7 @@ private static function HMACTestVector($config) $key = \str_repeat("\x0b", 20); $data = "Hi There"; $correct = "b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7"; - if (\hash_hmac($config['HASH_FUNCTION'], $data, $key) !== $correct) { + if (\hash_hmac($config->hashFunctionName(), $data, $key) !== $correct) { throw new Ex\CryptoTestFailedException(); } } diff --git a/test/unit/CtrModeTest.php b/test/unit/CtrModeTest.php index deea3f5..bf16118 100644 --- a/test/unit/CtrModeTest.php +++ b/test/unit/CtrModeTest.php @@ -1,6 +1,18 @@ 'aes-128-ctr', - ]; - + $config = new MockConfig; $actual_end = \Defuse\Crypto\Core::incrementCounter(\hex2bin($start), $inc, $config); $this->assertEquals( $end, @@ -86,9 +95,7 @@ public function testIncrementCounterTestVector($start, $end, $inc) public function testFuzzIncrementCounter() { - $config = [ - 'CIPHER_METHOD' => 'aes-128-ctr', - ]; + $config = new MockConfig; /* Test carry propagation. */ for ($offset = 0; $offset < 16; $offset++) { @@ -147,9 +154,7 @@ public function testFuzzIncrementCounter() */ public function testIncrementByNegativeValue() { - $config = [ - 'CIPHER_METHOD' => 'aes-128-ctr', - ]; + $config = new MockConfig; \Defuse\Crypto\Core::incrementCounter( str_repeat("\x00", 16), @@ -174,9 +179,8 @@ public function allNonZeroByteValuesProvider() */ public function testIncrementCausingOverflowInFirstByte($lsb) { - $config = [ - 'CIPHER_METHOD' => 'aes-128-ctr', - ]; + $config = new MockConfig; + /* Smallest value that will overflow. */ $increment = (PHP_INT_MAX - $lsb) + 1; $start = str_repeat("\x00", 15) . chr($lsb); @@ -188,9 +192,8 @@ public function testIncrementCausingOverflowInFirstByte($lsb) */ public function testIncrementWithShortIvLength() { - $config = [ - 'CIPHER_METHOD' => 'aes-128-ctr', - ]; + $config = new MockConfig; + \Defuse\Crypto\Core::incrementCounter( str_repeat("\x00", 15), 1, @@ -203,9 +206,8 @@ public function testIncrementWithShortIvLength() */ public function testIncrementWithLongIvLength() { - $config = [ - 'CIPHER_METHOD' => 'aes-128-ctr', - ]; + $config = new MockConfig; + \Defuse\Crypto\Core::incrementCounter( str_repeat("\x00", 17), 1, @@ -218,9 +220,8 @@ public function testIncrementWithLongIvLength() */ public function testIncrementByNonInteger() { - $config = [ - 'CIPHER_METHOD' => 'aes-128-ctr', - ]; + $config = new MockConfig; + \Defuse\Crypto\Core::incrementCounter( str_repeat("\x00", 16), 1.0, @@ -230,9 +231,7 @@ public function testIncrementByNonInteger() public function testCompatibilityWithOpenSSL() { - $config = [ - 'CIPHER_METHOD' => 'aes-128-ctr', - ]; + $config = new MockConfig; /* Plaintext is 0x300 blocks. */ $plaintext = str_repeat('a', 0x300 * 16); @@ -242,7 +241,7 @@ public function testCompatibilityWithOpenSSL() $ciphertext = openssl_encrypt( $plaintext, - $config['CIPHER_METHOD'], + $config->cipherMethod(), 'YELLOW SUBMARINE', OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING, $starting_nonce @@ -261,7 +260,7 @@ public function testCompatibilityWithOpenSSL() /* Try to decrypt it using that nonce. */ $decrypt = openssl_decrypt( $cipher_lasthalf, - $config['CIPHER_METHOD'], + $config->cipherMethod(), 'YELLOW SUBMARINE', OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING, $computed_nonce From 35c618a549913fda864bb875d3831a912b1f2ab2 Mon Sep 17 00:00:00 2001 From: Taylor Hornby Date: Mon, 19 Oct 2015 00:03:32 -0600 Subject: [PATCH 34/35] Use HEADER_VERSION_SIZE instead of magic number 4. --- src/File.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/File.php b/src/File.php index c07f031..3db66bb 100644 --- a/src/File.php +++ b/src/File.php @@ -411,12 +411,12 @@ public static function decryptResource($inputHandle, $outputHandle, $key) ); } - // Parse the header, ensuring we get 4 bytes + // Parse the header. $header = ''; - $remaining = 4; + $remaining = Core::HEADER_VERSION_SIZE; do { $header .= \fread($inputHandle, $remaining); - $remaining = 4 - Core::ourStrlen($header); + $remaining = Core::HEADER_VERSION_SIZE - Core::ourStrlen($header); } while ($remaining > 0); $config = self::getFileVersionConfigFromHeader( From 6c1d4bc31d88c4118a282b8a25524d5e6fb9eae3 Mon Sep 17 00:00:00 2001 From: Taylor Hornby Date: Mon, 19 Oct 2015 00:17:10 -0600 Subject: [PATCH 35/35] Make Key not use an error-prone array for config options. --- src/Config.php | 14 +++---- src/Key.php | 103 ++++++++++++++++++++++++++++++++++++++++--------- 2 files changed, 92 insertions(+), 25 deletions(-) diff --git a/src/Config.php b/src/Config.php index 4052577..9be0d59 100644 --- a/src/Config.php +++ b/src/Config.php @@ -29,14 +29,14 @@ public function __construct($config_array) ); if (sort($expected_keys) !== true) { throw Ex\CannotPerformOperationException( - "sort() failed.\n" + "sort() failed." ); } $actual_keys = array_keys($config_array); if (sort($actual_keys) !== true) { throw Ex\CannotPerformOperationException( - "sort() failed.\n" + "sort() failed." ); } @@ -62,13 +62,13 @@ public function __construct($config_array) ); } - if (!is_int($this->block_byte_size) || $this->block_byte_size <= 0) { + if (!\is_int($this->block_byte_size) || $this->block_byte_size <= 0) { throw new Ex\CannotPerformOperationException( "Configuration contains an invalid block byte size." ); } - if (!is_int($this->key_byte_size) || $this->key_byte_size <= 0) { + if (!\is_int($this->key_byte_size) || $this->key_byte_size <= 0) { throw new Ex\CannotPerformOperationException( "Configuration contains an invalid key byte size." ); @@ -82,7 +82,7 @@ public function __construct($config_array) } } - if (!is_int($this->mac_byte_size) || $this->mac_byte_size <= 0) { + if (!\is_int($this->mac_byte_size) || $this->mac_byte_size <= 0) { throw new Ex\CannotPerformOperationException( "Configuration contains an invalid MAC byte size." ); @@ -94,13 +94,13 @@ public function __construct($config_array) ); } - if (!is_string($this->encryption_info_string) || $this->encryption_info_string === "") { + if (!\is_string($this->encryption_info_string) || $this->encryption_info_string === "") { throw new Ex\CannotPerformOperationException( "Configuration contains an invalid encryption info string." ); } - if (!is_string($this->authentication_info_string) || $this->authentication_info_string === "") { + if (!\is_string($this->authentication_info_string) || $this->authentication_info_string === "") { throw new Ex\CannotPerformOperationException( "Configuration contains an invalid authentication info string." ); diff --git a/src/Key.php b/src/Key.php index 5946110..3596261 100644 --- a/src/Key.php +++ b/src/Key.php @@ -5,6 +5,77 @@ use \Defuse\Crypto\Core; use \Defuse\Crypto\Encoding; +final class KeyConfig +{ + private $key_byte_size; + private $checksum_hash_function; + private $checksum_byte_size; + + public function __construct($config_array) + { + $expected_keys = array( + "key_byte_size", + "checksum_hash_function", + "checksum_byte_size" + ); + if (sort($expected_keys) !== true) { + throw Ex\CannotPerformOperationException( + "sort() failed." + ); + } + + $actual_keys = array_keys($config_array); + if (sort($actual_keys) !== true) { + throw Ex\CannotPerformOperationException( + "sort() failed." + ); + } + + if ($expected_keys !== $actual_keys) { + throw new Ex\CannotPerformOperationException( + "Trying to instantiate a bad key configuration." + ); + } + + $this->key_byte_size = $config_array["key_byte_size"]; + $this->checksum_hash_function = $config_array["checksum_hash_function"]; + $this->checksum_byte_size = $config_array["checksum_byte_size"]; + + if (!\is_int($this->key_byte_size) || $this->key_byte_size <= 0) { + throw new Ex\CannotPerformOperationException( + "Invalid key byte size." + ); + } + + if (\in_array($this->checksum_hash_function, \hash_algos()) === false) { + throw new Ex\CannotPerformOperationException( + "Invalid hash function name." + ); + } + + if (!\is_int($this->checksum_byte_size) || $this->checksum_byte_size <= 0) { + throw new Ex\CannotPerformOperationException( + "Invalid checksum byte size." + ); + } + } + + public function keyByteSize() + { + return $this->key_byte_size; + } + + public function checksumHashFunction() + { + return $this->checksum_hash_function; + } + + public function checksumByteSize() + { + return $this->checksum_byte_size; + } +} + final class Key { /* We keep the key versioning independent of the ciphertext versioning. */ @@ -59,7 +130,7 @@ final class Key public static function CreateNewRandomKey() { $config = self::GetKeyVersionConfigFromKeyHeader(self::KEY_CURRENT_VERSION); - $bytes = Core::secureRandom($config['KEY_BYTE_SIZE']); + $bytes = Core::secureRandom($config->keyByteSize()); return new Key(self::KEY_CURRENT_VERSION, $bytes); } @@ -88,8 +159,8 @@ public static function LoadFromAsciiSafeString($savedKeyString) /* Now that we know the version, check the length is correct. */ if (Core::ourStrlen($bytes) !== self::KEY_HEADER_SIZE + - $config['KEY_BYTE_SIZE'] + - $config['CHECKSUM_BYTE_SIZE'] ) { + $config->keyByteSize() + + $config->checksumByteSize()) { throw new Ex\CannotPerformOperationException( "Saved Key is not the correct size." ); @@ -99,22 +170,18 @@ public static function LoadFromAsciiSafeString($savedKeyString) $checked_bytes = Core::ourSubstr( $bytes, 0, - self::KEY_HEADER_SIZE + $config['KEY_BYTE_SIZE'] + self::KEY_HEADER_SIZE + $config->keyByteSize() ); /* Grab the included checksum. */ $checksum_a = Core::ourSubstr( $bytes, - self::KEY_HEADER_SIZE + $config['KEY_BYTE_SIZE'], - $config['CHECKSUM_BYTE_SIZE'] + self::KEY_HEADER_SIZE + $config->keyByteSize(), + $config->checksumByteSize() ); /* Re-compute the checksum. */ - $checksum_b = Core::ourSubstr( - hash($config['CHECKSUM_HASH_FUNCTION'], $checked_bytes, true), - 0, - $config['CHECKSUM_BYTE_SIZE'] - ); + $checksum_b = hash($config->checksumHashFunction(), $checked_bytes, true); /* Validate it. It *is* important for this to be constant time. */ if (!Core::hashEquals($checksum_a, $checksum_b)) { @@ -124,7 +191,7 @@ public static function LoadFromAsciiSafeString($savedKeyString) } /* Everything checks out. Grab the key and create a Key object. */ - $key_bytes = Core::ourSubstr($bytes, self::KEY_HEADER_SIZE, $config['KEY_BYTE_SIZE']); + $key_bytes = Core::ourSubstr($bytes, self::KEY_HEADER_SIZE, $config->keyByteSize()); return new Key($version_header, $key_bytes); } @@ -141,7 +208,7 @@ public function saveToAsciiSafeString() $this->key_version_header . $this->key_bytes . hash( - $this->config['CHECKSUM_HASH_FUNCTION'], + $this->config->checksumHashFunction(), $this->key_version_header . $this->key_bytes, true ) @@ -166,11 +233,11 @@ public function getRawBytes() private static function GetKeyVersionConfigFromKeyHeader($key_header) { if ($key_header === self::KEY_CURRENT_VERSION) { - return [ - 'KEY_BYTE_SIZE' => 32, - 'CHECKSUM_HASH_FUNCTION' => 'sha256', - 'CHECKSUM_BYTE_SIZE' => 32 - ]; + return new KeyConfig([ + 'key_byte_size' => 32, + 'checksum_hash_function' => 'sha256', + 'checksum_byte_size' => 32 + ]); } throw new Ex\CannotPerformOperationException( "Invalid key version header."