-
Notifications
You must be signed in to change notification settings - Fork 192
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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).
- Loading branch information
Showing
6 changed files
with
46 additions
and
169 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
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>; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters