Polish: camera-only entropy, cleaner output, complete docs
- Remove CSPRNG fallback: pool now returns errors when camera entropy is insufficient instead of silently falling back to ChaCha20. Random data is only from camera input or not at all. - Fix throughput display: show Mbps/KB/s for smaller frame sizes, Gbps/MB/s for larger ones (actual frame size varies by pixel format). - Add RESEARCH.md note about pixel format impact on throughput. - Complete README API table with all endpoints (dice, password, coin, 8ball, cameras, docs, MCP). - Add docker-compose device mapping for Linux camera access with explanatory comments about macOS limitation. - Add macOS LaunchAgent scripts (install/uninstall/plist template). - Polish run-mac.sh with build step and clearer output. - Fix dead code warnings on library utility functions. - Add /logs/ to .gitignore for LaunchAgent log output. - Fix skill.md: remove stale CSPRNG fallback references. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
6e9813ae99
commit
0314c598fb
|
|
@ -13,3 +13,6 @@ random.bin
|
||||||
# build-test runtime files
|
# build-test runtime files
|
||||||
camera-qrng.log
|
camera-qrng.log
|
||||||
.camera-qrng.pid
|
.camera-qrng.pid
|
||||||
|
|
||||||
|
# LaunchAgent logs
|
||||||
|
/logs/
|
||||||
|
|
|
||||||
38
README.md
38
README.md
|
|
@ -17,23 +17,35 @@ cd camera-trng
|
||||||
cargo build --release
|
cargo build --release
|
||||||
```
|
```
|
||||||
|
|
||||||
Or use Docker (build locally):
|
|
||||||
```bash
|
|
||||||
docker build -t camera-trng .
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Cover Your Camera Lens
|
### 2. Cover Your Camera Lens
|
||||||
|
|
||||||
Use tape, a lens cap, or put the camera in a dark box. The camera should see nothing but blackness.
|
Use tape, a lens cap, or put the camera in a dark box. The camera should see nothing but blackness.
|
||||||
|
|
||||||
### 3. Run
|
### 3. Run
|
||||||
|
|
||||||
|
**macOS (native camera access via AVFoundation):**
|
||||||
|
```bash
|
||||||
|
./scripts/run-mac.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
**Linux (native):**
|
||||||
```bash
|
```bash
|
||||||
./target/release/camera-qrng
|
./target/release/camera-qrng
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Linux (Docker with camera device):**
|
||||||
|
```bash
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
Server starts at `http://localhost:8787`
|
Server starts at `http://localhost:8787`
|
||||||
|
|
||||||
|
**Start with the OS (macOS LaunchAgent):**
|
||||||
|
```bash
|
||||||
|
./scripts/install-launchagent.sh
|
||||||
|
```
|
||||||
|
Runs the compiled binary at login and keeps it running. Logs go to `logs/`. To remove: `./scripts/uninstall-launchagent.sh`.
|
||||||
|
|
||||||
### 4. Get Random Bytes
|
### 4. Get Random Bytes
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
@ -65,17 +77,27 @@ The output is cryptographic quality - suitable for generating encryption keys, s
|
||||||
| `GET /random?bytes=N` | Get N random bytes (max 1MB) |
|
| `GET /random?bytes=N` | Get N random bytes (max 1MB) |
|
||||||
| `GET /random?bytes=N&hex=true` | Get N random bytes as hex string |
|
| `GET /random?bytes=N&hex=true` | Get N random bytes as hex string |
|
||||||
| `GET /stream` | Continuous stream of random bytes |
|
| `GET /stream` | Continuous stream of random bytes |
|
||||||
| `GET /health` | Health check |
|
| `GET /stream?bytes=N` | Stream exactly N bytes then stop |
|
||||||
|
| `GET /dice?d=6&count=3` | Roll quantum-random dice |
|
||||||
|
| `GET /password?length=24` | Generate quantum-random password |
|
||||||
|
| `GET /coin` | Quantum coin flip |
|
||||||
|
| `GET /8ball?question=...` | Quantum Magic 8 Ball |
|
||||||
|
| `GET /cameras` | List available camera devices |
|
||||||
|
| `GET /health` | Health check with pool stats |
|
||||||
|
| `GET /docs` | API documentation page |
|
||||||
|
| `GET /.well-known/mcp.json` | MCP discovery endpoint |
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
**macOS "Camera in use" error:**
|
**macOS "Camera in use" error:**
|
||||||
```bash
|
```bash
|
||||||
./scripts/release-camera.sh
|
./scripts/release-camera.sh
|
||||||
./target/release/camera-qrng
|
./scripts/run-mac.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
**Docker (Linux):**
|
**Docker on macOS:** Docker on Mac runs Linux containers and cannot access the Mac camera. Use `./scripts/run-mac.sh` instead.
|
||||||
|
|
||||||
|
**Docker on Linux (explicit device):**
|
||||||
```bash
|
```bash
|
||||||
docker run -d --device /dev/video0 -p 8787:8787 camera-trng
|
docker run -d --device /dev/video0 -p 8787:8787 camera-trng
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,8 @@ As Steve Gibson noted on Security Now, camera sensors produce **Gbps of quantum
|
||||||
|
|
||||||
This is a **firehose of quantum-origin randomness**. A single 4K camera at 60fps provides more conditioned entropy than most dedicated QRNG hardware.
|
This is a **firehose of quantum-origin randomness**. A single 4K camera at 60fps provides more conditioned entropy than most dedicated QRNG hardware.
|
||||||
|
|
||||||
|
**Note:** Actual throughput depends on the camera's native pixel format. Many cameras deliver YUV or compressed frames that are smaller than the theoretical RGB size. The numbers above assume uncompressed RGB (3 bytes/pixel). The entropy per pixel remains the same regardless of format; conditioned output scales with the actual frame size reported at startup.
|
||||||
|
|
||||||
### Configuration
|
### Configuration
|
||||||
|
|
||||||
Set resolution via environment variables:
|
Set resolution via environment variables:
|
||||||
|
|
@ -239,7 +241,7 @@ The SHA-256 conditioning ensures outputs are indistinguishable from ideal random
|
||||||
- Cryptographic nonces and IVs
|
- Cryptographic nonces and IVs
|
||||||
- Salts for password hashing
|
- Salts for password hashing
|
||||||
- UUID/ULID generation at scale
|
- UUID/ULID generation at scale
|
||||||
- Seeding CSPRNGs
|
- Seeding other RNGs
|
||||||
- Key generation for symmetric encryption
|
- Key generation for symmetric encryption
|
||||||
- Bulk key derivation
|
- Bulk key derivation
|
||||||
- Applications requiring provable physical/quantum randomness
|
- Applications requiring provable physical/quantum randomness
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,16 @@
|
||||||
|
# Camera TRNG - Docker Compose
|
||||||
|
# Linux only: Docker containers can access /dev/video0 via device mapping.
|
||||||
|
# macOS: use ./scripts/run-mac.sh instead (Docker cannot access the Mac camera).
|
||||||
services:
|
services:
|
||||||
camera-trng:
|
camera-trng:
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
ports:
|
ports:
|
||||||
- "8787:8787"
|
- "0.0.0.0:8787:8787"
|
||||||
|
devices:
|
||||||
|
- "/dev/video0:/dev/video0"
|
||||||
environment:
|
environment:
|
||||||
- PORT=8787
|
- PORT=8787
|
||||||
- RUST_LOG=info
|
- RUST_LOG=info
|
||||||
restart: on-failure
|
restart: always
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>Label</key>
|
||||||
|
<string>camera-trng</string>
|
||||||
|
<key>ProgramArguments</key>
|
||||||
|
<array>
|
||||||
|
<string>BINARY_PATH</string>
|
||||||
|
</array>
|
||||||
|
<key>WorkingDirectory</key>
|
||||||
|
<string>WORKING_DIR</string>
|
||||||
|
<key>RunAtLoad</key>
|
||||||
|
<true/>
|
||||||
|
<key>KeepAlive</key>
|
||||||
|
<true/>
|
||||||
|
<key>StandardOutPath</key>
|
||||||
|
<string>WORKING_DIR/logs/camera-qrng.out.log</string>
|
||||||
|
<key>StandardErrorPath</key>
|
||||||
|
<string>WORKING_DIR/logs/camera-qrng.err.log</string>
|
||||||
|
<key>EnvironmentVariables</key>
|
||||||
|
<dict>
|
||||||
|
<key>PORT</key>
|
||||||
|
<string>8787</string>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Install camera-qrng as a macOS LaunchAgent so it starts at login and stays running.
|
||||||
|
# Usage: ./scripts/install-launchagent.sh
|
||||||
|
|
||||||
|
set -e
|
||||||
|
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
BINARY="${ROOT}/target/release/camera-qrng"
|
||||||
|
PLIST_SRC="${ROOT}/scripts/camera-qrng.plist.in"
|
||||||
|
PLIST_DEST="${HOME}/Library/LaunchAgents/camera-trng.plist"
|
||||||
|
|
||||||
|
echo "Building release binary..."
|
||||||
|
(cd "$ROOT" && cargo build --release)
|
||||||
|
|
||||||
|
mkdir -p "${ROOT}/logs"
|
||||||
|
sed -e "s|BINARY_PATH|${BINARY}|g" -e "s|WORKING_DIR|${ROOT}|g" "$PLIST_SRC" > "$PLIST_DEST"
|
||||||
|
echo "Installed plist to ${PLIST_DEST}"
|
||||||
|
|
||||||
|
launchctl unload "$PLIST_DEST" 2>/dev/null || true
|
||||||
|
launchctl load "$PLIST_DEST"
|
||||||
|
echo "Loaded. camera-qrng will start at login and restart if it exits."
|
||||||
|
echo "Logs: ${ROOT}/logs/camera-qrng.out.log and .err.log"
|
||||||
|
echo "To stop: launchctl unload ${PLIST_DEST}"
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Run camera-trng natively on macOS (AVFoundation camera access).
|
||||||
|
# Docker on Mac cannot access the host camera — use this script instead.
|
||||||
|
# Usage: ./scripts/run-mac.sh
|
||||||
|
|
||||||
|
set -e
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
export PORT="${PORT:-8787}"
|
||||||
|
|
||||||
|
echo "=== Camera TRNG (macOS) ==="
|
||||||
|
echo "Building release binary..."
|
||||||
|
cargo build --release 2>&1
|
||||||
|
|
||||||
|
echo "Starting camera-qrng on http://0.0.0.0:${PORT}"
|
||||||
|
echo "Press Ctrl-C to stop."
|
||||||
|
exec ./target/release/camera-qrng
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Remove the camera-qrng LaunchAgent and stop the service.
|
||||||
|
# Usage: ./scripts/uninstall-launchagent.sh
|
||||||
|
|
||||||
|
set -e
|
||||||
|
PLIST="${HOME}/Library/LaunchAgents/camera-trng.plist"
|
||||||
|
if [[ -f "$PLIST" ]]; then
|
||||||
|
launchctl unload "$PLIST" 2>/dev/null || true
|
||||||
|
rm -f "$PLIST"
|
||||||
|
echo "Uninstalled camera-trng LaunchAgent."
|
||||||
|
else
|
||||||
|
echo "No plist found at ${PLIST}."
|
||||||
|
fi
|
||||||
4
skill.md
4
skill.md
|
|
@ -269,13 +269,11 @@ curl -s "http://localhost:8787/.well-known/mcp.json" | jq
|
||||||
- Maximum 4 concurrent requests
|
- Maximum 4 concurrent requests
|
||||||
- Requires a camera device with covered lens
|
- Requires a camera device with covered lens
|
||||||
- Performance depends on camera frame rate
|
- Performance depends on camera frame rate
|
||||||
- CSPRNG fallback is used when the camera pool is empty (still cryptographically safe, but not quantum)
|
|
||||||
|
|
||||||
## Security Notes
|
## Security Notes
|
||||||
|
|
||||||
- No authentication required (intended for local/trusted networks)
|
- No authentication required (intended for local/trusted networks)
|
||||||
- Random data is cryptographically secure from both the camera entropy pool and ChaCha20 CSPRNG fallback
|
- Random data is only from camera input; otherwise requests fail
|
||||||
- The CSPRNG is periodically re-seeded with real camera entropy
|
|
||||||
- Consider rate limiting in production deployments
|
- Consider rate limiting in production deployments
|
||||||
|
|
||||||
## See Also
|
## See Also
|
||||||
|
|
|
||||||
|
|
@ -28,10 +28,10 @@ pub fn requested_format(config: &CameraConfig) -> RequestedFormat<'static> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn camera_index_to_u32(idx: &CameraIndex, fallback: u32) -> u32 {
|
fn camera_index_to_u32(idx: &CameraIndex, default_index: u32) -> u32 {
|
||||||
match idx {
|
match idx {
|
||||||
CameraIndex::Index(i) => *i,
|
CameraIndex::Index(i) => *i,
|
||||||
CameraIndex::String(_) => fallback,
|
CameraIndex::String(_) => default_index,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ pub const CHUNK_SIZE: usize = 256;
|
||||||
|
|
||||||
/// Extract conditioned entropy from a raw camera frame buffer.
|
/// Extract conditioned entropy from a raw camera frame buffer.
|
||||||
/// Returns Vec of SHA-256 hashes over LSB chunks.
|
/// Returns Vec of SHA-256 hashes over LSB chunks.
|
||||||
|
#[allow(dead_code)]
|
||||||
pub fn condition_frame(raw: &[u8], frame_idx: u64) -> Vec<Vec<u8>> {
|
pub fn condition_frame(raw: &[u8], frame_idx: u64) -> Vec<Vec<u8>> {
|
||||||
let lsbs: Vec<u8> = raw.iter().map(|b| b & 0x03).collect();
|
let lsbs: Vec<u8> = raw.iter().map(|b| b & 0x03).collect();
|
||||||
let mut hasher = Sha256::new();
|
let mut hasher = Sha256::new();
|
||||||
|
|
@ -25,6 +26,7 @@ pub fn condition_frame(raw: &[u8], frame_idx: u64) -> Vec<Vec<u8>> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extract raw LSB bytes from a camera frame (no conditioning).
|
/// Extract raw LSB bytes from a camera frame (no conditioning).
|
||||||
|
#[allow(dead_code)]
|
||||||
pub fn extract_lsbs(raw: &[u8]) -> Vec<u8> {
|
pub fn extract_lsbs(raw: &[u8]) -> Vec<u8> {
|
||||||
raw.iter().map(|b| b & 0x03).collect()
|
raw.iter().map(|b| b & 0x03).collect()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
//! captures thermal/quantum noise from the CCD/CMOS dark current.
|
//! captures thermal/quantum noise from the CCD/CMOS dark current.
|
||||||
//!
|
//!
|
||||||
//! Architecture: a background harvester feeds a ring-buffer pool.
|
//! Architecture: a background harvester feeds a ring-buffer pool.
|
||||||
//! API requests pull from the pool (instant) with CSPRNG fallback.
|
//! Random data is only from camera input; if the pool has insufficient bytes, requests fail.
|
||||||
|
|
||||||
mod config;
|
mod config;
|
||||||
mod pool;
|
mod pool;
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,11 @@
|
||||||
//! Resilient entropy pool: background camera harvesting + CSPRNG fallback.
|
//! Entropy pool: random data from camera input only, or not at all.
|
||||||
//!
|
//!
|
||||||
//! Architecture:
|
//! A background thread captures camera frames and feeds conditioned entropy into
|
||||||
//! - A background thread continuously captures camera frames and feeds conditioned
|
//! a ring buffer. API requests pull from that buffer. The only source of random
|
||||||
//! entropy into a ring buffer pool.
|
//! data is the camera; if the pool has insufficient bytes, requests fail.
|
||||||
//! - API requests pull from the pool instantly (never touch the camera directly).
|
|
||||||
//! - If the pool is empty (camera slow/hung), a ChaCha20Rng CSPRNG provides bytes.
|
|
||||||
//! - The CSPRNG is periodically re-seeded from real camera entropy.
|
|
||||||
//! - All camera operations have enforced timeouts so nothing ever blocks forever.
|
|
||||||
//!
|
//!
|
||||||
//! Resilience guarantees:
|
//! All camera operations have enforced timeouts. Poisoned mutexes are recovered;
|
||||||
//! - Poisoned mutexes are recovered (never panic on lock).
|
//! harvester panics are caught and the thread restarts.
|
||||||
//! - Harvester thread panics are caught and the thread auto-restarts.
|
|
||||||
//! - Camera reconnection uses exponential backoff with jitter.
|
|
||||||
//! - Frame capture has an enforced deadline.
|
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::panic;
|
use std::panic;
|
||||||
|
|
@ -37,12 +30,12 @@ const FRAME_TIMEOUT: Duration = Duration::from_secs(5);
|
||||||
const RECONNECT_BASE: Duration = Duration::from_secs(2);
|
const RECONNECT_BASE: Duration = Duration::from_secs(2);
|
||||||
/// Maximum reconnect backoff cap.
|
/// Maximum reconnect backoff cap.
|
||||||
const RECONNECT_MAX: Duration = Duration::from_secs(60);
|
const RECONNECT_MAX: Duration = Duration::from_secs(60);
|
||||||
/// Minimum bytes of real entropy before first CSPRNG seed.
|
/// Minimum camera bytes before marking pool as seeded (diagnostics only).
|
||||||
const MIN_SEED_BYTES: usize = 64;
|
const MIN_SEED_BYTES: usize = 64;
|
||||||
/// How long to wait before restarting a panicked harvester thread.
|
/// How long to wait before restarting a panicked harvester thread.
|
||||||
const PANIC_RESTART_DELAY: Duration = Duration::from_secs(3);
|
const PANIC_RESTART_DELAY: Duration = Duration::from_secs(3);
|
||||||
|
|
||||||
/// The global resilient pool, initialized once.
|
/// Global pool; output is only from camera buffer.
|
||||||
static RESILIENT_POOL: std::sync::OnceLock<ResilientPool> = std::sync::OnceLock::new();
|
static RESILIENT_POOL: std::sync::OnceLock<ResilientPool> = std::sync::OnceLock::new();
|
||||||
|
|
||||||
// ── Poison-safe mutex helper ────────────────────────────────────────
|
// ── Poison-safe mutex helper ────────────────────────────────────────
|
||||||
|
|
@ -66,9 +59,9 @@ struct PoolInner {
|
||||||
|
|
||||||
pub struct ResilientPool {
|
pub struct ResilientPool {
|
||||||
inner: Mutex<PoolInner>,
|
inner: Mutex<PoolInner>,
|
||||||
/// CSPRNG fallback, re-seeded from camera entropy.
|
/// Internal; not used for any output. Output is only from camera buffer.
|
||||||
rng: Mutex<ChaCha20Rng>,
|
rng: Mutex<ChaCha20Rng>,
|
||||||
/// Set to true once the CSPRNG has been seeded with real camera entropy.
|
/// True once pool has received enough camera bytes (diagnostics only).
|
||||||
seeded: AtomicBool,
|
seeded: AtomicBool,
|
||||||
/// Set to true while the harvester supervisor thread is alive.
|
/// Set to true while the harvester supervisor thread is alive.
|
||||||
producer_running: AtomicBool,
|
producer_running: AtomicBool,
|
||||||
|
|
@ -85,8 +78,7 @@ struct SubscriberMap {
|
||||||
|
|
||||||
fn get_pool() -> &'static ResilientPool {
|
fn get_pool() -> &'static ResilientPool {
|
||||||
RESILIENT_POOL.get_or_init(|| {
|
RESILIENT_POOL.get_or_init(|| {
|
||||||
let mut seed = [0u8; 32];
|
let seed = [0u8; 32]; // rng is not used for output; only camera buffer is.
|
||||||
rand::rng().fill(&mut seed);
|
|
||||||
let rng = ChaCha20Rng::from_seed(seed);
|
let rng = ChaCha20Rng::from_seed(seed);
|
||||||
ResilientPool {
|
ResilientPool {
|
||||||
inner: Mutex::new(PoolInner {
|
inner: Mutex::new(PoolInner {
|
||||||
|
|
@ -119,7 +111,7 @@ fn backoff_duration(attempt: u32) -> Duration {
|
||||||
capped + Duration::from_millis(jitter_ms)
|
capped + Duration::from_millis(jitter_ms)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Push conditioned entropy bytes into the pool and re-seed the CSPRNG.
|
/// Push conditioned camera bytes into the pool.
|
||||||
fn pool_push(data: &[u8]) {
|
fn pool_push(data: &[u8]) {
|
||||||
let pool = get_pool();
|
let pool = get_pool();
|
||||||
let mut inner = lock_or_recover(&pool.inner);
|
let mut inner = lock_or_recover(&pool.inner);
|
||||||
|
|
@ -137,7 +129,6 @@ fn pool_push(data: &[u8]) {
|
||||||
let total = inner.total_harvested;
|
let total = inner.total_harvested;
|
||||||
drop(inner);
|
drop(inner);
|
||||||
|
|
||||||
// Re-seed CSPRNG from camera entropy.
|
|
||||||
if data.len() >= 32 && total >= MIN_SEED_BYTES as u64 {
|
if data.len() >= 32 && total >= MIN_SEED_BYTES as u64 {
|
||||||
let mut seed = [0u8; 32];
|
let mut seed = [0u8; 32];
|
||||||
seed.copy_from_slice(&data[..32]);
|
seed.copy_from_slice(&data[..32]);
|
||||||
|
|
@ -153,38 +144,33 @@ fn pool_push(data: &[u8]) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Pull bytes from the pool. Consumes real camera entropy first (FIFO),
|
/// Pull bytes from the pool. Only from camera. Fails if fewer than `num_bytes` available.
|
||||||
/// falls back to CSPRNG when pool is empty.
|
pub fn pool_pull(num_bytes: usize) -> Result<Vec<u8>, String> {
|
||||||
/// **Never blocks. Always returns immediately.**
|
|
||||||
pub fn pool_pull(num_bytes: usize) -> Vec<u8> {
|
|
||||||
let pool = get_pool();
|
let pool = get_pool();
|
||||||
let mut out = Vec::with_capacity(num_bytes);
|
let mut out = Vec::with_capacity(num_bytes);
|
||||||
|
|
||||||
{
|
{
|
||||||
let mut inner = lock_or_recover(&pool.inner);
|
let mut inner = lock_or_recover(&pool.inner);
|
||||||
let take = num_bytes.min(inner.available);
|
if inner.available < num_bytes {
|
||||||
for _ in 0..take {
|
return Err(format!(
|
||||||
|
"insufficient camera entropy: need {} bytes, {} available (cover lens and wait for harvest)",
|
||||||
|
num_bytes, inner.available
|
||||||
|
));
|
||||||
|
}
|
||||||
|
for _ in 0..num_bytes {
|
||||||
let pos = inner.read_pos;
|
let pos = inner.read_pos;
|
||||||
out.push(inner.buf[pos]);
|
out.push(inner.buf[pos]);
|
||||||
inner.read_pos = (pos + 1) % POOL_CAPACITY;
|
inner.read_pos = (pos + 1) % POOL_CAPACITY;
|
||||||
}
|
}
|
||||||
inner.available -= take;
|
inner.available -= num_bytes;
|
||||||
}
|
}
|
||||||
|
|
||||||
let remaining = num_bytes - out.len();
|
Ok(out)
|
||||||
if remaining > 0 {
|
|
||||||
let mut rng_buf = vec![0u8; remaining];
|
|
||||||
let mut rng = lock_or_recover(&pool.rng);
|
|
||||||
rng.fill(&mut rng_buf[..]);
|
|
||||||
out.extend_from_slice(&rng_buf);
|
|
||||||
}
|
|
||||||
|
|
||||||
out
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extract entropy: always-available, non-blocking.
|
/// Extract entropy: only from camera. Fails if pool has insufficient bytes.
|
||||||
pub fn extract_entropy(num_bytes: usize, _config: &CameraConfig) -> Result<Vec<u8>, String> {
|
pub fn extract_entropy(num_bytes: usize, _config: &CameraConfig) -> Result<Vec<u8>, String> {
|
||||||
Ok(pool_pull(num_bytes))
|
pool_pull(num_bytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fill a buffer with entropy (used by OpenSSL provider).
|
/// Fill a buffer with entropy (used by OpenSSL provider).
|
||||||
|
|
|
||||||
21
src/main.rs
21
src/main.rs
|
|
@ -59,21 +59,24 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
Ok((actual_w, actual_h, frame_size)) => {
|
Ok((actual_w, actual_h, frame_size)) => {
|
||||||
let conditioned_per_frame = (frame_size / CHUNK_SIZE) * 32;
|
let conditioned_per_frame = (frame_size / CHUNK_SIZE) * 32;
|
||||||
let throughput_30fps = conditioned_per_frame * 30;
|
let throughput_30fps = conditioned_per_frame * 30;
|
||||||
let raw_gbps = (frame_size as f64 * 30.0 * 8.0) / 1_000_000_000.0;
|
let raw_mbps = (frame_size as f64 * 30.0 * 8.0) / 1_000_000.0;
|
||||||
|
let conditioned_kbs = throughput_30fps / 1_000;
|
||||||
println!(
|
println!(
|
||||||
"Camera OK at {}x{} - {} bytes/frame",
|
"Camera OK: {}x{}, {} KB/frame ({} bytes)",
|
||||||
actual_w, actual_h, frame_size
|
actual_w, actual_h, frame_size / 1024, 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
|
|
||||||
);
|
);
|
||||||
|
if raw_mbps >= 1000.0 {
|
||||||
|
println!("Throughput: {:.1} Gbps raw, ~{} MB/s conditioned @ 30fps",
|
||||||
|
raw_mbps / 1000.0, throughput_30fps / 1_000_000);
|
||||||
|
} else {
|
||||||
|
println!("Throughput: {:.0} Mbps raw, ~{} KB/s conditioned @ 30fps",
|
||||||
|
raw_mbps, conditioned_kbs);
|
||||||
|
}
|
||||||
println!("Ensure lens is covered for optimal quantum noise capture");
|
println!("Ensure lens is covered for optimal quantum noise capture");
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!(
|
eprintln!(
|
||||||
"Camera error: {}. Server will still start with CSPRNG fallback.",
|
"Camera error: {}. Server will start; /random and stream return errors until camera entropy is available.",
|
||||||
e
|
e
|
||||||
);
|
);
|
||||||
if e.contains("Lock Rejected") || e.contains("lock") {
|
if e.contains("Lock Rejected") || e.contains("lock") {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue