feat(wasm): session stores image_secret for recovery QR generation

This commit is contained in:
adlee-was-taken
2026-05-03 20:56:39 -04:00
parent 762a008171
commit 42b746f9af
2 changed files with 26 additions and 8 deletions

View File

@@ -8,6 +8,7 @@ mod session;
mod device; mod device;
use wasm_bindgen::prelude::*; use wasm_bindgen::prelude::*;
use zeroize::Zeroizing;
use relicario_core::{derive_master_key, imgsecret, KdfParams}; use relicario_core::{derive_master_key, imgsecret, KdfParams};
@@ -36,7 +37,8 @@ pub fn unlock(
.map_err(|_| JsError::new("salt must be exactly 32 bytes"))?; .map_err(|_| JsError::new("salt must be exactly 32 bytes"))?;
let master_key = derive_master_key(passphrase.as_bytes(), &image_secret, salt_arr, &params) let master_key = derive_master_key(passphrase.as_bytes(), &image_secret, salt_arr, &params)
.map_err(|e| JsError::new(&e.to_string()))?; .map_err(|e| JsError::new(&e.to_string()))?;
let handle = session::insert(master_key); let stored_secret = Zeroizing::new(image_secret);
let handle = session::insert(master_key, stored_secret);
Ok(SessionHandle(handle)) Ok(SessionHandle(handle))
} }
@@ -492,7 +494,7 @@ mod session_tests {
#[test] #[test]
fn insert_then_remove_clears_entry() { fn insert_then_remove_clears_entry() {
session::clear(); session::clear();
let h = session::insert(Zeroizing::new([0x11u8; 32])); let h = session::insert(Zeroizing::new([0x11u8; 32]), Zeroizing::new([0u8; 32]));
assert_ne!(h, 0); assert_ne!(h, 0);
assert!(session::remove(h)); assert!(session::remove(h));
assert!(!session::remove(h)); // second remove false assert!(!session::remove(h)); // second remove false
@@ -501,7 +503,7 @@ mod session_tests {
#[test] #[test]
fn with_yields_key_only_while_session_lives() { fn with_yields_key_only_while_session_lives() {
session::clear(); session::clear();
let h = session::insert(Zeroizing::new([0x22u8; 32])); let h = session::insert(Zeroizing::new([0x22u8; 32]), Zeroizing::new([0u8; 32]));
let byte = session::with(h, |k| k[0]); let byte = session::with(h, |k| k[0]);
assert_eq!(byte, Some(0x22)); assert_eq!(byte, Some(0x22));
session::remove(h); session::remove(h);
@@ -513,7 +515,7 @@ mod session_tests {
fn manifest_round_trip_via_handle() { fn manifest_round_trip_via_handle() {
use relicario_core::{Manifest, decrypt_manifest}; use relicario_core::{Manifest, decrypt_manifest};
session::clear(); session::clear();
let h = session::insert(Zeroizing::new([0x55u8; 32])); let h = session::insert(Zeroizing::new([0x55u8; 32]), Zeroizing::new([0u8; 32]));
let handle = SessionHandle(h); let handle = SessionHandle(h);
let key = Zeroizing::new([0x55u8; 32]); let key = Zeroizing::new([0x55u8; 32]);
let empty = Manifest::new(); let empty = Manifest::new();

View File

@@ -6,12 +6,17 @@ use std::cell::RefCell;
use std::collections::HashMap; use std::collections::HashMap;
use zeroize::Zeroizing; use zeroize::Zeroizing;
pub struct SessionData {
pub master_key: Zeroizing<[u8; 32]>,
pub image_secret: Zeroizing<[u8; 32]>,
}
thread_local! { thread_local! {
static SESSIONS: RefCell<HashMap<u32, Zeroizing<[u8; 32]>>> = RefCell::new(HashMap::new()); static SESSIONS: RefCell<HashMap<u32, SessionData>> = RefCell::new(HashMap::new());
static NEXT_HANDLE: RefCell<u32> = const { RefCell::new(1) }; static NEXT_HANDLE: RefCell<u32> = const { RefCell::new(1) };
} }
pub fn insert(key: Zeroizing<[u8; 32]>) -> u32 { pub fn insert(master_key: Zeroizing<[u8; 32]>, image_secret: Zeroizing<[u8; 32]>) -> u32 {
let handle = NEXT_HANDLE.with(|n| { let handle = NEXT_HANDLE.with(|n| {
let mut n = n.borrow_mut(); let mut n = n.borrow_mut();
let h = *n; let h = *n;
@@ -19,15 +24,26 @@ pub fn insert(key: Zeroizing<[u8; 32]>) -> u32 {
if *n == 0 { *n = 1; } // avoid reserving 0 as a valid handle if *n == 0 { *n = 1; } // avoid reserving 0 as a valid handle
h h
}); });
SESSIONS.with(|s| { s.borrow_mut().insert(handle, key); }); SESSIONS.with(|s| {
s.borrow_mut().insert(handle, SessionData { master_key, image_secret });
});
handle handle
} }
/// Access the master key for a handle. Preserves original `with` signature for all existing callers.
pub fn with<F, R>(handle: u32, f: F) -> Option<R> pub fn with<F, R>(handle: u32, f: F) -> Option<R>
where where
F: FnOnce(&Zeroizing<[u8; 32]>) -> R, F: FnOnce(&Zeroizing<[u8; 32]>) -> R,
{ {
SESSIONS.with(|s| s.borrow().get(&handle).map(f)) SESSIONS.with(|s| s.borrow().get(&handle).map(|d| f(&d.master_key)))
}
/// Access the image_secret for a handle (used by recovery QR).
pub fn with_image_secret<F, R>(handle: u32, f: F) -> Option<R>
where
F: FnOnce(&Zeroizing<[u8; 32]>) -> R,
{
SESSIONS.with(|s| s.borrow().get(&handle).map(|d| f(&d.image_secret)))
} }
pub fn remove(handle: u32) -> bool { pub fn remove(handle: u32) -> bool {