/** * Derives authentication and encryption keys from the secret, using a slow * key derivation function if the secret is a password. * * @param string $salt * * @throws Ex\EnvironmentIsBrokenException * * @return DerivedKeys */ public function deriveKeys($salt) { if (Core::ourStrlen($salt) !== Core::SALT_BYTE_SIZE) { throw new Ex\EnvironmentIsBrokenException('Bad salt.'); } if ($this->secret_type === self::SECRET_TYPE_KEY) { $akey = Core::HKDF(Core::HASH_FUNCTION_NAME, $this->secret->getRawBytes(), Core::KEY_BYTE_SIZE, Core::AUTHENTICATION_INFO_STRING, $salt); $ekey = Core::HKDF(Core::HASH_FUNCTION_NAME, $this->secret->getRawBytes(), Core::KEY_BYTE_SIZE, Core::ENCRYPTION_INFO_STRING, $salt); return new DerivedKeys($akey, $ekey); } elseif ($this->secret_type === self::SECRET_TYPE_PASSWORD) { /* Our PBKDF2 polyfill is vulnerable to a DoS attack documented in * GitHub issue #230. The fix is to pre-hash the password to ensure * it is short. We do the prehashing here instead of in pbkdf2() so * that pbkdf2() still computes the function as defined by the * standard. */ $prehash = \hash(Core::HASH_FUNCTION_NAME, $this->secret, true); $prekey = Core::pbkdf2(Core::HASH_FUNCTION_NAME, $prehash, $salt, self::PBKDF2_ITERATIONS, Core::KEY_BYTE_SIZE, true); $akey = Core::HKDF(Core::HASH_FUNCTION_NAME, $prekey, Core::KEY_BYTE_SIZE, Core::AUTHENTICATION_INFO_STRING, $salt); /* Note the cryptographic re-use of $salt here. */ $ekey = Core::HKDF(Core::HASH_FUNCTION_NAME, $prekey, Core::KEY_BYTE_SIZE, Core::ENCRYPTION_INFO_STRING, $salt); return new DerivedKeys($akey, $ekey); } else { throw new Ex\EnvironmentIsBrokenException('Bad secret type.'); } }
/** * Decrypts a legacy ciphertext produced by version 1 of this library. * * @param string $ciphertext * @param string $key * * @throws Ex\EnvironmentIsBrokenException * @throws Ex\WrongKeyOrModifiedCiphertextException * * @return string */ public static function legacyDecrypt($ciphertext, $key) { RuntimeTests::runtimeTest(); // Extract the HMAC from the front of the ciphertext. if (Core::ourStrlen($ciphertext) <= Core::LEGACY_MAC_BYTE_SIZE) { throw new Ex\WrongKeyOrModifiedCiphertextException('Ciphertext is too short.'); } $hmac = Core::ourSubstr($ciphertext, 0, Core::LEGACY_MAC_BYTE_SIZE); if ($hmac === false) { throw new Ex\EnvironmentIsBrokenException(); } $ciphertext = Core::ourSubstr($ciphertext, Core::LEGACY_MAC_BYTE_SIZE); if ($ciphertext === false) { throw new Ex\EnvironmentIsBrokenException(); } // Regenerate the same authentication sub-key. $akey = Core::HKDF(Core::LEGACY_HASH_FUNCTION_NAME, $key, Core::LEGACY_KEY_BYTE_SIZE, Core::LEGACY_AUTHENTICATION_INFO_STRING, null); if (self::verifyHMAC($hmac, $ciphertext, $akey)) { // Regenerate the same encryption sub-key. $ekey = Core::HKDF(Core::LEGACY_HASH_FUNCTION_NAME, $key, Core::LEGACY_KEY_BYTE_SIZE, Core::LEGACY_ENCRYPTION_INFO_STRING, null); // Extract the IV from the ciphertext. if (Core::ourStrlen($ciphertext) <= Core::LEGACY_BLOCK_BYTE_SIZE) { throw new Ex\WrongKeyOrModifiedCiphertextException('Ciphertext is too short.'); } $iv = Core::ourSubstr($ciphertext, 0, Core::LEGACY_BLOCK_BYTE_SIZE); if ($iv === false) { throw new Ex\EnvironmentIsBrokenException(); } $ciphertext = Core::ourSubstr($ciphertext, Core::LEGACY_BLOCK_BYTE_SIZE); if ($ciphertext === false) { throw new Ex\EnvironmentIsBrokenException(); } // Do the decryption. $plaintext = self::plainDecrypt($ciphertext, $ekey, $iv, Core::LEGACY_CIPHER_METHOD); return $plaintext; } else { throw new Ex\WrongKeyOrModifiedCiphertextException('Integrity check failed.'); } }
/** * Decrypt the contents of a file handle $inputHandle and store the results * in $outputHandle using HKDF of $key to decrypt then verify * * @param resource $inputHandle * @param resource $outputHandle * @param Key $key * @return boolean */ public static function decryptResource($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!'); } // Parse the header. $header = self::readBytes($inputHandle, Core::HEADER_VERSION_SIZE); $config = self::getFileVersionConfigFromHeader($header, Core::CURRENT_FILE_VERSION); // 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 grab the file salt. $file_salt = self::readBytes($inputHandle, $config->saltByteSize()); // For storing MACs of each buffer chunk $macs = []; /** * 1. We need to decode some values from our files */ /** * Let's split our keys * * $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); /** * Grab our IV from the encrypted message * * It should be the first N blocks of the file (N = 16) */ $ivsize = \openssl_cipher_iv_length($config->cipherMethod()); $iv = self::readBytes($inputHandle, $ivsize); // How much do we increase the counter after each buffered encryption to prevent nonce reuse $inc = $config->bufferByteSize() / $config->blockByteSize(); $thisIv = $iv; /** * Let's grab our MAC * * It should be the last N blocks of the file (N = 32) */ if (\fseek($inputHandle, -1 * $config->macByteSize(), SEEK_END) === false) { throw new Ex\CannotPerformOperationException('Cannot seek to beginning of MAC within input file'); } // Grab our last position of ciphertext before we read the MAC $cipher_end = \ftell($inputHandle); if ($cipher_end === false) { throw new Ex\CannotPerformOperationException('Cannot read input file'); } --$cipher_end; // We need to subtract one // We keep our MAC stored in this variable $stored_mac = self::readBytes($inputHandle, $config->macByteSize()); /** * We begin recalculating the HMAC for the entire file... */ $hmac = \hash_init($config->hashFunctionName(), HASH_HMAC, $akey); if ($hmac === false) { throw new Ex\CannotPerformOperationException('Cannot initialize a hash context'); } /** * 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\CannotPerformOperationException('Cannot read seek within input file'); } /** * Set it to the first non-salt and non-IV byte */ if (\fseek($inputHandle, $config->saltByteSize() + $ivsize, SEEK_CUR) === false) { throw new Ex\CannotPerformOperationException('Cannot read seek input file to beginning of ciphertext'); } /** * 2. Let's recalculate the MAC */ /** * Let's initialize our $hmac hasher with our Salt and IV */ \hash_update($hmac, $header); \hash_update($hmac, $file_salt); \hash_update($hmac, $iv); $hmac2 = \hash_copy($hmac); $break = false; while (!$break) { /** * First, grab the current position */ $pos = \ftell($inputHandle); if ($pos === false) { throw new Ex\CannotPerformOperationException('Could not get current position in input file during decryption'); } /** * 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->bufferByteSize() >= $cipher_end) { $break = true; $read = self::readBytes($inputHandle, $cipher_end - $pos + 1); } else { $read = self::readBytes($inputHandle, $config->bufferByteSize()); } if ($read === false) { throw new Ex\CannotPerformOperationException('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\CannotPerformOperationException('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 (!Core::hashEquals($finalHMAC, $stored_mac)) { throw new Ex\InvalidCiphertextException('Message Authentication failure; tampering detected.'); } /** * 4. Okay, let's begin decrypting */ /** * Return file pointer to the first non-header, non-IV byte in the file */ 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'); } /** * Should we break the writing? */ $breakW = false; /** * This loop writes plaintext to the destination file: */ while (!$breakW) { /** * Get the current position */ $pos = \ftell($inputHandle); if ($pos === false) { throw new Ex\CannotPerformOperationException('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 + $config->bufferByteSize() >= $cipher_end) { $breakW = true; $read = self::readBytes($inputHandle, $cipher_end - $pos + 1); } else { $read = self::readBytes($inputHandle, $config->bufferByteSize()); } /** * 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\CannotPerformOperationException('Cannot duplicate a hash context'); } $calc = \hash_final($calcMAC); if (empty($macs)) { throw new Ex\InvalidCiphertextException('File was modified after MAC verification'); } elseif (!Core::hashEquals(\array_shift($macs), $calc)) { throw new Ex\InvalidCiphertextException('File was modified after MAC verification'); } $thisIv = Core::incrementCounter($thisIv, $inc, $config); /** * Perform the AES decryption. Decrypts the message. */ $decrypted = \openssl_decrypt($read, $config->cipherMethod(), $ekey, OPENSSL_RAW_DATA, $thisIv); /** * Test for decryption faulure */ if ($decrypted === false) { throw new Ex\CannotPerformOperationException('OpenSSL decryption error'); } /** * Write the plaintext out to the output file */ self::writeBytes($outputHandle, $decrypted, Core::ourStrlen($decrypted)); } return true; }
/** * Test HKDF against test vectors. * * @throws Ex\EnvironmentIsBrokenException */ private static function HKDFTestVector() { // HKDF test vectors from RFC 5869 // Test Case 1 $ikm = \str_repeat("\v", 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); if ($computed_okm !== $okm) { throw new Ex\EnvironmentIsBrokenException(); } // Test Case 7 $ikm = \str_repeat("\f", 22); $length = 42; $okm = Encoding::hexToBin('2c91117204d745f3500d636a62f64f0a' . 'b3bae548aa53d423b0d1f27ebba6f5e5' . '673a081d70cce7acfc48'); $computed_okm = Core::HKDF('sha1', $ikm, $length, '', null); if ($computed_okm !== $okm) { throw new Ex\EnvironmentIsBrokenException(); } }
/** * Decrypts a ciphertext (legacy -- before version tagging) * * $ciphertext is the ciphertext to decrypt. * $key is the key that the ciphertext was encrypted with. * You MUST catch exceptions thrown by this function. Read the docs. * * @param string $ciphertext * @param string $key * @return string * @throws Ex\CannotPerformOperationException * @throws Ex\CryptoTestFailedException * @throws Ex\InvalidCiphertextException */ public static function legacyDecrypt($ciphertext, $key) { RuntimeTests::runtimeTest(); $config = self::getVersionConfigFromHeader(Core::LEGACY_VERSION, Core::LEGACY_VERSION); // Extract the HMAC from the front of the ciphertext. if (Core::ourStrlen($ciphertext) <= $config->macByteSize()) { throw new Ex\InvalidCiphertextException("Ciphertext is too short."); } $hmac = Core::ourSubstr($ciphertext, 0, $config->macByteSize()); if ($hmac === false) { throw new Ex\CannotPerformOperationException(); } $ciphertext = Core::ourSubstr($ciphertext, $config->macByteSize()); if ($ciphertext === false) { throw new Ex\CannotPerformOperationException(); } // Regenerate the same authentication sub-key. $akey = Core::HKDF($config->hashFunctionName(), $key, $config->keyByteSize(), $config->authenticationInfoString(), null, $config); if (self::verifyHMAC($hmac, $ciphertext, $akey, $config)) { // Regenerate the same encryption sub-key. $ekey = Core::HKDF($config->hashFunctionName(), $key, $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->cipherMethod()); if ($ivsize === false || $ivsize <= 0) { throw new Ex\CannotPerformOperationException("Could not get the IV length from OpenSSL"); } if (Core::ourStrlen($ciphertext) <= $ivsize) { throw new Ex\InvalidCiphertextException("Ciphertext is too short."); } $iv = Core::ourSubstr($ciphertext, 0, $ivsize); if ($iv === false) { throw new Ex\CannotPerformOperationException(); } $ciphertext = Core::ourSubstr($ciphertext, $ivsize); if ($ciphertext === false) { throw new Ex\CannotPerformOperationException(); } $plaintext = self::plainDecrypt($ciphertext, $ekey, $iv, $config); return $plaintext; } else { /* * We throw an exception instead of returning false because we want * a script that doesn't handle this condition to CRASH, instead * of thinking the ciphertext decrypted to the value false. */ throw new Ex\InvalidCiphertextException("Integrity check failed."); } }