docs: document two-key privilege model

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-22 23:11:18 +01:00
parent 456e15a2f8
commit add9515b5c
5 changed files with 163 additions and 66 deletions
+52 -17
View File
@@ -62,23 +62,49 @@ The binary is organized into independently testable packages:
human-readable/TUI).
### Trust boundary
- The agent invokes only the **agent commands** (Section 7.1).
- `EMCLI_KEY` is supplied by the environment/orchestrator that launches `emcli`, never as
an argument the agent constructs. The agent has no command that reveals secret values.
- All policy decisions happen inside `emcli`; the agent cannot bypass them because it has
no other path to the mail servers.
Two keys enforce a hard privilege split — this is not convention; it is structurally enforced by
the DEK-wrapping scheme:
- **`EMCLI_ADMIN_KEY`** — base64-encoded 32-byte key held by the human operator. Authorises ALL
commands. Admin commands unwrap the DEK from the `dek_wrap_admin` slot only; there is no fallback
to the agent slot. A process holding only `EMCLI_KEY` cannot run admin commands.
- **`EMCLI_KEY`** — base64-encoded 32-byte key supplied to the agent orchestrator. Authorises agent
commands (`list`, `get`, `search`, `ack`, `send`, `doctor`) only. `EMCLI_ADMIN_KEY` is a superset:
a process with only the admin key can also run agent commands.
- Agent commands use `EMCLI_KEY`; if only `EMCLI_ADMIN_KEY` is set, they fall back to it.
If neither key satisfies the required slot, `emcli` exits with:
`emcli: this command requires EMCLI_ADMIN_KEY (admin privilege)`.
- `EMCLI_KEY` is supplied by the orchestrator that launches `emcli`, never as an argument the agent
constructs. The agent has no command that reveals secret values.
- All policy decisions happen inside `emcli`; the agent cannot bypass them because it has no other
path to the mail servers.
## 5. Configuration & secrets
- **Encryption key:** `EMCLI_KEY` env var, a base64-encoded 32-byte key (AES-256). If
absent or malformed, every command that touches the DB fails closed with an error
envelope; no plaintext fallback.
- **Admin key:** `EMCLI_ADMIN_KEY` env var, a base64-encoded 32-byte key (AES-256). Required for
admin commands and for `init`. If absent or malformed when an admin command is attempted, the
command fails with `emcli: this command requires EMCLI_ADMIN_KEY (admin privilege)`.
- **Agent key:** `EMCLI_KEY` env var, a base64-encoded 32-byte key (AES-256). Required for agent
commands. If absent or malformed, every agent command fails closed with a `config` error envelope;
no plaintext fallback. `EMCLI_ADMIN_KEY` is accepted as a fallback for agent commands when
`EMCLI_KEY` is not set.
- **Database path:** `EMCLI_DB` env var; default `~/.config/emcli/emcli.db`
(`%AppData%\emcli\emcli.db` on Windows).
- **Field-level encryption:** secret columns are stored as AES-256-GCM ciphertext with a
random 96-bit nonce per value, prefixed to the ciphertext. Non-secret config remains
plaintext for debuggability. Decryption with the wrong key fails (GCM auth tag) and is
surfaced as an error, never silently ignored.
- **Envelope encryption (DEK):** `emcli init` generates a random data-encryption key (DEK) that
protects all account secrets. The DEK is stored in the `settings` table sealed under both keys:
- `dek_wrap_admin` — the DEK encrypted under `EMCLI_ADMIN_KEY` (AES-256-GCM).
- `dek_wrap_agent` — the DEK encrypted under `EMCLI_KEY` (AES-256-GCM).
The DEK is never written in cleartext. Admin commands unwrap from `dek_wrap_admin` only; agent
commands unwrap from `dek_wrap_agent` (or `dek_wrap_admin` if only the admin key is present).
There is no cross-slot fallback for admin commands — a holder of only `EMCLI_KEY` cannot unwrap
the admin DEK slot.
- **`init` idempotency:** re-running `emcli init` does not regenerate the DEK; the existing wrapped
DEK rows are preserved.
- **Field-level encryption:** secret columns are encrypted with the DEK using AES-256-GCM with a
random 96-bit nonce per value, prefixed to the ciphertext. Non-secret config remains plaintext for
debuggability. Decryption with the wrong key fails (GCM auth tag) and is surfaced as an error,
never silently ignored.
Secret columns: account password, OAuth client secret, OAuth refresh token.
@@ -142,7 +168,9 @@ audit_log
settings
key TEXT PK
value TEXT
-- includes: audit_retention_days, schema_version
-- includes: audit_retention_days, schema_version,
-- dek_wrap_admin (DEK sealed under EMCLI_ADMIN_KEY),
-- dek_wrap_agent (DEK sealed under EMCLI_KEY)
```
Notes:
@@ -216,8 +244,11 @@ command that advances read state is `ack`.
### 7.2 Admin commands (human-readable / TUI)
- **`emcli init`** — TUI flow: creates the DB (generating schema), adds the first account,
and runs OAuth consent if the account is OAuth2.
Require `EMCLI_ADMIN_KEY`.
- **`emcli init`** — TUI flow: creates the DB (generating schema + DEK), adds the first account,
and runs OAuth consent if the account is OAuth2. Requires both `EMCLI_ADMIN_KEY` and `EMCLI_KEY`
(writes both DEK wrap slots). Idempotent — re-running does not regenerate the DEK.
- **`emcli account add | edit | remove | list`** — interactive add/edit; `list` prints a
table (never secrets). `account add` accepts `--process-backlog` (default off) which sets
the account's baseline policy: off ⇒ newly-seen folders floor at their current max UID
@@ -225,8 +256,12 @@ command that advances read state is `ack`.
- **`emcli whitelist in|out add|remove|list --account <name>`** — manage whitelist entries.
- **`emcli config set|get`** — global settings (e.g. `audit_retention_days`).
- **`emcli audit list [--account <name>] [--limit N]`** — view recent audit entries.
- **`emcli doctor`** — verifies `EMCLI_KEY` is present and valid, the DB opens, and each
account's IMAP/SMTP connectivity and auth succeed. Human-readable diagnostics.
### 7.2a `doctor` — agent-role diagnostics
`doctor` is authorised by `EMCLI_KEY` (or `EMCLI_ADMIN_KEY`). It verifies the key is present and
valid, the DB opens, and each account's IMAP/SMTP connectivity and auth succeed. Prints
human-readable diagnostics. Can be run by the agent or by a human; does not require admin privilege.
### 7.3 Defaults & limits
- `list --limit` default: 50; maximum: 500.