app

Local-first trade for farms and co-ops
git clone https://radroots.dev/git/app.git
Log | Files | Refs | README | LICENSE

commit 50c99cefdb531830b99fdee3aa374d7e83be804a
parent 159a6560fea0c9c47aba8a2a50f7511641c1ac4e
Author: triesap <triesap@radroots.dev>
Date:   Mon, 19 Jan 2026 01:44:14 +0000

app-core: add crypto registry storage

- add idb-backed crypto registry helpers for wasm
- encode/decode registry entries via serde_wasm_bindgen
- add device material storage helpers and key prefixes
- add serde-wasm-bindgen dependency and exports

Diffstat:
MCargo.lock | 12++++++++++++
MCargo.toml | 1+
Mcrates/core/Cargo.toml | 1+
Mcrates/core/src/crypto/mod.rs | 15+++++++++++++++
Acrates/core/src/crypto/registry.rs | 403+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
5 files changed, 432 insertions(+), 0 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -1166,6 +1166,7 @@ dependencies = [ "getrandom 0.2.17", "js-sys", "serde", + "serde-wasm-bindgen", "serde_json", "wasm-bindgen", "wasm-bindgen-futures", @@ -1341,6 +1342,17 @@ dependencies = [ ] [[package]] +name = "serde-wasm-bindgen" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + +[[package]] name = "serde_core" version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" diff --git a/Cargo.toml b/Cargo.toml @@ -37,6 +37,7 @@ web-sys = { version = "0.3.77", features = [ ] } wasm-bindgen-futures = "0.4" base64 = "0.22" +serde-wasm-bindgen = "0.6" [profile.release] codegen-units = 1 diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml @@ -21,6 +21,7 @@ js-sys = { workspace = true } web-sys = { workspace = true } wasm-bindgen-futures = { workspace = true } wasm-bindgen = { workspace = true } +serde-wasm-bindgen = { workspace = true } [dev-dependencies] futures = "0.3" diff --git a/crates/core/src/crypto/mod.rs b/crates/core/src/crypto/mod.rs @@ -4,6 +4,7 @@ pub mod envelope; pub mod random; pub mod keys; pub mod kdf; +pub mod registry; pub use error::{RadrootsClientCryptoError, RadrootsClientCryptoErrorMessage}; pub use types::{ @@ -22,6 +23,20 @@ pub use types::{ pub use envelope::{crypto_envelope_decode, crypto_envelope_encode}; pub use keys::crypto_key_id_create; pub use kdf::{crypto_kdf_iterations_default, crypto_kdf_salt_create}; +pub use registry::{ + crypto_registry_clear_key_entry, + crypto_registry_clear_store_index, + crypto_registry_export, + crypto_registry_get_device_material, + crypto_registry_get_key_entry, + crypto_registry_get_store_index, + crypto_registry_import, + crypto_registry_list_key_entries, + crypto_registry_list_store_indices, + crypto_registry_set_device_material, + crypto_registry_set_key_entry, + crypto_registry_set_store_index, +}; #[cfg(target_arch = "wasm32")] pub use keys::{ crypto_key_export_raw, diff --git a/crates/core/src/crypto/registry.rs b/crates/core/src/crypto/registry.rs @@ -0,0 +1,403 @@ +use crate::crypto::{RadrootsClientCryptoError, RadrootsClientCryptoRegistryExport}; +use crate::crypto::{RadrootsClientCryptoKeyEntry, RadrootsClientCryptoStoreIndex}; + +#[cfg(any(test, target_arch = "wasm32"))] +const STORE_INDEX_PREFIX: &str = "store:"; +#[cfg(any(test, target_arch = "wasm32"))] +const KEY_ENTRY_PREFIX: &str = "key:"; +#[cfg(target_arch = "wasm32")] +const DEVICE_MATERIAL_KEY: &str = "device:material"; + +#[cfg(any(test, target_arch = "wasm32"))] +fn store_index_key(store_id: &str) -> String { + format!("{STORE_INDEX_PREFIX}{store_id}") +} + +#[cfg(any(test, target_arch = "wasm32"))] +fn key_entry_key(key_id: &str) -> String { + format!("{KEY_ENTRY_PREFIX}{key_id}") +} + +#[cfg(target_arch = "wasm32")] +use crate::idb::{ + idb_del, + idb_get, + idb_keys, + idb_set, + idb_store_ensure, + idb_value_as_bytes, + IDB_CONFIG_CRYPTO_REGISTRY, + RadrootsClientIdbStoreError, +}; +#[cfg(target_arch = "wasm32")] +use js_sys::Uint8Array; +#[cfg(target_arch = "wasm32")] +use wasm_bindgen::JsValue; + +#[cfg(target_arch = "wasm32")] +fn map_idb_error(err: RadrootsClientIdbStoreError) -> RadrootsClientCryptoError { + match err { + RadrootsClientIdbStoreError::IdbUndefined => RadrootsClientCryptoError::IdbUndefined, + _ => RadrootsClientCryptoError::RegistryFailure, + } +} + +#[cfg(target_arch = "wasm32")] +async fn ensure_idb() -> Result<(), RadrootsClientCryptoError> { + idb_store_ensure( + IDB_CONFIG_CRYPTO_REGISTRY.database, + IDB_CONFIG_CRYPTO_REGISTRY.store, + ) + .await + .map_err(map_idb_error) +} + +#[cfg(target_arch = "wasm32")] +fn decode_store_index(value: &JsValue) -> Result<RadrootsClientCryptoStoreIndex, RadrootsClientCryptoError> { + if let Some(text) = value.as_string() { + return serde_json::from_str(&text) + .map_err(|_| RadrootsClientCryptoError::RegistryFailure); + } + serde_wasm_bindgen::from_value(value.clone()) + .map_err(|_| RadrootsClientCryptoError::RegistryFailure) +} + +#[cfg(target_arch = "wasm32")] +fn decode_key_entry(value: &JsValue) -> Result<RadrootsClientCryptoKeyEntry, RadrootsClientCryptoError> { + if let Some(text) = value.as_string() { + return serde_json::from_str(&text) + .map_err(|_| RadrootsClientCryptoError::RegistryFailure); + } + serde_wasm_bindgen::from_value(value.clone()) + .map_err(|_| RadrootsClientCryptoError::RegistryFailure) +} + +#[cfg(target_arch = "wasm32")] +fn encode_store_index( + index: &RadrootsClientCryptoStoreIndex, +) -> Result<JsValue, RadrootsClientCryptoError> { + serde_wasm_bindgen::to_value(index) + .map_err(|_| RadrootsClientCryptoError::RegistryFailure) +} + +#[cfg(target_arch = "wasm32")] +fn encode_key_entry( + entry: &RadrootsClientCryptoKeyEntry, +) -> Result<JsValue, RadrootsClientCryptoError> { + serde_wasm_bindgen::to_value(entry) + .map_err(|_| RadrootsClientCryptoError::RegistryFailure) +} + +#[cfg(target_arch = "wasm32")] +pub async fn crypto_registry_get_store_index( + store_id: &str, +) -> Result<Option<RadrootsClientCryptoStoreIndex>, RadrootsClientCryptoError> { + ensure_idb().await?; + let key = store_index_key(store_id); + let value = idb_get( + IDB_CONFIG_CRYPTO_REGISTRY.database, + IDB_CONFIG_CRYPTO_REGISTRY.store, + &key, + ) + .await + .map_err(map_idb_error)?; + let Some(value) = value else { + return Ok(None); + }; + decode_store_index(&value).map(Some) +} + +#[cfg(target_arch = "wasm32")] +pub async fn crypto_registry_set_store_index( + index: RadrootsClientCryptoStoreIndex, +) -> Result<(), RadrootsClientCryptoError> { + ensure_idb().await?; + let key = store_index_key(&index.store_id); + let value = encode_store_index(&index)?; + idb_set( + IDB_CONFIG_CRYPTO_REGISTRY.database, + IDB_CONFIG_CRYPTO_REGISTRY.store, + &key, + &value, + ) + .await + .map_err(map_idb_error) +} + +#[cfg(target_arch = "wasm32")] +pub async fn crypto_registry_get_key_entry( + key_id: &str, +) -> Result<Option<RadrootsClientCryptoKeyEntry>, RadrootsClientCryptoError> { + ensure_idb().await?; + let key = key_entry_key(key_id); + let value = idb_get( + IDB_CONFIG_CRYPTO_REGISTRY.database, + IDB_CONFIG_CRYPTO_REGISTRY.store, + &key, + ) + .await + .map_err(map_idb_error)?; + let Some(value) = value else { + return Ok(None); + }; + decode_key_entry(&value).map(Some) +} + +#[cfg(target_arch = "wasm32")] +pub async fn crypto_registry_set_key_entry( + entry: RadrootsClientCryptoKeyEntry, +) -> Result<(), RadrootsClientCryptoError> { + ensure_idb().await?; + let key = key_entry_key(&entry.key_id); + let value = encode_key_entry(&entry)?; + idb_set( + IDB_CONFIG_CRYPTO_REGISTRY.database, + IDB_CONFIG_CRYPTO_REGISTRY.store, + &key, + &value, + ) + .await + .map_err(map_idb_error) +} + +#[cfg(target_arch = "wasm32")] +pub async fn crypto_registry_list_store_indices( +) -> Result<Vec<RadrootsClientCryptoStoreIndex>, RadrootsClientCryptoError> { + ensure_idb().await?; + let keys = idb_keys( + IDB_CONFIG_CRYPTO_REGISTRY.database, + IDB_CONFIG_CRYPTO_REGISTRY.store, + ) + .await + .map_err(map_idb_error)?; + let mut out = Vec::new(); + for key in keys { + if !key.starts_with(STORE_INDEX_PREFIX) { + continue; + } + let value = idb_get( + IDB_CONFIG_CRYPTO_REGISTRY.database, + IDB_CONFIG_CRYPTO_REGISTRY.store, + &key, + ) + .await + .map_err(map_idb_error)?; + let Some(value) = value else { + continue; + }; + let index = decode_store_index(&value)?; + out.push(index); + } + Ok(out) +} + +#[cfg(target_arch = "wasm32")] +pub async fn crypto_registry_list_key_entries( +) -> Result<Vec<RadrootsClientCryptoKeyEntry>, RadrootsClientCryptoError> { + ensure_idb().await?; + let keys = idb_keys( + IDB_CONFIG_CRYPTO_REGISTRY.database, + IDB_CONFIG_CRYPTO_REGISTRY.store, + ) + .await + .map_err(map_idb_error)?; + let mut out = Vec::new(); + for key in keys { + if !key.starts_with(KEY_ENTRY_PREFIX) { + continue; + } + let value = idb_get( + IDB_CONFIG_CRYPTO_REGISTRY.database, + IDB_CONFIG_CRYPTO_REGISTRY.store, + &key, + ) + .await + .map_err(map_idb_error)?; + let Some(value) = value else { + continue; + }; + let entry = decode_key_entry(&value)?; + out.push(entry); + } + Ok(out) +} + +#[cfg(target_arch = "wasm32")] +pub async fn crypto_registry_export( +) -> Result<RadrootsClientCryptoRegistryExport, RadrootsClientCryptoError> { + let stores = crypto_registry_list_store_indices().await?; + let keys = crypto_registry_list_key_entries().await?; + Ok(RadrootsClientCryptoRegistryExport { stores, keys }) +} + +#[cfg(target_arch = "wasm32")] +pub async fn crypto_registry_import( + registry: RadrootsClientCryptoRegistryExport, +) -> Result<(), RadrootsClientCryptoError> { + ensure_idb().await?; + for store_index in registry.stores { + crypto_registry_set_store_index(store_index).await?; + } + for entry in registry.keys { + crypto_registry_set_key_entry(entry).await?; + } + Ok(()) +} + +#[cfg(target_arch = "wasm32")] +pub async fn crypto_registry_get_device_material( +) -> Result<Option<Vec<u8>>, RadrootsClientCryptoError> { + ensure_idb().await?; + let value = idb_get( + IDB_CONFIG_CRYPTO_REGISTRY.database, + IDB_CONFIG_CRYPTO_REGISTRY.store, + DEVICE_MATERIAL_KEY, + ) + .await + .map_err(map_idb_error)?; + let Some(value) = value else { + return Ok(None); + }; + idb_value_as_bytes(&value).ok_or(RadrootsClientCryptoError::RegistryFailure).map(Some) +} + +#[cfg(target_arch = "wasm32")] +pub async fn crypto_registry_set_device_material( + material: &[u8], +) -> Result<(), RadrootsClientCryptoError> { + ensure_idb().await?; + let value = Uint8Array::from(material); + idb_set( + IDB_CONFIG_CRYPTO_REGISTRY.database, + IDB_CONFIG_CRYPTO_REGISTRY.store, + DEVICE_MATERIAL_KEY, + &value.into(), + ) + .await + .map_err(map_idb_error) +} + +#[cfg(target_arch = "wasm32")] +pub async fn crypto_registry_clear_store_index( + store_id: &str, +) -> Result<(), RadrootsClientCryptoError> { + ensure_idb().await?; + let key = store_index_key(store_id); + idb_del( + IDB_CONFIG_CRYPTO_REGISTRY.database, + IDB_CONFIG_CRYPTO_REGISTRY.store, + &key, + ) + .await + .map_err(map_idb_error) +} + +#[cfg(target_arch = "wasm32")] +pub async fn crypto_registry_clear_key_entry( + key_id: &str, +) -> Result<(), RadrootsClientCryptoError> { + ensure_idb().await?; + let key = key_entry_key(key_id); + idb_del( + IDB_CONFIG_CRYPTO_REGISTRY.database, + IDB_CONFIG_CRYPTO_REGISTRY.store, + &key, + ) + .await + .map_err(map_idb_error) +} + +#[cfg(not(target_arch = "wasm32"))] +pub async fn crypto_registry_get_store_index( + _store_id: &str, +) -> Result<Option<RadrootsClientCryptoStoreIndex>, RadrootsClientCryptoError> { + Err(RadrootsClientCryptoError::IdbUndefined) +} + +#[cfg(not(target_arch = "wasm32"))] +pub async fn crypto_registry_set_store_index( + _index: RadrootsClientCryptoStoreIndex, +) -> Result<(), RadrootsClientCryptoError> { + Err(RadrootsClientCryptoError::IdbUndefined) +} + +#[cfg(not(target_arch = "wasm32"))] +pub async fn crypto_registry_get_key_entry( + _key_id: &str, +) -> Result<Option<RadrootsClientCryptoKeyEntry>, RadrootsClientCryptoError> { + Err(RadrootsClientCryptoError::IdbUndefined) +} + +#[cfg(not(target_arch = "wasm32"))] +pub async fn crypto_registry_set_key_entry( + _entry: RadrootsClientCryptoKeyEntry, +) -> Result<(), RadrootsClientCryptoError> { + Err(RadrootsClientCryptoError::IdbUndefined) +} + +#[cfg(not(target_arch = "wasm32"))] +pub async fn crypto_registry_list_store_indices( +) -> Result<Vec<RadrootsClientCryptoStoreIndex>, RadrootsClientCryptoError> { + Err(RadrootsClientCryptoError::IdbUndefined) +} + +#[cfg(not(target_arch = "wasm32"))] +pub async fn crypto_registry_list_key_entries( +) -> Result<Vec<RadrootsClientCryptoKeyEntry>, RadrootsClientCryptoError> { + Err(RadrootsClientCryptoError::IdbUndefined) +} + +#[cfg(not(target_arch = "wasm32"))] +pub async fn crypto_registry_export( +) -> Result<RadrootsClientCryptoRegistryExport, RadrootsClientCryptoError> { + Err(RadrootsClientCryptoError::IdbUndefined) +} + +#[cfg(not(target_arch = "wasm32"))] +pub async fn crypto_registry_import( + _registry: RadrootsClientCryptoRegistryExport, +) -> Result<(), RadrootsClientCryptoError> { + Err(RadrootsClientCryptoError::IdbUndefined) +} + +#[cfg(not(target_arch = "wasm32"))] +pub async fn crypto_registry_get_device_material( +) -> Result<Option<Vec<u8>>, RadrootsClientCryptoError> { + Err(RadrootsClientCryptoError::IdbUndefined) +} + +#[cfg(not(target_arch = "wasm32"))] +pub async fn crypto_registry_set_device_material( + _material: &[u8], +) -> Result<(), RadrootsClientCryptoError> { + Err(RadrootsClientCryptoError::IdbUndefined) +} + +#[cfg(not(target_arch = "wasm32"))] +pub async fn crypto_registry_clear_store_index( + _store_id: &str, +) -> Result<(), RadrootsClientCryptoError> { + Err(RadrootsClientCryptoError::IdbUndefined) +} + +#[cfg(not(target_arch = "wasm32"))] +pub async fn crypto_registry_clear_key_entry( + _key_id: &str, +) -> Result<(), RadrootsClientCryptoError> { + Err(RadrootsClientCryptoError::IdbUndefined) +} + +#[cfg(test)] +mod tests { + use super::{key_entry_key, store_index_key}; + + #[test] + fn store_index_key_prefixes() { + assert_eq!(store_index_key("alpha"), "store:alpha"); + } + + #[test] + fn key_entry_key_prefixes() { + assert_eq!(key_entry_key("beta"), "key:beta"); + } +}