Files
SealShare/CHANGELOG.md
T
Andreas Reinhold / reiniandClaude Opus 5 40e35bab0e Encrypt uploads in the browser and send them in chunks
A 6 GB upload kept a customer waiting long after its progress bar
reached 100%. The server wrote every upload three times: PHP's
temporary file, Livewire's copy of it ("Processing files...") and the
encrypted file ("Create Share Link"), each a full rewrite of a slow
disk. The unencrypted copy also stayed behind in livewire-tmp.

Now the uploader's browser encrypts each file in 16 MB chunks with
WebCrypto and PUTs them one at a time; the server checks each chunk in
memory and writes it once, already encrypted. Creating the share only
wraps its key and saves the options. A 200 MB upload through the
Docker image took 2.8 s, and its download matched byte for byte.

- SEALCHK2: a 19-byte header (chunk size, 7-byte nonce prefix), then
  ciphertext and tag per chunk. Each nonce holds the chunk index and a
  last-chunk flag (the STREAM construction), so cut or reordered files
  fail to decrypt. SEALCHK1 and the single-block format still read.
- Envelope encryption: one random key per share. With a password it is
  wrapped with Argon2id (sodium, libsodium's interactive limits) in
  shares.wrapped_key, which names its parameters. Password shares from
  before keep their PBKDF2-derived key.
- The upload page registers each selection with FileUploader into a
  pending share of its own, lists the files with their progress, retries
  a failed chunk after 1-16 s, then offers Retry; Remove and Cancel
  abort. UploadChunkController only accepts chunks from the session that
  started the share: a repeat is acknowledged, a skip gets 409 with the
  count stored. Chunks go out as Blobs, which Chromium sends about eight
  times faster than ArrayBuffers.
- Uploads need a secure context: over plain HTTP the page says HTTPS is
  needed and takes no files. The Docker image gains AUTO_HTTPS, which
  serves Let's Encrypt on 443 for SERVER_NAME and redirects 80; without
  it the container stays on HTTP 80 behind a proxy. docker/Caddyfile was
  never loaded and is gone; docker/healthcheck.sh covers both modes.
- "Download all" streams the ZIP with maennchen/zipstream-php (STORE,
  ZIP64) instead of decrypting whole files into memory and writing the
  archive unencrypted to /tmp.
- Pending shares count towards the quota, stay out of the admin
  dashboard and 404 everywhere else. shares:cleanup deletes uploads idle
  for 4 hours and Livewire temporary files older than that.
- PHP's upload limits no longer cap the admin's max file size and
  default to 64M; LIVEWIRE_MAX_UPLOAD_TIME is gone and
  UPLOAD_CHUNK_SIZE_MB is new.
- Tests cover the format, key wrapping, registration limits, the chunk
  endpoint's answers, completing a share, the streamed ZIP, cleanup,
  and in Chromium a real chunked upload and the HTTPS warning; the
  selected-files overflow test runs again. README, website, CHANGELOG
  and .ai/rules follow.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 20:49:17 +02:00

13 KiB
Raw Blame History

Changelog

All notable changes to this project are documented in this file.

The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.

2.1.0 - Unreleased

Added

  • A password generator for share passwords. With "Password protect" on, the upload page offers Generate and Copy under the password field. On the next page the password is offered once more beside the link — masked, with a copy button at the end of the field like the link's. It is passed along encrypted in the session and never stored.
  • A "Share Passwords" card in Admin settings controls the generator:
    • Mode: off, on request (the Generate button) or prefilled as soon as protection is switched on.
    • Kind: random characters (length 1264; uppercase, lowercase, numbers and symbols; look-alike characters left out if chosen) or a passphrase (410 words from EFF's large word list, with a chosen separator).
    • An example with its estimated entropy shows before saving.
    • Defaults: on request, 20 letters and numbers without look-alikes.
  • AUTO_HTTPS for the Docker image: with AUTO_HTTPS: "true" and SERVER_NAME set to the domain, the container fetches a Let's Encrypt certificate, serves HTTPS on port 443 and redirects port 80. Without it the container serves plain HTTP on port 80, as before, for a reverse proxy in front.
  • UPLOAD_CHUNK_SIZE_MB (default 16) sets the size of each encrypted chunk the browser sends.

Changed

  • Breaking: uploads need HTTPS. Files are now encrypted in the uploader's browser with WebCrypto, which browsers only offer over HTTPS or on localhost. Over plain HTTP the upload page says so and takes no files; downloads keep working. Serve SealShare with AUTO_HTTPS or behind a reverse proxy that terminates TLS.
  • Uploads are encrypted in the browser and sent in chunks of 16 MB, each written to disk once, already encrypted. A large file no longer waits on "Processing files..." or on "Create Share Link": before, the server wrote every upload three times (PHP's temporary file, Livewire's temporary copy, the encrypted file). The server checks every chunk as it arrives. A chunk that fails is retried automatically, then the file offers Retry; each file in the list shows its progress.
  • A share's files are encrypted with a random key of its own; with a share password, that key is wrapped with a key derived from the password with Argon2id instead of PBKDF2. Shares created before keep working as they are.
  • PHP's upload limits no longer cap a share's file size: "Max file size" in Admin settings can be set beyond them, and PHP_UPLOAD_MAX_FILESIZE / PHP_POST_MAX_SIZE default to 64M (they only apply to the logo upload). LIVEWIRE_MAX_UPLOAD_TIME is no longer needed.
  • Files still uploading count towards the storage quota. An upload no chunk reached for 4 hours is deleted by the hourly cleanup.
  • The interface moves to Livewire Material 2.0.0, which aligns every component with Material 3 Expressive as Google documents it. SealShare keeps the pages, the arrangement and the flow it had — rebuilt on the new components — and no longer ships Tailwind CSS.
  • The colour scheme is regenerated with Material 3's 2025 colour rules, at all three contrast levels. The colour profile an admin chose, and each visitor's light or dark setting, carry over unchanged.
  • The settings pages — Profile, Update password, Two Factor Authentication and Appearance — are shown as cards, the way Admin settings already were. Deleting the account and the two-factor recovery codes each sit in a card of their own beside the page's.
  • A form field now fills the card that holds it instead of stopping short of its edge.

Fixed

  • "Download all" works for large shares: the ZIP is streamed as it is built, file by file, instead of every file being decrypted into memory and the archive written unencrypted to a temporary file.
  • Unencrypted copies of uploads no longer stay behind in Livewire's temporary folder after a share is created; the hourly cleanup also removes those left by earlier versions.
  • The Docker image's docker/Caddyfile was never used and is removed; SERVER_NAME now only matters with AUTO_HTTPS.

Security

  • An encrypted file whose trailing chunks were cut off, or whose chunks were reordered, now fails to decrypt: each chunk's nonce carries its index and whether it is the last one.

2.0.1 - 2026-09-13

Fixed

  • The 2.0.0 Docker image did not start: the entrypoint's php artisan view:cache failed with "Unable to locate a class or view for component [showcase::example]", because Livewire Material only registered its showcase components while the showcase was enabled, which it is not in production. Livewire Material 1.1.1 registers them always, and a test now caches every view as the entrypoint does.

2.0.0 - 2026-09-13

Added

  • Eight colour profiles — Indigo (the default), Blue, Teal, Green, Amber, Rose, Violet and Graphite. An admin picks one in Admin settings, previews it on the page, and after saving every page, mail and error page uses it; light and dark stay each visitor's own choice.
  • The share created page offers the link as a QR code: "Show QR code" opens it in a dialog (full screen on a phone) and "Download" saves it as a PNG. For a password-protected share the dialog reminds that recipients also need the password; the code holds only the link.
  • A "Share…" button on the same page opens the device's share sheet with the link, where the browser has one (mostly phones and Safari).

Changed

  • SealShare moved from GitHub to Gitea: the source is at https://gitea.nonameweb.ch/noNameWEB/SealShare, and the Docker image is published as gitea.nonameweb.ch/nonameweb/sealshare. Images at ghcr.io/surtic86/sealshare are no longer updated — change image: in your docker-compose.yml to the new name to keep receiving releases.
  • The interface is rebuilt on Livewire Material, a Material 3 Expressive component library, replacing Mary UI and DaisyUI. Every page — upload, share created, download, sign-in, settings, admin and the setup wizard — uses its components, in a colour scheme generated from SealShare's indigo.
  • The theme follows the system's light or dark setting until a user picks Light, Dark or System in Settings → Appearance, or from the account menu. A theme chosen in 1.x is kept.
  • A floating toolbar centred at the bottom of every page replaces the sidebar and header layouts. Signed-in users reach Upload and the admin pages from it, and Settings, the theme and Log out from its account menu. The site's name and logo head the upload and download pages.
  • Confirmations for deleting a share, removing the logo, clearing the system password and deleting the account are dialogs instead of browser prompts, and "Saved." messages are snackbars.
  • The two-factor setup opens full screen on a phone.
  • HTTP error pages and Markdown mail (password reset, email verification) use the same Material design and colours. Set MAIL_MARKDOWN_THEME=default to get Laravel's mail theme back.
  • Signing in now lands on the admin dashboard. The starter kit's placeholder /dashboard page is gone.
  • The Docker build installs Composer packages before building the frontend, because the stylesheet imports Livewire Material from vendor/.
  • Removed the dependencies robsontenorio/mary, daisyui and alpinejs (Livewire bundles Alpine). The Bunny Fonts request is gone.

Fixed

  • The README called SealShare's encryption end-to-end. Files are encrypted at rest on the server; the README now says so, and that a share password's key is never stored.
  • The admin dashboard passed its sort column and direction straight to the query, so a crafted Livewire request could order shares by any column or cause a server error. It now sorts only by the columns it shows and otherwise falls back to newest first.

[1.2.0] - 2026-09-10

Added

  • LIVEWIRE_MAX_UPLOAD_TIME (minutes, default 30) sets how long a single upload may take before its signed upload URL expires. Raise it when large files arrive over slow connections.
  • A "Large files" section in the README listing every limit that has to be raised together for big uploads: PHP, admin settings, upload time, execution time and the reverse proxy.

Changed

  • Upgraded to Pest 5.1 (pestphp/pest ^5.1, pestphp/pest-plugin-laravel ^5.0), which brings PHPUnit 13.3. The test suite needed no changes.
  • Updated PHP dependencies within their existing constraints: Laravel 13.31, Livewire 4.4.4, Octane 2.19.1, Fortify 1.39, Mary 2.9.10, Boost 2.8 and Pint 1.32. Guzzle moves to 8.2 as a transitive dependency of Laravel.
  • Updated frontend dependencies: Alpine.js 3.17.2, DaisyUI 5.7.32, Vite 8.2.2 and laravel-vite-plugin 3.2.

Fixed

  • Files larger than 4 GB were always rejected with "Upload failed: file exceeds the maximum size of N MB", even when the admin's maximum file size N was bigger than the file. Livewire's temporary upload rule had a hard-coded 4 GB cap; it is gone, so PHP_UPLOAD_MAX_FILESIZE is the hard limit and the admin setting is the enforced one.
  • An upload the server rejects no longer blames the admin's maximum file size. The user is told the server could not accept the file, and the actual reason is logged as a warning.

[1.1.0] - 2026-07-23

Changed

  • Upgraded to Laravel 13 (laravel/framework ^13.0, laravel/tinker ^3.0). PHP 8.5 is now the minimum.
  • Set serializable_classes to false in config/cache.php, so a leaked APP_KEY cannot drive an object gadget chain through the cache.
  • Upgraded the frontend toolchain to match the Laravel 13 skeleton: Vite 8, laravel-vite-plugin 3, Tailwind CSS 4.3.3, DaisyUI 5.7, Alpine.js 3.15.12 and concurrently 10.

Fixed

  • Adding a second batch of files to a share left the uploader stuck on "Processing files…" forever, with the drop zone and the "Create Share Link" button permanently disabled. The uploading state is now cleared by a files-processed event dispatched on every batch, instead of a one-off x-init that only ran the first time the file list appeared.
  • Dropping files on the drop zone showed no upload progress at all, because uploadMultiple() was called without progress callbacks.
  • Dropping a second folder onto an existing selection replaced the collected relative paths instead of appending them, which shifted every earlier file's path onto the wrong file.

Removed

  • Dropped the unused axios dependency and the stale @rollup/rollup-linux-x64-gnu optional pin (Vite 8 builds with rolldown).

Security

  • Forced shell-quote to a patched release via an npm override, clearing GHSA-395f-4hp3-45gv (quadratic complexity DoS). npm audit reports 0 vulnerabilities, down from 3.

[1.0.1] - 2026-02-25

Fixed

  • ZIP downloads returned 0-byte archives under FrankenPHP. ZipStream writes through fwrite(php://output), which FrankenPHP silently drops; downloads are now built with native ZipArchive and served as a file response.
  • Docker image was missing the PHP zip extension required by ZipArchive.
  • Stale bootstrap/cache/*.php from the build context could load dev-only service providers in the production image.
  • DB_DATABASE now defaults to /app/database/database.sqlite in docker-compose.yml, so the env() fallback resolves correctly.
  • The unlock button on the password-protected share page rendered outside the form and did nothing. Share page action buttons are now consistently full width.

Removed

  • maennchen/zipstream-php dependency.

[1.0.0] - 2026-02-13

Added

  • Initial release.
  • File uploading via drag & drop or browse, supporting multiple files and folders with real-time progress.
  • Shareable links, one unique link per upload.
  • AES-256-GCM encryption at rest, chunked and streaming, with PBKDF2-SHA256 key derivation.
  • Optional password protection per share.
  • Configurable expiration from 1 hour to 30 days, and per-share download limits.
  • ZIP download of all files in a share.
  • Hourly auto-cleanup of expired shares and their files.
  • Admin dashboard and settings for upload limits, storage quotas and branding.
  • Site branding: custom logo, title and description.
  • Optional system password gate restricting upload access.
  • User authentication (login, registration, password reset, email verification) and TOTP two-factor authentication via Laravel Fortify.
  • First-run setup wizard for creating the initial admin account.
  • Dark themed UI built with Livewire, Alpine.js, Tailwind CSS and DaisyUI.
  • Docker images published to ghcr.io/surtic86/sealshare, served by FrankenPHP via Laravel Octane.