Initial commit: Camera TRNG project
Rust project for true random number generation using camera input.
This commit is contained in:
commit
1b7e21a8a0
|
|
@ -0,0 +1 @@
|
||||||
|
/target
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,25 @@
|
||||||
|
[package]
|
||||||
|
name = "camera-trng"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
description = "True random number generator using camera sensor noise"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
# Camera capture - cross platform
|
||||||
|
nokhwa = { version = "0.10", features = ["input-native"] }
|
||||||
|
|
||||||
|
# Lightweight HTTP server
|
||||||
|
axum = "0.7"
|
||||||
|
tokio = { version = "1", features = ["rt-multi-thread", "macros", "sync"] }
|
||||||
|
|
||||||
|
# For extracting entropy
|
||||||
|
sha2 = "0.10"
|
||||||
|
hex = "0.4"
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
opt-level = "z"
|
||||||
|
lto = true
|
||||||
|
codegen-units = 1
|
||||||
|
strip = true
|
||||||
|
panic = "abort"
|
||||||
|
|
@ -0,0 +1,72 @@
|
||||||
|
# Camera TRNG
|
||||||
|
|
||||||
|
A minimal cross-platform true random number generator that extracts entropy from camera sensor noise.
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
Camera sensors exhibit thermal noise and shot noise at the pixel level. This noise is most concentrated in the least significant bits (LSBs) of each pixel value. This service:
|
||||||
|
|
||||||
|
1. Captures frames from the default camera at the lowest resolution
|
||||||
|
2. Extracts the 2 LSBs from each pixel (where thermal/shot noise dominates)
|
||||||
|
3. Hashes the LSBs with SHA-256 to whiten the data and remove any bias
|
||||||
|
4. Mixes in timing entropy for additional randomness
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo build --release
|
||||||
|
```
|
||||||
|
|
||||||
|
## Run
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./target/release/camera-trng
|
||||||
|
# Or set a custom port
|
||||||
|
PORT=9000 ./target/release/camera-trng
|
||||||
|
```
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
### GET /random
|
||||||
|
|
||||||
|
Returns random bytes from camera noise.
|
||||||
|
|
||||||
|
**Query Parameters:**
|
||||||
|
- `bytes` - Number of bytes to return (default: 32, max: 1024)
|
||||||
|
- `hex` - Return as hex string instead of raw bytes (default: false)
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
```bash
|
||||||
|
# Get 32 random bytes as hex
|
||||||
|
curl "http://localhost:8787/random?hex=true"
|
||||||
|
|
||||||
|
# Get 64 raw random bytes
|
||||||
|
curl "http://localhost:8787/random?bytes=64" -o random.bin
|
||||||
|
|
||||||
|
# Get 256 bytes as hex
|
||||||
|
curl "http://localhost:8787/random?bytes=256&hex=true"
|
||||||
|
```
|
||||||
|
|
||||||
|
### GET /health
|
||||||
|
|
||||||
|
Returns `ok` if the server is running.
|
||||||
|
|
||||||
|
## Rate Limiting
|
||||||
|
|
||||||
|
- Maximum 4 concurrent requests
|
||||||
|
- Maximum 1024 bytes per request
|
||||||
|
- Returns 429 Too Many Requests when overloaded
|
||||||
|
|
||||||
|
## Cross-Platform Support
|
||||||
|
|
||||||
|
Uses `nokhwa` for camera access, supporting:
|
||||||
|
- macOS (AVFoundation)
|
||||||
|
- Windows (Media Foundation)
|
||||||
|
- Linux (V4L2)
|
||||||
|
|
||||||
|
## Security Notes
|
||||||
|
|
||||||
|
This is intended for hobby/experimental use. For cryptographic applications:
|
||||||
|
- Consider mixing with system entropy (`/dev/urandom`)
|
||||||
|
- The quality of randomness depends on camera sensor characteristics
|
||||||
|
- Environmental factors (lighting, temperature) affect noise levels
|
||||||
|
|
@ -0,0 +1,145 @@
|
||||||
|
use axum::{
|
||||||
|
body::Body,
|
||||||
|
extract::Query,
|
||||||
|
http::{header, StatusCode},
|
||||||
|
response::{IntoResponse, Response},
|
||||||
|
routing::get,
|
||||||
|
Router,
|
||||||
|
};
|
||||||
|
use nokhwa::{
|
||||||
|
pixel_format::RgbFormat,
|
||||||
|
utils::{CameraIndex, RequestedFormat, RequestedFormatType},
|
||||||
|
Camera,
|
||||||
|
};
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||||
|
|
||||||
|
const MAX_BYTES_PER_REQUEST: usize = 1024;
|
||||||
|
const MAX_CONCURRENT: usize = 4;
|
||||||
|
const DEFAULT_PORT: u16 = 8787;
|
||||||
|
|
||||||
|
static ACTIVE_REQUESTS: AtomicUsize = AtomicUsize::new(0);
|
||||||
|
|
||||||
|
#[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);
|
||||||
|
|
||||||
|
// Test camera access at startup
|
||||||
|
println!("Testing camera access...");
|
||||||
|
if let Err(e) = test_camera() {
|
||||||
|
eprintln!("Camera error: {}. Server will still start.", e);
|
||||||
|
} else {
|
||||||
|
println!("Camera OK");
|
||||||
|
}
|
||||||
|
|
||||||
|
let app = Router::new()
|
||||||
|
.route("/random", get(get_random))
|
||||||
|
.route("/health", get(health));
|
||||||
|
|
||||||
|
let addr = format!("0.0.0.0:{}", port);
|
||||||
|
println!("Camera TRNG on http://{}", addr);
|
||||||
|
println!(" GET /random?bytes=N&hex=bool (max {}B)", MAX_BYTES_PER_REQUEST);
|
||||||
|
|
||||||
|
let listener = tokio::net::TcpListener::bind(&addr).await?;
|
||||||
|
axum::serve(listener, app).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_camera() -> Result<(), String> {
|
||||||
|
let index = CameraIndex::Index(0);
|
||||||
|
let format = RequestedFormat::new::<RgbFormat>(RequestedFormatType::None);
|
||||||
|
Camera::new(index, format).map_err(|e| e.to_string())?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn health() -> &'static str { "ok" }
|
||||||
|
|
||||||
|
async fn get_random(Query(params): Query<RandomQuery>) -> Response {
|
||||||
|
// Simple rate limiting
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run camera ops in blocking task
|
||||||
|
let result = tokio::task::spawn_blocking(move || extract_entropy(bytes)).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(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extract_entropy(num_bytes: usize) -> Result<Vec<u8>, String> {
|
||||||
|
let index = CameraIndex::Index(0);
|
||||||
|
let format = RequestedFormat::new::<RgbFormat>(RequestedFormatType::None);
|
||||||
|
let mut camera = Camera::new(index, format).map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
camera.open_stream().map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
let mut entropy = Vec::with_capacity(num_bytes);
|
||||||
|
let mut hasher = Sha256::new();
|
||||||
|
let frames_needed = (num_bytes / 32) + 1;
|
||||||
|
|
||||||
|
for _ in 0..frames_needed {
|
||||||
|
let frame = camera.frame().map_err(|e| e.to_string())?;
|
||||||
|
let raw = frame.buffer();
|
||||||
|
|
||||||
|
// Extract LSBs - thermal/shot noise lives here
|
||||||
|
let lsbs: Vec<u8> = raw.iter().map(|b| b & 0x03).collect();
|
||||||
|
|
||||||
|
// Hash to whiten and remove bias
|
||||||
|
hasher.update(&lsbs);
|
||||||
|
hasher.update(&nanos_now().to_le_bytes());
|
||||||
|
|
||||||
|
let hash = hasher.finalize_reset();
|
||||||
|
entropy.extend_from_slice(&hash);
|
||||||
|
|
||||||
|
if entropy.len() >= num_bytes { break; }
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue