Skip to content
VoidNote

How VoidNote works

The full technical picture — what we see, what we never see, and why.

The split-token design

When you create a note, your browser generates 32 cryptographically random bytes — 64 hex characters. This token is split at the midpoint:

fullToken = a1b2c3...64 chars total

tokenId = first 32 chars // sent to server as a lookup key

secret = last 32 chars // embedded in share URL, never sent to server

The server stores only the tokenId alongside the encrypted ciphertext. The secret is embedded in the link you share. Neither half is useful alone: the server has an encrypted blob with no key; the link has a key with no data to find.

Encryption

The encryption key is derived by hashing the secret half of the token:

key = SHA-256( hexToBytes(secret) )

iv = 12 random bytes

ciphertext = AES-256-GCM( plaintext, key, iv )

AES-256-GCM provides both confidentiality and authenticated integrity — any tampering with the ciphertext causes decryption to fail with an authentication error, not silently corrupt data. All cryptographic operations run in your browser using the native SubtleCrypto API.

What the server stores

Field Value Sensitive?
token_id First 32 chars of token Low — a lookup key only
encrypted_content AES-256-GCM ciphertext (hex) Unreadable without key
iv 12-byte initialisation vector (hex) Public — needed for decrypt
title Plaintext title (if any) Yes — keep titles non-sensitive
expires_at 24h after creation No

The secret — the half that makes decryption possible — is never sent to or stored by our server. It exists only in the URL you share.

Reveal gate

When someone opens a note link, the page loads but does not automatically decrypt. The viewer must click "Reveal note" to trigger the decrypt request. This prevents browser link prefetching, URL scanning bots, and messaging app link previews from consuming a view.

Destruction

Notes are permanently deleted in two cases:

  • The view count reaches its limit (on the final view, deletion happens in the same database transaction)
  • 24 hours have elapsed since creation (an hourly cleanup job removes all expired records)

Deletion is permanent. There is no archive, no soft-delete, no recovery path. Once gone, the ciphertext is gone — even if you held the decryption key, there would be nothing left to decrypt.

Authentication

User accounts use post-quantum session tokens based on ML-DSA-65 (CRYSTALS-Dilithium), implemented via the @noble/post-quantum library running as WASM. Session tokens are signed with a Dilithium keypair stored in Cloudflare KV — not a symmetric secret or a password-derived key. Passwords are hashed with PBKDF2 (100,000 iterations, SHA-256) with a per-user random salt.

What we can't do

  • Read the content of any note — we never receive the decryption key
  • Recover a destroyed note — deletion is irreversible at the storage level
  • Extend a note's lifetime — expiry is enforced by the server regardless of any client request
  • Recover your password — we store only a salted PBKDF2 hash

SDKs & CLI

VoidNote is infrastructure you can call from code. All SDKs implement the same split-token encryption client-side — no plaintext ever leaves your environment. Every language, one API.

TypeScript · Node.js · Bun · Deno · Cloudflare Workers · Browser GitHub →
npm install voidnote-sdk
Python 3.9+ GitHub →
pip install voidnote
Go 1.21+ · stdlib only, zero dependencies GitHub →
go get github.com/quantum-encoding/voidnote-go
Rust · async + blocking API, pure-Rust crypto (no OpenSSL) GitHub →
cargo add voidnote
Zig 0.13+ · stdlib only, zero dependencies GitHub →
zig fetch --save https://github.com/quantum-encoding/voidnote-zig/archive/refs/tags/v0.1.0.tar.gz
Java 17+ · java.net.http built-in, Gson only external dep GitHub →
implementation("net.voidnote:voidnote-java:0.1.0")
CLI — works in any shell, Dockerfile, or GitHub Action GitHub →

# macOS / Linux

brew tap quantum-encoding/tap && brew install voidnote

# anywhere with Go

go install github.com/quantum-encoding/voidnote-cli@latest