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:
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());
+ }
+}