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>
This commit is contained in:
co-authored by
Claude Opus 5
parent
504971ad7f
commit
40e35bab0e
@@ -15,14 +15,31 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Kind: random characters (length 12–64; uppercase, lowercase, numbers and symbols; look-alike characters left out if chosen) or a passphrase (4–10 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](https://gitea.nonameweb.ch/noNameWEB/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
|
||||
|
||||
Reference in New Issue
Block a user