From 51303b5bb826c08e50f79e6b34ec16bfc36738f6 Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Mon, 25 Feb 2019 12:54:46 +0000 Subject: [PATCH] Implement on WASM via stdweb and bindgen; add tests --- .gitignore | 3 ++ Cargo.toml | 5 ++ src/lib.rs | 68 ++++++++++++++++++++++- src/wasm32_bindgen.rs | 83 ++++++++++++++++++++++++++++ src/wasm32_stdweb.rs | 98 ++++++++++++++++++++++++++++++++++ tests/wasm_bindgen/Cargo.toml | 15 ++++++ tests/wasm_bindgen/js/index.js | 5 ++ tests/wasm_bindgen/src/lib.rs | 55 +++++++++++++++++++ 8 files changed, 331 insertions(+), 1 deletion(-) create mode 100644 tests/wasm_bindgen/Cargo.toml create mode 100644 tests/wasm_bindgen/js/index.js create mode 100644 tests/wasm_bindgen/src/lib.rs diff --git a/.gitignore b/.gitignore index 6936990..435eeee 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ /target **/*.rs.bk Cargo.lock +*.ts +*.js +*.wasm diff --git a/Cargo.toml b/Cargo.toml index 302ea63..8dbc8a2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,11 @@ description = "A small cross-platform library to securely get random data (entro travis-ci = { repository = "rust-random/getrandom" } appveyor = { repository = "rust-random/getrandom" } +[workspace] +members = [ + "tests/wasm_bindgen", +] + [target.'cfg(unix)'.dependencies] libc = "0.2" diff --git a/src/lib.rs b/src/lib.rs index 69c90fe..b697744 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -97,9 +97,20 @@ #![no_std] +#![cfg_attr(feature = "stdweb", recursion_limit="128")] + #[cfg(not(target_env = "sgx"))] #[macro_use] extern crate std; +// We have to do it here because we load macros +#[cfg(all(target_arch = "wasm32", not(target_os = "emscripten"), + feature = "wasm-bindgen"))] +extern crate wasm_bindgen; +#[cfg(all(target_arch = "wasm32", not(target_os = "emscripten"), + not(feature = "wasm-bindgen"), + feature = "stdweb"))] +#[macro_use] extern crate stdweb; + #[cfg(any( target_os = "android", target_os = "netbsd", @@ -108,8 +119,8 @@ target_os = "redox", target_os = "dragonfly", target_os = "haiku", - target_os = "emscripten", target_os = "linux", + target_arch = "wasm32", ))] mod utils; mod error; @@ -213,3 +224,58 @@ mod_use!( pub fn getrandom(dest: &mut [u8]) -> Result<(), Error> { getrandom_inner(dest) } + +// Due to rustwasm/wasm-bindgen#201 this can't be defined in the inner os +// modules, so hack around it for now and place it at the root. +#[cfg(all(feature = "wasm-bindgen", target_arch = "wasm32"))] +#[doc(hidden)] +#[allow(missing_debug_implementations)] +pub mod __wbg_shims { + + // `extern { type Foo; }` isn't supported on 1.22 syntactically, so use a + // macro to work around that. + macro_rules! rust_122_compat { + ($($t:tt)*) => ($($t)*) + } + + rust_122_compat! { + extern crate wasm_bindgen; + + pub use wasm_bindgen::prelude::*; + + #[wasm_bindgen] + extern "C" { + pub type Function; + #[wasm_bindgen(constructor)] + pub fn new(s: &str) -> Function; + #[wasm_bindgen(method)] + pub fn call(this: &Function, self_: &JsValue) -> JsValue; + + pub type This; + #[wasm_bindgen(method, getter, structural, js_name = self)] + pub fn self_(me: &This) -> JsValue; + #[wasm_bindgen(method, getter, structural)] + pub fn crypto(me: &This) -> JsValue; + + #[derive(Clone, Debug)] + pub type BrowserCrypto; + + // TODO: these `structural` annotations here ideally wouldn't be here to + // avoid a JS shim, but for now with feature detection they're + // unavoidable. + #[wasm_bindgen(method, js_name = getRandomValues, structural, getter)] + pub fn get_random_values_fn(me: &BrowserCrypto) -> JsValue; + #[wasm_bindgen(method, js_name = getRandomValues, structural)] + pub fn get_random_values(me: &BrowserCrypto, buf: &mut [u8]); + + #[wasm_bindgen(js_name = require)] + pub fn node_require(s: &str) -> NodeCrypto; + + #[derive(Clone, Debug)] + pub type NodeCrypto; + + #[wasm_bindgen(method, js_name = randomFillSync, structural)] + pub fn random_fill_sync(me: &NodeCrypto, buf: &mut [u8]); + } + } +} diff --git a/src/wasm32_bindgen.rs b/src/wasm32_bindgen.rs index 6de8f04..3f04d32 100644 --- a/src/wasm32_bindgen.rs +++ b/src/wasm32_bindgen.rs @@ -7,3 +7,86 @@ // except according to those terms. //! Implementation for WASM via wasm-bindgen + +use std::cell::RefCell; +use std::mem; + +use wasm_bindgen::prelude::*; + +use super::__wbg_shims::*; +use super::{Error, UNAVAILABLE_ERROR}; +use super::utils::use_init; + + +#[derive(Clone, Debug)] +pub enum RngSource { + Node(NodeCrypto), + Browser(BrowserCrypto), +} + +thread_local!( + static RNG_SOURCE: RefCell> = RefCell::new(None); +); + +pub fn getrandom_inner(dest: &mut [u8]) -> Result<(), Error> { + assert_eq!(mem::size_of::(), 4); + + RNG_SOURCE.with(|f| { + use_init(f, getrandom_init, |source| { + match *source { + RngSource::Node(ref n) => n.random_fill_sync(dest), + RngSource::Browser(ref n) => { + // see https://developer.mozilla.org/en-US/docs/Web/API/Crypto/getRandomValues + // + // where it says: + // + // > A QuotaExceededError DOMException is thrown if the + // > requested length is greater than 65536 bytes. + for chunk in dest.chunks_mut(65536) { + n.get_random_values(chunk) + } + } + } + Ok(()) + }) + }) + +} + +fn getrandom_init() -> Result { + // First up we need to detect if we're running in node.js or a + // browser. To do this we get ahold of the `this` object (in a bit + // of a roundabout fashion). + // + // Once we have `this` we look at its `self` property, which is + // only defined on the web (either a main window or web worker). + let this = Function::new("return this").call(&JsValue::undefined()); + assert!(this != JsValue::undefined()); + let this = This::from(this); + let is_browser = this.self_() != JsValue::undefined(); + + if !is_browser { + return Ok(RngSource::Node(node_require("crypto"))) + } + + // If `self` is defined then we're in a browser somehow (main window + // or web worker). Here we want to try to use + // `crypto.getRandomValues`, but if `crypto` isn't defined we assume + // we're in an older web browser and the OS RNG isn't available. + let crypto = this.crypto(); + if crypto.is_undefined() { + let msg = "self.crypto is undefined"; + return Err(UNAVAILABLE_ERROR) // TODO: report msg + } + + // Test if `crypto.getRandomValues` is undefined as well + let crypto: BrowserCrypto = crypto.into(); + if crypto.get_random_values_fn().is_undefined() { + let msg = "crypto.getRandomValues is undefined"; + return Err(UNAVAILABLE_ERROR) // TODO: report msg + } + + // Ok! `self.crypto.getRandomValues` is a defined value, so let's + // assume we can do browser crypto. + Ok(RngSource::Browser(crypto)) +} diff --git a/src/wasm32_stdweb.rs b/src/wasm32_stdweb.rs index 7db8afe..510ce57 100644 --- a/src/wasm32_stdweb.rs +++ b/src/wasm32_stdweb.rs @@ -7,3 +7,101 @@ // except according to those terms. //! Implementation for WASM via stdweb + +use std::cell::RefCell; +use std::mem; + +use stdweb::unstable::TryInto; +use stdweb::web::error::Error as WebError; + +use super::{Error, UNAVAILABLE_ERROR, UNKNOWN_ERROR}; +use super::utils::use_init; + +#[derive(Clone, Debug)] +enum RngSource { + Browser, + Node +} + +thread_local!( + static RNG_SOURCE: RefCell> = RefCell::new(None); +); + +pub fn getrandom_inner(mut dest: &mut [u8]) -> Result<(), Error> { + assert_eq!(mem::size_of::(), 4); + + RNG_SOURCE.with(|f| { + use_init(f, getrandom_init, |source| getrandom_fill(source, dest)) + }) + +} + +fn getrandom_init() -> Result { + let result = js! { + try { + if ( + typeof self === "object" && + typeof self.crypto === "object" && + typeof self.crypto.getRandomValues === "function" + ) { + return { success: true, ty: 1 }; + } + + if (typeof require("crypto").randomBytes === "function") { + return { success: true, ty: 2 }; + } + + return { success: false, error: new Error("not supported") }; + } catch(err) { + return { success: false, error: err }; + } + }; + + if js!{ return @{ result.as_ref() }.success } == true { + let ty = js!{ return @{ result }.ty }; + + if ty == 1 { Ok(RngSource::Browser) } + else if ty == 2 { Ok(RngSource::Node) } + else { unreachable!() } + } else { + let err: WebError = js!{ return @{ result }.error }.try_into().unwrap(); + Err(UNAVAILABLE_ERROR) // TODO: forward err + } +} + +fn getrandom_fill(source: &mut RngSource, mut dest: &mut [u8]) -> Result<(), Error> { + for chunk in dest.chunks_mut(65536) { + let len = chunk.len() as u32; + let ptr = chunk.as_mut_ptr() as i32; + + let result = match source { + RngSource::Browser => js! { + try { + let array = new Uint8Array(@{ len }); + self.crypto.getRandomValues(array); + HEAPU8.set(array, @{ ptr }); + + return { success: true }; + } catch(err) { + return { success: false, error: err }; + } + }, + RngSource::Node => js! { + try { + let bytes = require("crypto").randomBytes(@{ len }); + HEAPU8.set(new Uint8Array(bytes), @{ ptr }); + + return { success: true }; + } catch(err) { + return { success: false, error: err }; + } + } + }; + + if js!{ return @{ result.as_ref() }.success } != true { + let err: WebError = js!{ return @{ result }.error }.try_into().unwrap(); + return Err(UNKNOWN_ERROR) // TODO: forward err + } + } + Ok(()) +} diff --git a/tests/wasm_bindgen/Cargo.toml b/tests/wasm_bindgen/Cargo.toml new file mode 100644 index 0000000..4b735c8 --- /dev/null +++ b/tests/wasm_bindgen/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "getrandom_wasm_bindgen_test" +description = "Minimal test crate for getrandom using wasm-bindgen" +version = "0.1.0" +authors = ["The Rand Project Developers"] +publish = false +license = "MIT/Apache-2.0" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +getrandom = { path = "../..", features = ["wasm-bindgen"] } +wasm-bindgen = "0.2" +wasm-bindgen-test = "0.2" diff --git a/tests/wasm_bindgen/js/index.js b/tests/wasm_bindgen/js/index.js new file mode 100644 index 0000000..d3390f6 --- /dev/null +++ b/tests/wasm_bindgen/js/index.js @@ -0,0 +1,5 @@ +'use strict'; + +const getrandom_wasm_bindgen_test = require('./getrandom_wasm_bindgen_test'); + +console.log(getrandom_wasm_bindgen_test.test_gen()); diff --git a/tests/wasm_bindgen/src/lib.rs b/tests/wasm_bindgen/src/lib.rs new file mode 100644 index 0000000..2da6127 --- /dev/null +++ b/tests/wasm_bindgen/src/lib.rs @@ -0,0 +1,55 @@ +// Copyright 2018 Developers of the Rand project. +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +// Crate to test WASM with the `wasm-bindgen` lib. + +#![doc(html_logo_url = "https://www.rust-lang.org/logos/rust-logo-128x128-blk.png")] + +extern crate getrandom; +extern crate wasm_bindgen; +extern crate wasm_bindgen_test; + +use std::slice; +use wasm_bindgen_test::*; +use wasm_bindgen::prelude::*; + +use getrandom::getrandom; + +#[wasm_bindgen] +pub fn test_gen() -> i32 { + let mut int: i32 = 0; + unsafe { + let ptr = &mut int as *mut i32 as *mut u8; + let slice = slice::from_raw_parts_mut(ptr, 4); + getrandom(slice).unwrap(); + } + int +} + +#[wasm_bindgen_test] +fn test_call() { + let mut buf = [0u8; 0]; + getrandom(&mut buf).unwrap(); +} + +#[wasm_bindgen_test] +fn test_diff() { + let mut v1 = [0u8; 1000]; + getrandom(&mut v1).unwrap(); + + let mut v2 = [0u8; 1000]; + getrandom(&mut v2).unwrap(); + + let mut n_diff_bits = 0; + for i in 0..v1.len() { + n_diff_bits += (v1[i] ^ v2[i]).count_ones(); + } + + // Check at least 1 bit per byte differs. p(failure) < 1e-1000 with random input. + assert!(n_diff_bits >= v1.len() as u32); +}