public function __construct($config_array) { $expected_keys = array("cipher_method", "block_byte_size", "key_byte_size", "salt_byte_size", "mac_byte_size", "hash_function_name", "encryption_info_string", "authentication_info_string"); 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 configuration."); } $this->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."); } }
/** * Runs the runtime tests. * * @throws Ex\EnvironmentIsBrokenException */ 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; 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\EnvironmentIsBrokenException('Tests failed previously.'); } try { $test_state = 2; Core::ensureFunctionExists('openssl_get_cipher_methods'); if (\in_array(Core::CIPHER_METHOD, \openssl_get_cipher_methods()) === false) { throw new Ex\EnvironmentIsBrokenException('Cipher method not supported. This is normally caused by an outdated ' . 'version of OpenSSL (and/or OpenSSL compiled for FIPS compliance). ' . 'Please upgrade to a newer version of OpenSSL that supports ' . Core::CIPHER_METHOD . ' to use this library.'); } RuntimeTests::AESTestVector(); RuntimeTests::HMACTestVector(); RuntimeTests::HKDFTestVector(); RuntimeTests::testEncryptDecrypt(); if (Core::ourStrlen(Key::createNewRandomKey()->getRawBytes()) != Core::KEY_BYTE_SIZE) { throw new Ex\EnvironmentIsBrokenException(); } if (Core::ENCRYPTION_INFO_STRING == Core::AUTHENTICATION_INFO_STRING) { throw new Ex\EnvironmentIsBrokenException(); } } catch (Ex\EnvironmentIsBrokenException $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; }
/** * Raw unauthenticated decryption (insecure on its own). * * @param string $ciphertext * @param string $key * @param string $iv * @param string $cipherMethod * * @throws Ex\EnvironmentIsBrokenException * * @return string */ protected static function plainDecrypt($ciphertext, $key, $iv, $cipherMethod) { Core::ensureConstantExists('OPENSSL_RAW_DATA'); Core::ensureFunctionExists('openssl_decrypt'); $plaintext = \openssl_decrypt($ciphertext, $cipherMethod, $key, OPENSSL_RAW_DATA, $iv); if ($plaintext === false) { throw new Ex\EnvironmentIsBrokenException('openssl_decrypt() failed.'); } return $plaintext; }
/** * Encrypt the contents of a file handle $inputHandle and store the results * in $outputHandle using HKDF of $key to perform authenticated encryption * * @param resource $inputHandle * @param resource $outputHandle * @param Key $key * @return boolean */ public static function encryptResource($inputHandle, $outputHandle, Key $key) { // Because we don't have strict typing in PHP 5 if (!\is_resource($inputHandle)) { throw new Ex\InvalidInput('Input handle must be a resource!'); } if (!\is_resource($outputHandle)) { throw new Ex\InvalidInput('Output handle must be a resource!'); } $config = self::getFileVersionConfigFromHeader(Core::CURRENT_FILE_VERSION, Core::CURRENT_FILE_VERSION); $inputStat = \fstat($inputHandle); $inputSize = $inputStat['size']; // Let's add this check before anything if (!\in_array($config->hashFunctionName(), \hash_algos())) { throw new Ex\CannotPerformOperationException('The specified hash function does not exist'); } /** * Let's split our keys */ $file_salt = Core::secureRandom($config->saltByteSize()); // $ekey -- Encryption Key -- used for AES $ekey = Core::HKDF($config->hashFunctionName(), $key->getRawBytes(), $config->keyByteSize(), $config->encryptionInfoString(), $file_salt, $config); // $akey -- Authentication Key -- used for HMAC $akey = Core::HKDF($config->hashFunctionName(), $key->getRawBytes(), $config->keyByteSize(), $config->authenticationInfoString(), $file_salt, $config); /** * Generate a random initialization vector. */ Core::ensureFunctionExists("openssl_cipher_iv_length"); $ivsize = \openssl_cipher_iv_length($config->cipherMethod()); if ($ivsize === false || $ivsize <= 0) { throw new Ex\CannotPerformOperationException('Improper IV size'); } $iv = Core::secureRandom($ivsize); /** * First let's write our header, file salt, and IV to the first N blocks of the output file */ self::writeBytes($outputHandle, Core::CURRENT_FILE_VERSION . $file_salt . $iv, Core::HEADER_VERSION_SIZE + $config->saltByteSize() + $ivsize); /** * We're going to initialize a HMAC-SHA256 with the given $akey * and update it with each ciphertext chunk */ $hmac = \hash_init($config->hashFunctionName(), HASH_HMAC, $akey); if ($hmac === false) { throw new Ex\CannotPerformOperationException('Cannot initialize a hash context'); } /** * We operate on $thisIv using a hash-based PRF derived from the initial * IV for the first block */ $thisIv = $iv; /** * How much do we increase the counter after each buffered encryption to * prevent nonce reuse? */ $inc = $config->bufferByteSize() / $config->blockByteSize(); /** * Let's MAC our salt and IV/nonce */ \hash_update($hmac, Core::CURRENT_FILE_VERSION); \hash_update($hmac, $file_salt); \hash_update($hmac, $iv); /** * Iterate until we reach the end of the input file */ $breakR = false; while (!\feof($inputHandle)) { $pos = \ftell($inputHandle); if ($pos + $config->bufferByteSize() >= $inputSize) { $breakR = true; // We need to break after this loop iteration $read = self::readBytes($inputHandle, $inputSize - $pos); } else { $read = self::readBytes($inputHandle, $config->bufferByteSize()); } $thisIv = Core::incrementCounter($thisIv, $inc, $config); /** * Perform the AES encryption. Encrypts the plaintext. */ $encrypted = \openssl_encrypt($read, $config->cipherMethod(), $ekey, OPENSSL_RAW_DATA, $thisIv); /** * Check that the encryption was performed successfully */ if ($encrypted === false) { throw new Ex\CannotPerformOperationException('OpenSSL encryption error'); } /** * Write the ciphertext to the output file */ self::writeBytes($outputHandle, $encrypted, Core::ourStrlen($encrypted)); /** * Update the HMAC for the entire file with the data from this block */ \hash_update($hmac, $encrypted); if ($breakR) { break; } } // Now let's get our HMAC and append it $finalHMAC = \hash_final($hmac, true); self::writeBytes($outputHandle, $finalHMAC, $config->macByteSize()); return true; }
/** * 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 */ protected static function plainDecrypt($ciphertext, $key, $iv, $config) { Core::ensureConstantExists("OPENSSL_RAW_DATA"); Core::ensureFunctionExists("openssl_decrypt"); $plaintext = \openssl_decrypt($ciphertext, $config->cipherMethod(), $key, OPENSSL_RAW_DATA, $iv); if ($plaintext === false) { throw new Ex\CannotPerformOperationException("openssl_decrypt() failed."); } return $plaintext; }