Skip to main content

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