Skip to content

v2.1: Files as secure messages - #38

Draft
robbinjanssen wants to merge 6 commits into
feature/v2-modernizationfrom
feature/file-messages
Draft

v2.1: Files as secure messages#38
robbinjanssen wants to merge 6 commits into
feature/v2-modernizationfrom
feature/file-messages

Conversation

@robbinjanssen

Copy link
Copy Markdown
Contributor

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 and Crypto are 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.
  • File names and mime types are validated as UTF-8 in the setters, because the meta is JSON encoded inside the crypto path.

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 new max_file_size config (default 10 MB; files are encrypted in memory and the stored blob is ~3x the original).
  • Encrypted blobs are stored on a new, lazily resolved files disk (files_disk_name config) under a files/ prefix; the database record has a null content column, which marks it as a file message. A new migration makes content nullable — upgrading is php artisan migrate.
  • Lazy resolution means installations that never use file messages need zero configuration changes; the 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.php round-trips a binary file end-to-end.
  • The new migration runs on sqlite (testbench RefreshDatabase) and uses Laravel-native change() (no doctrine/dbal).

Notes for review

  • Documented caveat: the file name is part of the meta and is readable server side via getMeta() without the verification code — don't render it on pre-verification pages.
  • A 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

robbinjanssen and others added 5 commits August 11, 2026 09:30
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>
@robbinjanssen robbinjanssen added the new-feature New features or options. label Aug 11, 2026
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

development new-feature New features or options.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants