Files
relicario/docs/SECURITY.md
adlee-was-taken a332a9e80d Merge feature/v0.5.0-plan-a-security-cleanup: Plan A security + cleanup
v0.5.0 Plan A — Security Fixes + Repo Cleanup. 7 commits, ~800 net
insertions across the Rust workspace. Four items delivered:

- S1 (HIGH-severity authentication bypass fix): rewrite verify_commit
  in relicario-server. The previous implementation accepted any
  GOODSIG/Good signature line on stderr, ignoring whether the signing
  key was registered or revoked. The new implementation:
  * builds a temp gpg.ssh.allowedSignersFile from devices.json at the
    commit (no global git-config mutation)
  * parses the SHA-256 fingerprint from `git verify-commit --raw`
    stderr via regex
  * checks revocation FIRST (revoked entries may have been removed
    from devices.json), with the historical-commit case
    (committer_ts < revoked_at) explicitly allowed
  * uses committer date (GIT_COMMITTER_DATE / `git show -s
    --format=%ct`), not author date or wall clock
  * tightened the bootstrap guard to require BOTH devices and revoked
    to be empty (closes an empty-devices.json privilege-escalation
    route present in the original code)
  * 4 acceptance integration tests build real on-disk repos with
    SSH-signed commits and verify each scenario

- S2 (tar archive path-traversal hardening): replace
  tar::Archive::unpack with safe_unpack_git_archive. Located in
  relicario-core (per-spec, so integration tests can reach it without
  the bytes-in/bytes-out invariant breaking). Validates each entry's
  type (rejects symlinks/hardlinks), path components (rejects '..',
  RootDir, Windows drive Prefix), and declared size (rejects
  individual or cumulative > 100×compressed-or-1-GiB whichever is
  lower). The CLI's restore path adds a paranoid OS-level
  starts_with(.git/) check on the joined destination as
  defense-in-depth even after textual validation. 5 acceptance tests
  cover path traversal, symlinks, oversized headers (header claim of
  2 GiB tested without allocating disk).

- S3 (RELICARIO_* env-var audit): docs/SECURITY.md gains a
  "Configuration env vars" section enumerating each variable, its
  purpose, and trust assumption. Active-in-all-builds variables
  (RELICARIO_IMAGE, RELICARIO_GITEA_*) are documented; debug-only
  variables (RELICARIO_NO_GROUPS_CACHE, RELICARIO_TEST_*) are gated
  behind cfg(debug_assertions) so the env-var lookup is removed from
  --release binaries.

- C1 (stale feature branch prune): 5 merged feature branches and
  3 worktrees pruned interactively per dev report.

- Bonus: 4d02a50 fixes pre-existing clippy warnings across
  crates/relicario-{core,cli} (deref operators, Option::is_none_or
  vs map_or(true, ...), iter_mut().enumerate() patterns,
  div_ceil()) so the workspace builds clean under `-D warnings`.

Merge resolution: docs/SECURITY.md had a conflict where main's F11/F12
(Device Authentication paragraph naming relicario-server + simplified
"Device registration is optional" line) collided with Plan A's S3
section. Resolved by keeping both — F11/F12's wording for the
Device Authentication section, then Plan A's "Configuration env vars"
section appended below.

Cargo.lock regenerated. The previous committed lock was stale since
commit 8855078 (--totp-qr); cargo test on both devs' worktrees
produced identical regenerated locks. Plan A genuinely added regex +
tempfile to relicario-server (both already transitively present from
relicario-cli), so no new top-level deps; the Cargo.lock churn is
catch-up of crate-version bumps that have happened since the last
commit-of-record.

Tests: 248 cargo tests pass; extension tests unchanged (336/8 with 8
pre-existing device-auth scaffolding failures).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-02 19:54:12 -04:00

4.5 KiB

Relicario Security Model

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:

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 <repo>/hooks/pre-receive
  • relicario-server verify-commit <sha> — 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.

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.