Skip to main content

uselesskey_entropy/
lib.rs

1#![forbid(unsafe_code)]
2
3//! High-entropy byte fixtures built on `uselesskey-core`.
4//!
5//! This crate is the narrow public lane for tests that only need stable,
6//! scanner-safe byte buffers and do not need real crypto semantics.
7//!
8//! Most users can depend on the [`uselesskey`](https://crates.io/crates/uselesskey)
9//! facade crate with `default-features = false, features = ["entropy"]`.
10
11use std::fmt;
12use std::sync::Arc;
13
14use uselesskey_core::Factory;
15
16/// Cache domain for entropy fixtures.
17///
18/// Keep this stable: changing it changes deterministic outputs.
19pub const DOMAIN_ENTROPY_FIXTURE: &str = "uselesskey:entropy:fixture";
20
21/// Handle used to derive deterministic high-entropy byte fixtures.
22#[derive(Clone)]
23pub struct EntropyFixture {
24    factory: Factory,
25    label: String,
26    variant: String,
27}
28
29struct Inner {
30    bytes: Vec<u8>,
31}
32
33impl fmt::Debug for EntropyFixture {
34    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
35        f.debug_struct("EntropyFixture")
36            .field("label", &self.label)
37            .field("variant", &self.variant)
38            .finish_non_exhaustive()
39    }
40}
41
42/// Extension trait to hang entropy helpers off the core [`Factory`].
43pub trait EntropyFactoryExt {
44    /// Create an entropy fixture handle for a label.
45    fn entropy(&self, label: impl AsRef<str>) -> EntropyFixture;
46
47    /// Create an entropy fixture handle with a custom variant.
48    fn entropy_with_variant(
49        &self,
50        label: impl AsRef<str>,
51        variant: impl AsRef<str>,
52    ) -> EntropyFixture;
53}
54
55impl EntropyFactoryExt for Factory {
56    fn entropy(&self, label: impl AsRef<str>) -> EntropyFixture {
57        EntropyFixture::new(self.clone(), label.as_ref(), "good")
58    }
59
60    fn entropy_with_variant(
61        &self,
62        label: impl AsRef<str>,
63        variant: impl AsRef<str>,
64    ) -> EntropyFixture {
65        EntropyFixture::new(self.clone(), label.as_ref(), variant.as_ref())
66    }
67}
68
69impl EntropyFixture {
70    fn new(factory: Factory, label: &str, variant: &str) -> Self {
71        Self {
72            factory,
73            label: label.to_string(),
74            variant: variant.to_string(),
75        }
76    }
77
78    /// Returns the label used to derive this fixture.
79    pub fn label(&self) -> &str {
80        &self.label
81    }
82
83    /// Returns the default variant used by this fixture.
84    pub fn variant(&self) -> &str {
85        &self.variant
86    }
87
88    /// Returns a deterministic byte buffer of the requested length.
89    pub fn bytes(&self, len: usize) -> Vec<u8> {
90        self.bytes_with_variant(len, &self.variant)
91    }
92
93    /// Returns a deterministic byte buffer for an explicit variant.
94    pub fn bytes_with_variant(&self, len: usize, variant: impl AsRef<str>) -> Vec<u8> {
95        load_inner(&self.factory, &self.label, len, variant.as_ref())
96            .bytes
97            .clone()
98    }
99
100    /// Fill an existing buffer with deterministic entropy.
101    pub fn fill_bytes(&self, dest: &mut [u8]) {
102        self.fill_bytes_with_variant(dest, &self.variant);
103    }
104
105    /// Fill an existing buffer with deterministic entropy for an explicit variant.
106    pub fn fill_bytes_with_variant(&self, dest: &mut [u8], variant: impl AsRef<str>) {
107        let bytes = load_inner(&self.factory, &self.label, dest.len(), variant.as_ref());
108        dest.copy_from_slice(&bytes.bytes);
109    }
110}
111
112fn load_inner(factory: &Factory, label: &str, len: usize, variant: &str) -> Arc<Inner> {
113    factory.get_or_init(
114        DOMAIN_ENTROPY_FIXTURE,
115        label,
116        &len.to_le_bytes(),
117        variant,
118        |seed| {
119            let mut bytes = vec![0u8; len];
120            seed.fill_bytes(&mut bytes);
121            Inner { bytes }
122        },
123    )
124}
125
126#[cfg(test)]
127mod tests {
128    use super::*;
129    use proptest::prop_assert_eq;
130    use uselesskey_core::Seed;
131    use uselesskey_test_support::{TestResult, require_ok};
132
133    #[test]
134    fn deterministic_entropy_is_stable() -> TestResult<()> {
135        let fx = Factory::deterministic(require_ok(
136            Seed::from_env_value("entropy-det"),
137            "entropy-det must parse as a deterministic seed",
138        )?);
139        let a = fx.entropy("svc").bytes(32);
140        let b = fx.entropy("svc").bytes(32);
141        assert_eq!(a, b);
142        Ok(())
143    }
144
145    #[test]
146    fn random_mode_still_caches_per_identity() {
147        let fx = Factory::random();
148        let a = fx.entropy("svc").bytes(32);
149        let b = fx.entropy("svc").bytes(32);
150        assert_eq!(a, b);
151    }
152
153    #[test]
154    fn different_labels_produce_different_bytes() -> TestResult<()> {
155        let fx = Factory::deterministic(require_ok(
156            Seed::from_env_value("entropy-labels"),
157            "entropy-labels must parse as a deterministic seed",
158        )?);
159        let a = fx.entropy("a").bytes(32);
160        let b = fx.entropy("b").bytes(32);
161        assert_ne!(a, b);
162        Ok(())
163    }
164
165    #[test]
166    fn different_variants_produce_different_bytes() -> TestResult<()> {
167        let fx = Factory::deterministic(require_ok(
168            Seed::from_env_value("entropy-variants"),
169            "entropy-variants must parse as a deterministic seed",
170        )?);
171        let fixture = fx.entropy("svc");
172        let good = fixture.bytes(32);
173        let alt = fixture.bytes_with_variant(32, "alt");
174        assert_ne!(good, alt);
175        Ok(())
176    }
177
178    #[test]
179    fn accessors_report_fixture_identity() -> TestResult<()> {
180        let fx = Factory::deterministic(require_ok(
181            Seed::from_env_value("entropy-identity"),
182            "entropy-identity must parse as a deterministic seed",
183        )?);
184        let default = fx.entropy("svc");
185        let custom = fx.entropy_with_variant("svc-alt", "custom");
186
187        assert_eq!(default.label(), "svc");
188        assert_eq!(default.variant(), "good");
189        assert_eq!(custom.label(), "svc-alt");
190        assert_eq!(custom.variant(), "custom");
191        Ok(())
192    }
193
194    #[test]
195    fn fill_bytes_matches_allocating_path() -> TestResult<()> {
196        let fx = Factory::deterministic(require_ok(
197            Seed::from_env_value("entropy-fill"),
198            "entropy-fill must parse as a deterministic seed",
199        )?);
200        let fixture = fx.entropy("svc");
201
202        let expected = fixture.bytes(24);
203        let mut actual = [0u8; 24];
204        fixture.fill_bytes(&mut actual);
205
206        assert_eq!(expected, actual);
207        Ok(())
208    }
209
210    #[test]
211    fn debug_does_not_include_bytes() -> TestResult<()> {
212        let fx = Factory::deterministic(require_ok(
213            Seed::from_env_value("entropy-debug"),
214            "entropy-debug must parse as a deterministic seed",
215        )?);
216        let fixture = fx.entropy("svc");
217        let dbg = format!("{fixture:?}");
218
219        assert!(dbg.contains("EntropyFixture"));
220        assert!(dbg.contains("svc"));
221        assert!(!dbg.contains("["));
222        Ok(())
223    }
224
225    proptest::proptest! {
226        #[test]
227        fn requested_length_is_preserved(len in 0usize..2048) {
228            let fx = Factory::deterministic(Seed::new([7u8; 32]));
229            let bytes = fx.entropy("prop").bytes(len);
230            prop_assert_eq!(bytes.len(), len);
231        }
232    }
233
234    #[test]
235    fn entropy_with_variant_constructor_derives_from_custom_variant() -> TestResult<()> {
236        let fx = Factory::deterministic(require_ok(
237            Seed::from_env_value("entropy-with-variant-ctor"),
238            "must parse seed",
239        )?);
240
241        let good = fx.entropy("svc").bytes(32);
242        let custom = fx.entropy_with_variant("svc", "alt").bytes(32);
243
244        assert_ne!(
245            good, custom,
246            "entropy_with_variant must derive from the variant, not the default"
247        );
248
249        // Calling with the same custom variant on either constructor produces
250        // the same bytes (cache identity is (label, len, variant)).
251        let custom_again = fx.entropy("svc").bytes_with_variant(32, "alt");
252        assert_eq!(custom, custom_again);
253        Ok(())
254    }
255
256    #[test]
257    fn fill_bytes_with_variant_matches_allocating_path() -> TestResult<()> {
258        let fx = Factory::deterministic(require_ok(
259            Seed::from_env_value("entropy-fill-with-variant"),
260            "must parse seed",
261        )?);
262        let fixture = fx.entropy("svc");
263
264        let expected = fixture.bytes_with_variant(40, "alt");
265        let mut actual = [0u8; 40];
266        fixture.fill_bytes_with_variant(&mut actual, "alt");
267
268        assert_eq!(expected.as_slice(), &actual[..]);
269
270        // And differs from the default-variant fill of the same length.
271        let mut default_filled = [0u8; 40];
272        fixture.fill_bytes(&mut default_filled);
273        assert_ne!(actual, default_filled);
274        Ok(())
275    }
276
277    #[test]
278    fn zero_length_request_returns_empty_buffer() -> TestResult<()> {
279        let fx = Factory::deterministic(require_ok(
280            Seed::from_env_value("entropy-zero-len"),
281            "must parse seed",
282        )?);
283        let fixture = fx.entropy("svc");
284
285        let empty = fixture.bytes(0);
286        assert!(empty.is_empty());
287
288        // fill_bytes on a zero-length buffer must be a no-op (and not panic).
289        let mut buf: [u8; 0] = [];
290        fixture.fill_bytes(&mut buf);
291        Ok(())
292    }
293
294    #[test]
295    fn distinct_lengths_produce_distinct_caches() -> TestResult<()> {
296        // The cache spec includes len.to_le_bytes(), so requesting different
297        // lengths from the same (label, variant) must produce independently
298        // derived bytes — not a prefix/extension of the longer buffer.
299        let fx = Factory::deterministic(require_ok(
300            Seed::from_env_value("entropy-len-distinct"),
301            "must parse seed",
302        )?);
303        let fixture = fx.entropy("svc");
304
305        let short = fixture.bytes(16);
306        let long = fixture.bytes(32);
307
308        assert_eq!(short.len(), 16);
309        assert_eq!(long.len(), 32);
310        assert_ne!(
311            short.as_slice(),
312            &long[..16],
313            "different lengths must hit distinct cache entries, not slice the same stream"
314        );
315        Ok(())
316    }
317
318    #[test]
319    fn cloned_fixture_handles_share_cache_identity() -> TestResult<()> {
320        let fx = Factory::deterministic(require_ok(
321            Seed::from_env_value("entropy-clone-handle"),
322            "must parse seed",
323        )?);
324        let fixture = fx.entropy("svc");
325        let cloned = fixture.clone();
326
327        assert_eq!(fixture.label(), cloned.label());
328        assert_eq!(fixture.variant(), cloned.variant());
329        assert_eq!(fixture.bytes(48), cloned.bytes(48));
330        Ok(())
331    }
332
333    #[test]
334    fn domain_constant_is_stable_for_the_lifetime_of_v1() {
335        // Changing this constant changes derived outputs. The test exists so
336        // an accidental rename is caught by CI rather than by silent fixture
337        // drift in downstream test suites.
338        assert_eq!(DOMAIN_ENTROPY_FIXTURE, "uselesskey:entropy:fixture");
339    }
340}