# Faction — we can’t read your messages.

[Faction Privacy First](/) [Open Faction](https://app.faction.chat) Technical · v0.3 alpha

# Whitepaper
Last updated: 2026-05-18

Faction is a privacy-first group-chat platform built on a **zero-trust-server** model. The product claim is simple: *the server cannot read your messages*. This document describes the primitives, libraries, and trust boundaries that make that claim cryptographic rather than promotional.

## Trust model

The Faction server is treated as hostile. Every assumption about what the server can see is constrained to what it provably must see to route and order ciphertext.

- End-to-end encryption is on by default for server channels and 1:1 direct messages. There is no plaintext fallback — if cryptographic initialisation fails, sending is blocked rather than degrading silently.
- Private keys never leave the device. They are generated, used, and destroyed locally. The server stores public halves only.
- Plaintext is never stored, never logged, and never transmitted for E2EE channels. Message rows in the database carry a ciphertext column and routing metadata — nothing else.
- Plugin channels and webhook-posted messages are explicitly non-E2EE. The UI displays a mandatory warning banner so trust boundaries are visible at point of use. This is a feature, not a leak.

## The cryptographic stack

Faction does not invent primitives. Each layer of the system uses a well-known protocol implemented by an audited upstream library, exercised inside a process the server does not control.
Layer Protocol Library Where it runs **Group channels** MLS — RFC 9420 `openmls` 0.8 Tauri (Rust) on desktop · WASM in browser **Direct messages** Signal Protocol — X3DH + Double Ratchet `vodozemac` Tauri (Rust) on desktop · WASM in browser **Password login** OPAQUE PAKE — RFC 9497, ristretto255 + TripleDH `opaque-ke` v3 Client + dedicated OPAQUE service **Voice & video** SFrame, keyed from the MLS epoch exporter `faction-mls` (in-repo crate) Client; SFU relays opaque frames **Bulk encryption** AES-256-GCM · ChaCha20-Poly1305 `aes-gcm`, `chacha20poly1305` Attachments, frames, local cache **Local cache (desktop)** SQLCipher — AES-256-CBC + HMAC-SHA-256 `libsqlcipher` Tauri process, key in OS keychain **Local cache (browser)** AES-256-GCM over IndexedDB; non-extractable WebCrypto key WebCrypto + HKDF-SHA-256 Tab memory; key derived from OPAQUE export **Update signing** Ed25519 signature, verified before install `ed25519-dalek` Client; public key baked into binary

## What the server sees and what it cannot

### What it sees

- That an account exists, and when it was created.
- Server and channel membership (which users belong to which groups).
- The fact that some message went between two parties at a given time.
- Ciphertext blobs and routing metadata (recipient device IDs, MLS epoch numbers).
- Public identity material — Ed25519 signing keys, X25519 prekey publics.
- Mentions and channel IDs the server needs to route notifications.
- Plaintext of plugin and webhook channels — flagged in the UI as non-E2EE.

### What it cannot

- Plaintext of E2EE messages. Group channels are encrypted with MLS; DMs with the Signal Protocol. The server stores ciphertext, never plaintext.
- Voice and video media. SFrame keys derive from the MLS group exporter on the client; the SFU forwards opaque encrypted frames.
- Passwords. OPAQUE never transmits the password. The login exchange completes against a server-stored OPAQUE record that cannot be inverted into the original password.
- Private keys. Identity, signed-prekey, one-time-prekey, and MLS leaf-node secret keys are generated and stored on the device — desktop in the OS keychain, browser as non-extractable WebCrypto keys held only in tab memory.
- Decrypted message content in logs. The server-side logger applies field redaction on emit; the Rust crypto commands carry explicit no-log invariants on every decrypt path.

## Channel types and their trust boundaries
Channel type Encrypted? Readable by UI indicator **Server text channel** Yes — MLS Channel members only Lock icon **Direct message** Yes — Signal The two participants Lock icon **Voice / video call** Yes — SFrame Call participants Lock icon **Plugin channel** **No** Plugin author + server + members **"Plugin — not E2EE" banner** **Webhook-posted message** **No** Webhook sender + server + members **"Webhook — not E2EE" tag** Non-E2EE by design Plugin channels and webhook-posted messages cannot be E2EE because the server (or a plugin author's external service) is one of the producers. Faction does not hide this — the UI marks every such channel and message with a visible warning so the trust model is never ambiguous.

## Where the cryptography lives in the codebase

The point of an audit-friendly codebase is that you can grep for the claims. The table below maps each privacy claim to the file (and, where useful, the function) that implements it.
Claim Where to verify it Server stores ciphertext only for E2EE channels `faction_api/src/domains/messages/MessagesRepository.ts` — the row interface carries `ciphertext: Buffer`, no plaintext column. Voice is end-to-end encrypted `faction_media/crates/faction-mls/src/sframe.rs` — `derive_sframe_key` drops out of the MLS epoch exporter; `apps/client/src-tauri/src/commands/voice.rs` uses it for the encrypt and decrypt path. OPAQUE: password never reaches the server `faction_media/crates/faction-opaque-server/` for the server-side ristretto255 handshake; `apps/client/src-tauri/src/commands/opaque.rs` for the client flow. Private keys never leave the device Every command under `apps/client/src-tauri/src/commands/` that touches a private key writes to SQLCipher or the OS keychain — never over the wire. Plugin channels are visibly non-E2EE `apps/client/src/lib/modules/messaging/components/*Banner*.svelte` — the warning banner is enforced at the channel-host component level. Update signature is verified before install `apps/client/src-tauri/src/commands/updater.rs` + the release pipeline doc. No decrypted content in logs Grep `logger` calls inside any `decrypt_*` or `mls_*` Rust command — there should be none that log message bodies. The Rust commands also carry explicit `// SECURITY:` markers on every decrypt path.

## What is not yet shipped

Faction is v0.3 alpha. Two cryptographic improvements are on the runway and will land before general availability:

- Verified-sender UI. The MLS leaf credential is verified on every message, but the chat header doesn't yet surface that verification separately from the displayed name. Landing in the next release.
- MLS state restore on backup. The encrypted-cache backup flow currently restores message history but does not yet re-import MLS group snapshots; new devices have to re-join groups instead of resuming. Tracking for v1.1.
Audit trail Faction is source-available under PolyForm Noncommercial 1.0.0 so the claims on this page can be
independently verified. See the [security page](/security) for vulnerability reporting,
severity classifications, and the disclosure timeline. See the [transparency page](/transparency) for what metadata the server retains and how requests are handled.
