Files
relicario/docs/architecture/overview.md
adlee-was-taken 39ae2ecbf3 style: capitalize "Relicario" in prose / UI / CLI help
Brand name uses capital R in user-facing text — extension UI strings,
CLI clap help / descriptions / error prose, markdown docs. Lowercase
preserved for the binary command, crate names, npm package, file
paths, env vars, and code identifiers.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-01 17:29:10 -04:00

16 KiB
Raw Blame History

Architecture overview — Relicario

This is the cross-codebase entry point. It describes how the three 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 three codebases

                       ┌─────────────────────┐
                       │  relicario-core     │
                       │  (Rust, no I/O)     │
                       │  crypto · items     │
                       │  manifest · stego   │
                       └──────────┬──────────┘
                                  │
            ┌─────────────────────┼─────────────────────┐
            │                     │                     │
            ▼                     ▼                     ▼
   ┌────────────────┐   ┌────────────────────┐  (compiled to WASM
   │ relicario-cli  │   │  relicario-wasm    │   inside the      )
   │ (Rust binary)  │   │  (#[wasm_bindgen]  │   extension       │
   │                │   │   bindings)        │                   │
   │ filesystem +   │   │                    │                   │
   │ git +          │   └────────┬───────────┘                   │
   │ clap UX        │            │                               │
   └────────────────┘            ▼                               │
                       ┌─────────────────────┐                   │
                       │  extension          │                   │
                       │  (TypeScript)       │                   │
                       │  popup · vault      │                   │
                       │  setup · content    │                   │
                       │  service worker     │                   │
                       └─────────────────────┘
Codebase Language Role Key boundary
relicario-core Rust Crypto, item types, manifest, attachments, imgsecret, generators. 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.
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.

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
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 8-char hex core/ids.rs Stable, short, no information leak
Attachment IDs are content-addressed (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
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).