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
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
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
Completes the extension restructure begun in v0.6.0. Phases 3 (setup
wizard SW migration + step registry), 4 (vault.ts split + vault_locked
lift), and 6 (get_vault_status + sidebar status indicator) all merged to
main (9df2fee, 3b8368d, 397cc78) via three parallel worktree streams.
This commit is the release-prep wrap-up:
- Version bump to v0.7.0 across the three relicario crates + Cargo.lock,
extension/package.json, and both extension manifests (the manifests had
lagged at 0.5.0 — corrected here).
- CHANGELOG.md v0.7.0 entry.
- STATUS.md: extension restructure moved to shipped; Phases 3/4/6 landing
section added.
- ROADMAP.md: v0.7.0 row added; Up-next now command palette.
- extension/ARCHITECTURE.md: all three phases integrated (new vault-*
modules, setup-steps.ts, get_vault_status protocol + status indicator,
vault_locked lift, git-host sync cache).
- Plan completion checkboxes ticked.
Task 7.1 verification: done-criteria sweep all green; 423/423 vitest;
build:all clean (only the pre-existing 4MB WASM size warning).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds top-level LICENSE (GPL-3.0 full text), updates README, and sets
`license = "GPL-3.0-or-later"` on all four crate manifests.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Plan B Phase 8 — three #[wasm_bindgen] exports for the parsers migrated
in Phase 7, mirrored in extension/src/wasm.d.ts under "Pure parsers
(no session needed)". snake_case JS naming consistent with every
existing export; SessionHandle not required.
- parse_month_year(s) → { month, year } via js_value_for
- base32_decode_lenient(s) → Uint8Array
- guess_mime(filename) → string
Tests in session_tests mod cover the OK paths; error-path / JsValue
serialization can't be tested natively (JsError construction panics
off-wasm) and is covered in core (time::tests + base32::tests).
Plan C will wire SW message handlers consuming these exports in a
future round; this commit delivers only the seam.
Includes simplify-feedback fixes:
- relicario-core lib.rs module-list mentions base32 and mime
- item_types/totp.rs neighbour comment unified to ///-style block
cargo test --workspace: green
cargo clippy --workspace: silent
cargo build -p relicario-wasm --target wasm32-unknown-unknown: clean
cd extension && npm run test: 17 pre-existing failures only (baseline)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Closes the P1.1 defense-in-depth gap: wasm-bindgen's auto-generated
.free() previously dropped the SessionHandle wrapper (a u32) without
removing the SESSIONS HashMap entry, leaving the master key and
image_secret in WASM linear memory until JS explicitly called
lock(handle). Drop now wires .free() to session::remove, and the
new native test pins the contract.
Refs: docs/superpowers/specs/2026-05-04-security-polish-design.md (Phase 1)
Refs: docs/superpowers/reviews/2026-05-04-architecture-review.md (P1.1)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Catches the workspace and the extension manifests up to the v0.5.x
release line (was still showing 0.2.0).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- register_device() generates signing + deploy keypairs via core device
module, stores them in DEVICE_STATE (once_cell Lazy<Mutex>), and
returns only public keys to JS
- sign_for_git() signs data using the internal signing key
- get_device_info() returns name and public keys; returns null if not
registered
- clear_device() zeroes and drops device state (logout / re-registration)
- Removed generate_device_keypair() which exposed raw private key bytes
Fixes audit I5: private key material no longer crosses the WASM boundary.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Returns { items: [Item], warnings: [ImportWarning] } as a JSON
string. The items already have fresh IDs + timestamps; the SW
caller encrypts and writes them through the existing
item_encrypt + manifest_encrypt bridges.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
generate_device_keypair returns an ed25519 keypair as JSON with hex pubkey
and base64 private key. get_field_history extracts tracked field history
from a decrypted item for the popup's history view.
Co-Authored-By: Claude <noreply@anthropic.com>
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>
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".
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).
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>