# Relicario Security Model > **Audience:** auditors and curious users. This doc owns the threat model, attacker-scenarios table, device-authentication model, env-var trust surface, and known limitations. **Does NOT own:** crypto primitive details (see [CRYPTO.md](CRYPTO.md)), wire formats (see [FORMATS.md](FORMATS.md)), or implementation (see [../crates/relicario-core/ARCHITECTURE.md](../crates/relicario-core/ARCHITECTURE.md) and [../crates/relicario-cli/ARCHITECTURE.md](../crates/relicario-cli/ARCHITECTURE.md)). ## Cryptographic Protection Relicario uses two-factor vault decryption: 1. **Passphrase** — user-memorized, zxcvbn score ≥3 required 2. **Reference image** — JPEG carrying 256-bit secret via DCT steganography Key derivation: Argon2id (64 MiB memory, 3 iterations, 4 parallelism) Encryption: XChaCha20-Poly1305 (192-bit nonce, 256-bit key) ## Manifest Integrity The manifest (`manifest.enc`) is encrypted with AEAD, which provides: - **Confidentiality**: Contents unreadable without master key - **Integrity**: Any modification detected and rejected on decrypt - **Authenticity**: Only master key holders can create valid ciphertexts ### What AEAD Does NOT Protect - **Item deletion**: An attacker with write access can delete `.enc` files or git-revert commits. The manifest decrypts successfully but won't contain the deleted items. - **Rollback attacks**: An attacker can replace `manifest.enc` with an older valid version. AEAD accepts any ciphertext created with the key. ### Mitigation Item deletion and rollback are detectable via **git history**: ```bash git log --oneline items/ ``` For environments where git history could be rewritten (force-push): 1. Enable device authentication (commit signing + pre-receive hook) 2. Use a git server that rejects non-fast-forward pushes 3. Regular backups with `relicario backup export` ## Device Authentication When enabled, device authentication provides: - **Commit authorship**: All commits signed by registered device keys - **Push access control**: Deploy keys managed via Gitea API - **Instant revocation**: One command cuts off both signing and push Enforcement requires deploying the `relicario-server` pre-receive hook on the vault remote. The crate provides two subcommands: - `relicario-server generate-hook` — emits the hook script to install at `/hooks/pre-receive` - `relicario-server verify-commit ` — checks one commit's signature against `.relicario/devices.json` and `.relicario/revoked.json` as of that commit; the hook calls this for every pushed ref Without the server hook, signed commits provide authorship metadata only — any process with push access can land an unsigned commit, since verification is otherwise advisory. See `docs/superpowers/specs/2026-05-02-device-authentication-design.md`. ## Access Control Without device authentication, access control is transport-layer only: - **CLI**: SSH key authentication to git remote - **Extension**: Git credentials in browser storage Device registration is optional but recommended for shared vaults. ## Org vault security An org vault is a separate git repository alongside the personal vault. It uses ed25519 commit-signing and a server-side pre-receive hook to make least-privilege access control server-enforced, not advisory. ### Org device-key authentication Every org member registers an ed25519 device key. The key appears in `members.json` as an OpenSSH public-key string alongside the member's role and collection grants. Fingerprint matching is done via `relicario_core::fingerprint`, which normalises the OpenSSH format so that whitespace and comment differences do not create phantom mismatches. Org access requires two things at once: a wrapped key blob (`keys/.enc`) and the device private key that can unwrap it. There is no org passphrase — removing a member's blob and rotating the org master key is sufficient to revoke access (see **Key rotation** below). Device keys are completely separate from the personal vault's KDF inputs; revoking org access does not affect the member's personal vault. ### Pre-receive hook enforcement `relicario-server generate-org-hook` (`crates/relicario-server/src/main.rs:511`) emits a hook script that calls `relicario-server verify-org-commit` for every pushed commit. Unsigned or structurally invalid commits are rejected before they land. `verify_org_commit` (`main.rs:286`) performs four checks in order: 1. **Signature verification** — a temporary `allowed_signers` file is constructed from the current `members.json`; `git verify-commit --raw` is run and the resulting SHA-256 fingerprint is matched back to a `members.json` entry. A commit not signed by a *current* member is rejected outright. 2. **Path-level write authorisation** — each modified path is classified by `classify_path` (`crates/relicario-server/src/lib.rs:19`) into `ProtectedJson` (owner/admin write only), `CollectionItem` (the `items//…` prefix; write allowed only if the slug appears in the signer's `collections` grant array), or `Unrestricted`. The write is authorised if and only if the signer's role and grants satisfy the classification. Item blobs are authorised by the leading path segment alone — the ciphertext is never decrypted by the hook. 3. **Owner-only elevation guard** (`enforce_owner_only_elevation`, `main.rs:438`) — only a member whose *pre-commit* (parent) role is Owner may introduce a new member at Owner or Admin level, or promote an existing member to either. Checking the pre-commit role means an Admin cannot self-promote in the same commit that writes the escalated `members.json`; there is no epoch in which the transition is self-authorised. 4. **Schema monotonicity** (`enforce_schema_monotonicity`, `main.rs:521`) — `schema_version` values in org JSON containers may not decrease. Merge commits are rejected. A genesis commit (no parents) is allowed only when it is signed by the sole Owner it introduces. ### Key rotation `relicario org rotate-key` generates a fresh 256-bit org master key, re-wraps it for every current member, and re-encrypts every `items//.enc` blob and the manifest under the new key in a single signed commit tagged `Relicario-Action: key-rotate`. A revoked member's wrapped blob is simply not written during rotation, so they hold a blob that decrypts to a stale key — they cannot read items encrypted under the new key. ### Audit action vocabulary The `relicario org audit` command attributes actions to their verified signer (not to the commit author or trailer value). Each event records two actors: the **verified** actor resolved from the signing key (authoritative) and the actor **claimed** by the `Relicario-Actor` trailer (advisory). When the claimed actor disagrees with the verified signer, the event is flagged `TAMPERED`. Trailers are advisory metadata; the trustworthy actor is always the cryptographically verified signer. Actions live in two groups: - **Membership / collections / lifecycle:** `member-add`, `member-remove`, `member-role-change`, `collection-create`, `collection-grant`, `collection-revoke`, `key-rotate`, `org-init`, `ownership-transfer`, `org-delete`. - **Item CRUD:** `item-create`, `item-update`, `item-delete` (soft-delete / trash), `item-restore`, `item-purge` — emitted by the `org add` / `edit` / `rm` / `restore` / `purge` commands. ### Honest limitations The following are deliberate design boundaries, not oversights: - **Shared org master key — reads are not cryptographically scoped per collection.** The pre-receive hook scopes *writes* by collection path and the CLI filters the manifest to each member's grants, but a single org key opens all collection blobs. A member with any grant can, outside the CLI, decrypt items from collections they are not granted. For true cryptographic separation, use a separate org vault per access boundary. Per-collection subkeys are a phase-2 non-goal. - **No read audit.** Git records writes only. A member who reads blobs directly leaves no server-visible trace. - **No "hide value."** There is no mechanism to show a member that an item exists without revealing its field values on decrypt. - **`delete-org` is a local tombstone in phase 1.** The schema-monotonicity check causes the hook to reject protected-file deletion, so an `org-delete` action cannot be pushed to a hook-protected remote. The deletion is recorded locally only until a future phase addresses it. ## Configuration env vars Relicario reads the following environment variables. Each is a trust boundary: an attacker who can set them in the user's environment can influence Relicario's behavior. They are listed here for security reviewers to audit the surface in one place. ### User-facing (active in all builds) | Variable | Purpose | Trust | |---|---|---| | `RELICARIO_IMAGE` | Override the reference-image JPEG path used during vault unlock. | Trusted: filesystem path under the user's control. Read-only; its bytes feed `imgsecret::extract_secret`. | | `RELICARIO_GITEA_URL` | Gitea API base URL for `relicario device add`. Equivalent to `--gitea-url`. | Trusted: HTTPS URL. Used only in the device-add code path. | | `RELICARIO_GITEA_TOKEN` | Gitea personal-access token. Equivalent to `--gitea-token`. | **Secret**: anyone who can read this env var can manage the user's deploy keys via the Gitea API. The CLI never logs it. | | `RELICARIO_GITEA_OWNER` | Gitea repository owner (e.g. `alee`). Equivalent to `--owner`. | Trusted: opaque string. | | `RELICARIO_GITEA_REPO` | Gitea repository name (e.g. `vault`). Equivalent to `--repo`. | Trusted: opaque string. | ### Debug-only (compiled out of `cargo build --release`) The following variables are gated behind `cfg(debug_assertions)` and are **no-ops** in release builds. The env-var lookup is removed by the optimiser from any binary built without debug assertions (i.e. the standard `--release` profile). | Variable | Purpose | |---|---| | `RELICARIO_NO_GROUPS_CACHE` | Suppress the plaintext `groups.cache` write. Developer debugging tool for the cache logic. | | `RELICARIO_TEST_PASSPHRASE` | Bypass the `rpassword` prompt during integration tests. | | `RELICARIO_TEST_ITEM_SECRET` | Bypass the `rpassword` prompt for item-secret fields during integration tests. | | `RELICARIO_TEST_BACKUP_PASSPHRASE` | Bypass the `rpassword` prompt for backup export/restore passphrases during integration tests. | --- **Next:** [../crates/relicario-core/ARCHITECTURE.md](../crates/relicario-core/ARCHITECTURE.md) — implementation, starting with the platform-agnostic core.