Skip to content

Releases: spaze/encryption

Asymmetric encryption, and stored values that say what created them

Choose a tag to compare

@spaze spaze released this 03 Aug 03:14
v3.0.0
69bdb31

TL;DR

  • Asymmetric encryption: Two new ways to encrypt
  • Markers: Stored values now say what created them.
  • Everything written by previous versions still decrypts, and always will.

Encryption between two parties, encryption to a public key

  • AuthenticatedPublicKeyEncryption: each side configures its own secret key and the other side's public key under one key id; both sides encrypt and decrypt, and decryption also proves the data came from the other party, or from us. (#38)
  • AnonymousPublicKeyEncryption: anyone with the public key can encrypt, only whoever holds the matching secret key can decrypt. A deployment that only writes data needs just the public keys and cannot read anything back, not even what it has just encrypted. (#38)
  • Key configuration works like the symmetric one, prefix_hex values; the recommended tagged form prefix_secret_hex/prefix_public_hex makes a key pasted into the wrong slot fail when the object is created, instead of producing data nobody can ever decrypt. A mismatched pair (AnonymousPublicKeyEncryption) or a missing half (AuthenticatedPublicKeyEncryption) fails there too. (#38)

Stored values carry a marker

  • Newly written values look like $keyId$SymV1$... (SymAdV1, AuthV1, AuthAdV1, AnonV1). Feeding a value to the wrong class or the wrong method fails with an exception that names the class and method that created it, instead of a misleading decryption error. (#38, #40)
  • The key id and the marker go into what decryption verifies (except in AnonymousPublicKeyEncryption, where a sealed value has no place for that): changing either in a stored value, stripping the marker, or forging one onto an old value makes decryption fail. The old "key id is not protected against tampering" caveat now applies only to values in the old format. (#38, #40)
  • Everything written by previous versions still decrypts, and always will.

Upgrading

  • needsReEncrypt() now also returns true for values in the old format, which is everything written before this release, so your usual re-encryption sweep migrates all data to the marked format. Rows created by encryptWithAd() must be re-encrypted with the row's additional data. (#38, #40)
  • Older versions cannot read the marked values: when multiple deployments share data, upgrade all of them before writing anything new. (#38, #40)
  • Mixed-up values now throw FormatMarkerMismatchException or UnknownFormatMarkerException where the old code threw Halite's InvalidMessage or InvalidNumberOfComponentsException; catch blocks written for the old types won't catch the new ones. The format-error message now names both accepted shapes. (#38, #40)
  • Requires paragonie/halite 5.1+ (was 5.0). (#36)

Smaller changes

  • Key ids and markers repeated in exception messages are shortened and made printable, so a key pasted into an id slot or a tampered stored value can't push key material or garbage into logs. (#38, #39)
  • README: key generation examples now use sodium_bin2hex() (#42), keys are documented as valid in both lowercase or uppercase (#43, #44), and the runtime-call service example uses the right parameter name (#39).
  • For contributors: PHPStan checks the code with all bundled rules of spaze/phpstan-disallowed-calls, including the new non-timing-safe encoding rules (#37, #42), and the repo has an AGENTS.md with the invariants that AI tools and reviewers must respect (#41).

Validate the configuration in the constructor, reject malformed encrypted data early

Choose a tag to compare

@spaze spaze released this 30 Jul 16:16
v2.3.2
a44397b

What's Changed

  • Follow-ups to the constructor validation from 2.3.0 (#35). A numeric key id no longer fails with a TypeError, and InvalidKeyPrefixException no longer includes the key value in its message. A key with no prefix at all now throws the new MissingKeyPrefixException, which extends InvalidKeyPrefixException, so existing catch blocks keep working.
2.3.1 release notes below
  • Require ext-sodium in composer.json (#34). The requirement was already announced in the 2.3.0 release notes below, but composer.json in 2.3.0 was accidentally missing it, this release adds it.
2.3.0 release notes below
  • Validate keys, key ids, and the active key id in the constructor instead of failing at first use (#31)
    • A misconfigured key - wrong length, not valid hex, a key id that would break the output format (empty or containing $), or an active key id missing from the keys array - now throws when the service is created, not on the first encrypt() call after a deploy.
    • New exceptions: InvalidKeyEncodingException, InvalidKeyLengthException, InvalidKeyIdException, ActiveKeyIdNotFoundException.
  • Reject encrypted data in a format that this library never produces (#32)
    • Inputs like garbage$keyId$ciphertext, $keyId$ or $$ciphertext now throw the new InvalidCipherTextFormatException instead of failing later with confusing downstream exceptions.
    • InvalidNumberOfComponentsException now extends the new class, so existing catch blocks keep working.
  • Documentation additions (#33)
    • Keys defined in Nette config parameters also end up in the compiled DI container file in the temp directory, treat it accordingly.
    • From the README: "The key id in the output […] is the only part of the output not protected against tampering […] Never configure the same key under two different ids."

Upgrade notes

  • Nothing changes for correctly configured setups, and everything they ever encrypted still decrypts.
  • The library now requires ext-sodium (bundled with PHP since 7.2); previously it could in theory run on the sodium_compat polyfill that Halite uses.
  • If a configuration was broken without you knowing, the constructor will now tell you.
  • One case possibly needs care: a key truncated to an odd number of hex characters (e.g. 63 instead of 64) used to work, because the decoder used previously, Hex::decode(), would quietly prepend a 0 instead of rejecting the input, so the key was used as if it had a leading zero, without anyone knowing. Keys are now decoded with sodium_hex2bin(), which rejects anything that is not valid hex, so such a configuration throws InvalidKeyEncodingException. To keep the already encrypted data readable, add the 0 back yourself, right after the prefix separator - that is byte for byte the key that was actually used.

Add Additional Authenticated Data (AAD) support

Choose a tag to compare

@spaze spaze released this 19 Jul 20:52
v2.2.0
301cb61

What's Changed

  • Add Additional Authenticated Data (AAD) support via encryptWithAd()/decryptWithAd() (#30)
    From the README: "Additional Authenticated Data (AAD) cryptographically binds a ciphertext to a context (like a row id, column name, or tenant id). The additional data (the context) is not encrypted, and thus it must not be a secret. This prevents attackers or buggy scripts from copying a valid ciphertext from one place and pasting it into another."
  • Docs updates (#28)
  • Bump actions/checkout (#26, #27)

Support PHP 8.5 and internal improvements

Choose a tag to compare

@spaze spaze released this 23 Nov 16:54
322c078

Internal changes and improvements:

  • Run tests on PHP 8.5 too (#22)
  • Exclude PHPCS constant type hint checks (#21)
  • Bump actions/checkout from 4 to 5 (#20)
  • When analyzing tests with PHPStan, extensions need to be specified (#23)
  • PHPStan 2.1 & strict rules (#24)
  • Test --prefer-lowest dependencies, too (#25)

Support PHP 8.4

Choose a tag to compare

@spaze spaze released this 26 Oct 00:04
095408b
  • Support PHP 8.4 (#19)

v2: support key prefixes, renamed class, no key groups

Choose a tag to compare

@spaze spaze released this 19 Jan 21:40
ad1dceb

Backwards compatibility breaks

  • Rename Symmetric\StaticKey class to SymmetricKeyEncryption (#12)
  • Use custom exceptions but new classes extend from the old ones, so your catch blocks should still work (#14)
  • Drop key group constructor param, you can always just pass particular keys (#16)
  • Add a prefix to encryption keys so they're easier to find with tools like Gitleaks (#18)

Other changes

  • Require PHP 8.2 (#13)
  • Hide sensitive keys and plaintext from stack traces with SensitiveParameter attributes available starting PHP 8.2 (#15)
  • Generate code coverage report (#17)

Support PHP 8.3

Choose a tag to compare

@spaze spaze released this 28 Oct 14:28
5d50c09

What's Changed

Support PHP 8.2

Choose a tag to compare

@spaze spaze released this 27 Nov 01:48
a763408
  • Tests are now running on PHP 8.2 as well (#7)
  • Internal code cleanup, uses, throws, types, [] instead of list() (#8)
  • "Tests passing" badge in README (#9)

Require PHP 8.0 at least

Choose a tag to compare

@spaze spaze released this 19 Jan 14:08

... not only because this release uses Halite v5 which requires PHP 8.0 at least.

Needs re-encrypt?

Choose a tag to compare

@spaze spaze released this 14 Apr 01:34
  • Added needsReEncrypt(): bool to see if the ciphertext was encrypted using an inactive key and thus should be re-encrypted ("key rotation") with the currently active one (#3, thanks @stpnkcrk!)
  • Tests, at last!