diff --git a/.gitignore b/.gitignore index 94676fd..b54a61f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,11 @@ /target scripts/node_modules/ + +# Release build output +/release/ + +# Man pages (generated) +/man/*.gz + +# Test artifacts +random.bin diff --git a/Cargo.lock b/Cargo.lock index 99f8503..b137382 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,28 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "async-trait" version = "0.1.89" @@ -173,7 +195,10 @@ checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" name = "camera-trng" version = "0.1.0" dependencies = [ + "async-stream", "axum", + "bytes", + "futures-util", "hex", "nokhwa", "serde", diff --git a/Cargo.toml b/Cargo.toml index b52ead2..6081742 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,14 @@ name = "camera-trng" version = "0.1.0" 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] nokhwa = { version = "0.10", features = ["input-native"] } @@ -12,6 +19,9 @@ sha2 = "0.10" hex = "0.4" serde = { version = "1", features = ["derive"] } serde_json = "1" +bytes = "1" +futures-util = { version = "0.3", default-features = false, features = ["alloc"] } +async-stream = "0.3" [profile.release] opt-level = "z" diff --git a/README.md b/README.md index 08d200d..f07ff7d 100644 --- a/README.md +++ b/README.md @@ -36,8 +36,29 @@ cargo build --release ./target/release/camera-trng # Or set a custom port 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 Pull the pre-built image: diff --git a/man/camera-qrng.1 b/man/camera-qrng.1 new file mode 100644 index 0000000..239136f --- /dev/null +++ b/man/camera-qrng.1 @@ -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 diff --git a/openssl-camera-qrng.cnf b/openssl-camera-qrng.cnf new file mode 100644 index 0000000..c070aa9 --- /dev/null +++ b/openssl-camera-qrng.cnf @@ -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 diff --git a/scripts/build-release.sh b/scripts/build-release.sh new file mode 100755 index 0000000..177969b --- /dev/null +++ b/scripts/build-release.sh @@ -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 "$@" diff --git a/scripts/release-camera.sh b/scripts/release-camera.sh new file mode 100755 index 0000000..d73d261 --- /dev/null +++ b/scripts/release-camera.sh @@ -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)" diff --git a/scripts/stream-demo.sh b/scripts/stream-demo.sh new file mode 100755 index 0000000..3de27db --- /dev/null +++ b/scripts/stream-demo.sh @@ -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.$$ diff --git a/scripts/stream-random.sh b/scripts/stream-random.sh new file mode 100755 index 0000000..0a28ffb --- /dev/null +++ b/scripts/stream-random.sh @@ -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 diff --git a/scripts/test-openssl-provider.sh b/scripts/test-openssl-provider.sh new file mode 100755 index 0000000..f08be7c --- /dev/null +++ b/scripts/test-openssl-provider.sh @@ -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!" diff --git a/src/entropy/camera.rs b/src/entropy/camera.rs new file mode 100644 index 0000000..c99ef49 --- /dev/null +++ b/src/entropy/camera.rs @@ -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::(RequestedFormatType::AbsoluteHighestResolution) + } else { + RequestedFormat::new::(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, String> { + let api = native_api_backend() + .ok_or_else(|| "No native camera backend available".to_string())?; + let infos: Vec = 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 { + 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 { + 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 { + 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 + } + } +} diff --git a/src/entropy/config.rs b/src/entropy/config.rs new file mode 100644 index 0000000..020ee1f --- /dev/null +++ b/src/entropy/config.rs @@ -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, +} diff --git a/src/entropy/extract.rs b/src/entropy/extract.rs new file mode 100644 index 0000000..9401186 --- /dev/null +++ b/src/entropy/extract.rs @@ -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, 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 = 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, 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, String> { + extract_raw_lsb_camera(num_bytes, config) +} + +pub fn extract_entropy(num_bytes: usize, config: &CameraConfig) -> Result, 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>, +) -> 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 = 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>, + ready: std::sync::mpsc::SyncSender>, +) -> 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 = 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(()) +} diff --git a/src/entropy/mod.rs b/src/entropy/mod.rs new file mode 100644 index 0000000..5931f9f --- /dev/null +++ b/src/entropy/mod.rs @@ -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, +}; diff --git a/src/entropy/pool.rs b/src/entropy/pool.rs new file mode 100644 index 0000000..b4fbbda --- /dev/null +++ b/src/entropy/pool.rs @@ -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> = std::sync::OnceLock::new(); + +struct EntropyPool { + subscribers: HashMap>>, + 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 { + 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>) { + 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 = 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 = 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; + }); +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..8c50776 --- /dev/null +++ b/src/lib.rs @@ -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; diff --git a/src/main.rs b/src/main.rs index ccd4728..87c8626 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,7 @@ +//! Camera QRNG HTTP Server +//! +//! Serves quantum random bytes via HTTP API. Also builds as an OpenSSL provider. + use axum::{ body::Body, extract::Query, @@ -6,43 +10,17 @@ use axum::{ routing::get, Router, }; -use nokhwa::{ - pixel_format::RgbFormat, - utils::{CameraIndex, ControlValueDescription, ControlValueSetter, KnownCameraControl, RequestedFormat, RequestedFormatType, Resolution}, - Camera, -}; -use sha2::{Digest, Sha256}; -use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering}; +use camera_trng::{extract_entropy, extract_raw_lsb, list_cameras, subscribe_entropy, unsubscribe_entropy, ensure_producer_running, test_camera, CameraConfig, CHUNK_SIZE}; +use bytes::Bytes; +use std::sync::{Arc, Mutex}; use serde_json::json; +use std::sync::atomic::{AtomicUsize, Ordering}; -// Throughput scales with resolution - at 1080p60: ~23 MB/s conditioned, ~3 Gbps raw -// With 4K60: ~93 MB/s conditioned, ~12 Gbps raw quantum noise -const MAX_BYTES_PER_REQUEST: usize = 1024 * 1024; // 1MB per request (high-res enables this) -const CHUNK_SIZE: usize = 256; // Bytes of LSB data per hash (8:1 conditioning ratio) +const MAX_BYTES_PER_REQUEST: usize = 1024 * 1024; // 1MB const MAX_CONCURRENT: usize = 4; const DEFAULT_PORT: u16 = 8787; static ACTIVE_REQUESTS: AtomicUsize = AtomicUsize::new(0); -static REQUEST_COUNTER: AtomicU64 = AtomicU64::new(0); - -fn is_fake_camera() -> bool { - std::env::var("FAKE_CAMERA") - .map(|v| v == "1" || v.to_lowercase() == "true") - .unwrap_or(false) -} - -/// Get requested resolution from environment, defaulting to 1080p for high throughput -fn get_resolution() -> (u32, u32) { - let width = std::env::var("CAMERA_WIDTH") - .ok() - .and_then(|w| w.parse().ok()) - .unwrap_or(1920); - let height = std::env::var("CAMERA_HEIGHT") - .ok() - .and_then(|h| h.parse().ok()) - .unwrap_or(1080); - (width, height) -} #[derive(serde::Deserialize)] struct RandomQuery { @@ -54,34 +32,50 @@ struct RandomQuery { 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, + #[serde(default)] + hex: bool, +} + #[tokio::main] async fn main() -> Result<(), Box> { 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!("FAKE_CAMERA mode enabled - using /dev/urandom for entropy"); - } else { - println!("Testing camera access..."); - match test_camera(width, height) { - Ok((actual_w, actual_h, frame_size)) => { - let conditioned_per_frame = (frame_size / CHUNK_SIZE) * 32; - let throughput_30fps = conditioned_per_frame * 30; - let raw_gbps = (frame_size as f64 * 30.0 * 8.0) / 1_000_000_000.0; - println!("Camera OK at {}x{} - {} bytes/frame", actual_w, actual_h, frame_size); - println!("Raw throughput: {:.1} Gbps at 30fps", raw_gbps); - println!("Conditioned output: ~{} MB/s at 30fps (8:1 ratio)", throughput_30fps / 1_000_000); - println!("Ensure lens is covered for optimal quantum noise capture"); - } - Err(e) => { - eprintln!("Camera error: {}. Server will still start.", e); + println!("Testing camera access..."); + match test_camera(&config) { + Ok((actual_w, actual_h, frame_size)) => { + let conditioned_per_frame = (frame_size / CHUNK_SIZE) * 32; + let throughput_30fps = conditioned_per_frame * 30; + let raw_gbps = (frame_size as f64 * 30.0 * 8.0) / 1_000_000_000.0; + println!("Camera OK at {}x{} - {} bytes/frame", actual_w, actual_h, frame_size); + println!("Raw throughput: {:.1} Gbps at 30fps", raw_gbps); + println!("Conditioned output: ~{} MB/s at 30fps (8:1 ratio)", throughput_30fps / 1_000_000); + println!("Ensure lens is covered for optimal quantum noise capture"); + } + Err(e) => { + eprintln!("Camera error: {}. Server will still start.", e); + if e.contains("Lock Rejected") || e.contains("lock") { + eprintln!(" → To release camera: ./scripts/release-camera.sh then restart."); } } } let app = Router::new() .route("/", get(index)) + .route("/cameras", get(get_cameras)) .route("/random", get(get_random)) + .route("/raw", get(get_raw)) + .route("/stream", get(get_stream)) .route("/health", get(health)) .route("/.well-known/mcp.json", get(mcp_wellknown)); @@ -95,6 +89,14 @@ async fn main() -> Result<(), Box> { async fn index() -> Html<&'static str> { Html(INDEX_HTML) } 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 { Json(json!({ "mcp": { @@ -103,97 +105,37 @@ async fn mcp_wellknown() -> Json { "servers": [], "tools": [{ "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}", "capabilities": ["random-generation", "entropy-source", "quantum"], "auth": { "type": "none" }, "parameters": { - "bytes": { "type": "integer", "default": 32, "max": 1048576, "description": "Number of random bytes (up to 1MB)" }, - "hex": { "type": "boolean", "default": false, "description": "Return hex-encoded string" } + "bytes": { "type": "integer", "default": 32, "max": 1048576 }, + "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::(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 { - 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) -> Response { let current = ACTIVE_REQUESTS.fetch_add(1, Ordering::SeqCst); if current >= MAX_CONCURRENT { ACTIVE_REQUESTS.fetch_sub(1, Ordering::SeqCst); return (StatusCode::TOO_MANY_REQUESTS, "Too many requests").into_response(); } + let bytes = params.bytes.min(MAX_BYTES_PER_REQUEST); if bytes == 0 { ACTIVE_REQUESTS.fetch_sub(1, Ordering::SeqCst); return (StatusCode::BAD_REQUEST, "bytes must be > 0").into_response(); } - let request_id = REQUEST_COUNTER.fetch_add(1, Ordering::SeqCst); - let use_fake = is_fake_camera(); - + + let config = CameraConfig::from_env(); let result = tokio::task::spawn_blocking(move || { - if use_fake { - extract_entropy_fake(bytes, request_id) - } else { - extract_entropy_camera(bytes, request_id) - } + extract_entropy(bytes, &config) }).await; + ACTIVE_REQUESTS.fetch_sub(1, Ordering::SeqCst); match result { @@ -203,6 +145,7 @@ async fn get_random(Query(params): Query) -> Response { .body(Body::from(hex::encode(&data))).unwrap() } else { Response::builder().header(header::CONTENT_TYPE, "application/octet-stream") + .header(header::CACHE_CONTROL, "no-store") .body(Body::from(data)).unwrap() } } @@ -211,104 +154,71 @@ async fn get_random(Query(params): Query) -> Response { } } -/// Fake entropy source using /dev/urandom - for testing without camera hardware. -/// Simulates high-resolution camera frames for realistic throughput testing. -fn extract_entropy_fake(num_bytes: usize, request_id: u64) -> Result, String> { - use std::io::Read; - - let (width, height) = get_resolution(); - let frame_size = (width * height * 3) as usize; - - let mut entropy = Vec::with_capacity(num_bytes); - let mut hasher = Sha256::new(); - - let mut urandom = std::fs::File::open("/dev/urandom").map_err(|e| e.to_string())?; - let mut fake_frame = vec![0u8; frame_size]; - - let mut frame_idx: u64 = 0; - while entropy.len() < num_bytes { - urandom.read_exact(&mut fake_frame).map_err(|e| e.to_string())?; - - // Extract LSBs (2 bits per byte - highest entropy density) - let lsbs: Vec = 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; + +async fn get_raw(Query(params): Query) -> Response { + let bytes = params.bytes.min(MAX_BYTES_PER_REQUEST); + if bytes == 0 { + return (StatusCode::BAD_REQUEST, "bytes must be > 0").into_response(); + } + let config = CameraConfig::from_env(); + match tokio::task::spawn_blocking(move || extract_raw_lsb(bytes, &config)).await { + Ok(Ok(data)) => Response::builder() + .header(header::CONTENT_TYPE, "application/octet-stream") + .header(header::CACHE_CONTROL, "no-store") + .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(), } - - entropy.truncate(num_bytes); - Ok(entropy) } -/// Extract entropy from camera quantum noise using chunked SHA-256 conditioning. -/// -/// Throughput scales with camera resolution: -/// - 640x480 @ 30fps: ~27 MB/s raw (~216 Mbps), ~3.4 MB/s conditioned -/// - 1080p @ 30fps: ~186 MB/s raw (~1.5 Gbps), ~23 MB/s conditioned -/// - 1080p @ 60fps: ~373 MB/s raw (~3 Gbps), ~47 MB/s conditioned -/// - 4K @ 30fps: ~746 MB/s raw (~6 Gbps), ~93 MB/s conditioned -/// - 4K @ 60fps: ~1.49 GB/s raw (~12 Gbps), ~186 MB/s conditioned -fn extract_entropy_camera(num_bytes: usize, request_id: u64) -> Result, 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::(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())?; +/// Cryptographically sound continuous random. GET /stream or /stream?bytes=N. +/// Multiple streams get different data (each chunk goes to one consumer). +async fn get_stream(Query(params): Query) -> Response { + let config = CameraConfig::from_env(); + let (sub_id, rx) = subscribe_entropy(); + ensure_producer_running(config); - // Configure camera for quantum noise capture (high gain, max brightness) - configure_for_thermal_noise(&mut camera); - - let mut entropy = Vec::with_capacity(num_bytes); - let mut hasher = Sha256::new(); - - let mut frame_idx: u64 = 0; - while entropy.len() < num_bytes { - let frame = camera.frame().map_err(|e| e.to_string())?; - let raw = frame.buffer(); - - // Extract LSBs (2 bits per byte - highest entropy density in quantum noise) - let lsbs: Vec = raw.iter().map(|b| b & 0x03).collect(); - - // Hash in chunks - each CHUNK_SIZE bytes produces 32 bytes conditioned output - // At 1080p: ~24,300 chunks/frame = ~778 KB conditioned per frame - // At 4K: ~97,200 chunks/frame = ~3.1 MB conditioned per frame - for (chunk_idx, chunk) in lsbs.chunks(CHUNK_SIZE).enumerate() { - hasher.update(chunk); - hasher.update(&request_id.to_le_bytes()); - hasher.update(&frame_idx.to_le_bytes()); - hasher.update(&(chunk_idx as u64).to_le_bytes()); - hasher.update(&nanos_now().to_le_bytes()); - - entropy.extend_from_slice(&hasher.finalize_reset()); - - if entropy.len() >= num_bytes { - break; + let rx = Arc::new(Mutex::new(rx)); + let limit = params.bytes; + let hex = params.hex; + let stream = async_stream::stream! { + let mut sent: usize = 0; + loop { + if limit.is_some() && sent >= limit.unwrap() { break; } + let rx = Arc::clone(&rx); + let chunk = tokio::task::spawn_blocking(move || rx.lock().unwrap().recv()).await; + match chunk { + Ok(Ok(vec)) => { + let take = match limit { + None => vec.len(), + Some(n) => { + let left = n.saturating_sub(sent); + left.min(vec.len()) + } + }; + if take == 0 { break; } + sent += take; + let bytes = vec[..take].to_vec(); + let payload: Bytes = if hex { + Bytes::from(hex::encode(&bytes)) + } else { + Bytes::from(bytes) + }; + yield Ok::<_, std::io::Error>(payload); + } + _ => break, } } - frame_idx += 1; - } - - camera.stop_stream().ok(); - entropy.truncate(num_bytes); - Ok(entropy) + unsubscribe_entropy(sub_id); + }; + let content_type = if hex { "text/plain" } else { "application/octet-stream" }; + Response::builder() + .header(header::CONTENT_TYPE, content_type) + .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"); diff --git a/src/provider.rs b/src/provider.rs new file mode 100644 index 0000000..4ed63f5 --- /dev/null +++ b/src/provider.rs @@ -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 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 +}