optional password

This commit is contained in:
Anaïs Saraiva
2026-08-15 10:42:20 +02:00
parent 12d9e99aa2
commit 6e76189e46
7 changed files with 50 additions and 38 deletions
+19 -19
View File
@@ -10,7 +10,7 @@ database, no object storage, and no queue. Expired files are swept automatically
## Features
- Drag & drop / file-picker uploads with progress, straight from the browser.
- **Required per-file password**: every download is protected by a password.
- **Optional per-file password**: set a password to gate downloads behind a prompt.
- Storage on the local filesystem; files survive restarts.
- A cryptographically secure, unguessable download key for every file.
- Streaming uploads and downloads — no full buffering in RAM, large files work.
@@ -71,9 +71,8 @@ Then open http://localhost:3000.
2. Drop `example.zip` (or click to pick a file).
3. The server stores it and returns a URL like
`https://files.example.com/8fJ...random-secret...`.
4. Copy the URL. Anyone possessing it can download the file — **after entering
the password you set at upload**. Nobody without the secret URL and the
password can download it.
4. Copy the URL. Anyone possessing it can download the file. If you set a
password, they must enter it before the download starts.
5. The file is removed automatically after 48 hours, or earlier by explicit
deletion.
@@ -119,9 +118,9 @@ touched.
### `POST /api/upload`
Multipart/form-data with a `file` field and a **required** `password` field
(this is what the web UI uses). Uploads without a password are rejected with
`400`.
Multipart/form-data with a `file` field and an **optional** `password` field
(this is what the web UI uses). When no password is sent, the file is
downloadable by anyone with the URL.
```bash
curl -F "password=hunter2" -F "[email protected]" http://localhost:3000/api/upload
@@ -139,23 +138,23 @@ curl -F "password=hunter2" -F "[email protected]" http://localhost:3000/api/uploa
```
`id` and `key` are the same 12-character base64url token (~72 bits of entropy);
the short length keeps share URLs tidy, and the required password adds a second
factor of protection.
the short length keeps share URLs tidy, and an optional password can add a
second factor of protection.
### `GET /:key` (also `HEAD`)
The download URL is just the key at the site root (`https://host/xmK5lLMRErlN`).
The legacy `/d/:key` form still works as an alias. Every file is
password-protected: without a valid auth cookie the server shows a password
prompt; a wrong or non-existent key returns a generic `404`.
The legacy `/d/:key` form still works as an alias. A file without a password
streams immediately; if a password is set, the server shows a password prompt
before streaming. A wrong or non-existent key returns a generic `404`.
```bash
curl -O http://localhost:3000/xmK5lLMRErlN # prompts for password first
curl -O http://localhost:3000/xmK5lLMRErlN # streams, or prompts if password set
```
### Password protection
Every file requires its password:
Files uploaded with a password are gated like this:
1. `GET /:key` returns a small HTML password form.
2. The browser submits it to `POST /api/auth/:key` with `password=...`.
@@ -168,8 +167,9 @@ curl -b cookies.txt -O http://localhost:3000/:key
```
The cookie is signed with a server secret (`storage/.secret`) so it cannot be
forged, and it survives server restarts. Without it, a file is never
downloadable — not even by the uploader.
forged, and it survives server restarts. Without it, a password-protected file
is never downloadable — not even by the uploader. Files uploaded without a
password stream immediately and have no auth gate.
### `DELETE /api/files/:id`
@@ -196,12 +196,12 @@ paths or stack traces.
- **Secrets.** Each file gets a fresh 72-bit (9 random bytes) base64url key via
`crypto.randomBytes`, used as both the share id and the download key. Keys are
encoded base64url; guessing one is infeasible at scale, and the required
password is an added second factor. No timestamps, incrementing ids,
encoded base64url; guessing one is infeasible at scale, and an optional
password can be an added second factor. No timestamps, incrementing ids,
`Math.random()`, or filenames are ever used as secrets.
- **At-rest.** Only a SHA-256 hash of the key is persisted. Comparisons use
constant-time `timingSafeEqual`.
- **Passwords.** Every file has a password stored only as an **scrypt** hash
- **Passwords.** A file's password (if any) is stored only as an **scrypt** hash
(salted), never in plaintext. Successful entry mints a short-lived HMAC-signed
cookie, so access is granted by the server and cannot be forged.
- **Path safety.** Filesystem paths are built only from server-generated,