Sign inSign up

viyh/whisper

By viyh

•Updated 6 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

6 months ago

docker pull viyh/whisper