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

Rework JS feature detection #284

Merged
merged 5 commits into from Oct 6, 2022
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
18 changes: 11 additions & 7 deletions src/error.rs
Expand Up @@ -43,16 +43,19 @@ 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);
/// 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
Expand Down Expand Up @@ -166,10 +169,11 @@ 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"),
Error::NODE_ES_MODULE => Some("Node.js ES modules are not directly supported, see https://docs.rs/getrandom#nodejs-es6-module-support"),
_ => None,
}
}
Expand Down
84 changes: 48 additions & 36 deletions src/js.rs
Expand Up @@ -10,15 +10,16 @@ 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
// 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.
Expand All @@ -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);
Expand All @@ -58,25 +59,33 @@ pub(crate) fn getrandom_inner(dest: &mut [u8]) -> Result<(), Error> {

fn getrandom_init() -> Result<RngSource, Error> {
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) => {
// If require isn't a valid function, we are in an ES module.
match Module::require_fn().dyn_into::<Function>() {
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() {
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
Expand All @@ -93,30 +102,33 @@ 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>;

// 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;
#[wasm_bindgen(js_name = module)]
static NODE_MODULE: NodeModule;
// Node JS crypto module (https://nodejs.org/api/crypto.html)
#[wasm_bindgen(method, catch)]
fn require(this: &NodeModule, s: &str) -> Result<NodeCrypto, JsValue>;
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 Module;
#[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)]
fn process(this: &Global) -> Process;
Expand Down
19 changes: 17 additions & 2 deletions src/lib.rs
Expand Up @@ -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]
//!
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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",
Expand Down