v2.0: PHP 8.2, Laravel 12/13, PHPUnit 11 and static analysis - #37
Merged
Conversation
Adds orchestra/testbench (Laravel 12/13), PHPUnit 11, Mockery 1.6, PHPStan with larastan and phpstan-mockery. Fixes the autoload-dev mapping, which pointed the root namespace at tests/ and never actually PSR-4-resolved. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Rewrites phpunit.xml to the 11.5 schema (backupStaticAttributes no longer exists, coverage include moved to <source>) with separate Core and Laravel test suites. Renames the test namespace to Exonet\SecureMessage\Tests, adds the Mockery-PHPUnit integration trait so expectations are actually verified, and adds void return types and strict_types to all tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
These tests never ran: they required a host Laravel application bootstrap file and their setUp/tearDown signatures have been fatal since PHPUnit 8. They now run on testbench with an in-memory sqlite database. This also fixes fixtures that omitted NOT NULL columns, a Carbon::setTestNow() leak, a Storage factory mock that stood in for the Filesystem returned by disk(), and moves the tests out of src/ so they no longer ship in dist installs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
declare(strict_types=1) everywhere, typed properties, constructor promotion, readonly and match. Properties wiped by sodium_memzero() stay nullable because memzero nulls its by-reference argument. setMeta() casts hit_points/expires_at to int to preserve v1 behaviour for numeric-string input. Behavioural fixes: - A malformed encrypted message now throws DecryptException instead of crashing with a TypeError (the wire format itself is unchanged). - The Laravel event payload is now a public readonly property; it used to be private without a getter, so listeners could never read it. - The migration is an anonymous class (same filename, so existing installations are unaffected). - The secure_messages config comment claimed the meta key is 32 characters; it is 10. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Uses larastan for the Eloquent magic and phpstan-mockery for the test mocks. Runs clean via 'composer analyse'. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replaces the php-actions images with shivammathur/setup-php and the composer-managed PHPUnit. Laravel 11 is EOL with open security advisories (composer refuses to install it), so the matrix covers testbench 10 (Laravel 12) and 11 (Laravel 13, PHP 8.3+). Adds a PHPStan job and the declare_strict_types rule to php-cs-fixer; the auto-commit style job is unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fixes the meta key examples (the README used 32 characters, docs/using.md used 11; the required length is exactly 10), documents the new requirements and adds an upgrade guide covering the breaking changes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A file is a regular SecureMessage: the file bytes are the (binary safe) content and the file name, mime type and size travel along in the already encrypted meta data. Factory::makeFile() reads a file from a path, with an optional file name override for files on temporary paths such as uploads. File names and mime types must be valid UTF-8, enforced in the setters: the meta data is JSON encoded inside Crypto::encrypt(), and json_encode() returning false would surface as a TypeError inside the crypto path. This keeps Crypto itself unchanged. Mime detection uses ext-fileinfo when available (suggested in composer.json) and falls back to application/octet-stream. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds round-trips for binary content (all 256 byte values, >1MB random bytes), file meta accessors including the UTF-8 guards, meta survival through encrypt/decrypt and through the failed-decrypt hit-point flow, and the makeFile happy and error paths. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
File blobs go to a new, lazily resolved files disk (config key files_disk_name) under a 'files/' prefix; the database record is stored with a null content column, which is what marks a record as a file message. A new migration makes the content column nullable. Two deliberate design points: - The files disk is resolved lazily and memoized, never in the constructor: existing installations upgrading to 2.1 have no files disk configured, and eager resolution would break every one of them. This is also why destroy() checks the record before touching the files disk (Housekeeping destroys plain messages too). - The 'files/' prefix prevents a blob from overwriting the storage key file when the files disk and the storage key disk point at the same location. The encrypted content is always loaded onto the SecureMessage before decrypting, also on failure paths: the hit-point reduction and the DecryptException constructor both need it. encryptFile() accepts a path or an SplFileInfo (so Laravel/Symfony uploads work out of the box, using the client name but never the client mime type) and enforces the new max_file_size config setting before reading the file into memory. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Covers encryptFile (blob on the files disk, null content column, max size guard), the file decrypt flow including a missing blob and the hit-point limit path, and destroy for file messages. All pre-existing tests pass unchanged, which proves the files disk is only resolved for file messages. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds usage documentation for makeFile/encryptFile, a runnable example, the files disk setup with a security note on separating it from the storage key disk, the 'php artisan migrate' upgrade step, and a caveat that the file name is part of the meta data and thus readable server side without the verification code. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
encryptFile()/makeFile() only read the file; removing the unencrypted original is the responsibility of the application. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Laravel events expose their SecureMessage as a public readonly property, so listeners (and anything they serialise the event to, such as a queued listener writing to Redis) can read it. Most decrypt-failure paths hand over a wiped instance, because the DecryptException constructor wipes the keys when it is given the secure message. Three paths throw without it and left the decrypted key material on the dispatched instance: - a missing storage key file (database key + verification code present); - malformed stored ciphertext (all key parts present); - a missing file blob (database key + verification code present). This violates the split-key promise that the key parts never co-locate. Wipe the secure message in the catch block, before any event is dispatched, regardless of whether the exception carried it. The already-wiped paths are unaffected (wiping is idempotent). Regression tests assert the dispatched event carries no key material on both null paths. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
robbinjanssen
marked this pull request as ready for review
August 27, 2026 07:45
Wipe keys before dispatching decrypt-failure events
ltenhagen
reviewed
Aug 27, 2026
The 1.8x in using.md is the core encrypted payload; the 3x in laravel.md and the config is the stored blob, which the Laravel integration encrypts a second time. Both are measured values; the note makes the difference explicit so they no longer read as a contradiction. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
mb_check_encoding reads nicer, but mbstring is not a package dependency and PCRE always is; both validate strict UTF-8 equally. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The configured meta key is 10 characters; the key the crypto validates is the composed database key (11) + storage key (11) + meta key (10). Spelled out so the 10 in the config and the 32 in Crypto no longer read as a contradiction. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
v2.1: Files as secure messages
ltenhagen
approved these changes
Aug 27, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
v2.0 modernization of the package. The encrypted wire format and the key structure are unchanged: messages encrypted with v1 remain decryptable (covered by the existing hard-coded ciphertext fixtures in
CryptoTest).^8.2(8.2/8.3/8.4) withext-sodiumas an explicit requirement; the whole code base usesdeclare(strict_types=1), typed properties, promotion,readonlyandmatch.src/Laravel/tests/(which shipped in dist installs) totests/Laravel/.MockeryPHPUnitIntegration).composer analyse.public readonly); malformed ciphertext crashed with aTypeError(now a cleanDecryptException); the meta key examples in README (32 chars) and docs/using.md (11 chars) did not match the required length of 10.Breaking changes
See the "Upgrading from v1" section added to the README. Highlights: PHP
^8.2, Laravel 12/13,ext-sodiumrequired at install time, strict types (check published configs for numeric strings inhit_points/expires_in),DecryptExceptioninstead ofTypeErrorfor malformed input, and the migration is now an anonymous class (same filename, existing installs unaffected).Verification
composer test: 34 tests, 210 assertions, green (Core + Laravel suites).composer analyse: PHPStan level 6, no errors.docs/examples/basic_example.phpruns and decrypts the v1 wire format.config.platform.php.🤖 Generated with Claude Code