Skip to content

Latest commit

 

History

History
146 lines (102 loc) · 11 KB

File metadata and controls

146 lines (102 loc) · 11 KB

Ghostdrop SPA — design and requirements

Single-page application for the E2E encrypted file-drop, built with React and Vite. This document describes the SPA’s behavior, data format, and requirements.

Modes: Run via make local (dev server, no backend — ciphertext in browser storage) or use a deployed build from make publish (SPA talks to API Gateway + S3 via presigned URLs). Constants (max file size, loading delay, expiry) come from the Makefile and are passed as VITE_* env vars when running or building; see README.


1. Purpose and scope

  • Role: The only client-side component. User selects a file → it is encrypted in the browser; the user receives a download link. User opens a link with a fragment → ciphertext is fetched (from browser storage when local, or via presigned URL when deployed), decrypted in the browser, and saved. Keys and nonces never leave the client (they live in the URL fragment).
  • Local: With no backend (make local), upload saves ciphertext to browser storage; download reads from that store. Use small test files.
  • Deployed: With VITE_API_BASE set (make publish), the SPA calls the upload/download signer APIs and uses presigned POST/GET to S3.
  • In scope: Encryption/decryption, link format, upload UX, download UX, error handling, browser support, and the contract with the “get upload URL” and “get download URL” APIs.
  • Out of scope for this doc: Terraform, Lambda, API Gateway, S3 policies, CloudFront. Security headers are applied when serving the SPA (CloudFront), not inside the SPA.

2. Tech stack: React and Vite

  • React: UI and view logic. No router library; routing is fragment-based. No fragment → upload view; valid fragment → download view; invalid fragment → error view. React state for loading, error, and success.
  • Vite: Build tool and dev server. Produces static assets for deployment to S3/CloudFront. The Makefile sets VITE_MAX_FILE_BYTES, VITE_MIN_LOADING_DELAY_MS, VITE_EXPIRY_HOURS (and when deploying, VITE_API_BASE) so the app does not hardcode constants.
  • Structure: Crypto and fragment parsing live in plain TS modules (crypto.ts, fragment.ts) with no React dependency. React components handle UI only (UploadView, DownloadView). A storage adapter (storage.ts) implements save/get ciphertext; it uses browser storage when VITE_API_BASE is unset and the API + S3 when set.
  • Run: From repo root, make local runs the dev server with the right env. For production, make publish builds the SPA with VITE_API_BASE and deploys dist/ to S3 and invalidates CloudFront.
  • Browser targets: Firefox, Edge, and other modern browsers with Web Crypto and enough memory for the max file size. No polyfills for crypto.

3. Browser and environment

  • Supported: Firefox and Edge, plus other modern browsers with Web Crypto and sufficient memory for the chosen max file size.
  • APIs: Web Crypto (subtle) for AES-GCM. File handling via file input and Blob/ArrayBuffer; no streaming, single chunk in memory.
  • Max file size: 100 MB (set in Makefile; passed as VITE_MAX_FILE_BYTES). The SPA enforces this before reading the file and shows it in the UI.

4. Cryptography

  • Algorithm: AES-GCM, 256-bit key.
  • Key: 256 bits (32 bytes), random per upload via crypto.getRandomValues(). Not derived.
  • Nonce: 96 bits (12 bytes), unique per file via crypto.getRandomValues(). Required for AES-GCM; must never be reused with the same key.
  • Where key and nonce live: Only in the URL fragment of the download link. Never sent to the server; never logged.
  • Ciphertext and tag: AES-GCM produces ciphertext plus a 128-bit authentication tag. Web Crypto returns them concatenated (tag last). The SPA stores and retrieves this blob as a single unit (browser storage or S3). The tag provides integrity and authenticity; no separate HMAC.

5. URL and fragment format

  • Base: Build the download link with window.location.origin + window.location.pathname so it works in dev and production.
  • Fragment contents (decoded by the SPA):
    • Object ID: UUID v4 for the blob in storage (e.g. S3 key uploads/<uuid>). Used when requesting a presigned download URL.
    • Key: 256-bit key, base64url-encoded.
    • Nonce: 96-bit nonce, base64url-encoded.
    • Name (required): Original filename for the suggested download name. Visible and tamperable by anyone with the link; no integrity check.
  • Encoding: #id=<uuid>&key=<base64url>&nonce=<base64url>&name=<base64url>. Object ID is the canonical UUID string (lowercase with hyphens). Key, nonce, and name are base64url (RFC 4648, no padding). Parse as key-value pairs; order does not matter. Required: id, key, nonce, name. Empty fragment, missing params, or invalid encoding → show a single user message (“Invalid or missing link”); do not expose parsing details.
  • Security: Fragment is not sent to the server. Do not put key/nonce in path or query.

6. Upload flow (SPA)

  1. User opens the app with no fragment → upload view.
  2. User selects a file. SPA checks size (≤ max); if over, show a clear message and stop.
  3. Generate: objectId (UUID v4), key (32 bytes), nonce (12 bytes).
  4. Read file into an ArrayBuffer (single chunk). Encrypt with AES-GCM. Result: ciphertext blob.
  5. Local: Save ciphertext to browser storage (keyed by object ID). Deployed: POST to API for presigned POST (size + objectId); then POST ciphertext as multipart form to the returned URL (S3 enforces size via POST policy).
  6. Build download link: base URL + # + encoded fragment (object ID, key, nonce, filename). Show link and copy button. Do not send fragment to any server or analytics.

7. Download flow (SPA)

  1. User opens a link with a fragment, or changes the hash. The SPA reacts to hashchange and initial load so a valid fragment shows the download flow.
  2. Read and parse window.location.hash. If missing or invalid, show error view with a single message (“Invalid or missing link”).
  3. Local: Read ciphertext from browser storage by object ID. If not found, show “Download failed or link expired”. Deployed: Call “get download URL” API with object ID (and size); GET the returned URL for the ciphertext.
  4. Decrypt with key and nonce. On failure, show a generic error; do not leak details.
  5. Trigger file save (Blob + object URL + programmatic download). Use name from the fragment as the suggested filename.

8. UX requirements

  • Limits visible: Show “Max upload 100 MB” and “File expires in 24 hours” (or current values from constants) in the UI.
  • Errors: Clear, safe messages for: file too large; missing or invalid fragment; upload failure; download failure (network, 404, expired, decryption). No stack traces or internal details.
  • Loading: Spinners with labels (“Encrypting…”, “Saving…”, “Decrypting…”, “Downloading…”). Minimum display time 500 ms per state so each phase is visible.
  • Copy link: After a successful upload, one-click copy of the full download link (including fragment).

9. API contract (backend integration)

The SPA calls two endpoints; the Lambdas implement them.

  • Upload (presigned POST)

    • Input: { "size": number, "objectId": "uuid" }. Size in bytes; objectId must be UUID v4.
    • Output: { "postUrl": "https://...", "formFields": { ... }, "expiresIn": number }. SPA POSTs to postUrl with multipart/form-data: all formFields plus ciphertext as a file (e.g. field "file"). S3 POST policy enforces content-length-range; backend validates size and objectId before issuing the URL.
    • Errors: SPA shows generic “Upload failed”; never log fragment or key.
  • Download URL

    • Input: { "objectId": "uuid", "size": number } (size used for validation).
    • Output: { "downloadUrl": "https://...", "expiresIn": number }. SPA GETs downloadUrl for the ciphertext.
    • Errors: 404 or 4xx/5xx. SPA shows “Download failed or link expired”; no internals.

The storage adapter (storage.ts) implements save/get ciphertext: when VITE_API_BASE is unset it uses browser storage; when set it calls these APIs and presigned URLs. Only the adapter switches between local and deployed.


10. Technical constraints

  • No streaming: Whole file in memory then encrypt; whole ciphertext fetched then decrypt. Suitable for the 100 MB cap.
  • Single-chunk upload: One POST with full ciphertext (presigned POST); no multipart streaming in the SPA.
  • No key, nonce, or filename on server: They exist only in the fragment; never in query, body, or headers.
  • Fragment never logged: No console.log or error reporting of location.hash or parsed key/nonce.

11. Local development and testing

  • Local: From repo root, make local runs the Vite dev server with constants from the Makefile. No AWS required. The app uses browser storage; use small test files.
  • Deployed: make publish builds with VITE_API_BASE and deploys; open the CloudFront URL to test the full flow with S3 and the APIs.
  • Validation: Test in Firefox and Edge (and Chrome); small files for routine checks; optionally a larger file for memory and Web Crypto.

12. Decisions

  • Fragment: Empty or partial → invalid; error view. Required: id, key, nonce, name. Base64url (RFC 4648, no padding). Tampering with name acceptable.
  • Routing: No fragment → upload. Valid fragment → download. Invalid fragment → error. React to hashchange and initial load.
  • Missing ciphertext (local): Valid fragment but no ciphertext in storage (e.g. other device, storage cleared) → same message as “Download failed or link expired”.
  • Filename: Required in fragment as name (base64url). Use as suggested download filename; tampering acceptable.
  • Loading delay: Minimum 500 ms per loading state (from VITE_MIN_LOADING_DELAY_MS).
  • Constants: Max file size, loading delay, and expiry are defined in the Makefile and passed as VITE_* when running or building. Terraform receives the same values via TF_VAR_* for Lambdas so UI and backend stay in sync.
  • Browser storage: localStorage is used for the local adapter; sufficient for small test files.

13. Summary (implemented)

  • React + Vite; one-page app; view driven by fragment (no fragment → upload, valid → download, invalid → error).
  • Crypto and fragment parsing in plain TS modules; React components + storage adapter (browser storage or API + S3).
  • AES-GCM 256; random key and nonce per file; key and nonce in fragment only.
  • Fragment format: #id=<uuid>&key=<base64url>&nonce=<base64url>&name=<base64url>; all four required; name = suggested download filename.
  • Max file size and expiry from Makefile (VITE_*); shown in UI.
  • User-facing errors for all failure cases; no sensitive details; loading states with minimum delay.
  • Works in Firefox and Edge; no streaming; single-chunk read/encrypt and fetch/decrypt.
  • Run via make local (no backend) or use deployed build from make publish.