Skip to content

Commit 51303b5

Browse files
committed
Implement on WASM via stdweb and bindgen; add tests
1 parent 5607368 commit 51303b5

File tree

8 files changed

+331
-1
lines changed

8 files changed

+331
-1
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
11
/target
22
**/*.rs.bk
33
Cargo.lock
4+
*.ts
5+
*.js
6+
*.wasm

Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ description = "A small cross-platform library to securely get random data (entro
99
travis-ci = { repository = "rust-random/getrandom" }
1010
appveyor = { repository = "rust-random/getrandom" }
1111

12+
[workspace]
13+
members = [
14+
"tests/wasm_bindgen",
15+
]
16+
1217
[target.'cfg(unix)'.dependencies]
1318
libc = "0.2"
1419

src/lib.rs

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,9 +97,20 @@
9797

9898
#![no_std]
9999

100+
#![cfg_attr(feature = "stdweb", recursion_limit="128")]
101+
100102
#[cfg(not(target_env = "sgx"))]
101103
#[macro_use] extern crate std;
102104

105+
// We have to do it here because we load macros
106+
#[cfg(all(target_arch = "wasm32", not(target_os = "emscripten"),
107+
feature = "wasm-bindgen"))]
108+
extern crate wasm_bindgen;
109+
#[cfg(all(target_arch = "wasm32", not(target_os = "emscripten"),
110+
not(feature = "wasm-bindgen"),
111+
feature = "stdweb"))]
112+
#[macro_use] extern crate stdweb;
113+
103114
#[cfg(any(
104115
target_os = "android",
105116
target_os = "netbsd",
@@ -108,8 +119,8 @@
108119
target_os = "redox",
109120
target_os = "dragonfly",
110121
target_os = "haiku",
111-
target_os = "emscripten",
112122
target_os = "linux",
123+
target_arch = "wasm32",
113124
))]
114125
mod utils;
115126
mod error;
@@ -213,3 +224,58 @@ mod_use!(
213224
pub fn getrandom(dest: &mut [u8]) -> Result<(), Error> {
214225
getrandom_inner(dest)
215226
}
227+
228+
// Due to rustwasm/wasm-bindgen#201 this can't be defined in the inner os
229+
// modules, so hack around it for now and place it at the root.
230+
#[cfg(all(feature = "wasm-bindgen", target_arch = "wasm32"))]
231+
#[doc(hidden)]
232+
#[allow(missing_debug_implementations)]
233+
pub mod __wbg_shims {
234+
235+
// `extern { type Foo; }` isn't supported on 1.22 syntactically, so use a
236+
// macro to work around that.
237+
macro_rules! rust_122_compat {
238+
($($t:tt)*) => ($($t)*)
239+
}
240+
241+
rust_122_compat! {
242+
extern crate wasm_bindgen;
243+
244+
pub use wasm_bindgen::prelude::*;
245+
246+
#[wasm_bindgen]
247+
extern "C" {
248+
pub type Function;
249+
#[wasm_bindgen(constructor)]
250+
pub fn new(s: &str) -> Function;
251+
#[wasm_bindgen(method)]
252+
pub fn call(this: &Function, self_: &JsValue) -> JsValue;
253+
254+
pub type This;
255+
#[wasm_bindgen(method, getter, structural, js_name = self)]
256+
pub fn self_(me: &This) -> JsValue;
257+
#[wasm_bindgen(method, getter, structural)]
258+
pub fn crypto(me: &This) -> JsValue;
259+
260+
#[derive(Clone, Debug)]
261+
pub type BrowserCrypto;
262+
263+
// TODO: these `structural` annotations here ideally wouldn't be here to
264+
// avoid a JS shim, but for now with feature detection they're
265+
// unavoidable.
266+
#[wasm_bindgen(method, js_name = getRandomValues, structural, getter)]
267+
pub fn get_random_values_fn(me: &BrowserCrypto) -> JsValue;
268+
#[wasm_bindgen(method, js_name = getRandomValues, structural)]
269+
pub fn get_random_values(me: &BrowserCrypto, buf: &mut [u8]);
270+
271+
#[wasm_bindgen(js_name = require)]
272+
pub fn node_require(s: &str) -> NodeCrypto;
273+
274+
#[derive(Clone, Debug)]
275+
pub type NodeCrypto;
276+
277+
#[wasm_bindgen(method, js_name = randomFillSync, structural)]
278+
pub fn random_fill_sync(me: &NodeCrypto, buf: &mut [u8]);
279+
}
280+
}
281+
}

src/wasm32_bindgen.rs

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,86 @@
77
// except according to those terms.
88

99
//! Implementation for WASM via wasm-bindgen
10+
11+
use std::cell::RefCell;
12+
use std::mem;
13+
14+
use wasm_bindgen::prelude::*;
15+
16+
use super::__wbg_shims::*;
17+
use super::{Error, UNAVAILABLE_ERROR};
18+
use super::utils::use_init;
19+
20+
21+
#[derive(Clone, Debug)]
22+
pub enum RngSource {
23+
Node(NodeCrypto),
24+
Browser(BrowserCrypto),
25+
}
26+
27+
thread_local!(
28+
static RNG_SOURCE: RefCell<Option<RngSource>> = RefCell::new(None);
29+
);
30+
31+
pub fn getrandom_inner(dest: &mut [u8]) -> Result<(), Error> {
32+
assert_eq!(mem::size_of::<usize>(), 4);
33+
34+
RNG_SOURCE.with(|f| {
35+
use_init(f, getrandom_init, |source| {
36+
match *source {
37+
RngSource::Node(ref n) => n.random_fill_sync(dest),
38+
RngSource::Browser(ref n) => {
39+
// see https://developer.mozilla.org/en-US/docs/Web/API/Crypto/getRandomValues
40+
//
41+
// where it says:
42+
//
43+
// > A QuotaExceededError DOMException is thrown if the
44+
// > requested length is greater than 65536 bytes.
45+
for chunk in dest.chunks_mut(65536) {
46+
n.get_random_values(chunk)
47+
}
48+
}
49+
}
50+
Ok(())
51+
})
52+
})
53+
54+
}
55+
56+
fn getrandom_init() -> Result<RngSource, Error> {
57+
// First up we need to detect if we're running in node.js or a
58+
// browser. To do this we get ahold of the `this` object (in a bit
59+
// of a roundabout fashion).
60+
//
61+
// Once we have `this` we look at its `self` property, which is
62+
// only defined on the web (either a main window or web worker).
63+
let this = Function::new("return this").call(&JsValue::undefined());
64+
assert!(this != JsValue::undefined());
65+
let this = This::from(this);
66+
let is_browser = this.self_() != JsValue::undefined();
67+
68+
if !is_browser {
69+
return Ok(RngSource::Node(node_require("crypto")))
70+
}
71+
72+
// If `self` is defined then we're in a browser somehow (main window
73+
// or web worker). Here we want to try to use
74+
// `crypto.getRandomValues`, but if `crypto` isn't defined we assume
75+
// we're in an older web browser and the OS RNG isn't available.
76+
let crypto = this.crypto();
77+
if crypto.is_undefined() {
78+
let msg = "self.crypto is undefined";
79+
return Err(UNAVAILABLE_ERROR) // TODO: report msg
80+
}
81+
82+
// Test if `crypto.getRandomValues` is undefined as well
83+
let crypto: BrowserCrypto = crypto.into();
84+
if crypto.get_random_values_fn().is_undefined() {
85+
let msg = "crypto.getRandomValues is undefined";
86+
return Err(UNAVAILABLE_ERROR) // TODO: report msg
87+
}
88+
89+
// Ok! `self.crypto.getRandomValues` is a defined value, so let's
90+
// assume we can do browser crypto.
91+
Ok(RngSource::Browser(crypto))
92+
}

src/wasm32_stdweb.rs

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,101 @@
77
// except according to those terms.
88

99
//! Implementation for WASM via stdweb
10+
11+
use std::cell::RefCell;
12+
use std::mem;
13+
14+
use stdweb::unstable::TryInto;
15+
use stdweb::web::error::Error as WebError;
16+
17+
use super::{Error, UNAVAILABLE_ERROR, UNKNOWN_ERROR};
18+
use super::utils::use_init;
19+
20+
#[derive(Clone, Debug)]
21+
enum RngSource {
22+
Browser,
23+
Node
24+
}
25+
26+
thread_local!(
27+
static RNG_SOURCE: RefCell<Option<RngSource>> = RefCell::new(None);
28+
);
29+
30+
pub fn getrandom_inner(mut dest: &mut [u8]) -> Result<(), Error> {
31+
assert_eq!(mem::size_of::<usize>(), 4);
32+
33+
RNG_SOURCE.with(|f| {
34+
use_init(f, getrandom_init, |source| getrandom_fill(source, dest))
35+
})
36+
37+
}
38+
39+
fn getrandom_init() -> Result<RngSource, Error> {
40+
let result = js! {
41+
try {
42+
if (
43+
typeof self === "object" &&
44+
typeof self.crypto === "object" &&
45+
typeof self.crypto.getRandomValues === "function"
46+
) {
47+
return { success: true, ty: 1 };
48+
}
49+
50+
if (typeof require("crypto").randomBytes === "function") {
51+
return { success: true, ty: 2 };
52+
}
53+
54+
return { success: false, error: new Error("not supported") };
55+
} catch(err) {
56+
return { success: false, error: err };
57+
}
58+
};
59+
60+
if js!{ return @{ result.as_ref() }.success } == true {
61+
let ty = js!{ return @{ result }.ty };
62+
63+
if ty == 1 { Ok(RngSource::Browser) }
64+
else if ty == 2 { Ok(RngSource::Node) }
65+
else { unreachable!() }
66+
} else {
67+
let err: WebError = js!{ return @{ result }.error }.try_into().unwrap();
68+
Err(UNAVAILABLE_ERROR) // TODO: forward err
69+
}
70+
}
71+
72+
fn getrandom_fill(source: &mut RngSource, mut dest: &mut [u8]) -> Result<(), Error> {
73+
for chunk in dest.chunks_mut(65536) {
74+
let len = chunk.len() as u32;
75+
let ptr = chunk.as_mut_ptr() as i32;
76+
77+
let result = match source {
78+
RngSource::Browser => js! {
79+
try {
80+
let array = new Uint8Array(@{ len });
81+
self.crypto.getRandomValues(array);
82+
HEAPU8.set(array, @{ ptr });
83+
84+
return { success: true };
85+
} catch(err) {
86+
return { success: false, error: err };
87+
}
88+
},
89+
RngSource::Node => js! {
90+
try {
91+
let bytes = require("crypto").randomBytes(@{ len });
92+
HEAPU8.set(new Uint8Array(bytes), @{ ptr });
93+
94+
return { success: true };
95+
} catch(err) {
96+
return { success: false, error: err };
97+
}
98+
}
99+
};
100+
101+
if js!{ return @{ result.as_ref() }.success } != true {
102+
let err: WebError = js!{ return @{ result }.error }.try_into().unwrap();
103+
return Err(UNKNOWN_ERROR) // TODO: forward err
104+
}
105+
}
106+
Ok(())
107+
}

tests/wasm_bindgen/Cargo.toml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
[package]
2+
name = "getrandom_wasm_bindgen_test"
3+
description = "Minimal test crate for getrandom using wasm-bindgen"
4+
version = "0.1.0"
5+
authors = ["The Rand Project Developers"]
6+
publish = false
7+
license = "MIT/Apache-2.0"
8+
9+
[lib]
10+
crate-type = ["cdylib"]
11+
12+
[dependencies]
13+
getrandom = { path = "../..", features = ["wasm-bindgen"] }
14+
wasm-bindgen = "0.2"
15+
wasm-bindgen-test = "0.2"

tests/wasm_bindgen/js/index.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
'use strict';
2+
3+
const getrandom_wasm_bindgen_test = require('./getrandom_wasm_bindgen_test');
4+
5+
console.log(getrandom_wasm_bindgen_test.test_gen());

tests/wasm_bindgen/src/lib.rs

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
// Copyright 2018 Developers of the Rand project.
2+
//
3+
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
4+
// https://www.apache.org/licenses/LICENSE-2.0> or the MIT license
5+
// <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your
6+
// option. This file may not be copied, modified, or distributed
7+
// except according to those terms.
8+
9+
// Crate to test WASM with the `wasm-bindgen` lib.
10+
11+
#![doc(html_logo_url = "https://www.rust-lang.org/logos/rust-logo-128x128-blk.png")]
12+
13+
extern crate getrandom;
14+
extern crate wasm_bindgen;
15+
extern crate wasm_bindgen_test;
16+
17+
use std::slice;
18+
use wasm_bindgen_test::*;
19+
use wasm_bindgen::prelude::*;
20+
21+
use getrandom::getrandom;
22+
23+
#[wasm_bindgen]
24+
pub fn test_gen() -> i32 {
25+
let mut int: i32 = 0;
26+
unsafe {
27+
let ptr = &mut int as *mut i32 as *mut u8;
28+
let slice = slice::from_raw_parts_mut(ptr, 4);
29+
getrandom(slice).unwrap();
30+
}
31+
int
32+
}
33+
34+
#[wasm_bindgen_test]
35+
fn test_call() {
36+
let mut buf = [0u8; 0];
37+
getrandom(&mut buf).unwrap();
38+
}
39+
40+
#[wasm_bindgen_test]
41+
fn test_diff() {
42+
let mut v1 = [0u8; 1000];
43+
getrandom(&mut v1).unwrap();
44+
45+
let mut v2 = [0u8; 1000];
46+
getrandom(&mut v2).unwrap();
47+
48+
let mut n_diff_bits = 0;
49+
for i in 0..v1.len() {
50+
n_diff_bits += (v1[i] ^ v2[i]).count_ones();
51+
}
52+
53+
// Check at least 1 bit per byte differs. p(failure) < 1e-1000 with random input.
54+
assert!(n_diff_bits >= v1.len() as u32);
55+
}

0 commit comments

Comments
 (0)