315 lines
12 KiB
Rust
315 lines
12 KiB
Rust
use axum::{
|
|
body::Body,
|
|
extract::Query,
|
|
http::{header, StatusCode},
|
|
response::{Html, IntoResponse, Response, Json},
|
|
routing::get,
|
|
Router,
|
|
};
|
|
use nokhwa::{
|
|
pixel_format::RgbFormat,
|
|
utils::{CameraIndex, ControlValueDescription, ControlValueSetter, KnownCameraControl, RequestedFormat, RequestedFormatType, Resolution},
|
|
Camera,
|
|
};
|
|
use sha2::{Digest, Sha256};
|
|
use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
|
|
use serde_json::json;
|
|
|
|
// Throughput scales with resolution - at 1080p60: ~23 MB/s conditioned, ~3 Gbps raw
|
|
// With 4K60: ~93 MB/s conditioned, ~12 Gbps raw quantum noise
|
|
const MAX_BYTES_PER_REQUEST: usize = 1024 * 1024; // 1MB per request (high-res enables this)
|
|
const CHUNK_SIZE: usize = 256; // Bytes of LSB data per hash (8:1 conditioning ratio)
|
|
const MAX_CONCURRENT: usize = 4;
|
|
const DEFAULT_PORT: u16 = 8787;
|
|
|
|
static ACTIVE_REQUESTS: AtomicUsize = AtomicUsize::new(0);
|
|
static REQUEST_COUNTER: AtomicU64 = AtomicU64::new(0);
|
|
|
|
fn is_fake_camera() -> bool {
|
|
std::env::var("FAKE_CAMERA")
|
|
.map(|v| v == "1" || v.to_lowercase() == "true")
|
|
.unwrap_or(false)
|
|
}
|
|
|
|
/// Get requested resolution from environment, defaulting to 1080p for high throughput
|
|
fn get_resolution() -> (u32, u32) {
|
|
let width = std::env::var("CAMERA_WIDTH")
|
|
.ok()
|
|
.and_then(|w| w.parse().ok())
|
|
.unwrap_or(1920);
|
|
let height = std::env::var("CAMERA_HEIGHT")
|
|
.ok()
|
|
.and_then(|h| h.parse().ok())
|
|
.unwrap_or(1080);
|
|
(width, height)
|
|
}
|
|
|
|
#[derive(serde::Deserialize)]
|
|
struct RandomQuery {
|
|
#[serde(default = "default_bytes")]
|
|
bytes: usize,
|
|
#[serde(default)]
|
|
hex: bool,
|
|
}
|
|
|
|
fn default_bytes() -> usize { 32 }
|
|
|
|
#[tokio::main]
|
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
let port = std::env::var("PORT").ok().and_then(|p| p.parse().ok()).unwrap_or(DEFAULT_PORT);
|
|
let (width, height) = get_resolution();
|
|
|
|
if is_fake_camera() {
|
|
println!("FAKE_CAMERA mode enabled - using /dev/urandom for entropy");
|
|
} else {
|
|
println!("Testing camera access...");
|
|
match test_camera(width, height) {
|
|
Ok((actual_w, actual_h, frame_size)) => {
|
|
let conditioned_per_frame = (frame_size / CHUNK_SIZE) * 32;
|
|
let throughput_30fps = conditioned_per_frame * 30;
|
|
let raw_gbps = (frame_size as f64 * 30.0 * 8.0) / 1_000_000_000.0;
|
|
println!("Camera OK at {}x{} - {} bytes/frame", actual_w, actual_h, frame_size);
|
|
println!("Raw throughput: {:.1} Gbps at 30fps", raw_gbps);
|
|
println!("Conditioned output: ~{} MB/s at 30fps (8:1 ratio)", throughput_30fps / 1_000_000);
|
|
println!("Ensure lens is covered for optimal quantum noise capture");
|
|
}
|
|
Err(e) => {
|
|
eprintln!("Camera error: {}. Server will still start.", e);
|
|
}
|
|
}
|
|
}
|
|
|
|
let app = Router::new()
|
|
.route("/", get(index))
|
|
.route("/random", get(get_random))
|
|
.route("/health", get(health))
|
|
.route("/.well-known/mcp.json", get(mcp_wellknown));
|
|
|
|
let addr = format!("0.0.0.0:{}", port);
|
|
println!("Camera QRNG (LavaRnd-style) on http://{}", addr);
|
|
let listener = tokio::net::TcpListener::bind(&addr).await?;
|
|
axum::serve(listener, app).await?;
|
|
Ok(())
|
|
}
|
|
|
|
async fn index() -> Html<&'static str> { Html(INDEX_HTML) }
|
|
async fn health() -> &'static str { "ok" }
|
|
|
|
async fn mcp_wellknown() -> Json<serde_json::Value> {
|
|
Json(json!({
|
|
"mcp": {
|
|
"spec_version": "2026-01-21",
|
|
"status": "active",
|
|
"servers": [],
|
|
"tools": [{
|
|
"name": "camera-qrng",
|
|
"description": "High-throughput quantum RNG using thermal noise from covered camera sensor - Gbps of raw quantum entropy",
|
|
"url_template": "{origin}/random?bytes={bytes}&hex={hex}",
|
|
"capabilities": ["random-generation", "entropy-source", "quantum"],
|
|
"auth": { "type": "none" },
|
|
"parameters": {
|
|
"bytes": { "type": "integer", "default": 32, "max": 1048576, "description": "Number of random bytes (up to 1MB)" },
|
|
"hex": { "type": "boolean", "default": false, "description": "Return hex-encoded string" }
|
|
}
|
|
}]
|
|
}
|
|
}))
|
|
}
|
|
|
|
/// Test camera and return (width, height, frame_size) on success
|
|
fn test_camera(req_width: u32, req_height: u32) -> Result<(u32, u32, usize), String> {
|
|
let index = CameraIndex::Index(0);
|
|
let resolution = Resolution::new(req_width, req_height);
|
|
let format = RequestedFormat::new::<RgbFormat>(RequestedFormatType::HighestResolution(resolution));
|
|
let mut camera = Camera::new(index, format).map_err(|e| e.to_string())?;
|
|
camera.open_stream().map_err(|e| e.to_string())?;
|
|
let frame = camera.frame().map_err(|e| e.to_string())?;
|
|
let res = camera.resolution();
|
|
let frame_size = frame.buffer().len();
|
|
camera.stop_stream().ok();
|
|
Ok((res.width(), res.height(), frame_size))
|
|
}
|
|
|
|
/// Extract maximum value from a ControlValueDescription if it's an integer range
|
|
fn get_max_int(desc: &ControlValueDescription) -> Option<i64> {
|
|
match desc {
|
|
ControlValueDescription::IntegerRange { max, .. } => Some(*max),
|
|
ControlValueDescription::Integer { value, .. } => Some(*value),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
/// Configure camera for optimal quantum noise capture (LavaRnd approach).
|
|
/// Maximizes gain and brightness to amplify dark current and thermal noise.
|
|
fn configure_for_thermal_noise(camera: &mut Camera) {
|
|
// Maximize gain to amplify thermal/quantum noise
|
|
if let Ok(ctrl) = camera.camera_control(KnownCameraControl::Gain) {
|
|
if let Some(max) = get_max_int(ctrl.description()) {
|
|
let _ = camera.set_camera_control(
|
|
KnownCameraControl::Gain,
|
|
ControlValueSetter::Integer(max),
|
|
);
|
|
}
|
|
}
|
|
|
|
// Maximize brightness
|
|
if let Ok(ctrl) = camera.camera_control(KnownCameraControl::Brightness) {
|
|
if let Some(max) = get_max_int(ctrl.description()) {
|
|
let _ = camera.set_camera_control(
|
|
KnownCameraControl::Brightness,
|
|
ControlValueSetter::Integer(max),
|
|
);
|
|
}
|
|
}
|
|
|
|
// Set exposure to maximum if available (longer exposure = more thermal noise accumulation)
|
|
if let Ok(ctrl) = camera.camera_control(KnownCameraControl::Exposure) {
|
|
if let Some(max) = get_max_int(ctrl.description()) {
|
|
let _ = camera.set_camera_control(
|
|
KnownCameraControl::Exposure,
|
|
ControlValueSetter::Integer(max),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
async fn get_random(Query(params): Query<RandomQuery>) -> Response {
|
|
let current = ACTIVE_REQUESTS.fetch_add(1, Ordering::SeqCst);
|
|
if current >= MAX_CONCURRENT {
|
|
ACTIVE_REQUESTS.fetch_sub(1, Ordering::SeqCst);
|
|
return (StatusCode::TOO_MANY_REQUESTS, "Too many requests").into_response();
|
|
}
|
|
let bytes = params.bytes.min(MAX_BYTES_PER_REQUEST);
|
|
if bytes == 0 {
|
|
ACTIVE_REQUESTS.fetch_sub(1, Ordering::SeqCst);
|
|
return (StatusCode::BAD_REQUEST, "bytes must be > 0").into_response();
|
|
}
|
|
let request_id = REQUEST_COUNTER.fetch_add(1, Ordering::SeqCst);
|
|
let use_fake = is_fake_camera();
|
|
|
|
let result = tokio::task::spawn_blocking(move || {
|
|
if use_fake {
|
|
extract_entropy_fake(bytes, request_id)
|
|
} else {
|
|
extract_entropy_camera(bytes, request_id)
|
|
}
|
|
}).await;
|
|
ACTIVE_REQUESTS.fetch_sub(1, Ordering::SeqCst);
|
|
|
|
match result {
|
|
Ok(Ok(data)) => {
|
|
if params.hex {
|
|
Response::builder().header(header::CONTENT_TYPE, "text/plain")
|
|
.body(Body::from(hex::encode(&data))).unwrap()
|
|
} else {
|
|
Response::builder().header(header::CONTENT_TYPE, "application/octet-stream")
|
|
.body(Body::from(data)).unwrap()
|
|
}
|
|
}
|
|
Ok(Err(e)) => (StatusCode::INTERNAL_SERVER_ERROR, e).into_response(),
|
|
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
|
}
|
|
}
|
|
|
|
/// Fake entropy source using /dev/urandom - for testing without camera hardware.
|
|
/// Simulates high-resolution camera frames for realistic throughput testing.
|
|
fn extract_entropy_fake(num_bytes: usize, request_id: u64) -> Result<Vec<u8>, String> {
|
|
use std::io::Read;
|
|
|
|
let (width, height) = get_resolution();
|
|
let frame_size = (width * height * 3) as usize;
|
|
|
|
let mut entropy = Vec::with_capacity(num_bytes);
|
|
let mut hasher = Sha256::new();
|
|
|
|
let mut urandom = std::fs::File::open("/dev/urandom").map_err(|e| e.to_string())?;
|
|
let mut fake_frame = vec![0u8; frame_size];
|
|
|
|
let mut frame_idx: u64 = 0;
|
|
while entropy.len() < num_bytes {
|
|
urandom.read_exact(&mut fake_frame).map_err(|e| e.to_string())?;
|
|
|
|
// Extract LSBs (2 bits per byte - highest entropy density)
|
|
let lsbs: Vec<u8> = fake_frame.iter().map(|b| b & 0x03).collect();
|
|
|
|
// Hash in chunks - each CHUNK_SIZE bytes of LSBs produces 32 bytes output
|
|
for (chunk_idx, chunk) in lsbs.chunks(CHUNK_SIZE).enumerate() {
|
|
hasher.update(chunk);
|
|
hasher.update(&request_id.to_le_bytes());
|
|
hasher.update(&frame_idx.to_le_bytes());
|
|
hasher.update(&(chunk_idx as u64).to_le_bytes());
|
|
hasher.update(&nanos_now().to_le_bytes());
|
|
|
|
entropy.extend_from_slice(&hasher.finalize_reset());
|
|
|
|
if entropy.len() >= num_bytes {
|
|
break;
|
|
}
|
|
}
|
|
frame_idx += 1;
|
|
}
|
|
|
|
entropy.truncate(num_bytes);
|
|
Ok(entropy)
|
|
}
|
|
|
|
/// Extract entropy from camera quantum noise using chunked SHA-256 conditioning.
|
|
///
|
|
/// Throughput scales with camera resolution:
|
|
/// - 640x480 @ 30fps: ~27 MB/s raw (~216 Mbps), ~3.4 MB/s conditioned
|
|
/// - 1080p @ 30fps: ~186 MB/s raw (~1.5 Gbps), ~23 MB/s conditioned
|
|
/// - 1080p @ 60fps: ~373 MB/s raw (~3 Gbps), ~47 MB/s conditioned
|
|
/// - 4K @ 30fps: ~746 MB/s raw (~6 Gbps), ~93 MB/s conditioned
|
|
/// - 4K @ 60fps: ~1.49 GB/s raw (~12 Gbps), ~186 MB/s conditioned
|
|
fn extract_entropy_camera(num_bytes: usize, request_id: u64) -> Result<Vec<u8>, String> {
|
|
let (req_width, req_height) = get_resolution();
|
|
let index = CameraIndex::Index(0);
|
|
let resolution = Resolution::new(req_width, req_height);
|
|
let format = RequestedFormat::new::<RgbFormat>(RequestedFormatType::HighestResolution(resolution));
|
|
let mut camera = Camera::new(index, format).map_err(|e| e.to_string())?;
|
|
camera.open_stream().map_err(|e| e.to_string())?;
|
|
|
|
// Configure camera for quantum noise capture (high gain, max brightness)
|
|
configure_for_thermal_noise(&mut camera);
|
|
|
|
let mut entropy = Vec::with_capacity(num_bytes);
|
|
let mut hasher = Sha256::new();
|
|
|
|
let mut frame_idx: u64 = 0;
|
|
while entropy.len() < num_bytes {
|
|
let frame = camera.frame().map_err(|e| e.to_string())?;
|
|
let raw = frame.buffer();
|
|
|
|
// Extract LSBs (2 bits per byte - highest entropy density in quantum noise)
|
|
let lsbs: Vec<u8> = raw.iter().map(|b| b & 0x03).collect();
|
|
|
|
// Hash in chunks - each CHUNK_SIZE bytes produces 32 bytes conditioned output
|
|
// At 1080p: ~24,300 chunks/frame = ~778 KB conditioned per frame
|
|
// At 4K: ~97,200 chunks/frame = ~3.1 MB conditioned per frame
|
|
for (chunk_idx, chunk) in lsbs.chunks(CHUNK_SIZE).enumerate() {
|
|
hasher.update(chunk);
|
|
hasher.update(&request_id.to_le_bytes());
|
|
hasher.update(&frame_idx.to_le_bytes());
|
|
hasher.update(&(chunk_idx as u64).to_le_bytes());
|
|
hasher.update(&nanos_now().to_le_bytes());
|
|
|
|
entropy.extend_from_slice(&hasher.finalize_reset());
|
|
|
|
if entropy.len() >= num_bytes {
|
|
break;
|
|
}
|
|
}
|
|
frame_idx += 1;
|
|
}
|
|
|
|
camera.stop_stream().ok();
|
|
entropy.truncate(num_bytes);
|
|
Ok(entropy)
|
|
}
|
|
|
|
fn nanos_now() -> u128 {
|
|
std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_nanos()
|
|
}
|
|
|
|
const INDEX_HTML: &str = include_str!("index.html");
|