Skip to content

Commit

Permalink
Rework JS feature detection
Browse files Browse the repository at this point in the history
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 #256 (comment)

Signed-off-by: Joe Richey <joerichey@google.com>
  • Loading branch information
josephlr committed Sep 13, 2022
1 parent d3aa089 commit 3d8c7af
Show file tree
Hide file tree
Showing 2 changed files with 48 additions and 42 deletions.
14 changes: 7 additions & 7 deletions src/error.rs
Expand Up @@ -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).
Expand Down Expand Up @@ -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,
}
}
Expand Down
76 changes: 41 additions & 35 deletions src/js.rs
Expand Up @@ -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.
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,29 @@ 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) => {
match global.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
Expand All @@ -93,27 +98,28 @@ 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;
// We call globalThis.require() instead of just calling require() so that
// Webpack doesn't give a warning. See:
// https://github.com/rust-random/getrandom/issues/224
#[wasm_bindgen(method, catch)]
fn require(this: &Global, s: &str) -> Result<NodeCrypto, JsValue>;
// crypto.randomFillSync()
#[wasm_bindgen(method, js_name = randomFillSync, catch)]
fn random_fill_sync(this: &NodeCrypto, buf: &mut [u8]) -> Result<(), JsValue>;

Expand Down

0 comments on commit 3d8c7af

Please sign in to comment.