- Rust 100%
| docs | ||
| src | ||
| tests | ||
| .gitignore | ||
| Cargo.lock | ||
| Cargo.toml | ||
| README.md | ||
unmtx
unmtx is a local Matrix E2EE proxy daemon for bots. Bots use the normal Matrix
Client-Server HTTP API, point their homeserver/base URL at unmtx, and do not
need to implement Matrix end-to-end encryption themselves.
bot -> http://127.0.0.1:8009 -> unmtx -> real Matrix homeserver
The design is proxy-first. unmtx does not implement a Matrix homeserver and
does not recreate general homeserver semantics. It forwards ordinary Matrix
endpoints unchanged, and only uses the Matrix Rust SDK where E2EE correctness
requires local device state: login/session capture, /sync, encrypted-capable
history, and plaintext room sends that must be encrypted before reaching the
upstream homeserver.
The listener is intentionally restricted to loopback IP literals. Do not expose
it on 0.0.0.0.
Trust Model
unmtx is designed for trusted bots running on the same host as the proxy. A
local process that can connect to the listener can use the proxied Matrix
session and can cause unmtx to send Matrix requests with that session's
authority.
There is no multi-tenant isolation, local authentication layer, browser-facing
CSRF protection, or remote-user access model. Run one proxy store per bot/device
identity and protect store_path as secret local state.
Configuration
Minimal unmtx.toml:
listen_host = "127.0.0.1"
listen_port = 8009
homeserver_url = "https://matrix.example.org"
store_path = "./unmtx-store"
secret_storage_key_file = "./recovery-key"
Put your Matrix recovery key, or Secret Storage passphrase, in the file named
by secret_storage_key_file. The file is read locally on startup/login and is
used to import cross-signing secrets and the Megolm backup key. Keep it private:
printf '%s\n' 'YOUR-RECOVERY-KEY' > ./recovery-key
chmod 600 ./recovery-key
CLI overrides:
unmtx --config unmtx.toml \
--homeserver-url https://matrix.example.org \
--store-path ./unmtx-store
homeserver_url is required. It must be an http:// or https:// URL with a
host and without credentials, query string, or fragment. A trailing slash is
normalized away. store_path defaults to ./unmtx-store.
unsafe_auto_confirm_verifications = true is optional and only for trusted
local deployments. Prefer secret_storage_key_file when you have a recovery
key.
Session And Key Storage
Start unmtx, then let the bot log in through the proxy using the normal Matrix
login endpoint:
POST /_matrix/client/v3/login
On successful upstream login, unmtx stores the returned user_id,
device_id, and access_token under store_path/session.json, restores a
Matrix SDK client for that device, and uses store_path/sdk-store for the SDK
persistent SQLite store.
unmtx-store/
session.json
sdk-store/
Back up store_path with the same care as a Matrix client profile. It contains
access tokens and Matrix E2EE state.
After login, unmtx imports secrets from secret_storage_key_file when set.
This is what lets the SDK enable key backup and decrypt encrypted rooms without
manual device verification.
If the upstream login succeeds before a local device exists but the SDK store
cannot be opened, unmtx moves that pre-session store aside as
sdk-store.failed.<pid>.<timestamp> and retries activation with a fresh SDK
store. Once session.json exists, the SDK store is considered bound to that
Matrix device and is not automatically replaced. A stored device whose SDK store
cannot be restored is a hard local error, because replacing it would lose the
crypto state needed to decrypt messages for that device.
Proxy Behavior
See docs/architecture.md for the detailed proxy boundary.
| Route class | Examples | Behavior |
|---|---|---|
| Generic client API | /_matrix/client/v3/profile/*, room metadata, aliases, membership, receipts, typing |
Forward upstream with hop-by-hop headers stripped and end-to-end headers such as Authorization preserved. |
| Media API | /_matrix/media/* |
Forward upstream unchanged. Encrypted attachment metadata belongs to room event content; media routes are not rewritten or decrypted. |
| Login/session | POST /login, POST /logout, POST /logout/all |
Forward upstream; successful login restores local SDK session, successful logout clears it. |
| Local identity | GET /account/whoami |
Answer from the stored session when loaded; otherwise forward upstream. |
| Sync | GET /sync |
Use Matrix SDK Client::sync_once so to-device keys, device lists, room state, and encrypted events update local crypto state before bot-visible JSON is returned. Events the SDK still cannot decrypt are returned as their original m.room.encrypted events so the bot can ignore them and advance since. |
| Message history | GET /rooms/{roomId}/messages |
Use Matrix SDK Room::messages; requires a local session and returns SDK-decrypted events where possible, preserving still-undecryptable chunks as m.room.encrypted. |
| Room send | PUT /rooms/{roomId}/send/{eventType}/{txnId} |
Use Matrix SDK Room::send_raw for message-like plaintext sends; preserve transaction IDs and cache successful responses. Already encrypted m.room.encrypted sends pass through. |
| Other local paths | anything outside /_matrix/client/ and /_matrix/media/ |
Reject with a Matrix-shaped M_NOT_FOUND. |
Incoming encrypted events are decrypted for the bot when the SDK has the needed
keys. If the SDK cannot decrypt an encrypted timeline or history event because a
room key may still be in-flight, unmtx performs short crypto warm-up syncs
that ignore room timeline data and retries decryption. If the key is withheld,
trust-gated, or still unavailable after retry, unmtx returns the original
m.room.encrypted event so the bot can ignore it without pinning /sync on the
same since token. Outgoing message-like sends fail closed with Matrix-shaped
errors when the SDK cannot identify the room, cannot prepare crypto state, or
cannot send.
Live E2EE Smoke Test
The ignored live test in tests/live_e2ee_pipeline.rs can be run manually
against a real homeserver account. Use disposable credentials and, for the send
check, an encrypted room the account has already joined:
UNMTX_LIVE_E2EE=1 \
UNMTX_LIVE_HOMESERVER_URL=https://matrix.example.org \
UNMTX_LIVE_USERNAME=bot-user \
UNMTX_LIVE_PASSWORD='secret' \
UNMTX_LIVE_ROOM_ID='!room:example.org' \
cargo test --test live_e2ee_pipeline -- --ignored --nocapture
UNMTX_LIVE_ROOM_ID is optional. Without it, the test logs in through unmtx
and checks SDK-backed /sync shape. With it, the test also sends an
m.room.message through the SDK send path and syncs again. Set
UNMTX_LIVE_SYNC_TIMEOUT_MS if the homeserver needs a nonzero sync wait.
Limits
unmtxis not a Matrix homeserver, multi-user reverse proxy, authorization gateway, or browser-facing service./syncand/messagesshaping targets common bot fields. Some optional upstream fields are not mirrored.- Historical decryption depends on available room keys. Configure
secret_storage_key_fileto let the SDK use Matrix key backup. --unsafe-auto-confirm-verificationsaccepts and confirms SAS device verifications without human comparison. This can make other devices treat theunmtxMatrix device as verified, but it deliberately bypasses the security purpose of SAS verification and should only be used for trusted local bot deployments.- Media routes do not decrypt encrypted attachments.