Add modern cryptors#71
Conversation
olegbaturin
commented
May 16, 2026
| Q | A |
|---|---|
| Is bugfix? | ❌ |
| New feature? | ✔️ |
| Breaks BC? | ❌ |
add tests
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## master #71 +/- ##
=============================================
- Coverage 100.00% 97.84% -2.16%
- Complexity 37 101 +64
=============================================
Files 7 15 +8
Lines 102 278 +176
=============================================
+ Hits 102 272 +170
- Misses 0 6 +6 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Pull request overview
This PR introduces a new “modern” AEAD-based encryption layer to the package (new cipher/KDF abstractions plus high-level cryptors), along with documentation, CI updates, and a small PasswordHasher adjustment.
Changes:
- Added AEAD cipher interfaces and implementations for OpenSSL (AES-GCM) and libsodium (AES-256-GCM/ChaCha20-Poly1305/XChaCha20-Poly1305).
- Added KDF abstractions/implementations (HKDF-based key derivation, plus PBKDF2+HKDF for passwords) and high-level cryptors (Session, Envelope, Versioned).
- Added PHPUnit coverage for new cryptors/ciphers/KDFs and updated README/CI/Composer metadata accordingly.
Reviewed changes
Copilot reviewed 28 out of 28 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
src/Crypt/CipherInterface.php |
Introduces base cipher abstraction used by new cryptors. |
src/Crypt/AeadCipherInterface.php |
Adds AEAD-specific cipher contract (tag size). |
src/Crypt/Cipher/OpenSSLAeadCipher.php |
OpenSSL-backed AES-GCM AEAD cipher implementation. |
src/Crypt/Cipher/SodiumAeadCipher.php |
libsodium-backed AEAD cipher implementation (AES-GCM/ChaCha variants). |
src/Crypt/KdfInterface.php |
Introduces key-derivation abstraction used by cryptors. |
src/Crypt/Kdf/KdfKey.php |
HKDF-based KDF for high-entropy secrets. |
src/Crypt/Kdf/KdfPassword.php |
PBKDF2+HKDF-based KDF for passwords. |
src/Crypt/CryptorInterface.php |
Defines high-level encrypt/decrypt contract for new cryptors. |
src/Crypt/SessionCryptor.php |
Implements per-message derived-key “session” encryption format. |
src/Crypt/EnvelopeCryptor.php |
Implements envelope encryption (wrapped DEK) format. |
src/Crypt/VersionedCryptor.php |
Adds version-prefix wrapper to support cryptor migrations. |
src/Crypt/EncryptionException.php |
Introduces a dedicated exception type for encryption/decryption failures. |
tests/Crypt/AbstractAeadCipherCase.php |
Shared test suite for AEAD cipher implementations. |
tests/Crypt/OpenSSLAeadCipherTest.php |
OpenSSL AEAD cipher tests. |
tests/Crypt/SodiumAeadCipherTest.php |
libsodium ChaCha/XChaCha AEAD cipher tests. |
tests/Crypt/SodiumGcmCipherTest.php |
libsodium AES-256-GCM AEAD cipher tests (hardware dependent). |
tests/Crypt/AbstractKdfCase.php |
Shared test suite for KDF implementations. |
tests/Crypt/KdfKeyTest.php |
HKDF KDF tests. |
tests/Crypt/KdfPasswordTest.php |
PBKDF2+HKDF KDF tests. |
tests/Crypt/SessionCryptorTest.php |
Session cryptor unit tests. |
tests/Crypt/EnvelopeCryptorTest.php |
Envelope cryptor unit tests. |
tests/Crypt/VersionedCryptorTest.php |
Versioned cryptor unit tests. |
README.md |
Documents the new cryptors/KDFs/ciphers and marks legacy cryptor as “old”. |
composer.json |
Adds ext-sodium requirement. |
.github/workflows/build.yml |
Enables sodium extension in CI. |
psalm.xml |
Adjusts Psalm configuration. |
CHANGELOG.md |
Notes the new cryptors in the unreleased version section. |
src/PasswordHasher.php |
Adjusts SAFE_PARAMETERS lookup when algorithm is null. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| "require": { | ||
| "php": "8.1 - 8.5", | ||
| "ext-hash": "*", | ||
| "ext-openssl": "*", | ||
| "ext-sodium": "*", | ||
| "yiisoft/strings": "^2.0" |
| `Crypt` provides encryption layer based on `AEAD` algorithms. | ||
| It supports key derivation, session‑oriented encryption, envelope encryption, and versioned ciphertexts for seamless algorithm migration. |
samdark
left a comment
There was a problem hiding this comment.
Not enough negative/security tests around metadata tampering: version byte changes, salt/nonce field mutation, truncated payloads at every boundary, truncated tags, empty plaintext, wrong context, and cross-version/cross-cryptor confusion.
| $tag = StringHelper::byteSubstring($data, -self::TAG_SIZE); | ||
| $ciphertext = StringHelper::byteSubstring($data, 0, -self::TAG_SIZE); | ||
|
|
||
| $decrypted = openssl_decrypt($ciphertext, $this->cipher, $key, OPENSSL_RAW_DATA | OPENSSL_DONT_ZERO_PAD_KEY, $nonce, $tag); |
There was a problem hiding this comment.
Truncated GCM tags issue. If $data is shorter than TAG_SIZE, we pass a 1-15 byte tag to openssl_decrypt(), and OpenSSL authenticates only the supplied tag bytes. That makes empty-plaintext forgery possible. One can search, for example, a 1-byte tag succeeds in about 256 attempts. Reject $data shorter than self::TAG_SIZE before slicing, and add tests for empty plaintext plus truncated tags.
| "php": "8.1 - 8.5", | ||
| "ext-hash": "*", | ||
| "ext-openssl": "*", | ||
| "ext-sodium": "*", |
| * @throws RuntimeException If sodium extension is missing, cipher not allowed, or AES-256-GCM without hardware support. | ||
| */ | ||
| public function __construct( | ||
| private readonly string $cipher = 'AES-256-GCM', |
There was a problem hiding this comment.
Is it a good default since there could be no hardware support? In the README, the cipher is not configured, so the following examples will result in an exception.
How about choosing default such as XChaCha20-Poly1305-IETF?
|
|
||
| ```php | ||
| // /config/di.php | ||
| use Yiisoft\Security\Crypt\Kdf\KdfPassword; |
There was a problem hiding this comment.
KdfPasswordPbkdf2 / KdfPasswordArgon2?
|
|
||
| KdfPassword::class => [ | ||
| '__construct()' => [ | ||
| 'algorithm' => 'sha512', // any hash_hmac_algos() |
| ): string { | ||
| $payload = $this->cryptors[$this->currentVersion]->encrypt($data, $secret, $context); | ||
|
|
||
| return $this->currentVersion . $payload; |
There was a problem hiding this comment.
Metadata is not being authenticated. Shouldn't it?
| $dataEncrypted = $this->cipher->encrypt($data, $dek, $dataNonce); | ||
|
|
||
| // keySalt || dekNonce || cipherdek || tag || dataNonce || ciphertext || tag | ||
| return $keySalt . $dekNonce . $dekEncrypted . $dataNonce . $dataEncrypted; |
There was a problem hiding this comment.
Metadata is not authenticated. Shouldn't it?
| string $salt, | ||
| ): string { | ||
| try { | ||
| $key = sodium_crypto_pwhash($keySize, $secret, $salt, $this->opslimit, $this->memlimit, $this->algo); |
There was a problem hiding this comment.
| $key = sodium_crypto_pwhash($keySize, $secret, $salt, $this->opslimit, $this->memlimit, $this->algo); | |
| $key = sodium_crypto_pwhash(32, $secret, $salt, $this->opslimit, $this->memlimit, $this->algo); |
This size should not depend on algo.
| string $salt, | ||
| ): string { | ||
| try { | ||
| $key = hash_pbkdf2($this->hashAlgo, $secret, $salt, $this->iterations, $keySize, true); |
There was a problem hiding this comment.
| $key = hash_pbkdf2($this->hashAlgo, $secret, $salt, $this->iterations, $keySize, true); | |
| $key = hash_pbkdf2($this->hashAlgo, $secret, $salt, $this->iterations, 32, true); |
This size should not depend on algo.