Skip to content
48 changes: 34 additions & 14 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -229,11 +229,31 @@ jobs:
# run: cargo test

web:
name: Web
name: ${{ matrix.rust.description }}
runs-on: ubuntu-24.04
strategy:
fail-fast: false
matrix:
rust:
- {
description: Web,
version: stable,
flags: -Dwarnings --cfg getrandom_backend="wasm_js",
args: --features=std,
}
- {
description: Web with Atomics,
version: nightly,
components: rust-src,
flags: '-Dwarnings --cfg getrandom_backend="wasm_js" -Ctarget-feature=+atomics,+bulk-memory',
args: '--features=std -Zbuild-std=panic_abort,std',
}
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: dtolnay/rust-toolchain@master
with:
toolchain: ${{ matrix.rust.version }}
components: ${{ matrix.rust.components }}
- name: Install precompiled wasm-pack
shell: bash
run: |
Expand All @@ -244,34 +264,34 @@ jobs:
- uses: Swatinem/rust-cache@v2
- name: Test (Node)
env:
RUSTFLAGS: -Dwarnings --cfg getrandom_backend="wasm_js"
run: wasm-pack test --node -- --features std
RUSTFLAGS: ${{ matrix.rust.flags }}
run: wasm-pack test --node -- ${{ matrix.rust.args }}
- name: Test (Firefox)
env:
WASM_BINDGEN_USE_BROWSER: 1
RUSTFLAGS: -Dwarnings --cfg getrandom_backend="wasm_js"
run: wasm-pack test --headless --firefox -- --features std
RUSTFLAGS: ${{ matrix.rust.flags }}
run: wasm-pack test --headless --firefox -- ${{ matrix.rust.args }}
- name: Test (Chrome)
env:
WASM_BINDGEN_USE_BROWSER: 1
RUSTFLAGS: -Dwarnings --cfg getrandom_backend="wasm_js"
run: wasm-pack test --headless --chrome -- --features std
RUSTFLAGS: ${{ matrix.rust.flags }}
run: wasm-pack test --headless --chrome -- ${{ matrix.rust.args }}
- name: Test (dedicated worker)
env:
WASM_BINDGEN_USE_DEDICATED_WORKER: 1
RUSTFLAGS: -Dwarnings --cfg getrandom_backend="wasm_js"
run: wasm-pack test --headless --firefox -- --features std
RUSTFLAGS: ${{ matrix.rust.flags }}
run: wasm-pack test --headless --firefox -- ${{ matrix.rust.args }}
- name: Test (shared worker)
env:
WASM_BINDGEN_USE_SHARED_WORKER: 1
RUSTFLAGS: -Dwarnings --cfg getrandom_backend="wasm_js"
run: wasm-pack test --headless --firefox -- --features std
RUSTFLAGS: ${{ matrix.rust.flags }}
run: wasm-pack test --headless --firefox -- ${{ matrix.rust.args }}
- name: Test (service worker)
env:
WASM_BINDGEN_USE_SERVICE_WORKER: 1
RUSTFLAGS: -Dwarnings --cfg getrandom_backend="wasm_js"
RUSTFLAGS: ${{ matrix.rust.flags }}
# Firefox doesn't support module service workers and therefor can't import scripts
run: wasm-pack test --headless --chrome -- --features std
run: wasm-pack test --headless --chrome -- ${{ matrix.rust.args }}

wasi:
name: WASI
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/workspace.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ jobs:
env:
RUSTFLAGS: -Dwarnings --cfg getrandom_backend="wasm_js"
run: cargo clippy -Zbuild-std --target wasm32-unknown-unknown
- name: Web WASM with atomics (wasm_js.rs)
env:
RUSTFLAGS: -Dwarnings --cfg getrandom_backend="wasm_js" -Ctarget-feature=+atomics,+bulk-memory
run: cargo clippy -Zbuild-std --target wasm32-unknown-unknown
- name: Linux (linux_android.rs)
env:
RUSTFLAGS: -Dwarnings --cfg getrandom_backend="linux_getrandom"
Expand Down
5 changes: 3 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,9 @@ windows-targets = "0.52"

# wasm_js
[target.'cfg(all(getrandom_backend = "wasm_js", target_arch = "wasm32", any(target_os = "unknown", target_os = "none")))'.dependencies]
wasm-bindgen = { version = "0.2.96", default-features = false }
js-sys = { version = "0.3.73", default-features = false }
wasm-bindgen = { version = "0.2.98", default-features = false }
[target.'cfg(all(getrandom_backend = "wasm_js", target_arch = "wasm32", any(target_os = "unknown", target_os = "none"), target_feature = "atomics"))'.dependencies]
js-sys = { version = "0.3.75", default-features = false }
[target.'cfg(all(getrandom_backend = "wasm_js", target_arch = "wasm32", any(target_os = "unknown", target_os = "none")))'.dev-dependencies]
wasm-bindgen-test = "0.3"

Expand Down
60 changes: 34 additions & 26 deletions src/backends/wasm_js.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,35 +7,46 @@ pub use crate::util::{inner_u32, inner_u64};
#[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, Uint8Array};
use wasm_bindgen::{prelude::wasm_bindgen, JsCast, JsValue};
use wasm_bindgen::{prelude::wasm_bindgen, 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 CRYPTO_BUFFER_SIZE: u16 = 256;
// Maximum buffer size allowed in `Crypto.getRandomValuesSize` is 65536 bytes.
// See https://developer.mozilla.org/en-US/docs/Web/API/Crypto/getRandomValues
const MAX_BUFFER_SIZE: usize = 65536;

#[cfg(not(target_feature = "atomics"))]
pub fn fill_inner(dest: &mut [MaybeUninit<u8>]) -> Result<(), Error> {
let global: Global = global().unchecked_into();
let crypto = global.crypto();

if !crypto.is_object() {
return Err(Error::WEB_CRYPTO);
for chunk in dest.chunks_mut(MAX_BUFFER_SIZE) {
if get_random_values(chunk).is_err() {
return Err(Error::WEB_CRYPTO);
}
}
Ok(())
}

#[cfg(target_feature = "atomics")]
pub fn fill_inner(dest: &mut [MaybeUninit<u8>]) -> Result<(), Error> {
// 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
let buf_len = usize::min(dest.len(), MAX_BUFFER_SIZE);
let buf_len_u32 = buf_len
.try_into()
.expect("buffer length is bounded by MAX_BUFFER_SIZE");
let buf = js_sys::Uint8Array::new_with_length(buf_len_u32);
for chunk in dest.chunks_mut(buf_len) {
let chunk_len = chunk
.len()
.try_into()
.expect("chunk length is bounded by CRYPTO_BUFFER_SIZE");
.expect("chunk length is bounded by MAX_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);
let sub_buf = if chunk_len == buf_len_u32 {
&buf
} else {
&buf.subarray(0, chunk_len)
};

if crypto.get_random_values(&sub_buf).is_err() {
return Err(Error::WEB_GET_RANDOM_VALUES);
if get_random_values(sub_buf).is_err() {
return Err(Error::WEB_CRYPTO);
}

// SAFETY: `sub_buf`'s length is the same length as `chunk`
Expand All @@ -46,14 +57,11 @@ pub fn fill_inner(dest: &mut [MaybeUninit<u8>]) -> Result<(), Error> {

#[wasm_bindgen]
extern "C" {
// Return type of js_sys::global()
type Global;
// Web Crypto API: Crypto interface (https://www.w3.org/TR/WebCryptoAPI/)
type Crypto;
// Getters for the Crypto API
#[wasm_bindgen(method, getter)]
fn crypto(this: &Global) -> Crypto;
// Crypto.getRandomValues()
#[wasm_bindgen(method, js_name = getRandomValues, catch)]
fn get_random_values(this: &Crypto, buf: &Uint8Array) -> Result<(), JsValue>;
#[cfg(not(target_feature = "atomics"))]
#[wasm_bindgen(js_namespace = ["globalThis", "crypto"], js_name = getRandomValues, catch)]
fn get_random_values(buf: &mut [MaybeUninit<u8>]) -> Result<(), JsValue>;
#[cfg(target_feature = "atomics")]
#[wasm_bindgen(js_namespace = ["globalThis", "crypto"], js_name = getRandomValues, catch)]
fn get_random_values(buf: &js_sys::Uint8Array) -> Result<(), JsValue>;
}
3 changes: 0 additions & 3 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,6 @@ impl Error {
pub const NO_RDRAND: Error = Self::new_internal(6);
/// The environment does not support the Web Crypto API.
pub const WEB_CRYPTO: Error = Self::new_internal(7);
/// Calling Web Crypto API `crypto.getRandomValues` failed.
pub const WEB_GET_RANDOM_VALUES: Error = Self::new_internal(8);
/// On VxWorks, call to `randSecure` failed (random number generator is not yet initialized).
pub const VXWORKS_RAND_SECURE: Error = Self::new_internal(11);
/// Calling Windows ProcessPrng failed.
Expand Down Expand Up @@ -155,7 +153,6 @@ fn internal_desc(error: Error) -> Option<&'static str> {
Error::FAILED_RDRAND => "RDRAND: failed multiple times: CPU issue likely",
Error::NO_RDRAND => "RDRAND: instruction not supported",
Error::WEB_CRYPTO => "Web Crypto API is unavailable",
Error::WEB_GET_RANDOM_VALUES => "Calling Web API crypto.getRandomValues failed",
Error::VXWORKS_RAND_SECURE => "randSecure: VxWorks RNG module is not initialized",
Error::WINDOWS_PROCESS_PRNG => "ProcessPrng: Windows system function failure",
Error::RNDR_FAILURE => "RNDR: Could not generate a random number",
Expand Down