README.md

# Vodozemac

Elixir bindings to Matrix's Olm and Megolm cryptographic primitives,
backed by Element's [vodozemac][1] (Rust) — the modern successor to
the deprecated [libolm][3].

```elixir
def deps do
  [{:vodozemac, "~> 0.1"}]
end
```

> ⚠️ Pre-1.0. The surface is documented and tested, but signatures
> may shift before 1.0 — see the stability table below for the rows
> that are explicitly unstable.

## Scope

Olm and Megolm are the pairwise and group ratchets defined by the
[Matrix end-to-end encryption spec][2]. This library wraps **only**
those primitives. It does **not** speak HTTP, parse Matrix events,
or talk to Synapse. Callers are responsible for:

- talking to the homeserver (`/keys/upload`, `/keys/query`,
  `/keys/claim`, `/sendToDevice`, `/sync`)
- routing `m.room_key` / `m.room.encrypted` events
- persisting the serialized session state this library returns
- key backup transport

That boundary is deliberate so the same library can power a thin
client SDK, a server-side chat backend, a CLI testing tool, or an
appservice — none of which agree on the rest of the stack.

## Quick start

```elixir
# Long-term identity for a device.
account = Vodozemac.account_create()

# Identity keys to publish to the server.
{curve25519, ed25519} = Vodozemac.account_identity_keys(account)

# Generate ten unpublished one-time keys for the pre-key bundle.
{account, otks} = Vodozemac.account_generate_one_time_keys(account, 10)

# After `/keys/upload` succeeds, mark them published so future
# generate calls return only NEW ones.
account = Vodozemac.account_mark_published(account)

# Establish an outbound Megolm session for a room.
{session_id, session_key, outbound} = Vodozemac.outbound_group_session_create()
{ciphertext, outbound} = Vodozemac.outbound_group_session_encrypt(outbound, "hello")

# The corresponding inbound session is built from the session_key the
# sender shares (typically wrapped in an Olm m.room_key to-device event).
{^session_id, inbound} = Vodozemac.inbound_group_session_create(session_key)
{:ok, "hello", _index, _inbound} =
  Vodozemac.inbound_group_session_decrypt(inbound, ciphertext)
```

Every function that mutates state returns a fresh pickle. Callers
**must** persist the latest pickle — once you advance the ratchet,
the previous pickle can no longer decrypt the next message.

## Status of the surface

| Area                          | API                            | Stability     |
| ----------------------------- | ------------------------------ | ------------- |
| Account (identity + OTKs)     | `account_*`                    | stable        |
| Olm pairwise sessions         | `olm_*`                        | stable        |
| Inbound Megolm group session  | `inbound_group_session_*`      | stable        |
| Outbound Megolm group session | `outbound_group_session_*`     | stable        |
| Raw Ed25519 / Curve25519      | `ed25519_*` / `curve25519_*`   | stable        |
| SAS (emoji verification)      | `sas_*`                        | stable        |
| Pickle encryption key         | (not exposed)                  | **unstable**  |

The last row is the headline pre-1.0 gap: pickle bytes are wrapped
with vodozemac's default zero-key (no confidentiality at rest).
Callers must apply their own at-rest encryption layer until a
`pickle_key` parameter lands in a later release.

## Precompiled binaries

**0.1.0 is source-only.** Consumers need a Rust toolchain
(`rustup install stable`) on `mix deps.get`; the NIF compiles from
source via Rustler. Precompiled tarballs for the targets below
are scheduled for **0.1.1**:

| Target                       | Status              |
| ---------------------------- | ------------------- |
| `aarch64-apple-darwin`       | planned for 0.1.1   |
| `x86_64-apple-darwin`        | planned for 0.1.1   |
| `aarch64-unknown-linux-gnu`  | planned for 0.1.1   |
| `x86_64-unknown-linux-gnu`   | planned for 0.1.1   |
| FreeBSD                      | planned             |

Once 0.1.1 ships, the precompiled artifact for your target is
fetched automatically from the [sr.ht][src] release ref. Force a
local cargo build at any time with
`RUSTLER_PRECOMPILATION_FORCE_BUILD=1` before `mix deps.get`.

## Hacking

The repo lives at [https://git.sr.ht/~sbr/vodozemac][src]. Patches
welcome — send via `git send-email` to the [~sbr/public-inbox][list]
list, or open a ticket on the [tracker][trk].

```sh
git clone https://git.sr.ht/~sbr/vodozemac
cd vodozemac
mix deps.get
RUSTLER_PRECOMPILATION_FORCE_BUILD=1 mix compile
mix test
```

## License

The Elixir + Rust glue in this repository is licensed under
**BSD-3-Clause** — see [`LICENSE`](LICENSE).

The precompiled NIF binaries (the `.tar.gz` shipped to the sr.ht
release ref for each tagged version) statically link Element's
[vodozemac][1] (Apache-2.0) and a tree of transitive Rust crates.
Each tarball includes:

- `LICENSE` — this project's BSD-3-Clause text
- `LICENSE-APACHE` — full Apache-2.0 text covering vodozemac
- `THIRD-PARTY-NOTICES` — auto-generated inventory of every linked
  crate with its license text (built via
  [`cargo-about`](https://embarkstudios.github.io/cargo-about/))

The source-only Hex package does **not** bundle vodozemac — Cargo
resolves it from crates.io at build time on the consumer's machine —
so the Apache-2.0 redistribution obligation only applies to the
precompiled artifacts.

[1]: https://github.com/matrix-org/vodozemac
[2]: https://spec.matrix.org/v1.11/client-server-api/#end-to-end-encryption
[3]: https://matrix.org/blog/2024/08/libolm-deprecated/
[src]: https://git.sr.ht/~sbr/vodozemac
[list]: https://lists.sr.ht/~sbr/public-inbox
[trk]: https://todo.sr.ht/~sbr/vodozemac