Skip to content

umputun/secrets

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Safe Secrets - safe(r) and easy way to transfer sensitive data

Build Status Go Report Card Coverage Status Docker Automated build

The primary use case is sharing sensitive data securely - messages that self-destruct, can only be accessed once, and are protected by an easy-to-share PIN code. I got tired of the usual "security" approach of sending username and password in two separate emails - that's just a joke. So I built this as a simple and better alternative to passing passwords around.

Quick Start

# run with docker
docker run -p 8080:8080 -e SIGN_KEY=your-random-secret-key -e DOMAIN=localhost -e PROTOCOL=http umputun/secrets

# or build and run locally
cd app && go build -o secrets && ./secrets -k "your-secret-key" -d localhost -p http

Then open http://localhost:8080 in your browser.

How It Works

  1. Enter your secret message (or upload a file)
  2. Set expiration time and a PIN
  3. Get a one-time link to share

Your recipient opens the link, enters the PIN, and sees the message. That's it. The message is deleted immediately after reading, and wrong PIN attempts are limited (default: 3 tries).

Try it live: safesecret.info - feel free to use it if you're crazy enough to trust me, or run your own instance.

Screenshots (click to expand)

Desktop View

Desktop View

Dark Mode

Dark Mode

Mobile View

Mobile View

PIN Entry

PIN Entry

Decoded Message

Decoded Message

How Safe Is This Thing

  • Messages are encrypted with your PIN (which is also hashed) - the original is never stored
  • Nothing sensitive in logs
  • Nothing on disk by default (in-memory storage)
  • Messages are permanently destroyed after reading or expiration
  • An attacker would need both the link AND the PIN to access anything
  • Zero-knowledge by default for web UI: all encryption happens in your browser

Feel free to suggest any other ways to make the process safer.

Encryption Architecture

The service uses hybrid encryption based on how you access it:

Web UI (zero-knowledge):

  • All encryption/decryption happens in your browser using Web Crypto API (AES-128-GCM)
  • The server stores only encrypted blobs - it cannot read your messages
  • The decryption key is stored in the URL fragment (#key) which never leaves your browser
  • Even if the server is compromised, your secrets remain encrypted

API (server-side):

  • Server handles encryption/decryption for API clients
  • Simpler integration - no client-side crypto needed
  • Server encrypts with your PIN before storing

Requirements for web UI:

  • HTTPS is required (Web Crypto API doesn't work on plain HTTP, except localhost)
  • JavaScript must be enabled
  • The full URL including the #key fragment must be shared - some apps strip URL fragments

Trade-offs:

  • Server cannot distinguish text from files in web-created messages (all content is opaque)
  • If you lose the URL fragment, the message is unrecoverable

Security Architecture

Encryption:

  • Server-side: AES-256-GCM for message encryption
  • Client-side (web UI): AES-128-GCM via Web Crypto API
  • PIN hashing: bcrypt (cost 14)
  • Random key generation: 32-byte cryptographically secure
  • Server rejects unencrypted content from web clients (ciphertext format validation)

HTTP Security Headers:

  • Content-Security-Policy: restricts scripts, styles, fonts to trusted sources; frame-ancestors 'none'; form-action 'self'
  • Strict-Transport-Security: HSTS with 1-year max-age (HTTPS only)
  • X-Frame-Options: DENY - prevents clickjacking
  • X-Content-Type-Options: nosniff - prevents MIME sniffing
  • Referrer-Policy: strict-origin-when-cross-origin
  • Cache-Control: no-cache, no-store, must-revalidate for dynamic content; long cache for static assets

Rate Limiting:

  • API requests: 10 req/sec per IP (configurable via --limit)
  • PIN attempts: 3 max per message (configurable via --pinattempts)
  • Failed attempts permanently logged with anonymized IP

Privacy:

  • IP addresses hashed with HMAC-SHA256 before logging (8-char prefix)
  • Sensitive URL paths masked in logs (/message/key/pin → /message/partial-key/*****)
  • No cookies except theme preference
  • No analytics or tracking

Installation

Docker (Recommended)

Simple setup:

docker run -p 8080:8080 \
  -e SIGN_KEY=your-long-random-secret \
  -e DOMAIN=example.com \
  -e PROTOCOL=https \
  umputun/secrets

Production setup with docker-compose:

  1. Download docker-compose.yml from this repo
  2. Configure environment variables (see Configuration section below)
  3. Run docker-compose up -d

For SSL termination, put a reverse proxy in front (e.g., reproxy, nginx, or traefik).

See docker-compose.yml for a complete example.

Container Security

The Docker image is built on a minimal scratch-based image for security hardening:

  • Minimal attack surface: No shell, package manager, or unnecessary utilities
  • Non-root user: Runs as app user with UID/GID 1001
  • Small footprint: ~21MB total image size

Volume permissions: The container stores SQLite data in /data by default. When mounting volumes for persistence, ensure the directory is accessible by UID 1001:

# Option 1: Set ownership on the host
mkdir -p /data/secrets && chown 1001:1001 /data/secrets
docker run -v /data/secrets:/data ... umputun/secrets

# Option 2: Run as your host user (e.g., UID 1000)
docker run --user 1000:1000 -v ./data:/data ... umputun/secrets

In docker-compose:

services:
  secrets:
    image: umputun/secrets
    user: "1000:1000"  # run as your host user instead of default 1001
    volumes:
      - ./data:/data

Alternatively, use Docker named volumes which handle permissions automatically:

services:
  secrets:
    image: umputun/secrets
    # no user override needed - named volumes adapt to container's UID
    volumes:
      - secrets-data:/data

volumes:
  secrets-data:

Binary

Download from releases or build from source:

cd app && go build -o secrets
./secrets -k "your-secret-key" -d "example.com" -p https

Running Tests

# Unit tests
go test ./...

# E2E tests (requires playwright setup)
make e2e-setup   # one-time: install playwright browsers
make e2e         # run e2e tests (headless)
make e2e-ui      # run with visible browser for debugging

Configuration

All options work as both CLI flags and environment variables.

Core Options

Flag Env Variable Default Description
-k, --key SIGN_KEY required Signing key for encryption
-d, --domain DOMAIN required Site domain(s), comma-separated for multiple
-p, --protocol PROTOCOL https Site protocol (http/https)
--listen LISTEN :8080 Server listen address (ip:port or :port)
--branding BRANDING Safe Secrets Application title
--branding-url BRANDING_URL https://safesecret.info Branding link URL for emails
--dbg - false Enable debug mode
--proxy-security-headers PROXY_SECURITY_HEADERS false Disable security headers (when proxy handles them)

Message Settings

Flag Env Variable Default Description
--pinsize PIN_SIZE 5 PIN length in characters
--expire MAX_EXPIRE 24h Maximum message lifetime
--pinattempts PIN_ATTEMPTS 3 Max wrong PIN attempts
--allow-no-pin ALLOW_NO_PIN false Allow creating secrets without PIN protection

When --allow-no-pin is enabled, users can skip PIN entry during secret creation. A confirmation modal ensures this is intentional. Use this for workflows where the sharing channel itself is already secure (e.g., Signal or other end-to-end encrypted messengers).

Storage

Flag Env Variable Default Description
-e, --engine ENGINE MEMORY Storage engine: MEMORY or SQLITE
--sqlite SQLITE_FILE /tmp/secrets.db SQLite database file path

File Uploads

Share files securely - they're encrypted with your PIN just like text messages and self-destruct after download. The filename is preserved but stored encrypted.

Flag Env Variable Default Description
--files.enabled FILES_ENABLED false Enable file uploads
--files.max-size FILES_MAX_SIZE 1048576 Max file size in bytes (1MB)

Authentication

Optional password protection for creating secrets. When enabled, users must log in before they can generate links. Viewing/consuming secrets remains anonymous - no login required to open a link with the correct PIN.

Flag Env Variable Default Description
--auth.hash AUTH_HASH disabled bcrypt hash to enable auth
--auth.session-ttl AUTH_SESSION_TTL 168h Session lifetime (7 days)

Generate a bcrypt hash:

# htpasswd (Apache utils)
htpasswd -bnBC 10 "" yourpassword | tr -d ':'

# mkpasswd (apt install whois)
mkpasswd -m bcrypt yourpassword

# Docker
docker run --rm caddy caddy hash-password --plaintext yourpassword

Web UI: Login popup appears when creating a secret. Sessions stored in cookies.

API: Requires HTTP Basic Auth with username secrets and your password.

Email Sharing

Send secret links directly via email. Recipients receive a nicely formatted email with the link - they still need the PIN (share it separately for security).

Flag Env Variable Default Description
--email.enabled EMAIL_ENABLED false Enable email sharing
--email.host EMAIL_HOST required SMTP server host
--email.port EMAIL_PORT 587 SMTP server port
--email.username EMAIL_USERNAME - SMTP auth username
--email.password EMAIL_PASSWORD - SMTP auth password
--email.from EMAIL_FROM required Sender address (e.g., "App Name <noreply@example.com>")
--email.tls EMAIL_TLS false Use TLS (not STARTTLS)
--email.timeout EMAIL_TIMEOUT 30s Connection timeout
--email.template EMAIL_TEMPLATE built-in Custom email template path

When enabled, a "Send Email" button appears after creating a secret link. The email includes a preview of the message body (customizable via template).

Warning

Do not enable email sharing on public instances without authentication (--auth.hash). Without auth, anyone can use your SMTP server to send emails to arbitrary addresses, which can be abused for spam or phishing. Always require authentication when email sharing is enabled on publicly accessible instances.

Mailgun SMTP Setup

Mailgun requires specific configuration:

--email.enabled \
--email.host=smtp.mailgun.org \
--email.port=465 \
--email.tls \
--email.username=postmaster@mg.yourdomain.com \
--email.password=your-mailgun-smtp-password \
--email.from="Your App <noreply@mg.yourdomain.com>"

Important notes:

  • Use port 465 with --email.tls (implicit TLS), not port 587 with STARTTLS
  • Add your server's IP to Mailgun's Authorized Recipients or IP Allowlist (required since April 2024)
  • Authentication failures (535) usually indicate IP not in allowlist, not wrong credentials
  • Sandbox domains can only send to verified recipients

Examples

# basic usage (web UI uses zero-knowledge encryption automatically)
./secrets -k "secret-key" -d "example.com"

# multiple domains
./secrets -k "secret-key" -d "example.com,alt.example.com"

# persistent storage
./secrets -k "secret-key" -d "example.com" -e SQLITE --sqlite=/data/secrets.db

# with file uploads (5MB limit)
./secrets -k "secret-key" -d "example.com" --files.enabled --files.max-size=5242880

# with authentication
./secrets -k "secret-key" -d "example.com" --auth.hash='$2a$10$...'

# with email sharing
./secrets -k "secret-key" -d "example.com" \
  --email.enabled --email.host=smtp.example.com \
  --email.from="Safe Secrets <noreply@example.com>"

Architecture

Safesecret is a single binary with embedded web UI. Typical production setup:

  • secrets container - handles API and serves the web interface (port 8080)
  • reverse proxy - SSL termination (reproxy, nginx, traefik, etc.)

The app works fine without a proxy for development or if you're running behind a load balancer (AWS ALB, haproxy, etc.).

Integrations

API

Simple REST API for programmatic access.

Health Check

GET /ping
$ curl https://safesecret.info/ping
pong

Create Secret

POST /api/v1/message

Body: {"message": "secret text", "exp": 3600, "pin": "12345"}

  • exp - expiration in seconds
  • pin - PIN code (must match configured length)
  • Requires Basic Auth when authentication is enabled (user: secrets)
$ curl -X POST https://safesecret.info/api/v1/message \
  -H "Content-Type: application/json" \
  -d '{"message": "my secret", "exp": 3600, "pin": "12345"}'

{
  "exp": "2024-01-15T10:30:00Z",
  "key": "f1acfe04-277f-4016-518d-16c312ab84b5"
}

Retrieve Secret

GET /api/v1/message/:key/:pin
$ curl https://safesecret.info/api/v1/message/f1acfe04-277f-4016-518d-16c312ab84b5/12345

{
  "key": "f1acfe04-277f-4016-518d-16c312ab84b5",
  "message": "my secret"
}

Get Configuration

GET /api/v1/params
$ curl https://safesecret.info/api/v1/params

{
  "max_exp_sec": 86400,
  "max_pin_attempts": 3,
  "pin_size": 5,
  "files_enabled": true,
  "max_file_size": 1048576
}

Packages

No packages published

Contributors 10