All apps · 0 apps
Whisper
Docker app from grtgbln's Repository
Overview
Readme
View on GitHubWhisper
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.
Architecture
flowchart TD
subgraph Client ["Client-side"]
direction LR
PS["Plaintext<br>Secret"] -->|"encrypt with<br>Web Crypto API"| AES["AES-256-GCM<br>Encrypted Secret"]
PP["Plaintext<br>Password"] -.->|"used as<br>encryption key"| AES
PP -->|"hash with<br>Argon2id WASM"| AH["Argon2id<br>Password Hash"]
end
subgraph Server ["Server-side"]
direction LR
SK["Server<br>Secret Key"] -.-> SAH
AH2["Argon2id<br>Password Hash"] -->|"re-hash with<br>server key"| SAH["Argon2id<br>Server Hash"]
SAES["AES-256-GCM<br>Encrypted Secret"]
end
AES --> SAES
AH --> AH2
style PS fill:#e74c3c,color:#fff
style PP fill:#e74c3c,color:#fff
style AES fill:#3498db,color:#fff
style AH fill:#e8a0a0,color:#333
style SK fill:#f39c12,color:#fff
style AH2 fill:#e8a0a0,color:#333
style SAH fill:#aed6f1,color:#333
style SAES fill:#aed6f1,color:#333
How It Works
- Enter secret text or select a file, along with a password.
- Choose an expiration: one-time use, 1 hour, 1 day, or 1 week.
- Click Create Secret to get a shareable link.
- Send the link and password separately (e.g., link via email, password via text).
- 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
| 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). |
Optional Settings
| 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. |
Environment Variables
| 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. |
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
}
| 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. |
Cryptographic Flow (v2)
Creating a secret:
- User enters plaintext + password.
- Client hashes password with Argon2id (configurable params, public salt from URL origin).
- Client encrypts plaintext with AES-256-GCM via Web Crypto API (random 96-bit IV, PBKDF2-derived key at 210,000 iterations).
- Client sends encrypted data + Argon2id hash to server.
- Server re-hashes with Argon2id using server
secret_keyas additional input. - Server stores encrypted data + server-side hash.
Retrieving a secret:
- Client hashes password with Argon2id (same params).
- Server verifies against stored hash.
- For one-time secrets: atomic claim-and-delete before returning data.
- 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:
| 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.
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.13, 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
Media gallery
Requirements
Expects a `config.yaml` file to be available at the Config File path BEFORE starting the container. See documentation: https://github.com/viyh/whisper#configuration-file
Categories
Download Statistics
Total Downloads Over Time
Related apps
Explore more like this
Explore allDetails
viyh/whisper:0.1.0-alphaRuntime arguments
- Web UI
http://[IP]:[PORT:8000]/- Network
bridge- Privileged
- false
Template configuration
Container Port: 8000
- Target
- 8000
- Default
- 8000
- Value
- 8000
Path to the config file
- Target
- /usr/src/app/config.yaml
- Default
- /mnt/user/appdata/whisper/config.yaml
- Value
- /mnt/user/appdata/whisper/config.yaml
Path to the storage directory. Remove to use in-memory storage (lost when container restarts)
- Target
- /tmp/whisper
- Default
- /mnt/user/appdata/whisper/storage
- Value
- /mnt/user/appdata/whisper/storage