Fix core RNG: seed HMAC-DRBG from camera frame hash
The previous version computed a SHA-256 of the frame into a variable named `entropy` and then discarded it, drawing instead from the OS CSPRNG via secrets.randbits(). The camera contributed nothing. Replace with a NIST SP 800-90A HMAC-DRBG seeded from the frame hash so the camera genuinely contributes entropy. Add --interval for loop rate control, --out for writing raw bytes to a FIFO, mutual exclusion of --loop/--single, and bit-multiple validation. Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
+88
-21
@@ -1,13 +1,57 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
import cv2
|
"""Camera-based entropy source.
|
||||||
import hashlib
|
|
||||||
import secrets
|
Fetches MJPEG frames from a network camera, hashes them, and feeds the
|
||||||
import time
|
hash into a NIST SP 800-90A HMAC-DRBG. Output is the DRBG's generated
|
||||||
|
bytes, so the camera genuinely contributes entropy (unlike the previous
|
||||||
|
version which computed a hash and then discarded it).
|
||||||
|
"""
|
||||||
import argparse
|
import argparse
|
||||||
|
import hashlib
|
||||||
|
import hmac
|
||||||
import sys
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
import cv2
|
||||||
|
|
||||||
sys.stdout.reconfigure(line_buffering=True)
|
sys.stdout.reconfigure(line_buffering=True)
|
||||||
|
|
||||||
|
|
||||||
|
class HMACDRBG:
|
||||||
|
"""NIST SP 800-90A HMAC-DRBG using SHA-256.
|
||||||
|
|
||||||
|
Seeded with camera frame entropy. Supports reseed so each frame
|
||||||
|
mixes fresh entropy into the internal state.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, entropy, personalization=b""):
|
||||||
|
self.K = b"\x00" * 32
|
||||||
|
self.V = b"\x01" * 32
|
||||||
|
self.reseed_counter = 0
|
||||||
|
self._update(entropy + personalization)
|
||||||
|
|
||||||
|
def _update(self, provided_data):
|
||||||
|
self.K = hmac.new(self.K, self.V + b"\x00" + provided_data, hashlib.sha256).digest()
|
||||||
|
self.V = hmac.new(self.K, self.V, hashlib.sha256).digest()
|
||||||
|
self.K = hmac.new(self.K, self.V + b"\x01" + provided_data, hashlib.sha256).digest()
|
||||||
|
self.V = hmac.new(self.K, self.V, hashlib.sha256).digest()
|
||||||
|
|
||||||
|
def reseed(self, entropy):
|
||||||
|
self._update(entropy)
|
||||||
|
self.reseed_counter = 0
|
||||||
|
|
||||||
|
def generate(self, num_bytes):
|
||||||
|
if self.reseed_counter > 10000:
|
||||||
|
raise RuntimeError("DRBG requires reseed before generating more output.")
|
||||||
|
self.reseed_counter += 1
|
||||||
|
out = b""
|
||||||
|
while len(out) < num_bytes:
|
||||||
|
self.V = hmac.new(self.K, self.V, hashlib.sha256).digest()
|
||||||
|
out += self.V
|
||||||
|
self._update(b"")
|
||||||
|
return out[:num_bytes]
|
||||||
|
|
||||||
|
|
||||||
def fetch_frame_opencv(url):
|
def fetch_frame_opencv(url):
|
||||||
cap = cv2.VideoCapture(url)
|
cap = cv2.VideoCapture(url)
|
||||||
ret, frame = cap.read()
|
ret, frame = cap.read()
|
||||||
@@ -16,11 +60,20 @@ def fetch_frame_opencv(url):
|
|||||||
raise ValueError("Failed to fetch frame.")
|
raise ValueError("Failed to fetch frame.")
|
||||||
return frame
|
return frame
|
||||||
|
|
||||||
def generate_random_numbers(frame, bit_lengths):
|
|
||||||
_, img_encoded = cv2.imencode('.jpg', frame)
|
def frame_entropy(frame):
|
||||||
img_bytes = img_encoded.tobytes()
|
"""Hash a captured frame to a 32-byte digest."""
|
||||||
entropy = hashlib.sha256(img_bytes).digest()
|
_, img_encoded = cv2.imencode(".jpg", frame)
|
||||||
return {length: secrets.randbits(length) for length in bit_lengths}
|
return hashlib.sha256(img_encoded.tobytes()).digest()
|
||||||
|
|
||||||
|
|
||||||
|
def emit(blob, out_path):
|
||||||
|
if out_path:
|
||||||
|
with open(out_path, "ab") as f:
|
||||||
|
f.write(blob)
|
||||||
|
else:
|
||||||
|
sys.stdout.write(blob.hex() + "\n")
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
parser = argparse.ArgumentParser(description="Generate random numbers from camera entropy.")
|
parser = argparse.ArgumentParser(description="Generate random numbers from camera entropy.")
|
||||||
@@ -28,31 +81,45 @@ def main():
|
|||||||
parser.add_argument("--single", type=int, default=1, help="Generate N random numbers and exit (default: 1)")
|
parser.add_argument("--single", type=int, default=1, help="Generate N random numbers and exit (default: 1)")
|
||||||
parser.add_argument("--bit", type=int, default=256, help="Bit length of each random number (default: 256)")
|
parser.add_argument("--bit", type=int, default=256, help="Bit length of each random number (default: 256)")
|
||||||
parser.add_argument("--loop", action="store_true", help="Run forever, generating numbers every second")
|
parser.add_argument("--loop", action="store_true", help="Run forever, generating numbers every second")
|
||||||
|
parser.add_argument("--interval", type=float, default=1.0, help="Seconds between iterations in --loop (default: 1.0)")
|
||||||
|
parser.add_argument("--out", help="Append raw bytes to this path (e.g. a FIFO) instead of printing hex")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if args.bit <= 0 or args.bit % 8 != 0:
|
||||||
|
print("Error: --bit must be a positive multiple of 8", file=sys.stderr)
|
||||||
|
return 2
|
||||||
|
num_bytes = args.bit // 8
|
||||||
|
|
||||||
|
if args.loop and args.single != 1:
|
||||||
|
print("Error: --loop and --single are mutually exclusive", file=sys.stderr)
|
||||||
|
return 2
|
||||||
|
|
||||||
|
def produce():
|
||||||
|
frame = fetch_frame_opencv(args.url)
|
||||||
|
entropy = frame_entropy(frame)
|
||||||
|
drbg = HMACDRBG(entropy)
|
||||||
|
return drbg.generate(num_bytes)
|
||||||
|
|
||||||
if args.loop:
|
if args.loop:
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
frame = fetch_frame_opencv(args.url)
|
blob = produce()
|
||||||
random_numbers = generate_random_numbers(frame, [args.bit])
|
emit(blob, args.out)
|
||||||
for number in random_numbers.values():
|
|
||||||
print(f"{number:x}")
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error: {e}", file=sys.stderr)
|
print(f"Error: {e}", file=sys.stderr)
|
||||||
time.sleep(1) # Wait before retrying
|
time.sleep(args.interval)
|
||||||
else:
|
else:
|
||||||
for i in range(args.single):
|
for i in range(args.single):
|
||||||
try:
|
try:
|
||||||
frame = fetch_frame_opencv(args.url)
|
blob = produce()
|
||||||
random_numbers = generate_random_numbers(frame, [args.bit])
|
emit(blob, args.out)
|
||||||
for number in random_numbers.values():
|
|
||||||
print(f"{number:x}")
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error: {e}", file=sys.stderr)
|
print(f"Error: {e}", file=sys.stderr)
|
||||||
return 1
|
return 1
|
||||||
if args.single > 1 and i < args.single - 1:
|
if args.single > 1 and i < args.single - 1:
|
||||||
time.sleep(1) # Small delay between frames
|
time.sleep(args.interval)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
import sys
|
sys.exit(main())
|
||||||
main()
|
|
||||||
|
|||||||
Reference in New Issue
Block a user