1use std::sync::{Arc, Mutex, OnceLock};
22
23use crate::source::{EntropySource, Platform, Requirement, SourceCategory, SourceInfo};
24
25pub static QCICADA_CLI_MODE: OnceLock<String> = OnceLock::new();
29
30static QCICADA_INFO: SourceInfo = SourceInfo {
31 name: "qcicada",
32 description: "Crypta Labs QCicada USB QRNG \u{2014} quantum shot noise",
33 physics: "Photonic shot noise from an LED/photodiode pair inside the QCicada USB device. \
34 Photon emission and detection are inherently quantum processes governed by Poisson \
35 statistics. The device digitises photodiode current fluctuations to produce true \
36 quantum random numbers at full entropy (8 bits/byte) per NIST SP 800-90B.",
37 category: SourceCategory::Quantum,
38 platform: Platform::Any,
39 requirements: &[Requirement::QCicada],
40 entropy_rate_estimate: 8.0,
41 composite: false,
42 is_fast: false, };
44
45trait QCicadaDevice: Send {
46 fn set_postprocess(&mut self, mode: qcicada::PostProcess) -> Result<(), qcicada::QCicadaError>;
47 fn start_continuous_fresh(&mut self) -> Result<(), qcicada::QCicadaError>;
48 fn read_continuous(&mut self, n: usize) -> Result<Vec<u8>, qcicada::QCicadaError>;
49 fn stop(&mut self) -> Result<(), qcicada::QCicadaError>;
50}
51
52impl QCicadaDevice for qcicada::QCicada {
53 fn set_postprocess(&mut self, mode: qcicada::PostProcess) -> Result<(), qcicada::QCicadaError> {
54 Self::set_postprocess(self, mode)
55 }
56
57 fn start_continuous_fresh(&mut self) -> Result<(), qcicada::QCicadaError> {
58 Self::start_continuous_fresh(self).map(|_| ())
59 }
60
61 fn read_continuous(&mut self, n: usize) -> Result<Vec<u8>, qcicada::QCicadaError> {
62 Self::read_continuous(self, n)
63 }
64
65 fn stop(&mut self) -> Result<(), qcicada::QCicadaError> {
66 Self::stop(self)
67 }
68}
69
70type DeviceHandle = Box<dyn QCicadaDevice>;
71type DeviceOpener =
72 dyn Fn(&QCicadaConfig, qcicada::PostProcess) -> Option<DeviceHandle> + Send + Sync;
73
74fn configure_device(device: &mut impl QCicadaDevice, mode: qcicada::PostProcess) -> Option<()> {
75 let _ = device.set_postprocess(mode);
78 device.start_continuous_fresh().ok()?;
79 Some(())
80}
81
82fn default_device_opener(
83 config: &QCicadaConfig,
84 mode: qcicada::PostProcess,
85) -> Option<DeviceHandle> {
86 let timeout = std::time::Duration::from_millis(config.timeout_ms);
87 let port_str = config.port.as_deref();
88 let mut qrng = match qcicada::QCicada::open(port_str, Some(timeout)) {
89 Ok(q) => q,
90 Err(_) => return None,
91 };
92
93 configure_device(&mut qrng, mode)?;
94
95 Some(Box::new(qrng))
96}
97
98pub struct QCicadaConfig {
100 pub port: Option<String>,
102 pub timeout_ms: u64,
104 pub post_process: String,
106}
107
108impl Default for QCicadaConfig {
109 fn default() -> Self {
110 let port = std::env::var("QCICADA_PORT").ok();
111 let timeout_ms = std::env::var("QCICADA_TIMEOUT")
112 .ok()
113 .and_then(|s| s.parse().ok())
114 .unwrap_or(5000);
115 let post_process = QCICADA_CLI_MODE
116 .get()
117 .cloned()
118 .or_else(|| std::env::var("QCICADA_MODE").ok())
119 .or_else(|| std::env::var("QCICADA_POST_PROCESS").ok())
120 .unwrap_or_else(|| "raw".into());
121 Self {
122 port,
123 timeout_ms,
124 post_process,
125 }
126 }
127}
128
129pub struct QCicadaSource {
131 pub config: QCicadaConfig,
132 device: Mutex<Option<DeviceHandle>>,
133 available: Mutex<Option<bool>>,
134 mode: Mutex<String>,
136 opener: Arc<DeviceOpener>,
137}
138
139impl Default for QCicadaSource {
140 fn default() -> Self {
141 let config = QCicadaConfig::default();
142 let mode = config.post_process.clone();
143 Self {
144 config,
145 device: Mutex::new(None),
146 available: Mutex::new(None),
147 mode: Mutex::new(mode),
148 opener: Arc::new(default_device_opener),
149 }
150 }
151}
152
153impl QCicadaSource {
154 #[cfg(test)]
155 fn with_opener(config: QCicadaConfig, opener: Arc<DeviceOpener>) -> Self {
156 let mode = config.post_process.clone();
157 Self {
158 config,
159 device: Mutex::new(None),
160 available: Mutex::new(None),
161 mode: Mutex::new(mode),
162 opener,
163 }
164 }
165
166 fn post_process_mode(&self) -> qcicada::PostProcess {
168 let mode = self.mode.lock().unwrap_or_else(|e| e.into_inner());
169 match mode.as_str() {
170 "sha256" => qcicada::PostProcess::Sha256,
171 "samples" => qcicada::PostProcess::RawSamples,
172 _ => qcicada::PostProcess::RawNoise,
173 }
174 }
175
176 fn stop_device(device: &mut Option<DeviceHandle>) {
177 if let Some(qrng) = device.as_mut() {
178 let _ = qrng.stop();
179 }
180 *device = None;
181 }
182
183 fn try_open(&self) -> Option<DeviceHandle> {
188 (self.opener)(&self.config, self.post_process_mode())
189 }
190}
191
192impl Drop for QCicadaSource {
193 fn drop(&mut self) {
194 let device = self.device.get_mut().unwrap_or_else(|e| e.into_inner());
195 Self::stop_device(device);
196 }
197}
198
199impl EntropySource for QCicadaSource {
200 fn info(&self) -> &SourceInfo {
201 &QCICADA_INFO
202 }
203
204 fn is_available(&self) -> bool {
205 let mut cached = self.available.lock().unwrap_or_else(|e| e.into_inner());
206 if *cached == Some(true) {
209 return true;
210 }
211 let avail = !qcicada::discover_devices().is_empty();
212 if avail {
213 *cached = Some(true);
214 }
215 avail
216 }
217
218 fn collect(&self, n_samples: usize) -> Vec<u8> {
219 const CHUNK_SIZE: usize = 8192;
223
224 let mut guard = self.device.lock().unwrap_or_else(|e| e.into_inner());
225
226 if guard.is_none() {
228 *guard = self.try_open();
229 if guard.is_none() {
230 std::thread::sleep(std::time::Duration::from_millis(500));
232 *guard = self.try_open();
233 }
234 }
235
236 if guard.is_none() {
237 return Vec::new();
238 }
239
240 let mut result = Vec::with_capacity(n_samples);
241 let mut remaining = n_samples;
242
243 while remaining > 0 {
244 let chunk = remaining.min(CHUNK_SIZE);
245 let read_result = guard.as_mut().unwrap().read_continuous(chunk);
246 match read_result {
247 Ok(bytes) => {
248 if bytes.is_empty() {
249 break;
250 }
251 remaining -= bytes.len();
252 result.extend_from_slice(&bytes);
253 }
254 Err(_) => {
255 Self::stop_device(&mut guard);
257 std::thread::sleep(std::time::Duration::from_millis(300));
258 *guard = self.try_open();
259 match guard.as_mut().map(|q| q.read_continuous(chunk)) {
260 Some(Ok(bytes)) => {
261 if bytes.is_empty() {
262 break;
263 }
264 remaining -= bytes.len();
265 result.extend_from_slice(&bytes);
266 }
267 _ => break,
268 }
269 }
270 }
271 }
272
273 result
274 }
275
276 fn set_config(&self, key: &str, value: &str) -> Result<(), String> {
277 if key != "mode" {
278 return Err(format!("unknown config key: {key}"));
279 }
280 match value {
281 "raw" | "sha256" | "samples" => {}
282 _ => {
283 return Err(format!(
284 "invalid mode: {value} (expected raw|sha256|samples)"
285 ));
286 }
287 }
288 *self.mode.lock().unwrap_or_else(|e| e.into_inner()) = value.to_string();
289
290 Self::stop_device(&mut self.device.lock().unwrap_or_else(|e| e.into_inner()));
293 Ok(())
294 }
295
296 fn config_options(&self) -> Vec<(&'static str, String)> {
297 vec![(
298 "mode",
299 self.mode.lock().unwrap_or_else(|e| e.into_inner()).clone(),
300 )]
301 }
302}
303
304#[cfg(test)]
305mod tests {
306 use super::*;
307 use std::collections::VecDeque;
308 use std::sync::atomic::{AtomicUsize, Ordering};
309
310 #[derive(Default)]
311 struct FakeDeviceState {
312 set_postprocess_calls: Vec<qcicada::PostProcess>,
313 start_continuous_fresh_calls: usize,
314 read_requests: Vec<usize>,
315 stop_calls: usize,
316 }
317
318 struct FakeDevice {
319 state: Arc<Mutex<FakeDeviceState>>,
320 scripted_reads: VecDeque<Result<Vec<u8>, qcicada::QCicadaError>>,
321 }
322
323 impl QCicadaDevice for FakeDevice {
324 fn set_postprocess(
325 &mut self,
326 mode: qcicada::PostProcess,
327 ) -> Result<(), qcicada::QCicadaError> {
328 self.state
329 .lock()
330 .unwrap_or_else(|e| e.into_inner())
331 .set_postprocess_calls
332 .push(mode);
333 Ok(())
334 }
335
336 fn start_continuous_fresh(&mut self) -> Result<(), qcicada::QCicadaError> {
337 self.state
338 .lock()
339 .unwrap_or_else(|e| e.into_inner())
340 .start_continuous_fresh_calls += 1;
341 Ok(())
342 }
343
344 fn read_continuous(&mut self, n: usize) -> Result<Vec<u8>, qcicada::QCicadaError> {
345 self.state
346 .lock()
347 .unwrap_or_else(|e| e.into_inner())
348 .read_requests
349 .push(n);
350 self.scripted_reads
351 .pop_front()
352 .unwrap_or_else(|| Ok(vec![0; n]))
353 }
354
355 fn stop(&mut self) -> Result<(), qcicada::QCicadaError> {
356 self.state
357 .lock()
358 .unwrap_or_else(|e| e.into_inner())
359 .stop_calls += 1;
360 Ok(())
361 }
362 }
363
364 fn test_config(mode: &str) -> QCicadaConfig {
365 QCicadaConfig {
366 port: None,
367 timeout_ms: 5000,
368 post_process: mode.into(),
369 }
370 }
371
372 fn make_test_source(
373 mode: &str,
374 devices: Vec<(
375 Arc<Mutex<FakeDeviceState>>,
376 VecDeque<Result<Vec<u8>, qcicada::QCicadaError>>,
377 )>,
378 opened_modes: Arc<Mutex<Vec<qcicada::PostProcess>>>,
379 open_count: Arc<AtomicUsize>,
380 ) -> QCicadaSource {
381 let scripted_devices = Arc::new(Mutex::new(VecDeque::from(devices)));
382 let opener = Arc::new(move |_config: &QCicadaConfig, mode: qcicada::PostProcess| {
383 open_count.fetch_add(1, Ordering::SeqCst);
384 opened_modes
385 .lock()
386 .unwrap_or_else(|e| e.into_inner())
387 .push(mode);
388 let (state, scripted_reads) = scripted_devices
389 .lock()
390 .unwrap_or_else(|e| e.into_inner())
391 .pop_front()?;
392 let mut device = FakeDevice {
393 state,
394 scripted_reads,
395 };
396 configure_device(&mut device, mode)?;
397 Some(Box::new(device) as DeviceHandle)
398 });
399 QCicadaSource::with_opener(test_config(mode), opener)
400 }
401
402 #[test]
403 fn info() {
404 let src = QCicadaSource::default();
405 assert_eq!(src.name(), "qcicada");
406 assert_eq!(src.info().category, SourceCategory::Quantum);
407 assert_eq!(src.info().platform, Platform::Any);
408 assert_eq!(src.info().entropy_rate_estimate, 8.0);
409 assert!(!src.info().composite);
410 assert!(!src.info().is_fast);
411 assert_eq!(src.info().requirements, &[Requirement::QCicada]);
412 }
413
414 #[test]
415 fn config_default() {
416 let config = QCicadaConfig {
417 port: None,
418 timeout_ms: 5000,
419 post_process: "raw".into(),
420 };
421 assert!(config.port.is_none());
422 assert_eq!(config.timeout_ms, 5000);
423 assert_eq!(config.post_process, "raw");
424 }
425
426 #[test]
427 fn config_explicit() {
428 let config = QCicadaConfig {
429 port: Some("/dev/ttyUSB0".into()),
430 timeout_ms: 3000,
431 post_process: "sha256".into(),
432 };
433 let src = QCicadaSource::with_opener(config, Arc::new(default_device_opener));
434 assert_eq!(src.config.port.as_deref(), Some("/dev/ttyUSB0"));
435 assert_eq!(src.config.timeout_ms, 3000);
436 assert_eq!(src.config.post_process, "sha256");
437 }
438
439 #[test]
440 fn post_process_mode_parsing() {
441 let src = |mode: &str| {
442 QCicadaSource::with_opener(test_config(mode), Arc::new(default_device_opener))
443 };
444 assert!(matches!(
445 src("sha256").post_process_mode(),
446 qcicada::PostProcess::Sha256
447 ));
448 assert!(matches!(
449 src("samples").post_process_mode(),
450 qcicada::PostProcess::RawSamples
451 ));
452 assert!(matches!(
453 src("raw").post_process_mode(),
454 qcicada::PostProcess::RawNoise
455 ));
456 assert!(matches!(
457 src("anything").post_process_mode(),
458 qcicada::PostProcess::RawNoise
459 ));
460 }
461
462 #[test]
463 fn set_config_mode() {
464 let src = QCicadaSource::default();
465 assert!(src.set_config("mode", "sha256").is_ok());
466 assert_eq!(src.config_options(), vec![("mode", "sha256".into())]);
467 assert!(src.set_config("mode", "samples").is_ok());
468 assert_eq!(src.config_options(), vec![("mode", "samples".into())]);
469 assert!(src.set_config("mode", "raw").is_ok());
470 assert_eq!(src.config_options(), vec![("mode", "raw".into())]);
471 }
472
473 #[test]
474 fn set_config_invalid() {
475 let src = QCicadaSource::default();
476 assert!(src.set_config("mode", "invalid").is_err());
477 assert!(src.set_config("unknown_key", "raw").is_err());
478 }
479
480 #[test]
481 fn source_is_send_sync() {
482 fn assert_send_sync<T: Send + Sync>() {}
483 assert_send_sync::<QCicadaSource>();
484 }
485
486 #[test]
487 fn collect_uses_continuous_reads_and_chunks_large_requests() {
488 let state = Arc::new(Mutex::new(FakeDeviceState::default()));
489 let opened_modes = Arc::new(Mutex::new(Vec::new()));
490 let open_count = Arc::new(AtomicUsize::new(0));
491 let src = make_test_source(
492 "raw",
493 vec![(
494 Arc::clone(&state),
495 VecDeque::from([Ok(vec![0xAA; 8192]), Ok(vec![0xBB; 808])]),
496 )],
497 Arc::clone(&opened_modes),
498 Arc::clone(&open_count),
499 );
500
501 let data = src.collect(9000);
502 let state = state.lock().unwrap_or_else(|e| e.into_inner());
503
504 assert_eq!(data.len(), 9000);
505 assert_eq!(open_count.load(Ordering::SeqCst), 1);
506 assert_eq!(
507 *opened_modes.lock().unwrap_or_else(|e| e.into_inner()),
508 vec![qcicada::PostProcess::RawNoise]
509 );
510 assert_eq!(
511 state.set_postprocess_calls,
512 vec![qcicada::PostProcess::RawNoise]
513 );
514 assert_eq!(state.start_continuous_fresh_calls, 1);
515 assert_eq!(state.read_requests, vec![8192, 808]);
516 assert_eq!(state.stop_calls, 0);
517 }
518
519 #[test]
520 fn collect_reconnects_and_restarts_continuous_after_read_error() {
521 let first_state = Arc::new(Mutex::new(FakeDeviceState::default()));
522 let second_state = Arc::new(Mutex::new(FakeDeviceState::default()));
523 let opened_modes = Arc::new(Mutex::new(Vec::new()));
524 let open_count = Arc::new(AtomicUsize::new(0));
525 let src = make_test_source(
526 "raw",
527 vec![
528 (
529 Arc::clone(&first_state),
530 VecDeque::from([Err(qcicada::QCicadaError::Protocol(
531 "simulated read failure".into(),
532 ))]),
533 ),
534 (
535 Arc::clone(&second_state),
536 VecDeque::from([Ok(vec![0x5A; 64])]),
537 ),
538 ],
539 Arc::clone(&opened_modes),
540 Arc::clone(&open_count),
541 );
542
543 let data = src.collect(64);
544 let first_state = first_state.lock().unwrap_or_else(|e| e.into_inner());
545 let second_state = second_state.lock().unwrap_or_else(|e| e.into_inner());
546
547 assert_eq!(data, vec![0x5A; 64]);
548 assert_eq!(open_count.load(Ordering::SeqCst), 2);
549 assert_eq!(
550 *opened_modes.lock().unwrap_or_else(|e| e.into_inner()),
551 vec![
552 qcicada::PostProcess::RawNoise,
553 qcicada::PostProcess::RawNoise
554 ]
555 );
556 assert_eq!(first_state.start_continuous_fresh_calls, 1);
557 assert_eq!(first_state.read_requests, vec![64]);
558 assert_eq!(first_state.stop_calls, 1);
559 assert_eq!(second_state.start_continuous_fresh_calls, 1);
560 assert_eq!(second_state.read_requests, vec![64]);
561 }
562
563 #[test]
564 fn set_config_stops_active_device_and_reopens_with_new_mode() {
565 let first_state = Arc::new(Mutex::new(FakeDeviceState::default()));
566 let second_state = Arc::new(Mutex::new(FakeDeviceState::default()));
567 let opened_modes = Arc::new(Mutex::new(Vec::new()));
568 let open_count = Arc::new(AtomicUsize::new(0));
569 let src = make_test_source(
570 "raw",
571 vec![
572 (
573 Arc::clone(&first_state),
574 VecDeque::from([Ok(vec![0x11; 4])]),
575 ),
576 (
577 Arc::clone(&second_state),
578 VecDeque::from([Ok(vec![0x22; 4])]),
579 ),
580 ],
581 Arc::clone(&opened_modes),
582 Arc::clone(&open_count),
583 );
584
585 assert_eq!(src.collect(4), vec![0x11; 4]);
586 assert!(src.set_config("mode", "sha256").is_ok());
587 assert!(
588 src.device
589 .lock()
590 .unwrap_or_else(|e| e.into_inner())
591 .is_none()
592 );
593 assert_eq!(src.collect(4), vec![0x22; 4]);
594
595 let first_state = first_state.lock().unwrap_or_else(|e| e.into_inner());
596 let second_state = second_state.lock().unwrap_or_else(|e| e.into_inner());
597 assert_eq!(first_state.stop_calls, 1);
598 assert_eq!(
599 *opened_modes.lock().unwrap_or_else(|e| e.into_inner()),
600 vec![qcicada::PostProcess::RawNoise, qcicada::PostProcess::Sha256]
601 );
602 assert_eq!(
603 second_state.set_postprocess_calls,
604 vec![qcicada::PostProcess::Sha256]
605 );
606 assert_eq!(open_count.load(Ordering::SeqCst), 2);
607 }
608
609 #[test]
610 fn drop_stops_active_device() {
611 let state = Arc::new(Mutex::new(FakeDeviceState::default()));
612 let opened_modes = Arc::new(Mutex::new(Vec::new()));
613 let open_count = Arc::new(AtomicUsize::new(0));
614 let src = make_test_source(
615 "raw",
616 vec![(Arc::clone(&state), VecDeque::from([Ok(vec![0x33; 8])]))],
617 opened_modes,
618 open_count,
619 );
620
621 assert_eq!(src.collect(8), vec![0x33; 8]);
622 drop(src);
623
624 let state = state.lock().unwrap_or_else(|e| e.into_inner());
625 assert_eq!(state.stop_calls, 1);
626 }
627
628 #[test]
629 #[ignore] fn collects_quantum_bytes() {
631 let src = QCicadaSource::default();
632 if src.is_available() {
633 let data = src.collect(64);
634 assert!(!data.is_empty());
635 assert!(data.len() <= 64);
636 }
637 }
638}