Summary
Three coordinated changes addressing the three cryptographic issues in the 2.0.x line of FuzzyConfigKeyProvider:
- Cipher. AES-256-CBC → AES-256-GCM. GCM is authenticated — any tampering with stored bytes causes decryption to fail, instead of silently returning corrupted plaintext.
- Key derivation.
base64_decode(Settings::get('hash_salt'))→hash('sha256', Settings::get('hash_salt'), TRUE). SHA-256 produces a stable 32-byte AES key regardless of whetherhash_saltis base64-shaped or arbitrary text. The previous derivation silently dropped non-base64 characters and could collapse to far fewer than 32 bytes of entropy. - Versioned storage format. New writes carry the literal
v1:prefix followed bybase64(IV || ciphertext || GCM tag). Legacy (no-prefix) stored values from 2.0.x remain readable via adecryptLegacy()fallback path keyed off the oldbase64_decode(hash_salt)derivation, so sites upgrading from 2.0.x keep reading their stored keys until the post-update migrates them. 2.1.x never writes the legacy format.
The v1: prefix is unambiguous: a legacy stored value is pure base64 (alphabet [A-Za-z0-9+/=]), which cannot contain : at any position.
Upgrade path
Two layers, no operator intervention required:
hook_post_update_NAMEruns once ondrush updbafter upgrading to 2.1.x, walks everykey.key.*config entity whose provider isfuzzy_config, reads the stored value through the legacy fallback path, and re-saves through the v1 encrypt path. Idempotent. Per-key failures are logged via thefuzzy_key_providerchannel and the update continues.drush fuzzy_key_provider:re-encryptexposes the same walker on demand. Re-runnable; same idempotency contract.
Both paths share a tiny static helper Drupal\fuzzy_key_provider\KeyMigrator rather than a permanent container service — the migration is one-shot and the helper has a known end-of-life when the legacy decrypt path is eventually removed.
Tests
Kernel test Drupal\Tests\fuzzy_key_provider\Kernel\FuzzyConfigKeyProviderTest covers:
- v1 encrypt → decrypt round-trip.
- IV randomness (same plaintext encrypted twice produces different ciphertexts).
- GCM auth-tag tampering detection (flipping a bit in the ciphertext makes decryption return
''). - Legacy fallback read with an inline-built CBC fixture.
- Migrator end-to-end: legacy key gets re-encrypted to v1; already-v1 key is untouched byte-for-byte.
Versioning
Ships as a 2.1.0 minor release. Public API is unchanged, the migration is fully automatic via drush updb, and the existing 2.0.x stored values remain readable until they are migrated — none of the criteria that would warrant a major bump under Drupal contrib convention apply.
Note on phpstan locally
Running phpstan with a vanilla config (not mglaman/phpstan-drupal) surfaces a handful of \Drupal::* and Key-method-signature warnings on src/KeyMigrator.php. These resolve cleanly under the contrib CI template (.gitlab-ci.yml) which loads phpstan-drupal and provides the necessary stubs. The MR pipeline should report green.
Related
A follow-up issue will add a previous-hash_salt fallback so rotation can be staged gracefully. That work depends on this MR's v1 format — the GCM auth tag is what makes "try multiple salts" safe (CBC silently returns garbage on a wrong key, GCM fails loudly). I'll link the follow-up issue once filed.