1 //! Implementation for WASM based on Web and Node.js
2 use crate::Error;
3 
4 extern crate std;
5 use std::{mem::MaybeUninit, thread_local};
6 
7 use js_sys::{global, Function, Uint8Array};
8 use wasm_bindgen::{prelude::wasm_bindgen, JsCast, JsValue};
9 
10 // Size of our temporary Uint8Array buffer used with WebCrypto methods
11 // Maximum is 65536 bytes see https://developer.mozilla.org/en-US/docs/Web/API/Crypto/getRandomValues
12 const WEB_CRYPTO_BUFFER_SIZE: usize = 256;
13 // Node.js's crypto.randomFillSync requires the size to be less than 2**31.
14 const NODE_MAX_BUFFER_SIZE: usize = (1 << 31) - 1;
15 
16 enum RngSource {
17     Node(NodeCrypto),
18     Web(WebCrypto, Uint8Array),
19 }
20 
21 // JsValues are always per-thread, so we initialize RngSource for each thread.
22 //   See: https://github.com/rustwasm/wasm-bindgen/pull/955
23 thread_local!(
24     static RNG_SOURCE: Result<RngSource, Error> = getrandom_init();
25 );
26 
getrandom_inner(dest: &mut [MaybeUninit<u8>]) -> Result<(), Error>27 pub(crate) fn getrandom_inner(dest: &mut [MaybeUninit<u8>]) -> Result<(), Error> {
28     RNG_SOURCE.with(|result| {
29         let source = result.as_ref().map_err(|&e| e)?;
30 
31         match source {
32             RngSource::Node(n) => {
33                 for chunk in dest.chunks_mut(NODE_MAX_BUFFER_SIZE) {
34                     // SAFETY: chunk is never used directly, the memory is only
35                     // modified via the Uint8Array view, which is passed
36                     // directly to JavaScript. Also, crypto.randomFillSync does
37                     // not resize the buffer. We know the length is less than
38                     // u32::MAX because of the chunking above.
39                     // Note that this uses the fact that JavaScript doesn't
40                     // have a notion of "uninitialized memory", this is purely
41                     // a Rust/C/C++ concept.
42                     let res = n.random_fill_sync(unsafe {
43                         Uint8Array::view_mut_raw(chunk.as_mut_ptr() as *mut u8, chunk.len())
44                     });
45                     if res.is_err() {
46                         return Err(Error::NODE_RANDOM_FILL_SYNC);
47                     }
48                 }
49             }
50             RngSource::Web(crypto, buf) => {
51                 // getRandomValues does not work with all types of WASM memory,
52                 // so we initially write to browser memory to avoid exceptions.
53                 for chunk in dest.chunks_mut(WEB_CRYPTO_BUFFER_SIZE) {
54                     // The chunk can be smaller than buf's length, so we call to
55                     // JS to create a smaller view of buf without allocation.
56                     let sub_buf = buf.subarray(0, chunk.len() as u32);
57 
58                     if crypto.get_random_values(&sub_buf).is_err() {
59                         return Err(Error::WEB_GET_RANDOM_VALUES);
60                     }
61 
62                     // SAFETY: `sub_buf`'s length is the same length as `chunk`
63                     unsafe { sub_buf.raw_copy_to_ptr(chunk.as_mut_ptr() as *mut u8) };
64                 }
65             }
66         };
67         Ok(())
68     })
69 }
70 
getrandom_init() -> Result<RngSource, Error>71 fn getrandom_init() -> Result<RngSource, Error> {
72     let global: Global = global().unchecked_into();
73 
74     // Get the Web Crypto interface if we are in a browser, Web Worker, Deno,
75     // or another environment that supports the Web Cryptography API. This
76     // also allows for user-provided polyfills in unsupported environments.
77     let crypto = match global.crypto() {
78         // Standard Web Crypto interface
79         c if c.is_object() => c,
80         // Node.js CommonJS Crypto module
81         _ if is_node(&global) => {
82             // If module.require isn't a valid function, we are in an ES module.
83             match Module::require_fn().and_then(JsCast::dyn_into::<Function>) {
84                 Ok(require_fn) => match require_fn.call1(&global, &JsValue::from_str("crypto")) {
85                     Ok(n) => return Ok(RngSource::Node(n.unchecked_into())),
86                     Err(_) => return Err(Error::NODE_CRYPTO),
87                 },
88                 Err(_) => return Err(Error::NODE_ES_MODULE),
89             }
90         }
91         // IE 11 Workaround
92         _ => match global.ms_crypto() {
93             c if c.is_object() => c,
94             _ => return Err(Error::WEB_CRYPTO),
95         },
96     };
97 
98     let buf = Uint8Array::new_with_length(WEB_CRYPTO_BUFFER_SIZE as u32);
99     Ok(RngSource::Web(crypto, buf))
100 }
101 
102 // Taken from https://www.npmjs.com/package/browser-or-node
is_node(global: &Global) -> bool103 fn is_node(global: &Global) -> bool {
104     let process = global.process();
105     if process.is_object() {
106         let versions = process.versions();
107         if versions.is_object() {
108             return versions.node().is_string();
109         }
110     }
111     false
112 }
113 
114 #[wasm_bindgen]
115 extern "C" {
116     // Return type of js_sys::global()
117     type Global;
118 
119     // Web Crypto API: Crypto interface (https://www.w3.org/TR/WebCryptoAPI/)
120     type WebCrypto;
121     // Getters for the WebCrypto API
122     #[wasm_bindgen(method, getter)]
crypto(this: &Global) -> WebCrypto123     fn crypto(this: &Global) -> WebCrypto;
124     #[wasm_bindgen(method, getter, js_name = msCrypto)]
ms_crypto(this: &Global) -> WebCrypto125     fn ms_crypto(this: &Global) -> WebCrypto;
126     // Crypto.getRandomValues()
127     #[wasm_bindgen(method, js_name = getRandomValues, catch)]
get_random_values(this: &WebCrypto, buf: &Uint8Array) -> Result<(), JsValue>128     fn get_random_values(this: &WebCrypto, buf: &Uint8Array) -> Result<(), JsValue>;
129 
130     // Node JS crypto module (https://nodejs.org/api/crypto.html)
131     type NodeCrypto;
132     // crypto.randomFillSync()
133     #[wasm_bindgen(method, js_name = randomFillSync, catch)]
random_fill_sync(this: &NodeCrypto, buf: Uint8Array) -> Result<(), JsValue>134     fn random_fill_sync(this: &NodeCrypto, buf: Uint8Array) -> Result<(), JsValue>;
135 
136     // Ideally, we would just use `fn require(s: &str)` here. However, doing
137     // this causes a Webpack warning. So we instead return the function itself
138     // and manually invoke it using call1. This also lets us to check that the
139     // function actually exists, allowing for better error messages. See:
140     //   https://github.com/rust-random/getrandom/issues/224
141     //   https://github.com/rust-random/getrandom/issues/256
142     type Module;
143     #[wasm_bindgen(getter, static_method_of = Module, js_class = module, js_name = require, catch)]
require_fn() -> Result<JsValue, JsValue>144     fn require_fn() -> Result<JsValue, JsValue>;
145 
146     // Node JS process Object (https://nodejs.org/api/process.html)
147     #[wasm_bindgen(method, getter)]
process(this: &Global) -> Process148     fn process(this: &Global) -> Process;
149     type Process;
150     #[wasm_bindgen(method, getter)]
versions(this: &Process) -> Versions151     fn versions(this: &Process) -> Versions;
152     type Versions;
153     #[wasm_bindgen(method, getter)]
node(this: &Versions) -> JsValue154     fn node(this: &Versions) -> JsValue;
155 }
156