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