Skip to content

Commit 79117a3

Browse files
committed
feat(harvesters): add RadioactiveHarvester for quantum randomness
- Uses random.org atmospheric noise API - Falls back to ANU Quantum RNG (vacuum fluctuations) - True hardware-quality randomness from quantum phenomena
1 parent 9a1e2f1 commit 79117a3

1 file changed

Lines changed: 358 additions & 0 deletions

File tree

Lines changed: 358 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,358 @@
1+
# =============================================================================
2+
# TrueEntropy - Radioactive Harvester
3+
# =============================================================================
4+
#
5+
# This harvester collects entropy from random.org, which uses atmospheric
6+
# noise and radioactive decay to generate true random numbers.
7+
#
8+
# random.org is one of the most trusted sources of true randomness,
9+
# used by lotteries, scientific research, and cryptographic applications.
10+
#
11+
# Data Sources:
12+
# - Random integers from atmospheric noise
13+
# - Random bytes in various formats
14+
#
15+
# Why Radioactive/Atmospheric Data is Random:
16+
# - Quantum mechanical phenomena are truly unpredictable
17+
# - Atmospheric noise is chaotic and uncontrollable
18+
# - Independent of any computer system
19+
#
20+
# Note: random.org has rate limits for free tier:
21+
# - 1,000,000 bits/day for free
22+
# - Consider getting an API key for heavy usage
23+
#
24+
# =============================================================================
25+
26+
"""
27+
Radioactive decay and atmospheric noise entropy harvester.
28+
29+
Collects entropy from random.org, which generates random numbers
30+
from atmospheric noise and radioactive decay.
31+
"""
32+
33+
from __future__ import annotations
34+
35+
import hashlib
36+
import struct
37+
import time
38+
from typing import Any, List, Optional
39+
40+
from trueentropy.harvesters.base import BaseHarvester, HarvestResult
41+
42+
43+
class RadioactiveHarvester(BaseHarvester):
44+
"""
45+
Harvests entropy from random.org (atmospheric noise/radioactive decay).
46+
47+
random.org generates randomness from atmospheric noise, which is
48+
ultimately derived from quantum mechanical phenomena like radioactive
49+
decay. This provides true hardware-quality randomness.
50+
51+
Features:
52+
- Uses random.org's public API for random integers
53+
- Falls back to random.org's simple API if JSON fails
54+
- Respects rate limits (free tier: 1M bits/day)
55+
56+
Attributes:
57+
api_key: Optional random.org API key for higher quotas
58+
timeout: Request timeout in seconds
59+
num_integers: Number of random integers to request per collection
60+
61+
Example:
62+
>>> harvester = RadioactiveHarvester()
63+
>>> result = harvester.collect()
64+
>>> if result.success:
65+
... print(f"Got {result.entropy_bits} bits from random.org!")
66+
"""
67+
68+
# -------------------------------------------------------------------------
69+
# API Configuration
70+
# -------------------------------------------------------------------------
71+
72+
# random.org JSON-RPC API (requires API key for full access)
73+
RANDOM_ORG_API_URL = "https://api.random.org/json-rpc/4/invoke"
74+
75+
# random.org simple HTTP API (free, rate-limited)
76+
RANDOM_ORG_INTEGER_URL = "https://www.random.org/integers/"
77+
RANDOM_ORG_STRINGS_URL = "https://www.random.org/strings/"
78+
79+
# ANU Quantum Random Numbers (alternative source)
80+
ANU_QRNG_URL = "https://qrng.anu.edu.au/API/jsonI.php"
81+
82+
# -------------------------------------------------------------------------
83+
# Initialization
84+
# -------------------------------------------------------------------------
85+
86+
def __init__(
87+
self,
88+
api_key: Optional[str] = None,
89+
timeout: float = 10.0,
90+
num_integers: int = 10
91+
) -> None:
92+
"""
93+
Initialize the radioactive harvester.
94+
95+
Args:
96+
api_key: Optional random.org API key for higher quotas.
97+
Without a key, uses the free public endpoints.
98+
timeout: Request timeout in seconds (default: 10.0 - random.org
99+
can be slow sometimes)
100+
num_integers: Number of random integers to request per collection.
101+
More integers = more entropy but slower.
102+
"""
103+
self._api_key = api_key
104+
self._timeout = timeout
105+
self._num_integers = num_integers
106+
107+
# -------------------------------------------------------------------------
108+
# BaseHarvester Implementation
109+
# -------------------------------------------------------------------------
110+
111+
@property
112+
def name(self) -> str:
113+
"""Return harvester name."""
114+
return "radioactive"
115+
116+
def collect(self) -> HarvestResult:
117+
"""
118+
Collect entropy from random.org.
119+
120+
Process:
121+
1. Request random integers from random.org
122+
2. If that fails, try ANU Quantum RNG
123+
3. Pack integers into bytes
124+
4. Hash for additional mixing
125+
126+
Returns:
127+
HarvestResult containing radioactive/atmospheric entropy
128+
"""
129+
try:
130+
import requests
131+
except ImportError:
132+
return HarvestResult(
133+
data=b"",
134+
entropy_bits=0,
135+
source=self.name,
136+
success=False,
137+
error="requests library not available"
138+
)
139+
140+
# Try random.org first
141+
values = self._fetch_random_org(requests)
142+
143+
# Fallback to ANU Quantum RNG if random.org fails
144+
if not values:
145+
values = self._fetch_anu_qrng(requests)
146+
147+
if not values:
148+
return HarvestResult(
149+
data=b"",
150+
entropy_bits=0,
151+
source=self.name,
152+
success=False,
153+
error="All random sources unavailable (rate limited or offline)"
154+
)
155+
156+
# Add local timestamp for mixing
157+
values.append(time.time_ns())
158+
159+
# Pack as 64-bit integers
160+
data = struct.pack(f"!{len(values)}Q", *[v & 0xFFFFFFFFFFFFFFFF for v in values])
161+
162+
# Hash for uniform distribution
163+
hashed = hashlib.sha256(data).digest()
164+
165+
# Entropy estimate: random.org provides true random bits
166+
# Each 32-bit integer = 32 bits of entropy
167+
entropy_bits = (len(values) - 1) * 32 # -1 for timestamp
168+
169+
return HarvestResult(
170+
data=data + hashed,
171+
entropy_bits=entropy_bits,
172+
source=self.name,
173+
success=True
174+
)
175+
176+
# -------------------------------------------------------------------------
177+
# random.org API
178+
# -------------------------------------------------------------------------
179+
180+
def _fetch_random_org(self, requests: Any) -> List[int]:
181+
"""
182+
Fetch random integers from random.org.
183+
184+
Uses the simple HTTP API which doesn't require authentication
185+
but has daily quotas.
186+
187+
Args:
188+
requests: The imported requests module
189+
190+
Returns:
191+
List of random integers
192+
"""
193+
try:
194+
# Use simple HTTP API (no auth needed)
195+
params = {
196+
"num": self._num_integers,
197+
"min": 0,
198+
"max": 2147483647, # Max 32-bit signed int
199+
"col": 1,
200+
"base": 10,
201+
"format": "plain",
202+
"rnd": "new"
203+
}
204+
205+
response = requests.get(
206+
self.RANDOM_ORG_INTEGER_URL,
207+
params=params,
208+
timeout=self._timeout
209+
)
210+
211+
# Check for quota exceeded
212+
if response.status_code == 503:
213+
return [] # Rate limited, try fallback
214+
215+
response.raise_for_status()
216+
217+
# Parse the response (one integer per line)
218+
lines = response.text.strip().split('\n')
219+
values = [int(line.strip()) for line in lines if line.strip()]
220+
221+
return values
222+
223+
except Exception:
224+
return []
225+
226+
def _fetch_random_org_api(self, requests: Any) -> List[int]:
227+
"""
228+
Fetch random integers using random.org JSON-RPC API.
229+
230+
Requires API key but provides higher quotas.
231+
232+
Args:
233+
requests: The imported requests module
234+
235+
Returns:
236+
List of random integers
237+
"""
238+
if not self._api_key:
239+
return []
240+
241+
try:
242+
payload = {
243+
"jsonrpc": "2.0",
244+
"method": "generateIntegers",
245+
"params": {
246+
"apiKey": self._api_key,
247+
"n": self._num_integers,
248+
"min": 0,
249+
"max": 2147483647,
250+
"replacement": True
251+
},
252+
"id": 1
253+
}
254+
255+
response = requests.post(
256+
self.RANDOM_ORG_API_URL,
257+
json=payload,
258+
timeout=self._timeout
259+
)
260+
response.raise_for_status()
261+
262+
data = response.json()
263+
264+
if "error" in data:
265+
return []
266+
267+
result = data.get("result", {})
268+
random_data = result.get("random", {})
269+
values = random_data.get("data", [])
270+
271+
return values
272+
273+
except Exception:
274+
return []
275+
276+
# -------------------------------------------------------------------------
277+
# ANU Quantum Random Number Generator (Fallback)
278+
# -------------------------------------------------------------------------
279+
280+
def _fetch_anu_qrng(self, requests: Any) -> List[int]:
281+
"""
282+
Fetch random numbers from ANU Quantum Random Number Generator.
283+
284+
ANU QRNG generates random numbers by measuring quantum vacuum
285+
fluctuations. This is true quantum randomness.
286+
287+
Args:
288+
requests: The imported requests module
289+
290+
Returns:
291+
List of random integers
292+
"""
293+
try:
294+
params = {
295+
"length": self._num_integers,
296+
"type": "uint32"
297+
}
298+
299+
response = requests.get(
300+
self.ANU_QRNG_URL,
301+
params=params,
302+
timeout=self._timeout
303+
)
304+
response.raise_for_status()
305+
306+
data = response.json()
307+
308+
if not data.get("success", False):
309+
return []
310+
311+
values = data.get("data", [])
312+
313+
return values
314+
315+
except Exception:
316+
return []
317+
318+
# -------------------------------------------------------------------------
319+
# Configuration Properties
320+
# -------------------------------------------------------------------------
321+
322+
@property
323+
def api_key(self) -> Optional[str]:
324+
"""Get the API key (masked for security)."""
325+
if self._api_key:
326+
return self._api_key[:8] + "..."
327+
return None
328+
329+
@api_key.setter
330+
def api_key(self, value: Optional[str]) -> None:
331+
"""Set the API key."""
332+
self._api_key = value
333+
334+
@property
335+
def timeout(self) -> float:
336+
"""Get the request timeout in seconds."""
337+
return self._timeout
338+
339+
@timeout.setter
340+
def timeout(self, value: float) -> None:
341+
"""Set the request timeout."""
342+
if value <= 0:
343+
raise ValueError("timeout must be positive")
344+
self._timeout = value
345+
346+
@property
347+
def num_integers(self) -> int:
348+
"""Get the number of integers to request."""
349+
return self._num_integers
350+
351+
@num_integers.setter
352+
def num_integers(self, value: int) -> None:
353+
"""Set the number of integers to request."""
354+
if value < 1:
355+
raise ValueError("num_integers must be at least 1")
356+
if value > 1000:
357+
raise ValueError("num_integers must be at most 1000")
358+
self._num_integers = value

0 commit comments

Comments
 (0)