Skip to content

Commit 5fbfd2e

Browse files
authored
wasm_js: remove the separate codepath for Node.js and TLS caching (#557)
Acquire of "global" objects is relatively cheap (adds ~40 ns of overhead), so it make sense to do it on each call instead of trying to cache objects in a TLS variable. Additionally, removes the separate codepath for Node.js since it fully supports the Web Crypto API since v19 (released 2022-10-18).
1 parent 96ed30e commit 5fbfd2e

File tree

6 files changed

+46
-169
lines changed

6 files changed

+46
-169
lines changed

CHANGELOG.md

+4-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2828
- Switch from `libpthread`'s mutex to `futex` on Linux and to `nanosleep`-based wait loop
2929
on other targets in the `use_file` backend [#490]
3030
- Do not retry on `EAGAIN` while polling `/dev/random` on Linux [#522]
31-
31+
- Remove separate codepath for Node.js in the `wasm_js` backend (bumps minimum supported Node.js
32+
version to v19) [#557]
33+
3234
### Added
3335
- `wasm32-wasip1` and `wasm32-wasip2` support [#499]
3436
- `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
5860
[#544]: https://github.com/rust-random/getrandom/pull/544
5961
[#554]: https://github.com/rust-random/getrandom/pull/554
6062
[#555]: https://github.com/rust-random/getrandom/pull/555
63+
[#557]: https://github.com/rust-random/getrandom/pull/557
6164

6265
## [0.2.15] - 2024-05-06
6366
### Added

Cargo.toml

+5-6
Original file line numberDiff line numberDiff line change
@@ -63,11 +63,11 @@ wasi = { version = "0.13", default-features = false }
6363
windows-targets = "0.52"
6464

6565
# wasm_js
66-
[target.'cfg(all(getrandom_backend = "wasm_js", target_arch = "wasm32", target_os = "unknown"))'.dependencies]
67-
wasm-bindgen = { version = "0.2.89", default-features = false }
68-
js-sys = "0.3"
69-
[target.'cfg(all(getrandom_backend = "wasm_js", target_arch = "wasm32", target_os = "unknown"))'.dev-dependencies]
70-
wasm-bindgen-test = "0.3.39"
66+
[target.'cfg(all(getrandom_backend = "wasm_js", target_arch = "wasm32", any(target_os = "unknown", target_os = "none")))'.dependencies]
67+
wasm-bindgen = { version = "0.2.96", default-features = false }
68+
js-sys = "0.3.73"
69+
[target.'cfg(all(getrandom_backend = "wasm_js", target_arch = "wasm32", any(target_os = "unknown", target_os = "none")))'.dev-dependencies]
70+
wasm-bindgen-test = "0.3"
7171

7272
[features]
7373
# Implement std::error::Error for getrandom::Error and
@@ -81,7 +81,6 @@ level = "warn"
8181
check-cfg = [
8282
'cfg(getrandom_backend, values("custom", "rdrand", "rndr", "linux_getrandom", "linux_rustix", "wasm_js", "esp_idf"))',
8383
'cfg(getrandom_sanitize)',
84-
'cfg(getrandom_browser_test)',
8584
'cfg(getrandom_test_linux_fallback)',
8685
'cfg(getrandom_test_netbsd_fallback)',
8786
]

README.md

+4-20
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ of randomness based on their specific needs:
8383
| `rdrand` | x86, x86-64 | `x86_64-*`, `i686-*` | [`RDRAND`] instruction
8484
| `rndr` | AArch64 | `aarch64-*` | [`RNDR`] register
8585
| `esp_idf` | ESP-IDF | `*‑espidf` | [`esp_fill_random`]. WARNING: can return low-quality entropy without proper hardware configuration!
86-
| `wasm_js` | Web Browser, Node.js | `wasm32‑unknown‑unknown` | [`Crypto.getRandomValues`] if available, then [`crypto.randomFillSync`] if on Node.js (see [WebAssembly support])
86+
| `wasm_js` | Web Browser, Node.js | `wasm32‑unknown‑unknown`, `wasm32v1-none` | [`Crypto.getRandomValues`]
8787
| `custom` | All targets | `*` | User-provided custom implementation (see [custom backend])
8888

8989
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)
115115

116116
Instead, *if the `wasm_js` backend is enabled*, this crate will assume
117117
that you are building for an environment containing JavaScript, and will
118-
call the appropriate methods. Both web browser (main window and Web Workers)
119-
and Node.js environments are supported, invoking the methods
120-
[described above](#opt-in-backends) using the [`wasm-bindgen`] toolchain.
118+
call the appropriate Web Crypto methods [described above](#opt-in-backends) using
119+
the [`wasm-bindgen`] toolchain. Both web browser (main window and Web Workers)
120+
and Node.js (v19 or later) environments are supported.
121121

122122
To enable the `wasm_js` backend, you can add the following lines to your
123123
project's `.cargo/config.toml` file:
@@ -126,18 +126,6 @@ project's `.cargo/config.toml` file:
126126
rustflags = ['--cfg', 'getrandom_backend="wasm_js"']
127127
```
128128

129-
#### Node.js ES module support
130-
131-
Node.js supports both [CommonJS modules] and [ES modules]. Due to
132-
limitations in wasm-bindgen's [`module`] support, we cannot directly
133-
support ES Modules running on Node.js. However, on Node v15 and later, the
134-
module author can add a simple shim to support the Web Cryptography API:
135-
```js
136-
import { webcrypto } from 'node:crypto'
137-
globalThis.crypto = webcrypto
138-
```
139-
This crate will then use the provided `webcrypto` implementation.
140-
141129
### Custom backend
142130

143131
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.
348336
[`RNDR`]: https://developer.arm.com/documentation/ddi0601/2024-06/AArch64-Registers/RNDR--Random-Number
349337
[`CCRandomGenerateBytes`]: https://opensource.apple.com/source/CommonCrypto/CommonCrypto-60074/include/CommonRandom.h.auto.html
350338
[`cprng_draw`]: https://fuchsia.dev/fuchsia-src/zircon/syscalls/cprng_draw
351-
[`crypto.randomFillSync`]: https://nodejs.org/api/crypto.html#cryptorandomfillsyncbuffer-offset-size
352339
[`esp_fill_random`]: https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/system/random.html#_CPPv415esp_fill_randomPv6size_t
353340
[`random_get`]: https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-random_getbuf-pointeru8-buf_len-size---errno
354341
[`get-random-u64`]: https://github.com/WebAssembly/WASI/blob/v0.2.1/wasip2/random/random.wit#L23-L28
355-
[WebAssembly support]: #webassembly-support
356342
[configuration flags]: #configuration-flags
357343
[custom backend]: #custom-backend
358344
[`wasm-bindgen`]: https://github.com/rustwasm/wasm-bindgen
359345
[`module`]: https://rustwasm.github.io/wasm-bindgen/reference/attributes/on-js-imports/module.html
360-
[CommonJS modules]: https://nodejs.org/api/modules.html
361-
[ES modules]: https://nodejs.org/api/esm.html
362346
[`sys_read_entropy`]: https://github.com/hermit-os/kernel/blob/315f58ff5efc81d9bf0618af85a59963ff55f8b1/src/syscalls/entropy.rs#L47-L55
363347
[platform-support]: https://doc.rust-lang.org/stable/rustc/platform-support.html
364348
[WASI]: https://github.com/CraneStation/wasi

src/backends.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ cfg_if! {
150150
pub use rdrand::*;
151151
} else if #[cfg(all(
152152
target_arch = "wasm32",
153-
target_os = "unknown",
153+
any(target_os = "unknown", target_os = "none")
154154
))] {
155155
compile_error!("the wasm32-unknown-unknown targets are not supported \
156156
by default, you may need to enable the \"wasm_js\" \

src/backends/wasm_js.rs

+29-128
Original file line numberDiff line numberDiff line change
@@ -1,158 +1,59 @@
11
//! Implementation for WASM based on Web and Node.js
22
use crate::Error;
3-
4-
extern crate std;
5-
use std::{mem::MaybeUninit, thread_local};
3+
use core::mem::MaybeUninit;
64

75
pub use crate::util::{inner_u32, inner_u64};
86

9-
#[cfg(not(all(target_arch = "wasm32", target_os = "unknown",)))]
7+
#[cfg(not(all(target_arch = "wasm32", any(target_os = "unknown", target_os = "none"))))]
108
compile_error!("`wasm_js` backend can be enabled only for OS-less WASM targets!");
119

12-
use js_sys::{global, Function, Uint8Array};
10+
use js_sys::{global, Uint8Array};
1311
use wasm_bindgen::{prelude::wasm_bindgen, JsCast, JsValue};
1412

1513
// Size of our temporary Uint8Array buffer used with WebCrypto methods
1614
// Maximum is 65536 bytes see https://developer.mozilla.org/en-US/docs/Web/API/Crypto/getRandomValues
17-
const WEB_CRYPTO_BUFFER_SIZE: u16 = 256;
18-
// Node.js's crypto.randomFillSync requires the size to be less than 2**31.
19-
const NODE_MAX_BUFFER_SIZE: usize = (1 << 31) - 1;
20-
21-
enum RngSource {
22-
Node(NodeCrypto),
23-
Web(WebCrypto, Uint8Array),
24-
}
25-
26-
// JsValues are always per-thread, so we initialize RngSource for each thread.
27-
// See: https://github.com/rustwasm/wasm-bindgen/pull/955
28-
thread_local!(
29-
static RNG_SOURCE: Result<RngSource, Error> = getrandom_init();
30-
);
15+
const CRYPTO_BUFFER_SIZE: u16 = 256;
3116

3217
pub fn fill_inner(dest: &mut [MaybeUninit<u8>]) -> Result<(), Error> {
33-
RNG_SOURCE.with(|result| {
34-
let source = result.as_ref().map_err(|&e| e)?;
35-
36-
match source {
37-
RngSource::Node(n) => {
38-
for chunk in dest.chunks_mut(NODE_MAX_BUFFER_SIZE) {
39-
// SAFETY: chunk is never used directly, the memory is only
40-
// modified via the Uint8Array view, which is passed
41-
// directly to JavaScript. Also, crypto.randomFillSync does
42-
// not resize the buffer. We know the length is less than
43-
// u32::MAX because of the chunking above.
44-
// Note that this uses the fact that JavaScript doesn't
45-
// have a notion of "uninitialized memory", this is purely
46-
// a Rust/C/C++ concept.
47-
let res = n.random_fill_sync(unsafe {
48-
Uint8Array::view_mut_raw(chunk.as_mut_ptr().cast::<u8>(), chunk.len())
49-
});
50-
if res.is_err() {
51-
return Err(Error::NODE_RANDOM_FILL_SYNC);
52-
}
53-
}
54-
}
55-
RngSource::Web(crypto, buf) => {
56-
// getRandomValues does not work with all types of WASM memory,
57-
// so we initially write to browser memory to avoid exceptions.
58-
for chunk in dest.chunks_mut(WEB_CRYPTO_BUFFER_SIZE.into()) {
59-
let chunk_len: u32 = chunk
60-
.len()
61-
.try_into()
62-
.expect("chunk length is bounded by WEB_CRYPTO_BUFFER_SIZE");
63-
// The chunk can be smaller than buf's length, so we call to
64-
// JS to create a smaller view of buf without allocation.
65-
let sub_buf = buf.subarray(0, chunk_len);
66-
67-
if crypto.get_random_values(&sub_buf).is_err() {
68-
return Err(Error::WEB_GET_RANDOM_VALUES);
69-
}
70-
71-
// SAFETY: `sub_buf`'s length is the same length as `chunk`
72-
unsafe { sub_buf.raw_copy_to_ptr(chunk.as_mut_ptr().cast::<u8>()) };
73-
}
74-
}
75-
};
76-
Ok(())
77-
})
78-
}
79-
80-
fn getrandom_init() -> Result<RngSource, Error> {
8118
let global: Global = global().unchecked_into();
82-
83-
// Get the Web Crypto interface if we are in a browser, Web Worker, Deno,
84-
// or another environment that supports the Web Cryptography API. This
85-
// also allows for user-provided polyfills in unsupported environments.
8619
let crypto = global.crypto();
87-
if crypto.is_object() {
88-
let buf = Uint8Array::new_with_length(WEB_CRYPTO_BUFFER_SIZE.into());
89-
Ok(RngSource::Web(crypto, buf))
90-
} else if is_node(&global) {
91-
// If module.require isn't a valid function, we are in an ES module.
92-
let require_fn = Module::require_fn()
93-
.and_then(JsCast::dyn_into::<Function>)
94-
.map_err(|_| Error::NODE_ES_MODULE)?;
95-
let n = require_fn
96-
.call1(&global, &JsValue::from_str("crypto"))
97-
.map_err(|_| Error::NODE_CRYPTO)?
98-
.unchecked_into();
99-
Ok(RngSource::Node(n))
100-
} else {
101-
Err(Error::WEB_CRYPTO)
20+
21+
if !crypto.is_object() {
22+
return Err(Error::WEB_CRYPTO);
10223
}
103-
}
10424

105-
// Taken from https://www.npmjs.com/package/browser-or-node
106-
fn is_node(global: &Global) -> bool {
107-
let process = global.process();
108-
if process.is_object() {
109-
let versions = process.versions();
110-
if versions.is_object() {
111-
return versions.node().is_string();
25+
// getRandomValues does not work with all types of WASM memory,
26+
// so we initially write to browser memory to avoid exceptions.
27+
let buf = Uint8Array::new_with_length(CRYPTO_BUFFER_SIZE.into());
28+
for chunk in dest.chunks_mut(CRYPTO_BUFFER_SIZE.into()) {
29+
let chunk_len: u32 = chunk
30+
.len()
31+
.try_into()
32+
.expect("chunk length is bounded by CRYPTO_BUFFER_SIZE");
33+
// The chunk can be smaller than buf's length, so we call to
34+
// JS to create a smaller view of buf without allocation.
35+
let sub_buf = buf.subarray(0, chunk_len);
36+
37+
if crypto.get_random_values(&sub_buf).is_err() {
38+
return Err(Error::WEB_GET_RANDOM_VALUES);
11239
}
40+
41+
// SAFETY: `sub_buf`'s length is the same length as `chunk`
42+
unsafe { sub_buf.raw_copy_to_ptr(chunk.as_mut_ptr().cast::<u8>()) };
11343
}
114-
false
44+
Ok(())
11545
}
11646

11747
#[wasm_bindgen]
11848
extern "C" {
11949
// Return type of js_sys::global()
12050
type Global;
121-
12251
// Web Crypto API: Crypto interface (https://www.w3.org/TR/WebCryptoAPI/)
123-
type WebCrypto;
124-
// Getters for the WebCrypto API
52+
type Crypto;
53+
// Getters for the Crypto API
12554
#[wasm_bindgen(method, getter)]
126-
fn crypto(this: &Global) -> WebCrypto;
127-
#[wasm_bindgen(method, getter, js_name = msCrypto)]
128-
fn ms_crypto(this: &Global) -> WebCrypto;
55+
fn crypto(this: &Global) -> Crypto;
12956
// Crypto.getRandomValues()
13057
#[wasm_bindgen(method, js_name = getRandomValues, catch)]
131-
fn get_random_values(this: &WebCrypto, buf: &Uint8Array) -> Result<(), JsValue>;
132-
133-
// Node JS crypto module (https://nodejs.org/api/crypto.html)
134-
type NodeCrypto;
135-
// crypto.randomFillSync()
136-
#[wasm_bindgen(method, js_name = randomFillSync, catch)]
137-
fn random_fill_sync(this: &NodeCrypto, buf: Uint8Array) -> Result<(), JsValue>;
138-
139-
// Ideally, we would just use `fn require(s: &str)` here. However, doing
140-
// this causes a Webpack warning. So we instead return the function itself
141-
// and manually invoke it using call1. This also lets us to check that the
142-
// function actually exists, allowing for better error messages. See:
143-
// https://github.com/rust-random/getrandom/issues/224
144-
// https://github.com/rust-random/getrandom/issues/256
145-
type Module;
146-
#[wasm_bindgen(getter, static_method_of = Module, js_class = module, js_name = require, catch)]
147-
fn require_fn() -> Result<JsValue, JsValue>;
148-
149-
// Node JS process Object (https://nodejs.org/api/process.html)
150-
#[wasm_bindgen(method, getter)]
151-
fn process(this: &Global) -> Process;
152-
type Process;
153-
#[wasm_bindgen(method, getter)]
154-
fn versions(this: &Process) -> Versions;
155-
type Versions;
156-
#[wasm_bindgen(method, getter)]
157-
fn node(this: &Versions) -> JsValue;
58+
fn get_random_values(this: &Crypto, buf: &Uint8Array) -> Result<(), JsValue>;
15859
}

src/error.rs

+3-13
Original file line numberDiff line numberDiff line change
@@ -43,19 +43,12 @@ impl Error {
4343
pub const WEB_GET_RANDOM_VALUES: Error = Self::new_internal(8);
4444
/// On VxWorks, call to `randSecure` failed (random number generator is not yet initialized).
4545
pub const VXWORKS_RAND_SECURE: Error = Self::new_internal(11);
46-
/// Node.js does not have the `crypto` CommonJS module.
47-
pub const NODE_CRYPTO: Error = Self::new_internal(12);
48-
/// Calling Node.js function `crypto.randomFillSync` failed.
49-
pub const NODE_RANDOM_FILL_SYNC: Error = Self::new_internal(13);
50-
/// Called from an ES module on Node.js. This is unsupported, see:
51-
/// <https://docs.rs/getrandom#nodejs-es-module-support>.
52-
pub const NODE_ES_MODULE: Error = Self::new_internal(14);
5346
/// Calling Windows ProcessPrng failed.
54-
pub const WINDOWS_PROCESS_PRNG: Error = Self::new_internal(15);
47+
pub const WINDOWS_PROCESS_PRNG: Error = Self::new_internal(12);
5548
/// RNDR register read failed due to a hardware issue.
56-
pub const RNDR_FAILURE: Error = Self::new_internal(16);
49+
pub const RNDR_FAILURE: Error = Self::new_internal(13);
5750
/// RNDR register is not supported on this target.
58-
pub const RNDR_NOT_AVAILABLE: Error = Self::new_internal(17);
51+
pub const RNDR_NOT_AVAILABLE: Error = Self::new_internal(14);
5952

6053
/// Codes below this point represent OS Errors (i.e. positive i32 values).
6154
/// Codes at or above this point, but below [`Error::CUSTOM_START`] are
@@ -164,9 +157,6 @@ fn internal_desc(error: Error) -> Option<&'static str> {
164157
Error::WEB_CRYPTO => "Web Crypto API is unavailable",
165158
Error::WEB_GET_RANDOM_VALUES => "Calling Web API crypto.getRandomValues failed",
166159
Error::VXWORKS_RAND_SECURE => "randSecure: VxWorks RNG module is not initialized",
167-
Error::NODE_CRYPTO => "Node.js crypto CommonJS module is unavailable",
168-
Error::NODE_RANDOM_FILL_SYNC => "Calling Node.js API crypto.randomFillSync failed",
169-
Error::NODE_ES_MODULE => "Node.js ES modules are not directly supported, see https://docs.rs/getrandom#nodejs-es-module-support",
170160
Error::WINDOWS_PROCESS_PRNG => "ProcessPrng: Windows system function failure",
171161
Error::RNDR_FAILURE => "RNDR: Could not generate a random number",
172162
Error::RNDR_NOT_AVAILABLE => "RNDR: Register not supported",

0 commit comments

Comments
 (0)