Files
relicario/docs/architecture/overview.md
adlee-was-taken ca059e7507 docs(overview): add relicario-server crate to four-codebase framing
Doc-audit Finding 1. The repo has had four Rust crates since early May
when the pre-receive hook crate landed, but docs/architecture/overview.md
still framed itself around three. Update:

- "The three codebases" → "The four codebases" (intro + heading)
- ASCII diagram fans core out to cli + server + wasm, with wasm feeding
  the extension
- Table gains a relicario-server row noting it lives on the git server
  and only sees public key material
- Build matrix adds `cargo build -p relicario-server --release`
- "Where to look next" points at server src + the device-auth design spec

Server has no user-facing surface, so the CLI/extension parity rule is
clarified to exclude it (it is server-side enforcement of an invariant
the clients already agreed to).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-02 16:20:45 -04:00

18 KiB
Raw Blame History

Architecture overview — Relicario

This is the cross-codebase entry point. It describes how the four Relicario codebases fit together, the contracts that flow between them, and the conventions they share. It is deliberately thin; the deep content lives in per-codebase docs.

If you are about to make a change in a single codebase, read its ARCHITECTURE.md first:

If you want historical why, see docs/superpowers/specs/ — those are time-stamped decision artifacts. This overview describes what is.

The four codebases

                          ┌─────────────────────┐
                          │  relicario-core     │
                          │  (Rust, no I/O)     │
                          │  crypto · items     │
                          │  manifest · stego   │
                          │  device keys + fp   │
                          └──┬───────────┬──────┘
                             │           │
            ┌────────────────┼───────────┴──────┬────────────────────┐
            │                │                  │                    │
            ▼                ▼                  ▼                    ▼
   ┌────────────────┐ ┌──────────────────┐ ┌────────────────────┐
   │ relicario-cli  │ │ relicario-server │ │  relicario-wasm    │
   │ (Rust binary)  │ │ (Rust binary)    │ │  (#[wasm_bindgen]  │
   │                │ │                  │ │   bindings)        │
   │ filesystem +   │ │ pre-receive hook │ │                    │
   │ git +          │ │ verify-commit  + │ │  compiled to WASM  │
   │ clap UX        │ │ generate-hook    │ │  for the extension │
   └────────────────┘ └──────────────────┘ └──────────┬─────────┘
                                                      │
                                                      ▼
                                           ┌─────────────────────┐
                                           │  extension          │
                                           │  (TypeScript)       │
                                           │  popup · vault      │
                                           │  setup · content    │
                                           │  service worker     │
                                           └─────────────────────┘
Codebase Language Role Key boundary
relicario-core Rust Crypto, item types, manifest, attachments, imgsecret, generators, device keys / fingerprints. Pure, no I/O. Only bytes-in / bytes-out. No filesystem, no git, no network.
relicario-cli Rust binary Wraps core with filesystem ops, git plumbing, clap UX. Only entry point that runs without a browser; sole working interface during disaster recovery.
relicario-wasm Rust → WASM Thin #[wasm_bindgen] exports from core for the extension. Compiles relicario-core to WASM; no extra logic.
relicario-server Rust binary Pre-receive Git hook (verify-commit) plus hook installer (generate-hook) running on the vault remote. Verifies SSH-signed commits against .relicario/devices.json and .relicario/revoked.json. Lives on the git server, not on a client device. The only Relicario component the user does not run themselves. Sees only public key material.
extension TypeScript Browser-resident UI. Five entry-point bundles (popup, vault tab, setup, content script, service worker). The service worker is the only crypto holder; popup/vault/content/setup never touch the master key.

The CLI and the extension are at parity: every user-facing capability lands in both surfaces together. Diverging is allowed only with a documented reason. See the per-codebase docs for which surface owns which user flow. The server has no user-facing surface — it is a server-side enforcer of the device-auth invariant the clients already agreed to.

Inter-codebase contracts

There are four boundaries where the codebases agree on a wire format. Each is versioned independently.

1. Core → WASM ABI (Rust / JS edge)

The relicario-wasm crate is the JS/Rust contract. Every WASM export takes JsValue / &[u8] / &str and returns the same. Strings on the wire are JSON-encoded for any structured data; raw bytes for ciphertext / images / attachments.

Adding a new core capability for the extension requires:

  1. Add the capability to relicario-core/src/.
  2. Re-export through lib.rs.
  3. Add a thin #[wasm_bindgen] wrapper to relicario-wasm/src/lib.rs.
  4. Run wasm-pack build (via npm run build:wasm in extension/).
  5. Use it from the extension's service worker (or setup wizard).

The SessionHandle is the cross-language opaque token: WASM owns the Zeroizing<[u8;32]> master key behind a numeric handle; JS only ever holds the number. JS calling wasm.lock(handle) zeroes the WASM-side memory and invalidates the handle.

2. Service worker ↔ popup / vault tab / content script (chrome.runtime messages)

All extension bundles other than the SW communicate with the SW exclusively via chrome.runtime.sendMessage. The protocol is defined in extension/src/shared/messages.ts:

  • PopupMessage — sent by popup, vault tab, or setup wizard
  • ContentMessage — sent by content scripts injected into web pages
  • Response — returned by the SW: { ok: true, data?: ... } | { ok: false, error: string }

Two capability sets in messages.ts gate which sender can issue which message:

  • POPUP_ONLY_TYPES — accepted only from popup.html, vault.html, or setup.html
  • CONTENT_CALLABLE_TYPES — accepted only from content scripts

The router (service-worker/router/index.ts) dispatches by sender. Adding a new message type requires adding it to one of the capability sets, or it is silently rejected. Vault tab parity (commit a7dbf35) is implemented by recognizing vault.html as a popup-class sender at the router level.

3. Vault on disk (shared by CLI and extension)

Every relicario vault — whether on disk for the CLI or in a git remote read by the extension — has the same layout:

<vault root>/
├── .relicario/
│   ├── salt              # 32 bytes, random per vault, stays constant
│   ├── params.json       # KdfParams: argon2_m, argon2_t, argon2_p
│   └── devices.json      # [{ name, public_key }, ...]
├── manifest.enc          # encrypted Manifest (browse-without-decrypt index)
├── settings.enc          # encrypted VaultSettings
├── items/
│   └── <id>.enc          # encrypted Item, one per file
└── attachments/
    └── <item-id>/
        └── <aid>.enc     # encrypted attachment blob; aid is content-addressed SHA-256

The reference image (reference.jpg) lives outside the vault by convention — it is the second factor and the user's responsibility to safeguard. It is not in .relicario/, not in items/, and never committed to git.

This layout is not formally versioned — the content within each .enc file carries its own version byte (see § Versioning below). The directory layout itself is conventional and changes would be breaking.

4. Git remote API (extension's GitHost)

The extension cannot shell out to git; it talks to the remote via the host's REST API. Two implementations live in extension/src/service-worker/:

  • gitea.ts — Gitea / Forgejo API
  • github.ts — GitHub API

Both implement the GitHost interface in git-host.ts. Adding a third host (GitLab, Bitbucket, custom) means implementing that interface — the rest of the extension is host-agnostic.

The CLI does not use GitHost; it shells out to git directly via the hardened wrapper in relicario-cli/src/helpers.rs:46.

Versioning strategy

There is no single "relicario format version." Each piece of the format is versioned independently so we can evolve without coordinated upgrades.

Artifact Where versioned Current value Failure mode on read
AEAD ciphertext First byte of every .enc blob VERSION_BYTE = 0x02 (in relicario-core/src/crypto.rs) RelicarioError::Format — refuses to attempt decryption
Manifest schema Manifest.schema_version field 2 (set in relicario-core/src/manifest.rs) v1 manifests are explicitly rejected with a clear error
KDF parameters .relicario/params.json Vault-specific (initially m=64MiB, t=3, p=4) Read at unlock; stored alongside the vault
Backup container First 5 bytes of .relbak: magic "RBAK" + version byte 0x01 (designed; see import/export spec) Format-version error if newer-version backup is read by older binary
Device entry devices.json array of { name, public_key } Unversioned (extend by adding optional fields)

The intentional design: no big-bang upgrades. A user can run an older CLI against a newer vault as long as the AEAD version, manifest schema, and KDF params are still compatible.

Where secrets live

The threat model differs by codebase. This is the per-secret per-codebase residence map:

Secret relicario-core relicario-cli extension SW extension popup/vault/content/setup
Passphrase (UTF-8 bytes) Zeroizing<String> only during a single derive_master_key call Same, in UnlockedVault::unlock_interactive Same, used briefly to derive master key inside WASM Never seen — entered into a <input type="password">, sent to SW via unlock message, immediately forgotten
Reference image bytes Held by caller; core only reads Held by UnlockedVault::unlock_interactive long enough to extract the secret Same Setup wizard holds the bytes briefly during create/attach modes
Image secret (32 B) Zeroizing<[u8;32]> during KDF Same Same Never sees it
Master key Zeroizing<[u8;32]> returned by derive_master_key UnlockedVault.master_key for the lifetime of one CLI invocation WASM-side memory behind an opaque SessionHandle; JS never sees the bytes Never sees it
Item secret (password, card number, etc.) Zeroizing<String> / Zeroizing<Vec<u8>> Same Briefly held in WASM during item_decrypt; results passed to popup as plaintext for display Held in DOM (the user is staring at it); cleared when view changes
Device private key Filesystem under ~/.config/relicario/devices/<name>.key (mode 0600) chrome.storage.local.device_private_key

The popup / vault / content surfaces of the extension cannot decrypt an item independently — they all message the SW. Content scripts in particular get back already-prepared payloads (e.g. { username, password }) from fill_credentials after the SW resolved everything.

The CLI keeps its master key in process memory; if the process exits or crashes, the key is gone (Zeroize on drop). There is no CLI session daemon. The lock subcommand exists only for UX parity with the extension and is a no-op.

Build matrix

Target Tool Output When to run
Native CLI cargo build (debug or --release) target/{debug,release}/relicario After CLI changes; for distribution
Server hook cargo build -p relicario-server --release target/release/relicario-server After server changes; deploy onto the git remote
Native test suites cargo test (workspace) After any Rust change
WASM module wasm-pack build --target web (via npm run build:wasm) extension/wasm/relicario_wasm{,_bg.wasm,.js} After core or wasm crate changes
Chrome extension webpack (npm run build) extension/dist/ After TS or WASM changes; for Chrome distribution
Firefox extension webpack --config webpack.firefox.config.js (npm run build:firefox) extension/dist-firefox/ After TS or WASM changes; for Firefox distribution
All extension targets npm run build:all Both dist/ and dist-firefox/ plus rebuilt WASM Pre-release
Extension tests npm test (vitest, happy-dom) After TS changes

The WASM build sequence matters: wasm-pack writes the binary into extension/wasm/ before webpack picks it up. npm run build:all runs them in order. Manual builds need the same order.

Test strategy at the workspace level

Layer Tool Where What it covers
Core unit tests cargo test -p relicario-core crates/relicario-core/src/**/#[cfg(test)] and tests/*.rs Crypto round-trip, item serialization, manifest schema, generators, imgsecret embed/extract, format-v2 parsing
CLI integration tests cargo test -p relicario-cli crates/relicario-cli/tests/*.rs End-to-end via TestVault::init() harness with synthetic JPEGs and RELICARIO_TEST_* env-var escape hatches; covers basic flows, edit + history (incl. TOTP), attachments, settings, vault detection
Extension unit tests npm test (vitest) extension/src/**/__tests__/*.test.ts Component render + click handlers (mocked SW), router sender dispatch, SW handler logic (mocked WASM + chrome.storage)
End-to-end none No real-browser tests; mocks stand in. Build-vs-test gap is documented in extension/ARCHITECTURE.md

Core tests use fast Argon2id params (m=256, t=1, p=1) so they don't take forever; the production path is the same code with real params. The CLI's init command always uses production-grade params even under tests.

Conventions that span all three codebases

Rule Where enforced Why
Master key only in Zeroizing<[u8;32]> core types; CLI follows; extension WASM follows Drop-on-scope-exit zeroization; never leaves stack
AEAD ciphertext starts with version byte core/crypto.rs Format identification; reject v1 blobs cleanly
Item IDs are random 16-char hex (64 bits) core/ids.rs Stable, short, no information leak
Attachment IDs are content-addressed (first 32 hex chars / 128 bits of SHA-256) core/ids.rs Dedup; integrity check
KDF input is length-prefixed core/crypto.rs Prevents `passphrase
Git history is preserved as audit log; never squash CLI commits; SW commits Per-action history is a feature
Per-action git commits with structured messages cli (via commit_paths); SW (via vault.ts helpers) Greppable, useful as audit log
Hardened git invocations (-c core.hooksPath=/dev/null etc.) CLI's helpers::git_command; SW does not shell out Prevent hostile hooks; no GPG prompt holding key alive
Atomic writes (write .tmp → rename) CLI's session::atomic_write; SW's vault.ts equivalents Partial-write safety
Tests use synthesized JPEGs (make_test_jpeg), not committed binaries Both Rust and TS test harnesses Repo stays small; reproducible
Test-only env vars (RELICARIO_TEST_*) have no production fall-through Verified in relicario-cli audit Escape hatches don't leak into builds

Where to look next

If you're working on... Start with
Crypto, item types, manifest format crates/relicario-core/ARCHITECTURE.md
A new CLI command or a CLI bug crates/relicario-cli/ARCHITECTURE.md
A new popup view, vault tab feature, or autofill change extension/ARCHITECTURE.md
A new SW message type extension/src/shared/messages.ts (capability sets), then extension/ARCHITECTURE.md § Invariants
A new GitHost (e.g. GitLab support) extension/src/service-worker/git-host.ts (interface) and existing implementations
The pre-receive hook / device-auth enforcement crates/relicario-server/src/main.rs, then docs/superpowers/specs/2026-05-02-device-authentication-design.md for rationale
Adding a new item type core's item_types/ mod, then CLI's build_*_item/edit_* helpers, then extension's popup/components/types/<type>.ts
Threat model / why a primitive was chosen docs/superpowers/specs/2026-04-11-relicario-design.md (historical, but authoritative for rationale)
Format of the import/export feature docs/superpowers/specs/2026-04-27-relicario-import-export-design.md (designed but not yet implemented)
Running the full test suite cargo test && (cd extension && npm test)
Bumping the WASM module after a core change cd extension && npm run build:wasm

Stale spec docs

The docs/superpowers/specs/ tree is historical — it captures the design decisions made at planning time. Some specs (e.g. Plan 1A, 1B, 1C-α/β/γ) describe work that has shipped. Do not edit them as if they were the architecture docs; instead update the appropriate ARCHITECTURE.md. The specs are valuable for why (why XChaCha20-Poly1305, why central-embed DCT, why two-factor with steganography); the architecture docs are valuable for what (current invariants, current flows, current contracts).