Encryption & Keys
Opake employs a hybrid encryption model: symmetric content encryption for speed, and asymmetric key wrapping for sharing and identity.
What actually happens #
Standard, audited cryptographic primitives. Here's what runs when you upload a file.
1. Content Encryption #
When you select a file to upload, your device generates a random 256-bit symmetric key (Content Key).
The file is then encrypted using AES-256-GCM. This algorithm is fast and handles arbitrary-size data efficiently. This produces the ciphertext blob that will actually be uploaded to your PDS.
// The core primitive.
pub fn encrypt_blob(
plaintext: &[u8],
key: &ContentKey
) -> Result<Vec<u8>, CryptoError> {
// Generates 12-byte random nonce
// Applies AES-GCM
// Returns ciphertext with appended authentication tag
}2. Key Wrapping (The Lockbox) #
We have the encrypted file, but how do we store the Content Key so that only you can decrypt it later? We wrap it.
Your Opake identity consists of a hybrid keypair: an X25519 half (classical, fast, well-understood) and an ML-KEM-768 half (post-quantum, lattice-based). We wrap the Content Key to both halves at once using a scheme called x25519-mlkem768-hkdf-a256kw-v2.
- We generate an ephemeral X25519 keypair and run ECDH against your X25519 public key → 32-byte shared secret.
- We run ML-KEM-768 Encaps against your post-quantum public key → 32-byte shared secret + a 1088-byte ciphertext.
- We feed both shared secrets into HKDF-SHA256, with the entire transcript (both ephemeral pubkeys and the ML-KEM ciphertext) committed in the salt — so an adversary who tries to flip the post-quantum half breaks the integrity check.
- We wrap the
Content Keyusing AES-256-KW with the resulting 32-byte key.
The result is a WrappedKey — a 1160-byte envelope: [X25519 ephemeral pub (32) ‖ ML-KEM ciphertext (1088) ‖ AES-KW wrapped (40)]. This lockbox is safe to store publicly while being streamed through the AT Protocol firehose: opening it requires breaking both X25519 and ML-KEM-768.
Influences #
Opake's primitives and patterns come from a few specific places:
- Signal Protocol — Curve25519 (X25519) for the classical half of identity. Used here for the same reasons Signal picked it: fast, well-understood, resistant to many side-channel attacks.
- ML-KEM-768 (NIST FIPS-203) — the post-quantum KEM standardized by NIST in 2024. Built on
Module-LWE; expected to remain secure against quantum attacks. Implementation:
libcrux-ml-kem, formally verified in F*. - age — the file-for-recipients model (one content key, wrapped to each recipient's public key) is borrowed directly. Same "static" encryption — nothing to keep in sync between sessions.
- Bindel-Brendel-Fischlin-Goncalves-Stebila (PQCrypto 2019) — the splice-resistance argument for the HKDF combiner. Including the full transcript in the salt means a flipped post-quantum half can't redirect the wrap to a different content key.
- Noise Protocol Framework — Opake isn't a messaging protocol, but the underlying composition (Diffie-Hellman + HKDF + AEAD) is the Noise recipe.
Where Do Keys Come From? #
Your keys are derived from a 24-word secret phrase generated when you first set up Opake. The same phrase always produces the same keys — this is how you can recover your identity on a new device without needing your old one. For the user-facing walkthrough (what to do with the words, how to back them up), see What your key actually looks like.
The derivation pipeline #
Turning 24 words into a working identity is a three-stage process; each stage uses a standard, audited primitive.
-
BIP-39 → 256 bits of entropy. The 24 words are drawn from the BIP-39 English wordlist: 2048 entries, 11 bits each, so 24 × 11 = 264 bits total. The last 8 bits are a checksum, leaving 256 bits of actual randomness — the same entropy budget as the AES-256 keys that get derived from it.
-
PBKDF2-HMAC-SHA512 → 512-bit master seed. The words (as UTF-8 bytes) are stretched through PBKDF2 with 2048 rounds and salt
"mnemonic"— the BIP-39 standard. The rounds are intentional cost: unnoticeable on a legitimate login, meaningful when an attacker is trying to run billions of guesses. -
HKDF-SHA256 → three keys. The 512-bit master seed is fed through HKDF-SHA256 three times, with different
infostrings:opake-v1-x25519-identity→ 32-byte X25519 private key (classical half of key wrapping)opake-v1-mlkem768-keygen→ 64-byte seed for ML-KEM-768 keygen (post-quantum half)opake-v1-ed25519-signing→ 32-byte Ed25519 signing key (indexer authentication + record signing)
The
infostrings provide domain separation: even with the same master seed, you can't substitute one key for another, and a future schema version (opake-v2-...) produces cleanly independent keys from the same words. ML-KEM-768 keygen is itself deterministic given a 64-byte randomness seed, so the entire identity is reproducible from the mnemonic.
The pipeline is deterministic: same 24 words, same master seed, same X25519 and Ed25519 keys, every time, on every device. That's what makes seed-phrase recovery possible.
The PBKDF2 salt is the fixed string "mnemonic" per the BIP-39 specification. Security rests
on the 256-bit entropy of the words themselves, which is already enough randomness to make
per-user salting redundant.
The three keys, and what they do #
| Key | Purpose |
|---|---|
| X25519 keypair | Classical half of encryption. The private half participates in ECDH to unwrap content keys sent to you. The public half is published on your PDS so others can wrap to you. |
| ML-KEM-768 keypair | Post-quantum half of encryption. The private half decapsulates the post-quantum component of every wrapped key. The 1184-byte public half is published on your PDS alongside the X25519 key. |
| Ed25519 keypair | Signing. The private half signs indexer auth tokens and (eventually) record envelopes. The public half is published alongside the encryption keys so services can verify signatures. |
All three public halves live in a single at.opake.publicKey/self record on your PDS — a
singleton, republished on every login so it stays current.
Where Are My Keys Stored? #
Your private keys never touch the network.
- On the Web: They are stored in your browser's
IndexedDB, accessible only to the Opake web app domain. - On the CLI: They are stored in
~/.config/opake/accounts/, guarded by strict0600UNIX file permissions.
Your seed phrase is shown once at setup and never stored. To learn how to back it up and recover, see What your key actually looks like. To transfer keys between devices without re-entering the phrase, read about Device Pairing.