> For the complete documentation index, see [llms.txt](https://hinkal-team.gitbook.io/hinkal/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://hinkal-team.gitbook.io/hinkal/technical-description/setup/encryption.md).

# Encryption

### Note Encryption

Within Hinkal Protocol, the note (UTXO) data created by a transaction is encrypted and published on-chain so that only the recipient's viewing key can decrypt it. This lets users privately and trustlessly detect their incoming notes by scanning published ciphertexts: a note belongs to them precisely when it decrypts and authenticates under their viewing key.

#### Overview

Hinkal uses **Hybrid Public Key Encryption (HPKE)** to accomplish this, instantiated through libsodium's *sealed box* primitive. Before describing the scheme in detail, here is how HPKE works at a high level:

The **sender**…

* generates an ephemeral, single-use asymmetric keypair and, from it, a symmetric encryption key;
* uses the recipient's public key to **encapsulate** that symmetric key so that only the recipient can **decapsulate** it. This mechanism is a Key Encapsulation Module (KEM);
* encrypts the message under the symmetric key using an **authenticated** encryption scheme, so any tampering with the ciphertext in transit is detected;
* publishes the encapsulated key together with the ciphertext.

The **recipient**…

* decapsulates the encapsulated key using their private key to recover the symmetric key;
* attempts to decrypt and authenticate the ciphertext with that key. If authentication fails, they reject the message — which is also the signal that the note was not addressed to them.

Public-key encryption lets the sender encrypt to a recipient without any shared secret, but it is slow; symmetric encryption is fast but needs a pre-shared secret. HPKE combines both, which matters because clients must encrypt and scan many notes on resource-constrained devices while having no trustless channel for sharing secret keys.

#### Detailed Description

Hinkal realizes this scheme with libsodium: `crypto_box_seal` for encryption and `crypto_box_seal_open` for decryption. These operate over **Curve25519** and use **XSalsa20-Poly1305** for authenticated symmetric encryption. First, some definitions:

* `B^N` is the set of all `N`-byte strings; `B` is the set of byte strings of any length.
* `G`: the Curve25519 base point. All curve operations are X25519 scalar multiplications.
* `R ∈ G`: the recipient's **encryption (viewing) public key**, `R = rvk · G`, where `rvk` is the recipient's private viewing key. As described in *Key Derivation*, the viewing keypair is generated with `crypto_box_seed_keypair` seeded from the recipient's nullifying key.
* `(esk, epk)`: an ephemeral, single-use Curve25519 keypair generated by the sender, with `epk = esk · G`. The `epk` is the encapsulated value — the sender's half of a non-interactive Diffie-Hellman exchange.
* `sharedSecret ∈ G`: the Diffie-Hellman shared point established between sender and recipient.
* `BLAKE2b(·) → B^24`: the hash libsodium uses to derive a deterministic 24-byte nonce.
* `KDF(sharedSecret) → key`: the symmetric key derivation libsodium applies to the DH point inside `crypto_box` (HSalsa20), producing the XSalsa20-Poly1305 key.
* `XSalsa20Poly1305`, with:
  * `seal(key, nonce, plaintext) → B`: authenticated encryption of `plaintext`;
  * `open(key, nonce, ciphertext) → B ∪ ⊥`: authenticated decryption, returning an error `⊥` if the Poly1305 tag does not verify.

**Plaintext.** The note plaintext is the serialized UTXO: the `amount`, `erc20TokenAddress`, `randomization`, `stealthAddress`, `timeStamp`, the `isNewStyle` flag, the curve point `H0 = (H0[0], H0[1])`, and an optional Solana `mintAddress`, concatenated as `0x`-prefixed byte strings.

**Encryption** takes as input the note plaintext `msg` and the recipient's viewing public key `R`, and proceeds as follows:

1. `(esk, epk) ← keypair()` — generate the ephemeral keypair, `epk = esk · G`.
2. `sharedSecret ← esk · R` — the `g^{ab}` term of Diffie-Hellman (`a = esk`, `b = rvk`).
3. `nonce ← BLAKE2b(epk ∣∣ R)` — a deterministic 24-byte nonce bound to both public keys.
4. `key ← KDF(sharedSecret)` — derive the symmetric key.
5. `ciphertext ← XSalsa20Poly1305.seal(key, nonce, msg)`.
6. return `(epk ∣∣ ciphertext)` — the encapsulated key prepended to the ciphertext.

**Decryption** takes as input the published `(epk ∣∣ ciphertext)` and the recipient's viewing keypair `(R, rvk)`, and proceeds as follows:

1. parse `epk` from the first 32 bytes of the input.
2. `sharedSecret ← rvk · epk` — the recipient's side of Diffie-Hellman; `(esk · G) · rvk = esk · rvk · G`, the same point computed in step 2 of encryption.
3. `nonce ← BLAKE2b(epk ∣∣ R)`.
4. `key ← KDF(sharedSecret)`.
5. `plaintext ← XSalsa20Poly1305.open(key, nonce, ciphertext)`.
6. if `plaintext = ⊥`, reject the message (the note is not addressed to this recipient, or was tampered with).
7. otherwise, parse the UTXO fields from `plaintext` and return the recovered note.

Because the symmetric key is bound to a fresh ephemeral `epk` for every note and is authenticated with Poly1305, ciphertexts are unlinkable to each other and to the recipient, and only the holder of `rvk` can reproduce the shared secret needed to open them. Trustless detection follows directly: a client simply attempts `crypto_box_seal_open` on each published ciphertext, keeping the notes that authenticate and discarding the rest.
