/** * Decrypts a ciphertext to a string with either a key or a password. * * @param string $ciphertext * @param KeyOrPassword $secret * @param bool $raw_binary * * @throws Ex\EnvironmentIsBrokenException * @throws Ex\WrongKeyOrModifiedCiphertextException * * @return string */ private static function decryptInternal($ciphertext, KeyOrPassword $secret, $raw_binary) { RuntimeTests::runtimeTest(); if (!$raw_binary) { try { $ciphertext = Encoding::hexToBin($ciphertext); } catch (Ex\BadFormatException $ex) { throw new Ex\WrongKeyOrModifiedCiphertextException('Ciphertext has invalid hex encoding.'); } } if (Core::ourStrlen($ciphertext) < Core::MINIMUM_CIPHERTEXT_SIZE) { throw new Ex\WrongKeyOrModifiedCiphertextException('Ciphertext is too short.'); } // Get and check the version header. $header = Core::ourSubstr($ciphertext, 0, Core::HEADER_VERSION_SIZE); if ($header !== Core::CURRENT_VERSION) { throw new Ex\WrongKeyOrModifiedCiphertextException('Bad version header.'); } // Get the salt. $salt = Core::ourSubstr($ciphertext, Core::HEADER_VERSION_SIZE, Core::SALT_BYTE_SIZE); if ($salt === false) { throw new Ex\EnvironmentIsBrokenException(); } // Get the IV. $iv = Core::ourSubstr($ciphertext, Core::HEADER_VERSION_SIZE + Core::SALT_BYTE_SIZE, Core::BLOCK_BYTE_SIZE); if ($iv === false) { throw new Ex\EnvironmentIsBrokenException(); } // Get the HMAC. $hmac = Core::ourSubstr($ciphertext, Core::ourStrlen($ciphertext) - Core::MAC_BYTE_SIZE, Core::MAC_BYTE_SIZE); if ($hmac === false) { throw new Ex\EnvironmentIsBrokenException(); } // Get the actual encrypted ciphertext. $encrypted = Core::ourSubstr($ciphertext, Core::HEADER_VERSION_SIZE + Core::SALT_BYTE_SIZE + Core::BLOCK_BYTE_SIZE, Core::ourStrlen($ciphertext) - Core::MAC_BYTE_SIZE - Core::SALT_BYTE_SIZE - Core::BLOCK_BYTE_SIZE - Core::HEADER_VERSION_SIZE); if ($encrypted === false) { throw new Ex\EnvironmentIsBrokenException(); } // Derive the separate encryption and authentication keys from the key // or password, whichever it is. $keys = $secret->deriveKeys($salt); if (self::verifyHMAC($hmac, $header . $salt . $iv . $encrypted, $keys->getAuthenticationKey())) { $plaintext = self::plainDecrypt($encrypted, $keys->getEncryptionKey(), $iv, Core::CIPHER_METHOD); return $plaintext; } else { throw new Ex\WrongKeyOrModifiedCiphertextException('Integrity check failed.'); } }
/** * Decrypts a file-backed resource with either a key or a password. * * @param resource $inputHandle * @param resource $outputHandle * @param KeyOrPassword $secret * * @throws Defuse\Crypto\Exception\EnvironmentIsBrokenException * @throws Defuse\Crypto\Exception\IOException * @throws Defuse\Crypto\Exception\WrongKeyOrModifiedCiphertextException */ public static function decryptResourceInternal($inputHandle, $outputHandle, KeyOrPassword $secret) { if (!\is_resource($inputHandle)) { throw new Ex\IOException('Input handle must be a resource!'); } if (!\is_resource($outputHandle)) { throw new Ex\IOException('Output handle must be a resource!'); } /* Make sure the file is big enough for all the reads we need to do. */ $stat = \fstat($inputHandle); if ($stat['size'] < Core::MINIMUM_CIPHERTEXT_SIZE) { throw new Ex\WrongKeyOrModifiedCiphertextException('Input file is too small to have been created by this library.'); } /* Check the version header. */ $header = self::readBytes($inputHandle, Core::HEADER_VERSION_SIZE); if ($header !== Core::CURRENT_VERSION) { throw new Ex\WrongKeyOrModifiedCiphertextException('Bad version header.'); } /* Get the salt. */ $file_salt = self::readBytes($inputHandle, Core::SALT_BYTE_SIZE); /* Get the IV. */ $ivsize = Core::BLOCK_BYTE_SIZE; $iv = self::readBytes($inputHandle, $ivsize); /* Derive the authentication and encryption keys. */ $keys = $secret->deriveKeys($file_salt); $ekey = $keys->getEncryptionKey(); $akey = $keys->getAuthenticationKey(); /* We'll store the MAC of each buffer-sized chunk as we verify the * actual MAC, so that we can check them again when decrypting. */ $macs = []; /* $thisIv will be incremented after each call to the decryption. */ $thisIv = $iv; /* How many blocks do we encrypt at a time? We increment by this value. */ $inc = Core::BUFFER_BYTE_SIZE / Core::BLOCK_BYTE_SIZE; /* Get the HMAC. */ if (\fseek($inputHandle, -1 * Core::MAC_BYTE_SIZE, SEEK_END) === false) { throw new Ex\IOException('Cannot seek to beginning of MAC within input file'); } /* Get the position of the last byte in the actual ciphertext. */ $cipher_end = \ftell($inputHandle); if ($cipher_end === false) { throw new Ex\IOException('Cannot read input file'); } /* We have the position of the first byte of the HMAC. Go back by one. */ --$cipher_end; /* Read the HMAC. */ $stored_mac = self::readBytes($inputHandle, Core::MAC_BYTE_SIZE); /* Initialize a streaming HMAC state. */ $hmac = \hash_init(Core::HASH_FUNCTION_NAME, HASH_HMAC, $akey); if ($hmac === false) { throw new Ex\EnvironmentIsBrokenException('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\IOException('Cannot read seek within input file'); } /* Seek to the start of the actual ciphertext. */ if (\fseek($inputHandle, Core::SALT_BYTE_SIZE + $ivsize, SEEK_CUR) === false) { throw new Ex\IOException('Cannot seek input file to beginning of ciphertext'); } /* PASS #1: Calculating the HMAC. */ \hash_update($hmac, $header); \hash_update($hmac, $file_salt); \hash_update($hmac, $iv); $hmac2 = \hash_copy($hmac); $break = false; while (!$break) { $pos = \ftell($inputHandle); if ($pos === false) { throw new Ex\IOException('Could not get current position in input file during decryption'); } /* Read the next buffer-sized chunk (or less). */ if ($pos + Core::BUFFER_BYTE_SIZE >= $cipher_end) { $break = true; $read = self::readBytes($inputHandle, $cipher_end - $pos + 1); } else { $read = self::readBytes($inputHandle, Core::BUFFER_BYTE_SIZE); } /* Update the HMAC. */ \hash_update($hmac, $read); /* Remember this buffer-sized chunk's HMAC. */ $chunk_mac = \hash_copy($hmac); if ($chunk_mac === false) { throw new Ex\EnvironmentIsBrokenException('Cannot duplicate a hash context'); } $macs[] = \hash_final($chunk_mac); } /* Get the final HMAC, which should match the stored one. */ $final_mac = \hash_final($hmac, true); /* Verify the HMAC. */ if (!Core::hashEquals($final_mac, $stored_mac)) { throw new Ex\WrongKeyOrModifiedCiphertextException('Integrity check failed.'); } /* PASS #2: Decrypt and write output. */ /* Rewind to the start of the actual ciphertext. */ if (\fseek($inputHandle, Core::SALT_BYTE_SIZE + $ivsize + Core::HEADER_VERSION_SIZE, SEEK_SET) === false) { throw new Ex\IOException('Could not move the input file pointer during decryption'); } $at_file_end = false; while (!$at_file_end) { $pos = \ftell($inputHandle); if ($pos === false) { throw new Ex\IOException('Could not get current position in input file during decryption'); } /* Read the next buffer-sized chunk (or less). */ if ($pos + Core::BUFFER_BYTE_SIZE >= $cipher_end) { $at_file_end = true; $read = self::readBytes($inputHandle, $cipher_end - $pos + 1); } else { $read = self::readBytes($inputHandle, Core::BUFFER_BYTE_SIZE); } /* Recalculate the MAC (so far) and compare it with the one we * remembered from pass #1 to ensure attackers didn't change the * ciphertext after MAC verification. */ \hash_update($hmac2, $read); $calc_mac = \hash_copy($hmac2); if ($calc_mac === false) { throw new Ex\EnvironmentIsBrokenException('Cannot duplicate a hash context'); } $calc = \hash_final($calc_mac); if (empty($macs)) { throw new Ex\WrongKeyOrModifiedCiphertextException('File was modified after MAC verification'); } elseif (!Core::hashEquals(\array_shift($macs), $calc)) { throw new Ex\WrongKeyOrModifiedCiphertextException('File was modified after MAC verification'); } /* Decrypt this buffer-sized chunk. */ $decrypted = \openssl_decrypt($read, Core::CIPHER_METHOD, $ekey, OPENSSL_RAW_DATA, $thisIv); if ($decrypted === false) { throw new Ex\EnvironmentIsBrokenException('OpenSSL decryption error'); } /* Write the plaintext to the output file. */ self::writeBytes($outputHandle, $decrypted, Core::ourStrlen($decrypted)); /* Increment the IV by the amount of blocks in a buffer. */ $thisIv = Core::incrementCounter($thisIv, $inc); /* WARNING: Usually, unless the file is a multiple of the buffer * size, $thisIv will contain an incorrect value here on the last * iteration of this loop. */ } }