Fixes the nine findings of security audit run 1 on 2.4.1 (T1-T11).
F1: Uploads that are not complete now count only the bytes the server
received, and a chunk that goes above the storage quota gets 422. An
anonymous uploader can no longer fill the quota with declared sizes.
F2: The share password and the admin login have a second limit without
the address (20 attempts per minute per share and per e-mail), because
a forged X-Forwarded-For changes the address.
F3: The system password gate and the admin check now also run on
Livewire updates, so an old uploader snapshot cannot register files.
F4: The session keeps a fingerprint of the system password's hash.
When the admin changes the system password, earlier verifications end.
F5: The page after the upload opens only in the session that created
the share, and only while recipients can still reach the share.
F6: The share password limit counts an attempt before the password is
checked, so parallel attempts cannot pass it.
F7: The system password has the same two limits (5 per address, 20 in
total per minute).
F8: A password change or reset signs the user's other sessions out.
F9: The only admin cannot delete the account, which would open /setup
to every visitor.
The setup wizard also holds the admin password to the password
defaults, and Livewire applies EnsureAdmin to updates.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A new "Download into a folder" feature card describes the folder
download for large shares: the files saved one by one into a folder the
recipient picks, in their folders, resuming after a broken connection,
in Chrome and Edge on a computer. How it works, Files and folders
(Browse Folder), Download limits (the hour starts again with every
download) and ZIP download (progress and size) say what 2.4 changed,
and the download screenshots' captions and alt texts match the page.
Every screenshot is retaken: the upload page shows Browse Folder, the
download page the file count, the total size and the folder download.
The phone shots switch the folder download off, as real phones have no
folder picker and desktop Chromium would otherwise show it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A share can now hold a private text (a password, a key, a short note)
with its files or on its own. The upload page's new "Private text" card
takes up to 100 KB; the browser encrypts the text and sends it through
the same chunk pipeline as a file, flagged is_text on share_files, so it
gets the share's password, expiry, download limit and cleanup.
The recipient sees the text only after pressing "Show text", which
counts as their download, so a messenger link preview cannot use up a
share limited to one download. The text is left out of the file list,
the ZIP and the file counts; the admin dashboard marks shares that hold
one with "Text".
The website gains a Private text feature card and a fifth phone
screenshot; every screenshot is retaken.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The download page and the admin dashboard screenshots are retaken for
2.2: the demo delivery share now has a download limit, so the download
page shows how many downloads are left and the dashboard shows "3 of 5
downloads" beside the full-width Sort by field. The download limit and
admin dashboard features mention the remaining downloads, the hour a
recipient then has for the files, and the installed version, and the
alt texts match the new screenshots.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The desktop upload shot fills the password with Generate, and the share
created shot now comes from creating that share, so the password is
offered beside the link. The created share takes a fixed token, so its
link and QR code read the same on every run, and is deleted before the
dashboard shot, which no longer counts the unfinished upload in its disk
usage.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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>
composer screenshots runs Pest browser tests in tests/Screenshots, outside
every test suite: fixed demo data under a frozen clock, desktop (MacBook
14, 2x) and phone (iPhone 15 Pro, 3x) in light and dark, each capture
published at once as WebP at two widths into website/img/screenshots.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V9NnLxnPp8vaaurb3Z1MFy