Skip to main content

openentropy_wasm/
lib.rs

1//! OpenEntropy WebAssembly bindings — browser-based entropy collection.
2//!
3//! Exposes two entropy sources via `wasm-bindgen`:
4//!
5//! 1. **Timing jitter** — `performance.now()` micro-timing variations
6//! 2. **Crypto seed mixer** — `crypto.getRandomValues()` as an OS entropy seed
7//!
8//! Plus a combined SHA-256 conditioned output (`get_random_bytes`) that mixes
9//! both sources. All raw sources produce bytes that can be further conditioned
10//! on the JS side or consumed directly.
11
12use sha2::{Digest, Sha256};
13use wasm_bindgen::prelude::*;
14
15// ---------------------------------------------------------------------------
16// Browser API helpers
17// ---------------------------------------------------------------------------
18
19/// Get `performance.now()` as f64 milliseconds.
20fn performance_now() -> f64 {
21    js_sys::Reflect::get(&js_sys::global(), &JsValue::from_str("performance"))
22        .ok()
23        .and_then(|perf| js_sys::Reflect::get(&perf, &JsValue::from_str("now")).ok())
24        .and_then(|func| {
25            let func: js_sys::Function = func.dyn_into().ok()?;
26            func.call0(&js_sys::global().into()).ok()?.as_f64()
27        })
28        .unwrap_or(0.0)
29}
30
31/// Fill a buffer with `crypto.getRandomValues()`.
32fn crypto_get_random(buf: &mut [u8]) -> bool {
33    let global = js_sys::global();
34    let crypto = js_sys::Reflect::get(&global, &JsValue::from_str("crypto")).ok();
35    let crypto = match crypto {
36        Some(c) if !c.is_undefined() => c,
37        _ => return false,
38    };
39
40    let array = js_sys::Uint8Array::new_with_length(buf.len() as u32);
41    let func = js_sys::Reflect::get(&crypto, &JsValue::from_str("getRandomValues")).ok();
42    let func = match func {
43        Some(f) => match f.dyn_into::<js_sys::Function>() {
44            Ok(f) => f,
45            Err(_) => return false,
46        },
47        None => return false,
48    };
49
50    if func.call1(&crypto, &array).is_err() {
51        return false;
52    }
53
54    array.copy_to(buf);
55    true
56}
57
58// ---------------------------------------------------------------------------
59// XOR-fold helper
60// ---------------------------------------------------------------------------
61
62/// XOR-fold a f64 (8 bytes) into a single byte.
63#[inline]
64fn xor_fold_f64(v: f64) -> u8 {
65    let b = v.to_le_bytes();
66    b[0] ^ b[1] ^ b[2] ^ b[3] ^ b[4] ^ b[5] ^ b[6] ^ b[7]
67}
68
69// ---------------------------------------------------------------------------
70// Timing jitter source
71// ---------------------------------------------------------------------------
72
73/// Collect entropy from `performance.now()` timing jitter.
74///
75/// Performs rapid back-to-back `performance.now()` calls and extracts
76/// entropy from the timing deltas. Browser timer resolution is typically
77/// 5-100 µs (reduced by Spectre mitigations), but the jitter between
78/// consecutive calls still carries entropy from CPU scheduling, cache
79/// state, and GC activity.
80#[wasm_bindgen]
81pub fn collect_timing_jitter(n_bytes: usize) -> Vec<u8> {
82    // Oversample 8x — each timing produces ~1 bit of useful jitter.
83    let raw_count = n_bytes * 8 + 64;
84    let mut timings = Vec::with_capacity(raw_count);
85
86    // Warm up the timer
87    for _ in 0..16 {
88        let _ = performance_now();
89    }
90
91    // Interleave timing measurements with small computational work
92    // to vary cache/pipeline state between measurements.
93    let mut work: u64 = performance_now().to_bits();
94    for _ in 0..raw_count {
95        let t = performance_now();
96        timings.push(t);
97
98        // Small amount of work to perturb microarchitectural state
99        work = work.wrapping_mul(6364136223846793005).wrapping_add(1);
100        std::hint::black_box(work);
101    }
102
103    // Compute deltas
104    let deltas: Vec<f64> = timings.windows(2).map(|w| w[1] - w[0]).collect();
105
106    // XOR consecutive deltas and fold
107    let mut raw = Vec::with_capacity(n_bytes);
108    for pair in deltas.windows(2) {
109        let xored = (pair[0] - pair[1]).to_bits() ^ pair[0].to_bits();
110        raw.push(xor_fold_f64(f64::from_bits(xored)));
111        if raw.len() >= n_bytes {
112            break;
113        }
114    }
115
116    raw.truncate(n_bytes);
117    raw
118}
119
120// ---------------------------------------------------------------------------
121// Crypto seed source
122// ---------------------------------------------------------------------------
123
124/// Collect OS entropy via `crypto.getRandomValues()`.
125///
126/// This uses the browser's built-in CSPRNG, which typically draws from
127/// the OS entropy pool. Useful as a high-quality seed to mix with
128/// timing-based sources.
129#[wasm_bindgen]
130pub fn collect_crypto_random(n_bytes: usize) -> Vec<u8> {
131    let mut buf = vec![0u8; n_bytes];
132    if !crypto_get_random(&mut buf) {
133        // Fallback: fill with timing-based entropy if crypto API unavailable
134        return collect_timing_jitter(n_bytes);
135    }
136    buf
137}
138
139// ---------------------------------------------------------------------------
140// Combined conditioned output
141// ---------------------------------------------------------------------------
142
143/// Collect `n_bytes` of SHA-256 conditioned entropy from all available
144/// browser sources.
145///
146/// Combines timing jitter and crypto.getRandomValues() into a SHA-256
147/// conditioned output stream. This is the recommended entry point for
148/// applications that need high-quality random bytes.
149#[wasm_bindgen]
150pub fn get_random_bytes(n_bytes: usize) -> Vec<u8> {
151    let mut output = Vec::with_capacity(n_bytes);
152    let mut counter: u64 = 0;
153
154    // Collect raw material from both sources
155    let timing = collect_timing_jitter(n_bytes.max(32));
156    let crypto = collect_crypto_random(32);
157
158    // Initial state from crypto source
159    let mut state: [u8; 32] = {
160        let mut h = Sha256::new();
161        h.update(&crypto);
162        h.update(performance_now().to_le_bytes());
163        h.finalize().into()
164    };
165
166    while output.len() < n_bytes {
167        counter += 1;
168        let mut h = Sha256::new();
169        h.update(state);
170        h.update(counter.to_le_bytes());
171
172        // Mix in timing entropy
173        let offset = (counter as usize * 16) % timing.len().max(1);
174        let end = (offset + 16).min(timing.len());
175        if offset < end {
176            h.update(&timing[offset..end]);
177        }
178
179        // Mix in fresh timing sample
180        h.update(performance_now().to_le_bytes());
181
182        let digest: [u8; 32] = h.finalize().into();
183        output.extend_from_slice(&digest);
184
185        // Derive next state separately from output for forward secrecy.
186        // An adversary who observes output cannot reconstruct the state.
187        let mut h2 = Sha256::new();
188        h2.update(digest);
189        h2.update(b"openentropy_state");
190        state = h2.finalize().into();
191    }
192
193    output.truncate(n_bytes);
194    output
195}
196
197/// Return the number of available entropy sources in this WASM environment.
198#[wasm_bindgen]
199pub fn available_source_count() -> u32 {
200    let mut count = 1; // timing jitter is always available
201
202    // Check if crypto.getRandomValues() is available
203    let global = js_sys::global();
204    if let Ok(crypto) = js_sys::Reflect::get(&global, &JsValue::from_str("crypto"))
205        && !crypto.is_undefined()
206    {
207        count += 1;
208    }
209
210    count
211}
212
213#[cfg(test)]
214mod tests {
215    use super::*;
216
217    #[test]
218    fn xor_fold_f64_zero() {
219        assert_eq!(xor_fold_f64(0.0), 0);
220    }
221
222    #[test]
223    fn xor_fold_f64_one() {
224        let v = xor_fold_f64(1.0);
225        // 1.0 as f64 = 0x3FF0000000000000
226        // bytes: [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x3F]
227        assert_eq!(v, 0xF0 ^ 0x3F);
228    }
229
230    #[test]
231    fn xor_fold_f64_negative_zero() {
232        // -0.0 as f64 = 0x8000000000000000
233        // bytes: [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80]
234        assert_eq!(xor_fold_f64(-0.0), 0x80);
235    }
236
237    #[test]
238    fn xor_fold_f64_nan() {
239        let v = xor_fold_f64(f64::NAN);
240        // NaN has non-zero bits, so fold should be non-trivial
241        // (exact value depends on NaN representation, just check it runs)
242        let _ = v;
243    }
244
245    #[test]
246    fn xor_fold_f64_infinity() {
247        let v = xor_fold_f64(f64::INFINITY);
248        // INFINITY = 0x7FF0000000000000
249        // bytes: [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x7F]
250        assert_eq!(v, 0xF0 ^ 0x7F);
251    }
252}