Skip to main content

openentropy_core/sources/
disk.rs

1//! DiskIOSource — NVMe/SSD read latency jitter.
2//!
3//! Creates a temporary 64KB file, performs random seeks and 4KB reads,
4//! and extracts LSBs of nanosecond timing deltas as entropy.
5//!
6//! **Raw output characteristics:** LSBs of inter-read timing deltas.
7//! Use SHA-256 conditioning for uniform output.
8
9use std::io::{Read, Seek, SeekFrom, Write};
10use std::time::Instant;
11
12use tempfile::NamedTempFile;
13
14use crate::source::{EntropySource, Platform, SourceCategory, SourceInfo};
15
16/// Size of the temporary file used for random reads.
17const TEMP_FILE_SIZE: usize = 64 * 1024; // 64 KB
18
19/// Size of each random read operation.
20const READ_BLOCK_SIZE: usize = 4 * 1024; // 4 KB
21
22static DISK_IO_INFO: SourceInfo = SourceInfo {
23    name: "disk_io",
24    description: "NVMe/SSD read latency jitter from random 4KB reads",
25    physics: "Measures NVMe/SSD read latency for small random reads. Jitter sources: \
26              flash translation layer (FTL) remapping, wear leveling, garbage collection, \
27              read disturb mitigation, NAND page read latency variation (depends on charge \
28              level in floating-gate transistors), and NVMe controller queue arbitration.",
29    category: SourceCategory::IO,
30    platform: Platform::Any,
31    requirements: &[],
32    entropy_rate_estimate: 800.0,
33    composite: false,
34};
35
36/// Entropy source that harvests timing jitter from NVMe/SSD random reads.
37pub struct DiskIOSource;
38
39impl EntropySource for DiskIOSource {
40    fn info(&self) -> &SourceInfo {
41        &DISK_IO_INFO
42    }
43
44    fn is_available(&self) -> bool {
45        true
46    }
47
48    fn collect(&self, n_samples: usize) -> Vec<u8> {
49        // Create a temporary 64KB file filled with varied data.
50        let mut tmpfile = match NamedTempFile::new() {
51            Ok(f) => f,
52            Err(_) => return Vec::new(),
53        };
54
55        let mut fill_data = vec![0u8; TEMP_FILE_SIZE];
56        let mut lcg: u64 = 0xCAFE_BABE_DEAD_BEEF;
57        for chunk in fill_data.chunks_mut(8) {
58            lcg = lcg.wrapping_mul(6364136223846793005).wrapping_add(1);
59            let bytes = lcg.to_le_bytes();
60            for (i, b) in chunk.iter_mut().enumerate() {
61                *b = bytes[i % 8];
62            }
63        }
64        if tmpfile.write_all(&fill_data).is_err() {
65            return Vec::new();
66        }
67        if tmpfile.flush().is_err() {
68            return Vec::new();
69        }
70
71        let mut raw = Vec::with_capacity(n_samples);
72        let mut read_buf = vec![0u8; READ_BLOCK_SIZE];
73        let max_offset = TEMP_FILE_SIZE.saturating_sub(READ_BLOCK_SIZE);
74
75        let seed = std::time::SystemTime::now()
76            .duration_since(std::time::UNIX_EPOCH)
77            .unwrap_or_default()
78            .as_nanos() as u64;
79        let mut lcg_state = seed | 1;
80
81        let mut prev_ns: u64 = 0;
82
83        // Oversample to ensure enough raw data.
84        let num_reads = n_samples * 2 + 64;
85
86        for i in 0..num_reads {
87            lcg_state = lcg_state
88                .wrapping_mul(6364136223846793005)
89                .wrapping_add(1442695040888963407);
90            let offset = (lcg_state as usize) % (max_offset + 1);
91
92            let t0 = Instant::now();
93            let _ = tmpfile.seek(SeekFrom::Start(offset as u64));
94            let _ = tmpfile.read(&mut read_buf);
95            let elapsed_ns = t0.elapsed().as_nanos() as u64;
96
97            if i > 0 {
98                let delta = elapsed_ns.wrapping_sub(prev_ns);
99                // Extract lowest byte of delta — raw, unconditioned
100                raw.push(delta as u8);
101            }
102
103            prev_ns = elapsed_ns;
104
105            if raw.len() >= n_samples {
106                break;
107            }
108        }
109
110        raw.truncate(n_samples);
111        raw
112    }
113}
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118
119    #[test]
120    #[ignore] // Run with: cargo test -- --ignored
121    fn disk_io_collects_bytes() {
122        let src = DiskIOSource;
123        assert!(src.is_available());
124        let data = src.collect(128);
125        assert!(!data.is_empty());
126    }
127
128    #[test]
129    fn disk_io_info() {
130        let src = DiskIOSource;
131        assert_eq!(src.name(), "disk_io");
132        assert_eq!(src.info().category, SourceCategory::IO);
133    }
134}