A-vs-E keep-both integration in 3 security-sensitive SW files:
- shared/messages.ts: kept E's attach_vault keyfile second-factor fields
(secondFactor/referenceImageBytes?/keyfileBytes?) AND my 4 org message types.
- vault.ts: registerDeviceAndPersistConfig auto-merged to take BOTH E's
factorStorage (image|keyfile) AND my handle (Task 4.5 device-key persist);
both create/attach call sites pass (factorStorage, deviceName, h) so each
flow does E secret-gen AND my register+persist in one coherent path.
- router/popup-only.ts: took E's 'unlock' -> handleUnlock refactor and moved my
device-key restore + migration into handleUnlock's shared tail (after
setCurrent, before manifest fetch) so it runs for both image + keyfile unlock.
- keyfile-unlock.test.ts: makeWasm mock gains persist_device_key + restore_device_key
stubs (the merged create/attach/unlock flow now calls them) — no assertion weakened.
Gates: SW vitest 175/175 (18 files); cargo core+wasm 243/0; wasm32 build clean;
clippy -D warnings clean; build:all clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014s527M917W47LDrfQ4t47g
Add OrgManifest::filter_by_collections(&[String]) as the single source of
grant-filter logic. Refactor filter_for_member to delegate to it (no
duplicated loop). Add org_manifest_decrypt_filtered WASM binding that
decrypts + filters in core so ungranted entries never cross to JS (phase-1
SOFT/UX filter; per-collection crypto isolation is phase-2). Declare the
binding in extension/src/wasm.d.ts. Vec<String> compiled cleanly for
wasm32-unknown-unknown.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014s527M917W47LDrfQ4t47g
Review fix (Important secret-hygiene finding): export_state_bytes built a
PersistedDeviceState via state.signing_private.as_str().to_owned(), creating
non-zeroized heap copies of the device ed25519 private keys (signing + deploy)
that lingered as plaintext residue after the struct dropped.
- Cargo.toml: enable zeroize "derive" feature (zeroize_derive already in lock tree)
- device.rs: PersistedDeviceState now derives Zeroize + ZeroizeOnDrop, wiping all
String fields on every drop path (success, serde_json::to_vec ? error path, panic)
- device.rs: import_state_bytes uses std::mem::take per field (ZeroizeOnDrop adds a
Drop impl, so fields can no longer be moved out — E0509); mem::take transfers the
heap buffers without copying, leaving empty Strings the drop-zeroize no-ops
- doc-comments updated to describe the ZeroizeOnDrop / mem::take hygiene
Hygiene-only; no behavior change. Gates: cargo test -p relicario-wasm 15/15,
clippy -D warnings clean, build --target wasm32-unknown-unknown clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014s527M917W47LDrfQ4t47g
Additive conflict in extension/src/wasm.d.ts resolved by keeping BOTH the
org_* declarations (org_unwrap_key/org_manifest_decrypt/org_manifest_encrypt)
and the keyfile_* declarations (unlock_with_secret/keyfile_encode/keyfile_decode).
Rust lib.rs (core + wasm) auto-merged cleanly with both feature sets.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014s527M917W47LDrfQ4t47g
Add `--key-file <path>` to `relicario init` as a mutually exclusive
alternative to `--image`/`--output`. When given, a 32-byte secret is
generated with OsRng, armored via `keyfile_encode`, written to the
supplied path (gitignored, never committed), and `params.json` records
`"second_factor": "keyfile"`. The `--image` arg is now Option<PathBuf>;
existing image-vault callers are unchanged. `ParamsFile::for_new_vault`
now takes the second-factor variant as an explicit argument.
Tests: un-ignore init_keyfile_then_unlock_keyfile_round_trips (full
round-trip), strengthen its assertion to check for distinctive username
"octocat", and add init_keyfile_writes_relkey_and_keyfile_params
(armor, params parse, gitignore). Full workspace green (0 failures),
clippy clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01McMF5pUJbCMricmFEnf2sG
Adds three WASM bindings for the pluggable second-factor work:
- unlock_with_secret: mirrors unlock() but accepts a raw 32-byte secret
instead of a carrier image, skipping imgsecret::extract
- keyfile_encode: armors a 32-byte secret into relicario-keyfile-v1 format
- keyfile_decode: decodes keyfile armor back to the raw 32-byte secret
Adds native equivalence test proving that unlock_with_secret and unlock
derive an identical Argon2id master key when given the same secret,
passphrase, salt, and KDF params. Comparison is direct (master-key bytes),
not cross-decrypt, so the test is both native-safe and unambiguous.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01McMF5pUJbCMricmFEnf2sG
Review fixes for Task 1 (org_unwrap_key). Public signature unchanged.
- Add device::extract_ed25519_seed -> Zeroizing<[u8;32]>: length-checks the
slice then copy_from_slice straight into the Zeroizing buffer, so the raw
seed never lands in a plain [u8;32] (Important #1).
- Refactor device::sign() to call the new helper (pure refactor; device/sign
roundtrip tests unchanged and green) (Important #2).
- org_unwrap_key now calls relicario_core::device::extract_ed25519_seed
directly; drop the duplicated local decode_device_seed helper and remove the
redundant ssh-key dep from relicario-wasm (core already pins it) (Important #2).
- Device-key-form finding (OpenSSH PEM, not base64; spec name matches reality)
moved onto the core helper, condensed copy kept on org_unwrap_key.
- Test helper keeps the private key as Zeroizing<String> and passes &priv
(derefs to &str) — no plain-String copy (Minor #3).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014s527M917W47LDrfQ4t47g
Expose org_unwrap_key(keys_blob, device_private_openssh) to let the
extension SW unwrap a member's ECIES-wrapped org master key into an
ordinary Zeroizing session handle. The returned handle is key-agnostic
and works with the existing item_decrypt/manifest_decrypt AEAD.
Key-form finding: device private keys are OpenSSH PEM blobs produced by
relicario_core::device::generate_keypair(), not base64 raw bytes. The
plan's "device_private_key_base64" parameter name was misleading; all
code and the .d.ts declaration use "device_private_openssh" to match
the spec and reality.
Adds ssh-key dep to relicario-wasm for PEM parsing in decode_device_seed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014s527M917W47LDrfQ4t47g
Add SecondFactor enum (Image | Keyfile, serde lowercase) to relicario-core::crypto,
re-export from lib.rs alongside KdfParams. Add #[serde(default)] second_factor field
to ParamsFile in relicario-cli::session so existing params.json files (no field) still
parse as Image. for_new_vault() initialises the field to Image. No unlock logic
changed; that is deferred to Task 4. TDD: tests written RED first, then GREEN.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01McMF5pUJbCMricmFEnf2sG
Extension-only patch: fixes vault create/attach in the browser extension, where
chrome.runtime.sendMessage silently dropped ArrayBuffer payloads (carrier JPEG →
0 bytes → opaque "no SOF marker found in JPEG"). See CHANGELOG for the full set.
- Version bumps: extension 0.7.0→0.8.2 (it had no changes in 0.8.0/0.8.1);
relicario-core/-cli/-wasm 0.8.1→0.8.2 for unified-tag consistency. The Rust
core/CLI are functionally unchanged from v0.8.1. relicario-server stays 0.1.1
(independent version line; no functional change).
- CHANGELOG.md: v0.8.2 section. STATUS.md / ROADMAP.md: v0.8.2 landed, v0.9.0
flagged as ready to kick off.
Verify: cargo build/test/clippy -D warnings all green (35 test binaries);
extension 435/435 vitest, build:all clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pe8qw5KePDqAEBsAxnVQuJ
clippy 1.93.0 added/tightened several lints that broke `cargo clippy -D warnings`
(the release gate). All behavior-preserving:
- unnecessary_map_or: `signer_parent.map_or(false, |r| …)` → `is_some_and`
(relicario-server/src/main.rs).
- needless_question_mark: drop redundant `Ok(… ?)` wrappers around
`serde_json::from_str(…).context(…)` in org_session.rs load_meta/load_members/
load_collections.
- print_literal: inline the trailing "FLAG" header literal in `org audit`.
- items_after_test_module: move `configure_git_signing` above the
`seed_helper_tests` module in device.rs so the test module is last.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pe8qw5KePDqAEBsAxnVQuJ
Closes the minor coverage gaps from the final adversarial review:
- org add card/key/totp reject ungranted + unknown collections (pins the
grant gate on the new write paths, which runs before any secret prompt)
- secure-note --body-stdin masks body; totp --secret-stdin round-trips
(completes the --*-stdin matrix for the org surface)
- key-material edit accept-branch round-trip, verified via get --show
B3 dropped the flat --username/--url/... flags from `org edit`, so the
ungranted-member denial test must drive the bare interactive form. The
ungranted member is now rejected at manifest lookup (filter_for_member +
resolve_org_query) before any prompt is read.
Adds run_stdin + create_collection_and_grant fixture helpers and three
acceptance tests for org add card/key/totp. Red until B1/B2 wire the
subcommands (currently: unrecognized subcommand). Asserts org get masks
card number + key material without --show. Edit round-trips land with B3.
- edit_secure_note / edit_key now call the module's resolve_secret_multiline
instead of open-coding the eprintln-hint + read-to-EOF pattern (the helper
exists precisely to centralize this; build_secure_note/build_key already use it).
- drop redundant fn-local imports: `use zeroize::Zeroizing;` from the five edit_*
helpers and the re-imported `TotpAlgorithm` from edit_login/build_login
(all covered by module-level imports; leftover from the verbatim A2/A3 move).
- build_login passes the password_stdin flag through to resolve_secret_line for
consistency with build_card/build_totp (behavior identical — that branch is
only reached when password_stdin is true).
- restore #[allow(clippy::too_many_arguments)] on build_totp (8 args; the old
build_totp_item carried the same allow — signature is frozen for B/C).
A3 routed personal `add` through the shared item_build builders, which use
prompt_secret / resolve_secret_*; the generic single-line prompt() and
prompt_optional() lost their last callers. read_required_line /
read_optional_line stay (used by prompt_or_flag*).
Cover two authz gaps left by the B9-B14 org item-CRUD work:
1. Grant-DENIAL on the read/mutate-by-query commands. A second member
added with their own device key but NOT granted `prod` is rejected by
every one of `org get`, `edit`, `rm`, `restore`, and `purge`, and
`org get` (with and without --show) leaks no plaintext. Previously
only `org add` had a denial test. Also asserts the item is untouched
afterward (owner still reads the original password/username).
2. SecureNote body masking: `org get <note>` prints `********` and not
the body; `org get <note> --show` reveals it. Mirrors the existing
Login-password masking assertions in org_items.rs.
New tests/org_authz.rs reuses the multi-member `Dev` harness pattern
from org_lifecycle.rs (one XDG config home + ed25519 device key per
member), so a second member joins with their own keypair.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RXpTHcQzw1n8qjYwZqruzQ
Pre-stages the A5 living-docs sweep for the already-merged A (relicario-core org
module) + C (relicario-server pre-receive hook) + CLI admin/rotate/status-audit
work, so the final A5 sweep (after Dev-B B9-B14 merges) is fast.
Adds org sections to docs/FORMATS.md (org repo wire formats + wrapped-key blob
layout), docs/CRYPTO.md (ECIES X25519 wrap/unwrap, no-Argon2id contrast, rotate
re-encryption), docs/SECURITY.md (signature-verifying hook, owner-only elevation,
audit vocabulary, honest limitations), DESIGN.md (org-master-key secrets row +
server org mode + deps), core/cli ARCHITECTURE.md (org module + org_session), and
an Unreleased CHANGELOG entry.
B item-CRUD (org add/get/list/edit/rm/restore/purge + main.rs wiring) and extension
parity are left as explicit TODO. STATUS/ROADMAP mark-shipped and
extension/ARCHITECTURE are deferred to the full A5 (track not yet landed; Dev-D
deferred). All cited code constants pinned with file:line per living-docs discipline.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TJo44YM3UbBjro2fG6NrKy