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
Install Whisper on Unraid in a few clicks.
Find Whisper in Community Apps on your Unraid server, review the template, and click Install. Unraid handles the Docker app or plugin setup from the published template.
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