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.
Files changed (30) hide show
  1. {multirandom-0.0.7 → multirandom-0.0.8}/PKG-INFO +16 -2
  2. {multirandom-0.0.7 → multirandom-0.0.8}/README.md +15 -1
  3. {multirandom-0.0.7 → multirandom-0.0.8}/pyproject.toml +1 -1
  4. {multirandom-0.0.7 → multirandom-0.0.8}/src/multirandom/__init__.py +9 -3
  5. multirandom-0.0.8/src/multirandom/csprng/__init__.py +13 -0
  6. multirandom-0.0.8/src/multirandom/csprng/original_random.py +11 -0
  7. {multirandom-0.0.7 → multirandom-0.0.8}/src/multirandom/csprng/secure_rand.py +7 -1
  8. {multirandom-0.0.7 → multirandom-0.0.8}/src/multirandom/csprng/sha_rand.py +7 -1
  9. multirandom-0.0.8/src/multirandom/prng_unsecure/__init__.py +12 -0
  10. multirandom-0.0.8/src/multirandom/prng_unsecure/flash_rand_and_reverse.py +10 -0
  11. {multirandom-0.0.7 → multirandom-0.0.8}/src/multirandom/prng_unsecure/rand_using_virt_lcg.py +18 -0
  12. {multirandom-0.0.7 → multirandom-0.0.8}/src/multirandom/prng_unsecure/xor_shift.py +24 -0
  13. multirandom-0.0.8/src/multirandom/trng/__init__.py +12 -0
  14. {multirandom-0.0.7 → multirandom-0.0.8}/src/multirandom/trng/rand_using_online_api.py +14 -0
  15. {multirandom-0.0.7 → multirandom-0.0.8}/src/multirandom/trng/rand_using_real_random.py +7 -2
  16. {multirandom-0.0.7 → multirandom-0.0.8}/src/multirandom/trng/rand_via_clicks.py +16 -0
  17. multirandom-0.0.8/src/multirandom/trng/weather_rand.py +539 -0
  18. {multirandom-0.0.7 → multirandom-0.0.8}/src/multirandom.egg-info/PKG-INFO +16 -2
  19. multirandom-0.0.7/src/multirandom/csprng/__init__.py +0 -0
  20. multirandom-0.0.7/src/multirandom/csprng/original_random.py +0 -1
  21. multirandom-0.0.7/src/multirandom/prng_unsecure/__init__.py +0 -0
  22. multirandom-0.0.7/src/multirandom/prng_unsecure/flash_rand_and_reverse.py +0 -1
  23. multirandom-0.0.7/src/multirandom/trng/__init__.py +0 -0
  24. multirandom-0.0.7/src/multirandom/trng/weather_rand.py +0 -281
  25. {multirandom-0.0.7 → multirandom-0.0.8}/LICENSE +0 -0
  26. {multirandom-0.0.7 → multirandom-0.0.8}/setup.cfg +0 -0
  27. {multirandom-0.0.7 → multirandom-0.0.8}/src/multirandom.egg-info/SOURCES.txt +0 -0
  28. {multirandom-0.0.7 → multirandom-0.0.8}/src/multirandom.egg-info/dependency_links.txt +0 -0
  29. {multirandom-0.0.7 → multirandom-0.0.8}/src/multirandom.egg-info/requires.txt +0 -0
  30. {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.7
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 # [Coming Soon] Weather-based entropy
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 # [Coming Soon] Weather-based entropy
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "multirandom"
7
- version = "0.0.7"
7
+ version = "0.0.8"
8
8
  authors = [
9
9
  { name="BlackCatOfficialytb", email="quanvm0501@gmail.com" },
10
10
  ]
@@ -1,8 +1,14 @@
1
- """multirandom - one random-number package, several philosophies of chance.
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
- """Cryptographic random generators ("csprng").
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
- """Hash-based pseudo-random generators.
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 :/")
@@ -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
- """Ambient-microphone entropy via the optional `real-random` package.
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.7
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 # [Coming Soon] Weather-based entropy
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 *
@@ -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