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 <noreply@anthropic.com>
This commit is contained in:
adlee-was-taken
2026-04-26 15:44:04 -04:00
parent af050f176c
commit caebe9f97e
3 changed files with 72 additions and 0 deletions

View File

@@ -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"

View File

@@ -196,6 +196,64 @@ pub fn rate_passphrase(p: &str) -> Result<JsValue, JsError> {
}))
}
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<JsValue, JsError> {
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<JsValue, JsError> {
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 &section.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::<Vec<_>>(),
}));
}
}
}
}
}
js_value_for(&results)
}
#[wasm_bindgen]
pub fn extract_image_secret(image_bytes: &[u8]) -> Result<Vec<u8>, JsError> {
let s = imgsecret::extract(image_bytes).map_err(|e| JsError::new(&e.to_string()))?;