fix(security): don't propagate ValueError from Crypto::decrypt() fallback

When decrypting a v3 ciphertext with a mismatched secret, the first
attempt throws an Exception (HMAC mismatch). The fallback then calls
decryptWithoutSecret() with an empty string, which causes hash_hkdf()
to throw a ValueError. Since ValueError extends \Error rather than
\Exception, it bypassed the catch block and propagated as an unhandled
error, crashing the whole request.

Wrap the fallback in its own try/catch(\Throwable) and rethrow the
original Exception so callers get a meaningful HMAC mismatch error.

Signed-off-by: Anna Larch <anna@nextcloud.com>
AI-Assisted-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Anna Larch 2026-05-26 16:08:38 +02:00 committed by Anna
parent 0a4d4bc8ab
commit 29f43d8e7a
2 changed files with 28 additions and 1 deletions

View file

@ -102,7 +102,13 @@ class Crypto implements ICrypto {
} catch (Exception $e) {
if ($password === '') {
// Retry with empty secret as a fallback for instances where the secret might not have been set by accident
return $this->decryptWithoutSecret($authenticatedCiphertext, '');
try {
return $this->decryptWithoutSecret($authenticatedCiphertext, '');
} catch (\Throwable) {
// Fallback failed (e.g. v3 ciphertext requires a non-empty key for hash_hkdf),
// rethrow the original exception
throw $e;
}
}
throw $e;
}

View file

@ -102,6 +102,27 @@ class CryptoTest extends \Test\TestCase {
);
}
public function testDecryptWithWrongSecretThrowsHmacExceptionNotValueError(): void {
// Encrypt with 'secret-A'
$configA = $this->createMock(IConfig::class);
$configA->method('getSystemValue')->with('secret')->willReturn('secret-A');
$configA->method('getSystemValueString')->with('secret')->willReturn('secret-A');
$cryptoA = new Crypto($configA);
$ciphertext = $cryptoA->encrypt('plaintext');
// Decrypt with 'secret-B': first attempt fails (HMAC mismatch), fallback to empty
// string must not propagate a ValueError for v3 ciphertexts — it must rethrow the
// original HMAC exception instead.
$configB = $this->createMock(IConfig::class);
$configB->method('getSystemValue')->with('secret')->willReturn('secret-B');
$configB->method('getSystemValueString')->with('secret')->willReturn('secret-B');
$cryptoB = new Crypto($configB);
$this->expectException(\Exception::class);
$this->expectExceptionMessage('HMAC does not match.');
$cryptoB->decrypt($ciphertext);
}
public function testVersion3CiphertextDecryptsToCorrectPlaintext(): void {
$this->assertSame(
'Another plaintext value that will be encrypted with version 3. It addresses the related key issue. Old ciphertexts should be decrypted properly, but only use the better version for encryption.',