From d69e8e003b434e98f1ad675cae4b53d3e3864fa0 Mon Sep 17 00:00:00 2001 From: Joe Richey Date: Tue, 13 Sep 2022 00:13:09 -0700 Subject: [PATCH 1/5] Rework JS feature detection Now we look for the standard Web Cryptography API before attempting to check for Node.js support. This allows Node.js ES6 module users to add a polyfill like: ```js import {webcrypto} from 'crypto' globalThis.crypto = webcrypto ``` as described in https://github.com/rust-random/getrandom/issues/256#issuecomment-1161028902 Signed-off-by: Joe Richey --- src/error.rs | 14 +++++----- src/js.rs | 78 +++++++++++++++++++++++++++++----------------------- 2 files changed, 51 insertions(+), 41 deletions(-) diff --git a/src/error.rs b/src/error.rs index b5ab2bb1..c3e71be3 100644 --- a/src/error.rs +++ b/src/error.rs @@ -43,15 +43,15 @@ impl Error { pub const FAILED_RDRAND: Error = internal_error(5); /// RDRAND instruction unsupported on this target. pub const NO_RDRAND: Error = internal_error(6); - /// The browser does not have support for `self.crypto`. + /// The environment does not support the Web Crypto API. pub const WEB_CRYPTO: Error = internal_error(7); - /// The browser does not have support for `crypto.getRandomValues`. + /// Calling Web Crypto API `crypto.getRandomValues` failed. pub const WEB_GET_RANDOM_VALUES: Error = internal_error(8); /// On VxWorks, call to `randSecure` failed (random number generator is not yet initialized). pub const VXWORKS_RAND_SECURE: Error = internal_error(11); - /// NodeJS does not have support for the `crypto` module. + /// Node.js does not have the `crypto` CommonJS module. pub const NODE_CRYPTO: Error = internal_error(12); - /// NodeJS does not have support for `crypto.randomFillSync`. + /// Calling Node.js function `crypto.randomFillSync` failed. pub const NODE_RANDOM_FILL_SYNC: Error = internal_error(13); /// Codes below this point represent OS Errors (i.e. positive i32 values). @@ -166,10 +166,10 @@ fn internal_desc(error: Error) -> Option<&'static str> { Error::FAILED_RDRAND => Some("RDRAND: failed multiple times: CPU issue likely"), Error::NO_RDRAND => Some("RDRAND: instruction not supported"), Error::WEB_CRYPTO => Some("Web Crypto API is unavailable"), - Error::WEB_GET_RANDOM_VALUES => Some("Web API crypto.getRandomValues is unavailable"), + Error::WEB_GET_RANDOM_VALUES => Some("Calling Web API crypto.getRandomValues failed"), Error::VXWORKS_RAND_SECURE => Some("randSecure: VxWorks RNG module is not initialized"), - Error::NODE_CRYPTO => Some("Node.js crypto module is unavailable"), - Error::NODE_RANDOM_FILL_SYNC => Some("Node.js API crypto.randomFillSync is unavailable"), + Error::NODE_CRYPTO => Some("Node.js crypto CommonJS module is unavailable"), + Error::NODE_RANDOM_FILL_SYNC => Some("Calling Node.js API crypto.randomFillSync failed"), _ => None, } } diff --git a/src/js.rs b/src/js.rs index e910f2bc..9ff69a58 100644 --- a/src/js.rs +++ b/src/js.rs @@ -13,12 +13,13 @@ use std::thread_local; 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 BROWSER_CRYPTO_BUFFER_SIZE: usize = 256; +const WEB_CRYPTO_BUFFER_SIZE: usize = 256; enum RngSource { Node(NodeCrypto), - Browser(BrowserCrypto, Uint8Array), + Web(WebCrypto, Uint8Array), } // JsValues are always per-thread, so we initialize RngSource for each thread. @@ -37,10 +38,10 @@ pub(crate) fn getrandom_inner(dest: &mut [u8]) -> Result<(), Error> { return Err(Error::NODE_RANDOM_FILL_SYNC); } } - RngSource::Browser(crypto, buf) => { + 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(BROWSER_CRYPTO_BUFFER_SIZE) { + for chunk in dest.chunks_mut(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() as u32); @@ -58,25 +59,29 @@ pub(crate) fn getrandom_inner(dest: &mut [u8]) -> Result<(), Error> { fn getrandom_init() -> Result { let global: Global = global().unchecked_into(); - if is_node(&global) { - let crypto = NODE_MODULE - .require("crypto") - .map_err(|_| Error::NODE_CRYPTO)?; - return Ok(RngSource::Node(crypto)); - } - // Assume we are in some Web environment (browser or web worker). We get - // `self.crypto` (called `msCrypto` on IE), so we can call - // `crypto.getRandomValues`. If `crypto` isn't defined, we assume that - // we are in an older web browser and the OS RNG isn't available. - let crypto = match (global.crypto(), global.ms_crypto()) { - (c, _) if c.is_object() => c, - (_, c) if c.is_object() => c, - _ => return Err(Error::WEB_CRYPTO), + // 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 = match global.crypto() { + // Standard Web Crypto interface + c if c.is_object() => c, + // Node.js CommonJS Crypto module + _ if is_node(&global) => { + match MODULE.require("crypto") { + Ok(n) if n.is_object() => return Ok(RngSource::Node(n)), + _ => return Err(Error::NODE_CRYPTO), + }; + } + // IE 11 Workaround + _ => match global.ms_crypto() { + c if c.is_object() => c, + _ => return Err(Error::WEB_CRYPTO), + }, }; - let buf = Uint8Array::new_with_length(BROWSER_CRYPTO_BUFFER_SIZE as u32); - Ok(RngSource::Browser(crypto, buf)) + let buf = Uint8Array::new_with_length(WEB_CRYPTO_BUFFER_SIZE as u32); + Ok(RngSource::Web(crypto, buf)) } // Taken from https://www.npmjs.com/package/browser-or-node @@ -93,29 +98,34 @@ fn is_node(global: &Global) -> bool { #[wasm_bindgen] extern "C" { - type Global; // Return type of js_sys::global() + // Return type of js_sys::global() + type Global; - // Web Crypto API (https://www.w3.org/TR/WebCryptoAPI/) - #[wasm_bindgen(method, getter, js_name = "msCrypto")] - fn ms_crypto(this: &Global) -> BrowserCrypto; + // Web Crypto API: Crypto interface (https://www.w3.org/TR/WebCryptoAPI/) + type WebCrypto; + // Getters for the WebCrypto API #[wasm_bindgen(method, getter)] - fn crypto(this: &Global) -> BrowserCrypto; - type BrowserCrypto; + fn crypto(this: &Global) -> WebCrypto; + #[wasm_bindgen(method, getter, js_name = msCrypto)] + fn ms_crypto(this: &Global) -> WebCrypto; + // Crypto.getRandomValues() #[wasm_bindgen(method, js_name = getRandomValues, catch)] - fn get_random_values(this: &BrowserCrypto, buf: &Uint8Array) -> Result<(), JsValue>; + 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: &mut [u8]) -> Result<(), JsValue>; // We use a "module" object here instead of just annotating require() with // js_name = "module.require", so that Webpack doesn't give a warning. See: // https://github.com/rust-random/getrandom/issues/224 - type NodeModule; + type Module; #[wasm_bindgen(js_name = module)] - static NODE_MODULE: NodeModule; - // Node JS crypto module (https://nodejs.org/api/crypto.html) + static MODULE: Module; #[wasm_bindgen(method, catch)] - fn require(this: &NodeModule, s: &str) -> Result; - type NodeCrypto; - #[wasm_bindgen(method, js_name = randomFillSync, catch)] - fn random_fill_sync(this: &NodeCrypto, buf: &mut [u8]) -> Result<(), JsValue>; + fn require(this: &Module, s: &str) -> Result; // Node JS process Object (https://nodejs.org/api/process.html) #[wasm_bindgen(method, getter)] From 0579fe3014fda4c96ff2f8ab5e92641dba29d30d Mon Sep 17 00:00:00 2001 From: Joe Richey Date: Tue, 13 Sep 2022 01:21:36 -0700 Subject: [PATCH 2/5] Update documentation and error messages This allows users to get an actionable error message about this particular problem. We also add detection for this problem. Signed-off-by: Joe Richey --- src/error.rs | 4 ++++ src/js.rs | 20 +++++++++++--------- src/lib.rs | 19 +++++++++++++++++-- 3 files changed, 32 insertions(+), 11 deletions(-) diff --git a/src/error.rs b/src/error.rs index c3e71be3..ef44e5a5 100644 --- a/src/error.rs +++ b/src/error.rs @@ -53,6 +53,9 @@ impl Error { pub const NODE_CRYPTO: Error = internal_error(12); /// Calling Node.js function `crypto.randomFillSync` failed. pub const NODE_RANDOM_FILL_SYNC: Error = internal_error(13); + /// Called from an ES module on Node.js. This is unsupported, see: + /// https://docs.rs/getrandom#nodejs-es6-module-support. + pub const NODE_ES_MODULE: Error = internal_error(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 @@ -170,6 +173,7 @@ fn internal_desc(error: Error) -> Option<&'static str> { Error::VXWORKS_RAND_SECURE => Some("randSecure: VxWorks RNG module is not initialized"), Error::NODE_CRYPTO => Some("Node.js crypto CommonJS module is unavailable"), Error::NODE_RANDOM_FILL_SYNC => Some("Calling Node.js API crypto.randomFillSync failed"), + Error::NODE_ES_MODULE => Some("Node.js ES modules are not directly supported, see https://docs.rs/getrandom#nodejs-es6-module-support"), _ => None, } } diff --git a/src/js.rs b/src/js.rs index 9ff69a58..979a28ba 100644 --- a/src/js.rs +++ b/src/js.rs @@ -10,7 +10,7 @@ use crate::Error; extern crate std; use std::thread_local; -use js_sys::{global, Uint8Array}; +use js_sys::{global, Function, Uint8Array}; use wasm_bindgen::{prelude::wasm_bindgen, JsCast, JsValue}; // Size of our temporary Uint8Array buffer used with WebCrypto methods @@ -68,10 +68,14 @@ fn getrandom_init() -> Result { c if c.is_object() => c, // Node.js CommonJS Crypto module _ if is_node(&global) => { - match MODULE.require("crypto") { - Ok(n) if n.is_object() => return Ok(RngSource::Node(n)), - _ => return Err(Error::NODE_CRYPTO), - }; + // If require isn't a valid function, we are in an ES module. + match Module::require_fn().dyn_into::() { + Ok(require_fn) => match require_fn.call1(&global, &JsValue::from_str("crypto")) { + Ok(n) => return Ok(RngSource::Node(n.unchecked_into())), + Err(_) => return Err(Error::NODE_CRYPTO), + }, + Err(_) => return Err(Error::NODE_ES_MODULE), + } } // IE 11 Workaround _ => match global.ms_crypto() { @@ -122,10 +126,8 @@ extern "C" { // js_name = "module.require", so that Webpack doesn't give a warning. See: // https://github.com/rust-random/getrandom/issues/224 type Module; - #[wasm_bindgen(js_name = module)] - static MODULE: Module; - #[wasm_bindgen(method, catch)] - fn require(this: &Module, s: &str) -> Result; + #[wasm_bindgen(getter, static_method_of = Module, js_class = module, js_name = require)] + fn require_fn() -> JsValue; // Node JS process Object (https://nodejs.org/api/process.html) #[wasm_bindgen(method, getter)] diff --git a/src/lib.rs b/src/lib.rs index d7f67722..91b0b7bb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -31,7 +31,7 @@ //! | Emscripten | `*‑emscripten` | `/dev/random` (identical to `/dev/urandom`) //! | WASI | `wasm32‑wasi` | [`random_get`] //! | Web Browser | `wasm32‑*‑unknown` | [`Crypto.getRandomValues`], see [WebAssembly support] -//! | Node.js | `wasm32‑*‑unknown` | [`crypto.randomBytes`], see [WebAssembly support] +//! | Node.js | `wasm32‑*‑unknown` | [`crypto.randomFillSync`], see [WebAssembly support] //! | SOLID | `*-kmc-solid_*` | `SOLID_RNG_SampleRandomBytes` //! | Nintendo 3DS | `armv6k-nintendo-3ds` | [`getrandom`][1] //! @@ -91,6 +91,18 @@ //! //! This feature has no effect on targets other than `wasm32-unknown-unknown`. //! +//! #### 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 implementations //! //! The [`register_custom_getrandom!`] macro allows a user to mark their own @@ -154,11 +166,14 @@ //! [`RDRAND`]: https://software.intel.com/en-us/articles/intel-digital-random-number-generator-drng-software-implementation-guide //! [`SecRandomCopyBytes`]: https://developer.apple.com/documentation/security/1399291-secrandomcopybytes?language=objc //! [`cprng_draw`]: https://fuchsia.dev/fuchsia-src/zircon/syscalls/cprng_draw -//! [`crypto.randomBytes`]: https://nodejs.org/api/crypto.html#crypto_crypto_randombytes_size_callback +//! [`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/main/phases/snapshot/docs.md#-random_getbuf-pointeru8-buf_len-size---errno //! [WebAssembly support]: #webassembly-support //! [`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 #![doc( html_logo_url = "https://www.rust-lang.org/logos/rust-logo-128x128-blk.png", From 0503000381d08046490f6f3afe141b5cd3b0bcd4 Mon Sep 17 00:00:00 2001 From: Joe Richey Date: Thu, 6 Oct 2022 14:51:49 -0700 Subject: [PATCH 3/5] Fix link typo Signed-off-by: Joe Richey --- src/error.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/error.rs b/src/error.rs index ef44e5a5..ab39a3c3 100644 --- a/src/error.rs +++ b/src/error.rs @@ -54,7 +54,7 @@ impl Error { /// Calling Node.js function `crypto.randomFillSync` failed. pub const NODE_RANDOM_FILL_SYNC: Error = internal_error(13); /// Called from an ES module on Node.js. This is unsupported, see: - /// https://docs.rs/getrandom#nodejs-es6-module-support. + /// . pub const NODE_ES_MODULE: Error = internal_error(14); /// Codes below this point represent OS Errors (i.e. positive i32 values). @@ -173,7 +173,7 @@ fn internal_desc(error: Error) -> Option<&'static str> { Error::VXWORKS_RAND_SECURE => Some("randSecure: VxWorks RNG module is not initialized"), Error::NODE_CRYPTO => Some("Node.js crypto CommonJS module is unavailable"), Error::NODE_RANDOM_FILL_SYNC => Some("Calling Node.js API crypto.randomFillSync failed"), - Error::NODE_ES_MODULE => Some("Node.js ES modules are not directly supported, see https://docs.rs/getrandom#nodejs-es6-module-support"), + Error::NODE_ES_MODULE => Some("Node.js ES modules are not directly supported, see https://docs.rs/getrandom#nodejs-es-module-support"), _ => None, } } From e0c93b10d5fa11ad9983e7da3d2ca973f5554c48 Mon Sep 17 00:00:00 2001 From: Joe Richey Date: Thu, 6 Oct 2022 14:52:07 -0700 Subject: [PATCH 4/5] Catch call to module.require This call throws an exception if module isn't defined. Signed-off-by: Joe Richey --- src/js.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/js.rs b/src/js.rs index 979a28ba..59b7c0fe 100644 --- a/src/js.rs +++ b/src/js.rs @@ -68,8 +68,8 @@ fn getrandom_init() -> Result { c if c.is_object() => c, // Node.js CommonJS Crypto module _ if is_node(&global) => { - // If require isn't a valid function, we are in an ES module. - match Module::require_fn().dyn_into::() { + // If module.require isn't a valid function, we are in an ES module. + match Module::require_fn().and_then(JsCast::dyn_into::) { Ok(require_fn) => match require_fn.call1(&global, &JsValue::from_str("crypto")) { Ok(n) => return Ok(RngSource::Node(n.unchecked_into())), Err(_) => return Err(Error::NODE_CRYPTO), @@ -126,8 +126,8 @@ extern "C" { // js_name = "module.require", so that Webpack doesn't give a warning. See: // https://github.com/rust-random/getrandom/issues/224 type Module; - #[wasm_bindgen(getter, static_method_of = Module, js_class = module, js_name = require)] - fn require_fn() -> JsValue; + #[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)] From 9962c706c913a0d3ec36b8c2f9d4c913f24f3313 Mon Sep 17 00:00:00 2001 From: Joe Richey Date: Thu, 6 Oct 2022 14:52:39 -0700 Subject: [PATCH 5/5] Update Module::require internal comments Signed-off-by: Joe Richey --- src/js.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/js.rs b/src/js.rs index 59b7c0fe..574c4dc3 100644 --- a/src/js.rs +++ b/src/js.rs @@ -122,9 +122,12 @@ extern "C" { #[wasm_bindgen(method, js_name = randomFillSync, catch)] fn random_fill_sync(this: &NodeCrypto, buf: &mut [u8]) -> Result<(), JsValue>; - // We use a "module" object here instead of just annotating require() with - // js_name = "module.require", so that Webpack doesn't give a warning. See: + // 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;