Skip to content

Commit a4343bb

Browse files
committed
#194 - Random seeds. On native, it tries getrandom first. If you're on an architecture that doesn't support that syscall, falls back to time since start. On WASM, it queries your WASM host for either the browser or Node RNG functions. If they are available, it uses them to generate a good seed. If that isn't available, it falls back to using js time functions.
1 parent aabfa49 commit a4343bb

File tree

5 files changed

+295
-24
lines changed

5 files changed

+295
-24
lines changed

bracket-random/Cargo.toml

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,6 @@ keywords = ["roguelike", "gamedev", "random", "xorshift", "dice"]
1212
categories = ["game-engines", "random"]
1313
license = "MIT"
1414

15-
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
16-
1715
[features]
1816
default = [ "parsing" ]
1917
parsing = [ "regex", "lazy_static" ]
@@ -26,8 +24,12 @@ lazy_static = { version = "1.4.0", optional = true }
2624
serde_crate = { version = "~1.0.110", features = ["derive"], optional = true, package = "serde" }
2725
rand = { version = "0.8.3", default-features = false }
2826

27+
[target.'cfg(not(any(target_arch = "wasm32")))'.dependencies]
28+
getrandom = { version = "0.2.2" }
29+
2930
[target.wasm32-unknown-unknown.dependencies]
30-
js-sys = "0.3.47"
31+
js-sys = "0.3.48"
32+
wasm-bindgen = "0.2"
3133

3234
[dev-dependencies]
3335
criterion = "0.3.4"

bracket-random/src/js_seed/error.rs

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
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+
use core::{fmt, num::NonZeroU32};
9+
10+
/// A small and `no_std` compatible error type
11+
///
12+
/// The [`Error::raw_os_error()`] will indicate if the error is from the OS, and
13+
/// if so, which error code the OS gave the application. If such an error is
14+
/// encountered, please consult with your system documentation.
15+
///
16+
/// Internally this type is a NonZeroU32, with certain values reserved for
17+
/// certain purposes, see [`Error::INTERNAL_START`] and [`Error::CUSTOM_START`].
18+
///
19+
/// *If this crate's `"std"` Cargo feature is enabled*, then:
20+
/// - [`getrandom::Error`][Error] implements
21+
/// [`std::error::Error`](https://doc.rust-lang.org/std/error/trait.Error.html)
22+
/// - [`std::io::Error`](https://doc.rust-lang.org/std/io/struct.Error.html) implements
23+
/// [`From<getrandom::Error>`](https://doc.rust-lang.org/std/convert/trait.From.html).
24+
#[derive(Copy, Clone, Eq, PartialEq)]
25+
pub struct Error(NonZeroU32);
26+
27+
const fn internal_error(n: u16) -> Error {
28+
// SAFETY: code > 0 as INTERNAL_START > 0 and adding n won't overflow a u32.
29+
let code = Error::INTERNAL_START + (n as u32);
30+
Error(unsafe { NonZeroU32::new_unchecked(code) })
31+
}
32+
33+
impl Error {
34+
/// This target/platform is not supported by `getrandom`.
35+
pub const UNSUPPORTED: Error = internal_error(0);
36+
/// The platform-specific `errno` returned a non-positive value.
37+
pub const ERRNO_NOT_POSITIVE: Error = internal_error(1);
38+
/// Call to iOS [`SecRandomCopyBytes`](https://developer.apple.com/documentation/security/1399291-secrandomcopybytes) failed.
39+
pub const IOS_SEC_RANDOM: Error = internal_error(3);
40+
/// Call to Windows [`RtlGenRandom`](https://docs.microsoft.com/en-us/windows/win32/api/ntsecapi/nf-ntsecapi-rtlgenrandom) failed.
41+
pub const WINDOWS_RTL_GEN_RANDOM: Error = internal_error(4);
42+
/// RDRAND instruction failed due to a hardware issue.
43+
pub const FAILED_RDRAND: Error = internal_error(5);
44+
/// RDRAND instruction unsupported on this target.
45+
pub const NO_RDRAND: Error = internal_error(6);
46+
/// The browser does not have support for `self.crypto`.
47+
pub const WEB_CRYPTO: Error = internal_error(7);
48+
/// The browser does not have support for `crypto.getRandomValues`.
49+
pub const WEB_GET_RANDOM_VALUES: Error = internal_error(8);
50+
/// On VxWorks, call to `randSecure` failed (random number generator is not yet initialized).
51+
pub const VXWORKS_RAND_SECURE: Error = internal_error(11);
52+
/// NodeJS does not have support for the `crypto` module.
53+
pub const NODE_CRYPTO: Error = internal_error(12);
54+
/// NodeJS does not have support for `crypto.randomFillSync`.
55+
pub const NODE_RANDOM_FILL_SYNC: Error = internal_error(13);
56+
57+
/// Codes below this point represent OS Errors (i.e. positive i32 values).
58+
/// Codes at or above this point, but below [`Error::CUSTOM_START`] are
59+
/// reserved for use by the `rand` and `getrandom` crates.
60+
pub const INTERNAL_START: u32 = 1 << 31;
61+
62+
/// Codes at or above this point can be used by users to define their own
63+
/// custom errors.
64+
pub const CUSTOM_START: u32 = (1 << 31) + (1 << 30);
65+
66+
/// Extract the raw OS error code (if this error came from the OS)
67+
///
68+
/// This method is identical to [`std::io::Error::raw_os_error()`][1], except
69+
/// that it works in `no_std` contexts. If this method returns `None`, the
70+
/// error value can still be formatted via the `Display` implementation.
71+
///
72+
/// [1]: https://doc.rust-lang.org/std/io/struct.Error.html#method.raw_os_error
73+
#[inline]
74+
pub fn raw_os_error(self) -> Option<i32> {
75+
if self.0.get() < Self::INTERNAL_START {
76+
Some(self.0.get() as i32)
77+
} else {
78+
None
79+
}
80+
}
81+
82+
/// Extract the bare error code.
83+
///
84+
/// This code can either come from the underlying OS, or be a custom error.
85+
/// Use [`Error::raw_os_error()`] to disambiguate.
86+
#[inline]
87+
pub const fn code(self) -> NonZeroU32 {
88+
self.0
89+
}
90+
}
91+
92+
fn os_err(_errno: i32, _buf: &mut [u8]) -> Option<&str> {
93+
None
94+
}
95+
96+
impl fmt::Debug for Error {
97+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
98+
let mut dbg = f.debug_struct("Error");
99+
if let Some(errno) = self.raw_os_error() {
100+
dbg.field("os_error", &errno);
101+
let mut buf = [0u8; 128];
102+
if let Some(err) = os_err(errno, &mut buf) {
103+
dbg.field("description", &err);
104+
}
105+
} else if let Some(desc) = internal_desc(*self) {
106+
dbg.field("internal_code", &self.0.get());
107+
dbg.field("description", &desc);
108+
} else {
109+
dbg.field("unknown_code", &self.0.get());
110+
}
111+
dbg.finish()
112+
}
113+
}
114+
115+
impl fmt::Display for Error {
116+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
117+
if let Some(errno) = self.raw_os_error() {
118+
let mut buf = [0u8; 128];
119+
match os_err(errno, &mut buf) {
120+
Some(err) => err.fmt(f),
121+
None => write!(f, "OS Error: {}", errno),
122+
}
123+
} else if let Some(desc) = internal_desc(*self) {
124+
f.write_str(desc)
125+
} else {
126+
write!(f, "Unknown Error: {}", self.0.get())
127+
}
128+
}
129+
}
130+
131+
impl From<NonZeroU32> for Error {
132+
fn from(code: NonZeroU32) -> Self {
133+
Self(code)
134+
}
135+
}
136+
137+
fn internal_desc(error: Error) -> Option<&'static str> {
138+
match error {
139+
Error::UNSUPPORTED => Some("getrandom: this target is not supported"),
140+
Error::ERRNO_NOT_POSITIVE => Some("errno: did not return a positive value"),
141+
Error::IOS_SEC_RANDOM => Some("SecRandomCopyBytes: iOS Security framework failure"),
142+
Error::WINDOWS_RTL_GEN_RANDOM => Some("RtlGenRandom: Windows system function failure"),
143+
Error::FAILED_RDRAND => Some("RDRAND: failed multiple times: CPU issue likely"),
144+
Error::NO_RDRAND => Some("RDRAND: instruction not supported"),
145+
Error::WEB_CRYPTO => Some("Web API self.crypto is unavailable"),
146+
Error::WEB_GET_RANDOM_VALUES => Some("Web API crypto.getRandomValues is unavailable"),
147+
Error::VXWORKS_RAND_SECURE => Some("randSecure: VxWorks RNG module is not initialized"),
148+
Error::NODE_CRYPTO => Some("Node.js crypto module is unavailable"),
149+
Error::NODE_RANDOM_FILL_SYNC => Some("Node.js API crypto.randomFillSync is unavailable"),
150+
_ => None,
151+
}
152+
}
153+
154+
#[cfg(test)]
155+
mod tests {
156+
use super::Error;
157+
use core::mem::size_of;
158+
159+
#[test]
160+
fn test_size() {
161+
assert_eq!(size_of::<Error>(), 4);
162+
assert_eq!(size_of::<Result<(), Error>>(), 4);
163+
}
164+
}

bracket-random/src/js_seed/mod.rs

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
mod error;
2+
use error::Error;
3+
4+
extern crate std;
5+
use std::thread_local;
6+
7+
use js_sys::Uint8Array;
8+
use wasm_bindgen::prelude::*;
9+
10+
// Maximum is 65536 bytes see https://developer.mozilla.org/en-US/docs/Web/API/Crypto/getRandomValues
11+
const BROWSER_CRYPTO_BUFFER_SIZE: usize = 256;
12+
13+
enum RngSource {
14+
Node(NodeCrypto),
15+
Browser(BrowserCrypto, Uint8Array),
16+
}
17+
18+
// JsValues are always per-thread, so we initialize RngSource for each thread.
19+
// See: https://github.com/rustwasm/wasm-bindgen/pull/955
20+
thread_local!(
21+
static RNG_SOURCE: Result<RngSource, Error> = getrandom_init();
22+
);
23+
24+
pub(crate) fn getrandom_inner(dest: &mut [u8]) -> Result<(), Error> {
25+
RNG_SOURCE.with(|result| {
26+
let source = result.as_ref().map_err(|&e| e)?;
27+
28+
match source {
29+
RngSource::Node(n) => {
30+
if n.random_fill_sync(dest).is_err() {
31+
return Err(Error::NODE_RANDOM_FILL_SYNC);
32+
}
33+
}
34+
RngSource::Browser(crypto, buf) => {
35+
// getRandomValues does not work with all types of WASM memory,
36+
// so we initially write to browser memory to avoid exceptions.
37+
for chunk in dest.chunks_mut(BROWSER_CRYPTO_BUFFER_SIZE) {
38+
// The chunk can be smaller than buf's length, so we call to
39+
// JS to create a smaller view of buf without allocation.
40+
let sub_buf = buf.subarray(0, chunk.len() as u32);
41+
42+
if crypto.get_random_values(&sub_buf).is_err() {
43+
return Err(Error::WEB_GET_RANDOM_VALUES);
44+
}
45+
sub_buf.copy_to(chunk);
46+
}
47+
}
48+
};
49+
Ok(())
50+
})
51+
}
52+
53+
fn getrandom_init() -> Result<RngSource, Error> {
54+
if let Ok(self_) = Global::get_self() {
55+
// If `self` is defined then we're in a browser somehow (main window
56+
// or web worker). We get `self.crypto` (called `msCrypto` on IE), so we
57+
// can call `crypto.getRandomValues`. If `crypto` isn't defined, we
58+
// assume we're in an older web browser and the OS RNG isn't available.
59+
60+
let crypto: BrowserCrypto = match (self_.crypto(), self_.ms_crypto()) {
61+
(crypto, _) if !crypto.is_undefined() => crypto,
62+
(_, crypto) if !crypto.is_undefined() => crypto,
63+
_ => return Err(Error::WEB_CRYPTO),
64+
};
65+
66+
let buf = Uint8Array::new_with_length(BROWSER_CRYPTO_BUFFER_SIZE as u32);
67+
return Ok(RngSource::Browser(crypto, buf));
68+
}
69+
70+
let crypto = MODULE.require("crypto").map_err(|_| Error::NODE_CRYPTO)?;
71+
Ok(RngSource::Node(crypto))
72+
}
73+
74+
#[wasm_bindgen]
75+
extern "C" {
76+
type Global;
77+
#[wasm_bindgen(getter, catch, static_method_of = Global, js_class = self, js_name = self)]
78+
fn get_self() -> Result<Self_, JsValue>;
79+
80+
type Self_;
81+
#[wasm_bindgen(method, getter, js_name = "msCrypto")]
82+
fn ms_crypto(me: &Self_) -> BrowserCrypto;
83+
#[wasm_bindgen(method, getter)]
84+
fn crypto(me: &Self_) -> BrowserCrypto;
85+
86+
type BrowserCrypto;
87+
#[wasm_bindgen(method, js_name = getRandomValues, catch)]
88+
fn get_random_values(me: &BrowserCrypto, buf: &Uint8Array) -> Result<(), JsValue>;
89+
90+
#[wasm_bindgen(js_name = module)]
91+
static MODULE: NodeModule;
92+
93+
type NodeModule;
94+
#[wasm_bindgen(method, catch)]
95+
fn require(this: &NodeModule, s: &str) -> Result<NodeCrypto, JsValue>;
96+
97+
type NodeCrypto;
98+
#[wasm_bindgen(method, js_name = randomFillSync, catch)]
99+
fn random_fill_sync(crypto: &NodeCrypto, buf: &mut [u8]) -> Result<(), JsValue>;
100+
}

bracket-random/src/lib.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ mod random;
77
#[cfg(feature = "parsing")]
88
mod parsing;
99

10+
#[cfg(target_arch = "wasm32")]
11+
mod js_seed;
12+
1013
mod iterators;
1114

1215
pub mod prelude {
@@ -16,4 +19,4 @@ pub mod prelude {
1619
pub use crate::parsing::*;
1720

1821
pub use crate::iterators::*;
19-
}
22+
}

bracket-random/src/random.rs

Lines changed: 22 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,28 @@ use rand_xorshift::XorShiftRng;
66
#[cfg(feature = "serde")]
77
use serde_crate::{Deserialize, Serialize};
88

9-
109
#[cfg(target_arch = "wasm32")]
11-
fn unix_now() -> f64 {
12-
js_sys::Date::now()
10+
fn get_seed() -> u64 {
11+
let mut buf = [0u8; 8];
12+
if crate::js_seed::getrandom_inner(&mut buf).is_ok() {
13+
u64::from_be_bytes(buf)
14+
} else {
15+
js_sys::Date::now() as u64
16+
}
17+
}
18+
19+
#[cfg(not(target_arch = "wasm32"))]
20+
fn get_seed() -> u64 {
21+
let mut buf = [0u8; 8];
22+
if getrandom::getrandom(&mut buf).is_ok() {
23+
u64::from_be_bytes(buf)
24+
} else {
25+
use std::time::{SystemTime, UNIX_EPOCH};
26+
SystemTime::now()
27+
.duration_since(UNIX_EPOCH)
28+
.unwrap()
29+
.as_secs() as u64
30+
}
1331
}
1432

1533
#[derive(Clone)]
@@ -25,24 +43,8 @@ pub struct RandomNumberGenerator {
2543
impl RandomNumberGenerator {
2644
/// Creates a new RNG from a randomly generated seed
2745
#[allow(clippy::new_without_default)] // XorShiftRng doesn't have a Default, so we don't either
28-
#[cfg(not(target_arch = "wasm32"))]
29-
pub fn new() -> RandomNumberGenerator {
30-
use std::time::{SystemTime, UNIX_EPOCH};
31-
let rng: XorShiftRng = SeedableRng::seed_from_u64(
32-
SystemTime::now()
33-
.duration_since(UNIX_EPOCH)
34-
.unwrap()
35-
.as_secs() as u64,
36-
);
37-
RandomNumberGenerator { rng }
38-
}
39-
40-
#[cfg(target_arch = "wasm32")]
41-
#[allow(clippy::new_without_default)] // XorShiftRng doesn't have a Default, so we don't either
4246
pub fn new() -> RandomNumberGenerator {
43-
let rng: XorShiftRng = SeedableRng::seed_from_u64(
44-
unix_now() as u64
45-
);
47+
let rng: XorShiftRng = SeedableRng::seed_from_u64(get_seed());
4648
RandomNumberGenerator { rng }
4749
}
4850

0 commit comments

Comments
 (0)