Encryption
How new notes are encrypted to their recipients, how viewing-key shares reach the enclaves, and how the sender record is encrypted inside the proof.
Umbra encrypts new notes to their recipients with ML-KEM-768 and AES-256-GCM, and sends viewing-key shares and fee openings to its enclaves under a hybrid of ML-KEM and X25519. The sender record is encrypted inside the zero-knowledge proof with a Poseidon keystream, so the proof guarantees it matches the transaction.
Note encryption
Every value transaction produces three new notes, and each is encrypted into a fixed-size blob of 1,352 bytes. To encrypt a note, the sender's device:
- encapsulates to the recipient's ML-KEM-768 public key, which yields a 1,088-byte ciphertext and a 32-byte shared secret;
- derives two values from the shared secret with SHAKE256, each under its own label: a 4-byte view tag, and a 32-byte AES-256-GCM key that also takes in the note's commitment;
- encrypts the note's 232-byte plaintext with AES-256-GCM under that key, with a 12-byte nonce and the note's commitment as associated data, which adds a 16-byte authentication tag.
The chart below shows how the pieces come out of those three steps.
flowchart LR pk["Recipient's ML-KEM key"] --> kem["Encapsulate"] kem --> ct["KEM ciphertext"] kem --> ss["Shared secret"] ss --> kdf["SHAKE256"] kdf --> tag["View tag"] kdf --> aes["AES-256-GCM"] note["Note plaintext"] --> aes aes --> enc["Encrypted note and tag"]
Each input to SHAKE256 starts with the output length, the protocol name umbra-note-seal-v0 and the suite name shake256+aes-256-gcm+ml-kem-768, then the label and the values, each prefixed with its length. The separate labels keep the two outputs independent, so publishing the view tag reveals nothing about the key. The view tag takes in only the shared secret, so a recipient can check it before reading any commitment.
The blob is those outputs in a fixed order: the view tag, the ML-KEM ciphertext, the nonce, the encrypted note and the authentication tag. Every blob has the same size whatever the note holds, so its length says nothing about it. The SDK reference carries the exact encoding.
The plaintext holds 12 of the note's 13 fields. The owner commitment is left out because the recipient can rebuild it from their own identity, and a spend proof derives it from the spender's identity rather than taking it as a witness, so a copy in the blob would never be used. The insertion slot and position are also absent, because the chain stamps them when it appends the note. Using the note commitment as associated data ties each blob to one note, so a blob cannot be moved to a different commitment and still open.
The encapsulation is derandomised: its random input is derived from the transaction's keys, not drawn fresh, so the sender can reproduce every blob. Dummy output slots are encrypted too, to real ML-KEM keys generated for the purpose and then discarded, so all three blobs look alike whether or not a slot carries value.
Where encrypted notes live
Encrypted notes are not stored on chain. The transaction carries a memo digest, a Keccak-256 hash whose preimage includes a root over the three blobs, and the memo digest is inside the payload every attester signs. The data endorser checks that the blobs it is given hash to that memo digest, archives them, and only then signs. The indexer publishes the archive for recipients to download. See receive and sync.
Finding your notes
A recipient cannot tell which blobs are theirs without trying. For each blob, their device decapsulates the ML-KEM ciphertext with its secret key, derives the view tag from the shared secret and compares it with the published one. A mismatch means the blob is someone else's, and the device skips the AES-GCM step. The view tag saves that step, not the decapsulation: every blob still costs one ML-KEM decapsulation, and about one blob in 2³² that is not yours passes the tag check and then fails to open.
Why ML-KEM-768
Encrypted notes are published for anyone to download, and a copy, once taken, can be kept forever. An attacker who later has a quantum computer large enough to break elliptic-curve cryptography could decrypt anything protected only by it; this is the harvest-now, decrypt-later threat. ML-KEM, standardised by NIST in FIPS 203, is built on lattice problems for which no such quantum attack is known, and ML-KEM-768 is its parameter set for NIST security category 3.
Note encryption uses ML-KEM-768 alone, without a classical key exchange combined with it. If ML-KEM-768 itself were broken, even by classical means, note encryption would have no second layer.
The recipient's key
Umbra's clients derive your ML-KEM-768 keypair deterministically from your account signature, like every other key, using the FIPS 203 key generation from a seed; the protocol does not require it. The public key is sent to the data endorser at registration and archived, so senders can fetch it. Its Keccak-256 hash is bound into slot 6 of your identity commitment (sometimes called the user commitment) and published in the registration transaction. The slot itself is blinded, so a sender checks a fetched key against the hash in that public registration payload, not against the commitment. See your keys.
Enclave transport
Your viewing-key shares and a transaction's fee openings are sent to enclaves, and they use a hybrid of a post-quantum and a classical key exchange. Shares for the guardian nodes travel under ML-KEM-1024 combined with X25519. Fee openings for the fee enclave travel under ML-KEM-768 combined with X25519. In both cases the combined key encrypts the payload with AES-256-GCM.
The two shared secrets are combined with KMAC256 over a transcript of the ciphertexts and public keys, so the combined key stays out of an attacker's reach as long as either half holds. A future quantum computer would break X25519 but not ML-KEM; an unexpected flaw in ML-KEM would leave X25519 standing. This lane is hybrid while note encryption is not, and the security levels page compares what each would withstand.
Each guardian node publishes its own transport key, and each share is encrypted to exactly one node. A share travelling to one node is unreadable to the others and to anyone watching the traffic, so each node receives only its own share.
The sender record, encrypted in the circuit
Every value transaction carries a sender record: 19 field elements describing it from the sender's side. The first seven are the sender's identity commitment and the token and amount of each of the three spent notes. The other twelve are four per output: the owner (two values, since an address owner takes two halves), the token and the amount.
The record is encrypted inside the transaction proof. The circuit derives the per-transaction viewing key from your outgoing viewing key, the transaction's three nullifiers and a public random value. It runs the Poseidon permutation keyed by that viewing key to produce a keystream, one field element per record lane, and adds each keystream element to its lane in the field. A second keystream, from a key derived the same way under another domain, blinds a commitment to each keystream element. The ciphertexts and these lane commitments are folded into the privacy artifacts root the proof is bound to, and the data endorser archives them.
Because the circuit computes the ciphertext, the record is proved to describe the transaction it travels with. A record encrypted outside the proof could say anything.
Anyone holding the per-transaction viewing key can decrypt the record. You can, because you can re-derive the key from your outgoing viewing key and the transaction's public values.
The construction has one requirement the chain cannot enforce. The encryption is additive, so if two records were ever encrypted under the same per-transaction viewing key, subtracting one ciphertext from the other would cancel the keystream and expose the difference between the plaintexts. That could happen if a spend were rebuilt with the same inputs and the same random value but different outputs. Your device must draw a fresh random value for every attempt and never reuse one.
Related
Last updated