diff --git a/crates/relicario-wasm/src/lib.rs b/crates/relicario-wasm/src/lib.rs index 5ddb7be..66825e7 100644 --- a/crates/relicario-wasm/src/lib.rs +++ b/crates/relicario-wasm/src/lib.rs @@ -12,7 +12,13 @@ use zeroize::Zeroizing; use relicario_core::{derive_master_key, imgsecret, KdfParams}; -/// Handle type returned from `unlock`. Backed by a `u32`; opaque to JS. +/// Handle returned from `unlock`. Backed by a `u32`; opaque to JS. +/// +/// Dropping the handle (or invoking `.free()` from JS) removes the entry from +/// the session registry, zeroizing the wrapped master key and image_secret. +/// `lock(handle)` remains available as the explicit early-cleanup path; the +/// `Drop` impl is the safety net that catches code paths which forget to call +/// `lock` before letting the handle go out of scope. #[wasm_bindgen] pub struct SessionHandle(u32); @@ -22,6 +28,23 @@ impl SessionHandle { pub fn value(&self) -> u32 { self.0 } } +impl Drop for SessionHandle { + fn drop(&mut self) { let _ = session::remove(self.0); } +} + +#[doc(hidden)] +pub fn __test_make_handle() -> SessionHandle { + SessionHandle(session::insert( + Zeroizing::new([0x77u8; 32]), + Zeroizing::new([0u8; 32]), + )) +} + +#[doc(hidden)] +pub fn __test_session_exists(handle: u32) -> bool { + session::with(handle, |_| ()).is_some() +} + #[wasm_bindgen] pub fn unlock( passphrase: &str, @@ -533,6 +556,19 @@ mod session_tests { assert!(!session::remove(h)); // second remove false } + #[test] + fn dropping_session_handle_clears_registry_entry() { + session::clear(); + let handle = SessionHandle(session::insert( + Zeroizing::new([0x33u8; 32]), + Zeroizing::new([0u8; 32]), + )); + let id = handle.value(); + assert!(session::with(id, |_| ()).is_some()); + drop(handle); + assert!(session::with(id, |_| ()).is_none()); + } + #[test] fn with_yields_key_only_while_session_lives() { session::clear(); diff --git a/crates/relicario-wasm/src/session.rs b/crates/relicario-wasm/src/session.rs index ae084ba..ace135f 100644 --- a/crates/relicario-wasm/src/session.rs +++ b/crates/relicario-wasm/src/session.rs @@ -46,6 +46,9 @@ where SESSIONS.with(|s| s.borrow().get(&handle).map(|d| f(&d.image_secret))) } +/// Remove a session entry. Called by both `lock(handle)` (the explicit +/// path) and `impl Drop for SessionHandle` (the safety net). Returns +/// `true` if an entry was removed, `false` if the handle was already gone. pub fn remove(handle: u32) -> bool { SESSIONS.with(|s| s.borrow_mut().remove(&handle).is_some()) } diff --git a/crates/relicario-wasm/tests/session_drop.rs b/crates/relicario-wasm/tests/session_drop.rs new file mode 100644 index 0000000..cae25f0 --- /dev/null +++ b/crates/relicario-wasm/tests/session_drop.rs @@ -0,0 +1,16 @@ +//! Belt-and-suspenders companion to the native `dropping_session_handle_clears_registry_entry` +//! test in `lib.rs`. This file exists for `wasm-pack test --node` symmetry; the +//! native test in the same crate is what gates CI. + +use wasm_bindgen_test::wasm_bindgen_test; + +use relicario_wasm::{__test_make_handle, __test_session_exists}; + +#[wasm_bindgen_test] +fn dropping_session_handle_clears_registry_entry() { + let handle = __test_make_handle(); + let id = handle.value(); + assert!(__test_session_exists(id)); + drop(handle); + assert!(!__test_session_exists(id)); +}