Skip to content

Add modern cryptors - #71

Open
olegbaturin wants to merge 74 commits into
yiisoft:masterfrom
olegbaturin:add-cryptor
Open

olegbaturin wants to merge 74 commits into
yiisoft:masterfrom
olegbaturin:add-cryptor

Conversation

@olegbaturin

Copy link
Copy Markdown
Contributor
Q A
Is bugfix? ❌
New feature? ✔️
Breaks BC? ❌

@codecov

codecov Bot commented May 16, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.14414% with 13 lines in your changes missing coverage. Please review.
✅ Project coverage is 96.00%. Comparing base (552f49e) to head (fa57456).

Files with missing lines Patch % Lines
src/Crypto/Cipher/OpenSSLAeadCipher.php 91.17% 3 Missing ⚠️
src/Crypto/Cipher/OpenSSLWrapCipher.php 89.65% 3 Missing ⚠️
src/Crypto/Cipher/SodiumAeadCipher.php 94.11% 2 Missing ⚠️
src/Crypto/Kdf/KdfKey.php 86.66% 2 Missing ⚠️
src/Crypto/Kdf/KdfPasswordPbkdf2.php 87.50% 2 Missing ⚠️
src/Crypto/Kdf/KdfPasswordArgon2.php 92.85% 1 Missing ⚠️
Additional details and impacted files
@@              Coverage Diff              @@
##              master      #71      +/-   ##
=============================================
- Coverage     100.00%   96.00%   -4.00%     
- Complexity        37      133      +96     
=============================================
  Files              7       16       +9     
  Lines            103      325     +222     
=============================================
+ Hits             103      312     +209     
- Misses             0       13      +13     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@olegbaturin
olegbaturin requested review from samdark and vjik June 20, 2026 17:07

@samdark samdark left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The truncated tag issue from my previous review is still there. The reply in that thread isn't correct: openssl_decrypt() sets the tag at whatever length it's given and compares only those bytes. With the default CHACHA20-POLY1305 a zero-length tag is always accepted.

I've checked it at the current head: KdfCryptor with default KdfKey and OpenSSLAeadCipher decrypts any 44 random bytes to an empty string without knowing the secret, and the same goes through VersionedCryptor. EnvelopeCryptor does the same for any real ciphertext cut down to its header. Details are inline.

if (StringHelper::byteLength($nonce) !== $this->nonceSize) {
throw new EncryptionException("Nonce must be {$this->nonceSize} bytes long.");
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's still no length check here. If $data is shorter than 16 bytes, the whole input becomes the tag and OpenSSL checks only that many bytes. An empty input with CHACHA20-POLY1305 always decrypts to '', and a 1-byte tag passes about once in 256 tries for any of the allowed ciphers. Please reject short input before slicing:

Suggested change
if (StringHelper::byteLength($data) < self::TAG_SIZE) {
throw new EncryptionException('Encrypted data is too short.');
}

Comment thread src/Crypto/KdfCryptor.php
string $secret,
string $context = '',
): string {
if (StringHelper::byteLength($data) < $this->headerLength) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The minimum length doesn't include the cipher tag, so a header-only payload (salt + nonce, 44 bytes by default) gets to the cipher with empty data. Combined with the cipher issue, that's an empty-string forgery without the secret. The cryptor shouldn't depend on every CipherInterface implementation to catch it:

Suggested change
if (StringHelper::byteLength($data) < $this->headerLength) {
if (StringHelper::byteLength($data) < $this->headerLength + $this->cipher->getOverheadSize()) {

string $secret,
string $context = '',
): string {
if (StringHelper::byteLength($data) < $this->headerLength) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here: the data tag isn't part of headerLength. Any real ciphertext cut down to its header passes the wrapped DEK check and then decrypts to ''.

Suggested change
if (StringHelper::byteLength($data) < $this->headerLength) {
if (StringHelper::byteLength($data) < $this->headerLength + $this->cipher->getOverheadSize()) {

$tagSize = $cipherInstance->getOverheadSize();

$this->expectException(EncryptionException::class);
$cipherInstance->decrypt(substr($ciphertext, 0, -$tagSize), $key, $nonce); // remove auth tag

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't cover the bug: the plaintext is 15 bytes, so after removing the tag there are still 15 bytes left, and they're used as the tag. Please add cases for decrypt(''), every input length from 1 to 15, and an empty plaintext with its tag truncated. Also test KdfCryptor/EnvelopeCryptor with real ciphers rather than mocks, using input truncated to the header length. These should fail on the current code.

}

[$this->keySize, $nonceSize] = self::ALLOWED_CIPHERS[$this->cipher];
$this->dummyNonce = str_repeat("\0", $nonceSize);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For AES-*-WRAP, OpenSSL doesn't ignore the IV. It uses it as the integrity check value. With zeros, the output doesn't match RFC 3394: the §4.1 vector gives faacb56e… instead of 1fa68b0a…. It's still authenticated, but no other AES-KW implementation can unwrap it, and once released the format can't be changed. Use the RFC default IV, update the docblock above, and replace the current expected values in OpenSSLWrapCipherTest with the RFC 3394 test vectors.

Suggested change
$this->dummyNonce = str_repeat("\0", $nonceSize);
$this->dummyNonce = str_repeat("\xA6", $nonceSize);

if (StringHelper::byteLength($key) !== $this->keySize) {
throw new EncryptionException("Key must be {$this->keySize} bytes long.");
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

decrypt('') returns '' instead of throwing. Wrapped data is at least 16 bytes and a multiple of 8, so please check that:

Suggested change
$length = StringHelper::byteLength($data);
if ($length < 16 || $length % 8 !== 0) {
throw new EncryptionException('Wrapped data must be at least 16 bytes long and a multiple of 8 bytes.');
}

use const OPENSSL_RAW_DATA;

/**
* Key wrapping cipher using OpenSSL (RFC 5649 / AES-KW).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AES-*-WRAP is RFC 3394. RFC 5649 is the padded variant (-WRAP-PAD).

Suggested change
* Key wrapping cipher using OpenSSL (RFC 5649 / AES-KW).
* Key wrapping cipher using OpenSSL (RFC 3394 / AES-KW).

Comment thread README.md
'__construct()' => [
'cryptors' => ReferencesArray::from([
"\x01" => Reference::to(KdfCryptor::class),
"\x96" => Reference::to(EnvelopeCryptor::class},

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo, } instead of ).

Suggested change
"\x96" => Reference::to(EnvelopeCryptor::class},
"\x96" => Reference::to(EnvelopeCryptor::class),

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants