docs: refresh per-crate ARCHITECTURE — missing core modules + CLI commands

Punch items from doc audit:
- relicario-core: module map missing 5 public modules (backup,
  device, import_lastpass, recovery_qr, tar_safe); added with
  1-2 sentence descriptions in the existing voice.
- relicario-core: "ed25519-dalek is a dependency placeholder" was
  stale — device.rs now consumes it for signing/verify/keypair.
- relicario-cli: Rate (zxcvbn scoring) and RecoveryQr (generate/unwrap)
  commands were absent from Key flows; added.
- relicario-cli: "Backup-passphrase-style commands (none yet)" rewritten
  — Backup (export/restore .relbak) and Import (lastpass) both shipped.
- relicario-cli: module map refreshed — handlers moved out of main.rs
  into commands/, plus prompt.rs/parse.rs/device.rs/gitea.rs surfaced.

Stale main.rs:NNNN line citations on individual flows are not fixed
here — those handlers now live in commands/*.rs and warrant a deeper
pass later.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
adlee-was-taken
2026-05-30 13:24:49 -04:00
parent 210232d156
commit cf7478d178
2 changed files with 134 additions and 26 deletions

View File

@@ -16,22 +16,46 @@ locally, and lets recovery debugging happen with familiar tooling.
## Module map ## Module map
The crate is three files of source and a `tests/` directory. Each source file `src/main.rs` is now a thin clap-surface + dispatcher; per-command logic lives
has one job. under `src/commands/`. Each source file has one job.
- **`src/main.rs`** (`main.rs:1-1719`) — clap surface plus every command - **`src/main.rs`** (`main.rs:1-492`) — clap surface and the flat dispatcher.
handler. Internal structure: a top-level `Cli` / `Commands` enum Owns the top-level `Cli` / `Commands` enum and every subcommand enum
(`main.rs:13-275`), a flat dispatcher `match` in `main()` (`AddKind`, `TrashAction`, `SettingsAction`, `BackupAction`, `ImportAction`,
(`main.rs:277-303`), per-command handlers named `cmd_<verb>`, and a layer of `DeviceAction`, `RecoveryQrCmd`). `main()` is a single `match` that
per-type item helpers (`build_<type>_item` for `cmd_add`, `edit_<type>` for delegates each variant to `commands::<verb>::cmd_<verb>(...)`. Also owns the
`cmd_edit`). The per-type split is recent: commit `3f0f5b1` extracted three test-only env-var hooks (`test_passphrase_override`,
~217-line `match` arms in `cmd_add` and `cmd_edit` into focused functions, `test_item_secret_override`, `test_backup_passphrase_override`) — each is
one per `ItemCore` variant, so each builder/editor reads top-to-bottom and stripped from release builds via `#[cfg(debug_assertions)]`.
can be tested through the same integration paths. Owns all clap argument
parsing, all interactive prompts (`prompt`, `prompt_optional`, `prompt_keep`, - **`src/commands/`** — one module per top-level command. `mod.rs` re-exports
`prompt_keep_opt`, `prompt_yesno`, `prompt_secret`), and the shared the public surface and hosts the shared `commit_paths` helper (the single
`commit_paths` helper that is the single chokepoint for git commits during chokepoint for git commits during vault mutations) plus other cross-command
vault mutations. glue. Per-command modules: `init`, `add`, `get`, `list` (also hosts
`cmd_history`), `edit`, `trash` (rm / restore / purge / trash empty),
`backup` (export / restore), `import` (lastpass), `attach` (attach /
attachments / extract / detach), `generate`, `settings`, `sync`, `status`,
`rate`, `device`, `recovery_qr`. `add` and `edit` each fan out internally to
per-`ItemCore` helpers (`build_<type>_item`, `edit_<type>`) so each
builder/editor reads top-to-bottom and can be tested through the same
integration paths.
- **`src/prompt.rs`** — interactive prompt primitives shared across commands:
`prompt`, `prompt_optional`, `prompt_keep`, `prompt_keep_opt`,
`prompt_yesno`, `prompt_secret`. `prompt_secret` honours
`RELICARIO_TEST_ITEM_SECRET` before falling back to `rpassword`.
- **`src/parse.rs`** — pure parsers for CLI-typed inputs (e.g. MonthYear
expiries, TOTP `otpauth://` URIs, comma-separated tag lists). No I/O.
- **`src/device.rs`** — device-management plumbing called by
`commands::device`: ed25519 keypair generation via `relicario-core::device`,
on-disk layout under `<config_dir>/relicario/devices/<name>/`, and the
read/write of `.relicario/devices.json` / `revoked.json`.
- **`src/gitea.rs`** — minimal Gitea REST client used by `commands::device add`
/ `revoke` to register and remove deploy keys. Reads
`RELICARIO_GITEA_{URL,TOKEN,OWNER,REPO}` env vars (overridable via CLI flags).
- **`src/session.rs`** (`session.rs:1-152`) — `UnlockedVault` lifecycle. Holds - **`src/session.rs`** (`session.rs:1-152`) — `UnlockedVault` lifecycle. Holds
the derived master key in `Zeroizing<[u8; 32]>` for one CLI invocation; the the derived master key in `Zeroizing<[u8; 32]>` for one CLI invocation; the
@@ -306,13 +330,65 @@ rewrite `devices.json`, commit `device: revoke <name>`. Note that device
keys are kept entirely separate from the KDF (passphrase × image stays keys are kept entirely separate from the KDF (passphrase × image stays
unchanged across device add/revoke), as per the design spec. unchanged across device add/revoke), as per the design spec.
### Backup-passphrase-style commands (none yet) ### Backup (`commands::backup`, `commands/backup.rs`)
The import / export / `import-lastpass` commands described in Two subcommands, both keyed by a *backup* passphrase that is independent of
`docs/superpowers/specs/2026-04-27-relicario-import-export-design.md` are the vault master passphrase.
not yet implemented. When they land they'll fit in the dispatcher
(`main.rs:279-302`) alongside `Sync` and `Status`. Don't add stubs here - **`backup export <out> [--include-image] [--image PATH] [--no-history]`** —
until that work begins. reads the entire on-disk vault layout (`.relicario/{salt,params.json,
devices.json}`, `manifest.enc`, `settings.enc`, every `items/*.enc`, every
`attachments/<iid>/<aid>.enc`), optionally bundles the reference JPEG and
the `.git/` directory (as an in-memory tar), and hands the lot to
`relicario_core::backup::pack_backup` with a zxcvbn-gated backup
passphrase prompted twice. The resulting `.relbak` is written via
`tmp` + rename. A `.relicario/last_backup` marker file (ISO-8601 line) is
also written so `cmd_status` can show "last backup at …".
- **`backup restore <input> [<target>]`** — refuses to overwrite an existing
vault (`target/.relicario/` must not exist). Unpacks the `.relbak` via
`unpack_backup`, then materialises every byte into the target layout. The
bundled `.git/` tar is extracted via the hardened
`relicario_core::safe_unpack_git_archive` (path-traversal / symlink /
size-cap guards) with a cap of `min(100 × tar_size, 1 GiB)`; if no
history was bundled, the target gets a fresh `git init` + initial commit.
### Import (`commands::import`, `commands/import.rs`)
- **`import lastpass <csv>`** — reads the CSV, calls
`relicario_core::import_lastpass::parse_lastpass_csv`, then unlocks the
vault and writes every produced `Item` through `vault.save_item` + manifest
upsert. Failed rows surface as `ImportWarning`s on stderr and never abort
the import; only a missing or malformed header is fatal. Commit message:
`import: <N> items from LastPass (<csv-filename>)`. The dispatch shape
(`ImportAction` subcommand enum) is in place for future importers
(Bitwarden, 1Password, etc.) — each would add one `ImportAction` variant
and one helper.
### Rate (`commands::rate`, `commands/rate.rs`)
`rate <passphrase|->` runs `relicario_core::generators::rate_passphrase`
(zxcvbn-backed) and prints the 04 score, a human-readable label, and the
estimated guess count as `~10^N`. Reads one line from stdin when the
argument is `-`, which keeps the passphrase out of shell history. Purely
informational — does not unlock or mutate anything; the `init` command
calls `validate_passphrase_strength` directly and does not consult `rate`.
### RecoveryQr (`commands::recovery_qr`, `commands/recovery_qr.rs`)
Two subcommands wrapping `relicario_core::recovery_qr::{generate_recovery_qr,
unwrap_recovery_qr}`.
- **`recovery-qr generate`** — re-extracts the 32-byte image_secret from the
reference JPEG (via `get_image_path` + `imgsecret::extract`), prompts for
the recovery passphrase (which may be the same as the vault passphrase or
different — domain-separated by core), produces the 109-byte sealed
payload, and renders it as a Unicode-block QR (EcLevel::M) directly to
stdout. The payload is **never written to disk** — the user is expected to
print or photograph it.
- **`recovery-qr unwrap`** — reads a base64-encoded payload from stdin,
prompts for the recovery passphrase, runs `unwrap_recovery_qr`, and prints
the recovered `image_secret` as hex. Useful for recovery dry-runs and for
reconstructing a lost reference image.
## Cross-cutting concerns ## Cross-cutting concerns

View File

@@ -101,6 +101,38 @@ Pipeline" and "Crate Layout").
auth factor. Owns its own `YChannel`, `EmbedRegion`, 8×8 DCT/IDCT, auth factor. Owns its own `YChannel`, `EmbedRegion`, 8×8 DCT/IDCT,
Quantization Index Modulation, and crop-recovery extractor. No other module Quantization Index Modulation, and crop-recovery extractor. No other module
imports it; it is consumed only via the public re-export from `lib.rs`. imports it; it is consumed only via the public re-export from `lib.rs`.
- **`backup.rs`** — `.relbak` v1 container format: `pack_backup` /
`unpack_backup` plus the `BackupInput` / `BackupOutput` / `BackupItem` /
`BackupAttachment` shapes. Wraps a zstd-compressed JSON envelope of vault
bytes (salt, params.json, devices.json, manifest, settings, items,
attachments, optional reference JPEG, optional `.git/` tar) in an
XChaCha20-Poly1305 envelope keyed by Argon2id over a user-chosen *backup*
passphrase. The backup key is independent of any vault master key, and
Argon2id parameters are pinned to the v1 values (m=64MiB, t=3, p=4) so a v1
reader doesn't need to negotiate them.
- **`import_lastpass.rs`** — `parse_lastpass_csv` plus `ImportWarning`. Pure
bytes-in / `Vec<Item>`-out LastPass CSV importer: validates the fixed
8-column header, mints fresh IDs and timestamps for each row, downgrades or
skips malformed rows into `ImportWarning`s instead of aborting the import.
Only fatal error is a missing/malformed header.
- **`device.rs`** — Device-identity surface: `DeviceEntry`, `RevokedEntry`,
`generate_keypair`, `sign`, `verify`, `fingerprint`. ed25519 in OpenSSH
format (so private keys are interchangeable with `ssh-keygen`-produced
keys); the same module backs both `.relicario/devices.json` entries and the
server's pre-receive commit-verification hook.
- **`tar_safe.rs`** — `safe_unpack_git_archive` + `DEFAULT_MAX_UNCOMPRESSED`
(1 GiB). Hardened tar reader used by `backup::unpack_backup` for the
bundled `.git/` directory: rejects `..` components, absolute paths, Windows
drive prefixes, symlinks, hardlinks, and any entry whose declared size
(or running total across all entries) exceeds the supplied cap.
- **`recovery_qr.rs`** — `generate_recovery_qr` / `unwrap_recovery_qr` plus
`recovery_qr_to_svg`. Produces a 109-byte XChaCha20-Poly1305 envelope
around the 32-byte image_secret, keyed by Argon2id over a user-chosen
recovery passphrase with the domain-separation prefix
`b"relicario-recovery-v1\0"`. Parameters are pinned at module scope —
changing them invalidates every printed QR — and both salt and nonce are
freshly randomized per call so two QRs printed from the same inputs are
different bytes.
## Invariants & contracts ## Invariants & contracts
@@ -386,11 +418,11 @@ when subsequent `decrypt_*` returns `RelicarioError::Decrypt`.
`generators::bip39_passphrase`. A single `rand::thread_rng()` call exists `generators::bip39_passphrase`. A single `rand::thread_rng()` call exists
inside an `imgsecret` test (`imgsecret.rs:1033`) to generate a random test inside an `imgsecret` test (`imgsecret.rs:1033`) to generate a random test
secret; production code is `OsRng` only. secret; production code is `OsRng` only.
- **`ed25519-dalek` is a dependency placeholder.** Listed in - **`ed25519-dalek` is consumed by `device.rs`.** Together with `ssh-key` (for
`Cargo.toml:17` but unused in `src/`. It exists for the future OpenSSH wire encoding) it backs `generate_keypair`, `sign`, and `verify`
device-key surface (`RelicarioError::DeviceKey` is the reserved variant, the same primitives the CLI uses to populate `.relicario/devices.json` and
`error.rs:84-88`); device-key signing currently happens in the server uses to verify pre-receive commit signatures. The corresponding
`relicario-cli` instead. error variant is `RelicarioError::DeviceKey`.
## Test architecture ## Test architecture