Files
droplake/README.md
T
2026-04-02 16:05:40 +03:00

11 KiB

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

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:

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

  • FastAPI backend with SQLite
  • AES-256-GCM server-side encryption, 64 MB chunked streaming
  • Argon2id password hashing
  • Per-share brute-force lockout + security event log
  • Background cleanup scheduler (expired + burn-after-read)
  • 10-char base62 share IDs
  • Mandatory TTL (1 h / 6 h / 24 h / 2 d / 3 d) — default 24 h
  • Burn after read
  • Dark glassmorphism UI (Inter font, spring transitions, animated checkmark)
  • Upload view → result view crossfade (form replaced by result, not stacked)
  • Live upload progress bar (XHR)
  • File drop zone + text tab
  • 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