diff --git a/module/VuFind/src/VuFind/Crypt/BlockCipher.php b/module/VuFind/src/VuFind/Crypt/BlockCipher.php index eee8a440db1..b0ba7fe601b 100644 --- a/module/VuFind/src/VuFind/Crypt/BlockCipher.php +++ b/module/VuFind/src/VuFind/Crypt/BlockCipher.php @@ -35,6 +35,8 @@ namespace VuFind\Crypt; +use InvalidArgumentException; + use function chr; use function extension_loaded; use function in_array; @@ -213,14 +215,14 @@ protected function getKeySize(): int * @param string $key OpenSSL encryption key * * @return static - * @throws \InvalidArgumentException + * @throws InvalidArgumentException */ protected function setOpenSslKey(string $key): static { $keyLen = mb_strlen($key, '8bit'); if ($keyLen < $this->getKeySize()) { - throw new \InvalidArgumentException('OpenSSL key is too short.'); + throw new InvalidArgumentException('OpenSSL key is too short.'); } $this->openSslKey = $key; @@ -246,12 +248,12 @@ protected function getOpenSslKey(): string * @param string $algo New algorithm * * @return static - * @throws \InvalidArgumentException + * @throws InvalidArgumentException */ protected function setAlgorithm(string $algo): static { if (!in_array($this->encryptionAlgos[$algo] . '-cbc', openssl_get_cipher_methods(true))) { - throw new \InvalidArgumentException( + throw new InvalidArgumentException( 'Unsupported algorithm: ' . $algo ); } @@ -264,17 +266,17 @@ protected function setAlgorithm(string $algo): static * * @param string $data Data to encrypt * - * @throws \InvalidArgumentException + * @throws InvalidArgumentException * @return string */ protected function openSslEncrypt(string $data): string { if ($data === '') { - throw new \InvalidArgumentException('Empty strings cannot be encrypted'); + throw new InvalidArgumentException('Empty strings cannot be encrypted'); } if (null === $this->getSalt() && $this->getSaltSize() > 0) { - throw new \InvalidArgumentException('The salt (IV) cannot be empty'); + throw new InvalidArgumentException('The salt (IV) cannot be empty'); } // padding @@ -301,13 +303,13 @@ protected function openSslEncrypt(string $data): string * * @param string $data Data to decrypt * - * @throws Exception\InvalidArgumentException + * @throws InvalidArgumentException * @return string */ protected function openSslDecrypt(string $data): string { if (empty($data)) { - throw new \InvalidArgumentException('The data to decrypt cannot be empty'); + throw new InvalidArgumentException('The data to decrypt cannot be empty'); } $result = openssl_decrypt( @@ -343,7 +345,7 @@ protected function getSaltSize(): int * @param string $salt Salt value * * @return static - * @throws Exception\InvalidArgumentException + * @throws InvalidArgumentException */ protected function setSalt(string $salt): static { @@ -381,12 +383,12 @@ protected function getBlockSize(): int * @param string $key Encryption/decryption key * * @return static - * @throws \InvalidArgumentException + * @throws InvalidArgumentException */ public function setKey(string $key): static { if (empty($key)) { - throw new \InvalidArgumentException('The key cannot be empty'); + throw new InvalidArgumentException('The key cannot be empty'); } $this->key = $key; @@ -407,7 +409,9 @@ protected function getHashSize(string $hash, bool $binary = false): int } /** - * Generate the new key (PKCS #5 v2.0 standard RFC 2898) + * Generate a Pbkdf2 key (PKCS #5 v2.0 standard RFC 2898) using the Laminas\Crypt + * algorithm, which varies from PHP's built-in hash_pbkdf2(). We need to use this + * logic for compatibility with data generated by versions of VuFind prior to 11.0. * * @param string $hash The hash algorithm to be used by HMAC * @param string $password The source password/key @@ -415,16 +419,16 @@ protected function getHashSize(string $hash, bool $binary = false): int * @param int $iterations The number of iterations * @param int $length The output size * - * @throws \InvalidArgumentException + * @throws InvalidArgumentException * @return string */ - protected function getPbkdf2(string $hash, string $password, string $salt, int $iterations, int $length): string + protected function getLegacyPbkdf2(string $hash, string $password, string $salt, int $iterations, int $length): string { - $num = ceil($length / $this->getHashSize($hash, true)); + $num = ceil($length / $this->getHashSize($hash, true)); $result = ''; for ($block = 1; $block <= $num; $block++) { $hmac = hash_hmac($hash, $salt . pack('N', $block), $password, true); - $mix = $hmac; + $mix = $hmac; for ($i = 1; $i < $iterations; $i++) { $hmac = hash_hmac($hash, $hmac, $password, true); $mix ^= $hmac; @@ -440,23 +444,23 @@ protected function getPbkdf2(string $hash, string $password, string $salt, int $ * @param string $data Data to encrypt * * @return string - * @throws \InvalidArgumentException + * @throws InvalidArgumentException */ public function encrypt(string $data): string { // 0 (as integer), 0.0 (as float) & '0' (as string) will return false, though these should be allowed // Must be a string, integer, or float in order to encrypt if ($data === '') { - throw new \InvalidArgumentException('Cannot encrypt empty data'); + throw new InvalidArgumentException('Cannot encrypt empty data'); } if (empty($this->key)) { - throw new \InvalidArgumentException('A key is required'); + throw new InvalidArgumentException('A key is required'); } $keySize = $this->getKeySize(); $this->setSalt(random_bytes($this->getSaltSize())); // generate the encryption key and the HMAC key for the authentication - $hash = $this->getPbkdf2( + $hash = hash_pbkdf2( $this->pbkdf2Hash, $this->key, $this->getSalt(), @@ -475,6 +479,34 @@ public function encrypt(string $data): string return $hmac . base64_encode($ciphertext); } + /** + * Generate the encryption key and the HMAC key for the authentication + + * @param string $ciphertext Text to decrypt + * @param string $salt Salt + * @param int $keySize Key size + * @param bool $legacy Use legacy pbkdf2 algorithm for compatibility with old data? + * + * @return string + * @throws InvalidArgumentException + */ + protected function getValidationHmac(string $ciphertext, string $salt, int $keySize, bool $legacy = false): string + { + $callback = $legacy ? [$this, 'getLegacyPbkdf2'] : 'hash_pbkdf2'; + $hash = $callback( + $this->pbkdf2Hash, + $this->key, + $salt, + $this->keyIteration, + $keySize * 2 + ); + // set the decryption key + $this->setOpenSslKey(mb_substr($hash, 0, $keySize, '8bit')); + // set the key for HMAC + $keyHmac = mb_substr($hash, $keySize, null, '8bit'); + return hash_hmac($this->hash, $this->algo . $ciphertext, $keyHmac); + } + /** * Decrypt data * @@ -482,15 +514,15 @@ public function encrypt(string $data): string * * @return string|bool * - * @throws \InvalidArgumentException + * @throws InvalidArgumentException */ public function decrypt(string $data): string|bool { if ('' === $data) { - throw new \InvalidArgumentException('The data to decrypt cannot be empty'); + throw new InvalidArgumentException('The data to decrypt cannot be empty'); } if (empty($this->key)) { - throw new \InvalidArgumentException('A key is required'); + throw new InvalidArgumentException('A key is required'); } $keySize = $this->getKeySize(); @@ -499,21 +531,13 @@ public function decrypt(string $data): string|bool $hmac = mb_substr($data, 0, $hmacSize, '8bit'); $ciphertext = base64_decode(mb_substr($data, $hmacSize, null, '8bit') ?: ''); $iv = mb_substr($ciphertext, 0, $this->getSaltSize(), '8bit'); - // generate the encryption key and the HMAC key for the authentication - $hash = $this->getPbkdf2( - $this->pbkdf2Hash, - $this->key, - $iv, - $this->keyIteration, - $keySize * 2 - ); - // set the decryption key - $this->setOpenSslKey(mb_substr($hash, 0, $keySize, '8bit')); - // set the key for HMAC - $keyHmac = mb_substr($hash, $keySize, null, '8bit'); - $hmacNew = hash_hmac($this->hash, $this->algo . $ciphertext, $keyHmac); + $hmacNew = $this->getValidationHmac($ciphertext, $iv, $keySize); if (strcmp($hmacNew, $hmac) !== 0) { - return false; + // If authentication failed using new algorithm, fall back to legacy: + $hmacNew = $this->getValidationHmac($ciphertext, $iv, $keySize, true); + if (strcmp($hmacNew, $hmac) !== 0) { + return false; + } } return $this->openSslDecrypt($ciphertext);