Skip to content

Commit 7cdea5f

Browse files
30678678ona-agent
andcommitted
security: fix insecure PRNG, pickle RCE, path traversal, and hardcoded secrets
Replace non-CSPRNG random with secrets module in all cryptographic code: - rabin_miller.py: secrets.randbits/randbelow, 40 witness rounds (was 5), guard against num<5 to prevent secrets.randbelow(0) ValueError - rsa_key_generator.py: secrets.randbits for public exponent e - elgamal_key_generator.py: secrets.randbelow for private key d and primitive root g; fix broken primitive_root() filter (pow(g,p,p)==1 is always False by Fermat's Little Theorem; replace with correct Legendre symbol check pow(g,(p-1)//2,p)!=1) - onepad_cipher.py: secrets.randbelow for key generation; update seed-dependent doctests to property-based assertions Replace pickle with json+numpy in CNN model persistence: - convolution_neural_network.py: save_model writes config.json (hyperparameters) and weights.npz (arrays); read_model loads both. Pickle executes arbitrary code on load; neither json nor npz does. Breaking change: existing .pkl model files must be re-saved. Fix path traversal in image downloader: - download_images_from_google_query.py: sanitise query string with re.sub before use as directory name; add Path.resolve() boundary check to reject destinations outside cwd Fix hardcoded secret and plaintext HTTP: - recaptcha_verification.py: read secret key from RECAPTCHA_SECRET_KEY env var instead of hardcoded placeholder - current_weather.py: WEATHERSTACK_URL_BASE was http://, now https:// Replace assert-based validation with proper exceptions: - xor_cipher.py: assert isinstance -> raise TypeError (12 sites) - base64_cipher.py: assert -> raise TypeError/ValueError (3 sites) assert statements are silently removed with python -O/-OO Co-authored-by: Ona <no-reply@ona.com>
1 parent 68473af commit 7cdea5f

File tree

10 files changed

+205
-95
lines changed

10 files changed

+205
-95
lines changed

ciphers/base64_cipher.py

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ def base64_decode(encoded_data: str) -> bytes:
8383
>>> base64_decode("abc")
8484
Traceback (most recent call last):
8585
...
86-
AssertionError: Incorrect padding
86+
ValueError: Incorrect padding
8787
"""
8888
# Make sure encoded_data is either a string or a bytes-like object
8989
if not isinstance(encoded_data, bytes) and not isinstance(encoded_data, str):
@@ -105,16 +105,15 @@ def base64_decode(encoded_data: str) -> bytes:
105105

106106
# Check if the encoded string contains non base64 characters
107107
if padding:
108-
assert all(char in B64_CHARSET for char in encoded_data[:-padding]), (
109-
"Invalid base64 character(s) found."
110-
)
108+
if not all(char in B64_CHARSET for char in encoded_data[:-padding]):
109+
raise ValueError("Invalid base64 character(s) found.")
111110
else:
112-
assert all(char in B64_CHARSET for char in encoded_data), (
113-
"Invalid base64 character(s) found."
114-
)
111+
if not all(char in B64_CHARSET for char in encoded_data):
112+
raise ValueError("Invalid base64 character(s) found.")
115113

116114
# Check the padding
117-
assert len(encoded_data) % 4 == 0 and padding < 3, "Incorrect padding"
115+
if not (len(encoded_data) % 4 == 0 and padding < 3):
116+
raise ValueError("Incorrect padding")
118117

119118
if padding:
120119
# Remove padding if there is one

ciphers/elgamal_key_generator.py

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import os
2-
import random
2+
import secrets
33
import sys
44

55
from . import cryptomath_module as cryptomath
@@ -8,18 +8,29 @@
88
min_primitive_root = 3
99

1010

11-
# I have written my code naively same as definition of primitive root
12-
# however every time I run this program, memory exceeded...
13-
# so I used 4.80 Algorithm in
14-
# Handbook of Applied Cryptography(CRC Press, ISBN : 0-8493-8523-7, October 1996)
15-
# and it seems to run nicely!
11+
# Algorithm 4.80 from Handbook of Applied Cryptography
12+
# (CRC Press, ISBN: 0-8493-8523-7, October 1996).
13+
#
14+
# For a large prime p, p-1 = 2 * ((p-1)/2). A generator g of Z_p* must
15+
# satisfy g^((p-1)/q) ≢ 1 (mod p) for every prime factor q of p-1.
16+
# Because p is a safe-ish large prime here, checking q=2 (i.e. the Legendre
17+
# symbol) is the dominant filter; we also skip g=2 as a degenerate case.
1618
def primitive_root(p_val: int) -> int:
19+
"""
20+
Return a primitive root modulo the prime p_val.
21+
22+
>>> p = 23 # small prime for testing
23+
>>> g = primitive_root(p)
24+
>>> pow(g, (p - 1) // 2, p) != 1
25+
True
26+
>>> 3 <= g < p
27+
True
28+
"""
1729
print("Generating primitive root of p")
1830
while True:
19-
g = random.randrange(3, p_val)
20-
if pow(g, 2, p_val) == 1:
21-
continue
22-
if pow(g, p_val, p_val) == 1:
31+
g = secrets.randbelow(p_val - 3) + 3 # range [3, p_val-1]
32+
# g must not be a quadratic residue mod p (order would divide (p-1)/2)
33+
if pow(g, (p_val - 1) // 2, p_val) == 1:
2334
continue
2435
return g
2536

@@ -28,7 +39,7 @@ def generate_key(key_size: int) -> tuple[tuple[int, int, int, int], tuple[int, i
2839
print("Generating prime p...")
2940
p = rabin_miller.generate_large_prime(key_size) # select large prime number.
3041
e_1 = primitive_root(p) # one primitive root on modulo p.
31-
d = random.randrange(3, p) # private_key -> have to be greater than 2 for safety.
42+
d = secrets.randbelow(p - 3) + 3 # private key in [3, p-1]
3243
e_2 = cryptomath.find_mod_inverse(pow(e_1, d, p), p)
3344

3445
public_key = (key_size, e_1, e_2, p)

ciphers/onepad_cipher.py

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,24 @@
1-
import random
1+
import secrets
22

33

44
class Onepad:
55
@staticmethod
66
def encrypt(text: str) -> tuple[list[int], list[int]]:
77
"""
8-
Function to encrypt text using pseudo-random numbers
8+
Function to encrypt text using cryptographically secure random numbers.
9+
910
>>> Onepad().encrypt("")
1011
([], [])
1112
>>> Onepad().encrypt([])
1213
([], [])
13-
>>> random.seed(1)
14-
>>> Onepad().encrypt(" ")
15-
([6969], [69])
16-
>>> random.seed(1)
17-
>>> Onepad().encrypt("Hello")
18-
([9729, 114756, 4653, 31309, 10492], [69, 292, 33, 131, 61])
14+
>>> c, k = Onepad().encrypt(" ")
15+
>>> len(c) == 1 and len(k) == 1
16+
True
17+
>>> c, k = Onepad().encrypt("Hello")
18+
>>> len(c) == 5 and len(k) == 5
19+
True
20+
>>> Onepad().decrypt(c, k)
21+
'Hello'
1922
>>> Onepad().encrypt(1)
2023
Traceback (most recent call last):
2124
...
@@ -29,7 +32,7 @@ def encrypt(text: str) -> tuple[list[int], list[int]]:
2932
key = []
3033
cipher = []
3134
for i in plain:
32-
k = random.randint(1, 300)
35+
k = secrets.randbelow(300) + 1 # range [1, 300]
3336
c = (i + k) * k
3437
cipher.append(c)
3538
key.append(k)
@@ -38,7 +41,7 @@ def encrypt(text: str) -> tuple[list[int], list[int]]:
3841
@staticmethod
3942
def decrypt(cipher: list[int], key: list[int]) -> str:
4043
"""
41-
Function to decrypt text using pseudo-random numbers.
44+
Function to decrypt text using the key produced by encrypt().
4245
>>> Onepad().decrypt([], [])
4346
''
4447
>>> Onepad().decrypt([35], [])
@@ -47,7 +50,6 @@ def decrypt(cipher: list[int], key: list[int]) -> str:
4750
Traceback (most recent call last):
4851
...
4952
IndexError: list index out of range
50-
>>> random.seed(1)
5153
>>> Onepad().decrypt([9729, 114756, 4653, 31309, 10492], [69, 292, 33, 131, 61])
5254
'Hello'
5355
"""

ciphers/rabin_miller.py

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,40 @@
11
# Primality Testing with the Rabin-Miller Algorithm
22

3-
import random
3+
import secrets
44

55

66
def rabin_miller(num: int) -> bool:
7+
"""
8+
Rabin-Miller primality test using a cryptographically secure PRNG.
9+
10+
Uses 40 witness rounds (vs the original 5) to reduce the probability of
11+
a composite passing to at most 4^-40 ≈ 8.3e-25.
12+
13+
Requires num >= 5; smaller values are handled by is_prime_low_num via the
14+
low_primes list and never reach this function in normal use.
15+
16+
>>> rabin_miller(17)
17+
True
18+
>>> rabin_miller(21)
19+
False
20+
>>> rabin_miller(561) # Carmichael number, composite
21+
False
22+
>>> rabin_miller(7919) # prime
23+
True
24+
"""
25+
if num < 5:
26+
raise ValueError(f"rabin_miller requires num >= 5, got {num}")
27+
728
s = num - 1
829
t = 0
930

1031
while s % 2 == 0:
1132
s = s // 2
1233
t += 1
1334

14-
for _ in range(5):
15-
a = random.randrange(2, num - 1)
35+
for _ in range(40): # 40 rounds: false-positive probability ≤ 4^-40
36+
# Witness a must be in [2, num-2]; num >= 5 guarantees num-3 >= 2.
37+
a = secrets.randbelow(num - 3) + 2 # range [2, num-2]
1638
v = pow(a, s, num)
1739
if v != 1:
1840
i = 0
@@ -26,6 +48,16 @@ def rabin_miller(num: int) -> bool:
2648

2749

2850
def is_prime_low_num(num: int) -> bool:
51+
"""
52+
>>> is_prime_low_num(1)
53+
False
54+
>>> is_prime_low_num(2)
55+
True
56+
>>> is_prime_low_num(97)
57+
True
58+
>>> is_prime_low_num(100)
59+
False
60+
"""
2961
if num < 2:
3062
return False
3163

@@ -211,8 +243,19 @@ def is_prime_low_num(num: int) -> bool:
211243

212244

213245
def generate_large_prime(keysize: int = 1024) -> int:
246+
"""
247+
Generate a large prime using a cryptographically secure PRNG.
248+
249+
>>> p = generate_large_prime(16)
250+
>>> is_prime_low_num(p)
251+
True
252+
>>> p.bit_length() >= 15 # at least keysize-1 bits
253+
True
254+
"""
214255
while True:
215-
num = random.randrange(2 ** (keysize - 1), 2 ** (keysize))
256+
# secrets.randbits produces a CSPRNG integer; set the high bit to
257+
# guarantee the result is in [2^(keysize-1), 2^keysize - 1].
258+
num = secrets.randbits(keysize) | (1 << (keysize - 1))
216259
if is_prime_low_num(num):
217260
return num
218261

ciphers/rsa_key_generator.py

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import os
2-
import random
2+
import secrets
33
import sys
44

55
from maths.greatest_common_divisor import gcd_by_iterative
@@ -15,20 +15,27 @@ def main() -> None:
1515

1616
def generate_key(key_size: int) -> tuple[tuple[int, int], tuple[int, int]]:
1717
"""
18-
>>> random.seed(0) # for repeatability
19-
>>> public_key, private_key = generate_key(8)
20-
>>> public_key
21-
(26569, 239)
22-
>>> private_key
23-
(26569, 2855)
18+
Generate an RSA key pair of the given bit size.
19+
20+
Uses secrets.randbits (CSPRNG) for all random values so that generated
21+
keys are not predictable from observed outputs.
22+
23+
>>> public_key, private_key = generate_key(16)
24+
>>> public_key[0] == private_key[0] # same modulus n
25+
True
26+
>>> 0 < public_key[1] < public_key[0] # e < n
27+
True
28+
>>> 0 < private_key[1] < private_key[0] # d < n
29+
True
2430
"""
2531
p = rabin_miller.generate_large_prime(key_size)
2632
q = rabin_miller.generate_large_prime(key_size)
2733
n = p * q
2834

2935
# Generate e that is relatively prime to (p - 1) * (q - 1)
3036
while True:
31-
e = random.randrange(2 ** (key_size - 1), 2 ** (key_size))
37+
# Set the high bit so e is always in [2^(key_size-1), 2^key_size - 1]
38+
e = secrets.randbits(key_size) | (1 << (key_size - 1))
3239
if gcd_by_iterative(e, (p - 1) * (q - 1)) == 1:
3340
break
3441

ciphers/xor_cipher.py

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,10 @@ def encrypt(self, content: str, key: int) -> list[str]:
5555
"""
5656

5757
# precondition
58-
assert isinstance(key, int)
59-
assert isinstance(content, str)
58+
if not isinstance(key, int):
59+
raise TypeError(f"key must be an int, not {type(key).__name__!r}")
60+
if not isinstance(content, str):
61+
raise TypeError(f"content must be a str, not {type(content).__name__!r}")
6062

6163
key = key or self.__key or 1
6264

@@ -90,8 +92,10 @@ def decrypt(self, content: str, key: int) -> list[str]:
9092
"""
9193

9294
# precondition
93-
assert isinstance(key, int)
94-
assert isinstance(content, str)
95+
if not isinstance(key, int):
96+
raise TypeError(f"key must be an int, not {type(key).__name__!r}")
97+
if not isinstance(content, str):
98+
raise TypeError(f"content must be a str, not {type(content).__name__!r}")
9599

96100
key = key or self.__key or 1
97101

@@ -125,8 +129,10 @@ def encrypt_string(self, content: str, key: int = 0) -> str:
125129
"""
126130

127131
# precondition
128-
assert isinstance(key, int)
129-
assert isinstance(content, str)
132+
if not isinstance(key, int):
133+
raise TypeError(f"key must be an int, not {type(key).__name__!r}")
134+
if not isinstance(content, str):
135+
raise TypeError(f"content must be a str, not {type(content).__name__!r}")
130136

131137
key = key or self.__key or 1
132138

@@ -166,8 +172,10 @@ def decrypt_string(self, content: str, key: int = 0) -> str:
166172
"""
167173

168174
# precondition
169-
assert isinstance(key, int)
170-
assert isinstance(content, str)
175+
if not isinstance(key, int):
176+
raise TypeError(f"key must be an int, not {type(key).__name__!r}")
177+
if not isinstance(content, str):
178+
raise TypeError(f"content must be a str, not {type(content).__name__!r}")
171179

172180
key = key or self.__key or 1
173181

@@ -192,8 +200,10 @@ def encrypt_file(self, file: str, key: int = 0) -> bool:
192200
"""
193201

194202
# precondition
195-
assert isinstance(file, str)
196-
assert isinstance(key, int)
203+
if not isinstance(file, str):
204+
raise TypeError(f"file must be a str, not {type(file).__name__!r}")
205+
if not isinstance(key, int):
206+
raise TypeError(f"key must be an int, not {type(key).__name__!r}")
197207

198208
# make sure key is an appropriate size
199209
key %= 256
@@ -219,8 +229,10 @@ def decrypt_file(self, file: str, key: int) -> bool:
219229
"""
220230

221231
# precondition
222-
assert isinstance(file, str)
223-
assert isinstance(key, int)
232+
if not isinstance(file, str):
233+
raise TypeError(f"file must be a str, not {type(file).__name__!r}")
234+
if not isinstance(key, int):
235+
raise TypeError(f"key must be an int, not {type(key).__name__!r}")
224236

225237
# make sure key is an appropriate size
226238
key %= 256

0 commit comments

Comments
 (0)