Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

wasm_js: remove the separate codepath for Node.js and TLS caching #557

Merged
merged 17 commits into from
Dec 4, 2024
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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
Expand Down
11 changes: 5 additions & 6 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)',
]
Expand Down
24 changes: 4 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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])
newpavlov marked this conversation as resolved.
Show resolved Hide resolved
| `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.
Expand Down Expand Up @@ -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:
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/backends.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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\" \
Expand Down
157 changes: 29 additions & 128 deletions src/backends/wasm_js.rs
Original file line number Diff line number Diff line change
@@ -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<RngSource, Error> = getrandom_init();
);
const CRYPTO_BUFFER_SIZE: u16 = 256;

pub fn fill_inner(dest: &mut [MaybeUninit<u8>]) -> 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::<u8>(), 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::<u8>()) };
}
}
};
Ok(())
})
}

fn getrandom_init() -> Result<RngSource, Error> {
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();
newpavlov marked this conversation as resolved.
Show resolved Hide resolved
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::<Function>)
.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::<u8>()) };
}
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<JsValue, JsValue>;

// 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>;
}
16 changes: 3 additions & 13 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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:
/// <https://docs.rs/getrandom#nodejs-es-module-support>.
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
Expand Down Expand Up @@ -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",
Expand Down