# ADR-010: SQLCipher for message history (crypto provider = vendored OpenSSL)

## Status

Accepted

## Date

2026-06-03

## Context

Message history was stored as one sealed file per contact (`MessageLog` sealed whole with
ChaCha20-Poly1305 under the device key), re-encrypted in full on **every** append — O(N²) over a
conversation, and the whole-file read is needed for search/delete/prune. The documented plan
(ADR-001, `docs/ARCHITECTURE.md`, `docs/SECURITY.md`) always specified **SQLCipher (AES-256)** for
storage at rest, with PRAGMAs `kdf_iter>600k`, `cipher_memory_security=ON`, `secure_delete=ON`,
`trusted_schema=OFF`. This ADR records moving message history onto SQLCipher and the contested choice
of crypto provider.

The tension: the project ships **zero OpenSSL** on the transport side (TLS uses rustls, deliberately).
SQLCipher needs a C crypto provider — OpenSSL or LibTomCrypt. Gemini recommended LibTomCrypt to keep
OpenSSL out. However, the project's **own CI-gate task (SR-2026-05-30-008) lists verifying
`OpenSSL>=3.5.5`** — i.e. the plan already anticipates OpenSSL entering the dependency tree via
SQLCipher. The "no OpenSSL" rule was scoped to the TLS path, not at-rest storage.

## Decisions

1. **Message history → SQLCipher** (`rusqlite` `bundled-sqlcipher-vendored-openssl`, always-on), in
   `pvtcoms-<profile>-history.db`. The whole DB (schema + indexes + rows) is page-encrypted, so unlike
   a plain DB it leaks **no metadata**, and unlike the old whole-file seal it appends/queries in place.
   Scope is **history only** — contacts/identity/invites stay as the existing small sealed files (they
   don't suffer the O(N²) hot path). *(Claude final call; Gemini consulted.)*
2. **Crypto provider = vendored OpenSSL**, not LibTomCrypt. *Why:* it cross-compiles cleanly to
   `x86_64-pc-windows-gnu` from Linux (verified), it is statically vendored (no system dependency), and
   the plan already expects OpenSSL via SQLCipher (the CI gate above). The OpenSSL here does **local
   AES on the DB only** — it never parses hostile network input, so its real attack surface is far
   smaller than the TLS use the project avoided. *Cost:* ~5 MB larger exe (13→18 MB), slower cold
   build. **LibTomCrypt remains a future surface-reduction option** if the OpenSSL footprint becomes a
   concern.
3. **Raw key, no KDF.** The device storage key (32 bytes, already high-entropy, from the
   DPAPI/dev keystore) is passed as `PRAGMA key = "x'<hex>'"`, which bypasses PBKDF2 (so `kdf_iter` is
   moot for raw keys). `cipher_memory_security=ON`, `secure_delete=ON`, `trusted_schema=OFF`,
   `journal_mode=WAL`. Opening **fails hard** if the key is wrong / file corrupt (never operate on a
   mystery store).
4. **Migration is data-safe + idempotent.** Legacy sealed history files are imported once; the rows and
   a `migrated_contacts` flag are written in the **same transaction** (atomic), and the old file is
   removed only after a verified commit. A present flag → skip + clean up; an absent flag → a prior
   attempt rolled back → re-import safely (no duplicates, no loss, even if new messages arrived after a
   failed attempt). _(Three Gemini BLOCKs on data-loss/duplication paths found + fixed before ship.)_

## Consequences

- Large histories no longer rewrite the whole file per message; search/delete/prune are indexed.
- `+OpenSSL` in the dependency tree (vendored, static) — the CI security gate should pin/verify its
  version (SR-2026-05-30-008). An eventual move to LibTomCrypt would drop it.
- Other platforms (macOS/Linux) build the same SQLCipher path; only the keystore differs.
