initphp/encryption 2.0 is a deliberate hard reset. It tightens the type
system, derives keys to the right length automatically, defaults to a safer
serializer, and adopts a self-describing ciphertext format that rejects
ambiguous input.
The cost: ciphertexts produced by 1.x cannot be decrypted by 2.x. You need a re-encryption plan before you upgrade.
This page is the full checklist.
- Bump PHP to 8.1+ in your CI and production environments.
- Add 1.x and 2.x side by side temporarily so you can decrypt with 1.x and re-encrypt with 2.x (see Re-encryption pattern).
- Replace
Encrypt::create(...)withEncrypt::use(...). - Decide whether your stored payloads should switch to JSON (recommended)
or keep using
serialize/unserialize. The new option is'serializer' => 'php_serialize'if you keep the old behaviour. - Remove the old, manually-derived 32-byte key dance for Sodium — pass any non-empty key now.
- Drop
ext-mbstringfrom your runtime requirements if you only required it for this package.
| 1.x | 2.x | |
|---|---|---|
| PHP | >= 7.4 |
^8.1 |
The 2.x source uses mixed, static return types, enum (via match), and
readonly-friendly patterns that require PHP 8.1.
1.x ciphertexts had no version byte: the first bytes were the HMAC (for
OpenSSL) or the nonce (for Sodium), followed directly by the encrypted
payload. 2.x prepends a 2-byte header (VERSION || SERIALIZER_FLAG).
Concretely, this means a 2.x handler asked to decrypt a 1.x ciphertext will throw:
EncryptionException: Unsupported ciphertext format version 0x..; expected 0x02.
Ciphertexts produced by 1.x are not readable by 2.x.
There is no auto-upgrade path. Re-encrypt your data (see below) before flipping the handler classes.
| 1.x | 2.x | |
|---|---|---|
| Default | serialize() / unserialize() |
JSON |
| Why | Backwards-compatible with PHP's native shapes. | unserialize() of attacker-controlled bytes is the canonical PHP object-injection vector. JSON cannot instantiate classes. |
| Opt-in to old behaviour | n/a | 'serializer' => 'php_serialize' |
If your payloads are scalars, arrays, or plain objects (stdClass /
deeply-mapped DTOs), JSON is fine and is the recommended target. If they
contain raw binary blobs (e.g. random_bytes() output), JSON is not
8-bit-clean — keep php_serialize.
1.x silently broke if the user key wasn't exactly 32 bytes long. The README
example ('key' => 'TOP_Secret_Key', 14 bytes) actually couldn't run.
2.x derives a 32-byte key from any non-empty input via
sodium_crypto_generichash. Any key string you can pass in now Just Works.
If your application was working around this by passing a pre-derived 32-byte key, stop doing that — let the package do the derivation. If you keep doing your own derivation and the result differs from BLAKE2b of your old key, your ciphertexts won't decrypt cleanly across the upgrade.
Encrypt::create()— was an alias forEncrypt::use(). UseEncrypt::use()instead.ext-mbstringrequirement — the package no longer uses any mbstring function. Drop it from yourrequireif it was there only for this package.
OpenSSLandSodiumare nowfinal. If you were extending them, switch to extendingBaseHandler(see 04 — Custom Handlers).decrypt()returnsmixedinstead of an untyped value. Consumers typehinted: stringwill start to fail.EncryptionExceptionextends\RuntimeException(was\Exception). All existingcatch (Exception $e)andcatch (EncryptionException $e)callsites continue to work.- The HandlerInterface is now narrower: it requires
encrypt(),decrypt(), andsetOptions()withmixed/string/statictypes. If you implemented the interface yourself, update the signatures.
Encrypt::use()accepts a typedstring|HandlerInterfaceparameter (previously documented but not enforced).BaseHandler::SERIALIZER_JSONandBaseHandler::SERIALIZER_PHPconstants for setting the serializer option without string literals.BaseHandler::FORMAT_VERSIONconstant (currently0x02) — useful when writing tests against the wire format.BaseHandler::setOptions()andsetOption()returnstatic, so fluent chains preserve the concrete handler type.- New per-handler error messages identifying what specifically went wrong
(the 1.x message was always
"Decryption failed!").
The minimal-disruption recipe:
// composer.json: require BOTH versions, namespaced separately, e.g. via a
// fork like initphp/encryption-v1 (manual setup). In practice most users
// vendor the 1.x source into App\Legacy\Crypto for the migration window
// and delete it afterwards.
use App\Legacy\Crypto\OpenSSL as LegacyOpenSSL; // 1.x
use InitPHP\Encryption\OpenSSL; // 2.x
$legacy = new LegacyOpenSSL([
'key' => getenv('APP_ENCRYPTION_KEY'),
// ... whatever options 1.x was using
]);
$new = new OpenSSL([
'key' => getenv('APP_ENCRYPTION_KEY'),
// 'serializer' => 'php_serialize', // ← keep the old serializer
// until you also convert payloads
]);
// Migration step (background job, batched):
foreach (legacyCiphertextRows() as $row) {
try {
$plaintext = $legacy->decrypt($row->ciphertext);
} catch (\Throwable $e) {
// 1.x didn't have version bytes; a successful decrypt is the
// strongest evidence the row is in fact 1.x.
logger()->warning('legacy decrypt failed', [
'id' => $row->id,
'msg' => $e->getMessage(),
]);
continue;
}
$newCiphertext = $new->encrypt($plaintext);
persist($row->id, $newCiphertext);
}When every row has been re-encrypted: remove the legacy import,
composer remove the fork, delete the migration job, and drop the dual-key
fallback from your runtime code path.
Then either:
- Stay on 1.x. It still works on its supported PHP range. There is no forced upgrade.
- Pin 1.x indefinitely in your
composer.json("initphp/encryption": "^1.0") and accept that security fixes will only land in 2.x.
Per the org-wide security policy, only the latest stable major receives security fixes. Once 2.0 ships, 1.x is end-of-life.
- CI passes on PHP 8.1+ (drop older PHP from your matrix).
- All
Encrypt::create(...)calls replaced withEncrypt::use(...). - No code extends
OpenSSLorSodiumdirectly (extendBaseHandler). -
decrypt()callers handlemixed(or you cast in one place). - Re-encryption job has finished, or you have an explicit dual-stack
decrypt path with a
try/catch (EncryptionException)fallback. - You decided whether to switch to
'serializer' => 'json'or pin'serializer' => 'php_serialize'. -
ext-mbstringremoved from your require list if you don't need it elsewhere.
- 01 — Getting Started — to learn the new defaults.
- 05 — Options — the full new option surface.
- 07 — Security — what
unserializeon attacker bytes used to risk, and why JSON is the new default. CHANGELOG.md— the canonical list of changes in 2.0.