viyh/whisper

By viyh

Updated 3 months ago

Whisper provides a simple way to share secret information with someone else.

Image
Security
0

100K+

viyh/whisper repository overview

Whisper

Secure secret sharing via one-time or short-lived links.

Share passwords, credentials, keys, or other sensitive data without using insecure channels like email or chat. Secrets are encrypted client-side before transmission -- the server never sees plaintext.

How It Works

  1. Enter secret text or select a file, along with a password.
  2. Choose an expiration: one-time use, 1 hour, 1 day, or 1 week.
  3. Click Create Secret to get a shareable link.
  4. Send the link and password separately (e.g., link via email, password via text).
  5. The recipient opens the link, enters the password, and retrieves the secret.

Security Model

Whisper uses a layered security approach:

  • Client-side encryption: Secrets are encrypted with AES-256-GCM (Web Crypto API) before leaving the browser. The server never has access to plaintext data.
  • Argon2id password hashing: Passwords are hashed client-side using Argon2id (WASM) before transmission. The server re-hashes with its own Argon2id salt, so even a compromised server cannot recover passwords.
  • One-time secrets: Deleted atomically after retrieval -- concurrent requests cannot both succeed.
  • Rate limiting: 10 attempts per minute per IP, plus per-secret lockout after 10 failed password attempts.
  • Strict CSP: No inline scripts. Content-Security-Policy with explicit allowlists.

See Security Details for the full cryptographic design.

Quick Start

# Generate a config file with a random secret key
export SECRET_KEY=$(head /dev/urandom | LC_ALL=C tr -dc 'A-Za-z0-9' | head -c 48)
sed "s/CHANGE_ME_TO_A_RANDOM_STRING_OF_32_OR_MORE_CHARACTERS/$SECRET_KEY/" src/config.default.yaml > config.yaml

# Build from source (optional, can also pull viyh/whisper:0.2.0):
# docker build -t viyh/whisper .

# Run
docker run -d --name whisper -p 8000:8000 \
    -v $(pwd)/config.yaml:/usr/src/app/config.yaml \
    viyh/whisper:0.2.0

Browse to http://localhost:8000

Configuration

Copy src/config.default.yaml to config.yaml and customize. Mount it into the container at /usr/src/app/config.yaml.

Required Settings
SettingDescription
secret_keyServer-level secret for password hashing. Must be a unique random string of 32+ characters. Change this from the default.
storage_classStorage backend class path (see Storage Backends).
Optional Settings
SettingDefaultDescription
storage_config{}Backend-specific settings (bucket name, path, etc.)
storage_clean_interval900Seconds between expired secret cleanup runs.
max_data_size_kb100Maximum upload size in KB.
app_listen_ip0.0.0.0Internal listen address.
app_port5000Internal Flask port (nginx proxies externally).
app_url_base/URL base path (e.g., /whisper/ for subpath hosting).
argon2_time_cost5Argon2id time cost parameter. Higher = slower + more secure.
argon2_memory_cost131072Argon2id memory cost in KB (131072 = 128 MB).
argon2_parallelism1Argon2id parallelism parameter.
Environment Variables
VariableDefaultDescription
WEB_PORT8000External nginx listen port.
LOG_LEVELINFOSet to DEBUG for verbose logging.
CONFIG_FILEconfig.yamlConfig file path inside the container.

Storage Backends

Local Disk (default)

Stores secrets as JSON files. Mount a volume for persistence.

storage_class: whisper.storage.local.local
storage_config:
    path: /tmp/whisper
docker run -d --name whisper -p 8000:8000 \
    -v $(pwd)/config.yaml:/usr/src/app/config.yaml \
    -v whisper-data:/tmp/whisper \
    whisper
In-Memory

Simplest and most secure -- secrets are lost on restart.

storage_class: whisper.storage.memory.memory
storage_config: {}
AWS S3

Uses S3 object tags (create_date, expire_date) so the cleanup process only reads tags, not full objects.

storage_class: whisper.storage.aws.s3
storage_config:
    bucket_name: my-whisper-bucket
    bucket_path: secrets

Credentials: IAM role (recommended), credential file mount (-v ~/.aws:/.aws:ro), or environment variables (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_DEFAULT_REGION).

Minimum IAM permissions:

{
    "Version": "2012-10-17",
    "Statement": [{
        "Effect": "Allow",
        "Action": [
            "s3:ListBucket", "s3:GetBucketLocation",
            "s3:GetObject", "s3:PutObject", "s3:DeleteObject",
            "s3:GetObjectTagging", "s3:PutObjectTagging", "s3:DeleteObjectTagging"
        ],
        "Resource": [
            "arn:aws:s3:::my-whisper-bucket",
            "arn:aws:s3:::my-whisper-bucket/*"
        ]
    }]
}
GCP Cloud Storage

Uses object metadata (create_date, expire_date) for efficient cleanup.

storage_class: whisper.storage.gcp.gcs
storage_config:
    gcp_project: my-project
    bucket_name: my-whisper-bucket
    bucket_path: secrets

Credentials: Application default credentials (recommended), or mount gcloud config (-v ~/.config/gcloud:/.config/gcloud:ro).

Required GCS permissions:

storage.objects.get, storage.objects.update, storage.objects.list,
storage.objects.create, storage.objects.delete,
storage.multipartUploads.create, storage.multipartUploads.abort,
storage.multipartUploads.listParts, storage.multipartUploads.list,
storage.buckets.get

Or use built-in roles: roles/storage.legacyObjectOwner + roles/storage.legacyBucketWriter.

Security Details

Secret Structure
{
    "id": "8d692508856cabba82d73e15ef6f0364de70546c",
    "create_date": 1657421435,
    "expire_date": -1,
    "data": "U2FsdGVkX1/qMytBOYisCGZ3KjpkowinHQhu12lGY8E=",
    "hash": "$argon2id$v=19$m=131072,t=5,p=1$...",
    "version": 2
}
FieldDescription
id160-bit random hex identifier (40 chars).
create_dateUNIX timestamp of creation.
expire_dateUNIX timestamp of expiration, or -1 for one-time use.
dataClient-encrypted ciphertext (AES-256-GCM for v2, AES-CBC for v1).
hashServer-side password hash (Argon2id for v2, bcrypt for v1).
versionCrypto version: 1 = legacy, 2 = current.
Cryptographic Flow (v2)

Creating a secret:

  1. User enters plaintext + password.
  2. Client hashes password with Argon2id (configurable params, public salt from URL origin).
  3. Client encrypts plaintext with AES-256-GCM via Web Crypto API (random 96-bit IV, PBKDF2-derived key at 210,000 iterations).
  4. Client sends encrypted data + Argon2id hash to server.
  5. Server re-hashes with Argon2id using server secret_key as additional input.
  6. Server stores encrypted data + server-side hash.

Retrieving a secret:

  1. Client hashes password with Argon2id (same params).
  2. Server verifies against stored hash.
  3. For one-time secrets: atomic claim-and-delete before returning data.
  4. Client decrypts with AES-256-GCM using original password.

The server never has access to the plaintext password or data.

Legacy v1 Secrets

If you have a pre-0.2.0 secret link that was created with the v1 format (PBKDF2 + bcrypt + CryptoJS AES-CBC), check out the release/0.1.0_to_0.2.0 branch, which retains v1 decryption support.

Rate Limiting

The default in-memory rate limiter (storage_uri="memory://") tracks state per-gunicorn-worker. In multi-worker or multi-container deployments, each worker enforces limits independently. For shared rate-limit state across workers, configure a Redis backend via flask-limiter.

Development

Writing Storage Backends

Storage backends must subclass store and implement:

MethodDescription
start()Initialize the backend.
get_secret(secret_id)Return secret object or False.
set_secret(s)Save a secret object.
delete_secret(secret_id)Delete a secret.
claim_secret(secret_id)Atomically delete and return True if it existed (for one-time secrets).
delete_expired()Iterate stored secrets and delete any where is_expired() returns True.

See src/whisper/storage/memory.py for a minimal example.

Running for Development
docker build -t whisper:dev .
docker run -it --rm --name whisper -p 8000:8000 \
    -e LOG_LEVEL=DEBUG \
    -v $(pwd)/src/config.yaml:/usr/src/app/config.yaml \
    whisper:dev
Building
# Standard build
docker build -t whisper:0.2.0 .

# Cross-platform build
docker buildx build --platform=linux/amd64,linux/arm64 \
    -t whisper:0.2.0 .
Tech Stack

Backend: Python 3.12, Flask 3.1, Gunicorn 23, Nginx (Debian Slim)

Client-side crypto: Web Crypto API (AES-256-GCM, PBKDF2), argon2-browser (WASM)

Frontend: Bootstrap 5.3, clipboard.js, vanilla JS (no jQuery)

Storage: Local disk, in-memory, AWS S3, GCP Cloud Storage

Contributing

Pull requests are welcome. See CONTRIBUTING.md for setup and guidelines. For security issues, see SECURITY.md.

Author

Joe Richards (GitHub: @viyh)

License

MIT License

Tag summary

Content type

Image

Digest

sha256:a7f3a0bed

Size

81.7 MB

Last updated

3 months ago

docker pull viyh/whisper