Skip to content

ling0x/paravoid

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

paravoid

A self-hosted one-time secret notes service built with Axum + redb, using hybrid post-quantum cryptography.

Crypto Scheme

ML-KEM-768 (FIPS 203 / CRYSTALS-Kyber)
  +
AES-256-GCM
  +
HKDF-SHA256 key derivation

Why hybrid?

Layer Algorithm Quantum threat
KEM ML-KEM-768 (Kyber) Quantum-hard (lattice)
Symmetric AES-256-GCM ~128-bit post-quantum security (Grover)
KDF HKDF-SHA256 Combines both secrets

Breaking the scheme requires simultaneously breaking both ML-KEM-768 (no known quantum attack) and the classical 32-byte random secret. This is the same hybrid approach used by Google, Cloudflare, and Apple.

Encryption Flow

POST /api/secrets
  1. Generate ML-KEM-768 keypair (ek, dk)
  2. Encapsulate: (ct_kem, ss_pq) = ek.encapsulate()
  3. Generate 32-byte classical_secret
  4. Derive AES key: HKDF-SHA256(ss_pq || classical_secret)
  5. Encrypt message with AES-256-GCM
  6. Store in redb: { ciphertext, nonce, ek, ct_kem, expires_at }
  7. Return to creator: { id, dk, classical_secret }  <- NEVER stored
POST /api/secrets/:id/reveal
  1. Atomically DELETE record from redb
  2. Decapsulate: ss_pq = dk.decapsulate(ct_kem)
  3. Re-derive AES key: HKDF-SHA256(ss_pq || classical_secret)
  4. Decrypt AES-256-GCM ciphertext
  5. Return plaintext

The server never stores dk or classical_secret. A complete database dump reveals only quantum-hard ciphertext.

Quickstart

cargo run

Server starts on http://0.0.0.0:3000.

API

Create a secret

POST /api/secrets
Content-Type: application/json

{ "message": "my secret", "ttl_secs": 3600 }

Response:

{
  "id": "<uuid>",
  "dk": "<hex-ml-kem-768-decap-key>",
  "classical_secret": "<hex-32-bytes>",
  "link": "/api/secrets/<uuid>/reveal",
  "expires_at": 1234567890
}

Share id + dk + classical_secret with the recipient — ideally in a URL fragment (#dk=...&cs=...) so they never appear in server logs.

Check existence (no plaintext)

GET /api/secrets/:id

Reveal (one-time, then permanently deleted)

POST /api/secrets/:id/reveal
Content-Type: application/json

{ "dk": "<hex-ml-kem-768-decap-key>", "classical_secret": "<hex>" }

Response:

{ "id": "<uuid>", "message": "my secret" }

After this call the secret is permanently gone from the database.

Security Properties

  • Post-quantum KEM: ML-KEM-768 is NIST FIPS 203 standardised — lattice-based, no known quantum attack
  • Hybrid construction: AES key derived from ss_pq || classical_secret via HKDF — both must be broken simultaneously
  • Zero server-side key storage: dk and classical_secret are never written to disk
  • Atomic deletion: record is removed from redb before decryption begins — no race conditions
  • TTL enforcement: lazy cleanup on peek and reveal
  • AES-256-GCM authentication: provides ciphertext integrity (AEAD)

Storage

Uses redb — pure-Rust embedded key-value store. Database file: paravoid.redb.

About

Single‑use, self‑destructing, end‑to‑end‑encrypted notes

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages