From caebe9f97e1df0c0247e6e005cfab8d8f5f5a757 Mon Sep 17 00:00:00 2001 From: adlee-was-taken Date: Sun, 26 Apr 2026 15:44:04 -0400 Subject: [PATCH] feat(wasm): add generate_device_keypair + get_field_history bindings 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 --- Cargo.lock | 10 ++++++ crates/relicario-wasm/Cargo.toml | 4 +++ crates/relicario-wasm/src/lib.rs | 58 ++++++++++++++++++++++++++++++++ 3 files changed, 72 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index a1baf86..2bf3280 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -162,6 +162,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "base64ct" version = "1.8.3" @@ -1557,8 +1563,12 @@ dependencies = [ name = "relicario-wasm" version = "0.1.0" dependencies = [ + "base64", + "ed25519-dalek", "getrandom 0.2.17", + "hex", "image", + "rand", "relicario-core", "serde", "serde-wasm-bindgen", diff --git a/crates/relicario-wasm/Cargo.toml b/crates/relicario-wasm/Cargo.toml index 2f04cdc..9489329 100644 --- a/crates/relicario-wasm/Cargo.toml +++ b/crates/relicario-wasm/Cargo.toml @@ -15,6 +15,10 @@ serde_json = "1" serde = { version = "1", features = ["derive"] } zeroize = "1" getrandom = { version = "0.2", features = ["js"] } +ed25519-dalek = { version = "2", features = ["rand_core"] } +base64 = "0.22" +hex = "0.4" +rand = "0.8" [dev-dependencies] wasm-bindgen-test = "0.3" diff --git a/crates/relicario-wasm/src/lib.rs b/crates/relicario-wasm/src/lib.rs index 3e8d604..7fc0324 100644 --- a/crates/relicario-wasm/src/lib.rs +++ b/crates/relicario-wasm/src/lib.rs @@ -196,6 +196,64 @@ pub fn rate_passphrase(p: &str) -> Result { })) } +use ed25519_dalek::SigningKey; +use base64::Engine; + +/// Generate an ed25519 keypair for device registration. +/// Returns JSON: { "public_key_hex": "...", "private_key_base64": "..." } +#[wasm_bindgen] +pub fn generate_device_keypair() -> Result { + let mut rng = rand::thread_rng(); + let signing_key = SigningKey::generate(&mut rng); + let verifying_key = signing_key.verifying_key(); + + let public_hex = hex::encode(verifying_key.as_bytes()); + let private_b64 = base64::engine::general_purpose::STANDARD.encode(signing_key.as_bytes()); + + js_value_for(&serde_json::json!({ + "public_key_hex": public_hex, + "private_key_base64": private_b64, + })) +} + +/// Extract field history from a decrypted item JSON. +/// Returns JSON array of { field_id, field_name, current_value, entries: [{ value, changed_at }] } +#[wasm_bindgen] +pub fn get_field_history(item_json: &str) -> Result { + let item: Item = serde_json::from_str(item_json) + .map_err(|e| JsError::new(&format!("item json: {e}")))?; + + let mut results = Vec::new(); + + // Only section fields are tracked in field_history (set_field_value operates on sections). + for section in &item.sections { + for field in §ion.fields { + if field.value.is_history_tracked() { + if let Some(entries) = item.field_history.get(&field.id) { + if !entries.is_empty() { + let current = match &field.value { + relicario_core::FieldValue::Password(v) => v.as_str().to_owned(), + relicario_core::FieldValue::Concealed(v) => v.as_str().to_owned(), + _ => String::new(), + }; + results.push(serde_json::json!({ + "field_id": field.id.as_str(), + "field_name": &field.label, + "current_value": current, + "entries": entries.iter().map(|e| serde_json::json!({ + "value": e.value.as_str(), + "changed_at": e.replaced_at, + })).collect::>(), + })); + } + } + } + } + } + + js_value_for(&results) +} + #[wasm_bindgen] pub fn extract_image_secret(image_bytes: &[u8]) -> Result, JsError> { let s = imgsecret::extract(image_bytes).map_err(|e| JsError::new(&e.to_string()))?;