diff --git a/doc/01-Formats.md b/doc/01-Formats.md new file mode 100644 index 0000000..f7ca32f --- /dev/null +++ b/doc/01-Formats.md @@ -0,0 +1,9 @@ +# Cryptographic Message Formats + +## `\Defuse\Crypto\Crypto` + +`[____HMAC____][____IV____][____CIPHERTEXT____]` + +## `\Defuse\Crypto\File` + +`[____FIRST_NONCE____][____CIPHERTEXT____][____HMAC____]` diff --git a/doc/02-Crypto.php.md b/doc/02-Crypto.php.md new file mode 100644 index 0000000..0234697 --- /dev/null +++ b/doc/02-Crypto.php.md @@ -0,0 +1,79 @@ +Symmetric Key Encryption +======================== + +At a glance: + +* **Cipher and Mode**: `AES-128-CBC` +* **Padding**: `PKCS#7` +* **Authentication**: `HMAC-SHA-256` +* **Construction**: `Encrypt then MAC` +* **Algorithm Backend**: `ext/openssl` + +## `\Defuse\Crypto\Crypto` + +**WARNING:** This encryption library is not a silver bullet. It only provides +symmetric encryption given a uniformly random key. This means you **MUST NOT** +use an ASCII string like a password as the key parameter, it **MUST** be +a uniformly random key generated by `createNewRandomKey()`. If you want to +encrypt something with a password, apply a password key derivation function +like PBKDF2 or scrypt with a random salt to generate a key. + +***WARNING:*** Error handling is *very* important, especially for crypto code! + +How to use this code: +===================== + +Generating a Key +---------------- + +```php +try { + $key = \Defuse\Crypto\Crypto::createNewRandomKey(); + // WARNING: Do NOT encode $key with bin2hex() or base64_encode(), + // they may leak the key to the attacker through side channels. + // + // This library offers these two for cache-timing-safe encoding: + // + // \Defuse\Crypto\Crypto\binToHex() + // \Defuse\Crypto\Crypto\hexToBin() + // +} catch (\Defuse\Crypto\Exception\CryptoTestFailed $ex) { + die('Cannot safely create a key'); +} catch (\Defuse\Crypto\Exception\CannotPerformOperation $ex) { + die('Cannot safely create a key'); +} +``` + +Encrypting a Message +-------------------- + +```php +$message = 'ATTACK AT DAWN'; +try { + $ciphertext = \Defuse\Crypto\Crypto::Encrypt($message, $key); +} catch (\Defuse\Crypto\Exception\CryptoTestFailed $ex) { + die('Cannot safely perform encryption'); +} catch (\Defuse\Crypto\Exception\CannotPerformOperation $ex) { + die('Cannot safely perform encryption'); +} +``` + +Decrypting a Message +-------------------- + +```php +try { + $decrypted = self::Decrypt($ciphertext, $key); +} catch (\Defuse\Crypto\Exception\InvalidCiphertext $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) { + die('Cannot safely perform decryption'); +} catch (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 new file mode 100644 index 0000000..c7ce9c4 --- /dev/null +++ b/doc/03-File.php.md @@ -0,0 +1,136 @@ +Symmetric Key File Encryption +============================= + +At a glance: + +* **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` + +## `\Defuse\Crypto\File` + +**WARNING:** This encryption library is not a silver bullet. It only provides +symmetric encryption given a uniformly random key. This means you **MUST NOT** +use an ASCII string like a password as the key parameter, it **MUST** be +a uniformly random key generated by `createNewRandomKey()`. If you want to +encrypt something with a password, apply a password key derivation function +like PBKDF2 or scrypt with a random salt to generate a key. + +***WARNING:*** Error handling is *very* important, especially for crypto code! + +How to use this code: +===================== + +Generating a Key +---------------- + +```php +try { + $key = \Defuse\Crypto\File::createNewRandomKey(); + // WARNING: Do NOT encode $key with bin2hex() or base64_encode(), + // they may leak the key to the attacker through side channels. + // + // This library offers these two for cache-timing-safe encoding: + // + // \Defuse\Crypto\File\binToHex() + // \Defuse\Crypto\File\hexToBin() + // +} catch (\Defuse\Crypto\Exception\CryptoTestFailed $ex) { + die('Cannot safely create a key'); +} catch (\Defuse\Crypto\Exception\CannotPerformOperation $ex) { + die('Cannot safely create a key'); +} + +Encrypting a File +----------------- + +```php +/* + // Don't forget to generate a random key + $key = \Defuse\Crypto\File::createNewRandomKey(); +*/ + +$inputFilename = 'image.jpg'; +$outputFilename = 'image.enc.jpg'; + +try { + \Defuse\Crypto\File::encryptFile( + $inputFilename, + $outputFilename, + $key + ); +} catch (\Defuse\Crypto\Exception\CryptoTestFailed $ex) { + die('Cannot safely perform encryption'); +} catch (\Defuse\Crypto\Exception\CannotPerformOperation $ex) { + die('Cannot safely perform encryption'); +} +``` + +Decrypting a File +----------------- + +```php +/* + // Don't forget to generate a random key + $key = \Defuse\Crypto\File::createNewRandomKey(); +*/ + +$inputFilename = 'image.enc.jpg'; +$outputFilename = 'image.dec.jpg'; + +try { + \Defuse\Crypto\File::decryptFile( + $inputFilename, + $outputFilename, + $key + ); +} catch (\Defuse\Crypto\Exception\CryptoTestFailed $ex) { + die('Cannot safely perform decryption'); +} catch (\Defuse\Crypto\Exception\CannotPerformOperation $ex) { + die('Cannot safely perform decryption'); +} +``` + +Encrypting a File Resource +-------------------------- + +```php +/* + // Don't forget to generate a random key + $key = \Defuse\Crypto\File::createNewRandomKey(); +*/ + +$iFile = \fopen('image2.jpg', 'rb'); +$oFile = \fopen('image2.enc.jpg', 'wb'); + +try { + \Defuse\Crypto\File::encryptResource($iFile, $oFile, $key); +} catch (\Defuse\Crypto\Exception\CryptoTestFailed $ex) { + die('Cannot safely perform encryption'); +} catch (\Defuse\Crypto\Exception\CannotPerformOperation $ex) { + die('Cannot safely perform encryption'); +} +``` + +Decrypting a File Resource +-------------------------- + +```php +/* + // Don't forget to generate a random key + $key = \Defuse\Crypto\File::createNewRandomKey(); +*/ + +$iFile = \fopen('image2.enc.jpg', 'rb'); +$oFile = \fopen('image2.dec.jpg', 'wb'); + +try { + \Defuse\Crypto\File::decryptResource($iFile, $oFile, $key); +} catch (\Defuse\Crypto\Exception\CryptoTestFailed $ex) { + die('Cannot safely perform decryption'); +} catch (\Defuse\Crypto\Exception\CannotPerformOperation $ex) { + die('Cannot safely perform decryption'); +} +``` \ No newline at end of file diff --git a/doc/README.md b/doc/README.md new file mode 100644 index 0000000..125099e --- /dev/null +++ b/doc/README.md @@ -0,0 +1,4 @@ +# PHP Encryption Documentation + +Web: https://defuse.ca/secure-php-encryption.htm +GitHub: https://github.com/defuse/php-encryption \ No newline at end of file diff --git a/src/Core.php b/src/Core.php new file mode 100644 index 0000000..7cca79a --- /dev/null +++ b/src/Core.php @@ -0,0 +1,332 @@ + 255 * $digest_length) { + throw new Ex\CannotPerformOperation(); + } + + // "if [salt] not provided, is set to a string of HashLen zeroes." + if (\is_null($salt)) { + $salt = \str_repeat("\x00", $digest_length); + } + + // HKDF-Extract: + // PRK = HMAC-Hash(salt, IKM) + // The salt is the HMAC key. + $prk = \hash_hmac($hash, $ikm, $salt, true); + + // HKDF-Expand: + + // This check is useless, but it serves as a reminder to the spec. + if (self::ourStrlen($prk) < $digest_length) { + throw new Ex\CannotPerformOperation(); + } + + // T(0) = '' + $t = ''; + $last_block = ''; + for ($block_index = 1; self::ourStrlen($t) < $length; ++$block_index) { + // T(i) = HMAC-Hash(PRK, T(i-1) | info | 0x??) + $last_block = \hash_hmac( + $hash, + $last_block . $info . \chr($block_index), + $prk, + true + ); + // T = T(1) | T(2) | T(3) | ... | T(N) + $t .= $last_block; + } + + // ORM = first L octets of T + $orm = self::ourSubstr($t, 0, $length); + if ($orm === FALSE) { + throw new Ex\CannotPerformOperation(); + } + return $orm; + } + + /** + * Verify a HMAC without crypto side-channels + * + * @staticvar boolean $native Use native hash_equals()? + * @param string $expected string (raw binary) + * @param string $given string (raw binary) + * @return boolean + * @throws Ex\CannotPerformOperation + */ + protected static function hashEquals($expected, $given) + { + static $native = null; + + if ($native === null) { + $native = \function_exists('hash_equals'); + } + if ($native) { + return \hash_equals($expected, $given); + } + + // We can't just compare the strings with '==', since it would make + // timing attacks possible. We could use the XOR-OR constant-time + // comparison algorithm, but I'm not sure if that's good enough way up + // here in an interpreted language. So we use the method of HMACing the + // strings we want to compare with a random key, then comparing those. + + // NOTE: This leaks information when the strings are not the same + // length, but they should always be the same length here. Enforce it: + if (self::ourStrlen($expected) !== self::ourStrlen($given)) { + throw new Ex\CannotPerformOperation(); + } + + $blind = self::createNewRandomKey(); + $message_compare = hash_hmac(self::HASH_FUNCTION, $given, $blind); + $correct_compare = hash_hmac(self::HASH_FUNCTION, $expected, $blind); + return $correct_compare === $message_compare; + } + + /** + * Verify a HMAC without crypto side-channels + * + * @param string $correct_hmac HMAC string (raw binary) + * @param string $message Ciphertext (raw binary) + * @param string $key Authentication key (raw binary) + * @return boolean + * @throws Ex\CannotPerformOperation + */ + protected static function verifyHMAC($correct_hmac, $message, $key) + { + $message_hmac = hash_hmac(self::HASH_FUNCTION, $message, $key, true); + + return self::hashEquals($correct_hmac, $message_hmac); + } + + /* WARNING: Do not call this function on secrets. It creates side channels. */ + protected static function hexToBytes($hex_string) + { + return \pack("H*", $hex_string); + } + + /** + * If the constant doesn't exist, throw an exception + * + * @param string $name + * @throws Ex\CannotPerformOperation + */ + protected static function ensureConstantExists($name) + { + if (!\defined($name)) { + throw new Ex\CannotPerformOperation(); + } + } + + /** + * If the functon doesn't exist, throw an exception + * + * @param string $name Function name + * @throws Ex\CannotPerformOperation + */ + protected static function ensureFunctionExists($name) + { + if (!\function_exists($name)) { + throw new Ex\CannotPerformOperation(); + } + } + + /* + * We need these strlen() and substr() functions because when + * 'mbstring.func_overload' is set in php.ini, the standard strlen() and + * substr() are replaced by mb_strlen() and mb_substr(). + */ + + /** + * Safe string length + * + * @staticvar boolean $exists + * @param string $str + * @return int + */ + protected static function ourStrlen($str) + { + static $exists = null; + if ($exists === null) { + $exists = \function_exists('mb_strlen'); + } + if ($exists) { + $length = \mb_strlen($str, '8bit'); + if ($length === FALSE) { + throw new Ex\CannotPerformOperation(); + } + return $length; + } else { + return \strlen($str); + } + } + + /** + * Safe substring + * + * @staticvar boolean $exists + * @param string $str + * @param int $start + * @param int $length + * @return string + */ + protected static function ourSubstr($str, $start, $length = null) + { + static $exists = null; + if ($exists === null) { + $exists = \function_exists('mb_substr'); + } + if ($exists) + { + // mb_substr($str, 0, NULL, '8bit') returns an empty string on PHP + // 5.3, so we have to find the length ourselves. + if (!isset($length)) { + if ($start >= 0) { + $length = self::ourStrlen($str) - $start; + } else { + $length = -$start; + } + } + + return \mb_substr($str, $start, $length, '8bit'); + } + + // Unlike mb_substr(), substr() doesn't accept NULL for length + if (isset($length)) { + return \substr($str, $start, $length); + } else { + return \substr($str, $start); + } + } + /** + * 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; + + 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 \DomainException( + '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/Crypto.php b/src/Crypto.php index 144a01d..343a5e0 100755 --- a/src/Crypto.php +++ b/src/Crypto.php @@ -30,7 +30,7 @@ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ -final class Crypto +final class Crypto extends Core { // Ciphertext format: [____HMAC____][____IV____][____CIPHERTEXT____]. @@ -48,13 +48,7 @@ final class Crypto * * So, PLEASE, do not change these constants. */ - const CIPHER = 'aes-128'; - const KEY_BYTE_SIZE = 16; const CIPHER_MODE = 'cbc'; - const HASH_FUNCTION = 'sha256'; - const MAC_BYTE_SIZE = 32; - const ENCRYPTION_INFO = 'DefusePHP|KeyForEncryption'; - const AUTHENTICATION_INFO = 'DefusePHP|KeyForAuthentication'; /** * Use this to generate a random encryption key. @@ -194,7 +188,7 @@ public static function decrypt($ciphertext, $key) /* * Runs tests. - * Raises CannotPerformOperationException or CryptoTestFailedException if + * Raises Exception\CannotPerformOperation or Exception\CryptoTestFailed 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. */ @@ -284,7 +278,7 @@ private static function plainDecrypt($ciphertext, $key, $iv) $method = self::CIPHER.'-'.self::CIPHER_MODE; self::ensureConstantExists("OPENSSL_RAW_DATA"); - self::ensureFunctionExists("openssl_encrypt"); + self::ensureFunctionExists("openssl_decrypt"); $plaintext = \openssl_decrypt( $ciphertext, $method, @@ -299,130 +293,6 @@ private static function plainDecrypt($ciphertext, $key, $iv) return $plaintext; } - /** - * Returns a random binary string of length $octets bytes. - * - * @param int $octets - * @return string (raw binary) - * @throws Ex\CannotPerformOperation - */ - private static function secureRandom($octets) - { - self::ensureFunctionExists('openssl_random_pseudo_bytes'); - $secure = false; - $random = \openssl_random_pseudo_bytes($octets, $secure); - if ($random === FALSE || $secure === FALSE) { - throw new Ex\CannotPerformOperation(); - } - return $random; - } - - /** - * Use HKDF to derive multiple keys from one. - * http://tools.ietf.org/html/rfc5869 - * - * @param string $hash Hash Function - * @param string $ikm Initial Keying Material - * @param int $length How many bytes? - * @param string $info What sort of key are we deriving? - * @param string $salt - * @return string - * @throws Ex\CannotPerformOperation - */ - private static function HKDF($hash, $ikm, $length, $info = '', $salt = null) - { - // Find the correct digest length as quickly as we can. - $digest_length = self::MAC_BYTE_SIZE; - if ($hash != self::HASH_FUNCTION) { - $digest_length = self::ourStrlen(\hash_hmac($hash, '', '', true)); - } - - // Sanity-check the desired output length. - if (empty($length) || !\is_int($length) || - $length < 0 || $length > 255 * $digest_length) { - throw new Ex\CannotPerformOperation(); - } - - // "if [salt] not provided, is set to a string of HashLen zeroes." - if (\is_null($salt)) { - $salt = \str_repeat("\x00", $digest_length); - } - - // HKDF-Extract: - // PRK = HMAC-Hash(salt, IKM) - // The salt is the HMAC key. - $prk = \hash_hmac($hash, $ikm, $salt, true); - - // HKDF-Expand: - - // This check is useless, but it serves as a reminder to the spec. - if (self::ourStrlen($prk) < $digest_length) { - throw new Ex\CannotPerformOperation(); - } - - // T(0) = '' - $t = ''; - $last_block = ''; - for ($block_index = 1; self::ourStrlen($t) < $length; ++$block_index) { - // T(i) = HMAC-Hash(PRK, T(i-1) | info | 0x??) - $last_block = \hash_hmac( - $hash, - $last_block . $info . \chr($block_index), - $prk, - true - ); - // T = T(1) | T(2) | T(3) | ... | T(N) - $t .= $last_block; - } - - // ORM = first L octets of T - $orm = self::ourSubstr($t, 0, $length); - if ($orm === FALSE) { - throw new Ex\CannotPerformOperation(); - } - return $orm; - } - - /** - * Verify a HMAC without crypto side-channels - * - * @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) - * @return boolean - * @throws Ex\CannotPerformOperation - */ - private static function verifyHMAC($correct_hmac, $message, $key) - { - static $native = null; - $message_hmac = \hash_hmac(self::HASH_FUNCTION, $message, $key, true); - - if ($native === null) { - $native = \function_exists('hash_equals'); - } - if ($native) { - return \hash_equals($correct_hmac, $message_hmac); - } - - // We can't just compare the strings with '==', since it would make - // timing attacks possible. We could use the XOR-OR constant-time - // comparison algorithm, but I'm not sure if that's good enough way up - // here in an interpreted language. So we use the method of HMACing the - // strings we want to compare with a random key, then comparing those. - - // NOTE: This leaks information when the strings are not the same - // length, but they should always be the same length here. Enforce it: - if (self::ourStrlen($correct_hmac) !== self::ourStrlen($message_hmac)) { - throw new Ex\CannotPerformOperation(); - } - - $blind = self::createNewRandomKey(); - $message_compare = \hash_hmac(self::HASH_FUNCTION, $message_hmac, $blind); - $correct_compare = \hash_hmac(self::HASH_FUNCTION, $correct_hmac, $blind); - return $correct_compare === $message_compare; - } - private static function testEncryptDecrypt() { $key = self::createNewRandomKey(); @@ -434,7 +304,7 @@ private static function testEncryptDecrypt() $decrypted = self::decrypt($ciphertext, $key); } catch (Ex\InvalidCiphertext $ex) { // It's important to catch this and change it into a - // CryptoTestFailedException, otherwise a test failure could trick + // Exception\CryptoTestFailed, otherwise a test failure could trick // the user into thinking it's just an invalid ciphertext! throw new Ex\CryptoTestFailed(); } @@ -485,10 +355,10 @@ private static function HKDFTestVector() // Test Case 1 $ikm = \str_repeat("\x0b", 22); - $salt = self::hexToBytes("000102030405060708090a0b0c"); - $info = self::hexToBytes("f0f1f2f3f4f5f6f7f8f9"); + $salt = self::hexToBin("000102030405060708090a0b0c"); + $info = self::hexToBin("f0f1f2f3f4f5f6f7f8f9"); $length = 42; - $okm = self::hexToBytes( + $okm = self::hexToBin( "3cb25f25faacd57a90434f64d0362f2a" . "2d2d0a90cf1a5a4c5db02d56ecc4c5bf" . "34007208d5b887185865" @@ -501,7 +371,7 @@ private static function HKDFTestVector() // Test Case 7 $ikm = \str_repeat("\x0c", 22); $length = 42; - $okm = self::hexToBytes( + $okm = self::hexToBin( "2c91117204d745f3500d636a62f64f0a" . "b3bae548aa53d423b0d1f27ebba6f5e5" . "673a081d70cce7acfc48" @@ -537,15 +407,15 @@ private static function HMACTestVector() private static function AESTestVector() { // AES CBC mode test vector from NIST SP 800-38A - $key = self::hexToBytes("2b7e151628aed2a6abf7158809cf4f3c"); - $iv = self::hexToBytes("000102030405060708090a0b0c0d0e0f"); - $plaintext = self::hexToBytes( + $key = self::hexToBin("2b7e151628aed2a6abf7158809cf4f3c"); + $iv = self::hexToBin("000102030405060708090a0b0c0d0e0f"); + $plaintext = self::hexToBin( "6bc1bee22e409f96e93d7e117393172a" . "ae2d8a571e03ac9c9eb76fac45af8e51" . "30c81c46a35ce411e5fbc1191a0a52ef" . "f69f2445df4f9b17ad2b417be66c3710" ); - $ciphertext = self::hexToBytes( + $ciphertext = self::hexToBin( "7649abac8119b246cee98e9b12e9197d" . "5086cb9b507219ee95db113a917678b2" . "73bed6b8e3c1743b7116e69e22229516" . @@ -570,161 +440,4 @@ private static function AESTestVector() throw new Ex\CryptoTestFailed(); } } - - /* WARNING: Do not call this function on secrets. It creates side channels. */ - private static function hexToBytes($hex_string) - { - return \pack("H*", $hex_string); - } - - - /** - * If the constant doesn't exist, throw an exception - * - * @param string $name - * @throws Ex\CannotPerformOperation - */ - private static function ensureConstantExists($name) - { - if (!\defined($name)) { - throw new Ex\CannotPerformOperation(); - } - } - - /** - * If the functon doesn't exist, throw an exception - * - * @param string $name Function name - * @throws Ex\CannotPerformOperation - */ - private static function ensureFunctionExists($name) - { - if (!\function_exists($name)) { - throw new Ex\CannotPerformOperation(); - } - } - - /* - * We need these strlen() and substr() functions because when - * 'mbstring.func_overload' is set in php.ini, the standard strlen() and - * substr() are replaced by mb_strlen() and mb_substr(). - */ - - /** - * Safe string length - * - * @staticvar boolean $exists - * @param string $str - * @return int - */ - private static function ourStrlen($str) - { - static $exists = null; - if ($exists === null) { - $exists = \function_exists('mb_strlen'); - } - if ($exists) { - $length = \mb_strlen($str, '8bit'); - if ($length === FALSE) { - throw new Ex\CannotPerformOperation(); - } - return $length; - } else { - return \strlen($str); - } - } - - /** - * Safe substring - * - * @staticvar boolean $exists - * @param string $str - * @param int $start - * @param int $length - * @return string - */ - private static function ourSubstr($str, $start, $length = null) - { - static $exists = null; - if ($exists === null) { - $exists = \function_exists('mb_substr'); - } - if ($exists) - { - // mb_substr($str, 0, NULL, '8bit') returns an empty string on PHP - // 5.3, so we have to find the length ourselves. - if (!isset($length)) { - if ($start >= 0) { - $length = self::ourStrlen($str) - $start; - } else { - $length = -$start; - } - } - - return \mb_substr($str, $start, $length, '8bit'); - } - - // Unlike mb_substr(), substr() doesn't accept NULL for length - if (isset($length)) { - return \substr($str, $start, $length); - } else { - return \substr($str, $start); - } - } - /** - * 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; - - 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 \DomainException( - '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/Exception/InvalidInput.php b/src/Exception/InvalidInput.php new file mode 100644 index 0000000..e74abe5 --- /dev/null +++ b/src/Exception/InvalidInput.php @@ -0,0 +1,7 @@ += $cipher_end) { + $break = true; + $read = \fread($inputHandle, $cipher_end - $pos + 1); + } else { + $read = \fread($inputHandle, self::BUFFER); + } + if ($read === false) { + throw new Ex\CannotPerformOperation( + 'Could not read input file during decryption' + ); + } + /** + * We're updating our HMAC and nothing else + */ + \hash_update($hmac, $read); + + /** + * Store a MAC of each chunk + */ + $chunkMAC = \hash_copy($hmac); + if ($chunkMAC === false) { + throw new Ex\CannotPerformOperation( + 'Cannot duplicate a hash context' + ); + } + $macs []= \hash_final($chunkMAC); + } + /** + * We should now have enough data to generate an identical HMAC + */ + $finalHMAC = \hash_final($hmac, true); + /** + * 3. Did we match? + */ + if (!self::hashEquals($finalHMAC, $stored_mac)) { + throw new Ex\InvalidCiphertext( + 'Message Authentication failure; tampering detected.' + ); + } + /** + * 4. Okay, let's begin decrypting + */ + /** + * Return file pointer to the first non-IV byte in the file + */ + if (\fseek($inputHandle, $ivsize, SEEK_SET) === false) { + throw new Ex\CannotPerformOperation( + 'Could not move the input file pointer during decryption' + ); + } + + /** + * Should we break the writing? + */ + $breakW = false; + + /** + * This loop writes plaintext to the destination file: + */ + $result = null; + while (!$breakW) { + /** + * Get the current position + */ + $pos = \ftell($inputHandle); + if ($pos === false) { + throw new Ex\CannotPerformOperation( + 'Could not get current position in input file during decryption' + ); + } + + /** + * Would a full BUFFER read put it past the end of the + * ciphertext? If so, only return a portion of the file. + */ + if ($pos + self::BUFFER >= $cipher_end) { + $breakW = true; + $read = \fread($inputHandle, $cipher_end - $pos + 1); + } else { + $read = \fread($inputHandle, self::BUFFER); + } + if ($read === false) { + throw new Ex\CannotPerformOperation( + 'Could not read input file during decryption' + ); + } + + /** + * Recalculate the MAC, compare with the one stored in the $macs + * array to ensure attackers couldn't tamper with the file + * after MAC verification + */ + \hash_update($hmac2, $read); + $calcMAC = \hash_copy($hmac2); + if ($calcMAC === false) { + throw new Ex\CannotPerformOperation( + 'Cannot duplicate a hash context' + ); + } + $calc = \hash_final($calcMAC); + + if (empty($macs)) { + throw new Ex\InvalidCiphertext( + 'File was modified after MAC verification' + ); + } elseif (!self::hashEquals(\array_shift($macs), $calc)) { + throw new Ex\InvalidCiphertext( + 'File was modified after MAC verification' + ); + } + + $thisIv = self::incrementCounter($thisIv, $inc); + + /** + * Perform the AES decryption. Decrypts the message. + */ + $decrypted = \openssl_decrypt( + $read, + $method, + $ekey, + OPENSSL_RAW_DATA, + $thisIv + ); + + /** + * Test for decryption faulure + */ + if ($decrypted === false) { + throw new Ex\CannotPerformOperation( + 'OpenSSL decryption error' + ); + } + + /** + * Write the plaintext out to the output file + */ + $result = \fwrite( + $outputHandle, + $decrypted, + self::ourStrlen($decrypted) + ); + + /** + * Check result + */ + if ($result === false) { + throw new Ex\CannotPerformOperation( + 'Could not write to output file during decryption.' + ); + } + } + // This should be an integer + return $result; + } + + /** + * Increment a counter (prevent nonce reuse) + * + * @param string $ctr - raw binary + * @param int $inc - how much? + * + * @return string (raw binary) + */ + protected static function incrementCounter($ctr, $inc) + { + static $ivsize = null; + if ($ivsize === null) { + $ivsize = \openssl_cipher_iv_length(self::CIPHER.'-'.self::CIPHER_MODE); + } + + /** + * 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) & (PHP_INT_MAX - 1); + } + return $ctr; + } +} diff --git a/src/StreamInterface.php b/src/StreamInterface.php new file mode 100644 index 0000000..a155cbb --- /dev/null +++ b/src/StreamInterface.php @@ -0,0 +1,49 @@ +