Skip to main content

bitwarden_random/
ffi.rs

1//! Cross-platform (WASM / UniFFI) bindings for random-number generation.
2
3use bitwarden_error::bitwarden_error;
4use rand::{Rng, RngExt};
5use thiserror::Error;
6#[cfg(feature = "wasm")]
7use wasm_bindgen::prelude::*;
8
9use crate::rng;
10
11/// Error returned by [`SdkRandomNumberClient::gen_bytes`].
12#[bitwarden_error(flat)]
13#[derive(Debug, Error)]
14pub enum GenBytesError {
15    /// More than 1 KiB of randomness was requested.
16    #[error("gen_bytes() is limited to 1KiB; requested {requested} bytes")]
17    TooManyBytes {
18        /// The number of bytes that was requested.
19        requested: u32,
20    },
21}
22
23/// Error returned by [`SdkRandomNumberClient::gen_range`].
24#[bitwarden_error(flat)]
25#[derive(Debug, Error)]
26pub enum GenRangeError {
27    /// `min` was greater than `max`, so the range is empty.
28    #[error("Invalid range: min ({min}) must not exceed max ({max})")]
29    InvalidRange {
30        /// The requested lower bound.
31        min: u32,
32        /// The requested upper bound.
33        max: u32,
34    },
35}
36
37/// Client exposing random-number generation to cross-platform bindings.
38#[derive(Default)]
39#[cfg_attr(feature = "uniffi", derive(uniffi::Object))]
40#[cfg_attr(feature = "wasm", wasm_bindgen)]
41pub struct SdkRandomNumberClient;
42
43#[cfg_attr(feature = "wasm", wasm_bindgen)]
44#[cfg_attr(feature = "uniffi", uniffi::export)]
45impl SdkRandomNumberClient {
46    /// Construct a new client.
47    #[cfg_attr(feature = "wasm", wasm_bindgen(constructor))]
48    #[cfg_attr(feature = "uniffi", uniffi::constructor)]
49    pub fn new() -> Self {
50        // UniFFI does not support associated (static) functions, so the client is exposed as an
51        // object with a constructor and `&self` methods. The WASM binding mirrors that
52        // shape (constructor + instance methods) so a single impl block serves both.
53        SdkRandomNumberClient
54    }
55
56    /// Generate `len` cryptographically-secure random bytes.
57    ///
58    /// Returns [`GenBytesError::TooManyBytes`] if `len` exceeds 1 KiB. If you want over 1KiB of
59    /// randomness, please reconsider your design.
60    pub fn gen_bytes(&self, len: u32) -> Result<Vec<u8>, GenBytesError> {
61        if len > 1024 {
62            return Err(GenBytesError::TooManyBytes { requested: len });
63        }
64
65        let mut buf = vec![0u8; len as usize];
66        rng().fill_bytes(&mut buf);
67        Ok(buf)
68    }
69
70    /// Generate a random v4 UUID, sampled from a CRNG
71    ///
72    /// This has 122 random bits of entropy.
73    pub fn gen_uuid(&self) -> String {
74        let mut bytes = [0u8; 16];
75        rng().fill_bytes(&mut bytes);
76        uuid::Builder::from_random_bytes(bytes)
77            .with_variant(uuid::Variant::RFC4122)
78            .with_version(uuid::Version::Random)
79            .into_uuid()
80            .to_string()
81    }
82
83    /// Generate a cryptographically-secure random number in the range `[min, max]` (inclusive).
84    ///
85    /// Returns [`GenRangeError::InvalidRange`] if `min > max`.
86    pub fn gen_range(&self, min: u32, max: u32) -> Result<u32, GenRangeError> {
87        if min > max {
88            return Err(GenRangeError::InvalidRange { min, max });
89        }
90        Ok(rng().random_range(min..=max))
91    }
92}
93
94#[cfg(test)]
95mod tests {
96    use uuid::Uuid;
97
98    use super::*;
99
100    #[test]
101    fn gen_bytes_returns_requested_length() {
102        let client = SdkRandomNumberClient::new();
103        assert_eq!(client.gen_bytes(0).unwrap().len(), 0);
104        assert_eq!(client.gen_bytes(1).unwrap().len(), 1);
105        assert_eq!(client.gen_bytes(32).unwrap().len(), 32);
106        // 1 KiB is the documented maximum and must succeed.
107        assert_eq!(client.gen_bytes(1024).unwrap().len(), 1024);
108    }
109
110    #[test]
111    fn gen_bytes_is_random() {
112        let client = SdkRandomNumberClient::new();
113        // Two independent draws differ with overwhelming probability.
114        assert_ne!(client.gen_bytes(32).unwrap(), client.gen_bytes(32).unwrap());
115    }
116
117    #[test]
118    fn gen_bytes_errors_above_1_kib() {
119        let err = SdkRandomNumberClient::new().gen_bytes(1025).unwrap_err();
120        assert!(matches!(
121            err,
122            GenBytesError::TooManyBytes { requested: 1025 }
123        ));
124    }
125
126    #[test]
127    fn gen_uuid_is_a_valid_random_v4_uuid() {
128        let client = SdkRandomNumberClient::new();
129        let uuid = Uuid::parse_str(&client.gen_uuid()).expect("a valid UUID string");
130        assert_eq!(uuid.get_version(), Some(uuid::Version::Random));
131        assert_eq!(uuid.get_variant(), uuid::Variant::RFC4122);
132    }
133
134    #[test]
135    fn gen_uuid_is_distinct() {
136        let client = SdkRandomNumberClient::new();
137        assert_ne!(client.gen_uuid(), client.gen_uuid());
138    }
139
140    #[test]
141    fn gen_range_stays_within_inclusive_bounds() {
142        let client = SdkRandomNumberClient::new();
143        for _ in 0..1000 {
144            let n = client.gen_range(10, 20).unwrap();
145            assert!((10..=20).contains(&n));
146        }
147    }
148
149    #[test]
150    fn gen_range_with_single_value_is_that_value() {
151        let client = SdkRandomNumberClient::new();
152        assert_eq!(client.gen_range(7, 7).unwrap(), 7);
153    }
154
155    #[test]
156    fn gen_range_errors_when_min_exceeds_max() {
157        let err = SdkRandomNumberClient::new().gen_range(20, 10).unwrap_err();
158        assert!(matches!(
159            err,
160            GenRangeError::InvalidRange { min: 20, max: 10 }
161        ));
162    }
163}