Skip to content
skidoodlePublic

Latest commit

Β 

History

31 Commits

Folders and files

Repository files navigation

safebin

Go Version License Docker Image

safebin is a minimalist, self-hosted file storage service designed for efficiency and privacy. It utilizes Convergent Encryption to provide secure storage at rest while automatically deduplicating identical files to save disk space.

πŸ“– Architecture & Security Model

Safebin is designed to be Host-Proof at Rest. While it is not a client-side E2EE solution, it ensures that the server cannot access stored data without the specific link generated at upload time.

How it Works

  1. Upload: The server receives the file stream and calculates a SHA-256 hash of the content.
  2. Key Generation: This hash becomes the encryption key (Convergent Encryption).
  3. Encryption: The file is encrypted using AES-128-GCM and written to disk.
  4. Deduplication: Because the key is derived from the content, identical files generate the same ID. The server detects this and stores only one physical copy, regardless of how many times it is uploaded.
  5. Zero-Knowledge Storage: The server saves the file metadata (ID, size, expiry) but discards the encryption key.
  6. Link Generation: The key is encoded into the URL fragment returned to the user.

Security Note: If the server's database or physical storage is seized, the files are mathematically inaccessible. However, because encryption occurs on the server, the process does have access to the plaintext in memory during the brief window of upload and download.

Known Trade-offs of Convergent Encryption (read this)

The design above has inherent properties you should understand before deploying:

  • URLs are derivable from content. The link is the hash of the plaintext. Anyone who knows a file's exact content can compute its full URL and download it β€” the key offers no protection against someone who already knows the data.
  • Equality leaks. The server (or anyone with database access) can see that two uploads were identical.
  • Confirmation attacks. An operator can verify a guess about a file's contents by encrypting a candidate and comparing IDs.
  • Plaintext window. During upload and download the plaintext exists in server memory; chunked uploads also buffer plaintext chunks on local disk until finalization. This changes nothing about who can read the data (the server could always read it), but it means "encrypted at rest" applies only to long-lived storage, never to bytes currently in flight.

If these trade-offs are unacceptable for your threat model, use a client-side-encrypting solution instead.

✨ Features

  • Convergent Encryption & Deduplication: Files are addressed by their content. Uploading the same file twice results in a single storage entry, significantly reducing disk usage.
  • Optional End-to-End Encryption: One checkbox in the web UI encrypts your file in the browser before it ever leaves your machine. The key lives only in your link's #fragment (never sent to the server), filenames are encrypted too, and dedup is bypassed for these uploads.
  • Works Without JavaScript: The upload form is a plain HTML form β€” disable or block JS entirely and uploads still work via a single POST. (Client-side E2EE inherently requires JS; no-JS users can pre-encrypt locally, see below.)
  • Tamper-Proof Storage: Uses Galois/Counter Mode (GCM) to ensure data integrity. Modified files will fail decryption.
  • Volatile Keys: Decryption keys reside only in the generated URLs, not in the database.
  • Smart Retention: A cubic scaling algorithm prioritizes keeping small files (snippets, logs) for a long time, while large binaries expire quickly.
  • Chunked Uploads: Robust handling of large files via the web interface using 8MB chunks against server-issued upload sessions.

πŸš€ Deployment

Docker Compose (Recommended)

services:
  safebin:
    image: ghcr.io/skidoodle/safebin:latest
    container_name: safebin
    restart: unless-stopped
    ports:
      - "8080:8080"
    environment:
      - SAFEBIN_MAX_MB=512
    volumes:
      - safebin_data:/app/storage

volumes:
  safebin_data:

Manual Installation

Requires Go 1.25 or higher.

# Build the binary
go build -o safebin .

# Run the server
./safebin -p 8080 -s ./data -m 1024

βš™οΈ Configuration

Configuration is handled via environment variables or command-line flags. Flags take precedence over environment variables.

Flag Environment Variable Description Default
-h SAFEBIN_HOST Interface/Bind address. 0.0.0.0
-p SAFEBIN_PORT Port to listen on. 8080
-s SAFEBIN_STORAGE Directory for database and files. ./storage
-m SAFEBIN_MAX_MB Maximum allowed file size in MB. 512
-q SAFEBIN_MAX_PENDING_MB Global quota (MB) reserved for in-flight chunked uploads. 2048
-trust-proxy SAFEBIN_TRUST_PROXY Trust X-Forwarded-Proto/X-Forwarded-Host from your reverse proxy when generating links. false
β€” SAFEBIN_LOG_LEVEL Log level: debug, info, warn, error. info

Reverse proxy note: keep SAFEBIN_TRUST_PROXY disabled unless you are behind a proxy you control; otherwise clients can spoof the scheme/host used in generated links.

πŸ’» Usage

Web Interface

Navigate to http://localhost:8080. Drag and drop files to upload. The browser handles chunking automatically.

Chunked uploads use a three-step protocol against server-issued sessions: POST /upload/start reserves capacity and returns a random upload ID, chunks are streamed to POST /upload/chunk, and POST /upload/finish assembles the file. Sessions live in memory only β€” if the server restarts mid-upload, the client must start over. Unissued upload IDs and uploads exceeding the pending quota (SAFEBIN_MAX_PENDING_MB) are rejected, which prevents anonymous disk-exhaustion attacks through the chunk endpoints.

Web UI without JavaScript

The upload form is a standard HTML <form> β€” with JavaScript disabled or blocked you can still pick a file and upload in a single request; the result page renders your link server-side. Drag-and-drop, progress bars, and browser-side E2EE require JavaScript (E2EE additionally requires a secure context / HTTPS). Without JS you can still get true end-to-end encryption by encrypting before upload β€” see the CLI recipes below.

End-to-End Encryption (Web UI)

Tick β€œEnd-to-end encrypt in browser” before uploading. The file (and its filename/MIME type) is encrypted with AES-256-GCM inside your browser; only opaque ciphertext reaches the server, which stores it as an unidentifiable blob marked encrypted. Your link gains a #fragment carrying the key:

https://bin.example.com/o9917RC07lM8uFDb0NKWMA#a1Bc...key

Everything after # is never transmitted to the server. Opening such a link in a browser shows a decrypt page that fetches the ciphertext and decrypts it locally. Note that E2EE uploads skip deduplication (random keys β‡’ unique ciphertext) and are stored as application/octet-stream attachments for non-browser clients.

CLI (curl)

Safebin is optimized for terminal usage. You can upload files directly via curl:

# Upload a file
curl -F 'file=@screenshot.png' https://bin.example.com

# Response
https://bin.example.com/0iEZGtW-ikVdu...png

E2EE from the command line

Encrypt locally with any tool you trust, then upload the ciphertext β€” the server never sees plaintext or keys:

# Using age (recommended):
age -p -o screenshot.png.age screenshot.png      # passphrase-encrypt locally
curl -F 'file=@screenshot.png.age' https://bin.example.com

# Recipient decrypts:
curl -s 'https://bin.example.com/0iEZGt...' | age -d -o screenshot.png

# Using gpg:
gpg -c screenshot.png && curl -F 'file=@screenshot.png.gpg' https://bin.example.com
curl -s 'https://bin.example.com/0iEZGt...' | gpg -d -o screenshot.png

⏳ Retention Policy

To keep storage manageable, Safebin runs a cleanup task every hour. File lifetime is determined by size using a cubic curve:

  • Small Files (< 1MB): Retained for 365 days.
  • Medium Files (~50% Max Size): Retained for ~30 days.
  • Large Files (Max Size): Retained for 24 hours.
  • Incomplete Uploads: Purged after 4 hours.

πŸ“„ License

This project is licensed under the GNU General Public License v2.0.