Skip to content

Encryption stack

master_password + argon2_salt (16 bytes, per-user, stored on server)
Argon2id(
time=1,
memory=65536 KB (64 MB),
threads=4,
keyLen=32
)
KEK — 32 bytes

Why Argon2id: Memory-hard. Resistant to GPU and ASIC brute-force attacks. Winner of the Password Hashing Competition. The id variant combines the side-channel resistance of Argon2i with the GPU resistance of Argon2d.

Why these parameters: Calibrated to ~500ms on a modern laptop — fast enough to not be annoying, slow enough to make brute-force prohibitively expensive. Parameters are hardcoded client-side and not negotiated with the server.


Each secret has a unique, randomly generated Data Encryption Key (DEK):

crypto/rand → DEK (32 bytes)
AES-256-GCM(
key=KEK,
nonce=crypto/rand(12 bytes),
plaintext=DEK
) → dek_encrypted + dek_nonce

The encrypted DEK (dek_encrypted) and its nonce (dek_nonce) are stored on the server.


AES-256-GCM(
key=DEK,
nonce=crypto/rand(12 bytes),
plaintext=secret_value
) → ciphertext + nonce

The ciphertext and nonce are stored on the server. The DEK is zeroed from memory immediately after this operation.


{
"ciphertext_b64": "base64(AES-256-GCM(DEK, plaintext))",
"nonce_b64": "base64(12-byte nonce for ciphertext)",
"dek_encrypted_b64": "base64(AES-256-GCM(KEK, DEK))",
"dek_nonce_b64": "base64(12-byte nonce for DEK encryption)"
}

The server has none of the keys required to decrypt any of these fields.


All HTTPS traffic between ov mcp serve and api.opaquevault.com uses a post-quantum hybrid KEM:

client_x25519_private + server_x25519_public → shared_x25519
client_mlkem_private + server_mlkem_public → shared_mlkem
shared_secret = KDF(shared_x25519 XOR shared_mlkem)

Why hybrid: If X25519 is broken by a classical adversary, ML-KEM-768 still protects the session. If ML-KEM-768 is broken by a quantum adversary (harvest-now-decrypt-later), X25519 still protects the session. Both must be broken simultaneously to compromise transport security.

Why ML-KEM-768: NIST FIPS 203 standard (formerly Kyber-768). Recommended security level III — equivalent to AES-192. Implemented via cloudflare/circl in pure Go, no CGO.

No fallback. There is no X25519-only mode, no --no-pqc flag, no version negotiation that could downgrade to classical-only. PQC is on unconditionally from day one.


Every encryption operation generates a fresh 12-byte nonce via crypto/rand.Read. Nonces are never derived, never sequential, never reused. This is enforced by tests — two encryptions of the same value must produce different ciphertexts.


All cryptographic code lives in internal/crypto/:

FileContents
argon2.goKey derivation — DeriveKEK(password, salt)
envelope.goDEK generation, AES-256-GCM encrypt/decrypt
pqc.goML-KEM-768 + X25519 hybrid KEM
sanitize.goMemory zeroing helpers for DEKs and KEKs

Test coverage for internal/crypto/ is 100% — a hard requirement.