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:
parent
fd0598be09
commit
df18197a1d
|
|
@ -1,2 +1,11 @@
|
|||
/target
|
||||
scripts/node_modules/
|
||||
|
||||
# Release build output
|
||||
/release/
|
||||
|
||||
# Man pages (generated)
|
||||
/man/*.gz
|
||||
|
||||
# Test artifacts
|
||||
random.bin
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
12
Cargo.toml
12
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"
|
||||
|
|
|
|||
21
README.md
21
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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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 "$@"
|
||||
|
|
@ -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)"
|
||||
|
|
@ -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.$$
|
||||
|
|
@ -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
|
||||
|
|
@ -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!"
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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(())
|
||||
}
|
||||
|
|
@ -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,
|
||||
};
|
||||
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
|
|
@ -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;
|
||||
324
src/main.rs
324
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<usize>,
|
||||
#[serde(default)]
|
||||
hex: bool,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let port = std::env::var("PORT").ok().and_then(|p| p.parse().ok()).unwrap_or(DEFAULT_PORT);
|
||||
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<dyn std::error::Error>> {
|
|||
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<serde_json::Value> {
|
||||
Json(json!({
|
||||
"mcp": {
|
||||
|
|
@ -103,97 +105,37 @@ async fn mcp_wellknown() -> Json<serde_json::Value> {
|
|||
"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::<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 {
|
||||
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<RandomQuery>) -> 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<RandomQuery>) -> 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<Vec<u8>, 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<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;
|
||||
|
||||
async fn get_raw(Query(params): Query<RawQuery>) -> 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<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())?;
|
||||
/// 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<StreamQuery>) -> 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<u8> = 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");
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
Loading…
Reference in New Issue