# End-to-end encryption

How to send and receive encrypted Matrix messages from R, and what each step does on the wire. Code blocks here are display-only: the flow needs a homeserver, a second device, and the `mx.crypto` package (which needs a Rust toolchain to build).

`mx.crypto` provides the cryptographic primitives; `mx.client` keeps it optional (Suggests) and only calls it from E2EE entry points, so plaintext Matrix clients install and run without Rust.

## Security model

Read this first; it frames what the rest of the vignette delivers.

- Trust on first use. Device keys come from `/keys/query` and are used as-is. There is no cross-signing trust store yet, so a malicious homeserver could substitute device keys on first contact. (`mx.crypto` ships the verification primitives, `mxc_verify_device_keys()`; wiring a trust store is future work.)
- No key requests or forwarded keys. A device that missed the original `m.room_key` cannot ask peers to re-share it.
- No SAS (emoji) verification.
- Local-key storage. Ratchet state is pickled with a locally stored 32-byte key (file mode 0600). That guards against casual inspection; it is only as strong as access to the local filesystem. No passphrase or hardware backing.

This matches what bots and controlled deployments need. For human-grade verification flows, watch the `mx.crypto` roadmap.

## The pieces

Matrix encryption uses two ratchets, both provided by `mx.crypto` (wrapping Matrix.org’s `vodozemac`):

- Olm encrypts 1:1 between two devices. It is expensive to set up (a key agreement per peer device) and is used only to deliver secrets, chiefly Megolm session keys.
- Megolm encrypts room messages. One outbound session per room you send to; everyone you have shared its key with can decrypt.

So sending one encrypted room message means: have an Olm session with each recipient device, send each of them your Megolm session key over Olm (as to-device messages), then encrypt the actual message with Megolm. mx.client orchestrates all of that; after one-time setup, a send is one call and a receive is one call.

## Setup: a device with published keys

A Matrix device (a login session) gets long-lived identity keys plus a pool of one-time keys others use to open Olm sessions with it. The crypto store persists all of it (pickled with a locally stored 32-byte key, mode 0600) so the device identity survives restarts.

    library(mx.client)

    client <- mx_client_load(app = "myapp")    # see mx_client_configure()

    store <- mx_crypto_store_dir("myapp")      # under tools::R_user_dir()
    acct <- mx_crypto_account(store)           # load or mint identity keys
    mx_crypto_publish_keys(client, acct, store, n_otks = 50L)

`mx_crypto_publish_keys()` builds the signed `device_keys` object, signs and uploads one-time keys (`/keys/upload`), marks them published, and saves the account. Run it again whenever the server’s `one_time_key_counts` runs low.

## Sending

    sessions <- mx_crypto_sessions_load(store)

    res <- mx_send_encrypted(
        client, acct, sessions,
        room_id = "!abc:example.org",
        content = list(msgtype = "m.text", body = "the eagle lands at noon"),
        store_dir = store,
        member_ids = c("@friend:example.org")
    )
    res$event_id

On the wire, `mx_send_encrypted()`:

1. `/keys/query` — discovers the members’ devices and identity keys (`mx_crypto_known_devices()`).
2. `/keys/claim` — claims a one-time key for each device we have no Olm session with (`mx_crypto_claim_otks()`), then opens the sessions.
3. `/sendToDevice` — Olm-encrypts the room’s Megolm session key to each device that hasn’t received it (`m.room_key` inside `m.room.encrypted`).
4. `/send` — Megolm-encrypts the actual content and posts the `m.room.encrypted` room event.
5. Persists the updated sessions to the store.

Steps 1–3 only do work the first time. Later sends to the same room are a single Megolm encrypt + send.

## Receiving

    res <- mx_sync_update(client, timeout = 30000L)

    my_curve <- mx.crypto::mxc_account_identity_keys(acct)$curve25519
    out <- mx_crypto_process_sync(acct, sessions, res$sync, my_curve,
                                  self_id = client$user_id)
    sessions <- out$sessions
    mx_crypto_sessions_save(sessions, store)

    for (ev in out$events) cat(ev$sender, ":", ev$body, "\n")

`mx_crypto_process_sync()` makes two passes over the sync response:

1. To-device events: Olm-decrypts anything addressed to this device’s Curve25519 key. Each recovered `m.room_key` becomes an inbound Megolm session, stored under `room_id|session_id`.
2. Room timelines: decrypts every `m.room.encrypted` event whose session is known, returning records in the same shape as `mx_extract_text_events()` — `room_id`, `event_id`, `sender`, `is_self`, `body`, `msgtype`, `mentions`.

Events whose keys haven’t arrived yet are skipped, not errored; they decrypt on a later pass once the to-device message lands.

## Persistence

Everything stateful lives in the crypto store directory:

File | Holds
--- | ---
`pickle.key` | the locally stored 32-byte key the pickles are encrypted with
`account.pickle` | device identity (Curve25519 + Ed25519 keys, OTK state)
`sessions.json` | pickled Olm sessions and Megolm in/outbound sessions

`mx_crypto_sessions_save()` / `mx_crypto_sessions_load()` round-trip the session set, so an established room key keeps decrypting across process restarts. One caution: the account binds to the config’s `device_id`. A homeserver will reject re-publishing different identity keys for an existing device, so don’t delete the store while keeping the login.

## Marking a room encrypted

Encryption is a room state event. Any member with permission can set it (this is one-way; rooms don’t downgrade to plaintext):

    s <- mx_client_session(client)
    mx.api::mx_set_state(s, room_id, "m.room.encryption",
                         list(algorithm = "m.megolm.v1.aes-sha2"))

The boundaries of what this layer does are in the Security model section at the top.