Commit Graph

153 Commits

Author SHA1 Message Date
adlee-was-taken
eed11acba2 feat(ext/popup): snapshot activeTab at popup-open for fill_credentials (audit M5)
Extend PopupState with {capturedTabId, capturedUrl} populated via
chrome.tabs.query({active: true, currentWindow: true}) in init().
These are later passed with fill_credentials so the SW can verify
the captured tab's hostname hasn't changed out from under the user
before forwarding credentials. Combined with expectedHost in the
forwarded payload + content-side re-check in fill.ts, this closes
the TOCTOU window on the popup → SW → content fill path.

popup.ts stays under @ts-nocheck (Slice 6 removes it alongside the
item-* rewrites).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 20:37:49 -04:00
adlee-was-taken
14397b33f0 feat(ext/content): closed Shadow DOM for icon/picker/TOFU + close fill TOCTOU
Two security fixes bundled together because they all live on the
icon-click/fill path:

1. Icon + picker + TOFU hint now render inside closed-mode Shadow DOM
   (via shadow.createShadowHost). Page scripts can no longer find our
   overlay via document.querySelector or rewrite buttons.

2. Icon's get_autofill_candidates call drops the `url` field — router
   derives origin from sender.tab.url. Similarly get_credentials.

3. Icon's get_credentials response handling was buggy: the response is a
   discriminated union { requires_ack, hostname } | { username, password }
   and the old code always read .username (→ undefined when requires_ack).
   New code dispatches on the `requires_ack` marker and either shows an
   in-page TOFU hint or fills directly.

4. fill_credentials is popup-only in the router — the icon click cannot
   (and MUST NOT) issue it from content. The new flow calls fillFields()
   directly after get_credentials returns the plaintext: the content
   script IS the origin, so no SW round-trip is needed for the typing.

5. TOCTOU on the popup → SW → content fill path: the SW verified the
   captured tab's hostname matched capturedUrl, then forwarded blindly.
   Between that check and chrome.tabs.sendMessage delivery, the tab can
   navigate; chrome.tabs.sendMessage delivers to whatever content-script
   principal is loaded at send-time. Closed by:
   - Router forwards { expectedHost: currentHost } in the payload.
   - fill.ts re-checks location.href.hostname === expectedHost before
     typing anything; on mismatch replies { ok: false, error: 'origin_changed' }
     and types nothing.

6. Remove @ts-nocheck from icon.ts, fill.ts, and detector.ts — all three
   now type-check clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 20:37:25 -04:00
adlee-was-taken
8cc1e777be feat(ext/content): closed Shadow DOM + textContent for capture prompt
Previously the capture prompt was a normal <div> appended to document.body
with innerHTML assembly. Any page script could find it via
document.querySelector('#relicario-capture-prompt') and either scrape
values or rewrite the buttons — and the innerHTML pattern meant hostname
interpolation was a latent XSS path (escapeForHtml helped but one mistake
would break it).

- Add content/shadow.ts — createShadowHost() with mode: 'closed', host.style.all = 'initial'.
- Rewrite capture.ts to mount inside the shadow root, build DOM via
  createElement + textContent only, never innerHTML.
- Drop the `url` field from check_credential / blacklist_site — the router
  now derives origin from sender.tab.url (Slice 3 contract).
- Update add_entry / update_entry calls to add_item / update_item with the
  new typed Item + LoginCore shape.
- Swap RelicarioSettings → DeviceSettings.
- Remove @ts-nocheck — the file type-checks clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 20:35:36 -04:00
adlee-was-taken
fbb64729ce feat(ext/popup): open setup via chrome.tabs.create, drop setup view from popup
The popup is too constrained for multi-step setup (Chrome closes it when
focus shifts to a file picker). Previously the popup rendered a pass-through
setup-wizard component that itself opened setup.html in a tab. Cut the
middleman: if not configured, directly chrome.tabs.create the setup page
and window.close() the popup.

- Remove 'setup' from the View union and the setup case from render().
- Delete setup-wizard component entirely — setup.html is the canonical flow.
- Drop renderSetupWizard import.

The @ts-nocheck stays on popup.ts until Slice 6 (item-* rewrites).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 20:33:49 -04:00
adlee-was-taken
2ff3ab1d7f feat(ext): drop setup.html / wasm from web_accessible_resources (audit C1)
setup.html is opened via chrome.tabs.create using a chrome-extension:// URL
which doesn't require WAR. WASM is bundled into service-worker.js/setup.js
and never fetched from a web page origin. Leaving them in WAR would expose
their URLs to any origin for probing/fingerprinting; shipping an empty WAR
array closes the surface.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 20:33:09 -04:00
adlee-was-taken
0cef607859 fix(ext/build): exclude test files from webpack tsc compile
Slice 4 spec review caught: router.test.ts's narrow chrome.* shim
triggered 4 TS errors in webpack's ts-loader pass during production
builds (partial mocks don't match the full chrome.* type surface).

Plan's verbatim test body assumes tests aren't part of the build
compile. Add src/**/__tests__/** to tsconfig exclude — tests still
compile under Vitest's independent ts pipeline (42/42 passing).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 20:25:53 -04:00
adlee-was-taken
3d2b021cb2 test(ext): vitest + router sender-check + origin-bound autofill 2026-04-20 20:15:49 -04:00
adlee-was-taken
2d4dcb5f6b feat(ext/sw): collapse flat index onto router 2026-04-20 20:11:59 -04:00
adlee-was-taken
56ab58cbe9 feat(ext/sw): router index with sender-based dispatch 2026-04-20 20:11:20 -04:00
adlee-was-taken
be32ea13c6 feat(ext/sw): router/content-callable handlers with origin derivation 2026-04-20 20:11:02 -04:00
adlee-was-taken
533bfd5bea feat(ext/sw): router/popup-only handlers 2026-04-20 20:10:34 -04:00
adlee-was-taken
2fd6daad8e docs(ext/sw): tighten slice-3 comments per code review
Non-functional tightening flagged in the slice-3 code review:

- session.ts: document future multi-vault refactor (β+) so the module-
  scope singleton is explicitly "deliberately simple," not an oversight.
- vault.ts: move findByHostname doc comment above the function; note
  α's intentionally-coarse hostname match (no www-stripping, no
  public-suffix matching) and that tighter matching is a β/γ concern.
- index.ts: expand the passphrase scope-clearing comment to make
  the theatre explicit rather than leaving it looking like real defense.
- index.ts: TODO(slice-4) marker on delete_item's non-atomic two-write
  path — consider manifest-first ordering or retry/rollback at router-
  split time.
- index.ts: cross-reference comment on itemToManifestEntry pointing at
  the Rust-side ManifestEntry::from_item derivation it must mirror.

No behavior change; build still compiles with 2 bundle-size warnings.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 20:07:27 -04:00
adlee-was-taken
c0fba2a8dc chore(ext): silence popup/content errors until slice 6 2026-04-20 19:57:32 -04:00
adlee-was-taken
20144e8e02 feat(ext/sw): rewire flat handler onto typed-item vault + SessionHandle 2026-04-20 19:55:50 -04:00
adlee-was-taken
bd9dd206ac feat(ext/sw): typed-item vault ops via SessionHandle 2026-04-20 19:53:28 -04:00
adlee-was-taken
7781a51848 feat(ext/sw): SessionHandle lifecycle module 2026-04-20 19:52:54 -04:00
adlee-was-taken
dc8afcb634 feat(ext): base32 encode/decode for TOTP secret parse 2026-04-20 19:44:18 -04:00
adlee-was-taken
b4da5bffcf feat(ext): split PopupMessage / ContentMessage unions + capability sets 2026-04-20 19:43:09 -04:00
adlee-was-taken
04c9503036 feat(ext): typed-item TS types mirroring relicario-core serde 2026-04-20 19:42:31 -04:00
adlee-was-taken
14aaac672c build(ext): align wasm.d.ts with relicario-wasm surface
Add initSync named export (Chrome MV3 service worker path — can't use
dynamic import()), and correct TotpCode.expires_at from number to bigint
to match the u64 wasm-bindgen output.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 19:36:54 -04:00
adlee-was-taken
c03a492ee3 docs: Plan 1C-α (extension foundation) implementation plan
28 tasks across 6 slices + pre-flight + acceptance, following the 1C-α
design spec (a1d733d/ad6d8af). Each task is a single commit; each step
is 2-5 minutes of work. Design choices locked in:

- Slice 1 (Tasks 1-3): WASM artifact rebuild (replace stale idfoto_wasm)
- Slice 2 (Tasks 4-6): shared TS types + message unions + base32 util
- Slice 3 (Tasks 7-10): session.ts, vault.ts, transitional index.ts
- Slice 4 (Tasks 11-15): split router + Vitest + sender-check matrix
- Slice 5 (Tasks 16-20): WAR cleanup, setup-via-tabs, closed Shadow DOM
  for capture/icon/picker/ack, popup captured-tab snapshot
- Slice 6 (Tasks 21-27): popup rename + Login-parity + zxcvbn + manual
  cross-browser verification
- Slice 7 (Task 28): acceptance checks (cargo test, build, lint greps)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 19:28:13 -04:00
adlee-was-taken
ad6d8af2f6 docs(1c-alpha): correct TS type definitions to match actual serde shapes
Verified against the Plan 1A Rust sources:
- ItemType / ItemCore use snake_case with tag="type" internal tagging
  (not the external tagging I initially wrote)
- TotpKind is default-externally-tagged (no tag attr), so it serializes
  as bare "totp"/"steam" for unit variants and { hotp: { counter } }
- GeneratorRequest uses tag="kind" internal tagging
- FieldValue / TrashRetention / HistoryRetention / SymbolCharset use
  adjacent tagging { tag: "kind", content: "value" }
- Fix Login form TOTP parse example and "gen" button payload

No scope change — this is a bookkeeping correction so the plan
author references the correct wire shapes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 19:19:44 -04:00
adlee-was-taken
a1d733ddeb docs: Plan 1C-α (extension foundation) design spec
Foundation slice of the browser-extension migration onto the typed-item
core from Plans 1A+1B. Scope: WASM artifact rebuild, typed-item shared
types, SessionHandle-based service worker, split router with sender
checks, full security architecture (origin-bound autofill, TOFU ack,
closed Shadow DOM, popup captured-tab verification), zxcvbn setup gate,
Login-parity popup. Other 6 item types land in 1C-β; attachments/trash/
history/device UI in 1C-γ.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 19:14:25 -04:00
adlee-was-taken
76f34bfcf5 chore: remove stray vault files from Plan 1B + add plan doc
A Task 6 implementer subagent ran `relicario init` inside the worktree
root during manual testing and committed the resulting vault skeleton
(.relicario/, manifest.enc, settings.enc) plus overwrote .gitignore.
None of these should be in the source repo.

Restores the original .gitignore (adds reference.jpg and ref.jpg to it)
and checks in the Plan 1B design doc that describes the work just merged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 18:50:37 -04:00
adlee-was-taken
e0c511e320 Merge Plan 1B: typed-item CLI + WASM bridge
34 commits from plan-1a-rust-core-complete landing:
- Rename reconciliation (Task 1)
- Core imgsecret MAX_DIMENSION cap (Task 2, audit M3)
- CLI rewrite against typed-item core API (Tasks 3-17)
- WASM opaque SessionHandle bridge (Tasks 18-21)
- CLI integration test harness + tests (Tasks 22-24)
- CLAUDE.md typed-item layout refresh (Task 25)

Audit fixes: H4 H5 H6 H7 M3 M6 M7 M11 L8.
Tests: 151 passing (core + CLI + WASM native), WASM target builds clean.
Tag: plan-1b-cli-wasm-complete
2026-04-20 18:48:56 -04:00
adlee-was-taken
65e0d3cb80 docs: update CLAUDE.md for the typed-item module layout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
plan-1b-cli-wasm-complete
2026-04-20 18:47:08 -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
494eedbbb8 init: new relicario vault (format v2) 2026-04-20 18:31:46 -04:00
adlee-was-taken
b8afec3560 feat(wasm): configure serde_wasm_bindgen for plain-object HashMap
Maps serialize as JS objects, not Maps — what the extension popup
expects. Also ships hand-written TS declarations for the bridge
(consumed by Plan 1C).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-20 17:41:41 -04:00
adlee-was-taken
92b9e64ef9 feat(wasm): attachment / generator / totp / imgsecret / id bridges
Also ports TOTP RFC 6238 compute to relicario-core::item_types::totp
so native + CLI + WASM share one implementation (audit H5: CSPRNG
via core's Uniform-sampling generator).

Adds hmac = "0.12" and sha1 = "0.10" to relicario-core deps to support
HOTP/TOTP HMAC with Sha1/Sha256/Sha512. RFC 6238 test vector (t=59,
SHA-1, 8 digits) passes: "94287082".
2026-04-20 17:39:45 -04:00
adlee-was-taken
fac2e49cf1 feat(wasm): manifest / item / settings encrypt+decrypt via SessionHandle
Adds six #[wasm_bindgen] functions (manifest_encrypt/decrypt,
item_encrypt/decrypt, settings_encrypt/decrypt) plus a native
round-trip test that verifies encrypt→core_decrypt and nonce
uniqueness without calling js-sys (serde_wasm_bindgen::from_value
is wasm32-only; documented in test comment).
2026-04-20 17:37:50 -04:00
adlee-was-taken
f3ce76d9fb feat(wasm): opaque SessionHandle bridge with unlock/lock
Master key never leaves WASM linear memory. Held in Zeroizing<[u8;32]>
inside a thread_local HashMap keyed by u32. lock() removes + zeroizes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-20 17:34:50 -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