Add OpenSSL provider, camera reconnection, manpage, and cross-compile support

Major changes:
- Refactor entropy extraction into modular src/entropy/ with camera, config,
  extract, and pool submodules
- Add automatic camera reconnection with retry logic (5 retries, 500ms delay)
  so the service survives temporary camera disconnections
- Build as OpenSSL 3.x provider (cdylib) for system-wide entropy integration
- Add manpage (man/camera-qrng.1) with setup instructions and API docs
- Add cross-compilation script for ARM64/x86_64 on macOS and Linux
- Add helper scripts: release-camera.sh, stream-random.sh, stream-demo.sh

The camera now automatically attempts to reconnect when frame capture fails
instead of dying, making the service more resilient for long-running deployments.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Leopere 2026-02-05 14:53:19 -05:00
parent fd0598be09
commit df18197a1d
Signed by: colin
SSH Key Fingerprint: SHA256:nRPCQTeMFLdGytxRQmPVK9VXY3/ePKQ5lGRyJhT5DY8
19 changed files with 1713 additions and 208 deletions

9
.gitignore vendored
View File

@ -1,2 +1,11 @@
/target /target
scripts/node_modules/ scripts/node_modules/
# Release build output
/release/
# Man pages (generated)
/man/*.gz
# Test artifacts
random.bin

25
Cargo.lock generated
View File

@ -17,6 +17,28 @@ version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
[[package]]
name = "async-stream"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476"
dependencies = [
"async-stream-impl",
"futures-core",
"pin-project-lite",
]
[[package]]
name = "async-stream-impl"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "async-trait" name = "async-trait"
version = "0.1.89" version = "0.1.89"
@ -173,7 +195,10 @@ checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3"
name = "camera-trng" name = "camera-trng"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"async-stream",
"axum", "axum",
"bytes",
"futures-util",
"hex", "hex",
"nokhwa", "nokhwa",
"serde", "serde",

View File

@ -2,7 +2,14 @@
name = "camera-trng" name = "camera-trng"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2021"
description = "True random number generator using camera sensor noise" description = "True random number generator using camera sensor noise - with OpenSSL provider"
[lib]
crate-type = ["lib", "cdylib"]
[[bin]]
name = "camera-qrng"
path = "src/main.rs"
[dependencies] [dependencies]
nokhwa = { version = "0.10", features = ["input-native"] } nokhwa = { version = "0.10", features = ["input-native"] }
@ -12,6 +19,9 @@ sha2 = "0.10"
hex = "0.4" hex = "0.4"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1" serde_json = "1"
bytes = "1"
futures-util = { version = "0.3", default-features = false, features = ["alloc"] }
async-stream = "0.3"
[profile.release] [profile.release]
opt-level = "z" opt-level = "z"

View File

@ -36,8 +36,29 @@ cargo build --release
./target/release/camera-trng ./target/release/camera-trng
# Or set a custom port # Or set a custom port
PORT=9000 ./target/release/camera-trng PORT=9000 ./target/release/camera-trng
Use the camera at **max resolution** (and highest frame rate):
```bash
CAMERA_MAX_RESOLUTION=1 cargo run
``` ```
**macOS — camera in use ("Lock Rejected")?** Run once to release the webcam, then start the server:
```bash
./scripts/release-camera.sh # may prompt for sudo password
cargo run
```
### Streaming random (multiple terminals)
To see a stream of random in the terminal and verify each stream is unique:
- **One stream:** `./scripts/stream-random.sh 0` (infinite; Ctrl+C to stop)
- **Several streams in different terminals** (each gets different random; never the same):
- Terminal 1: `./scripts/stream-random.sh "Stream-1" 0`
- Terminal 2: `./scripts/stream-random.sh "Stream-2" 0`
- Terminal 3: `./scripts/stream-random.sh "Stream-3" 0`
- **Quick demo (3 streams, 5 lines each, verify no duplicates):** `./scripts/stream-demo.sh 5`
## Docker ## Docker
Pull the pre-built image: Pull the pre-built image:

191
man/camera-qrng.1 Normal file
View File

@ -0,0 +1,191 @@
.TH CAMERA-QRNG 1 "February 2026" "camera-qrng 0.1.0" "User Commands"
.SH NAME
camera-qrng \- quantum random number generator using camera sensor thermal noise
.SH SYNOPSIS
.B camera-qrng
[\fIENVIRONMENT VARIABLES\fR]
.SH DESCRIPTION
.B camera-qrng
is a true random number generator that extracts entropy from camera sensor
thermal noise, following the LavaRnd methodology. It runs an HTTP server that
provides cryptographically secure random bytes on demand.
.PP
The camera sensor's dark current and thermal electron activity produce quantum
noise that is harvested as entropy. This approach provides high-throughput
random data suitable for cryptographic applications.
.SH SETUP REQUIREMENTS
.SS Cover the Camera Lens
.B CRITICAL:
The camera lens \fBmust\fR be covered for proper operation.
.PP
.RS 4
\(bu Use a lens cap or opaque tape
.br
\(bu Place camera in a light-proof enclosure
.br
\(bu Verify the camera captures pure black frames
.RE
.PP
Covering the lens isolates pure thermal noise from the sensor, eliminating
any scene-correlated data and providing a simpler security model.
.SS Release Camera on macOS
If you see "Lock Rejected" errors, another process is using the camera.
Run:
.PP
.RS 4
.nf
./scripts/release-camera.sh
.fi
.RE
.PP
This kills VDCAssistant/AppleCameraAssistant processes. Then restart the server.
.SS Verify Camera Access
List available cameras:
.PP
.RS 4
.nf
curl http://localhost:8787/cameras
.fi
.RE
.SH ENVIRONMENT
.TP
.B PORT
HTTP server port (default: 8787)
.TP
.B CAMERA_INDEX
Camera device index to use (default: 0). Use \fI/cameras\fR endpoint to list.
.TP
.B CAMERA_WIDTH
Requested camera width in pixels (default: 1920)
.TP
.B CAMERA_HEIGHT
Requested camera height in pixels (default: 1080)
.TP
.B CAMERA_MAX_RESOLUTION
Set to "1" or "true" to use maximum camera resolution instead of configured.
Higher resolution = more entropy per frame.
.SH API ENDPOINTS
.TP
.B GET /random
Returns random bytes. Query parameters:
.RS 4
\(bu \fBbytes\fR \- number of bytes (default: 32, max: 1048576)
.br
\(bu \fBhex\fR \- return as hex string (default: false)
.RE
.TP
.B GET /raw
Returns raw LSB bytes without cryptographic conditioning.
.TP
.B GET /stream
Continuous stream of random bytes. Each connected client receives unique data.
Query parameters: \fBbytes\fR (limit), \fBhex\fR.
.TP
.B GET /cameras
List available camera devices.
.TP
.B GET /health
Returns "ok" if server is running.
.TP
.B GET /.well-known/mcp.json
MCP (Model Context Protocol) discovery endpoint.
.SH EXAMPLES
.SS Start the server
.nf
# Default settings
camera-qrng
# Custom port and camera
PORT=9000 CAMERA_INDEX=1 camera-qrng
# Maximum resolution
CAMERA_MAX_RESOLUTION=1 camera-qrng
.fi
.SS Get random bytes
.nf
# 32 bytes as hex
curl "http://localhost:8787/random?hex=true"
# 1KB raw bytes to file
curl "http://localhost:8787/random?bytes=1024" -o random.bin
# Stream 1MB of random
curl "http://localhost:8787/stream?bytes=1048576" -o stream.bin
.fi
.SS Raspberry Pi Setup
.nf
# Enable camera in raspi-config
sudo raspi-config # Interface Options > Camera > Enable
# Check camera device
ls /dev/video*
# Run with appropriate camera index
CAMERA_INDEX=0 camera-qrng
.fi
.SH HOW IT WORKS
.IP 1. 4
Opens the camera and maximizes gain/brightness/exposure settings
.IP 2. 4
Captures frames of pure sensor noise (no light = no scene data)
.IP 3. 4
Extracts the 2 LSBs from each pixel (highest entropy density)
.IP 4. 4
Hashes 256-byte chunks with SHA-256 (8:1 conditioning ratio)
.IP 5. 4
Mixes in timing and counter data for additional uniqueness
.PP
The camera automatically reconnects on errors (up to 5 retries with 500ms delay).
.SH PLATFORM SUPPORT
.TP
.B macOS (Apple Silicon, Intel)
Uses AVFoundation. May require releasing camera from other apps first.
.TP
.B Linux (x86_64, ARM/Raspberry Pi)
Uses V4L2. Ensure /dev/video* device permissions allow access.
.TP
.B Windows
Uses Media Foundation.
.SH OPENSSL PROVIDER
camera-qrng can be used as an OpenSSL 3.x provider for system-wide entropy:
.PP
.RS 4
.nf
# Set in openssl.cnf or via environment
OPENSSL_CONF=/path/to/openssl-camera-qrng.cnf openssl rand -hex 32
.fi
.RE
.SH SECURITY CONSIDERATIONS
.IP \(bu 4
\fBAlways cover the lens\fR \- Required for the intended security model
.IP \(bu 4
Gain is automatically maximized to amplify thermal noise
.IP \(bu 4
With lens covered, there is no side-channel information leakage
.IP \(bu 4
SHA-256 conditioning removes bias and ensures uniform distribution
.IP \(bu 4
For high-security applications, consider mixing with system entropy
.SH FILES
.TP
.I /dev/video*
Camera devices on Linux
.TP
.I openssl-camera-qrng.cnf
OpenSSL provider configuration
.SH EXIT STATUS
.TP
.B 0
Server started successfully
.TP
.B 1
Camera access error or configuration problem
.SH SEE ALSO
.BR openssl (1),
.BR v4l2-ctl (1)
.PP
LavaRnd Project: https://www.lavarnd.org/
.SH AUTHORS
Written for the camera-trng project.
.SH BUGS
Report bugs at: https://git.nixc.us/colin/camera-trng/issues

25
openssl-camera-qrng.cnf Normal file
View File

@ -0,0 +1,25 @@
# OpenSSL configuration to use Camera QRNG provider
# Usage: OPENSSL_CONF=openssl-camera-qrng.cnf openssl rand -hex 32
openssl_conf = openssl_init
[openssl_init]
providers = provider_sect
[provider_sect]
camera-qrng = camera_qrng_sect
default = default_sect
[default_sect]
activate = 1
[camera_qrng_sect]
# Path to the shared library (adjust for your system)
# macOS: libcamera_trng.dylib
# Linux: libcamera_trng.so
module = ./target/release/libcamera_trng.dylib
activate = 1
# Optional: Set as the primary RAND source
# [algorithm_sect]
# default_properties = ?provider=camera-qrng

215
scripts/build-release.sh Executable file
View File

@ -0,0 +1,215 @@
#!/usr/bin/env bash
# Build camera-qrng for multiple platforms.
# Usage: ./scripts/build-release.sh [target...]
#
# Targets:
# native - Build for current platform (default)
# aarch64-apple - Apple Silicon (M1/M2/M3)
# x86_64-apple - Intel Mac
# aarch64-linux - Linux ARM64 (Raspberry Pi 4/5, etc.)
# x86_64-linux - Linux x86_64
# all - Build all targets
#
# Prerequisites:
# - Rust toolchain with cross-compilation targets
# - For Linux cross-compile from macOS: cross (cargo install cross)
#
# Install targets:
# rustup target add aarch64-apple-darwin x86_64-apple-darwin
# rustup target add aarch64-unknown-linux-gnu x86_64-unknown-linux-gnu
set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
OUTPUT_DIR="$PROJECT_DIR/release"
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
info() { echo -e "${GREEN}[INFO]${NC} $1"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
error() { echo -e "${RED}[ERROR]${NC} $1"; exit 1; }
get_rust_target() {
case "$1" in
aarch64-apple) echo "aarch64-apple-darwin" ;;
x86_64-apple) echo "x86_64-apple-darwin" ;;
aarch64-linux) echo "aarch64-unknown-linux-gnu" ;;
x86_64-linux) echo "x86_64-unknown-linux-gnu" ;;
*) echo "" ;;
esac
}
get_friendly_name() {
case "$1" in
aarch64-apple) echo "Apple Silicon (M1/M2/M3)" ;;
x86_64-apple) echo "Intel Mac" ;;
aarch64-linux) echo "Linux ARM64 (Raspberry Pi)" ;;
x86_64-linux) echo "Linux x86_64" ;;
*) echo "$1" ;;
esac
}
get_output_name() {
case "$1" in
aarch64-apple) echo "camera-qrng-aarch64-macos" ;;
x86_64-apple) echo "camera-qrng-x86_64-macos" ;;
aarch64-linux) echo "camera-qrng-aarch64-linux" ;;
x86_64-linux) echo "camera-qrng-x86_64-linux" ;;
*) echo "camera-qrng-$1" ;;
esac
}
build_native() {
info "Building for native platform..."
cargo build --release
local binary="$PROJECT_DIR/target/release/camera-qrng"
if [[ -f "$binary" ]]; then
cp "$binary" "$OUTPUT_DIR/camera-qrng-native"
info "Built: $OUTPUT_DIR/camera-qrng-native"
file "$OUTPUT_DIR/camera-qrng-native"
fi
}
build_target() {
local shortname=$1
local target
target=$(get_rust_target "$shortname")
local friendly
friendly=$(get_friendly_name "$shortname")
local output_name
output_name=$(get_output_name "$shortname")
if [[ -z "$target" ]]; then
error "Unknown target: $shortname"
fi
info "Building for $friendly ($target)..."
# Check if target is installed
if ! rustup target list --installed 2>/dev/null | grep -q "$target"; then
warn "Target $target not installed. Installing..."
rustup target add "$target" || {
warn "Failed to add target $target. Skipping."
return 1
}
fi
# For Linux targets from macOS, try cross first
local use_cross=false
if [[ "$OSTYPE" == "darwin"* ]] && [[ "$target" == *"linux"* ]]; then
if command -v cross &> /dev/null; then
use_cross=true
else
warn "Linux cross-compilation requires 'cross'. Install with: cargo install cross"
warn "Attempting native cargo build (may fail due to linker issues)..."
fi
fi
if [[ "$use_cross" == true ]]; then
cross build --release --target "$target" 2>&1 || {
warn "Cross build failed for $target"
return 1
}
else
cargo build --release --target "$target" 2>&1 || {
warn "Build failed for $target"
return 1
}
fi
local binary="$PROJECT_DIR/target/$target/release/camera-qrng"
if [[ -f "$binary" ]]; then
cp "$binary" "$OUTPUT_DIR/$output_name"
info "Built: $OUTPUT_DIR/$output_name"
file "$OUTPUT_DIR/$output_name"
else
warn "Binary not found at $binary"
return 1
fi
}
install_manpage() {
info "Copying manpage..."
if [[ -f "$PROJECT_DIR/man/camera-qrng.1" ]]; then
cp "$PROJECT_DIR/man/camera-qrng.1" "$OUTPUT_DIR/"
gzip -c "$PROJECT_DIR/man/camera-qrng.1" > "$OUTPUT_DIR/camera-qrng.1.gz"
info "Manpage: $OUTPUT_DIR/camera-qrng.1.gz"
fi
}
show_usage() {
cat << EOF
Usage: $0 [target...]
Targets:
native Build for current platform (default)
aarch64-apple Apple Silicon (M1/M2/M3)
x86_64-apple Intel Mac
aarch64-linux Linux ARM64 (Raspberry Pi 4/5)
x86_64-linux Linux x86_64
all Build all targets
Examples:
$0 # Build native only
$0 all # Build all platforms
$0 aarch64-apple # Build Apple Silicon only
$0 aarch64-linux x86_64-linux # Build Linux targets
EOF
}
main() {
cd "$PROJECT_DIR"
mkdir -p "$OUTPUT_DIR"
local success=0
local failed=0
# Default to native if no args
if [[ $# -eq 0 ]]; then
set -- "native"
fi
# Process targets
for target in "$@"; do
case "$target" in
-h|--help)
show_usage
exit 0
;;
all)
for t in native aarch64-apple x86_64-apple aarch64-linux x86_64-linux; do
if [[ "$t" == "native" ]]; then
build_native && success=$((success+1)) || failed=$((failed+1))
else
build_target "$t" && success=$((success+1)) || failed=$((failed+1))
fi
done
;;
native)
build_native && success=$((success+1)) || failed=$((failed+1))
;;
*)
build_target "$target" && success=$((success+1)) || failed=$((failed+1))
;;
esac
done
install_manpage
echo ""
info "Build complete: $success succeeded, $failed failed"
echo ""
info "Release artifacts in: $OUTPUT_DIR/"
ls -la "$OUTPUT_DIR/"
if [[ $failed -gt 0 ]]; then
exit 1
fi
}
main "$@"

12
scripts/release-camera.sh Executable file
View File

@ -0,0 +1,12 @@
#!/usr/bin/env bash
# Release the webcam on macOS so camera-qrng can use it.
# Run before starting the server if you see "Lock Rejected" or camera in use.
# Usage: ./scripts/release-camera.sh (may prompt for sudo password)
set -e
echo "Releasing camera (killing VDCAssistant / AppleCameraAssistant)..."
sudo killall VDCAssistant 2>/dev/null || true
sudo killall AppleCameraAssistant 2>/dev/null || true
sleep 1
echo "Done. Start the server with: cargo run"
echo " (Or: PORT=8787 CAMERA_INDEX=0 ./target/debug/camera-qrng)"

22
scripts/stream-demo.sh Executable file
View File

@ -0,0 +1,22 @@
#!/usr/bin/env bash
# Run 3 streams in parallel; each prints 5 random hex lines with a label.
# Proves each stream gets different random (no duplicates across streams).
# Requires server running: cargo run (or ./scripts/release-camera.sh first if camera locked)
PORT="${PORT:-8787}"
N="${1:-5}"
echo "Three streams, ${N} lines each — verify no line is repeated across streams."
echo ""
( echo "--- Stream-A ---"; for ((i=1;i<=N;i++)); do curl -s "http://127.0.0.1:${PORT}/random?bytes=32&hex=true"; echo ""; done ) | tee /tmp/stream-a.$$ &
( echo "--- Stream-B ---"; for ((i=1;i<=N;i++)); do curl -s "http://127.0.0.1:${PORT}/random?bytes=32&hex=true"; echo ""; done ) | tee /tmp/stream-b.$$ &
( echo "--- Stream-C ---"; for ((i=1;i<=N;i++)); do curl -s "http://127.0.0.1:${PORT}/random?bytes=32&hex=true"; echo ""; done ) | tee /tmp/stream-c.$$ &
wait
echo ""
echo "--- Verification: total unique hex lines (should be ${N} per stream = $((N*3)) total) ---"
total=$(cat /tmp/stream-a.$$ /tmp/stream-b.$$ /tmp/stream-c.$$ 2>/dev/null | grep -E '^[0-9a-f]{64}$' | sort -u | wc -l)
expected=$((N*3))
echo "Unique 64-char hex lines: $total (expected $expected)"
rm -f /tmp/stream-a.$$ /tmp/stream-b.$$ /tmp/stream-c.$$

40
scripts/stream-random.sh Executable file
View File

@ -0,0 +1,40 @@
#!/usr/bin/env bash
# Stream random bytes from camera-qrng to the terminal.
# Start the server first: cargo run (or ./scripts/release-camera.sh if camera locked)
#
# Usage:
# ./scripts/stream-random.sh [count] # 10 lines (or 0 = infinite), no label
# ./scripts/stream-random.sh [label] [count] # label e.g. "Stream-1" so you can run in multiple terminals and see different streams
#
# Example (run in 3 different terminals to see 3 independent streams, never the same):
# Terminal 1: ./scripts/stream-random.sh "Stream-1" 0
# Terminal 2: ./scripts/stream-random.sh "Stream-2" 0
# Terminal 3: ./scripts/stream-random.sh "Stream-3" 0
PORT="${PORT:-8787}"
BYTES="${BYTES:-32}"
LABEL=""
COUNT=""
if [[ "$1" =~ ^[0-9]+$ ]]; then
COUNT="$1"
else
LABEL="$1"
COUNT="${2:-10}"
fi
COUNT="${COUNT:-10}"
if [[ -n "$LABEL" ]]; then
PREFIX="${LABEL}: "
else
PREFIX=""
fi
if [[ "$COUNT" == "0" ]]; then
echo "Streaming random (hex) to terminal${LABEL:+ as $LABEL} — Ctrl+C to stop"
while true; do echo -n "$PREFIX"; curl -s "http://127.0.0.1:${PORT}/random?bytes=${BYTES}&hex=true"; echo ""; done
else
echo "Streaming ${COUNT} x ${BYTES} bytes (hex)${LABEL:+ as $LABEL}:"
for ((i=1; i<=COUNT; i++)); do echo -n "$PREFIX"; curl -s "http://127.0.0.1:${PORT}/random?bytes=${BYTES}&hex=true"; echo ""; done
echo "Done."
fi

View File

@ -0,0 +1,65 @@
#!/bin/bash
# Test the Camera QRNG OpenSSL provider
set -e
cd "$(dirname "$0")/.."
# Build release version
echo "Building release..."
cargo build --release
# Find the library
if [[ "$OSTYPE" == "darwin"* ]]; then
LIB="target/release/libcamera_trng.dylib"
else
LIB="target/release/libcamera_trng.so"
fi
if [[ ! -f "$LIB" ]]; then
echo "Error: Library not found at $LIB"
exit 1
fi
echo "Library built: $LIB"
echo "Exported symbols:"
nm -gU "$LIB" | grep -i ossl || true
# Check OpenSSL version
echo ""
echo "OpenSSL version:"
openssl version
# Test loading the provider (requires OpenSSL 3.x)
echo ""
echo "Testing provider load..."
# Create temp config
TMPCONF=$(mktemp)
cat > "$TMPCONF" << CONF
openssl_conf = openssl_init
[openssl_init]
providers = provider_sect
[provider_sect]
camera-qrng = camera_qrng_sect
default = default_sect
[default_sect]
activate = 1
[camera_qrng_sect]
module = $(pwd)/$LIB
activate = 1
CONF
echo "Config written to: $TMPCONF"
cat "$TMPCONF"
echo ""
echo "Loading provider (camera required)..."
OPENSSL_CONF="$TMPCONF" openssl list -providers 2>&1 || echo "(provider listing may require additional setup)"
echo ""
echo "Generating random bytes with provider..."
OPENSSL_CONF="$TMPCONF" openssl rand -hex 32 2>&1 || echo "(rand may use default provider)"
rm -f "$TMPCONF"
echo ""
echo "Test complete!"

159
src/entropy/camera.rs Normal file
View File

@ -0,0 +1,159 @@
//! Camera utilities: list, test, configure for quantum noise capture.
use std::thread;
use std::time::Duration;
use nokhwa::{
native_api_backend, pixel_format::RgbFormat, query,
utils::{
CameraIndex, CameraInfo, ControlValueDescription, ControlValueSetter,
KnownCameraControl, RequestedFormat, RequestedFormatType,
},
Camera,
};
use super::config::{CameraConfig, CameraListItem};
/// Retry configuration for camera operations
pub const MAX_RETRIES: u32 = 5;
pub const RETRY_DELAY_MS: u64 = 500;
pub const RECONNECT_DELAY_MS: u64 = 1000;
/// Build requested format: max resolution from camera, or configured resolution.
pub fn requested_format(config: &CameraConfig) -> RequestedFormat<'static> {
if CameraConfig::use_max_resolution() {
RequestedFormat::new::<RgbFormat>(RequestedFormatType::AbsoluteHighestResolution)
} else {
RequestedFormat::new::<RgbFormat>(RequestedFormatType::HighestResolution(config.resolution()))
}
}
fn camera_index_to_u32(idx: &CameraIndex, fallback: u32) -> u32 {
match idx {
CameraIndex::Index(i) => *i,
CameraIndex::String(_) => fallback,
}
}
/// List available cameras on the system (uses native backend: AVFoundation on macOS, V4L2 on Linux).
pub fn list_cameras() -> Result<Vec<CameraListItem>, String> {
let api = native_api_backend()
.ok_or_else(|| "No native camera backend available".to_string())?;
let infos: Vec<CameraInfo> = query(api).map_err(|e| e.to_string())?;
Ok(infos
.into_iter()
.enumerate()
.map(|(i, info)| CameraListItem {
index: camera_index_to_u32(info.index(), i as u32),
human_name: info.human_name().to_string(),
description: info.description().to_string(),
misc: info.misc().to_string(),
})
.collect())
}
/// Test camera access and return (width, height, frame_size) on success
pub fn test_camera(config: &CameraConfig) -> Result<(u32, u32, usize), String> {
let index = CameraIndex::Index(config.index);
let format = requested_format(config);
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.
/// Maximizes gain and brightness to amplify dark current and thermal noise.
pub fn configure_for_thermal_noise(camera: &mut Camera) {
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),
);
}
}
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),
);
}
}
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),
);
}
}
}
/// Open camera with retry logic. Attempts to reconnect on failure.
pub fn open_camera_with_retry(config: &CameraConfig) -> Result<Camera, String> {
let index = CameraIndex::Index(config.index);
let format = requested_format(config);
for attempt in 1..=MAX_RETRIES {
match Camera::new(index.clone(), format.clone()) {
Ok(mut camera) => {
match camera.open_stream() {
Ok(_) => {
configure_for_thermal_noise(&mut camera);
if attempt > 1 {
eprintln!("[camera] reconnected on attempt {}", attempt);
}
return Ok(camera);
}
Err(e) => {
eprintln!("[camera] open_stream failed (attempt {}): {}", attempt, e);
if attempt < MAX_RETRIES {
thread::sleep(Duration::from_millis(RETRY_DELAY_MS));
}
}
}
}
Err(e) => {
eprintln!("[camera] Camera::new failed (attempt {}): {}", attempt, e);
if attempt < MAX_RETRIES {
thread::sleep(Duration::from_millis(RETRY_DELAY_MS));
}
}
}
}
Err(format!("Failed to open camera index {} after {} attempts", config.index, MAX_RETRIES))
}
/// Try to recover camera connection. Returns new Camera if successful.
pub fn try_reconnect(config: &CameraConfig, error: &str) -> Option<Camera> {
eprintln!("[camera] error: {}, attempting reconnect...", error);
thread::sleep(Duration::from_millis(RECONNECT_DELAY_MS));
match open_camera_with_retry(config) {
Ok(camera) => {
eprintln!("[camera] successfully reconnected");
Some(camera)
}
Err(e) => {
eprintln!("[camera] reconnection failed: {}", e);
None
}
}
}

63
src/entropy/config.rs Normal file
View File

@ -0,0 +1,63 @@
//! Camera configuration and type definitions.
use nokhwa::utils::Resolution;
/// Camera configuration for entropy extraction
#[derive(Clone, Copy)]
pub struct CameraConfig {
pub width: u32,
pub height: u32,
pub index: u32,
}
impl Default for CameraConfig {
fn default() -> Self {
Self {
width: 1920,
height: 1080,
index: 0,
}
}
}
impl CameraConfig {
pub fn from_env() -> Self {
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);
let index = std::env::var("CAMERA_INDEX")
.ok()
.and_then(|i| i.parse().ok())
.unwrap_or(0);
Self { width, height, index }
}
pub fn frame_size(&self) -> usize {
(self.width * self.height * 3) as usize
}
/// Use camera's maximum resolution (and highest frame rate) instead of configured width/height.
pub fn use_max_resolution() -> bool {
std::env::var("CAMERA_MAX_RESOLUTION")
.map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
.unwrap_or(false)
}
pub fn resolution(&self) -> Resolution {
Resolution::new(self.width, self.height)
}
}
/// List of cameras for API response
#[derive(Clone, Debug, serde::Serialize)]
pub struct CameraListItem {
pub index: u32,
pub human_name: String,
pub description: String,
pub misc: String,
}

316
src/entropy/extract.rs Normal file
View File

@ -0,0 +1,316 @@
//! Entropy extraction functions: conditioned and raw.
use std::sync::atomic::{AtomicU64, Ordering};
use nokhwa::utils::CameraIndex;
use nokhwa::Camera;
use sha2::{Digest, Sha256};
use super::camera::{
configure_for_thermal_noise, open_camera_with_retry, requested_format, try_reconnect,
MAX_RETRIES,
};
use super::config::CameraConfig;
/// Bytes of LSB data per hash (8:1 conditioning ratio)
pub const CHUNK_SIZE: usize = 256;
static REQUEST_COUNTER: AtomicU64 = AtomicU64::new(0);
fn nanos_now() -> u128 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
}
/// Extract entropy from camera quantum noise using chunked SHA-256 conditioning.
/// Automatically retries and reconnects on camera errors.
pub fn extract_entropy_camera(num_bytes: usize, config: &CameraConfig) -> Result<Vec<u8>, String> {
let request_id = REQUEST_COUNTER.fetch_add(1, Ordering::SeqCst);
let index = CameraIndex::Index(config.index);
let format = requested_format(config);
let mut camera = Camera::new(index, format).map_err(|e| e.to_string())?;
camera.open_stream().map_err(|e| e.to_string())?;
configure_for_thermal_noise(&mut camera);
let mut entropy = Vec::with_capacity(num_bytes);
let mut hasher = Sha256::new();
let mut consecutive_errors: u32 = 0;
let mut frame_idx: u64 = 0;
while entropy.len() < num_bytes {
let frame = match camera.frame() {
Ok(f) => {
consecutive_errors = 0;
f
}
Err(e) => {
consecutive_errors += 1;
let err_str = e.to_string();
eprintln!(
"[extract] frame failed ({}x): {}",
consecutive_errors, err_str
);
if consecutive_errors >= MAX_RETRIES {
camera.stop_stream().ok();
return Err(format!(
"Too many consecutive frame errors: {}",
err_str
));
}
// Try to reconnect
camera.stop_stream().ok();
match try_reconnect(config, &err_str) {
Some(new_camera) => {
camera = new_camera;
continue;
}
None => {
return Err(format!("Camera reconnection failed: {}", err_str));
}
}
}
};
let raw = frame.buffer();
let lsbs: Vec<u8> = raw.iter().map(|b| b & 0x03).collect();
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)
}
/// Raw LSB bytes from camera (no hashing) - continuous stream of sensor noise.
/// Automatically retries and reconnects on camera errors.
pub fn extract_raw_lsb_camera(num_bytes: usize, config: &CameraConfig) -> Result<Vec<u8>, String> {
let index = CameraIndex::Index(config.index);
let format = requested_format(config);
let mut camera = Camera::new(index, format).map_err(|e| e.to_string())?;
camera.open_stream().map_err(|e| e.to_string())?;
configure_for_thermal_noise(&mut camera);
let mut out = Vec::with_capacity(num_bytes);
let mut consecutive_errors: u32 = 0;
while out.len() < num_bytes {
let frame = match camera.frame() {
Ok(f) => {
consecutive_errors = 0;
f
}
Err(e) => {
consecutive_errors += 1;
let err_str = e.to_string();
eprintln!(
"[raw-lsb] frame failed ({}x): {}",
consecutive_errors, err_str
);
if consecutive_errors >= MAX_RETRIES {
camera.stop_stream().ok();
return Err(format!(
"Too many consecutive frame errors: {}",
err_str
));
}
camera.stop_stream().ok();
match try_reconnect(config, &err_str) {
Some(new_camera) => {
camera = new_camera;
continue;
}
None => {
return Err(format!("Camera reconnection failed: {}", err_str));
}
}
}
};
let raw = frame.buffer();
for b in raw.iter() {
out.push(b & 0x03);
if out.len() >= num_bytes {
break;
}
}
}
camera.stop_stream().ok();
out.truncate(num_bytes);
Ok(out)
}
/// Raw LSB bytes from camera (no hashing).
pub fn extract_raw_lsb(num_bytes: usize, config: &CameraConfig) -> Result<Vec<u8>, String> {
extract_raw_lsb_camera(num_bytes, config)
}
pub fn extract_entropy(num_bytes: usize, config: &CameraConfig) -> Result<Vec<u8>, String> {
extract_entropy_camera(num_bytes, config)
}
/// Fill a buffer with entropy - used by OpenSSL provider
pub fn fill_entropy(out: &mut [u8], config: &CameraConfig) -> Result<(), String> {
let entropy = extract_entropy(out.len(), config)?;
out.copy_from_slice(&entropy);
Ok(())
}
/// Spawns a thread that sends raw LSB bytes (one Vec per frame) until the sender is dropped.
/// Use the returned receiver to stream data; when the receiver is dropped, the thread exits.
/// Automatically reconnects on camera failure.
pub fn spawn_raw_lsb_stream(
config: CameraConfig,
tx: std::sync::mpsc::SyncSender<Vec<u8>>,
) -> Result<(), String> {
std::thread::spawn(move || {
let mut camera = match open_camera_with_retry(&config) {
Ok(c) => c,
Err(e) => {
eprintln!("[stream] initial camera open failed: {}", e);
return;
}
};
let mut consecutive_errors: u32 = 0;
loop {
let frame = match camera.frame() {
Ok(f) => {
consecutive_errors = 0;
f
}
Err(e) => {
consecutive_errors += 1;
let err_str = e.to_string();
eprintln!("[stream] frame failed ({}x): {}", consecutive_errors, err_str);
if consecutive_errors >= MAX_RETRIES {
eprintln!("[stream] too many errors, stopping");
break;
}
camera.stop_stream().ok();
match try_reconnect(&config, &err_str) {
Some(new_camera) => {
camera = new_camera;
continue;
}
None => {
eprintln!("[stream] reconnection failed, stopping");
break;
}
}
}
};
let lsbs: Vec<u8> = frame.buffer().iter().map(|b| b & 0x03).collect();
if tx.send(lsbs).is_err() {
break;
}
}
camera.stop_stream().ok();
});
Ok(())
}
/// Spawns a thread that produces a continuous stream of conditioned (hashed) random bytes.
/// Sends one message on 'ready' when camera is open (Ok) or on failure (Err); then streams on 'tx'.
/// Automatically reconnects on camera failure.
pub fn spawn_entropy_stream(
config: CameraConfig,
tx: std::sync::mpsc::SyncSender<Vec<u8>>,
ready: std::sync::mpsc::SyncSender<Result<(), String>>,
) -> Result<(), String> {
const STREAM_CHUNK_BYTES: usize = 1024;
std::thread::spawn(move || {
let mut camera = match open_camera_with_retry(&config) {
Ok(c) => c,
Err(e) => {
let _ = ready.send(Err(e.to_string()));
return;
}
};
let mut hasher = Sha256::new();
let mut frame_idx: u64 = 0;
let mut first = true;
let mut consecutive_errors: u32 = 0;
loop {
let frame = match camera.frame() {
Ok(f) => {
consecutive_errors = 0;
f
}
Err(e) => {
consecutive_errors += 1;
let err_str = e.to_string();
eprintln!("[entropy-stream] frame failed ({}x): {}", consecutive_errors, err_str);
if consecutive_errors >= MAX_RETRIES {
eprintln!("[entropy-stream] too many errors, stopping");
break;
}
camera.stop_stream().ok();
match try_reconnect(&config, &err_str) {
Some(new_camera) => {
camera = new_camera;
continue;
}
None => {
eprintln!("[entropy-stream] reconnection failed, stopping");
break;
}
}
}
};
let lsbs: Vec<u8> = frame.buffer().iter().map(|b| b & 0x03).collect();
let mut out = Vec::with_capacity(STREAM_CHUNK_BYTES);
for (chunk_idx, chunk) in lsbs.chunks(CHUNK_SIZE).enumerate() {
// Pure camera entropy: only LSB data + position indices
hasher.update(chunk);
hasher.update(&frame_idx.to_le_bytes());
hasher.update(&(chunk_idx as u64).to_le_bytes());
out.extend_from_slice(&hasher.finalize_reset());
if out.len() >= STREAM_CHUNK_BYTES {
break;
}
}
if !out.is_empty() {
if tx.send(out).is_err() {
break;
}
if first {
let _ = ready.send(Ok(()));
first = false;
}
}
frame_idx += 1;
}
camera.stop_stream().ok();
});
Ok(())
}

17
src/entropy/mod.rs Normal file
View File

@ -0,0 +1,17 @@
//! Core entropy extraction from camera quantum noise.
//!
//! Uses the LavaRnd approach: covered camera sensor with high gain
//! captures thermal/quantum noise from the CCD/CMOS dark current.
mod config;
mod pool;
mod camera;
mod extract;
pub use config::{CameraConfig, CameraListItem};
pub use pool::{subscribe_entropy, unsubscribe_entropy, ensure_producer_running};
pub use camera::{list_cameras, test_camera, open_camera_with_retry, try_reconnect};
pub use extract::{
extract_entropy, extract_entropy_camera, extract_raw_lsb, extract_raw_lsb_camera,
fill_entropy, spawn_raw_lsb_stream, spawn_entropy_stream, CHUNK_SIZE,
};

151
src/entropy/pool.rs Normal file
View File

@ -0,0 +1,151 @@
//! Shared entropy pool: single camera feeds multiple consumers.
//! Each chunk goes to exactly one consumer (guarantees uniqueness).
use std::collections::HashMap;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Mutex;
use sha2::{Digest, Sha256};
use super::camera::{open_camera_with_retry, try_reconnect, MAX_RETRIES};
use super::config::CameraConfig;
use super::extract::CHUNK_SIZE;
static GLOBAL_FRAME_COUNTER: AtomicU64 = AtomicU64::new(0);
static ENTROPY_POOL: std::sync::OnceLock<Mutex<EntropyPool>> = std::sync::OnceLock::new();
struct EntropyPool {
subscribers: HashMap<u64, std::sync::mpsc::SyncSender<Vec<u8>>>,
next_id: u64,
producer_running: bool,
}
impl EntropyPool {
fn new() -> Self {
Self {
subscribers: HashMap::new(),
next_id: 0,
producer_running: false,
}
}
}
fn get_pool() -> &'static Mutex<EntropyPool> {
ENTROPY_POOL.get_or_init(|| Mutex::new(EntropyPool::new()))
}
/// Subscribe to the shared entropy pool. Returns (id, receiver).
pub fn subscribe_entropy() -> (u64, std::sync::mpsc::Receiver<Vec<u8>>) {
let (tx, rx) = std::sync::mpsc::sync_channel(4);
let mut pool = get_pool().lock().unwrap();
let id = pool.next_id;
pool.next_id += 1;
pool.subscribers.insert(id, tx);
(id, rx)
}
/// Unsubscribe from the pool.
pub fn unsubscribe_entropy(id: u64) {
let mut pool = get_pool().lock().unwrap();
pool.subscribers.remove(&id);
}
/// Start the shared camera producer if not running. Call after subscribing.
/// Uses automatic reconnection on camera failure.
pub fn ensure_producer_running(config: CameraConfig) {
let mut pool = get_pool().lock().unwrap();
if pool.producer_running {
return;
}
pool.producer_running = true;
drop(pool);
std::thread::spawn(move || {
let mut camera = match open_camera_with_retry(&config) {
Ok(c) => c,
Err(e) => {
eprintln!("[entropy-pool] initial camera open failed: {}", e);
get_pool().lock().unwrap().producer_running = false;
return;
}
};
let mut hasher = Sha256::new();
let mut consecutive_errors: u32 = 0;
loop {
// Global counter ensures uniqueness even if pool restarts with same camera data
let frame_idx = GLOBAL_FRAME_COUNTER.fetch_add(1, Ordering::SeqCst);
// Check if any subscribers remain
{
let pool = get_pool().lock().unwrap();
if pool.subscribers.is_empty() {
eprintln!("[entropy-pool] no subscribers, shutting down producer");
break;
}
}
let frame = match camera.frame() {
Ok(f) => {
consecutive_errors = 0;
f
}
Err(e) => {
consecutive_errors += 1;
let err_str = e.to_string();
eprintln!("[entropy-pool] frame failed ({}x): {}", consecutive_errors, err_str);
if consecutive_errors >= MAX_RETRIES {
eprintln!("[entropy-pool] too many consecutive errors, stopping");
break;
}
// Try to reconnect to the same camera
camera.stop_stream().ok();
match try_reconnect(&config, &err_str) {
Some(new_camera) => {
camera = new_camera;
continue;
}
None => {
eprintln!("[entropy-pool] reconnection failed, stopping");
break;
}
}
}
};
let lsbs: Vec<u8> = frame.buffer().iter().map(|b| b & 0x03).collect();
// Simple: sequential non-overlapping chunks. Each pixel LSB is independent noise.
for (chunk_idx, chunk) in lsbs.chunks(CHUNK_SIZE).enumerate() {
hasher.update(chunk);
hasher.update(&frame_idx.to_le_bytes());
hasher.update(&(chunk_idx as u64).to_le_bytes());
let hash = hasher.finalize_reset().to_vec();
// Send to one subscriber - clone tx and drop lock before blocking send
let (target_id, tx) = {
let pool = get_pool().lock().unwrap();
if pool.subscribers.is_empty() {
break;
}
let ids: Vec<u64> = pool.subscribers.keys().copied().collect();
let id = ids[chunk_idx % ids.len()];
match pool.subscribers.get(&id) {
Some(tx) => (id, tx.clone()),
None => continue,
}
};
// Send without holding lock
if tx.send(hash).is_err() {
// Receiver dropped, remove subscriber
get_pool().lock().unwrap().subscribers.remove(&target_id);
}
}
}
camera.stop_stream().ok();
get_pool().lock().unwrap().producer_running = false;
});
}

18
src/lib.rs Normal file
View File

@ -0,0 +1,18 @@
//! Camera QRNG Library
//!
//! Quantum random number generation using camera sensor thermal noise.
//! Can be used as:
//! - A Rust library
//! - An OpenSSL 3.x provider (cdylib)
//! - A standalone HTTP server (binary)
pub mod entropy;
pub mod provider;
pub use entropy::{
extract_entropy, extract_entropy_camera, fill_entropy,
list_cameras, spawn_raw_lsb_stream, subscribe_entropy, unsubscribe_entropy, ensure_producer_running, test_camera, extract_raw_lsb, CameraConfig, CameraListItem, CHUNK_SIZE,
};
// Re-export the OpenSSL provider init for cdylib
pub use provider::OSSL_provider_init;

View File

@ -1,3 +1,7 @@
//! Camera QRNG HTTP Server
//!
//! Serves quantum random bytes via HTTP API. Also builds as an OpenSSL provider.
use axum::{ use axum::{
body::Body, body::Body,
extract::Query, extract::Query,
@ -6,43 +10,17 @@ use axum::{
routing::get, routing::get,
Router, Router,
}; };
use nokhwa::{ use camera_trng::{extract_entropy, extract_raw_lsb, list_cameras, subscribe_entropy, unsubscribe_entropy, ensure_producer_running, test_camera, CameraConfig, CHUNK_SIZE};
pixel_format::RgbFormat, use bytes::Bytes;
utils::{CameraIndex, ControlValueDescription, ControlValueSetter, KnownCameraControl, RequestedFormat, RequestedFormatType, Resolution}, use std::sync::{Arc, Mutex};
Camera,
};
use sha2::{Digest, Sha256};
use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
use serde_json::json; use serde_json::json;
use std::sync::atomic::{AtomicUsize, Ordering};
// Throughput scales with resolution - at 1080p60: ~23 MB/s conditioned, ~3 Gbps raw const MAX_BYTES_PER_REQUEST: usize = 1024 * 1024; // 1MB
// 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 MAX_CONCURRENT: usize = 4;
const DEFAULT_PORT: u16 = 8787; const DEFAULT_PORT: u16 = 8787;
static ACTIVE_REQUESTS: AtomicUsize = AtomicUsize::new(0); 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)] #[derive(serde::Deserialize)]
struct RandomQuery { struct RandomQuery {
@ -54,34 +32,50 @@ struct RandomQuery {
fn default_bytes() -> usize { 32 } fn default_bytes() -> usize { 32 }
#[derive(serde::Deserialize)]
struct RawQuery {
#[serde(default = "default_raw_bytes")]
bytes: usize,
}
fn default_raw_bytes() -> usize { 65536 }
#[derive(serde::Deserialize)]
struct StreamQuery {
bytes: Option<usize>,
#[serde(default)]
hex: bool,
}
#[tokio::main] #[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> { 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 port = std::env::var("PORT").ok().and_then(|p| p.parse().ok()).unwrap_or(DEFAULT_PORT);
let (width, height) = get_resolution(); let config = CameraConfig::from_env();
if is_fake_camera() { println!("Testing camera access...");
println!("FAKE_CAMERA mode enabled - using /dev/urandom for entropy"); match test_camera(&config) {
} else { Ok((actual_w, actual_h, frame_size)) => {
println!("Testing camera access..."); let conditioned_per_frame = (frame_size / CHUNK_SIZE) * 32;
match test_camera(width, height) { let throughput_30fps = conditioned_per_frame * 30;
Ok((actual_w, actual_h, frame_size)) => { let raw_gbps = (frame_size as f64 * 30.0 * 8.0) / 1_000_000_000.0;
let conditioned_per_frame = (frame_size / CHUNK_SIZE) * 32; println!("Camera OK at {}x{} - {} bytes/frame", actual_w, actual_h, frame_size);
let throughput_30fps = conditioned_per_frame * 30; println!("Raw throughput: {:.1} Gbps at 30fps", raw_gbps);
let raw_gbps = (frame_size as f64 * 30.0 * 8.0) / 1_000_000_000.0; println!("Conditioned output: ~{} MB/s at 30fps (8:1 ratio)", throughput_30fps / 1_000_000);
println!("Camera OK at {}x{} - {} bytes/frame", actual_w, actual_h, frame_size); println!("Ensure lens is covered for optimal quantum noise capture");
println!("Raw throughput: {:.1} Gbps at 30fps", raw_gbps); }
println!("Conditioned output: ~{} MB/s at 30fps (8:1 ratio)", throughput_30fps / 1_000_000); Err(e) => {
println!("Ensure lens is covered for optimal quantum noise capture"); eprintln!("Camera error: {}. Server will still start.", e);
} if e.contains("Lock Rejected") || e.contains("lock") {
Err(e) => { eprintln!(" → To release camera: ./scripts/release-camera.sh then restart.");
eprintln!("Camera error: {}. Server will still start.", e);
} }
} }
} }
let app = Router::new() let app = Router::new()
.route("/", get(index)) .route("/", get(index))
.route("/cameras", get(get_cameras))
.route("/random", get(get_random)) .route("/random", get(get_random))
.route("/raw", get(get_raw))
.route("/stream", get(get_stream))
.route("/health", get(health)) .route("/health", get(health))
.route("/.well-known/mcp.json", get(mcp_wellknown)); .route("/.well-known/mcp.json", get(mcp_wellknown));
@ -95,6 +89,14 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
async fn index() -> Html<&'static str> { Html(INDEX_HTML) } async fn index() -> Html<&'static str> { Html(INDEX_HTML) }
async fn health() -> &'static str { "ok" } async fn health() -> &'static str { "ok" }
async fn get_cameras() -> Response {
match tokio::task::spawn_blocking(list_cameras).await {
Ok(Ok(cameras)) => Json(serde_json::json!({ "cameras": cameras })).into_response(),
Ok(Err(e)) => (StatusCode::INTERNAL_SERVER_ERROR, e).into_response(),
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
}
}
async fn mcp_wellknown() -> Json<serde_json::Value> { async fn mcp_wellknown() -> Json<serde_json::Value> {
Json(json!({ Json(json!({
"mcp": { "mcp": {
@ -103,97 +105,37 @@ async fn mcp_wellknown() -> Json<serde_json::Value> {
"servers": [], "servers": [],
"tools": [{ "tools": [{
"name": "camera-qrng", "name": "camera-qrng",
"description": "High-throughput quantum RNG using thermal noise from covered camera sensor - Gbps of raw quantum entropy", "description": "High-throughput quantum RNG using thermal noise from covered camera sensor",
"url_template": "{origin}/random?bytes={bytes}&hex={hex}", "url_template": "{origin}/random?bytes={bytes}&hex={hex}",
"capabilities": ["random-generation", "entropy-source", "quantum"], "capabilities": ["random-generation", "entropy-source", "quantum"],
"auth": { "type": "none" }, "auth": { "type": "none" },
"parameters": { "parameters": {
"bytes": { "type": "integer", "default": 32, "max": 1048576, "description": "Number of random bytes (up to 1MB)" }, "bytes": { "type": "integer", "default": 32, "max": 1048576 },
"hex": { "type": "boolean", "default": false, "description": "Return hex-encoded string" } "hex": { "type": "boolean", "default": false }
} }
}] }]
} }
})) }))
} }
/// 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 { async fn get_random(Query(params): Query<RandomQuery>) -> Response {
let current = ACTIVE_REQUESTS.fetch_add(1, Ordering::SeqCst); let current = ACTIVE_REQUESTS.fetch_add(1, Ordering::SeqCst);
if current >= MAX_CONCURRENT { if current >= MAX_CONCURRENT {
ACTIVE_REQUESTS.fetch_sub(1, Ordering::SeqCst); ACTIVE_REQUESTS.fetch_sub(1, Ordering::SeqCst);
return (StatusCode::TOO_MANY_REQUESTS, "Too many requests").into_response(); return (StatusCode::TOO_MANY_REQUESTS, "Too many requests").into_response();
} }
let bytes = params.bytes.min(MAX_BYTES_PER_REQUEST); let bytes = params.bytes.min(MAX_BYTES_PER_REQUEST);
if bytes == 0 { if bytes == 0 {
ACTIVE_REQUESTS.fetch_sub(1, Ordering::SeqCst); ACTIVE_REQUESTS.fetch_sub(1, Ordering::SeqCst);
return (StatusCode::BAD_REQUEST, "bytes must be > 0").into_response(); 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 config = CameraConfig::from_env();
let result = tokio::task::spawn_blocking(move || { let result = tokio::task::spawn_blocking(move || {
if use_fake { extract_entropy(bytes, &config)
extract_entropy_fake(bytes, request_id)
} else {
extract_entropy_camera(bytes, request_id)
}
}).await; }).await;
ACTIVE_REQUESTS.fetch_sub(1, Ordering::SeqCst); ACTIVE_REQUESTS.fetch_sub(1, Ordering::SeqCst);
match result { match result {
@ -203,6 +145,7 @@ async fn get_random(Query(params): Query<RandomQuery>) -> Response {
.body(Body::from(hex::encode(&data))).unwrap() .body(Body::from(hex::encode(&data))).unwrap()
} else { } else {
Response::builder().header(header::CONTENT_TYPE, "application/octet-stream") Response::builder().header(header::CONTENT_TYPE, "application/octet-stream")
.header(header::CACHE_CONTROL, "no-store")
.body(Body::from(data)).unwrap() .body(Body::from(data)).unwrap()
} }
} }
@ -211,104 +154,71 @@ async fn get_random(Query(params): Query<RandomQuery>) -> Response {
} }
} }
/// Fake entropy source using /dev/urandom - for testing without camera hardware.
/// Simulates high-resolution camera frames for realistic throughput testing. async fn get_raw(Query(params): Query<RawQuery>) -> Response {
fn extract_entropy_fake(num_bytes: usize, request_id: u64) -> Result<Vec<u8>, String> { let bytes = params.bytes.min(MAX_BYTES_PER_REQUEST);
use std::io::Read; if bytes == 0 {
return (StatusCode::BAD_REQUEST, "bytes must be > 0").into_response();
let (width, height) = get_resolution(); }
let frame_size = (width * height * 3) as usize; let config = CameraConfig::from_env();
match tokio::task::spawn_blocking(move || extract_raw_lsb(bytes, &config)).await {
let mut entropy = Vec::with_capacity(num_bytes); Ok(Ok(data)) => Response::builder()
let mut hasher = Sha256::new(); .header(header::CONTENT_TYPE, "application/octet-stream")
.header(header::CACHE_CONTROL, "no-store")
let mut urandom = std::fs::File::open("/dev/urandom").map_err(|e| e.to_string())?; .body(Body::from(data))
let mut fake_frame = vec![0u8; frame_size]; .unwrap(),
Ok(Err(e)) => (StatusCode::INTERNAL_SERVER_ERROR, e).into_response(),
let mut frame_idx: u64 = 0; Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
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. /// Cryptographically sound continuous random. GET /stream or /stream?bytes=N.
/// /// Multiple streams get different data (each chunk goes to one consumer).
/// Throughput scales with camera resolution: async fn get_stream(Query(params): Query<StreamQuery>) -> Response {
/// - 640x480 @ 30fps: ~27 MB/s raw (~216 Mbps), ~3.4 MB/s conditioned let config = CameraConfig::from_env();
/// - 1080p @ 30fps: ~186 MB/s raw (~1.5 Gbps), ~23 MB/s conditioned let (sub_id, rx) = subscribe_entropy();
/// - 1080p @ 60fps: ~373 MB/s raw (~3 Gbps), ~47 MB/s conditioned ensure_producer_running(config);
/// - 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) let rx = Arc::new(Mutex::new(rx));
configure_for_thermal_noise(&mut camera); let limit = params.bytes;
let hex = params.hex;
let mut entropy = Vec::with_capacity(num_bytes); let stream = async_stream::stream! {
let mut hasher = Sha256::new(); let mut sent: usize = 0;
loop {
let mut frame_idx: u64 = 0; if limit.is_some() && sent >= limit.unwrap() { break; }
while entropy.len() < num_bytes { let rx = Arc::clone(&rx);
let frame = camera.frame().map_err(|e| e.to_string())?; let chunk = tokio::task::spawn_blocking(move || rx.lock().unwrap().recv()).await;
let raw = frame.buffer(); match chunk {
Ok(Ok(vec)) => {
// Extract LSBs (2 bits per byte - highest entropy density in quantum noise) let take = match limit {
let lsbs: Vec<u8> = raw.iter().map(|b| b & 0x03).collect(); None => vec.len(),
Some(n) => {
// Hash in chunks - each CHUNK_SIZE bytes produces 32 bytes conditioned output let left = n.saturating_sub(sent);
// At 1080p: ~24,300 chunks/frame = ~778 KB conditioned per frame left.min(vec.len())
// 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); if take == 0 { break; }
hasher.update(&request_id.to_le_bytes()); sent += take;
hasher.update(&frame_idx.to_le_bytes()); let bytes = vec[..take].to_vec();
hasher.update(&(chunk_idx as u64).to_le_bytes()); let payload: Bytes = if hex {
hasher.update(&nanos_now().to_le_bytes()); Bytes::from(hex::encode(&bytes))
} else {
entropy.extend_from_slice(&hasher.finalize_reset()); Bytes::from(bytes)
};
if entropy.len() >= num_bytes { yield Ok::<_, std::io::Error>(payload);
break; }
_ => break,
} }
} }
frame_idx += 1; unsubscribe_entropy(sub_id);
} };
let content_type = if hex { "text/plain" } else { "application/octet-stream" };
camera.stop_stream().ok(); Response::builder()
entropy.truncate(num_bytes); .header(header::CONTENT_TYPE, content_type)
Ok(entropy) .header(header::CACHE_CONTROL, "no-store")
.body(Body::from_stream(stream))
.unwrap()
} }
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"); const INDEX_HTML: &str = include_str!("index.html");

236
src/provider.rs Normal file
View File

@ -0,0 +1,236 @@
//! OpenSSL 3.x Provider implementation for Camera QRNG.
//!
//! Provides the FFI layer for OpenSSL to use our camera-based
//! entropy source as a RAND provider.
use crate::entropy::{fill_entropy, CameraConfig};
use std::ffi::{c_char, c_int, c_uchar, c_uint, c_void};
use std::ptr;
use std::sync::{Mutex, OnceLock};
// OpenSSL function dispatch IDs for RAND
const OSSL_FUNC_RAND_NEWCTX: c_int = 1;
const OSSL_FUNC_RAND_FREECTX: c_int = 2;
const OSSL_FUNC_RAND_INSTANTIATE: c_int = 3;
const OSSL_FUNC_RAND_UNINSTANTIATE: c_int = 4;
const OSSL_FUNC_RAND_GENERATE: c_int = 5;
const OSSL_FUNC_RAND_RESEED: c_int = 6;
const OSSL_FUNC_RAND_GET_CTX_PARAMS: c_int = 9;
const OSSL_FUNC_RAND_GETTABLE_CTX_PARAMS: c_int = 11;
const OSSL_FUNC_RAND_ENABLE_LOCKING: c_int = 12;
const OSSL_FUNC_RAND_LOCK: c_int = 13;
const OSSL_FUNC_RAND_UNLOCK: c_int = 14;
// Provider function IDs
const OSSL_FUNC_PROVIDER_TEARDOWN: c_int = 1;
const OSSL_FUNC_PROVIDER_GETTABLE_PARAMS: c_int = 2;
const OSSL_FUNC_PROVIDER_GET_PARAMS: c_int = 3;
const OSSL_FUNC_PROVIDER_QUERY_OPERATION: c_int = 4;
// Operation IDs
const OSSL_OP_RAND: c_int = 20;
// OSSL_PARAM types
const OSSL_PARAM_UTF8_PTR: c_uint = 6;
const OSSL_PARAM_UNSIGNED_INTEGER: c_uint = 2;
/// OSSL_DISPATCH structure - function pointer table
#[repr(C)]
#[derive(Clone, Copy)]
pub struct OsslDispatch {
pub function_id: c_int,
pub function: Option<unsafe extern "C" fn()>,
}
unsafe impl Sync for OsslDispatch {}
unsafe impl Send for OsslDispatch {}
/// OSSL_PARAM structure for parameter passing
#[repr(C)]
#[derive(Clone, Copy)]
pub struct OsslParam {
pub key: *const c_char,
pub data_type: c_uint,
pub data: *mut c_void,
pub data_size: usize,
pub return_size: usize,
}
unsafe impl Sync for OsslParam {}
unsafe impl Send for OsslParam {}
/// OSSL_ALGORITHM structure for algorithm registration
#[repr(C)]
#[derive(Clone, Copy)]
pub struct OsslAlgorithm {
pub algorithm_names: *const c_char,
pub property_definition: *const c_char,
pub implementation: *const OsslDispatch,
pub algorithm_description: *const c_char,
}
unsafe impl Sync for OsslAlgorithm {}
unsafe impl Send for OsslAlgorithm {}
struct ProviderCtx { config: CameraConfig }
struct RandCtx { config: CameraConfig, lock: Mutex<()> }
// Static strings (null-terminated)
static RAND_NAME: &[u8] = b"camera-qrng\0";
static RAND_PROPS: &[u8] = b"provider=camera-qrng\0";
static RAND_DESC: &[u8] = b"Camera-based Quantum Random Number Generator\0";
static PARAM_STATE: &[u8] = b"state\0";
static PARAM_STRENGTH: &[u8] = b"strength\0";
static PARAM_MAX_REQUEST: &[u8] = b"max_request\0";
static PARAM_NAME: &[u8] = b"name\0";
static PARAM_VERSION: &[u8] = b"version\0";
static PARAM_STATUS: &[u8] = b"status\0";
static PROVIDER_NAME_VAL: &[u8] = b"Camera QRNG Provider\0";
static PROVIDER_VERSION_VAL: &[u8] = b"0.1.0\0";
// Lazily initialized dispatch tables
static RAND_DISPATCH: OnceLock<[OsslDispatch; 12]> = OnceLock::new();
static RAND_ALGORITHMS: OnceLock<[OsslAlgorithm; 2]> = OnceLock::new();
static PROVIDER_DISPATCH: OnceLock<[OsslDispatch; 5]> = OnceLock::new();
static GETTABLE_PARAMS: OnceLock<[OsslParam; 4]> = OnceLock::new();
static PROVIDER_GETTABLE: OnceLock<[OsslParam; 4]> = OnceLock::new();
fn init_gettable_params() -> [OsslParam; 4] {
[
OsslParam { key: PARAM_STATE.as_ptr() as _, data_type: OSSL_PARAM_UNSIGNED_INTEGER, data: ptr::null_mut(), data_size: 4, return_size: 0 },
OsslParam { key: PARAM_STRENGTH.as_ptr() as _, data_type: OSSL_PARAM_UNSIGNED_INTEGER, data: ptr::null_mut(), data_size: 4, return_size: 0 },
OsslParam { key: PARAM_MAX_REQUEST.as_ptr() as _, data_type: OSSL_PARAM_UNSIGNED_INTEGER, data: ptr::null_mut(), data_size: 4, return_size: 0 },
OsslParam { key: ptr::null(), data_type: 0, data: ptr::null_mut(), data_size: 0, return_size: 0 },
]
}
fn init_provider_gettable() -> [OsslParam; 4] {
[
OsslParam { key: PARAM_NAME.as_ptr() as _, data_type: OSSL_PARAM_UTF8_PTR, data: ptr::null_mut(), data_size: 0, return_size: 0 },
OsslParam { key: PARAM_VERSION.as_ptr() as _, data_type: OSSL_PARAM_UTF8_PTR, data: ptr::null_mut(), data_size: 0, return_size: 0 },
OsslParam { key: PARAM_STATUS.as_ptr() as _, data_type: OSSL_PARAM_UNSIGNED_INTEGER, data: ptr::null_mut(), data_size: 4, return_size: 0 },
OsslParam { key: ptr::null(), data_type: 0, data: ptr::null_mut(), data_size: 0, return_size: 0 },
]
}
// --- RAND Functions ---
unsafe extern "C" fn rand_newctx(provctx: *mut c_void, _parent: *mut c_void, _pd: *const OsslDispatch) -> *mut c_void {
if provctx.is_null() { return ptr::null_mut(); }
Box::into_raw(Box::new(RandCtx { config: (*(provctx as *const ProviderCtx)).config, lock: Mutex::new(()) })) as _
}
unsafe extern "C" fn rand_freectx(ctx: *mut c_void) { if !ctx.is_null() { drop(Box::from_raw(ctx as *mut RandCtx)); } }
unsafe extern "C" fn rand_instantiate(_: *mut c_void, _: c_uint, _: c_int, _: *const c_uchar, _: usize, _: *const OsslParam) -> c_int { 1 }
unsafe extern "C" fn rand_uninstantiate(_: *mut c_void) -> c_int { 1 }
unsafe extern "C" fn rand_generate(ctx: *mut c_void, out: *mut c_uchar, len: usize, _: c_uint, _: c_int, _: *const c_uchar, _: usize) -> c_int {
if ctx.is_null() || out.is_null() || len == 0 { return 0; }
let rctx = &*(ctx as *const RandCtx);
let _g = match rctx.lock.lock() { Ok(g) => g, Err(_) => return 0 };
match fill_entropy(std::slice::from_raw_parts_mut(out, len), &rctx.config) { Ok(()) => 1, Err(_) => 0 }
}
unsafe extern "C" fn rand_reseed(_: *mut c_void, _: c_int, _: *const c_uchar, _: usize, _: *const c_uchar, _: usize) -> c_int { 1 }
unsafe extern "C" fn rand_enable_locking(_: *mut c_void) -> c_int { 1 }
unsafe extern "C" fn rand_lock(_: *mut c_void) -> c_int { 1 }
unsafe extern "C" fn rand_unlock(_: *mut c_void) -> c_int { 1 }
unsafe extern "C" fn rand_get_ctx_params(_ctx: *mut c_void, params: *mut OsslParam) -> c_int {
if params.is_null() { return 0; }
let mut p = params;
while !(*p).key.is_null() {
let k = std::ffi::CStr::from_ptr((*p).key).to_bytes();
if k == &PARAM_STATE[..PARAM_STATE.len()-1] && (*p).data_type == OSSL_PARAM_UNSIGNED_INTEGER && !(*p).data.is_null() {
*((*p).data as *mut c_uint) = 1; (*p).return_size = 4;
} else if k == &PARAM_STRENGTH[..PARAM_STRENGTH.len()-1] && (*p).data_type == OSSL_PARAM_UNSIGNED_INTEGER && !(*p).data.is_null() {
*((*p).data as *mut c_uint) = 256; (*p).return_size = 4;
} else if k == &PARAM_MAX_REQUEST[..PARAM_MAX_REQUEST.len()-1] && (*p).data_type == OSSL_PARAM_UNSIGNED_INTEGER && !(*p).data.is_null() {
*((*p).data as *mut c_uint) = 1024*1024; (*p).return_size = 4;
}
p = p.add(1);
}
1
}
unsafe extern "C" fn rand_gettable_ctx_params(_: *mut c_void, _: *mut c_void) -> *const OsslParam {
GETTABLE_PARAMS.get_or_init(init_gettable_params).as_ptr()
}
// --- Provider Functions ---
unsafe extern "C" fn provider_teardown(ctx: *mut c_void) { if !ctx.is_null() { drop(Box::from_raw(ctx as *mut ProviderCtx)); } }
unsafe extern "C" fn provider_gettable_params(_: *mut c_void) -> *const OsslParam {
PROVIDER_GETTABLE.get_or_init(init_provider_gettable).as_ptr()
}
unsafe extern "C" fn provider_get_params(_provctx: *mut c_void, params: *mut OsslParam) -> c_int {
if params.is_null() { return 0; }
let mut p = params;
while !(*p).key.is_null() {
let k = std::ffi::CStr::from_ptr((*p).key).to_bytes();
if k == &PARAM_NAME[..PARAM_NAME.len()-1] && (*p).data_type == OSSL_PARAM_UTF8_PTR && !(*p).data.is_null() {
*((*p).data as *mut *const c_char) = PROVIDER_NAME_VAL.as_ptr() as _;
(*p).return_size = PROVIDER_NAME_VAL.len() - 1;
} else if k == &PARAM_VERSION[..PARAM_VERSION.len()-1] && (*p).data_type == OSSL_PARAM_UTF8_PTR && !(*p).data.is_null() {
*((*p).data as *mut *const c_char) = PROVIDER_VERSION_VAL.as_ptr() as _;
(*p).return_size = PROVIDER_VERSION_VAL.len() - 1;
} else if k == &PARAM_STATUS[..PARAM_STATUS.len()-1] && (*p).data_type == OSSL_PARAM_UNSIGNED_INTEGER && !(*p).data.is_null() {
*((*p).data as *mut c_uint) = 1; // active
(*p).return_size = 4;
}
p = p.add(1);
}
1
}
unsafe extern "C" fn provider_query_operation(_: *mut c_void, op: c_int, _: *mut c_int) -> *const OsslAlgorithm {
if op == OSSL_OP_RAND { RAND_ALGORITHMS.get_or_init(init_rand_algorithms).as_ptr() } else { ptr::null() }
}
fn dispatch(id: c_int, f: unsafe extern "C" fn()) -> OsslDispatch {
OsslDispatch { function_id: id, function: Some(f) }
}
fn init_rand_dispatch() -> [OsslDispatch; 12] {
[
dispatch(OSSL_FUNC_RAND_NEWCTX, unsafe { std::mem::transmute(rand_newctx as *const ()) }),
dispatch(OSSL_FUNC_RAND_FREECTX, unsafe { std::mem::transmute(rand_freectx as *const ()) }),
dispatch(OSSL_FUNC_RAND_INSTANTIATE, unsafe { std::mem::transmute(rand_instantiate as *const ()) }),
dispatch(OSSL_FUNC_RAND_UNINSTANTIATE, unsafe { std::mem::transmute(rand_uninstantiate as *const ()) }),
dispatch(OSSL_FUNC_RAND_GENERATE, unsafe { std::mem::transmute(rand_generate as *const ()) }),
dispatch(OSSL_FUNC_RAND_RESEED, unsafe { std::mem::transmute(rand_reseed as *const ()) }),
dispatch(OSSL_FUNC_RAND_GET_CTX_PARAMS, unsafe { std::mem::transmute(rand_get_ctx_params as *const ()) }),
dispatch(OSSL_FUNC_RAND_GETTABLE_CTX_PARAMS, unsafe { std::mem::transmute(rand_gettable_ctx_params as *const ()) }),
dispatch(OSSL_FUNC_RAND_ENABLE_LOCKING, unsafe { std::mem::transmute(rand_enable_locking as *const ()) }),
dispatch(OSSL_FUNC_RAND_LOCK, unsafe { std::mem::transmute(rand_lock as *const ()) }),
dispatch(OSSL_FUNC_RAND_UNLOCK, unsafe { std::mem::transmute(rand_unlock as *const ()) }),
OsslDispatch { function_id: 0, function: None },
]
}
fn init_rand_algorithms() -> [OsslAlgorithm; 2] {
[
OsslAlgorithm {
algorithm_names: RAND_NAME.as_ptr() as _,
property_definition: RAND_PROPS.as_ptr() as _,
implementation: RAND_DISPATCH.get_or_init(init_rand_dispatch).as_ptr(),
algorithm_description: RAND_DESC.as_ptr() as _,
},
OsslAlgorithm { algorithm_names: ptr::null(), property_definition: ptr::null(), implementation: ptr::null(), algorithm_description: ptr::null() },
]
}
fn init_provider_dispatch() -> [OsslDispatch; 5] {
[
dispatch(OSSL_FUNC_PROVIDER_TEARDOWN, unsafe { std::mem::transmute(provider_teardown as *const ()) }),
dispatch(OSSL_FUNC_PROVIDER_GETTABLE_PARAMS, unsafe { std::mem::transmute(provider_gettable_params as *const ()) }),
dispatch(OSSL_FUNC_PROVIDER_GET_PARAMS, unsafe { std::mem::transmute(provider_get_params as *const ()) }),
dispatch(OSSL_FUNC_PROVIDER_QUERY_OPERATION, unsafe { std::mem::transmute(provider_query_operation as *const ()) }),
OsslDispatch { function_id: 0, function: None },
]
}
/// OpenSSL provider entry point
#[no_mangle]
pub unsafe extern "C" fn OSSL_provider_init(
_handle: *const c_void, _in: *const OsslDispatch, out: *mut *const OsslDispatch, ctx: *mut *mut c_void,
) -> c_int {
*ctx = Box::into_raw(Box::new(ProviderCtx { config: CameraConfig::from_env() })) as _;
*out = PROVIDER_DISPATCH.get_or_init(init_provider_dispatch).as_ptr();
1
}