app

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

commit 5b00d310759668f3477467a5b438c316300db9e2
parent df9fdf93a99a7e748f51aab9e45cedcf61229003
Author: triesap <triesap@radroots.dev>
Date:   Mon, 19 Jan 2026 01:03:07 +0000

app-core: add crypto random utilities

- add random byte filler with wasm/native support
- implement crypto key id generation with hex encoding
- expose key id helper from crypto module
- add unit tests for random noop and hex key ids

Diffstat:
MCargo.lock | 24++++++++++++++++++++++--
MCargo.toml | 3+++
Mcrates/core/Cargo.toml | 5+++++
Acrates/core/src/crypto/keys.rs | 31+++++++++++++++++++++++++++++++
Mcrates/core/src/crypto/mod.rs | 3+++
Acrates/core/src/crypto/random.rs | 36++++++++++++++++++++++++++++++++++++
6 files changed, 100 insertions(+), 2 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -472,6 +472,17 @@ dependencies = [ [[package]] name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" @@ -728,7 +739,7 @@ dependencies = [ "cfg-if", "either_of", "futures", - "getrandom", + "getrandom 0.3.4", "hydration_context", "leptos_config", "leptos_dom", @@ -1150,8 +1161,11 @@ name = "radroots-app-core" version = "0.1.0" dependencies = [ "async-trait", + "getrandom 0.2.17", + "js-sys", "serde", "serde_json", + "web-sys", ] [[package]] @@ -1723,7 +1737,7 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a" dependencies = [ - "getrandom", + "getrandom 0.3.4", "js-sys", "wasm-bindgen", ] @@ -1745,6 +1759,12 @@ dependencies = [ ] [[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] name = "wasip2" version = "1.0.2+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" diff --git a/Cargo.toml b/Cargo.toml @@ -17,6 +17,9 @@ leptos = { version = "0.8.5", default-features = false } wasm-bindgen = "=0.2.100" serde = { version = "1", features = ["derive"] } serde_json = "1" +getrandom = "0.2" +js-sys = "0.3.77" +web-sys = { version = "0.3.77", features = ["Window", "Crypto"] } [profile.release] codegen-units = 1 diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml @@ -13,3 +13,8 @@ crate-type = ["rlib"] async-trait = "0.1.83" serde = { workspace = true } serde_json = { workspace = true } +getrandom = { workspace = true } + +[target.'cfg(target_arch = "wasm32")'.dependencies] +js-sys = { workspace = true } +web-sys = { workspace = true } diff --git a/crates/core/src/crypto/keys.rs b/crates/core/src/crypto/keys.rs @@ -0,0 +1,31 @@ +use super::{RadrootsClientCryptoError}; +use crate::crypto::random::fill_random; + +const KEY_ID_BYTES_LENGTH: usize = 16; + +fn bytes_to_hex(bytes: &[u8]) -> String { + let mut out = String::with_capacity(bytes.len() * 2); + for byte in bytes { + use std::fmt::Write; + let _ = write!(out, "{:02x}", byte); + } + out +} + +pub fn crypto_key_id_create() -> Result<String, RadrootsClientCryptoError> { + let mut bytes = [0u8; KEY_ID_BYTES_LENGTH]; + fill_random(&mut bytes)?; + Ok(bytes_to_hex(&bytes)) +} + +#[cfg(test)] +mod tests { + use super::crypto_key_id_create; + + #[test] + fn key_id_is_hex() { + let key_id = crypto_key_id_create().expect("key id"); + assert_eq!(key_id.len(), 32); + assert!(key_id.chars().all(|c| c.is_ascii_hexdigit())); + } +} diff --git a/crates/core/src/crypto/mod.rs b/crates/core/src/crypto/mod.rs @@ -1,6 +1,8 @@ pub mod error; pub mod types; pub mod envelope; +pub mod random; +pub mod keys; pub use error::{RadrootsClientCryptoError, RadrootsClientCryptoErrorMessage}; pub use types::{ @@ -17,3 +19,4 @@ pub use types::{ RadrootsClientWebCryptoService, }; pub use envelope::{crypto_envelope_decode, crypto_envelope_encode}; +pub use keys::crypto_key_id_create; diff --git a/crates/core/src/crypto/random.rs b/crates/core/src/crypto/random.rs @@ -0,0 +1,36 @@ +use super::RadrootsClientCryptoError; + +pub fn fill_random(bytes: &mut [u8]) -> Result<(), RadrootsClientCryptoError> { + if bytes.is_empty() { + return Ok(()); + } + fill_random_inner(bytes) +} + +#[cfg(target_arch = "wasm32")] +fn fill_random_inner(bytes: &mut [u8]) -> Result<(), RadrootsClientCryptoError> { + let window = web_sys::window().ok_or(RadrootsClientCryptoError::CryptoUndefined)?; + let crypto = window.crypto().map_err(|_| RadrootsClientCryptoError::CryptoUndefined)?; + let array = js_sys::Uint8Array::from(bytes); + crypto + .get_random_values_with_u8_array(&array) + .map_err(|_| RadrootsClientCryptoError::CryptoUndefined)?; + array.copy_to(bytes); + Ok(()) +} + +#[cfg(not(target_arch = "wasm32"))] +fn fill_random_inner(bytes: &mut [u8]) -> Result<(), RadrootsClientCryptoError> { + getrandom::getrandom(bytes).map_err(|_| RadrootsClientCryptoError::CryptoUndefined) +} + +#[cfg(test)] +mod tests { + use super::fill_random; + + #[test] + fn fill_random_noop_for_empty() { + let mut bytes = []; + assert!(fill_random(&mut bytes).is_ok()); + } +}