Issue #3590533: Switch to AES-256-GCM with versioned format and migration

Summary

Three coordinated changes addressing the three cryptographic issues in the 2.0.x line of FuzzyConfigKeyProvider:

  1. 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.
  2. 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 whether hash_salt is base64-shaped or arbitrary text. The previous derivation silently dropped non-base64 characters and could collapse to far fewer than 32 bytes of entropy.
  3. Versioned storage format. New writes carry the literal v1: prefix followed by base64(IV || ciphertext || GCM tag). Legacy (no-prefix) stored values from 2.0.x remain readable via a decryptLegacy() fallback path keyed off the old base64_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_NAME runs once on drush updb after upgrading to 2.1.x, walks every key.key.* config entity whose provider is fuzzy_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 the fuzzy_key_provider channel and the update continues.
  • drush fuzzy_key_provider:re-encrypt exposes 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.

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.

Merge request reports

Loading