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.
npm install voidnote-sdk pip install voidnote go get github.com/quantum-encoding/voidnote-go cargo add voidnote zig fetch --save https://github.com/quantum-encoding/voidnote-zig/archive/refs/tags/v0.1.0.tar.gz implementation("net.voidnote:voidnote-java:0.1.0") # macOS / Linux
brew tap quantum-encoding/tap && brew install voidnote
# anywhere with Go
go install github.com/quantum-encoding/voidnote-cli@latest