v2.1: Files as secure messages - #38
Draft
robbinjanssen wants to merge 6 commits into
Draft
Conversation
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>
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
Adds files as secure messages (v2.1, fully additive on top of #37). 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. Same split-key security, hit points, expiry and events as text messages. The wire format andCryptoare untouched.Core
Factory::makeFile($path, $hitPoints, $expiresAt, $fileName)— reads a file, detects the mime type (ext-fileinfo when available, suggested in composer.json), stores name/mime/size in the encrypted meta.Laravel
SecureMessage::encryptFile($pathOrSplFileInfo, ...)— uploads work out of the box (client file name is used, client mime type deliberately is not). Max size guarded by the newmax_file_sizeconfig (default 10 MB; files are encrypted in memory and the stored blob is ~3x the original).files_disk_nameconfig) under afiles/prefix; the database record has a nullcontentcolumn, which marks it as a file message. A new migration makescontentnullable — upgrading isphp artisan migrate.files/prefix prevents blob/key-file collisions when both disks point at the same location.destroy()and housekeeping clean up the blob as well.Verification
composer test: 51 tests, 325 assertions, green — all pre-existing tests pass unchanged, proving the feature is additive and the files disk is only resolved for file messages.composer analyse: PHPStan level 6 clean; php-cs-fixer dry run clean.docs/examples/file_example.phpround-trips a binary file end-to-end.RefreshDatabase) and uses Laravel-nativechange()(no doctrine/dbal).Notes for review
getMeta()without the verification code — don't render it on pre-verification pages.download()response helper was deliberately left out to keep the surface small; docs/laravel.md contains a 5-line recipe. Streaming encryption for large files is a possible follow-up.🤖 Generated with Claude Code