Zero-knowledge encrypted message sharing. Messages are encrypted client-side with AES-256-GCM (PBKDF2 800k iterations); the server never sees plaintext. Decryption keys are passed via URL fragments (#key=...), which browsers don't send to the server.
- Zero-Knowledge Encryption: AES-256-GCM + PBKDF2 (800k iterations), entirely client-side
- Key Fragment Delivery: Decryption keys in URL hash — never sent to the server
- RSA Key Pairs: Non-extractable RSA-OAEP 2048-bit keys stored in IndexedDB
- Message Expiration: TTL-based expiration with automatic cleanup
- Ownership Transfer: Unclaimed secrets can be claimed by the first viewer
- Custom Names: Optional labels for organizing secrets
- Nonce-Based CSP: Strict Content Security Policy without
unsafe-inlineorunsafe-eval - Distroless Container: Minimal attack surface — no shell, no package manager
cp .env.example .env
# Edit .env: set CF_DOMAIN, CF_TUNNEL_TOKEN, DOMAIN, CORS_ORIGINS
docker-compose up --build -dThis starts three services on an internal Docker network:
app— FastAPI backend (distroless container, port 8000 internal)nginx— reverse proxy with rate limiting and security headers (port 8080 internal, unprivileged)cloudflared— Cloudflare Tunnel for HTTPS ingress
No ports are published to the host. All traffic flows through the Cloudflare Tunnel.
pip install -r requirements.txt
python main.py
# Access at http://localhost:8000Two independent backends implement the same API and share the same frontend templates:
| Python/FastAPI | Cloudflare Workers | |
|---|---|---|
| Runtime | Docker (self-hosted) | Cloudflare edge (serverless) |
| Database | SQLite | D1 + KV |
| Entry point | main.py |
cloudflare-workers/src/index.js |
| Template resolution | Python at serve time | build.js at build time |
Internet → Cloudflare Tunnel → nginx (rate limiting, headers) → FastAPI app → SQLite
All containers run on an isolated Docker bridge network. The app container is read-only with a single writable volume for the SQLite database.
- All encryption/decryption happens client-side via Web Crypto API
- Server stores only ciphertext, IV, and salt — never plaintext or keys
- User identity derived from symmetric key:
SHA-256(key)[:12]→ UID - RSA key pair (non-extractable) stored in IndexedDB; encrypted symmetric key in localStorage
- Nonce-based Content Security Policy (no
unsafe-inline, nounsafe-eval) - Alpine.js CSP build for eval-free reactivity
All three containers run with zero Linux capabilities (cap_drop: ALL), read-only root filesystems, and no-new-privileges.
- Distroless app:
gcr.io/distroless/python3-debian12:nonroot— no shell, no coreutils, no package manager; uid 65532 - Unprivileged nginx:
nginxinc/nginx-unprivileged— runs entirely as uid 101, listens on 8080 (noNET_BIND_SERVICEneeded) - Read-only filesystems: only
/app/data(app, SQLite volume) and tmpfs mounts (nginx cache/run, cloudflared tmp) are writable - No privilege escalation:
no-new-privileges: trueon all containers - No bytecode:
PYTHONDONTWRITEBYTECODE=1prevents writes to read-only FS - Multi-stage build: TailwindCSS compiled at build time (node stage), Python deps installed separately, only artifacts copied to final image
All mutations use POST.
| Endpoint | Description |
|---|---|
POST /api/create |
Create encrypted message |
POST /api/view |
Retrieve message (requires UID if owned) |
POST /api/update |
Claim ownership (re-encrypt with owner's key) |
POST /api/list-secrets |
List owned secrets with pagination |
POST /api/list-pending-secrets |
List unclaimed secrets by creator |
POST /api/update-custom-name |
Update secret label |
POST /api/delete-secret |
Delete secret |
GET /health |
Health check |
CREATE TABLE messages (
id TEXT PRIMARY KEY,
ttl INTEGER NOT NULL,
uid TEXT NOT NULL DEFAULT '',
encrypted_message TEXT NOT NULL,
iv TEXT NOT NULL,
salt TEXT NOT NULL,
custom_name TEXT DEFAULT '',
creator_uid TEXT DEFAULT '',
created_at INTEGER NOT NULL
);Integration tests run the Python backend in Docker and exercise all API endpoints with a Python crypto client that replicates the browser-side encryption.
# Install test dependencies
pip install -r tests/requirements.txt
# Run all 30 tests (starts Docker automatically, tears down after)
pytest tests/ -v
# Run a specific test group
pytest tests/test_integration.py -v -k "test_create"Tests use tests/docker-compose.test.yaml, which publishes port 8000 to the host (unlike production compose which uses only internal networking).
- Health check
- Create & view with various TTL values (default, custom, permanent)
- Ownership flow (claim, double-claim rejection, owner-only access)
- List & pagination (owned, pending, page navigation)
- Delete (owned, pending, wrong-user rejection)
- Custom name updates
- Idempotent creation
- Input validation (invalid base64, bad UID format, nonexistent messages)
- Multiple reads of same secret
- Unicode content (Cyrillic, emoji, CJK)
- Full sender → recipient flow
See Quick Start above. Requires a Cloudflare Tunnel token.
cd cloudflare-workers/
npm install
npm run build
npm run deploy:productionSee cloudflare-workers/README.md for details.
StatefulSet with two containers (distroless app + nginx-unprivileged sidecar) and a PVC for SQLite. Single replica — SQLite doesn't support concurrent writers.
helm install inigma ./helm/ \
--set app.image.repository=ghcr.io/org/inigma \
--set app.image.tag=latest \
--set domain=inigma.example.com \
--set corsOrigins=https://inigma.example.com
helm upgrade inigma ./helm/ --set app.image.tag=v1.1.0Key Helm values:
| Value | Default | Description |
|---|---|---|
app.image.repository |
ghcr.io/org/inigma |
App container image |
app.image.tag |
Chart appVersion | App image tag |
nginx.image.tag |
1.28.2-alpine |
nginx-unprivileged tag |
domain |
example.com |
Domain for generated links |
corsOrigins |
https://example.com |
Allowed CORS origins |
persistence.size |
1Gi |
PVC size for SQLite |
ingress.enabled |
false |
Enable Ingress resource |
Security: runAsNonRoot, readOnlyRootFilesystem, cap_drop: ALL, seccompProfile: RuntimeDefault, automountServiceAccountToken: false, NetworkPolicy (ingress 8080 only, egress DNS only).
inigma/
├── main.py # FastAPI application
├── database.py # SQLite operations + TTL cleanup
├── requirements.txt # Python dependencies
├── Dockerfile # Multi-stage distroless build
├── Dockerfile.nginx # Nginx reverse proxy
├── docker-compose.yaml # Production: app + nginx + cloudflared
├── nginx.conf # Rate limiting, security headers, proxy
├── .env.example # Environment template
├── templates-modular/ # Shared frontend templates
│ ├── pages/ # Main HTML pages
│ ├── components/ # Reusable UI components
│ ├── scripts/ # JavaScript modules
│ │ ├── crypto-functions.js # RSA + AES + PBKDF2 crypto
│ │ ├── main-app.js # Alpine.js app (main page)
│ │ ├── view-app.js # Alpine.js app (view page)
│ │ ├── security-utils.js # XSS prevention, rate limiting
│ │ └── security-hardening.js
│ └── styles/ # CSS (Tailwind compiled at build)
├── tests/ # Integration test suite
│ ├── conftest.py # Docker fixtures (session-scoped)
│ ├── crypto_client.py # Python AES-256-GCM + PBKDF2 client
│ ├── test_integration.py # 30 API tests
│ ├── docker-compose.test.yaml
│ └── requirements.txt
├── cloudflare-workers/ # Serverless Workers deployment
│ ├── src/ # Worker source code
│ ├── build.js # Custom bundler
│ ├── schema.sql # D1 schema
│ └── migrations/ # D1 migrations
└── helm/ # Kubernetes Helm chart
├── Chart.yaml
├── values.yaml
├── nginx.conf # Sidecar nginx config (upstream 127.0.0.1:8000)
└── templates/
├── statefulset.yaml # App + nginx sidecar, PVC, security hardening
├── configmap.yaml # App env + nginx.conf
├── networkpolicy.yaml # Ingress 8080, egress DNS only
├── service.yaml
├── ingress.yaml
├── serviceaccount.yaml
└── secret.yaml
| Variable | Default | Description |
|---|---|---|
PORT |
8000 |
Application server port |
DOMAIN |
— | Domain for generated links (e.g. inigma.example.com) |
CORS_ORIGINS |
— | Allowed CORS origins (e.g. https://inigma.example.com) |
CF_DOMAIN |
— | Cloudflare Tunnel domain |
CF_TUNNEL_TOKEN |
— | Cloudflare Tunnel authentication token |
Web Crypto API Not Available
- Requires HTTPS or localhost. Cloudflare Tunnel provides HTTPS automatically.
Database Issues
- SQLite file stored in Docker volume
app-datamounted at/app/data - Check volume exists:
docker volume ls | grep app-data
Container won't start
- Distroless has no shell — you cannot
docker exec -it ... sh - Debug with:
docker logs <container>
docker-compose logs -f # All services
docker-compose logs -f app # FastAPI only
docker-compose logs -f nginx # Nginx only- Fork the repository
- Create a feature branch
- Run tests:
pytest tests/ -v - Submit a pull request
This project is licensed under the GNU General Public License v3.0. See the LICENSE file for details.
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program. If not, see https://www.gnu.org/licenses/.
- Freedom to use: You can use this software for any purpose
- Freedom to study: You can study how the program works and adapt it to your needs
- Freedom to share: You can redistribute copies to help others
- Freedom to improve: You can improve the program and release your improvements to the public
This software can be used commercially, but any modifications or derivative works must also be released under the GPL-3.0 license (copyleft provision).
For the full license text, see the LICENSE file in this repository or visit https://www.gnu.org/licenses/gpl-3.0.html.