Skip to content

Add Cubex\Encryption: XChaCha20-Poly1305 with key rotation - #74

Closed
TomK wants to merge 2 commits into
masterfrom
encryption-sodium
Closed

TomK wants to merge 2 commits into
masterfrom
encryption-sodium

Conversation

@TomK

@TomK TomK commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

Cubex 4 has no encryption primitive, so apps bring their own and have no built-in way to rotate a key. This adds the same design as the 2.x line (#73) in v4 style.

  • Cubex\Encryption\Encrypter implements EncrypterInterface (string in, string out) with XChaCha20-Poly1305. A payload is base64url of a version byte, a random 24-byte nonce and the ciphertext with its tag. The version byte is bound as additional data. The cipher key is derived with HKDF-SHA256 from each configured key, which must be at least 16 bytes.
  • Keys: security.encryption_key is a key or a list. The first key encrypts; later keys only decrypt. An empty first entry throws, so an unset env var can't promote an old key.
  • Getting an encrypter: Encrypter::fromConfig($config) builds one from config. Cubex registers a lazy EncrypterInterface factory that reads the context config, so nothing runs until an encrypter is retrieved.
  • No default key: 2.x keeps its hard-coded default for backwards compatibility; v4 has nothing to stay compatible with, so a missing key throws.
  • Sodium is optional: ext-sodium or paragonie/sodium_compat is listed under suggest and checked only when an encrypter is constructed. There is no fallback algorithm.
  • CI: the workflow now loads ext-sodium explicitly, because the Windows PHP builds from setup-php don't load it by default. actions/checkout also moves to its latest major, v7.
  • No Illuminate code: v4 has no Illuminate dependency, and no v4 app can hold payloads written by the old 2.x illuminate/encryption encrypter, so this PR has no legacy decrypt path and no Illuminate adapter.

Previous keys are normally compromised, so payloads they decrypt may be forged. Keep the rotation window short.

Test plan

EncrypterTest covers:

  • round trip, payload format and size, and nonce randomness
  • rotation and wrong key
  • tampering with the version, nonce, ciphertext or tag, and a forged version
  • malformed payloads, and empty or short keys
  • missing sodium, simulated with a test subclass
  • INI list config, missing config, and the lazy Cubex factory

Results:

  • PHP 8.2 with coverage (CI command): 160 tests pass, and coverage-check reports 75.34% against a threshold of 70. Encrypter has 100% coverage.
  • PHP 8.5: all 160 tests pass.
  • Baseline: master at c56e8db without this change has 133 passing tests.
  • GitHub Actions: before sodium was enabled, the Windows jobs failed because every EncrypterTest threw the missing-sodium error. With it enabled, all six jobs (Ubuntu and Windows, PHP 8.2 to 8.4) pass.

Rollout

Release 4.28.0 from master with gh release create.

🤖 Generated with Claude Code

EncrypterInterface (string in, string out) with an XChaCha20-Poly1305
implementation. Payloads are base64url of a version byte, a random 24
byte nonce and the ciphertext with tag; the version is bound as
additional data. Cipher keys are derived with HKDF-SHA256 from configured
keys of at least 16 bytes.

Keys come from security.encryption_key, a key or a list: the first key
encrypts, later keys only decrypt. An empty first entry throws instead of
promoting an old key. Cubex registers a lazy EncrypterInterface factory
reading the context config.

Sodium is suggested (ext-sodium or paragonie/sodium_compat) and checked
when an encrypter is constructed.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@TomK
TomK force-pushed the encryption-sodium branch from 7b542a5 to c0daa4a Compare September 29, 2026 09:24
The Windows PHP builds from setup-php do not load sodium by default.
Also move actions/checkout to its latest major, v7.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@TomK

TomK commented Sep 29, 2026

Copy link
Copy Markdown
Contributor Author

Superseded by packaged/http#9, which adds the encrypter along with an encrypted cookie handler, so v4 apps get it through packaged/http's CookieJar.

@TomK TomK closed this Sep 29, 2026
@TomK
TomK deleted the encryption-sodium branch September 29, 2026 12:21
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.

1 participant