3 Commits

Author SHA1 Message Date
twotalesanimation 0b56ec31ed Address remaining audit findings
Multiple files per transfer
- The UI accepted several files but only files[0] was ever uploaded, so
  recipients saw a list and received one file. Chunks now carry a
  fileIndex, the upload session tracks each file separately, and finalize
  assembles one encrypted payload and one File row per file.
- The download page lists real files with per-file download buttons; the
  previously unused isFileDownloading/isFileDownloaded helpers now drive
  that state. /api/file/[id] accepts a fileId, scoped to the transfer so
  an id from another transfer cannot be fetched.

Large downloads
- Replace res.blob() with a helper that streams the response body to disk
  via the File System Access API where available, keeping peak memory at
  roughly one chunk instead of the whole file. Falls back to the blob
  path elsewhere.

Password brute force
- Rate limit /api/verify and /api/file/[id] to 10 attempts per transfer,
  per client, per 15 minutes; a correct password clears the counter.
  Per-process state, matching the existing local-disk storage model.

Disk reclamation
- Nothing ever deleted payloads, so every transfer stayed on disk
  regardless of expiresAt. Add a cleanup routine that marks lapsed
  transfers EXPIRED, deletes payloads for expired and soft-deleted
  transfers, and sweeps abandoned chunk directories. It refuses to unlink
  anything outside UPLOAD_DIR. Exposed as POST /api/cron/cleanup behind
  CRON_SECRET, failing closed when that is unset.

Build hygiene
- Stop ignoring ESLint during builds and fix all 70 resulting violations:
  unused imports and state, unescaped JSX entities, explicit any, and a
  missing useEffect dependency. Seven catch blocks discarded their error
  silently and now log it.
- Delete dead code: two unused send components, the two legacy upload
  endpoints they called, the unused whole-file WebCrypto helpers, and a
  duplicate separator component differing only by a typo.

Add .env.example documenting configuration, including CRON_SECRET.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 09:32:37 +02:00
twotalesanimation 2d0c33a45b Honour write backpressure when reassembling uploads
createWriteStream.write() returns false once its internal buffer is full;
both reassembly loops ignored that and kept queueing chunks, so the queue
grew in memory instead of flushing to disk. Measured over a 1.5 GiB
reassembly in 10 MB chunks, this buffered 170 MB versus 10 MB when the
drain event is awaited.

Also fail loudly on a missing chunk in the v1 loop, matching v2, rather
than silently writing a corrupt file.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 09:15:32 +02:00
twotalesanimation 2e688c8e52 Add TransferTribe app and fix critical transfer security flaws
The application source was untracked, so this commit brings it under
version control together with fixes for the issues found while auditing
it. Notable fixes:

Authorization
- Require a session and scope to senderEmail on /api/transfers/[id]
  (GET, DELETE) and .../resend. These were unauthenticated over an
  autoincrement id, so the ids could be walked to soft-delete any
  transfer, read sender/recipient metadata, or make the app mail
  arbitrary recipients.
- Require a session on the legacy /api/send and /api/chunk-upload
  endpoints, and take the sender from the session rather than a request
  field so transfers cannot be posted as another user.

Encryption
- Replace the chunk encryption scheme. Every chunk was encrypted under
  one shared session IV with its auth tag discarded, which reuses the
  AES-GCM keystream (XORing two ciphertexts recovers plaintext without
  the key) and left the stored file undecryptable, surfacing to users as
  a wrong-password error. Chunks are now self-contained frames carrying
  their own random IV and auth tag, behind a magic+salt header.
- Files written by the previous format now report UNSUPPORTED_FORMAT
  instead of a misleading password error.

Download
- Verify the password against the stored bcrypt hash before serving a
  file, and enforce expiresAt and DELETED/EXPIRED status.
- Move the password from the query string into a POST body so it stays
  out of access logs and Referer headers.
- Record a download only after successful authentication.
- Decrypt frame by frame through a stream instead of buffering the whole
  file, and encode the Content-Disposition filename per RFC 5987.

Data exposure
- /api/download ran before the password prompt and returned the full
  transfer row, including absolute server file paths. It now returns
  only what the pre-password screen renders; filenames, message and
  recipient are withheld until /api/verify succeeds.

Correctness
- Fix BigInt handling that made /api/transfers and /api/transfers/[id]
  fail unconditionally (JSON.stringify cannot serialize BigInt, and
  seeding a BigInt reduce with 0 throws).
- Fail loudly on a missing chunk during reassembly rather than silently
  writing a corrupt file.
- Meter plan usage in plaintext bytes rather than on-disk encrypted size.

Ignore /uploads: it holds runtime transfer payloads, not source.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 08:48:53 +02:00