initial commit — dropLake

This commit is contained in:
bot
2026-04-02 16:05:40 +03:00
commit f80aadc3ab
17 changed files with 2206 additions and 0 deletions
+287
View File
@@ -0,0 +1,287 @@
# 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