Command Palette
Search for a command to run...

Cryptography

This page describes the primitives as they're implemented. open-secret uses hybrid constructions throughout: every asymmetric operation combines a post-quantum algorithm with a classical one, and a verifier requires both.

Algorithm matrix

ConcernAlgorithm
Key encapsulation (KEM)ML-KEM-768 (FIPS 203) ‖ X25519
SignaturesML-DSA-65 (FIPS 204) ‖ Ed25519
Symmetric AEADXChaCha20-Poly1305 (24-byte nonce)
Key derivationHKDF-SHA-256
Password-based key wrapArgon2id → HKDF-SHA-256
Session tokensPASETO v4.local (authenticated encryption with a server-only key)

All randomness comes from the platform CSPRNG (crypto.getRandomValues in the browser, crypto/rand in Go). There is no use of Math.random or math/rand anywhere in a security path.

Hybrid: both halves, always

The two halves are concatenated with an explicit length-prefixed layout ([len(pq)] [pq] [len(classical)] [classical]), and the Go and TypeScript implementations agree byte-for-byte.

Per-(version, device) envelopes

Entry content is never encrypted "to the vault", it's encrypted once per recipient device, for each version:

Encapsulate to the device

The writer runs the hybrid KEM against the recipient device's public key, producing a KEM ciphertext and a shared secret.

Derive a fresh AEAD key

HKDF-SHA-256 over the shared secret, with the salt bound to the KEM ciphertext and a domain-separation info tag, yields a one-use AEAD key.

Seal with key-committing AEAD

XChaCha20-Poly1305 with a fresh 24-byte nonce encrypts the content, and the authenticated data commits to the key and KEM ciphertext.

Key-committing AEAD

Plain AEAD authenticates the message but not which key opened it. open-secret binds SHA-256(key ‖ KEM-ciphertext) into the authenticated data, so a ciphertext can be opened under exactly one key. This closes the partitioning-oracle class of attacks (Len–Grubbs–Ristenpart, 2021) that bite naive multi-recipient AEAD.

The unlock vault (keys at rest)

Your device's private keys are stored wrapped under your unlock password, never in the clear:

  • The password (NFC-normalized, minimum 8 codepoints) is stretched with Argon2id, time cost 2, 128 MiB memory, parallelism 1, with a random 16-byte salt.
  • The Argon2id output is run through HKDF-SHA-256 under an unlock-specific domain tag to derive the AEAD key.
  • The keys are sealed with XChaCha20-Poly1305; the Argon2 parameters and salt are bound into the authenticated data, so an attacker can't shave the cost down by editing the stored parameters and still open the blob.

Because this is all local, unlocking works offline and the server can neither perform nor reset it.

Integrity of versions

Every entry version is signed (hybrid) over entry_id ‖ version_ulid ‖ content_hash ‖ outline_hash. content_hash covers the full encrypted detail blob; outline_hash covers only the outline so the list view can be verified independently. A client verifies that signature against the attestation-rooted trusted-device set before trusting the content, so a server that tampers with or swaps a version is detected, online or from cache.

Domain separation

Every distinct cryptographic use has its own info-tag / domain string (envelope encryption, unlock-vault KDF, device attestation, …), and both sides share the same constants with parity tests. This keeps a key or signature derived for one purpose from ever being valid for another.