Commit Graph

55 Commits

Author SHA1 Message Date
adlee-was-taken
a6071b4c0c feat(cli): recovery-qr generate / unwrap subcommands
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-03 21:01:29 -04:00
adlee-was-taken
4d02a50cc8 chore(core): fix pre-existing clippy warnings (-D warnings gate)
Resolves pre-existing lint issues in imgsecret.rs, time.rs, totp.rs,
and crypto.rs that blocked the cargo clippy --workspace -D warnings
gate. No logic changes: loop-index → iterator, manual div_ceil →
.div_ceil(), manual range contains → .contains(), auto-deref cleanup.

Also fixes pre-existing warnings in relicario-cli (main.rs, session.rs,
device.rs, gitea.rs, helpers.rs, test helpers): dead_code suppression,
too_many_arguments, literal_with_empty_format_string, manual_char_cmp,
map_or → is_none_or, and repeat().take() → vec! in test helpers.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-02 19:32:45 -04:00
adlee-was-taken
006e67c361 fix(cli): cfg-gate RELICARIO_NO_GROUPS_CACHE to debug builds (audit S3)
The groups-cache opt-out is a developer debugging knob, not a
user-facing config. Gating the env-var lookup behind cfg!(debug_assertions)
makes release builds ignore the variable; the optimiser removes the
lookup entirely, so the variable name doesn't appear in release binary
strings output.

Doc-comments updated to reflect the new behaviour.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-05-02 18:51:15 -04:00
adlee-was-taken
6a1c6d5875 fix(core,cli): harden backup-restore tar unpack against path traversal (audit S2)
cmd_backup_restore previously called tar::Archive::unpack with default
settings, allowing malicious .relbak archives to escape the target
directory via .. entries, absolute paths, or symlinks. No size cap
meant tar bombs could exhaust disk space.

Replaced with relicario_core::safe_unpack_git_archive which:
- Rejects .. (ParentDir), absolute (RootDir), and drive-prefix
  (Prefix) components with "path traversal blocked" error.
- Rejects symlinks and hardlinks outright.
- Checks declared header size before reading body; rejects entries or
  cumulative totals exceeding the caller's cap.
- Returns (relative-path, bytes) pairs; the CLI re-checks
  dest.starts_with(git_dir) after OS-level path resolution.
- CLI cap: min(100 × compressed size, 1 GiB).

Acceptance: 5 unit tests in relicario-core (traversal, absolute path,
symlink, size bomb, happy path); existing CLI backup roundtrip tests
remain green.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-02 17:16:11 -04:00
adlee-was-taken
15d691abb2 feat(cli): implement device revoke
- Remove device from devices.json
- Append to revoked.json with timestamp and revoked_by
- Delete Gitea deploy key (best-effort, warns if env vars missing)
- Always commit both devices.json and revoked.json together
- Print revoked signing public key for audit confirmation
- Guard against revoking the current device (would lose push access)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-02 12:22:59 -04:00
adlee-was-taken
b1f9f2fbfc feat(cli): implement device add with signing + deploy key
- Create crates/relicario-cli/src/device.rs: local key storage under
  ~/.config/relicario/devices/<name>/, current-device tracking, and
  git signing config (gpg.format=ssh, user.signingkey, core.sshCommand)
- Add Device command to CLI with add/revoke/list subcommands
- cmd_device add: generates two ed25519 keypairs (signing + deploy),
  registers deploy key via Gitea API, stores keys at 0600, configures
  git SSH signing, updates .relicario/devices.json and commits
- Gitea config read from flags or RELICARIO_GITEA_{URL,TOKEN,OWNER,REPO}
- --no-gitea flag skips API registration for non-Gitea remotes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-02 12:19:55 -04:00
adlee-was-taken
7e07d5d664 feat(cli): add Gitea API client for deploy keys
Create, delete, and list deploy keys via Gitea REST API.
Foundation for device authentication.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-05-02 12:14:46 -04:00
adlee-was-taken
b9f44a3d4f fix(cli): enforce per-vault attachment bytes cap (audit I3)
per_vault_soft_cap_bytes and per_vault_hard_cap_bytes were defined in
VaultSettings but never checked. Now enforced in cmd_attach with
warning at soft cap, error at hard cap.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-05-02 09:34:33 -04:00
adlee-was-taken
d6703be2b1 fix(cli): sanitize item titles in commit messages (audit I1)
Control characters (newlines, tabs) in item titles corrupted git log
output. Now strips control chars and truncates to 50 chars.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-05-02 09:29:49 -04:00
adlee-was-taken
81f1f8ec31 fix(cli): validate IDs on backup restore (audit B4)
Crafted .relbak files with IDs like "../../.bashrc" could escape the
target directory. Now validates that item/attachment IDs are hex-only
via is_valid() before any fs::write.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-05-02 02:21:49 -04:00
adlee-was-taken
2739eb4194 fix(cli): gate test env vars with #[cfg(debug_assertions)] (audit B3)
RELICARIO_TEST_PASSPHRASE and friends were checked in production code,
exposing the passphrase via /proc/<pid>/environ and shell history.

Now only compiled into debug binaries via cfg(debug_assertions) helper
functions. Release builds compile the helpers to return None, so the
env var names are absent from the release binary (verified via strings).

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-05-02 01:46:13 -04:00
adlee-was-taken
315967f4a1 Merge feature/fullscreen-ux-phase-2a: smart-input affordances
Phase 2A of the fullscreen UX redesign — 8 form-level smart-input
affordances (URL fill-from-tab + hostname chip, group autocomplete,
password reveal + strength bar, TOTP live preview + QR decode, notes
monospace toggle), shared between popup and fullscreen vault tabs via
the new extension/src/shared/form-affordances/ module set.

CLI parity:
- relicario rate <passphrase> (zxcvbn score / guess estimate)
- relicario completions <SHELL> (bash/zsh/fish via clap_complete)
- --group <TAB> dynamic enumeration via .relicario/groups.cache
  (plaintext leak surface; opt out with RELICARIO_NO_GROUPS_CACHE=1)
- --totp-qr <path> on add login + edit (rqrr decode)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-01 22:37:18 -04:00
adlee-was-taken
8855078179 cli: --totp-qr <path> flag on add login + edit (rqrr decode)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-01 22:22:20 -04:00
adlee-was-taken
ed2d299a92 cli: add 'rate <passphrase>' subcommand (zxcvbn) 2026-05-01 19:53:29 -04:00
adlee-was-taken
f7e245d6b0 cli: write groups.cache for shell-completion --group enumeration
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-01 18:19:53 -04:00
adlee-was-taken
6cbd011705 cli: add 'completions <SHELL>' subcommand via clap_complete 2026-05-01 18:13:17 -04:00
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
adlee-was-taken
cf3960186c docs(core,cli): document implicit contracts flagged in code review
- import_lastpass.rs: note that password and extra are intentionally
  not trimmed (leading/trailing whitespace is significant for both).
- cmd_import_lastpass: document the coupling between the
  ImportWarning message strings and the CLI summary's "skipped"
  filter — partial-import warnings (TOTP/URL) must not contain
  the word "skipped".

Comment-only; no behavior change. Catches I1 and M5 from the
final code review without taking on the cross-cut WarningKind
enum refactor (deferred to a follow-up if it ever ships).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-30 18:55:46 -04:00
adlee-was-taken
d6831fcfd8 test(cli): integration coverage for import lastpass
Fixture CSV exercises 11 rows: standard login, login + TOTP,
SecureNote (plain + structured), unicode title, bad URL,
malformed rows. Tests verify item count, single git commit,
warning surface area, exit code, and ID uniqueness across
back-to-back imports.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-29 23:22:54 -04:00
adlee-was-taken
2fda9e0d50 feat(cli): cmd_import_lastpass — full data flow
Unlocks the vault, parses the CSV, encrypts each item, writes
items/<id>.enc and manifest.enc, then a single
`git add … && git commit` covers all of them. Stderr progress
every 50 items + final summary. Exit non-zero only when zero
items imported.
2026-04-29 23:16:07 -04:00
adlee-was-taken
ab8839a46a feat(cli): clap surface for import lastpass
Adds the Import command group with a Lastpass subcommand.
Stub returns `not implemented` so the help text is reachable
ahead of the body landing in Task 8.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-29 23:12:44 -04:00
adlee-was-taken
6d96ca8288 test(cli): humanize_age bucket boundaries + plural transitions
Locks the singular vs plural transition (1 minute ago vs 2 minutes
ago) and each bucket boundary (59→60s minutes, 3599→3600s hours,
86400→86400×2 days, etc.) so future tweaks can't silently regress
the user-facing labels.
2026-04-28 19:48:50 -04:00
adlee-was-taken
536ef2464b test(cli): tighten last-export label assertions to exact match
Drop the dead `stdout.contains("last export:")` + `.to_lowercase()` fallback
in status_shows_last_backup_line and status_shows_recent_backup_after_export;
assert `stdout.contains("Last export:")` verbatim instead.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 19:46:03 -04:00
adlee-was-taken
a32f13b63a feat(cli): status shows last export age
Reads .relicario/last_backup (written by cmd_backup_export). Format:
'never' for fresh vaults, '4 days ago' otherwise. Closes the
'is my backup stale?' question without leaving the terminal.
2026-04-28 19:42:10 -04:00
adlee-was-taken
bd7bef7ce4 test(cli): export/restore round-trip + error paths 2026-04-28 19:32:58 -04:00
adlee-was-taken
734325a31f feat(cli): cmd_backup_restore — unpack .relbak into target dir
Refuses non-empty target, prompts for backup passphrase, writes the
full vault layout, untars .git/ when bundled or git-inits a fresh
'restore from backup <iso8601>' commit otherwise.

Also tightens error context on tar_directory's builder.finish().
2026-04-28 19:25:45 -04:00
adlee-was-taken
7ce57353f2 feat(cli): cmd_backup_export — pack vault into .relbak
Reads the vault layout from disk, prompts for backup passphrase
(zxcvbn-gated, independent of the live vault key), tars .git/
unless --no-history, optionally bundles the reference JPEG, and
atomic-writes the .relbak. Leaves .relicario/last_backup marker
for cmd_status.
2026-04-28 19:21:02 -04:00
adlee-was-taken
b8dfcd0e97 feat(cli): clap surface for backup export/restore (handlers stubbed)
Adds 'relicario backup' as a subcommand wrapping export and restore.
Stubs return 'not yet implemented' — handlers land in Tasks 8 and 9.
The existing top-level 'relicario restore <query>' (un-trash) is
untouched.
2026-04-28 19:16:05 -04:00
adlee-was-taken
c66fd520f8 docs(arch): per-codebase ARCHITECTURE.md + cross-codebase overview
Strategic-depth architecture documentation, the kind that's hard to
recover by reading code: invariants, multi-file flows, design rationale,
gotchas. Goal is to cut the token cost for future Claude sessions.

Four new docs (2091 lines total):

- crates/relicario-core/ARCHITECTURE.md (514 lines) — bytes-in/bytes-out
  boundary, 24 verified invariants (VERSION_BYTE=0x02, length-prefixed
  KDF input, NFC normalization, content-addressed AttachmentId, history-
  tracked field kinds, 60% imgsecret confidence floor, MAX_DIMENSION=
  10000, etc.), 7 multi-module flows, 16 non-obvious gotchas (QUANT_STEP=
  50, central-70%-embed, BIP39-128bit-then-truncate, Steam alphabet
  rationale).

- crates/relicario-cli/ARCHITECTURE.md (539 lines) — module map for the
  three source files; the cmd_add/cmd_edit per-type helper pattern (post-
  2026-04-27 refactor); the hardened-git invariant (Command::new("git")
  is gated to helpers.rs:46); the five history synthetic keys; the env-
  var escape-hatch policy; cmd_generate's two-mode design (no-unlock
  outside vault, unlock-and-read-defaults inside).

- extension/ARCHITECTURE.md (831 lines) — five-bundle structure (popup,
  vault, setup, content, service-worker); SW-as-crypto-fortress model;
  capability-set-or-silent-rejection contract; vault-tab-as-popup-class
  router parity (commit a7dbf35); origin TOFU flow; setup state machine;
  test-vs-build gap.

- docs/architecture/overview.md (207 lines) — cross-codebase entry point.
  How the three codebases fit together, the four versioned wire formats
  between them (core→WASM ABI, SW chrome.runtime protocol, vault on-disk
  layout, GitHost API), per-codebase secret residency table, build
  matrix, conventions that span all three.

Specs in docs/superpowers/specs/ remain as historical decision artifacts
("why we chose this") — the new arch docs are the source of truth for
"what is" current invariants and flows.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-27 21:41:26 -04:00
adlee-was-taken
3f0f5b1b28 feat(cli): close audit gaps — TOTP edit, history, detach, status, generator defaults
One coherent CLI completeness pass driven by the 2026-04-27 state-of-the-
project audit. All TDD; 6 new integration tests (workspace 158→164).

Stubs and dead state fixed:
- TOTP edit was an explicit stub at main.rs:925 ("delete and re-add for
  now"). Now supports editing issuer, label, and rotating the secret;
  rotated secrets are pushed to field_history under core:totp_secret.
- VaultSettings.generator_defaults was stored but never read by the CLI.
  cmd_generate now consults it when invoked inside an initialized vault;
  explicit flags override. Behavior outside a vault unchanged.

New commands:
- relicario settings generator-defaults [--random|--bip39] [--length |
  --words | --symbols | --separator] — view/edit the stored generator
  defaults.
- relicario history <query> [--show] [--field <name>] — view captured
  field history. Values masked by default.
- relicario detach <query> <aid> — remove an individual attachment +
  blob. Refuses to drop a Document item's primary attachment.
- relicario status — vault summary: root path, item counts (active /
  trashed), attachment count + total bytes, registered device count,
  last commit (%h %s).

Internal refactor (pure mechanical, no behavior change):
- cmd_add: 217-line match split into one build_<type>_item helper per
  ItemCore variant + a 7-arm dispatcher.
- cmd_edit: same treatment — edit_login, edit_card, edit_totp, etc. The
  history-tracking ones take a &mut FieldHistory alias for clarity.

Existing tests cover the refactor; the new helpers are tested through
the same integration paths.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-27 21:13:30 -04:00
adlee-was-taken
aa1ad99e6e chore: bump version to 0.2.0 + add CHANGELOG
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-27 19:02:35 -04:00
adlee-was-taken
c3edf9d413 test(cli): vault_dir detection (L8) + v1 vault rejection
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-20 18:39:13 -04:00
adlee-was-taken
20350d509b test(cli): integration tests for edit/history, attachments, settings
Adds RELICARIO_TEST_ITEM_SECRET env hatch for rpassword calls in
cmd_add / cmd_edit so piped-stdin tests can exercise the password
prompt paths without a TTY.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-20 18:37:56 -04:00
adlee-was-taken
b263c27da9 test(cli): integration harness + basic flow tests
Uses assert_cmd + tempfile to spin up a fresh vault per test.
Covers init layout, add/list/get mask semantics, rm/restore/purge cycle,
and generate smoke. Adds RELICARIO_TEST_PASSPHRASE env-var hatch in
unlock_interactive and cmd_init so tests don't need a TTY.

Also fixes read_params in session.rs to correctly parse the nested
params.json format (kdf sub-object) rather than trying to deserialize
the whole file as KdfParams.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-20 18:32:45 -04:00
adlee-was-taken
8c315654ae feat(cli): device add / list / revoke rewired to hardened git 2026-04-20 17:32:09 -04:00
adlee-was-taken
a3871ac890 feat(cli): relicario sync — pull --rebase then push via hardened git 2026-04-20 17:31:45 -04:00
adlee-was-taken
10f249d95e feat(cli): relicario settings show / trash-retention / history-retention / attachment-cap 2026-04-20 17:31:27 -04:00
adlee-was-taken
a6bad4bb3e feat(cli): relicario generate delegates to core (audit H6)
CLI no longer has its own charset-sampling path — uses the CSPRNG
generate_password / generate_passphrase in relicario-core, which use
rand::distributions::Uniform internally.
2026-04-20 17:30:56 -04:00
adlee-was-taken
cbd1dbd706 feat(cli): attachment ops — attach / attachments / extract
Respects AttachmentCaps from settings.enc; content-addressed aid
comes from core::encrypt_attachment.
2026-04-19 22:27:13 -04:00
adlee-was-taken
b5015b3e9b fix(cli): trash empty unlocks vault once, not per item
Extracted purge_item helper so cmd_trash_empty loops over it without
re-prompting for passphrase per item. Single git commit per trash empty
summarizing the count. Caught in Task 12 review.
2026-04-19 22:25:57 -04:00
adlee-was-taken
cc279bac0b feat(cli): trash ops — rm / restore / purge / trash empty
Soft-delete sets trashed_at via Item::soft_delete; restore clears it.
Purge deletes item + attachment dir and removes manifest entry.
Trash empty scans for items past settings.trash_retention.
2026-04-19 22:24:32 -04:00
adlee-was-taken
06c8903e2b feat(cli): relicario edit — interactive field updates + history
Title/group/tags always optional. Per-type prompts for core secret
fields (Login.password, Card.number, Key.material, SecureNote.body)
push the old value to field_history via a synthetic core:<key>
FieldId so rotation is audit-traceable.
2026-04-19 22:22:45 -04:00
adlee-was-taken
377d73355b feat(cli): relicario list with --type/--group/--tag/--trashed filters
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-19 22:20:26 -04:00
adlee-was-taken
ed451041b0 feat(cli): relicario get with masking, --show, and zeroize-clipboard
Secrets masked by default (audit M7). --show reveals plaintext.
--copy writes to clipboard and spawns a detached 30s auto-clear
thread holding a Zeroizing copy that wipes on drop (audit M6).
2026-04-19 22:19:15 -04:00
adlee-was-taken
fe017455d3 feat(cli): relicario add — remaining 6 item types
SecureNote, Identity, Card, Key, Document (with inline attachment),
and Totp with base32 secret decoding. Document widens the commit
to include the attachment blob path.
2026-04-19 22:16:51 -04:00
adlee-was-taken
89b22cb089 feat(cli): relicario add login with flag + interactive prompting
Unlocks vault, builds LoginCore from flags (password via rpassword if
--password-prompt), saves item + manifest, commits via hardened git.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-19 22:13:17 -04:00
adlee-was-taken
5dce2c10f9 fix(cli): init stages salt, handles --output ..-paths, zeroizes image_secret
1. Add .relicario/salt to the initial git commit so it syncs across
   devices (Argon2 salt must match at unlock time).
2. Return a proper error instead of panicking when --output has no
   filename component (e.g., trailing ..).
3. Wrap the generated 32-byte image_secret in Zeroizing for
   consistency with the passphrase + master_key handling in Task 4.

Caught in Task 6 review.
2026-04-19 22:11:10 -04:00
adlee-was-taken
a50099a066 feat(cli): relicario init creates a format-v2 vault
Prompts for a strong passphrase (zxcvbn gate via core), generates a
32-byte image secret, embeds it in the carrier JPEG, writes the
standard vault skeleton, and makes an initial git commit via the
hardened git_command helper.
2026-04-19 22:02:53 -04:00
adlee-was-taken
15e6ed9c75 feat(cli): scaffold clap surface for all typed-item commands
Every subcommand from the Plan 1B CLI spec present; bodies return
'not yet implemented' so subsequent tasks land one command at a time.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-19 22:00:14 -04:00
adlee-was-taken
589d7b90b4 fix(cli): zeroize image_secret + correct atomic_write temp path
atomic_write now appends .tmp instead of replacing the extension
(manifest.enc.tmp, not manifest.tmp). image_secret is wrapped in
Zeroizing so both KDF inputs wipe on drop. Caught in Task 4 review.
2026-04-19 21:57:42 -04:00