# dropLake A self-hosted, privacy-first file and text sharing web app. No accounts. No ads. No tracking. Password-protected shares. Auto-expiry. Built for personal use — deploy it on your own VPS, share with friends, keep full control over your data. --- ## What it does - Upload a **file** (any type, up to 6 GB) or paste **text** - Every share is **AES-256-GCM encrypted at rest** on the server - Optional **custom password** (Argon2id hashed, never stored plaintext) - Every share has a **mandatory TTL** (1 h → 3 days, default 24 h) - Optional **burn after read** — share deletes itself after the first download - Short, clean share URLs: `https://yourdomain.com/s/SE6RLPy53b` - Brute-force protection: 10 failed password attempts → 15-minute lockout - Security event log (no raw IPs stored — SHA-256 hashed) - Background job purges expired and burnt shares every 30 minutes --- ## Security model ### Encryption at rest — AES-256-GCM (server-side) Every upload is encrypted on the server before touching disk. The server generates a random 256-bit key per share, encrypts the content in 64 MB chunks (so even 6 GB files stream without loading into memory), and stores only the encrypted blob. The key is stored in the database alongside the share record. **Chunk format on disk:** ``` [4B — uint32 chunk count] Per chunk: [4B — uint32 payload length] [12B — AES-GCM nonce (unique per chunk)] [NB — ciphertext + 16B GCM authentication tag] ``` This means: - Disk theft / backup leak → files are unreadable without the database - Each chunk is independently authenticated → tampering is detectable - Streaming decryption on download → no memory ceiling for large files **Trade-off acknowledged:** the server holds the AES key. If an attacker fully owns both the filesystem and the database, they can decrypt. For a self-hosted personal tool where you control the server, this is the right trade-off — the realistic threats (physical disk theft, leaked backup) are covered. This is the same model used by most serious file-sharing services (WeTransfer, Filemail, etc.). ### Password protection — Argon2id Optional. If set: - Server stores only the Argon2id hash (time=2, mem=64 MB, par=2) - Encrypted blob is not served until the password is verified - 10 wrong attempts → share locked for 15 minutes - Password shown once to the uploader after the upload completes ### Share IDs 10-character base62 (`a-zA-Z0-9`), ~59 bits of entropy. Brute-force enumeration is blocked by per-IP rate limiting. No sequential or guessable patterns. ### Security event log All security-relevant events are written to the `security_events` table. No raw IP addresses are stored — only their SHA-256 hash, which allows pattern detection (repeated attacks from the same source) without logging personally identifiable information. | Event | When | |-------|------| | `share_created` | New upload completed | | `failed_attempt` | Wrong password submitted | | `share_locked` | Share locked after too many failures | | `share_accessed` | Successful download | | `share_cleaned` | Share purged by cleanup scheduler | ### OWASP Top 10 (2025) coverage | Risk | How it's addressed | |------|--------------------| | A01 Broken Access Control | Random 10-char IDs; password gate; no enumerable endpoints | | A02 Security Misconfiguration | No debug endpoints; Swagger/OpenAPI disabled in production | | A03 Supply Chain Failures | Pinned dependencies in `requirements.txt`; minimal footprint | | A04 Cryptographic Failures | AES-256-GCM per share; Argon2id for passwords | | A05 Injection | SQLAlchemy parameterized queries; filename sanitization | | A06 Insecure Design | Mandatory TTL; burn-after-read; short random IDs | | A07 Auth Failures | Per-share attempt lockout; IP-based rate limiting (slowapi) | | A08 Data Integrity | Blobs stored outside web root; never executed by the server | | A09 Logging & Monitoring | Security event table; structured application logging | | A10 Exceptional Conditions | Generic error responses; stack traces never sent to client | --- ## Stack | Layer | Technology | |-------|-----------| | Backend | Python 3.11+ / FastAPI | | Database | SQLite via SQLAlchemy | | Encryption | AES-256-GCM — `cryptography` library (server-side) | | Password hashing | Argon2id — `argon2-cffi` | | Rate limiting | `slowapi` (per-IP) | | Background cleanup | `APScheduler` | | Frontend | Plain HTML / CSS / JS — no framework, no build step | | Web server (prod) | Nginx + Let's Encrypt (Porkbun DNS) | --- ## Project structure ``` droplake/ ├── README.md ├── .gitignore ├── backend/ │ ├── main.py # FastAPI app, lifespan, static file serving │ ├── config.py # Settings — all configurable via .env │ ├── database.py # SQLAlchemy models (Share, SecurityEvent) + session │ ├── routes/ │ │ ├── upload.py # POST /api/upload │ │ │ # — streams upload, encrypts in 64 MB chunks, │ │ │ # stores blob + AES key, returns share ID │ │ └── download.py # GET /api/share/{id}/meta │ │ # POST /api/share/{id}/download │ │ # — verifies password, streams decrypted content │ ├── utils/ │ │ └── cleanup.py # APScheduler job: purges expired + burnt shares │ ├── storage/ # Encrypted blobs — gitignored, outside web root │ ├── requirements.txt │ └── .env.example └── frontend/ ├── index.html # Upload page (File tab + Text tab) ├── s.html # Download / view page └── static/ ├── css/ │ └── style.css # Dark glassmorphism theme, Inter font, spring transitions └── js/ ├── upload.js # XHR upload with live progress bar, view crossfade └── download.js# Fetch share, password prompt, trigger download / show text ``` --- ## Local development ```bash cd backend python -m venv .venv source .venv/bin/activate pip install -r requirements.txt cp .env.example .env uvicorn main:app --reload --port 8000 ``` Open: http://localhost:8000 The SQLite database and `storage/` directory are created automatically on first run. Both are gitignored. To reset during development: ```bash rm -f backend/droplake.db rm -rf backend/storage/ ``` --- ## Configuration — `.env` Copy `.env.example` to `.env` and adjust as needed. | Variable | Default | Notes | |----------|---------|-------| | `STORAGE_PATH` | `./storage` | Where encrypted blobs are stored. Use an absolute path on the VPS. | | `DATABASE_URL` | `sqlite:///./droplake.db` | SQLite file path. | | `MAX_UPLOAD_BYTES` | `6442450944` | 6 GB hard limit. Frontend shows a 1 GB recommendation. | | `MAX_FAILED_ATTEMPTS` | `10` | Failed password attempts before a share locks. | | `LOCKOUT_MINUTES` | `15` | How long a locked share stays locked. | | `CLEANUP_INTERVAL_MINUTES` | `30` | How often the background job runs to purge expired shares. | --- ## API endpoints | Method | Path | Description | |--------|------|-------------| | `GET` | `/` | Upload page | | `GET` | `/s/{share_id}` | Download page | | `POST` | `/api/upload` | Upload a file or text | | `GET` | `/api/share/{id}/meta` | Share metadata (type, size, expiry, has password) | | `POST` | `/api/share/{id}/download` | Verify password, stream decrypted content | | `GET` | `/health` | Health check — returns `{"status":"ok"}` | Swagger UI and OpenAPI schema are disabled (`docs_url=None`, `openapi_url=None`). --- ## Database schema ### `shares` | Column | Type | Notes | |--------|------|-------| | `id` | TEXT (10) | Base62 share ID | | `share_type` | TEXT | `'file'` or `'text'` | | `original_filename` | TEXT | Original name, sanitized. NULL for text shares. | | `mimetype` | TEXT | MIME type for Content-Type on download | | `filesize` | INTEGER | Plaintext byte count (shown to downloader) | | `storage_path` | TEXT | Random hex filename under `STORAGE_PATH` | | `encryption_key` | TEXT | AES-256 key, hex-encoded (64 chars) | | `password_hash` | TEXT | Argon2id hash. NULL = no password. | | `created_at` | DATETIME | UTC | | `expires_at` | DATETIME | UTC — enforced on every request | | `burn_after_read` | BOOLEAN | Delete after first successful download | | `downloaded` | BOOLEAN | Set to True on first successful download | | `failed_attempts` | INTEGER | Resets to 0 on correct password | | `locked_until` | DATETIME | NULL when not locked | ### `security_events` | Column | Type | Notes | |--------|------|-------| | `id` | INTEGER | Auto-increment PK | | `timestamp` | DATETIME | UTC | | `event_type` | TEXT | See event table above | | `share_id` | TEXT | References the share (not a FK, survives share deletion) | | `ip_hash` | TEXT | SHA-256 of client IP. NULL for non-network events. | | `details` | TEXT | Human-readable context (e.g. `"attempt #3"`) | --- ## VPS deployment — overview (to be expanded) Target: Ubuntu VPS, Nginx reverse proxy, Let's Encrypt via Porkbun. ``` [client] ──HTTPS──► [Nginx] ──HTTP──► [uvicorn :8000] │ TLS termination file size limits rate limiting headers ``` Steps (detailed guide coming): 1. Clone repo to `/opt/droplake` 2. Set up virtualenv, install deps, configure `.env` 3. Create `droplake.service` systemd unit 4. Configure Nginx reverse proxy with: - `client_max_body_size 6g` - Proxy timeouts sized for large uploads - Security headers (CSP, X-Frame-Options, HSTS) 5. Obtain TLS cert via Certbot (Porkbun DNS challenge or HTTP-01) 6. Point domain, test, done --- ## Roadmap ### Done - [x] FastAPI backend with SQLite - [x] AES-256-GCM server-side encryption, 64 MB chunked streaming - [x] Argon2id password hashing - [x] Per-share brute-force lockout + security event log - [x] Background cleanup scheduler (expired + burn-after-read) - [x] 10-char base62 share IDs - [x] Mandatory TTL (1 h / 6 h / 24 h / 2 d / 3 d) — default 24 h - [x] Burn after read - [x] Dark glassmorphism UI (Inter font, spring transitions, animated checkmark) - [x] Upload view → result view crossfade (form replaced by result, not stacked) - [x] Live upload progress bar (XHR) - [x] File drop zone + text tab - [x] Download page: info grid, password prompt, streaming download ### Next - [ ] Nginx config file + systemd service file (in repo) - [ ] Porkbun / Let's Encrypt setup guide - [ ] Short domain chosen and configured - [ ] Production `.env` hardening notes - [ ] Optional: rate-limit upload endpoint per IP - [ ] Optional: admin view of active shares via SSH + sqlite3 CLI