Skip to content

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Notes — Zero-Knowledge Encrypted Notes

A zero-knowledge encrypted notes webapp. All note content is encrypted and decrypted in your browser — the server stores only ciphertext and never sees your data.

  • Encryption: AES-256-GCM with PBKDF2 key derivation (600,000 iterations)
  • Auth: SHA-256(password + salt) → Argon2id on the server
  • Optional: TOTP two-factor authentication (via any authenticator app)
  • PWA: Installable as a standalone app on mobile/desktop

Architecture

Browser (HTTPS) ──► Reverse proxy ──► Container :8000
                                          │
                                          ▼
                                    SQLite (./data/)

Only one port (4861 by default) is needed — the container serves both the SPA frontend and the JSON API.


Quick Start

Prerequisites

  • Docker and Docker Compose (v2) on your server
  • A reverse proxy (Caddy, nginx, Traefik, NPM, Synology) for HTTPS termination
  • ~512 MB RAM for the container

1. Clone

git clone [email protected]:Matthew-Glt/zero-knowledge-notes.git
cd zero-knowledge-notes

2. Generate secrets

Option A — Use the included script (requires Python 3 + cryptography):

python scripts/generate-secrets.py

This prints everything you need for .env. Copy the output.

Option B — Generate manually (no dependencies):

# FLASK_SECRET_KEY — 64 hex characters (signs login sessions)
python3 -c "import secrets; print(f'FLASK_SECRET_KEY={secrets.token_hex(32)}')"

# FERNET_KEY — encrypts TOTP seed at rest (requires Fernet)
python3 -c "from cryptography.fernet import Fernet; print(f'FERNET_KEY={Fernet.generate_key().decode()}')"

If Python 3 + cryptography aren't available on the target machine, generate on any machine that has them and paste the values.

3. Create .env

cp .env.example .env
chmod 600 .env

Edit .env and paste the generated values:

FLASK_SECRET_KEY=a1b2c3...64-hex-chars...
FERNET_KEY=wNAUYHOZsMoyWGCEUA63cxbZ59ljSzqMk4WiI8ZiQu4=
NOTES_HTTPS=1
PORT=8000
Variable Required? What it does
FLASK_SECRET_KEY ✅ Yes Signs session cookies. Never change after go-live.
FERNET_KEY ✅ Yes (for TOTP) Encrypts TOTP seed at rest. Never change after enabling 2FA.
NOTES_HTTPS ✅ Yes 1 = cookies use Secure flag (keep 1 behind HTTPS proxy).
PORT ❌ Optional Internal container port. Leave as 8000.

4. Create data directory and admin user

mkdir -p data
chmod 700 data

Build the image and create the admin account (one-time setup):

sudo docker compose build
sudo docker compose run --rm notes python backend/setup.py --password "your-strong-password"

This creates data/notes.db with the admin user. Your password is hashed with Argon2id and used to derive an encryption key — the server never sees the plaintext.

Alternative: skip setup.py and run sudo docker compose up -d then open the site. The first-run browser setup wizard will appear (only works when no user exists).

5. Start the container

sudo docker compose up -d
sudo docker compose logs --tail 20 notes

Verify it's running:

curl -s http://127.0.0.1:4861/api/auth/salt

Expected output: {"auth_salt":"...","enc_salt":"..."} (JSON, not HTML).

6. Set up a reverse proxy

The container binds to 127.0.0.1:4861 (localhost only — not exposed to the network).

Configure your reverse proxy to forward https://notes.yourdomain.com → http://127.0.0.1:4861.

Caddy example:

notes.yourdomain.com {
    reverse_proxy 127.0.0.1:4861

    header {
        X-Content-Type-Options nosniff
        X-Frame-Options DENY
        Referrer-Policy no-referrer
        Content-Security-Policy "default-src 'self'; img-src 'self' https://api.qrserver.com data:; style-src 'self' 'unsafe-inline'; script-src 'self'"
    }
}

nginx example:

server {
    listen 443 ssl;
    server_name notes.yourdomain.com;

    location / {
        proxy_pass http://127.0.0.1:4861;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

Important: The API under /api/* is served by the same container. Ensure your proxy forwards all paths (/ and /api/*) to the same backend — don't split static vs API routes.


First Use

  1. Open https://notes.yourdomain.com in a browser
  2. Sign in with the password you set in step 4
  3. Start creating encrypted notes

All encryption/decryption happens in JavaScript — the server stores only ciphertext.


Day-2 Operations

View logs

sudo docker compose logs -f notes

Rebuild after code update

git pull
sudo docker compose up -d --build

Stop / Start

sudo docker compose down
sudo docker compose up -d

Backup the database

cp data/notes.db data/notes-$(date +%F).db

The entire database is a single SQLite file — back it up regularly.

Reset password (⚠️ destroys all notes)

sudo docker compose down
rm -f data/notes.db
sudo docker compose run --rm notes python backend/setup.py --password "new-password"
sudo docker compose up -d

Enable TOTP (2FA)

  1. Sign in → click the 🔐 button (or go to Settings)
  2. Scan the QR code with an authenticator app
  3. Enter the 6-digit code to confirm
  4. Save your backup codes somewhere safe

Before enabling TOTP, make sure FERNET_KEY is set in .env — the TOTP seed is encrypted at rest using Fernet.

Change the host port

Edit docker-compose.yaml and change the port mapping:

ports:
  - "127.0.0.1:4861:8000"

Change 4861 to your desired port. Update your reverse proxy target accordingly. Then:

sudo docker compose up -d

Files in this repo

├── Dockerfile              # Production image (Python 3.11-slim + Gunicorn)
├── docker-compose.yaml     # Compose stack
├── .env.example            # Template — copy to .env and fill in secrets
├── .gitignore
├── scripts/
│   └── generate-secrets.py # One-shot secret generator
├── backend/
│   ├── app.py              # Flask app routes
│   ├── auth.py             # Session management
│   ├── config.py           # Configuration from environment
│   ├── crypto_utils.py     # Argon2id, Fernet, key wrapping
│   ├── ip_blocker.py       # Rate limiting and IP bans
│   ├── models.py           # SQLite schema and connection management
│   ├── setup.py            # Admin user creation
│   └── requirements.txt
└── frontend/
    ├── index.html          # SPA shell
    ├── app.js              # Application controller
    ├── api.js              # API client (REST calls)
    ├── crypto.js           # Client-side AES-256-GCM + PBKDF2
    ├── notes.js            # Notes UI
    ├── styles.css          # Dark/light mode CSS
    ├── manifest.json       # PWA manifest
    ├── sw.js               # Service worker (app shell caching only)
    └── icons/
        ├── icon-192.png
        └── icon-512.png

Troubleshooting

Symptom Fix
encryption not configured (FERNET_KEY not set) Add FERNET_KEY to .env, rebuild, restart
Blank page after login Hard-refresh (Ctrl+Shift+R) or unregister service worker in DevTools → Application → Service Workers
Login returns HTML instead of JSON (Unexpected token '<'...) Browser got HTML instead of JSON — ensure reverse proxy forwards /api/* to the container on port 4861. Test: curl -s http://127.0.0.1:4861/api/auth/salt should return {"auth_salt":"..."}
TOTP codes not accepted Check server clock (date). TOTP requires the server time to be within ~30 seconds of the authenticator app.
Port already in use Change 4861 in docker-compose.yaml and update your reverse proxy
Permission denied on ./data chmod 700 data on the host; the container runs as root inside

Security

  • Zero-knowledge: All note content is encrypted client-side. The server never has access to plaintext or the encryption key.
  • Password hashing: SHA-256(password + auth_salt) → Argon2id on the server. Argon2id is memory-hard (64 MB, 3 iterations, 4 parallelism).
  • Key derivation: PBKDF2-SHA256 at 600,000 iterations using enc_salt to derive the wrapping key.
  • Session tokens: 64-byte random hex tokens, 24-hour expiry, HTTP-only cookies.
  • TOTP seed: Encrypted at rest with Fernet (AES-128-CBC + HMAC) using FERNET_KEY.
  • Rate limiting: IP-based bans after 3 failed attempts (5-min temp), 10 failures (1-hour long), 50 total events (permanent).
  • HTTPS: Set NOTES_HTTPS=1 so session cookies get the Secure flag.

License

MIT

About

Zero-knowledge encrypted notes webapp — client-side AES-256-GCM, Argon2id auth, Docker deployment

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages