multirandom 0.0.7__tar.gz → 0.0.8__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {multirandom-0.0.7 → multirandom-0.0.8}/PKG-INFO +16 -2
- {multirandom-0.0.7 → multirandom-0.0.8}/README.md +15 -1
- {multirandom-0.0.7 → multirandom-0.0.8}/pyproject.toml +1 -1
- {multirandom-0.0.7 → multirandom-0.0.8}/src/multirandom/__init__.py +9 -3
- multirandom-0.0.8/src/multirandom/csprng/__init__.py +13 -0
- multirandom-0.0.8/src/multirandom/csprng/original_random.py +11 -0
- {multirandom-0.0.7 → multirandom-0.0.8}/src/multirandom/csprng/secure_rand.py +7 -1
- {multirandom-0.0.7 → multirandom-0.0.8}/src/multirandom/csprng/sha_rand.py +7 -1
- multirandom-0.0.8/src/multirandom/prng_unsecure/__init__.py +12 -0
- multirandom-0.0.8/src/multirandom/prng_unsecure/flash_rand_and_reverse.py +10 -0
- {multirandom-0.0.7 → multirandom-0.0.8}/src/multirandom/prng_unsecure/rand_using_virt_lcg.py +18 -0
- {multirandom-0.0.7 → multirandom-0.0.8}/src/multirandom/prng_unsecure/xor_shift.py +24 -0
- multirandom-0.0.8/src/multirandom/trng/__init__.py +12 -0
- {multirandom-0.0.7 → multirandom-0.0.8}/src/multirandom/trng/rand_using_online_api.py +14 -0
- {multirandom-0.0.7 → multirandom-0.0.8}/src/multirandom/trng/rand_using_real_random.py +7 -2
- {multirandom-0.0.7 → multirandom-0.0.8}/src/multirandom/trng/rand_via_clicks.py +16 -0
- multirandom-0.0.8/src/multirandom/trng/weather_rand.py +539 -0
- {multirandom-0.0.7 → multirandom-0.0.8}/src/multirandom.egg-info/PKG-INFO +16 -2
- multirandom-0.0.7/src/multirandom/csprng/__init__.py +0 -0
- multirandom-0.0.7/src/multirandom/csprng/original_random.py +0 -1
- multirandom-0.0.7/src/multirandom/prng_unsecure/__init__.py +0 -0
- multirandom-0.0.7/src/multirandom/prng_unsecure/flash_rand_and_reverse.py +0 -1
- multirandom-0.0.7/src/multirandom/trng/__init__.py +0 -0
- multirandom-0.0.7/src/multirandom/trng/weather_rand.py +0 -281
- {multirandom-0.0.7 → multirandom-0.0.8}/LICENSE +0 -0
- {multirandom-0.0.7 → multirandom-0.0.8}/setup.cfg +0 -0
- {multirandom-0.0.7 → multirandom-0.0.8}/src/multirandom.egg-info/SOURCES.txt +0 -0
- {multirandom-0.0.7 → multirandom-0.0.8}/src/multirandom.egg-info/dependency_links.txt +0 -0
- {multirandom-0.0.7 → multirandom-0.0.8}/src/multirandom.egg-info/requires.txt +0 -0
- {multirandom-0.0.7 → multirandom-0.0.8}/src/multirandom.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: multirandom
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.8
|
|
4
4
|
Summary: 🎲 The truth external PyPI random package, have trng (true randomness), prng_unsecure, csprng, ...
|
|
5
5
|
Author-email: BlackCatOfficialytb <quanvm0501@gmail.com>
|
|
6
6
|
License: Apache-2.0
|
|
@@ -30,6 +30,20 @@ A comprehensive library for exploring and generating randomness across the spect
|
|
|
30
30
|
|
|
31
31
|
---
|
|
32
32
|
|
|
33
|
+
## 📚 Full Documentation
|
|
34
|
+
|
|
35
|
+
Deep-dive docs live in [`docs/`](docs/):
|
|
36
|
+
|
|
37
|
+
| Page | Contents |
|
|
38
|
+
|---|---|
|
|
39
|
+
| [Getting started](docs/getting-started.md) | installation, quickstart, which generator to pick |
|
|
40
|
+
| [True randomness (`trng`)](docs/trng.md) | mouse clicks, quantum APIs, microphone, weather |
|
|
41
|
+
| [Cryptographic randomness (`csprng`)](docs/csprng.md) | OS CSPRNGs, NIST DRBGs, the Dual_EC backdoor demo, Fortuna/ISAAC |
|
|
42
|
+
| [Unsecure PRNGs (`prng_unsecure`)](docs/prng_unsecure.md) | XorShift/Xoshiro, LCGs, and their reversal attacks |
|
|
43
|
+
| [Self-checks](docs/self-checks.md) | the assert batteries verifying every generator |
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
33
47
|
## 🏗 Project Structure
|
|
34
48
|
|
|
35
49
|
```text
|
|
@@ -40,7 +54,7 @@ multirandom/
|
|
|
40
54
|
│ ├── rand_via_clicks.py # Human-in-the-loop entropy
|
|
41
55
|
│ ├── rand_using_online_api.py # Quantum & Hardware APIs (ANU QRNG, Roll-API)
|
|
42
56
|
│ ├── rand_using_real_random.py # Microphone entropy (real-random)
|
|
43
|
-
│ └── weather_rand.py #
|
|
57
|
+
│ └── weather_rand.py # Open-Meteo weather entropy (large unhashed seeds)
|
|
44
58
|
├── csprng/ # Cryptographically Strong Randomness
|
|
45
59
|
│ ├── secure_rand.py # OS CSPRNGs, NIST DRBGs, Fortuna/Yarrow, ISAAC
|
|
46
60
|
│ ├── sha_rand.py # MD5, SHA-1/2/3, BLAKE2, SHAKE PRNGs
|
|
@@ -11,6 +11,20 @@ A comprehensive library for exploring and generating randomness across the spect
|
|
|
11
11
|
|
|
12
12
|
---
|
|
13
13
|
|
|
14
|
+
## 📚 Full Documentation
|
|
15
|
+
|
|
16
|
+
Deep-dive docs live in [`docs/`](docs/):
|
|
17
|
+
|
|
18
|
+
| Page | Contents |
|
|
19
|
+
|---|---|
|
|
20
|
+
| [Getting started](docs/getting-started.md) | installation, quickstart, which generator to pick |
|
|
21
|
+
| [True randomness (`trng`)](docs/trng.md) | mouse clicks, quantum APIs, microphone, weather |
|
|
22
|
+
| [Cryptographic randomness (`csprng`)](docs/csprng.md) | OS CSPRNGs, NIST DRBGs, the Dual_EC backdoor demo, Fortuna/ISAAC |
|
|
23
|
+
| [Unsecure PRNGs (`prng_unsecure`)](docs/prng_unsecure.md) | XorShift/Xoshiro, LCGs, and their reversal attacks |
|
|
24
|
+
| [Self-checks](docs/self-checks.md) | the assert batteries verifying every generator |
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
14
28
|
## 🏗 Project Structure
|
|
15
29
|
|
|
16
30
|
```text
|
|
@@ -21,7 +35,7 @@ multirandom/
|
|
|
21
35
|
│ ├── rand_via_clicks.py # Human-in-the-loop entropy
|
|
22
36
|
│ ├── rand_using_online_api.py # Quantum & Hardware APIs (ANU QRNG, Roll-API)
|
|
23
37
|
│ ├── rand_using_real_random.py # Microphone entropy (real-random)
|
|
24
|
-
│ └── weather_rand.py #
|
|
38
|
+
│ └── weather_rand.py # Open-Meteo weather entropy (large unhashed seeds)
|
|
25
39
|
├── csprng/ # Cryptographically Strong Randomness
|
|
26
40
|
│ ├── secure_rand.py # OS CSPRNGs, NIST DRBGs, Fortuna/Yarrow, ISAAC
|
|
27
41
|
│ ├── sha_rand.py # MD5, SHA-1/2/3, BLAKE2, SHAKE PRNGs
|
|
@@ -1,8 +1,14 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""
|
|
2
|
+
Project: https://pypi.org/project/multirandom/
|
|
3
|
+
Repo: https://github.com/BlackCatOfficialytb/multirandom
|
|
4
|
+
License: Apache 2.0
|
|
5
|
+
Module: multirandom
|
|
6
|
+
Description:
|
|
7
|
+
One random-number package, several philosophies of chance:
|
|
2
8
|
|
|
3
|
-
* ``prng_unsecure/`` - educational PRNGs that must never guard anything (LCGs, xorshift).
|
|
4
|
-
* ``csprng/`` - cryptographically strong generators (OS entropy, NIST DRBGs, stream ciphers).
|
|
5
9
|
* ``trng/`` - true/hardware entropy sources (microphones, weather, clicks, online APIs).
|
|
10
|
+
* ``csprng/`` - cryptographically strong generators (OS entropy, NIST DRBGs, stream ciphers).
|
|
11
|
+
* ``prng_unsecure/`` - educational PRNGs that must never guard anything (LCGs, xorshift).
|
|
6
12
|
|
|
7
13
|
Everything in ``prng_unsecure/`` and the deliberately broken ``Dual_EC_DRBG`` warns the
|
|
8
14
|
first time it is used. Those warnings are controlled by ONE global variable::
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Project: https://pypi.org/project/multirandom/
|
|
3
|
+
Repo: https://github.com/BlackCatOfficialytb/multirandom
|
|
4
|
+
License: Apache 2.0
|
|
5
|
+
Module: multirandom.csprng
|
|
6
|
+
Description:
|
|
7
|
+
Cryptographically strong randomness: OS-level CSPRNGs (secrets / os.urandom /
|
|
8
|
+
getrandom(2) / BCryptGenRandom / SecRandomCopyBytes), NIST SP 800-90A DRBGs
|
|
9
|
+
(CTR, HMAC, Hash, plus the deliberately backdoored Dual_EC_DRBG with a working
|
|
10
|
+
reversal demo), ChaCha20/Salsa20 counter-mode DRBGs, Fortuna and Yarrow
|
|
11
|
+
accumulators, Jenkins' ISAAC, hash-based counter-mode generators (MD5, SHA-1/2/3,
|
|
12
|
+
SHAKE, BLAKE2) and a passthrough wrapper for Python's random module.
|
|
13
|
+
"""
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Project: https://pypi.org/project/multirandom/
|
|
3
|
+
Repo: https://github.com/BlackCatOfficialytb/multirandom
|
|
4
|
+
License: Apache 2.0
|
|
5
|
+
Module: multirandom.csprng.original_random
|
|
6
|
+
Description:
|
|
7
|
+
Passthrough for Python's built-in random module (`from random import *`), kept
|
|
8
|
+
so the csprng package can be imported wholesale when bundling "everything
|
|
9
|
+
random". The Mersenne Twister behind it is NOT cryptographically secure.
|
|
10
|
+
"""
|
|
11
|
+
from random import *
|
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""
|
|
2
|
+
Project: https://pypi.org/project/multirandom/
|
|
3
|
+
Repo: https://github.com/BlackCatOfficialytb/multirandom
|
|
4
|
+
License: Apache 2.0
|
|
5
|
+
Module: multirandom.csprng.secure_rand
|
|
6
|
+
Description:
|
|
7
|
+
Cryptographic random generators ("csprng").
|
|
2
8
|
|
|
3
9
|
Three families live in this module:
|
|
4
10
|
|
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""
|
|
2
|
+
Project: https://pypi.org/project/multirandom/
|
|
3
|
+
Repo: https://github.com/BlackCatOfficialytb/multirandom
|
|
4
|
+
License: Apache 2.0
|
|
5
|
+
Module: multirandom.csprng.sha_rand
|
|
6
|
+
Description:
|
|
7
|
+
Hash-based pseudo-random generators.
|
|
2
8
|
|
|
3
9
|
Each generator runs a counter through a cryptographic hash: H(seed || counter).
|
|
4
10
|
This is the classic counter-mode hash DRBG construction (the same idea as NIST's
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Project: https://pypi.org/project/multirandom/
|
|
3
|
+
Repo: https://github.com/BlackCatOfficialytb/multirandom
|
|
4
|
+
License: Apache 2.0
|
|
5
|
+
Module: multirandom.prng_unsecure
|
|
6
|
+
Description:
|
|
7
|
+
Educational pseudo-randomness, deliberately insecure: the XorShift/Xoshiro
|
|
8
|
+
family (18 generators, reference vectors included, four equivalent reversal
|
|
9
|
+
attacks on Xorshift32) and classic Linear Congruential Generators with
|
|
10
|
+
user-supplied forward/inverse formulas. Never use these for cryptography; every
|
|
11
|
+
constructor warns once per process via the package's global warning flag.
|
|
12
|
+
"""
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Project: https://pypi.org/project/multirandom/
|
|
3
|
+
Repo: https://github.com/BlackCatOfficialytb/multirandom
|
|
4
|
+
License: Apache 2.0
|
|
5
|
+
Module: multirandom.prng_unsecure.flash_rand_and_reverse
|
|
6
|
+
Description:
|
|
7
|
+
Placeholder for specialized PRNGs and their reversal techniques (coming soon).
|
|
8
|
+
Currently prints a friendly reminder on import.
|
|
9
|
+
"""
|
|
10
|
+
print("coming soon :/")
|
{multirandom-0.0.7 → multirandom-0.0.8}/src/multirandom/prng_unsecure/rand_using_virt_lcg.py
RENAMED
|
@@ -1,3 +1,21 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Project: https://pypi.org/project/multirandom/
|
|
3
|
+
Repo: https://github.com/BlackCatOfficialytb/multirandom
|
|
4
|
+
License: Apache 2.0
|
|
5
|
+
Module: multirandom.prng_unsecure.rand_using_virt_lcg
|
|
6
|
+
Description:
|
|
7
|
+
Linear Congruential Generators exactly as the textbooks write them. The class
|
|
8
|
+
`LCGs` takes user-supplied formulas acting on the state s: `rules` for the
|
|
9
|
+
forward step (e.g. "(s * a + c) % d") and the optional `reverse_rules` for the
|
|
10
|
+
inverse step (e.g. "((s - c) * pow(a, -1, d)) % d"), with parameters a/b/c/d
|
|
11
|
+
(defaults 10 / 3 / 5 / 25565) and a randomised state by default. roll()
|
|
12
|
+
advances the state, reverse_roll() walks it backwards -- demonstrating the
|
|
13
|
+
insecurity of simple modular arithmetic: with a few consecutive outputs, state
|
|
14
|
+
(and every past and future output) is recoverable via the modular
|
|
15
|
+
multiplicative inverse, so a and d must be coprime for reversal to exist.
|
|
16
|
+
Intended for offline games/education, NOT cryptography; the class warns once
|
|
17
|
+
per process, governed by the package's global warning flag.
|
|
18
|
+
"""
|
|
1
19
|
import random
|
|
2
20
|
import warnings
|
|
3
21
|
|
|
@@ -1,3 +1,27 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Project: https://pypi.org/project/multirandom/
|
|
3
|
+
Repo: https://github.com/BlackCatOfficialytb/multirandom
|
|
4
|
+
License: Apache 2.0
|
|
5
|
+
Module: multirandom.prng_unsecure.xor_shift
|
|
6
|
+
Description:
|
|
7
|
+
The 18-generator XorShift / Xoshiro / Xoroshiro family (Marsaglia, Vigna):
|
|
8
|
+
Xorshift32/64/128, Xorwow (CUDA), XorshiftStar & Xorshift1024Star (Vigna),
|
|
9
|
+
XorshiftPlus (V8/WebKit), the Xoshiro256* / Xoshiro128* variants, and the
|
|
10
|
+
Xoroshiro64* / Xoroshiro128* variants (Minecraft Java 1.18+ uses Xoroshiro128++).
|
|
11
|
+
Core method ``roll()`` -- one register step, one output. Register-width classes
|
|
12
|
+
take a ``seed`` int; the multi-word classes take a ``seeds`` list (OS entropy
|
|
13
|
+
generates one when omitted). The module's assert battery reproduces the output
|
|
14
|
+
of the official C reference implementations for fixed seeds.
|
|
15
|
+
|
|
16
|
+
Because every step is linear over GF(2), the state is fully reversible. Xorshift32
|
|
17
|
+
ships four equivalent attacks side by side -- reverse_roll() (fixed-shift
|
|
18
|
+
feedback), reverse_roll_increments() (XOR the final value at s, 2s, 3s, ...),
|
|
19
|
+
reverse_roll_doubling() (XOR the intermediate value at s, 2s, 4s, ...) and
|
|
20
|
+
reverse_roll_matrix() (GF(2) matrix inversion) -- plus standalone helpers
|
|
21
|
+
reverse_xor_lshift/rshift, increments_xor_lshift/rshift, doubling_xor_lshift/rshift
|
|
22
|
+
and gf2_inverse_matrix. Insecure for cryptography: constructing any generator
|
|
23
|
+
warns once per process via the package's global warning flag.
|
|
24
|
+
"""
|
|
1
25
|
import functools
|
|
2
26
|
import inspect
|
|
3
27
|
import random
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Project: https://pypi.org/project/multirandom/
|
|
3
|
+
Repo: https://github.com/BlackCatOfficialytb/multirandom
|
|
4
|
+
License: Apache 2.0
|
|
5
|
+
Module: multirandom.trng
|
|
6
|
+
Description:
|
|
7
|
+
True / physical entropy sources: sub-millisecond mouse-click timing jitter and
|
|
8
|
+
cursor positions (Win32 + pyautogui), online quantum & hardware APIs
|
|
9
|
+
(Random.org, ANU QRNG, Roll-API), ambient microphone noise via the optional
|
|
10
|
+
real-random extra, and Open-Meteo weather entropy folded into large unhashed
|
|
11
|
+
seeds.
|
|
12
|
+
"""
|
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Project: https://pypi.org/project/multirandom/
|
|
3
|
+
Repo: https://github.com/BlackCatOfficialytb/multirandom
|
|
4
|
+
License: Apache 2.0
|
|
5
|
+
Module: multirandom.trng.rand_using_online_api
|
|
6
|
+
Description:
|
|
7
|
+
True randomness from remote physical processes, over aiohttp:
|
|
8
|
+
fetch_random_org_integers() - random.org atmospheric noise (plain-text API,
|
|
9
|
+
range-filterable integers); fetch_qrng_anu_integers() - the Australian National
|
|
10
|
+
University QRNG, quantum vacuum shot noise returning uint8 values;
|
|
11
|
+
fetch_roll_api_result() - TheLastGimbus Roll-API, a physical dice-rolling
|
|
12
|
+
robot. Plain Python return values (no random.Random wrapper); every endpoint
|
|
13
|
+
is best-effort and may return [] / None when unreachable.
|
|
14
|
+
"""
|
|
1
15
|
import aiohttp
|
|
2
16
|
import asyncio
|
|
3
17
|
|
|
@@ -1,5 +1,10 @@
|
|
|
1
|
-
"""
|
|
2
|
-
|
|
1
|
+
"""
|
|
2
|
+
Project: https://pypi.org/project/multirandom/
|
|
3
|
+
Repo: https://github.com/BlackCatOfficialytb/multirandom
|
|
4
|
+
License: Apache 2.0
|
|
5
|
+
Module: multirandom.trng.rand_using_real_random
|
|
6
|
+
Description:
|
|
7
|
+
Ambient-microphone entropy via the optional `real-random` package.
|
|
3
8
|
`real-random` (https://pypi.org/project/real-random/) records 1-2 seconds of
|
|
4
9
|
microphone audio, hashes it with SHA-256 and turns the hash into random values.
|
|
5
10
|
Install the optional dependency with:
|
|
@@ -1,3 +1,19 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Project: https://pypi.org/project/multirandom/
|
|
3
|
+
Repo: https://github.com/BlackCatOfficialytb/multirandom
|
|
4
|
+
License: Apache 2.0
|
|
5
|
+
Module: multirandom.trng.rand_via_clicks
|
|
6
|
+
Description:
|
|
7
|
+
Human-in-the-loop entropy: seeds a Mersenne Twister from the nanosecond-scale
|
|
8
|
+
timing jitter between mouse clicks plus the cursor positions at click time
|
|
9
|
+
(Win32 GetAsyncKeyState button polling + pyautogui cursor reading).
|
|
10
|
+
RealRandomUsingBetweenLeftClicks / ...BetweenRightClicks / ...BetweenMiddleClicks
|
|
11
|
+
need two clicks of their button to seed; RealRandomUsingBetweenMixedClicks needs
|
|
12
|
+
a Left -> Right -> Middle sequence. Construction blocks until you click them;
|
|
13
|
+
passing an explicit seed skips the interaction. Helpers is_left_click_pressed /
|
|
14
|
+
is_right_click_pressed / is_middle_click_pressed and
|
|
15
|
+
get_click_data(button_check_func) -> (perf_counter timestamp, cursor position).
|
|
16
|
+
"""
|
|
1
17
|
import pyautogui
|
|
2
18
|
import time
|
|
3
19
|
import ctypes
|
|
@@ -0,0 +1,539 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Project: https://pypi.org/project/multirandom/
|
|
3
|
+
Repo: https://github.com/BlackCatOfficialytb/multirandom
|
|
4
|
+
License: Apache 2.0
|
|
5
|
+
Module: multirandom.trng.weather_rand
|
|
6
|
+
Description:
|
|
7
|
+
A random source using the Open-Meteo weather API. Not fully random by itself,
|
|
8
|
+
usable once blended with OS entropy, and no cryptographic hash touches the data.
|
|
9
|
+
|
|
10
|
+
Each fetch harvests *much* more information than the original single-hour
|
|
11
|
+
snapshot: the complete hourly arrays (29 variables) and daily aggregates
|
|
12
|
+
(19 variables) of a rolling window (``past_days`` + ``forecast_days`` days)
|
|
13
|
+
for every sampled location and moment -- thousands of real-world readings per
|
|
14
|
+
call. The readings are combined WITHOUT hashing, by three techniques:
|
|
15
|
+
1. *Lossless packing* -- every measurement contributes its exact IEEE-754
|
|
16
|
+
bytes to the "tape", so no information is thrown into one digest.
|
|
17
|
+
2. *Von Neumann conditioning* -- tape bits are debiased pairwise
|
|
18
|
+
(01 -> 1, 10 -> 0, 00/11 dropped), the classic technique for real physical
|
|
19
|
+
noise (repeated identical readings are simply discarded).
|
|
20
|
+
3. *Wide reversible fold* -- tape and reservoir chunks are folded into a
|
|
21
|
+
big-integer state with a splitmix64-derived odd multiplier and a
|
|
22
|
+
chunk-dependent bit rotation per step. The fold keeps every chunk reachable
|
|
23
|
+
(it is invertible, not a hash) and stops any single bit position from
|
|
24
|
+
dominating.
|
|
25
|
+
|
|
26
|
+
The returned seed is configurable-large: 512 bits (64 bytes) by default, up to
|
|
27
|
+
8192 bits (1024 bytes). Failures on every weather fetch fall back to a plain
|
|
28
|
+
``os.urandom`` seed -- never a hash of the time.
|
|
29
|
+
"""
|
|
30
|
+
import aiohttp
|
|
31
|
+
import asyncio
|
|
32
|
+
import os
|
|
33
|
+
import random
|
|
34
|
+
import struct
|
|
35
|
+
import time
|
|
36
|
+
from datetime import datetime, timedelta, timezone
|
|
37
|
+
from typing import List, Optional, Sequence, Tuple
|
|
38
|
+
|
|
39
|
+
# --- Von Neumann table: byte -> (packed output bits, number of bits produced) ---
|
|
40
|
+
# For each of the four bit pairs of a byte: 01 -> emit 1, 10 -> emit 0, 00/11 drop.
|
|
41
|
+
_VN_TABLE = []
|
|
42
|
+
for _byte in range(256):
|
|
43
|
+
_bits = [(_byte >> _i) & 1 for _i in range(8)]
|
|
44
|
+
_out, _k = 0, 0
|
|
45
|
+
for _i in range(0, 8, 2):
|
|
46
|
+
_b1, _b2 = _bits[_i], _bits[_i + 1]
|
|
47
|
+
if _b1 != _b2:
|
|
48
|
+
_out = (_out << 1) | _b2
|
|
49
|
+
_k += 1
|
|
50
|
+
_VN_TABLE.append((_out, _k))
|
|
51
|
+
|
|
52
|
+
_SPLITMIX_MUL = 0xBF58476D1CE4E5B9
|
|
53
|
+
_SPLITMIX_ADD = 0x94D049BB133111EB
|
|
54
|
+
_GOLDEN = 0x9E3779B97F4A7C15
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _von_neumann_pool(src: bytes) -> bytes:
|
|
58
|
+
"""Debias *src* with the von Neumann technique and return the pool bytes.
|
|
59
|
+
|
|
60
|
+
Pairs of consecutive input bits are only used when they differ
|
|
61
|
+
(01 -> 1, 10 -> 0); equal pairs are discarded, which removes systematic
|
|
62
|
+
single-bit bias at the cost of output length (about 25 % of the input).
|
|
63
|
+
Widely used for conditioning true-RNG sensor data -- no hashing involved.
|
|
64
|
+
"""
|
|
65
|
+
acc = accn = 0
|
|
66
|
+
pool = bytearray()
|
|
67
|
+
for byte in src:
|
|
68
|
+
out, k = _VN_TABLE[byte]
|
|
69
|
+
if k:
|
|
70
|
+
acc = (acc << k) | out
|
|
71
|
+
accn += k
|
|
72
|
+
while accn >= 8:
|
|
73
|
+
accn -= 8
|
|
74
|
+
pool.append((acc >> accn) & 0xFF)
|
|
75
|
+
acc &= (1 << accn) - 1
|
|
76
|
+
if accn:
|
|
77
|
+
pool.append((acc << (8 - accn)) & 0xFF)
|
|
78
|
+
return bytes(pool)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class OpenMeteoAPI:
|
|
82
|
+
"""
|
|
83
|
+
Entropy source using Open-Meteo weather API data.
|
|
84
|
+
|
|
85
|
+
Each fetch harvests the full hourly and daily series of a rolling window
|
|
86
|
+
for one location; ``get_seed`` samples several random locations and
|
|
87
|
+
moments, folds all those readings into one large seed.
|
|
88
|
+
|
|
89
|
+
Only provides entropy - use RandomUsingOpenMeteoAPI for the full random
|
|
90
|
+
interface.
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
BASE_URL = "https://api.open-meteo.com/v1/forecast"
|
|
94
|
+
|
|
95
|
+
# The full harvest: every hourly series the best-match model publishes.
|
|
96
|
+
HOURLY_VARIABLES = [
|
|
97
|
+
"temperature_2m",
|
|
98
|
+
"relative_humidity_2m",
|
|
99
|
+
"dew_point_2m",
|
|
100
|
+
"apparent_temperature",
|
|
101
|
+
"precipitation",
|
|
102
|
+
"rain",
|
|
103
|
+
"showers",
|
|
104
|
+
"snowfall",
|
|
105
|
+
"snow_depth",
|
|
106
|
+
"freezing_level_height",
|
|
107
|
+
"pressure_msl",
|
|
108
|
+
"surface_pressure",
|
|
109
|
+
"cloud_cover",
|
|
110
|
+
"cloud_cover_low",
|
|
111
|
+
"cloud_cover_mid",
|
|
112
|
+
"cloud_cover_high",
|
|
113
|
+
"visibility",
|
|
114
|
+
"cape",
|
|
115
|
+
"wind_speed_10m",
|
|
116
|
+
"wind_direction_10m",
|
|
117
|
+
"wind_gusts_10m",
|
|
118
|
+
"temperature_80m",
|
|
119
|
+
"soil_temperature_0_to_7cm",
|
|
120
|
+
"soil_moisture_0_to_1cm",
|
|
121
|
+
"shortwave_radiation",
|
|
122
|
+
"direct_radiation",
|
|
123
|
+
"diffuse_radiation",
|
|
124
|
+
"vapour_pressure_deficit",
|
|
125
|
+
"uv_index",
|
|
126
|
+
]
|
|
127
|
+
|
|
128
|
+
# Daily aggregates: one row per day, still many real numbers per location.
|
|
129
|
+
DAILY_VARIABLES = [
|
|
130
|
+
"weather_code",
|
|
131
|
+
"temperature_2m_max",
|
|
132
|
+
"temperature_2m_min",
|
|
133
|
+
"apparent_temperature_max",
|
|
134
|
+
"apparent_temperature_min",
|
|
135
|
+
"precipitation_sum",
|
|
136
|
+
"rain_sum",
|
|
137
|
+
"showers_sum",
|
|
138
|
+
"snowfall_sum",
|
|
139
|
+
"precipitation_hours",
|
|
140
|
+
"precipitation_probability_max",
|
|
141
|
+
"daylight_duration",
|
|
142
|
+
"sunshine_duration",
|
|
143
|
+
"wind_speed_10m_max",
|
|
144
|
+
"wind_gusts_10m_max",
|
|
145
|
+
"wind_direction_10m_dominant",
|
|
146
|
+
"shortwave_radiation_sum",
|
|
147
|
+
"uv_index_max",
|
|
148
|
+
"et0_fao_evapotranspiration",
|
|
149
|
+
]
|
|
150
|
+
|
|
151
|
+
# Lean retry set: the original single-snapshot variable list, in case a
|
|
152
|
+
# model/location rejects parts of the full harvest.
|
|
153
|
+
LEGACY_HOURLY_VARIABLES = [
|
|
154
|
+
"temperature_2m",
|
|
155
|
+
"relative_humidity_2m",
|
|
156
|
+
"precipitation",
|
|
157
|
+
"surface_pressure",
|
|
158
|
+
"wind_speed_10m",
|
|
159
|
+
"wind_direction_10m",
|
|
160
|
+
"cloud_cover",
|
|
161
|
+
]
|
|
162
|
+
|
|
163
|
+
DEFAULT_SEED_BYTES = 64
|
|
164
|
+
|
|
165
|
+
def __init__(
|
|
166
|
+
self,
|
|
167
|
+
seed: Optional[int] = None,
|
|
168
|
+
forecast_days: int = 3,
|
|
169
|
+
past_days: int = 2,
|
|
170
|
+
):
|
|
171
|
+
forecast_days = int(forecast_days)
|
|
172
|
+
past_days = int(past_days)
|
|
173
|
+
if not 1 <= forecast_days <= 16:
|
|
174
|
+
raise ValueError("forecast_days must be between 1 and 16")
|
|
175
|
+
if not 0 <= past_days <= 92:
|
|
176
|
+
raise ValueError("past_days must be between 0 and 92")
|
|
177
|
+
self._rng = random.Random(seed)
|
|
178
|
+
self._forecast_days = forecast_days
|
|
179
|
+
self._past_days = past_days
|
|
180
|
+
self._current_location: Optional[Tuple[float, float]] = None
|
|
181
|
+
|
|
182
|
+
def set_current_location(self, latitude: float, longitude: float):
|
|
183
|
+
"""Set the current location for weather-based entropy."""
|
|
184
|
+
self._current_location = (latitude, longitude)
|
|
185
|
+
|
|
186
|
+
def _generate_random_location(self) -> Tuple[float, float]:
|
|
187
|
+
"""Generate a random latitude and longitude."""
|
|
188
|
+
latitude = self._rng.uniform(-60, 70)
|
|
189
|
+
longitude = self._rng.uniform(-180, 180)
|
|
190
|
+
return (latitude, longitude)
|
|
191
|
+
|
|
192
|
+
def _generate_random_time(self, days_back: int = 7) -> datetime:
|
|
193
|
+
"""Generate a random time within the past few days."""
|
|
194
|
+
now = datetime.now(timezone.utc)
|
|
195
|
+
random_hours = self._rng.randint(0, days_back * 24)
|
|
196
|
+
return now - timedelta(hours=random_hours)
|
|
197
|
+
|
|
198
|
+
async def _fetch_weather_data(
|
|
199
|
+
self,
|
|
200
|
+
latitude: float,
|
|
201
|
+
longitude: float,
|
|
202
|
+
target_time: Optional[datetime] = None,
|
|
203
|
+
) -> Optional[dict]:
|
|
204
|
+
"""Fetch the full hourly + daily arrays from Open-Meteo.
|
|
205
|
+
|
|
206
|
+
The full variable list is requested first; if the API rejects it for
|
|
207
|
+
this model/location (HTTP 400), the request is retried once with the
|
|
208
|
+
lean legacy list. Returns the JSON payload (may be missing variables)
|
|
209
|
+
or None.
|
|
210
|
+
"""
|
|
211
|
+
params = {
|
|
212
|
+
"latitude": latitude,
|
|
213
|
+
"longitude": longitude,
|
|
214
|
+
"hourly": ",".join(self.HOURLY_VARIABLES),
|
|
215
|
+
"daily": ",".join(self.DAILY_VARIABLES),
|
|
216
|
+
"timezone": "UTC",
|
|
217
|
+
"forecast_days": str(self._forecast_days),
|
|
218
|
+
"past_days": str(self._past_days),
|
|
219
|
+
}
|
|
220
|
+
async with aiohttp.ClientSession() as session:
|
|
221
|
+
try:
|
|
222
|
+
async with session.get(self.BASE_URL, params=params) as response:
|
|
223
|
+
if response.status == 200:
|
|
224
|
+
return await response.json()
|
|
225
|
+
except Exception:
|
|
226
|
+
pass
|
|
227
|
+
# Retry leaner: the full hourly list may be unsupported here.
|
|
228
|
+
lean = dict(params)
|
|
229
|
+
lean["hourly"] = ",".join(self.LEGACY_HOURLY_VARIABLES)
|
|
230
|
+
lean.pop("daily", None)
|
|
231
|
+
async with aiohttp.ClientSession() as session:
|
|
232
|
+
try:
|
|
233
|
+
async with session.get(self.BASE_URL, params=lean) as response:
|
|
234
|
+
if response.status == 200:
|
|
235
|
+
return await response.json()
|
|
236
|
+
except Exception:
|
|
237
|
+
pass
|
|
238
|
+
return None
|
|
239
|
+
|
|
240
|
+
async def _fetch_current_location(self) -> Optional[Tuple[float, float]]:
|
|
241
|
+
"""Fetch approximate current location via ipinfo.io (geopy fallback)."""
|
|
242
|
+
try:
|
|
243
|
+
# Free, no key needed.
|
|
244
|
+
async with aiohttp.ClientSession() as session:
|
|
245
|
+
async with session.get("https://ipinfo.io/json") as response:
|
|
246
|
+
if response.status == 200:
|
|
247
|
+
data = await response.json()
|
|
248
|
+
loc = data.get("loc", "")
|
|
249
|
+
if loc:
|
|
250
|
+
lat, lon = loc.split(",")
|
|
251
|
+
return (float(lat), float(lon))
|
|
252
|
+
|
|
253
|
+
# Fallback: geocode a random major city for coarse entropy.
|
|
254
|
+
from geopy.geocoders import Nominatim
|
|
255
|
+
|
|
256
|
+
geolocator = Nominatim(user_agent="multirandom_weather")
|
|
257
|
+
cities = ["Tokyo", "New York", "London", "Sydney", "Mumbai", "São Paulo"]
|
|
258
|
+
city = self._rng.choice(cities)
|
|
259
|
+
location = geolocator.geocode(city)
|
|
260
|
+
if location:
|
|
261
|
+
return (location.latitude, location.longitude)
|
|
262
|
+
return None
|
|
263
|
+
except Exception:
|
|
264
|
+
return None
|
|
265
|
+
|
|
266
|
+
def _collect_measurements(
|
|
267
|
+
self,
|
|
268
|
+
weather_data: dict,
|
|
269
|
+
target_time: Optional[datetime] = None,
|
|
270
|
+
) -> List[float]:
|
|
271
|
+
"""Pull every real number out of a weather response (no hashing).
|
|
272
|
+
|
|
273
|
+
Gathers the complete hourly series, the daily aggregates and the
|
|
274
|
+
response metadata, plus the chosen time and the wall clock as
|
|
275
|
+
floats -- the caller packs them losslessly.
|
|
276
|
+
"""
|
|
277
|
+
measurements: List[float] = []
|
|
278
|
+
hourly = weather_data.get("hourly") or {}
|
|
279
|
+
for var in self.HOURLY_VARIABLES:
|
|
280
|
+
values = hourly.get(var)
|
|
281
|
+
if isinstance(values, list):
|
|
282
|
+
measurements.extend(float(v) for v in values if v is not None)
|
|
283
|
+
daily = weather_data.get("daily") or {}
|
|
284
|
+
for var in self.DAILY_VARIABLES:
|
|
285
|
+
values = daily.get(var)
|
|
286
|
+
if isinstance(values, list):
|
|
287
|
+
measurements.extend(float(v) for v in values if v is not None)
|
|
288
|
+
for key in ("latitude", "longitude", "elevation",
|
|
289
|
+
"generationtime_ms", "utc_offset_seconds"):
|
|
290
|
+
try:
|
|
291
|
+
measurements.append(float(weather_data[key]))
|
|
292
|
+
except (KeyError, TypeError, ValueError):
|
|
293
|
+
pass
|
|
294
|
+
if target_time is not None:
|
|
295
|
+
measurements.append(float(target_time.timestamp()))
|
|
296
|
+
measurements.append(time.time_ns() / 1e9)
|
|
297
|
+
return measurements
|
|
298
|
+
|
|
299
|
+
def _fold_odd(self, seed_bytes: int) -> int:
|
|
300
|
+
"""Splitmix64-derived odd multiplier, fresh per seed width."""
|
|
301
|
+
x = _GOLDEN
|
|
302
|
+
raw = bytearray()
|
|
303
|
+
while len(raw) < seed_bytes:
|
|
304
|
+
x = (x * _SPLITMIX_MUL + _SPLITMIX_ADD) & 0xFFFFFFFFFFFFFFFF
|
|
305
|
+
raw += x.to_bytes(8, "big")
|
|
306
|
+
return int.from_bytes(bytes(raw[:seed_bytes]), "big") | 1
|
|
307
|
+
|
|
308
|
+
def _assemble_seed(self, measurements: Sequence[float], seed_bytes: int) -> int:
|
|
309
|
+
"""Fold every measurement into one large integer -- hash-free.
|
|
310
|
+
|
|
311
|
+
1. exact IEEE-754 bytes of each measurement -> the "tape";
|
|
312
|
+
2. the tape's bits get von Neumann-debiased into a second stream;
|
|
313
|
+
3. both streams are folded (multiply-add + chunk-dependent rotation)
|
|
314
|
+
into a seed_bytes-wide big integer, which is returned.
|
|
315
|
+
"""
|
|
316
|
+
tape = bytearray()
|
|
317
|
+
for value in measurements:
|
|
318
|
+
tape += struct.pack("<d", float(value))
|
|
319
|
+
|
|
320
|
+
pool = _von_neumann_pool(bytes(tape))
|
|
321
|
+
|
|
322
|
+
nbits = seed_bytes * 8
|
|
323
|
+
mask = (1 << nbits) - 1
|
|
324
|
+
odd = self._fold_odd(seed_bytes)
|
|
325
|
+
state = 1
|
|
326
|
+
for src in (bytes(tape), pool):
|
|
327
|
+
if not src:
|
|
328
|
+
continue
|
|
329
|
+
src += b"\x00" * (-len(src) % 8)
|
|
330
|
+
for off in range(0, len(src), 8):
|
|
331
|
+
chunk = int.from_bytes(src[off:off + 8], "big")
|
|
332
|
+
state = (state * odd + chunk) & mask
|
|
333
|
+
rot = 1 + chunk % (nbits - 1)
|
|
334
|
+
state = ((state >> rot) | ((state << (nbits - rot)) & mask)) & mask
|
|
335
|
+
return state
|
|
336
|
+
|
|
337
|
+
async def get_seed(
|
|
338
|
+
self,
|
|
339
|
+
use_random_location: bool = True,
|
|
340
|
+
use_current_location: bool = False,
|
|
341
|
+
use_random_time: bool = True,
|
|
342
|
+
num_random_locations: int = 3,
|
|
343
|
+
seed_bytes: int = DEFAULT_SEED_BYTES,
|
|
344
|
+
) -> int:
|
|
345
|
+
"""
|
|
346
|
+
Get a large seed value from weather entropy.
|
|
347
|
+
|
|
348
|
+
Args:
|
|
349
|
+
use_random_location: Fetch weather from random locations
|
|
350
|
+
use_current_location: Fetch weather from current location
|
|
351
|
+
use_random_time: Sample random moments (up to a week back)
|
|
352
|
+
num_random_locations: Number of random locations to sample (0-12)
|
|
353
|
+
seed_bytes: Seed width in bytes, 16..1024 (128..8192 bits)
|
|
354
|
+
|
|
355
|
+
Returns:
|
|
356
|
+
A big integer ``0 <= seed < 2^(8*seed_bytes)`` built from the raw
|
|
357
|
+
measurement bytes, WITHOUT cryptographic hashing (lossless tape +
|
|
358
|
+
von Neumann conditioning + reversible fold). If every fetch fails,
|
|
359
|
+
returns fresh ``os.urandom`` bytes as an int -- never a time hash.
|
|
360
|
+
"""
|
|
361
|
+
seed_bytes = int(seed_bytes)
|
|
362
|
+
if not 16 <= seed_bytes <= 1024:
|
|
363
|
+
raise ValueError("seed_bytes must be between 16 and 1024")
|
|
364
|
+
num_random_locations = int(num_random_locations)
|
|
365
|
+
if not 0 <= num_random_locations <= 12:
|
|
366
|
+
raise ValueError("num_random_locations must be between 0 and 12")
|
|
367
|
+
|
|
368
|
+
targets: List[Tuple[float, float, Optional[datetime]]] = []
|
|
369
|
+
if use_random_location:
|
|
370
|
+
for _ in range(num_random_locations):
|
|
371
|
+
lat, lon = self._generate_random_location()
|
|
372
|
+
target_time = self._generate_random_time() if use_random_time else None
|
|
373
|
+
targets.append((lat, lon, target_time))
|
|
374
|
+
|
|
375
|
+
if use_current_location:
|
|
376
|
+
if self._current_location:
|
|
377
|
+
lat, lon = self._current_location
|
|
378
|
+
else:
|
|
379
|
+
location = await self._fetch_current_location()
|
|
380
|
+
if location:
|
|
381
|
+
lat, lon = location
|
|
382
|
+
self._current_location = location
|
|
383
|
+
else:
|
|
384
|
+
lat, lon = 0.0, 0.0
|
|
385
|
+
target_time = self._generate_random_time() if use_random_time else None
|
|
386
|
+
targets.append((lat, lon, target_time))
|
|
387
|
+
|
|
388
|
+
measurements: List[float] = []
|
|
389
|
+
if targets:
|
|
390
|
+
results = await asyncio.gather(*(
|
|
391
|
+
self._fetch_weather_data(lat, lon, tt)
|
|
392
|
+
for lat, lon, tt in targets))
|
|
393
|
+
for data, (_lat, _lon, tt) in zip(results, targets):
|
|
394
|
+
if data:
|
|
395
|
+
measurements.extend(self._collect_measurements(data, tt))
|
|
396
|
+
|
|
397
|
+
if not measurements:
|
|
398
|
+
return int.from_bytes(os.urandom(seed_bytes), "big")
|
|
399
|
+
return self._assemble_seed(measurements, seed_bytes)
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
class RandomUsingOpenMeteoAPI(random.Random):
|
|
403
|
+
"""
|
|
404
|
+
A random.Random subclass seeded with a large, unhashed weather seed.
|
|
405
|
+
|
|
406
|
+
The weather seed (64 bytes / 512 bits by default; see OpenMeteoAPI.get_seed)
|
|
407
|
+
is XORed term-to-term with fresh os.urandom noise of the same width -- a
|
|
408
|
+
perfect blend that needs no hashing -- and handed whole to Mersenne
|
|
409
|
+
Twister's state loader, which consumes the entire integer.
|
|
410
|
+
"""
|
|
411
|
+
|
|
412
|
+
def __init__(
|
|
413
|
+
self,
|
|
414
|
+
auto_seed: bool = True,
|
|
415
|
+
seed_bytes: int = OpenMeteoAPI.DEFAULT_SEED_BYTES,
|
|
416
|
+
):
|
|
417
|
+
"""
|
|
418
|
+
Initialize the random generator.
|
|
419
|
+
|
|
420
|
+
Args:
|
|
421
|
+
auto_seed: If True, automatically seeds with weather entropy.
|
|
422
|
+
seed_bytes: Weather seed width in bytes, 16..1024 (default 64 =>
|
|
423
|
+
a 512-bit seed, three scales larger than the old 256-bit one).
|
|
424
|
+
"""
|
|
425
|
+
self._seed_bytes = int(seed_bytes)
|
|
426
|
+
if not 16 <= self._seed_bytes <= 1024:
|
|
427
|
+
raise ValueError("seed_bytes must be between 16 and 1024")
|
|
428
|
+
self._api = OpenMeteoAPI()
|
|
429
|
+
# random.Random.__init__ calls self.seed(x); with x=None our override
|
|
430
|
+
# would trigger a weather fetch, so hand it OS entropy directly and
|
|
431
|
+
# reserve the (auto) weather seed for the explicit step below.
|
|
432
|
+
super().__init__(int.from_bytes(os.urandom(32), "big"))
|
|
433
|
+
|
|
434
|
+
if auto_seed:
|
|
435
|
+
self.seed_from_weather()
|
|
436
|
+
|
|
437
|
+
def _run_async(self, coro):
|
|
438
|
+
"""Helper to run async code synchronously."""
|
|
439
|
+
try:
|
|
440
|
+
asyncio.get_running_loop()
|
|
441
|
+
import concurrent.futures
|
|
442
|
+
with concurrent.futures.ThreadPoolExecutor() as pool:
|
|
443
|
+
future = pool.submit(asyncio.run, coro)
|
|
444
|
+
return future.result()
|
|
445
|
+
except RuntimeError:
|
|
446
|
+
return asyncio.run(coro)
|
|
447
|
+
|
|
448
|
+
def seed_from_weather(
|
|
449
|
+
self,
|
|
450
|
+
use_random_location: bool = True,
|
|
451
|
+
use_current_location: bool = True,
|
|
452
|
+
use_random_time: bool = True,
|
|
453
|
+
):
|
|
454
|
+
"""Seed with a large weather seed XOR fresh OS noise (no hashing)."""
|
|
455
|
+
try:
|
|
456
|
+
weather_seed = self._run_async(self._api.get_seed(
|
|
457
|
+
use_random_location=use_random_location,
|
|
458
|
+
use_current_location=use_current_location,
|
|
459
|
+
use_random_time=use_random_time,
|
|
460
|
+
seed_bytes=self._seed_bytes,
|
|
461
|
+
))
|
|
462
|
+
except Exception:
|
|
463
|
+
weather_seed = 0
|
|
464
|
+
width = max((weather_seed.bit_length() + 7) // 8, self._seed_bytes)
|
|
465
|
+
noise = int.from_bytes(os.urandom(width), "big")
|
|
466
|
+
combined = weather_seed ^ noise
|
|
467
|
+
# Feed the whole wide integer to Mersenne Twister's state loader.
|
|
468
|
+
super().seed(combined)
|
|
469
|
+
|
|
470
|
+
def seed(self, a=None, version=2):
|
|
471
|
+
"""Seed the generator. If a is None, seeds from weather."""
|
|
472
|
+
if a is None:
|
|
473
|
+
self.seed_from_weather()
|
|
474
|
+
else:
|
|
475
|
+
super().seed(a, version)
|
|
476
|
+
|
|
477
|
+
def reseed_from_weather(self):
|
|
478
|
+
"""Force a reseed from fresh weather data."""
|
|
479
|
+
self.seed_from_weather()
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
if __name__ == "__main__":
|
|
483
|
+
api = OpenMeteoAPI(seed=1234)
|
|
484
|
+
|
|
485
|
+
# --- offline: determinism, sensitivity, configurability of the fold ---
|
|
486
|
+
tape = [api._rng.uniform(-40, 40) for _ in range(3000)]
|
|
487
|
+
s1 = api._assemble_seed(tape, 64)
|
|
488
|
+
assert s1.bit_length() <= 512, "64-byte seed overflowed 512 bits"
|
|
489
|
+
assert s1 == api._assemble_seed(list(tape), 64), "fold is not deterministic"
|
|
490
|
+
tape2 = list(tape)
|
|
491
|
+
tape2[0] += 1e-9
|
|
492
|
+
assert s1 != api._assemble_seed(tape2, 64), "seed ignores its measurements"
|
|
493
|
+
big = api._assemble_seed(tape, 128)
|
|
494
|
+
assert big.bit_length() <= 1024 and big != s1, "wider fold misbehaves"
|
|
495
|
+
tiny = api._assemble_seed(tape, 16)
|
|
496
|
+
assert tiny.bit_length() <= 128, "16-byte seed overflowed 128 bits"
|
|
497
|
+
print("OpenMeteoAPI fold: deterministic, sensitive, width-configurable: OK")
|
|
498
|
+
|
|
499
|
+
# --- offline: von Neumann conditioning really de-biases ---
|
|
500
|
+
rng = random.Random(9)
|
|
501
|
+
buf = bytearray(1 << 16)
|
|
502
|
+
# Build 65536 bytes whose bits are iid with P(1) = 0.75 (heavily biased).
|
|
503
|
+
for i in range(1 << 16):
|
|
504
|
+
acc = 0
|
|
505
|
+
for j in range(8):
|
|
506
|
+
if rng.random() < 0.75:
|
|
507
|
+
acc |= 1 << j
|
|
508
|
+
buf[i] = acc
|
|
509
|
+
raw_ratio = sum(bin(b).count("1") for b in buf) / (8 * len(buf))
|
|
510
|
+
pool = _von_neumann_pool(bytes(buf))
|
|
511
|
+
assert pool, "von Neumann pool came out empty"
|
|
512
|
+
pool_ratio = sum(bin(b).count("1") for b in pool) / (8 * len(pool))
|
|
513
|
+
assert 0.4 < pool_ratio < 0.6, \
|
|
514
|
+
f"von Neumann conditioning failed ({raw_ratio:.3f} -> {pool_ratio:.3f})"
|
|
515
|
+
print(f"Von Neumann conditioning: {raw_ratio:.3f} -> {pool_ratio:.3f} ones ratio: OK")
|
|
516
|
+
|
|
517
|
+
# --- the random.Random wrapper itself (no network needed) ---
|
|
518
|
+
g = RandomUsingOpenMeteoAPI(auto_seed=False, seed_bytes=64)
|
|
519
|
+
g.seed(42)
|
|
520
|
+
assert all(0.0 <= g.random() < 1.0 for _ in range(200))
|
|
521
|
+
assert 0 <= g.choice(range(50)) < 50
|
|
522
|
+
print("RandomUsingOpenMeteoAPI API (fixed seed): OK")
|
|
523
|
+
|
|
524
|
+
# --- live weather fetch (network guard; falls back to os.urandom) ---
|
|
525
|
+
try:
|
|
526
|
+
seed = asyncio.run(api.get_seed(num_random_locations=1, seed_bytes=64))
|
|
527
|
+
assert 0 <= seed < (1 << 512), "get_seed broke its width contract"
|
|
528
|
+
other = asyncio.run(OpenMeteoAPI(seed=5678).get_seed(num_random_locations=1))
|
|
529
|
+
assert seed != other, "two live harvests gave identical seeds"
|
|
530
|
+
g2 = RandomUsingOpenMeteoAPI(auto_seed=False, seed_bytes=64)
|
|
531
|
+
g2.reseed_from_weather()
|
|
532
|
+
assert 0.0 <= g2.random() < 1.0
|
|
533
|
+
print(f"Weather harvest + XOR-urandom reseed: OK "
|
|
534
|
+
f"(seed {seed.bit_length()} bits, top hex {hex(seed)[:10]}…)")
|
|
535
|
+
except Exception as exc: # noqa: BLE001 - offline environment / network down
|
|
536
|
+
fallback = int.from_bytes(os.urandom(64), "big")
|
|
537
|
+
assert 0 <= fallback < (1 << 512)
|
|
538
|
+
print(f"Live weather fetch skipped ({type(exc).__name__}: {exc}); "
|
|
539
|
+
"os.urandom fallback path OK")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: multirandom
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.8
|
|
4
4
|
Summary: 🎲 The truth external PyPI random package, have trng (true randomness), prng_unsecure, csprng, ...
|
|
5
5
|
Author-email: BlackCatOfficialytb <quanvm0501@gmail.com>
|
|
6
6
|
License: Apache-2.0
|
|
@@ -30,6 +30,20 @@ A comprehensive library for exploring and generating randomness across the spect
|
|
|
30
30
|
|
|
31
31
|
---
|
|
32
32
|
|
|
33
|
+
## 📚 Full Documentation
|
|
34
|
+
|
|
35
|
+
Deep-dive docs live in [`docs/`](docs/):
|
|
36
|
+
|
|
37
|
+
| Page | Contents |
|
|
38
|
+
|---|---|
|
|
39
|
+
| [Getting started](docs/getting-started.md) | installation, quickstart, which generator to pick |
|
|
40
|
+
| [True randomness (`trng`)](docs/trng.md) | mouse clicks, quantum APIs, microphone, weather |
|
|
41
|
+
| [Cryptographic randomness (`csprng`)](docs/csprng.md) | OS CSPRNGs, NIST DRBGs, the Dual_EC backdoor demo, Fortuna/ISAAC |
|
|
42
|
+
| [Unsecure PRNGs (`prng_unsecure`)](docs/prng_unsecure.md) | XorShift/Xoshiro, LCGs, and their reversal attacks |
|
|
43
|
+
| [Self-checks](docs/self-checks.md) | the assert batteries verifying every generator |
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
33
47
|
## 🏗 Project Structure
|
|
34
48
|
|
|
35
49
|
```text
|
|
@@ -40,7 +54,7 @@ multirandom/
|
|
|
40
54
|
│ ├── rand_via_clicks.py # Human-in-the-loop entropy
|
|
41
55
|
│ ├── rand_using_online_api.py # Quantum & Hardware APIs (ANU QRNG, Roll-API)
|
|
42
56
|
│ ├── rand_using_real_random.py # Microphone entropy (real-random)
|
|
43
|
-
│ └── weather_rand.py #
|
|
57
|
+
│ └── weather_rand.py # Open-Meteo weather entropy (large unhashed seeds)
|
|
44
58
|
├── csprng/ # Cryptographically Strong Randomness
|
|
45
59
|
│ ├── secure_rand.py # OS CSPRNGs, NIST DRBGs, Fortuna/Yarrow, ISAAC
|
|
46
60
|
│ ├── sha_rand.py # MD5, SHA-1/2/3, BLAKE2, SHAKE PRNGs
|
|
File without changes
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
from random import *
|
|
File without changes
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
print("coming soon :/")
|
|
File without changes
|
|
@@ -1,281 +0,0 @@
|
|
|
1
|
-
import aiohttp
|
|
2
|
-
import asyncio
|
|
3
|
-
import random
|
|
4
|
-
import hashlib
|
|
5
|
-
from datetime import datetime, timedelta
|
|
6
|
-
from typing import Optional, Tuple
|
|
7
|
-
|
|
8
|
-
"""
|
|
9
|
-
A random source using Open-Meteo weather API data.
|
|
10
|
-
While still not fully random and can be spoofed, it's much more reliable than any others cuz it uses hash random from random() too.
|
|
11
|
-
"""
|
|
12
|
-
|
|
13
|
-
class OpenMeteoAPI:
|
|
14
|
-
"""
|
|
15
|
-
Entropy source using Open-Meteo weather API data.
|
|
16
|
-
|
|
17
|
-
Fetches weather data from random locations and times to generate entropy
|
|
18
|
-
for seeding random.Random. Only provides entropy - use RandomUsingOpenMeteoAPI
|
|
19
|
-
for the full random interface.
|
|
20
|
-
"""
|
|
21
|
-
|
|
22
|
-
BASE_URL = "https://api.open-meteo.com/v1/forecast"
|
|
23
|
-
|
|
24
|
-
WEATHER_VARIABLES = [
|
|
25
|
-
"temperature_2m",
|
|
26
|
-
"relative_humidity_2m",
|
|
27
|
-
"precipitation",
|
|
28
|
-
"surface_pressure",
|
|
29
|
-
"wind_speed_10m",
|
|
30
|
-
"wind_direction_10m",
|
|
31
|
-
"cloud_cover"
|
|
32
|
-
]
|
|
33
|
-
|
|
34
|
-
def __init__(self, seed: Optional[int] = None):
|
|
35
|
-
self._rng = random.Random(seed)
|
|
36
|
-
self._current_location: Optional[Tuple[float, float]] = None
|
|
37
|
-
|
|
38
|
-
def set_current_location(self, latitude: float, longitude: float):
|
|
39
|
-
"""Set the current location for weather-based entropy."""
|
|
40
|
-
self._current_location = (latitude, longitude)
|
|
41
|
-
|
|
42
|
-
def _generate_random_location(self) -> Tuple[float, float]:
|
|
43
|
-
"""Generate a random latitude and longitude."""
|
|
44
|
-
latitude = self._rng.uniform(-60, 70)
|
|
45
|
-
longitude = self._rng.uniform(-180, 180)
|
|
46
|
-
return (latitude, longitude)
|
|
47
|
-
|
|
48
|
-
def _generate_random_time(self, days_back: int = 7) -> datetime:
|
|
49
|
-
"""Generate a random time within the past few days."""
|
|
50
|
-
now = datetime.utcnow()
|
|
51
|
-
random_hours = self._rng.randint(0, days_back * 24)
|
|
52
|
-
return now - timedelta(hours=random_hours)
|
|
53
|
-
|
|
54
|
-
async def _fetch_weather_data(
|
|
55
|
-
self,
|
|
56
|
-
latitude: float,
|
|
57
|
-
longitude: float,
|
|
58
|
-
target_time: Optional[datetime] = None
|
|
59
|
-
) -> Optional[dict]:
|
|
60
|
-
"""Fetch weather data from Open-Meteo API."""
|
|
61
|
-
params = {
|
|
62
|
-
"latitude": latitude,
|
|
63
|
-
"longitude": longitude,
|
|
64
|
-
"hourly": ",".join(self.WEATHER_VARIABLES),
|
|
65
|
-
"timezone": "UTC"
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
if target_time:
|
|
69
|
-
date_str = target_time.strftime("%Y-%m-%d")
|
|
70
|
-
params["start_date"] = date_str
|
|
71
|
-
params["end_date"] = date_str
|
|
72
|
-
|
|
73
|
-
async with aiohttp.ClientSession() as session:
|
|
74
|
-
try:
|
|
75
|
-
async with session.get(self.BASE_URL, params=params) as response:
|
|
76
|
-
if response.status == 200:
|
|
77
|
-
return await response.json()
|
|
78
|
-
return None
|
|
79
|
-
except Exception:
|
|
80
|
-
return None
|
|
81
|
-
|
|
82
|
-
async def _fetch_current_location(self) -> Optional[Tuple[float, float]]:
|
|
83
|
-
"""Fetch approximate current location using IP geolocation via geopy."""
|
|
84
|
-
try:
|
|
85
|
-
from geopy.geocoders import Nominatim
|
|
86
|
-
|
|
87
|
-
# First get public IP location using ipinfo.io (free, no key needed)
|
|
88
|
-
async with aiohttp.ClientSession() as session:
|
|
89
|
-
async with session.get("https://ipinfo.io/json") as response:
|
|
90
|
-
if response.status == 200:
|
|
91
|
-
data = await response.json()
|
|
92
|
-
loc = data.get("loc", "")
|
|
93
|
-
if loc:
|
|
94
|
-
lat, lon = loc.split(",")
|
|
95
|
-
return (float(lat), float(lon))
|
|
96
|
-
|
|
97
|
-
# Fallback: use geopy to geocode a random city for entropy
|
|
98
|
-
geolocator = Nominatim(user_agent="multirandom_weather")
|
|
99
|
-
# Pick a random major city for location entropy
|
|
100
|
-
cities = ["Tokyo", "New York", "London", "Sydney", "Mumbai", "São Paulo"]
|
|
101
|
-
city = self._rng.choice(cities)
|
|
102
|
-
location = geolocator.geocode(city)
|
|
103
|
-
if location:
|
|
104
|
-
return (location.latitude, location.longitude)
|
|
105
|
-
|
|
106
|
-
return None
|
|
107
|
-
except Exception:
|
|
108
|
-
return None
|
|
109
|
-
|
|
110
|
-
def _extract_entropy(self, weather_data: dict, target_time: Optional[datetime] = None) -> bytes:
|
|
111
|
-
"""Extract entropy bytes from weather data."""
|
|
112
|
-
entropy_values = []
|
|
113
|
-
hourly = weather_data.get("hourly", {})
|
|
114
|
-
|
|
115
|
-
hour_index = 0
|
|
116
|
-
if target_time and "time" in hourly:
|
|
117
|
-
times = hourly.get("time", [])
|
|
118
|
-
target_hour = target_time.strftime("%Y-%m-%dT%H:00")
|
|
119
|
-
if target_hour in times:
|
|
120
|
-
hour_index = times.index(target_hour)
|
|
121
|
-
else:
|
|
122
|
-
hour_index = self._rng.randint(0, max(0, len(times) - 1))
|
|
123
|
-
elif "time" in hourly:
|
|
124
|
-
times = hourly.get("time", [])
|
|
125
|
-
hour_index = self._rng.randint(0, max(0, len(times) - 1))
|
|
126
|
-
|
|
127
|
-
for var in self.WEATHER_VARIABLES:
|
|
128
|
-
values = hourly.get(var, [])
|
|
129
|
-
if values and hour_index < len(values) and values[hour_index] is not None:
|
|
130
|
-
entropy_values.append(values[hour_index])
|
|
131
|
-
|
|
132
|
-
entropy_values.extend([
|
|
133
|
-
weather_data.get("latitude", 0),
|
|
134
|
-
weather_data.get("longitude", 0),
|
|
135
|
-
weather_data.get("elevation", 0),
|
|
136
|
-
datetime.utcnow().timestamp()
|
|
137
|
-
])
|
|
138
|
-
|
|
139
|
-
entropy_str = "|".join(str(v) for v in entropy_values)
|
|
140
|
-
return hashlib.sha256(entropy_str.encode()).digest()
|
|
141
|
-
|
|
142
|
-
async def get_seed(
|
|
143
|
-
self,
|
|
144
|
-
use_random_location: bool = True,
|
|
145
|
-
use_current_location: bool = False,
|
|
146
|
-
use_random_time: bool = True,
|
|
147
|
-
num_random_locations: int = 2
|
|
148
|
-
) -> int:
|
|
149
|
-
"""
|
|
150
|
-
Get a seed value from weather entropy.
|
|
151
|
-
|
|
152
|
-
Args:
|
|
153
|
-
use_random_location: Fetch weather from random locations
|
|
154
|
-
use_current_location: Fetch weather from current location
|
|
155
|
-
use_random_time: Use random times for weather data
|
|
156
|
-
num_random_locations: Number of random locations to sample
|
|
157
|
-
|
|
158
|
-
Returns:
|
|
159
|
-
Integer seed derived from weather data
|
|
160
|
-
"""
|
|
161
|
-
entropy_pool = b""
|
|
162
|
-
tasks = []
|
|
163
|
-
|
|
164
|
-
# Random locations
|
|
165
|
-
if use_random_location:
|
|
166
|
-
for _ in range(num_random_locations):
|
|
167
|
-
lat, lon = self._generate_random_location()
|
|
168
|
-
target_time = self._generate_random_time() if use_random_time else None
|
|
169
|
-
tasks.append((self._fetch_weather_data(lat, lon, target_time), target_time))
|
|
170
|
-
|
|
171
|
-
# Current location
|
|
172
|
-
if use_current_location:
|
|
173
|
-
if self._current_location:
|
|
174
|
-
lat, lon = self._current_location
|
|
175
|
-
else:
|
|
176
|
-
location = await self._fetch_current_location()
|
|
177
|
-
if location:
|
|
178
|
-
lat, lon = location
|
|
179
|
-
self._current_location = location
|
|
180
|
-
else:
|
|
181
|
-
lat, lon = 0, 0
|
|
182
|
-
|
|
183
|
-
target_time = self._generate_random_time() if use_random_time else None
|
|
184
|
-
tasks.append((self._fetch_weather_data(lat, lon, target_time), target_time))
|
|
185
|
-
|
|
186
|
-
# Gather all weather data
|
|
187
|
-
for task, target_time in tasks:
|
|
188
|
-
weather_data = await task
|
|
189
|
-
if weather_data:
|
|
190
|
-
entropy = self._extract_entropy(weather_data, target_time)
|
|
191
|
-
entropy_pool = hashlib.sha256(entropy_pool + entropy).digest()
|
|
192
|
-
|
|
193
|
-
# Fallback if no data gathered
|
|
194
|
-
if not entropy_pool:
|
|
195
|
-
entropy_pool = hashlib.sha256(str(datetime.utcnow().timestamp()).encode()).digest()
|
|
196
|
-
|
|
197
|
-
return int.from_bytes(entropy_pool, 'big')
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
class RandomUsingOpenMeteoAPI(random.Random):
|
|
201
|
-
"""
|
|
202
|
-
A random.Random subclass seeded with weather entropy from Open-Meteo API.
|
|
203
|
-
|
|
204
|
-
Uses weather data to generate a seed, then delegates all random generation
|
|
205
|
-
to the standard Mersenne Twister (random.Random).
|
|
206
|
-
"""
|
|
207
|
-
|
|
208
|
-
def __init__(self, auto_seed: bool = True):
|
|
209
|
-
"""
|
|
210
|
-
Initialize the random generator.
|
|
211
|
-
|
|
212
|
-
Args:
|
|
213
|
-
auto_seed: If True, automatically seeds with weather entropy.
|
|
214
|
-
"""
|
|
215
|
-
super().__init__()
|
|
216
|
-
self._api = OpenMeteoAPI()
|
|
217
|
-
|
|
218
|
-
if auto_seed:
|
|
219
|
-
self.seed_from_weather()
|
|
220
|
-
|
|
221
|
-
def _run_async(self, coro):
|
|
222
|
-
"""Helper to run async code synchronously."""
|
|
223
|
-
try:
|
|
224
|
-
asyncio.get_running_loop()
|
|
225
|
-
import concurrent.futures
|
|
226
|
-
with concurrent.futures.ThreadPoolExecutor() as pool:
|
|
227
|
-
future = pool.submit(asyncio.run, coro)
|
|
228
|
-
return future.result()
|
|
229
|
-
except RuntimeError:
|
|
230
|
-
return asyncio.run(coro)
|
|
231
|
-
|
|
232
|
-
def seed_from_weather(
|
|
233
|
-
self,
|
|
234
|
-
use_random_location: bool = True,
|
|
235
|
-
use_current_location: bool = True,
|
|
236
|
-
use_random_time: bool = True
|
|
237
|
-
):
|
|
238
|
-
"""Seed the random generator using weather entropy mixed with hash random."""
|
|
239
|
-
try:
|
|
240
|
-
# Get weather-based seed
|
|
241
|
-
weather_seed = self._run_async(self._api.get_seed(
|
|
242
|
-
use_random_location=use_random_location,
|
|
243
|
-
use_current_location=use_current_location,
|
|
244
|
-
use_random_time=use_random_time
|
|
245
|
-
))
|
|
246
|
-
|
|
247
|
-
# Mix with original random module's entropy via SHA256
|
|
248
|
-
from ..csprng.sha_rand import SHA256Random
|
|
249
|
-
import time
|
|
250
|
-
import os
|
|
251
|
-
|
|
252
|
-
# Create SHA256 random seeded with os.urandom + time
|
|
253
|
-
sha_seed = int.from_bytes(os.urandom(32), 'big') ^ int(time.time() * 1000000)
|
|
254
|
-
sha_rng = SHA256Random(seed=sha_seed)
|
|
255
|
-
sha_entropy = int.from_bytes(sha_rng.getrandbytes(32), 'big')
|
|
256
|
-
|
|
257
|
-
# Also get some entropy from Python's random module
|
|
258
|
-
orig_entropy = int.from_bytes(os.urandom(16), 'big')
|
|
259
|
-
|
|
260
|
-
# Combine all entropy sources with XOR and hash
|
|
261
|
-
combined = weather_seed ^ sha_entropy ^ orig_entropy
|
|
262
|
-
final_seed = int.from_bytes(
|
|
263
|
-
hashlib.sha256(combined.to_bytes(64, 'big')).digest(),
|
|
264
|
-
'big'
|
|
265
|
-
)
|
|
266
|
-
|
|
267
|
-
super().seed(final_seed)
|
|
268
|
-
except Exception:
|
|
269
|
-
import time
|
|
270
|
-
super().seed(int(time.time() * 1000000))
|
|
271
|
-
|
|
272
|
-
def seed(self, a=None, version=2):
|
|
273
|
-
"""Seed the generator. If a is None, seeds from weather."""
|
|
274
|
-
if a is None:
|
|
275
|
-
self.seed_from_weather()
|
|
276
|
-
else:
|
|
277
|
-
super().seed(a, version)
|
|
278
|
-
|
|
279
|
-
def reseed_from_weather(self):
|
|
280
|
-
"""Force a reseed from fresh weather data."""
|
|
281
|
-
self.seed_from_weather()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|