diff --git a/CHANGELOG.md b/CHANGELOG.md index ce4cebbc..36cabb35 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,7 +28,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Switch from `libpthread`'s mutex to `futex` on Linux and to `nanosleep`-based wait loop on other targets in the `use_file` backend [#490] - Do not retry on `EAGAIN` while polling `/dev/random` on Linux [#522] - +- Remove separate codepath for Node.js in the `wasm_js` backend (bumps minimum supported Node.js + version to v19) [#557] + ### Added - `wasm32-wasip1` and `wasm32-wasip2` support [#499] - `getrandom_backend` configuration flag for selection of opt-in backends [#504] @@ -58,6 +60,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 [#544]: https://github.com/rust-random/getrandom/pull/544 [#554]: https://github.com/rust-random/getrandom/pull/554 [#555]: https://github.com/rust-random/getrandom/pull/555 +[#557]: https://github.com/rust-random/getrandom/pull/557 ## [0.2.15] - 2024-05-06 ### Added diff --git a/Cargo.toml b/Cargo.toml index 28b48a64..875a7fd0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -63,11 +63,11 @@ wasi = { version = "0.13", default-features = false } windows-targets = "0.52" # wasm_js -[target.'cfg(all(getrandom_backend = "wasm_js", target_arch = "wasm32", target_os = "unknown"))'.dependencies] -wasm-bindgen = { version = "0.2.89", default-features = false } -js-sys = "0.3" -[target.'cfg(all(getrandom_backend = "wasm_js", target_arch = "wasm32", target_os = "unknown"))'.dev-dependencies] -wasm-bindgen-test = "0.3.39" +[target.'cfg(all(getrandom_backend = "wasm_js", target_arch = "wasm32", any(target_os = "unknown", target_os = "none")))'.dependencies] +wasm-bindgen = { version = "0.2.96", default-features = false } +js-sys = "0.3.73" +[target.'cfg(all(getrandom_backend = "wasm_js", target_arch = "wasm32", any(target_os = "unknown", target_os = "none")))'.dev-dependencies] +wasm-bindgen-test = "0.3" [features] # Implement std::error::Error for getrandom::Error and @@ -81,7 +81,6 @@ level = "warn" check-cfg = [ 'cfg(getrandom_backend, values("custom", "rdrand", "rndr", "linux_getrandom", "linux_rustix", "wasm_js", "esp_idf"))', 'cfg(getrandom_sanitize)', - 'cfg(getrandom_browser_test)', 'cfg(getrandom_test_linux_fallback)', 'cfg(getrandom_test_netbsd_fallback)', ] diff --git a/README.md b/README.md index d145c2fe..ebd1b76a 100644 --- a/README.md +++ b/README.md @@ -83,7 +83,7 @@ of randomness based on their specific needs: | `rdrand` | x86, x86-64 | `x86_64-*`, `i686-*` | [`RDRAND`] instruction | `rndr` | AArch64 | `aarch64-*` | [`RNDR`] register | `esp_idf` | ESP-IDF | `*‑espidf` | [`esp_fill_random`]. WARNING: can return low-quality entropy without proper hardware configuration! -| `wasm_js` | Web Browser, Node.js | `wasm32‑unknown‑unknown` | [`Crypto.getRandomValues`] if available, then [`crypto.randomFillSync`] if on Node.js (see [WebAssembly support]) +| `wasm_js` | Web Browser, Node.js | `wasm32‑unknown‑unknown`, `wasm32v1-none` | [`Crypto.getRandomValues`] | `custom` | All targets | `*` | User-provided custom implementation (see [custom backend]) Opt-in backends can be enabled using the `getrandom_backend` configuration flag. @@ -115,9 +115,9 @@ which JavaScript interface should be used (or if JavaScript is available at all) Instead, *if the `wasm_js` backend is enabled*, this crate will assume that you are building for an environment containing JavaScript, and will -call the appropriate methods. Both web browser (main window and Web Workers) -and Node.js environments are supported, invoking the methods -[described above](#opt-in-backends) using the [`wasm-bindgen`] toolchain. +call the appropriate Web Crypto methods [described above](#opt-in-backends) using +the [`wasm-bindgen`] toolchain. Both web browser (main window and Web Workers) +and Node.js (v19 or later) environments are supported. To enable the `wasm_js` backend, you can add the following lines to your project's `.cargo/config.toml` file: @@ -126,18 +126,6 @@ project's `.cargo/config.toml` file: rustflags = ['--cfg', 'getrandom_backend="wasm_js"'] ``` -#### Node.js ES module support - -Node.js supports both [CommonJS modules] and [ES modules]. Due to -limitations in wasm-bindgen's [`module`] support, we cannot directly -support ES Modules running on Node.js. However, on Node v15 and later, the -module author can add a simple shim to support the Web Cryptography API: -```js -import { webcrypto } from 'node:crypto' -globalThis.crypto = webcrypto -``` -This crate will then use the provided `webcrypto` implementation. - ### Custom backend If this crate does not support your target out of the box or you have to use @@ -348,17 +336,13 @@ dual licensed as above, without any additional terms or conditions. [`RNDR`]: https://developer.arm.com/documentation/ddi0601/2024-06/AArch64-Registers/RNDR--Random-Number [`CCRandomGenerateBytes`]: https://opensource.apple.com/source/CommonCrypto/CommonCrypto-60074/include/CommonRandom.h.auto.html [`cprng_draw`]: https://fuchsia.dev/fuchsia-src/zircon/syscalls/cprng_draw -[`crypto.randomFillSync`]: https://nodejs.org/api/crypto.html#cryptorandomfillsyncbuffer-offset-size [`esp_fill_random`]: https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/system/random.html#_CPPv415esp_fill_randomPv6size_t [`random_get`]: https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-random_getbuf-pointeru8-buf_len-size---errno [`get-random-u64`]: https://github.com/WebAssembly/WASI/blob/v0.2.1/wasip2/random/random.wit#L23-L28 -[WebAssembly support]: #webassembly-support [configuration flags]: #configuration-flags [custom backend]: #custom-backend [`wasm-bindgen`]: https://github.com/rustwasm/wasm-bindgen [`module`]: https://rustwasm.github.io/wasm-bindgen/reference/attributes/on-js-imports/module.html -[CommonJS modules]: https://nodejs.org/api/modules.html -[ES modules]: https://nodejs.org/api/esm.html [`sys_read_entropy`]: https://github.com/hermit-os/kernel/blob/315f58ff5efc81d9bf0618af85a59963ff55f8b1/src/syscalls/entropy.rs#L47-L55 [platform-support]: https://doc.rust-lang.org/stable/rustc/platform-support.html [WASI]: https://github.com/CraneStation/wasi diff --git a/src/backends.rs b/src/backends.rs index a744a229..f7b720f5 100644 --- a/src/backends.rs +++ b/src/backends.rs @@ -150,7 +150,7 @@ cfg_if! { pub use rdrand::*; } else if #[cfg(all( target_arch = "wasm32", - target_os = "unknown", + any(target_os = "unknown", target_os = "none") ))] { compile_error!("the wasm32-unknown-unknown targets are not supported \ by default, you may need to enable the \"wasm_js\" \ diff --git a/src/backends/wasm_js.rs b/src/backends/wasm_js.rs index 15a4e66b..7753daf9 100644 --- a/src/backends/wasm_js.rs +++ b/src/backends/wasm_js.rs @@ -1,158 +1,59 @@ //! Implementation for WASM based on Web and Node.js use crate::Error; - -extern crate std; -use std::{mem::MaybeUninit, thread_local}; +use core::mem::MaybeUninit; pub use crate::util::{inner_u32, inner_u64}; -#[cfg(not(all(target_arch = "wasm32", target_os = "unknown",)))] +#[cfg(not(all(target_arch = "wasm32", any(target_os = "unknown", target_os = "none"))))] compile_error!("`wasm_js` backend can be enabled only for OS-less WASM targets!"); -use js_sys::{global, Function, Uint8Array}; +use js_sys::{global, Uint8Array}; use wasm_bindgen::{prelude::wasm_bindgen, JsCast, JsValue}; // Size of our temporary Uint8Array buffer used with WebCrypto methods // Maximum is 65536 bytes see https://developer.mozilla.org/en-US/docs/Web/API/Crypto/getRandomValues -const WEB_CRYPTO_BUFFER_SIZE: u16 = 256; -// Node.js's crypto.randomFillSync requires the size to be less than 2**31. -const NODE_MAX_BUFFER_SIZE: usize = (1 << 31) - 1; - -enum RngSource { - Node(NodeCrypto), - Web(WebCrypto, Uint8Array), -} - -// JsValues are always per-thread, so we initialize RngSource for each thread. -// See: https://github.com/rustwasm/wasm-bindgen/pull/955 -thread_local!( - static RNG_SOURCE: Result = getrandom_init(); -); +const CRYPTO_BUFFER_SIZE: u16 = 256; pub fn fill_inner(dest: &mut [MaybeUninit]) -> Result<(), Error> { - RNG_SOURCE.with(|result| { - let source = result.as_ref().map_err(|&e| e)?; - - match source { - RngSource::Node(n) => { - for chunk in dest.chunks_mut(NODE_MAX_BUFFER_SIZE) { - // SAFETY: chunk is never used directly, the memory is only - // modified via the Uint8Array view, which is passed - // directly to JavaScript. Also, crypto.randomFillSync does - // not resize the buffer. We know the length is less than - // u32::MAX because of the chunking above. - // Note that this uses the fact that JavaScript doesn't - // have a notion of "uninitialized memory", this is purely - // a Rust/C/C++ concept. - let res = n.random_fill_sync(unsafe { - Uint8Array::view_mut_raw(chunk.as_mut_ptr().cast::(), chunk.len()) - }); - if res.is_err() { - return Err(Error::NODE_RANDOM_FILL_SYNC); - } - } - } - RngSource::Web(crypto, buf) => { - // getRandomValues does not work with all types of WASM memory, - // so we initially write to browser memory to avoid exceptions. - for chunk in dest.chunks_mut(WEB_CRYPTO_BUFFER_SIZE.into()) { - let chunk_len: u32 = chunk - .len() - .try_into() - .expect("chunk length is bounded by WEB_CRYPTO_BUFFER_SIZE"); - // The chunk can be smaller than buf's length, so we call to - // JS to create a smaller view of buf without allocation. - let sub_buf = buf.subarray(0, chunk_len); - - if crypto.get_random_values(&sub_buf).is_err() { - return Err(Error::WEB_GET_RANDOM_VALUES); - } - - // SAFETY: `sub_buf`'s length is the same length as `chunk` - unsafe { sub_buf.raw_copy_to_ptr(chunk.as_mut_ptr().cast::()) }; - } - } - }; - Ok(()) - }) -} - -fn getrandom_init() -> Result { let global: Global = global().unchecked_into(); - - // Get the Web Crypto interface if we are in a browser, Web Worker, Deno, - // or another environment that supports the Web Cryptography API. This - // also allows for user-provided polyfills in unsupported environments. let crypto = global.crypto(); - if crypto.is_object() { - let buf = Uint8Array::new_with_length(WEB_CRYPTO_BUFFER_SIZE.into()); - Ok(RngSource::Web(crypto, buf)) - } else if is_node(&global) { - // If module.require isn't a valid function, we are in an ES module. - let require_fn = Module::require_fn() - .and_then(JsCast::dyn_into::) - .map_err(|_| Error::NODE_ES_MODULE)?; - let n = require_fn - .call1(&global, &JsValue::from_str("crypto")) - .map_err(|_| Error::NODE_CRYPTO)? - .unchecked_into(); - Ok(RngSource::Node(n)) - } else { - Err(Error::WEB_CRYPTO) + + if !crypto.is_object() { + return Err(Error::WEB_CRYPTO); } -} -// Taken from https://www.npmjs.com/package/browser-or-node -fn is_node(global: &Global) -> bool { - let process = global.process(); - if process.is_object() { - let versions = process.versions(); - if versions.is_object() { - return versions.node().is_string(); + // getRandomValues does not work with all types of WASM memory, + // so we initially write to browser memory to avoid exceptions. + let buf = Uint8Array::new_with_length(CRYPTO_BUFFER_SIZE.into()); + for chunk in dest.chunks_mut(CRYPTO_BUFFER_SIZE.into()) { + let chunk_len: u32 = chunk + .len() + .try_into() + .expect("chunk length is bounded by CRYPTO_BUFFER_SIZE"); + // The chunk can be smaller than buf's length, so we call to + // JS to create a smaller view of buf without allocation. + let sub_buf = buf.subarray(0, chunk_len); + + if crypto.get_random_values(&sub_buf).is_err() { + return Err(Error::WEB_GET_RANDOM_VALUES); } + + // SAFETY: `sub_buf`'s length is the same length as `chunk` + unsafe { sub_buf.raw_copy_to_ptr(chunk.as_mut_ptr().cast::()) }; } - false + Ok(()) } #[wasm_bindgen] extern "C" { // Return type of js_sys::global() type Global; - // Web Crypto API: Crypto interface (https://www.w3.org/TR/WebCryptoAPI/) - type WebCrypto; - // Getters for the WebCrypto API + type Crypto; + // Getters for the Crypto API #[wasm_bindgen(method, getter)] - fn crypto(this: &Global) -> WebCrypto; - #[wasm_bindgen(method, getter, js_name = msCrypto)] - fn ms_crypto(this: &Global) -> WebCrypto; + fn crypto(this: &Global) -> Crypto; // Crypto.getRandomValues() #[wasm_bindgen(method, js_name = getRandomValues, catch)] - fn get_random_values(this: &WebCrypto, buf: &Uint8Array) -> Result<(), JsValue>; - - // Node JS crypto module (https://nodejs.org/api/crypto.html) - type NodeCrypto; - // crypto.randomFillSync() - #[wasm_bindgen(method, js_name = randomFillSync, catch)] - fn random_fill_sync(this: &NodeCrypto, buf: Uint8Array) -> Result<(), JsValue>; - - // Ideally, we would just use `fn require(s: &str)` here. However, doing - // this causes a Webpack warning. So we instead return the function itself - // and manually invoke it using call1. This also lets us to check that the - // function actually exists, allowing for better error messages. See: - // https://github.com/rust-random/getrandom/issues/224 - // https://github.com/rust-random/getrandom/issues/256 - type Module; - #[wasm_bindgen(getter, static_method_of = Module, js_class = module, js_name = require, catch)] - fn require_fn() -> Result; - - // Node JS process Object (https://nodejs.org/api/process.html) - #[wasm_bindgen(method, getter)] - fn process(this: &Global) -> Process; - type Process; - #[wasm_bindgen(method, getter)] - fn versions(this: &Process) -> Versions; - type Versions; - #[wasm_bindgen(method, getter)] - fn node(this: &Versions) -> JsValue; + fn get_random_values(this: &Crypto, buf: &Uint8Array) -> Result<(), JsValue>; } diff --git a/src/error.rs b/src/error.rs index b27ef26b..0f486c7b 100644 --- a/src/error.rs +++ b/src/error.rs @@ -43,19 +43,12 @@ impl Error { pub const WEB_GET_RANDOM_VALUES: Error = Self::new_internal(8); /// On VxWorks, call to `randSecure` failed (random number generator is not yet initialized). pub const VXWORKS_RAND_SECURE: Error = Self::new_internal(11); - /// Node.js does not have the `crypto` CommonJS module. - pub const NODE_CRYPTO: Error = Self::new_internal(12); - /// Calling Node.js function `crypto.randomFillSync` failed. - pub const NODE_RANDOM_FILL_SYNC: Error = Self::new_internal(13); - /// Called from an ES module on Node.js. This is unsupported, see: - /// . - pub const NODE_ES_MODULE: Error = Self::new_internal(14); /// Calling Windows ProcessPrng failed. - pub const WINDOWS_PROCESS_PRNG: Error = Self::new_internal(15); + pub const WINDOWS_PROCESS_PRNG: Error = Self::new_internal(12); /// RNDR register read failed due to a hardware issue. - pub const RNDR_FAILURE: Error = Self::new_internal(16); + pub const RNDR_FAILURE: Error = Self::new_internal(13); /// RNDR register is not supported on this target. - pub const RNDR_NOT_AVAILABLE: Error = Self::new_internal(17); + pub const RNDR_NOT_AVAILABLE: Error = Self::new_internal(14); /// Codes below this point represent OS Errors (i.e. positive i32 values). /// Codes at or above this point, but below [`Error::CUSTOM_START`] are @@ -164,9 +157,6 @@ fn internal_desc(error: Error) -> Option<&'static str> { Error::WEB_CRYPTO => "Web Crypto API is unavailable", Error::WEB_GET_RANDOM_VALUES => "Calling Web API crypto.getRandomValues failed", Error::VXWORKS_RAND_SECURE => "randSecure: VxWorks RNG module is not initialized", - Error::NODE_CRYPTO => "Node.js crypto CommonJS module is unavailable", - Error::NODE_RANDOM_FILL_SYNC => "Calling Node.js API crypto.randomFillSync failed", - Error::NODE_ES_MODULE => "Node.js ES modules are not directly supported, see https://docs.rs/getrandom#nodejs-es-module-support", Error::WINDOWS_PROCESS_PRNG => "ProcessPrng: Windows system function failure", Error::RNDR_FAILURE => "RNDR: Could not generate a random number", Error::RNDR_NOT_AVAILABLE => "RNDR: Register not supported",