Skip to content

Commit

Permalink
Clean up exceptions; use native hash_pbkdf2 where possible.
Browse files Browse the repository at this point in the history
  • Loading branch information
demiankatz committed Nov 21, 2024
1 parent 14c4f25 commit 4c42ca3
Showing 1 changed file with 62 additions and 38 deletions.
100 changes: 62 additions & 38 deletions module/VuFind/src/VuFind/Crypt/BlockCipher.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@

namespace VuFind\Crypt;

use InvalidArgumentException;

use function chr;
use function extension_loaded;
use function in_array;
Expand Down Expand Up @@ -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;
Expand All @@ -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
);
}
Expand All @@ -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
Expand All @@ -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(
Expand Down Expand Up @@ -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
{
Expand Down Expand Up @@ -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;

Expand All @@ -407,24 +409,26 @@ 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
* @param string $salt Salt value
* @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;
Expand All @@ -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(),
Expand All @@ -475,22 +479,50 @@ 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
*
* @param string $data Data to decrypt
*
* @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();
Expand All @@ -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);
Expand Down

0 comments on commit 4c42ca3

Please sign in to comment.