> ## Documentation Index
> Fetch the complete documentation index at: https://docs.oximail.ch/llms.txt
> Use this file to discover all available pages before exploring further.

# Encryption at rest

> The LUKS-style at-rest model: what is encrypted, the key hierarchy, SQLCipher for the database, what a backup does and does not contain, key rotation and the escrow drill, and crypto-erasure.

Everything OxiMail stores — message blobs, attachments, chat content, calendar payloads, and the SQLite database itself — is encrypted on disk. The model to hold in mind is **LUKS, not end-to-end**: like an encrypted disk, the server can decrypt content at read time for an authenticated user; unlike end-to-end encryption, losing a user's password never makes their mail unreadable, and IMAP/CalDAV/webmail all keep working.

## What the model protects — and what it does not

**Protected**: a stolen disk, a stolen backup of the blob directory or database file, a decommissioned drive, an attacker with offline access to the storage. Without the key material, blobs are ciphertext and the database is a SQLCipher-encrypted file.

**Not protected**: a fully compromised *running* server (it holds the keys to do its job), or a malicious administrator with access to the key material. Those threats belong to other layers — access control, the [security hardening](./operations#security-operations), and physical custody.

## The key hierarchy

Keys wrap keys, so rotation and erasure act on small objects, never on terabytes of content:

1. **Tenant master key** — one per organization. It wraps everything below it.
2. **Per-account keypairs** (X25519) — each account's content keys are wrapped for its keypair; blob content is encrypted with XSalsa20-Poly1305.
3. **Tenant-wide symmetric keys** (AES-256-GCM) — for content that is shared organization-wide by nature (room attachments, shared blobs) rather than owned by one account.

Two properties are deliberate:

* **Account keys are wrapped with the tenant master key, never with the user's password.** A password-derived wrap would make blobs undecryptable whenever the server does not hold the password at read time — which is exactly the IMAP, CalDAV, and background-worker case. Password changes therefore never re-encrypt anything.
* **Blobs are self-describing** (ADR-096): each carries an encryption header naming its scheme, so the store never guesses how to decrypt.

Every blob is content-addressed by the SHA-256 of its **cleartext** (deduplication happens before encryption, within the organization only — cross-organization dedup does not exist, by design).

**In a shared mailbox, the key follows the effective account, not the caller.** A secret key only ever decrypts its own account's blobs, so anchoring the choice on the authenticated principal broke sharing in both directions: a grantee could list a shared mailbox and read subjects but never open a body, and a message a grantee filed into someone else's mailbox was encrypted for the grantee, leaving the owner unable to read it in their own mailbox. Both now resolve on the **effective** account — the one a validated grant produced, which differs from the caller only after `resolve_shared_context` has accepted the grant. This widens no authorization: impersonation is not representable, and the per-object gate in the handlers remains the authority on what is served. Capabilities stay resolved on the authenticated caller.

The read path enforces one more discriminator: a cross-account grantee or an anonymous share link can only decrypt a blob referenced by its own **projection** (files vs mail vs avatar) under the resolved account — so even if a route's authorization check were bypassed by a bug, the crypto layer refuses a blob from another projection. Defense in depth at the key layer, not just the ACL layer.

## The database: SQLCipher

The structured metadata (folders, headers, flags, the change log) lives in SQLite, encrypted with **SQLCipher**:

```toml theme={null}
[storage]
encrypted = true      # the default
```

The database key lives in the key material under `/etc/oximail`; the wizard provisions it. A copied `data.db` without that key is noise.

**Which SQLCipher, and how to check.** The current line ships **SQLCipher 4.14.0 over SQLite 3.51.3**, statically linked into the binary. That matters more than it looks: the engine's own CVEs only reach an encrypted database through a SQLCipher release, so a build that pins an old one is quietly carrying every SQLite fix it has missed. Ask the running binary rather than a manifest:

```bash theme={null}
oximail sql "PRAGMA cipher_version"
```

**The shipped binary depends on no system crypto library.** It carries its own, and the deploy path now proves it instead of assuming it: a build whose `ldd` output mentions a system TLS, crypto or SQLite library is rejected before it reaches a host. The property used to hold by an accident of link order, which is exactly the kind of thing that breaks silently on an unrelated dependency change.

## Backups carry no keys

By design (ADR-102), a backup of the database and blob directory **does not include the key material** — a stolen backup is therefore not a data breach. The corollary is operational, and absolute:

<Warning>
  Back up `/etc/oximail` (config, DKIM keys, **key material**) separately from the data, and store it with the same care as a disk-encryption recovery key. Data backups without the key backup are ciphertext forever. `oximail verify-backup` checks your backups before you need them.
</Warning>

## Key operations

```bash theme={null}
oximail key rotate ...        # rotate wrapping keys (re-wraps key objects, not content)
oximail key escrow ...        # export a sealed escrow of the key material
oximail account regenerate-key --email <e> --yes   # DESTRUCTIVE — see below
```

* **Rotation** re-wraps the small key objects under a new master key; content blobs are untouched, so rotation is cheap at any mailbox size (key/content separation).
* **Escrow** produces a sealed copy of the key material for custody outside the machine. Both `rotate` and `escrow` **hard-gate on an escrow drill**: the command refuses to proceed until the on-disk escrow has been proven restorable — an escrow you never tested is not an escrow.
* **`account regenerate-key`** mints a fresh keypair for one account and makes **every blob encrypted under the old one permanently unreadable**. It exists for deliberate recovery scenarios; the audit phase counts and shows the affected blobs before asking for confirmation. Missing-key conditions at runtime always fail loud (ADR-050) and point the operator here — the server never silently serves or stores cleartext in their place.

## Crypto-erasure

Deleting an account drops its key material: every blob of the account becomes undecryptable at that instant, without touching the blob files themselves. This is what makes [GDPR erasure](./compliance) tractable at mailbox scale — the sweep deletes rows and index documents, and the ciphertext left on disk is noise. The one store with no keys to drop, the full-text search index, is swept explicitly.

## What this is not

* **Not end-to-end encryption.** The server decrypts for authenticated reads. E2E for specific surfaces is a separate concern with separate trade-offs (searchability, DAV bridges, server-side filtering all stop working under E2E).
* **Not filesystem encryption.** It composes fine with LUKS/dm-crypt underneath, but does not require it: the application-level scheme protects backups and copied files even off the encrypted volume.
