Whisper provides a simple way to share secret information with someone else.
100K+
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.
Whisper uses a layered security approach:
See Security Details for the full cryptographic design.
# 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
Copy src/config.default.yaml to config.yaml and customize. Mount it into the container at /usr/src/app/config.yaml.
| Setting | Description |
|---|---|
secret_key | Server-level secret for password hashing. Must be a unique random string of 32+ characters. Change this from the default. |
storage_class | Storage backend class path (see Storage Backends). |
| Setting | Default | Description |
|---|---|---|
storage_config | {} | Backend-specific settings (bucket name, path, etc.) |
storage_clean_interval | 900 | Seconds between expired secret cleanup runs. |
max_data_size_kb | 100 | Maximum upload size in KB. |
app_listen_ip | 0.0.0.0 | Internal listen address. |
app_port | 5000 | Internal Flask port (nginx proxies externally). |
app_url_base | / | URL base path (e.g., /whisper/ for subpath hosting). |
argon2_time_cost | 5 | Argon2id time cost parameter. Higher = slower + more secure. |
argon2_memory_cost | 131072 | Argon2id memory cost in KB (131072 = 128 MB). |
argon2_parallelism | 1 | Argon2id parallelism parameter. |
| Variable | Default | Description |
|---|---|---|
WEB_PORT | 8000 | External nginx listen port. |
LOG_LEVEL | INFO | Set to DEBUG for verbose logging. |
CONFIG_FILE | config.yaml | Config file path inside the container. |
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
Simplest and most secure -- secrets are lost on restart.
storage_class: whisper.storage.memory.memory
storage_config: {}
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/*"
]
}]
}
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.
{
"id": "8d692508856cabba82d73e15ef6f0364de70546c",
"create_date": 1657421435,
"expire_date": -1,
"data": "U2FsdGVkX1/qMytBOYisCGZ3KjpkowinHQhu12lGY8E=",
"hash": "$argon2id$v=19$m=131072,t=5,p=1$...",
"version": 2
}
| Field | Description |
|---|---|
id | 160-bit random hex identifier (40 chars). |
create_date | UNIX timestamp of creation. |
expire_date | UNIX timestamp of expiration, or -1 for one-time use. |
data | Client-encrypted ciphertext (AES-256-GCM for v2, AES-CBC for v1). |
hash | Server-side password hash (Argon2id for v2, bcrypt for v1). |
version | Crypto version: 1 = legacy, 2 = current. |
Creating a secret:
secret_key as additional input.Retrieving a secret:
The server never has access to the plaintext password or data.
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.
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.
Storage backends must subclass store and implement:
| Method | Description |
|---|---|
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.
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
# 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 .
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
Pull requests are welcome. See CONTRIBUTING.md for setup and guidelines. For security issues, see SECURITY.md.
Joe Richards (GitHub: @viyh)
Content type
Image
Digest
sha256:a7f3a0bed…
Size
81.7 MB
Last updated
3 months ago
docker pull viyh/whisper