Commit Graph

765 Commits

Author SHA1 Message Date
adlee-was-taken
33dd018ea6 merge: feature/v0.9.0-dev-a-org-foundation (v0.9.0 Dev-A org foundation: SW+WASM, device-key persist/restore, org read) 2026-06-27 13:25:55 -04:00
adlee-was-taken
7c6e6fe32a feat(ext): org error-copy entries for ERROR_COPY coverage gate
The full-suite meta-test (src/shared/__tests__/error-copy.test.ts) requires a
user-facing ERROR_COPY entry for every literal 'ok: false, error: <code>' the SW
returns. The org handlers added org_not_configured (literal) with no copy → RED.
Add org_not_configured plus copy for the org codes that reach the popup via
thrown err.message (not_an_org_member, device_key_unavailable, not_in_org_context,
org_switch_failed). item_not_found already had an entry. (Dev-B added some of
these independently; B reconciles the wording when it syncs main post-merge.)

Full extension vitest: 70 files / 478 tests green (incl. error-copy 3/3).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014s527M917W47LDrfQ4t47g
2026-06-27 13:15:21 -04:00
adlee-was-taken
eae07e778b merge: origin/main (Dev-E keyfile-ext + D-minors) into org-foundation
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
2026-06-27 11:22:17 -04:00
adlee-was-taken
7913e3ad84 fix(ext/sw): free org handle on openOrg error path (no org-key leak past lock)
Security review I1: openOrg's org_unwrap_key inserts the org master key into the
WASM session at step 7, but on the non-cached manifest-error path the handle was
never returned / registered via session.setOrg — so session.clearAll() (lock +
inactivity timeout) could not see it, and the org key would outlive a failed open
(GC-backstopped, non-deterministic). Triggers on a corrupt manifest.enc or a
first-switch network error with no cache. Free the handle before rethrowing.
Adds a test asserting the error path frees the handle.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014s527M917W47LDrfQ4t47g
2026-06-27 11:05:58 -04:00
adlee-was-taken
7932703bc1 docs(status): keyfile track complete (D+minors+E merged); org track in progress (A I1-fix gated, B building+config-add, C held)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013Hc6Rvdz3DxLucqNtPE2iP
2026-06-27 11:04:09 -04:00
adlee-was-taken
2ff5e5b584 merge: feature/v0.9.0-dev-e-keyfile-ext (v0.9.0 Dev-E keyfile extension + positioning) 2026-06-26 22:57:45 -04:00
adlee-was-taken
7a6cfb5659 test(ext/sw): cover handleUnlock keyfile error paths (forward-compat guard, invalid_key_file, key-file-not-set) 2026-06-26 22:51:10 -04:00
adlee-was-taken
98861218f9 docs(formats): pin KdfParams citation to crypto.rs:180 (line shifted post-merge) 2026-06-26 22:48:03 -04:00
adlee-was-taken
f9b8f3821d feat(ext/sw): org_switch + org read messages (grant-filtered, offline-aware, cache cleared on lock)
Wires 4 SW messages to the org-vault.ts read functions (Task 5b):
- org_switch: opens org vault via openOrg(), caches OrgHandleState, sets
  session context; switching to 'personal' resets context without openOrg.
  Offline-aware: if openOrg throws a network error and a prior cached state
  exists, reuses it read-only (offline:true) via exported isNetworkError.
- org_list_items: returns cached, grant-filtered OrgManifestEntry[] (not
  personal ManifestEntry[]) via listOrgItems(requireCurrentOrgState()).
- org_get_item: decrypts a single org item via getOrgItem.
- org_list_collections: returns granted Collection[] via listOrgCollections.

Three-place rule satisfied for all 4 messages:
1. messages.ts PopupMessage union (4 variants)
2. messages.ts POPUP_ONLY_TYPES (4 entries)
3. popup-only.ts switch (4 case arms)

Cache lifecycle / use-after-free guard:
- clearOrgCache() added to org-handlers.ts; exports.
- Called in popup-only.ts 'lock' handler (before session.clearCurrent).
- Called in index.ts inactivity-timer onExpired (before clearCurrent).

Plan doc fixes:
- Line ~410: org_list_items return type corrected to OrgManifestEntry[].
- Line ~47: org_unwrap_key signature updated (no device_private_key_base64
  param — reads DEVICE_STATE internally).

9 new tests in org-handlers.test.ts (TDD: all watched fail before passing).
Full SW suite: 167 tests (17 files) green. build:all type-checks clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014s527M917W47LDrfQ4t47g
2026-06-26 22:45:09 -04:00
adlee-was-taken
df3aa319e7 Merge remote-tracking branch 'origin/main' into feature/v0.9.0-dev-e-keyfile-ext 2026-06-26 22:33:45 -04:00
adlee-was-taken
2eb49ac217 feat(ext/sw): org-vault read core (openOrg, grant-filtered list, collection-scoped get, offline)
- Add extension/src/service-worker/org-vault.ts: openOrg (members.json fingerprint
  match → org_unwrap_key → org_manifest_decrypt_filtered), listOrgItems, getOrgItem
  (collection-scoped path: items/<collection>/<id>.enc), listOrgCollections (grants filter).
- Add OrgMember, OrgMembers, CollectionDef, OrgCollections, Collection types to shared/types.ts
  (mirrors relicario-core org.rs shapes).
- Add 8-test suite covering: fingerprint match, not_an_org_member, listOrgItems passthrough,
  collection-scoped item path, listOrgCollections grant filter, storage.set never called,
  offline fallback (TypeError → cached manifest), offline with no cache → rethrow.
- Fix plan doc line ~326: wasm.manifest_decrypt → wasm.org_manifest_decrypt_filtered.

Gate results: 158/158 SW tests pass; npm run build:all clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014s527M917W47LDrfQ4t47g
2026-06-26 22:30:39 -04:00
adlee-was-taken
73877bad78 docs: pin keyfile.rs/second_factor citations; state the second factor is never pushed to the remote 2026-06-26 22:24:57 -04:00
adlee-was-taken
288956c089 feat(core,wasm): filter_by_collections single-source + org_manifest_decrypt_filtered
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
2026-06-26 22:19:55 -04:00
adlee-was-taken
201bf05adc feat(ext/setup+sw): attach via key file when the vault uses one (probe second_factor, .relkey picker, handleAttachVault keyfile branch)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014THSV6cA4Gxa7bxFfisHBB
2026-06-26 22:19:41 -04:00
adlee-was-taken
0d56b3333b feat(ext/sw): persist device key at register, restore at unlock (+ migration)
- Add `persistDeviceKey(w, handle)` to vault.ts: calls
  `w.persist_device_key(handle)` and stores the ciphertext as base64
  in chrome.storage.local `device_key_enc`.
- Wire into `registerDeviceAndPersistConfig` (create + attach flows)
  so the key is persisted immediately after every `register_device`.
- Wire into `register_this_device` handler via `session.requireCurrent()`.
- Restore at unlock: if `device_key_enc` present, call
  `w.restore_device_key(handle, blob)`; failure is caught + logged but
  must not block unlock.
- Migration for pre-4.5b installs (absent `device_key_enc`): re-register,
  persist new key, addDevice to remote (best-effort, must not block).
  New device identity — org members must be re-granted to new pubkey.
- 4 new tests in device-key-persist.test.ts; existing 146 tests updated
  and kept green; router.test.ts register_this_device stub extended.
2026-06-26 22:13:50 -04:00
adlee-was-taken
2f7a034a1b feat(ext/setup): download the generated .relkey at finish + skip carrier gate in keyfile mode
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014THSV6cA4Gxa7bxFfisHBB
2026-06-26 22:07:14 -04:00
adlee-was-taken
e6ec210736 fix(wasm): zeroize transient device-private-key copy in export_state_bytes
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
2026-06-26 22:01:00 -04:00
adlee-was-taken
d3f46389e4 feat(ext/sw): unlock resolves second factor from params hint (keyfile branch)
Extract handleUnlock from popup-only.ts handle() switch, add keyfile branch
(keyfile_decode + unlock_with_secret), forward-compat guard for unknown
second_factor values, loadKeyfileBase64 helper, loadSetupState isConfigured
fix for keyfile vaults. Register error copy for three new error codes.
Un-skip Task 4 TDD test — 440/440 green, build clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014THSV6cA4Gxa7bxFfisHBB
2026-06-26 21:59:17 -04:00
adlee-was-taken
2262272b20 merge: fix/v0.9.0-keyfile-minors (v0.9.0 Dev-D keyfile security-review minors) 2026-06-26 21:54:05 -04:00
adlee-was-taken
be619f8ed0 feat(wasm): persist/restore device key under master key; org_unwrap_key reads DEVICE_STATE
- device.rs: add PersistedDeviceState (serde mirror), export_state_bytes
  (plaintext JSON in Zeroizing<Vec<u8>>, NOT a wasm_bindgen export),
  import_state_bytes (repopulates DEVICE_STATE with Zeroizing<String> keys),
  signing_seed (extracts ed25519 seed via core::device::extract_ed25519_seed)
- lib.rs: add persist_device_key (#[wasm_bindgen], encrypts state bytes under
  master key via relicario_core::crypto::encrypt; ciphertext only reaches JS),
  restore_device_key (#[wasm_bindgen], decrypts inside WASM then calls
  import_state_bytes; plaintext never leaves WASM)
- lib.rs: refactor org_unwrap_key to drop device_private_openssh param; reads
  seed from DEVICE_STATE via device::signing_seed()
- lib.rs: update org_unwrap_key_yields_a_session_that_decrypts_org_blobs to
  new signature (register_device first, use signing_pub for wrap_org_key)
- lib.rs: add device_persist_tests module (4 tests: round-trip, org-unwrap
  post-restore, ciphertext-not-plaintext, no-device-error)
- lib.rs: add DEVICE_TEST_LOCK static to serialize DEVICE_STATE-touching tests
- lib.rs: add #[derive(Debug)] to SessionHandle
- wasm.d.ts: update org_unwrap_key sig; declare persist_device_key +
  restore_device_key

All gates: cargo test -p relicario-wasm (15/15), cargo test -p relicario-core
(no regression), cargo build --target wasm32-unknown-unknown (clean), clippy
-D warnings (clean).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014s527M917W47LDrfQ4t47g
2026-06-26 21:53:24 -04:00
adlee-was-taken
5cc16ddffb feat(ext/sw): create_vault key-file mode (generate secret, unlock_with_secret, store keyfileBase64)
- Add KEYFILE_PARAMS_JSON const (same KDF params as default + second_factor:"keyfile" hint)
- Generalize registerDeviceAndPersistConfig: takes factorStorage union instead of
  referenceImageBytes Uint8Array; spreads { imageBase64 } or { keyfileBase64 } into
  chrome.storage.local.set so exactly one factor key is present
- Branch handleCreateVault on msg.secondFactor === 'keyfile': skip carrier-image
  guards and embed_image_secret; call unlock_with_secret + keyfile_encode; share
  the file-write / device-registration / session tail with the image path
- Update handleAttachVault call to registerDeviceAndPersistConfig to pass
  { imageBase64: uint8ArrayToBase64(referenceImageBytes) }
- Relax carrierImageBytes to optional in messages.ts create_vault request type
- Skip Task-4 describe block in keyfile-unlock.test.ts (un-skip in Task 4)

TDD: Task-2 test RED → GREEN; Task-4 describe.skipped; full suite 439 passed/1 skipped; build clean

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014THSV6cA4Gxa7bxFfisHBB
2026-06-26 21:49:09 -04:00
adlee-was-taken
b2bed1c50e fix(core): zeroize keyfile_encode output + lock-in tests for backup-keyfile-error and malformed-vs-wrong unlock
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01McMF5pUJbCMricmFEnf2sG
2026-06-26 21:45:14 -04:00
adlee-was-taken
ce141480b9 merge: origin/main (Dev-D keyfile, d649203) into org-foundation
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
2026-06-26 21:35:56 -04:00
adlee-was-taken
6970893713 Merge remote-tracking branch 'origin/main' into feature/v0.9.0-dev-e-keyfile-ext 2026-06-26 21:35:39 -04:00
adlee-was-taken
2c5af671d0 docs(coordination): v0.9.0 dev resume prompts after ~21h stall (D merged; A/B/C/E resume)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013Hc6Rvdz3DxLucqNtPE2iP
2026-06-26 21:29:56 -04:00
adlee-was-taken
d649203335 docs(status): Dev-D keyfile core/cli/wasm merged (588495f); keyfile-minors follow-up tracked
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013Hc6Rvdz3DxLucqNtPE2iP
2026-06-26 00:45:50 -04:00
adlee-was-taken
588495fd7b merge: feature/v0.9.0-dev-d-keyfile-core (v0.9.0 Dev-D keyfile core/cli/wasm) 2026-06-26 00:39:55 -04:00
adlee-was-taken
13512ed9bf docs: record v0.9.0 org-write spike decision + device-key prereq (PM)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013Hc6Rvdz3DxLucqNtPE2iP
2026-06-26 00:39:54 -04:00
adlee-was-taken
110f02e4e3 refactor(cli): drop unused param from resolve_second_factor_secret (simplify pass)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01McMF5pUJbCMricmFEnf2sG
2026-06-26 00:19:55 -04:00
adlee-was-taken
2203d817ec fix(cli): resolve second factor via shared helper for recovery-qr + backup (key-file parity); wasm equivalence negative control
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01McMF5pUJbCMricmFEnf2sG
2026-06-26 00:11:50 -04:00
adlee-was-taken
23c8bbea65 test(cli): assert .relkey never enters git tree + init failure-path coverage + display polish
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01McMF5pUJbCMricmFEnf2sG
2026-06-25 23:43:31 -04:00
adlee-was-taken
f5336e352f feat(ext/sw): org config storage + org_list_configs message
Introduces OrgConfig/OrgConfigSummary types, chrome.storage.local backing
via org-config.ts, and the first org SW message (org_list_configs) wired in
all three required places: PopupMessage union, POPUP_ONLY_TYPES set, and a
case arm in popup-only.ts. Handler projects to OrgConfigSummary — apiToken,
hostUrl, repoPath, memberId never leave the SW. 3 new TDD tests (RED→GREEN).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014s527M917W47LDrfQ4t47g
2026-06-25 23:40:44 -04:00
adlee-was-taken
2ecd98208d feat(wasm,ext): org_manifest_decrypt/encrypt over WASM + faithful OrgManifest TS mirror
- Add `org_manifest_decrypt` + `org_manifest_encrypt` to relicario-wasm/src/lib.rs,
  placed after `org_unwrap_key`; mirror the existing manifest_decrypt/manifest_encrypt
  pattern exactly (need_key guard, session::with, js_value_for). Core import path:
  relicario_core::decrypt_org_manifest / relicario_core::encrypt_org_manifest (crate root).
- Add round-trip test `org_manifest_round_trip_via_handle` in org_tests: encrypt via
  WASM wrapper, decrypt via core directly (avoids serde_wasm_bindgen off-wasm).
  Asserts collection/title/tags/schema_version survive the round-trip; asserts nonce
  uniqueness. TDD: RED (compile error) → GREEN confirmed.
- Declare org_manifest_decrypt/org_manifest_encrypt in extension/src/wasm.d.ts.
- Add faithful/lean OrgManifestEntry + OrgManifest TS interfaces to
  extension/src/shared/types.ts: {id, type, title, tags[], modified, trashed_at?,
  collection} — no personal-only fields; collection required. Mirrors org.rs:184-202.
- Plan-doc: rewrite Task 2 body (re-scope rationale + checked steps), fix File
  Structure + Architecture lines, amend Hand-off contract (org_list_items ->
  OrgManifestEntry[]; org items remain personal Item).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014s527M917W47LDrfQ4t47g
2026-06-25 23:33:29 -04:00
adlee-was-taken
2daa3502e9 feat(cli): init --key-file generates a .relkey second factor
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
2026-06-25 23:32:41 -04:00
adlee-was-taken
f85dc6729e fix(ext/sw): best-effort clearAll + setOrg frees displaced handle
clearAll is the security-zero path: each .free() in its own try/catch so a
single throwing handle can't leave the others un-zeroed; state is fully reset
regardless. setOrg frees any prior handle for the same org before overwriting
so a re-unlock can't orphan an un-zeroed WASM key. Tests updated to the
best-effort contract (session.test.ts) plus independent-mock, throwing-free,
and re-unlock cases (org-session.test.ts).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014s527M917W47LDrfQ4t47g
2026-06-25 21:40:37 -04:00
adlee-was-taken
e8f6635188 feat(cli): unlock resolves second factor from params hint (image|keyfile)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01McMF5pUJbCMricmFEnf2sG
2026-06-25 21:37:59 -04:00
adlee-was-taken
c79ff1caf5 scaffold(ext): create_vault secondFactor request field + relkeyBytes response (held types) 2026-06-25 21:35:34 -04:00
adlee-was-taken
6bdb7cf262 feat(ext/sw): multi-context session (personal + orgs), clearAll zeroes all
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014s527M917W47LDrfQ4t47g
2026-06-25 21:29:08 -04:00
adlee-was-taken
9e19949880 docs: fix ParamsFile citation (cli/session.rs:117, not core/crypto.rs) + README scope header 2026-06-25 21:28:10 -04:00
adlee-was-taken
6cb12990ec feat(wasm): unlock_with_secret + keyfile encode/decode bindings + equivalence proof
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
2026-06-25 21:24:10 -04:00
adlee-was-taken
1591fc7825 docs: re-lead positioning on the two-factor-KDF thesis; document the key-file second factor
README: open with the two-independent-secrets-into-KDF thesis; reframe the
second factor as pluggable (reference image or key file); keep the stego
deep-dive as a distinctive option; update all scenario tables, recovery section,
env-var hint, and roadmap line to use "second-factor secret" language.

docs/CRYPTO.md: add "Pluggable second-factor containers" subsection explaining
that the 32-byte input to Argon2id is identical regardless of container;
reference-image / key-file / recovery-QR are interchangeable carriers.
Update org-vs-personal comparison table and entropy labels.

docs/FORMATS.md: document the .relkey armor format (relicario-keyfile-v1 +
base64 line) and the params.json second_factor field ("image"|"keyfile",
absent means "image" for back-compat). Cite crates/relicario-core/src/keyfile.rs
keyfile_encode/keyfile_decode with TODO-pin comments. Update KDF input formula
from image_secret to second_factor_secret.

DESIGN.md: split secrets-map "Reference image bytes" row to distinguish image
and key-file containers; add key-file row noting keyfileBase64 in
chrome.storage.local has the same in-the-clear posture as imageBase64.
Update vault-layout note and KDF conventions row.

docs/SECURITY.md: add "Second-factor storage posture" subsection explicitly
stating .relkey and keyfileBase64 are the second factor in the clear — same
posture as the reference JPEG / imageBase64, not weaker, protected by the
passphrase invariant. Add RELICARIO_KEYFILE to the env-var trust surface table.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014THSV6cA4Gxa7bxFfisHBB
2026-06-25 21:24:06 -04:00
adlee-was-taken
eabc93bd84 fix(core): zeroize keyfile decode intermediate + clearer errors + error-arm tests 2026-06-25 21:15:11 -04:00
adlee-was-taken
92febf38f1 refactor(core,wasm): zeroized ed25519 seed decode in core; org_unwrap_key reuses it
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
2026-06-25 21:13:49 -04:00
adlee-was-taken
3b7099c6f4 feat(core): key-file armor (relicario-keyfile-v1) encode/decode
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01McMF5pUJbCMricmFEnf2sG
2026-06-25 21:09:40 -04:00
adlee-was-taken
a25f8e8a30 feat(ext/setup): second-factor container choice (image | key file) 2026-06-25 21:06:58 -04:00
adlee-was-taken
d55861ffd8 feat(wasm): org_unwrap_key — ECIES unwrap into a session handle
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
2026-06-25 21:02:54 -04:00
adlee-was-taken
b08b388d5d feat(core+cli): SecondFactor hint on ParamsFile (default image, back-compat)
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
2026-06-25 21:02:13 -04:00
adlee-was-taken
79284979c1 docs(coordination): v0.9.0 multi-agent kickoff prompts (PM + 5 devs)
Generate the kickoff prompts for the v0.9.0 lift — two independent tracks
sharing the tag, each internally sequenced:

  org track:      dev-a (SW+WASM foundation) -> dev-b (read UI) -> dev-c (write)
  keyfile track:  dev-d (core/cli/wasm)      -> dev-e (extension + positioning)

- PM prompt encodes start-order gating (A+D start now; C runs its GO/NO-GO
  signed-commit spike now; B holds for A; E holds for D; C-writes hold for A+B),
  the Gitea git-merge mechanism (gh is unusable here — merge via git --no-ff),
  per-stream judgment calls, and the pre-tag checklist incl. the release.js
  version-check caveat (relicario-server's independent 0.1.x line).
- Each dev prompt: worktree setup, force-cd subagent rule, relay polling cadence,
  scope/out-of-scope partition, hard rules, final tests, push (no gh pr create).
- v0.9.0-launch.sh: relay health-check + tmux session (pm + dev-a..dev-e).

Relay already supports dev-a..dev-f (no queue.ts/server.ts change needed).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pe8qw5KePDqAEBsAxnVQuJ
2026-06-25 20:39:57 -04:00
adlee-was-taken
5a5cf04b8f release: v0.8.2 — extension vault-creation fix (binary transport)
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
v0.8.2
2026-06-25 20:26:36 -04:00
adlee-was-taken
d5dd550d4d fix(build): clippy 1.93.0 compliance
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
2026-06-25 20:26:26 -04:00