diff --git a/ciphers/base64_cipher.py b/ciphers/base64_cipher.py index 038d13963d95..ddea5e10fd0d 100644 --- a/ciphers/base64_cipher.py +++ b/ciphers/base64_cipher.py @@ -83,7 +83,7 @@ def base64_decode(encoded_data: str) -> bytes: >>> base64_decode("abc") Traceback (most recent call last): ... - AssertionError: Incorrect padding + ValueError: Incorrect padding """ # Make sure encoded_data is either a string or a bytes-like object if not isinstance(encoded_data, bytes) and not isinstance(encoded_data, str): @@ -105,16 +105,15 @@ def base64_decode(encoded_data: str) -> bytes: # Check if the encoded string contains non base64 characters if padding: - assert all(char in B64_CHARSET for char in encoded_data[:-padding]), ( - "Invalid base64 character(s) found." - ) + if not all(char in B64_CHARSET for char in encoded_data[:-padding]): + raise ValueError("Invalid base64 character(s) found.") else: - assert all(char in B64_CHARSET for char in encoded_data), ( - "Invalid base64 character(s) found." - ) + if not all(char in B64_CHARSET for char in encoded_data): + raise ValueError("Invalid base64 character(s) found.") # Check the padding - assert len(encoded_data) % 4 == 0 and padding < 3, "Incorrect padding" + if not (len(encoded_data) % 4 == 0 and padding < 3): + raise ValueError("Incorrect padding") if padding: # Remove padding if there is one diff --git a/ciphers/elgamal_key_generator.py b/ciphers/elgamal_key_generator.py index 17ba55c0d013..b8bb60e3881c 100644 --- a/ciphers/elgamal_key_generator.py +++ b/ciphers/elgamal_key_generator.py @@ -1,5 +1,5 @@ import os -import random +import secrets import sys from . import cryptomath_module as cryptomath @@ -8,18 +8,29 @@ min_primitive_root = 3 -# I have written my code naively same as definition of primitive root -# however every time I run this program, memory exceeded... -# so I used 4.80 Algorithm in -# Handbook of Applied Cryptography(CRC Press, ISBN : 0-8493-8523-7, October 1996) -# and it seems to run nicely! +# Algorithm 4.80 from Handbook of Applied Cryptography +# (CRC Press, ISBN: 0-8493-8523-7, October 1996). +# +# For a large prime p, p-1 = 2 * ((p-1)/2). A generator g of Z_p* must +# satisfy g^((p-1)/q) ≢ 1 (mod p) for every prime factor q of p-1. +# Because p is a safe-ish large prime here, checking q=2 (i.e. the Legendre +# symbol) is the dominant filter; we also skip g=2 as a degenerate case. def primitive_root(p_val: int) -> int: + """ + Return a primitive root modulo the prime p_val. + + >>> p = 23 # small prime for testing + >>> g = primitive_root(p) + >>> pow(g, (p - 1) // 2, p) != 1 + True + >>> 3 <= g < p + True + """ print("Generating primitive root of p") while True: - g = random.randrange(3, p_val) - if pow(g, 2, p_val) == 1: - continue - if pow(g, p_val, p_val) == 1: + g = secrets.randbelow(p_val - 3) + 3 # range [3, p_val-1] + # g must not be a quadratic residue mod p (order would divide (p-1)/2) + if pow(g, (p_val - 1) // 2, p_val) == 1: continue return g @@ -28,7 +39,7 @@ def generate_key(key_size: int) -> tuple[tuple[int, int, int, int], tuple[int, i print("Generating prime p...") p = rabin_miller.generate_large_prime(key_size) # select large prime number. e_1 = primitive_root(p) # one primitive root on modulo p. - d = random.randrange(3, p) # private_key -> have to be greater than 2 for safety. + d = secrets.randbelow(p - 3) + 3 # private key in [3, p-1] e_2 = cryptomath.find_mod_inverse(pow(e_1, d, p), p) public_key = (key_size, e_1, e_2, p) diff --git a/ciphers/onepad_cipher.py b/ciphers/onepad_cipher.py index c4fb22e14a06..9081a5b03d24 100644 --- a/ciphers/onepad_cipher.py +++ b/ciphers/onepad_cipher.py @@ -1,21 +1,24 @@ -import random +import secrets class Onepad: @staticmethod def encrypt(text: str) -> tuple[list[int], list[int]]: """ - Function to encrypt text using pseudo-random numbers + Function to encrypt text using cryptographically secure random numbers. + >>> Onepad().encrypt("") ([], []) >>> Onepad().encrypt([]) ([], []) - >>> random.seed(1) - >>> Onepad().encrypt(" ") - ([6969], [69]) - >>> random.seed(1) - >>> Onepad().encrypt("Hello") - ([9729, 114756, 4653, 31309, 10492], [69, 292, 33, 131, 61]) + >>> c, k = Onepad().encrypt(" ") + >>> len(c) == 1 and len(k) == 1 + True + >>> c, k = Onepad().encrypt("Hello") + >>> len(c) == 5 and len(k) == 5 + True + >>> Onepad().decrypt(c, k) + 'Hello' >>> Onepad().encrypt(1) Traceback (most recent call last): ... @@ -29,7 +32,7 @@ def encrypt(text: str) -> tuple[list[int], list[int]]: key = [] cipher = [] for i in plain: - k = random.randint(1, 300) + k = secrets.randbelow(300) + 1 # range [1, 300] c = (i + k) * k cipher.append(c) key.append(k) @@ -38,7 +41,7 @@ def encrypt(text: str) -> tuple[list[int], list[int]]: @staticmethod def decrypt(cipher: list[int], key: list[int]) -> str: """ - Function to decrypt text using pseudo-random numbers. + Function to decrypt text using the key produced by encrypt(). >>> Onepad().decrypt([], []) '' >>> Onepad().decrypt([35], []) @@ -47,7 +50,6 @@ def decrypt(cipher: list[int], key: list[int]) -> str: Traceback (most recent call last): ... IndexError: list index out of range - >>> random.seed(1) >>> Onepad().decrypt([9729, 114756, 4653, 31309, 10492], [69, 292, 33, 131, 61]) 'Hello' """ diff --git a/ciphers/rabin_miller.py b/ciphers/rabin_miller.py index 410d559d4315..07f3d65d25f9 100644 --- a/ciphers/rabin_miller.py +++ b/ciphers/rabin_miller.py @@ -1,9 +1,30 @@ # Primality Testing with the Rabin-Miller Algorithm -import random +import secrets def rabin_miller(num: int) -> bool: + """ + Rabin-Miller primality test using a cryptographically secure PRNG. + + Uses 40 witness rounds (vs the original 5) to reduce the probability of + a composite passing to at most 4^-40 ≈ 8.3e-25. + + Requires num >= 5; smaller values are handled by is_prime_low_num via the + low_primes list and never reach this function in normal use. + + >>> rabin_miller(17) + True + >>> rabin_miller(21) + False + >>> rabin_miller(561) # Carmichael number, composite + False + >>> rabin_miller(7919) # prime + True + """ + if num < 5: + raise ValueError(f"rabin_miller requires num >= 5, got {num}") + s = num - 1 t = 0 @@ -11,8 +32,9 @@ def rabin_miller(num: int) -> bool: s = s // 2 t += 1 - for _ in range(5): - a = random.randrange(2, num - 1) + for _ in range(40): # 40 rounds: false-positive probability ≤ 4^-40 + # Witness a must be in [2, num-2]; num >= 5 guarantees num-3 >= 2. + a = secrets.randbelow(num - 3) + 2 # range [2, num-2] v = pow(a, s, num) if v != 1: i = 0 @@ -26,6 +48,16 @@ def rabin_miller(num: int) -> bool: def is_prime_low_num(num: int) -> bool: + """ + >>> is_prime_low_num(1) + False + >>> is_prime_low_num(2) + True + >>> is_prime_low_num(97) + True + >>> is_prime_low_num(100) + False + """ if num < 2: return False @@ -211,8 +243,19 @@ def is_prime_low_num(num: int) -> bool: def generate_large_prime(keysize: int = 1024) -> int: + """ + Generate a large prime using a cryptographically secure PRNG. + + >>> p = generate_large_prime(16) + >>> is_prime_low_num(p) + True + >>> p.bit_length() >= 15 # at least keysize-1 bits + True + """ while True: - num = random.randrange(2 ** (keysize - 1), 2 ** (keysize)) + # secrets.randbits produces a CSPRNG integer; set the high bit to + # guarantee the result is in [2^(keysize-1), 2^keysize - 1]. + num = secrets.randbits(keysize) | (1 << (keysize - 1)) if is_prime_low_num(num): return num diff --git a/ciphers/rsa_key_generator.py b/ciphers/rsa_key_generator.py index 44970e8cbc15..d6954a799ffb 100644 --- a/ciphers/rsa_key_generator.py +++ b/ciphers/rsa_key_generator.py @@ -1,5 +1,5 @@ import os -import random +import secrets import sys from maths.greatest_common_divisor import gcd_by_iterative @@ -15,12 +15,18 @@ def main() -> None: def generate_key(key_size: int) -> tuple[tuple[int, int], tuple[int, int]]: """ - >>> random.seed(0) # for repeatability - >>> public_key, private_key = generate_key(8) - >>> public_key - (26569, 239) - >>> private_key - (26569, 2855) + Generate an RSA key pair of the given bit size. + + Uses secrets.randbits (CSPRNG) for all random values so that generated + keys are not predictable from observed outputs. + + >>> public_key, private_key = generate_key(16) + >>> public_key[0] == private_key[0] # same modulus n + True + >>> 0 < public_key[1] < public_key[0] # e < n + True + >>> 0 < private_key[1] < private_key[0] # d < n + True """ p = rabin_miller.generate_large_prime(key_size) q = rabin_miller.generate_large_prime(key_size) @@ -28,7 +34,8 @@ def generate_key(key_size: int) -> tuple[tuple[int, int], tuple[int, int]]: # Generate e that is relatively prime to (p - 1) * (q - 1) while True: - e = random.randrange(2 ** (key_size - 1), 2 ** (key_size)) + # Set the high bit so e is always in [2^(key_size-1), 2^key_size - 1] + e = secrets.randbits(key_size) | (1 << (key_size - 1)) if gcd_by_iterative(e, (p - 1) * (q - 1)) == 1: break diff --git a/ciphers/xor_cipher.py b/ciphers/xor_cipher.py index 24d88a0fd588..ed3ff38a621e 100644 --- a/ciphers/xor_cipher.py +++ b/ciphers/xor_cipher.py @@ -55,8 +55,10 @@ def encrypt(self, content: str, key: int) -> list[str]: """ # precondition - assert isinstance(key, int) - assert isinstance(content, str) + if not isinstance(key, int): + raise TypeError(f"key must be an int, not {type(key).__name__!r}") + if not isinstance(content, str): + raise TypeError(f"content must be a str, not {type(content).__name__!r}") key = key or self.__key or 1 @@ -90,8 +92,10 @@ def decrypt(self, content: str, key: int) -> list[str]: """ # precondition - assert isinstance(key, int) - assert isinstance(content, str) + if not isinstance(key, int): + raise TypeError(f"key must be an int, not {type(key).__name__!r}") + if not isinstance(content, str): + raise TypeError(f"content must be a str, not {type(content).__name__!r}") key = key or self.__key or 1 @@ -125,8 +129,10 @@ def encrypt_string(self, content: str, key: int = 0) -> str: """ # precondition - assert isinstance(key, int) - assert isinstance(content, str) + if not isinstance(key, int): + raise TypeError(f"key must be an int, not {type(key).__name__!r}") + if not isinstance(content, str): + raise TypeError(f"content must be a str, not {type(content).__name__!r}") key = key or self.__key or 1 @@ -166,8 +172,10 @@ def decrypt_string(self, content: str, key: int = 0) -> str: """ # precondition - assert isinstance(key, int) - assert isinstance(content, str) + if not isinstance(key, int): + raise TypeError(f"key must be an int, not {type(key).__name__!r}") + if not isinstance(content, str): + raise TypeError(f"content must be a str, not {type(content).__name__!r}") key = key or self.__key or 1 @@ -192,8 +200,10 @@ def encrypt_file(self, file: str, key: int = 0) -> bool: """ # precondition - assert isinstance(file, str) - assert isinstance(key, int) + if not isinstance(file, str): + raise TypeError(f"file must be a str, not {type(file).__name__!r}") + if not isinstance(key, int): + raise TypeError(f"key must be an int, not {type(key).__name__!r}") # make sure key is an appropriate size key %= 256 @@ -219,8 +229,10 @@ def decrypt_file(self, file: str, key: int) -> bool: """ # precondition - assert isinstance(file, str) - assert isinstance(key, int) + if not isinstance(file, str): + raise TypeError(f"file must be a str, not {type(file).__name__!r}") + if not isinstance(key, int): + raise TypeError(f"key must be an int, not {type(key).__name__!r}") # make sure key is an appropriate size key %= 256 diff --git a/neural_network/convolution_neural_network.py b/neural_network/convolution_neural_network.py index 6b1aa50c7981..7f7104f407fa 100644 --- a/neural_network/convolution_neural_network.py +++ b/neural_network/convolution_neural_network.py @@ -14,7 +14,7 @@ - - - - - -- - - - - - - - - - - - - - - - - - - - - - - """ -import pickle +import json import numpy as np from matplotlib import pyplot as plt @@ -52,9 +52,21 @@ def __init__( self.thre_bp2 = -2 * rng.random(self.num_bp2) + 1 self.thre_bp3 = -2 * rng.random(self.num_bp3) + 1 - def save_model(self, save_path): - # save model dict with pickle - model_dic = { + def save_model(self, save_path: str) -> None: + """ + Save the model to *save_path* using two safe, non-executable formats: + - ``/config.json`` — hyperparameters (human-readable) + - ``/weights.npz`` — weight arrays (numpy binary) + + This replaces the previous pickle-based format. Pickle files execute + arbitrary Python on load and must never be loaded from untrusted sources. + """ + import os + + os.makedirs(save_path, exist_ok=True) + + # Hyperparameters — plain JSON, no code execution risk. + config = { "num_bp1": self.num_bp1, "num_bp2": self.num_bp2, "num_bp3": self.num_bp3, @@ -63,41 +75,55 @@ def save_model(self, save_path): "size_pooling1": self.size_pooling1, "rate_weight": self.rate_weight, "rate_thre": self.rate_thre, - "w_conv1": self.w_conv1, - "wkj": self.wkj, - "vji": self.vji, - "thre_conv1": self.thre_conv1, - "thre_bp2": self.thre_bp2, - "thre_bp3": self.thre_bp3, } - with open(save_path, "wb") as f: - pickle.dump(model_dic, f) + with open(f"{save_path}/config.json", "w") as f: + json.dump(config, f) + + # Weight arrays — numpy's own safe binary format. + np.savez( + f"{save_path}/weights.npz", + **{f"w_conv1_{i}": np.asarray(w) for i, w in enumerate(self.w_conv1)}, + wkj=np.asarray(self.wkj), + vji=np.asarray(self.vji), + thre_conv1=np.asarray(self.thre_conv1), + thre_bp2=np.asarray(self.thre_bp2), + thre_bp3=np.asarray(self.thre_bp3), + ) print(f"Model saved: {save_path}") @classmethod - def read_model(cls, model_path): - # read saved model - with open(model_path, "rb") as f: - model_dic = pickle.load(f) # noqa: S301 - - conv_get = model_dic.get("conv1") - conv_get.append(model_dic.get("step_conv1")) - size_p1 = model_dic.get("size_pooling1") - bp1 = model_dic.get("num_bp1") - bp2 = model_dic.get("num_bp2") - bp3 = model_dic.get("num_bp3") - r_w = model_dic.get("rate_weight") - r_t = model_dic.get("rate_thre") - # create model instance - conv_ins = CNN(conv_get, size_p1, bp1, bp2, bp3, r_w, r_t) - # modify model parameter - conv_ins.w_conv1 = model_dic.get("w_conv1") - conv_ins.wkj = model_dic.get("wkj") - conv_ins.vji = model_dic.get("vji") - conv_ins.thre_conv1 = model_dic.get("thre_conv1") - conv_ins.thre_bp2 = model_dic.get("thre_bp2") - conv_ins.thre_bp3 = model_dic.get("thre_bp3") + def read_model(cls, model_path: str) -> "CNN": + """ + Load a model previously saved with :meth:`save_model`. + + Reads ``/config.json`` and ``/weights.npz``. + Unlike pickle, neither format can execute arbitrary code on load. + """ + with open(f"{model_path}/config.json") as f: + config = json.load(f) + + conv_get = config["conv1"] + [config["step_conv1"]] + conv_ins = cls( + conv_get, + config["size_pooling1"], + config["num_bp1"], + config["num_bp2"], + config["num_bp3"], + config["rate_weight"], + config["rate_thre"], + ) + + weights = np.load(f"{model_path}/weights.npz") + num_kernels = conv_ins.conv1[1] + conv_ins.w_conv1 = [ + np.asmatrix(weights[f"w_conv1_{i}"]) for i in range(num_kernels) + ] + conv_ins.wkj = np.asmatrix(weights["wkj"]) + conv_ins.vji = np.asmatrix(weights["vji"]) + conv_ins.thre_conv1 = weights["thre_conv1"] + conv_ins.thre_bp2 = weights["thre_bp2"] + conv_ins.thre_bp3 = weights["thre_bp3"] return conv_ins def sig(self, x): diff --git a/web_programming/current_weather.py b/web_programming/current_weather.py index 001eaf9020f4..0fd4e8eac344 100644 --- a/web_programming/current_weather.py +++ b/web_programming/current_weather.py @@ -13,7 +13,7 @@ # Define the URL for the APIs with placeholders OPENWEATHERMAP_URL_BASE = "https://api.openweathermap.org/data/2.5/weather" -WEATHERSTACK_URL_BASE = "http://api.weatherstack.com/current" +WEATHERSTACK_URL_BASE = "https://api.weatherstack.com/current" def current_weather(location: str) -> list[dict]: diff --git a/web_programming/download_images_from_google_query.py b/web_programming/download_images_from_google_query.py index 659cf6a398a3..c2023b71f34b 100644 --- a/web_programming/download_images_from_google_query.py +++ b/web_programming/download_images_from_google_query.py @@ -11,6 +11,7 @@ import re import sys import urllib.request +from pathlib import Path import httpx from bs4 import BeautifulSoup @@ -93,11 +94,18 @@ def download_images_from_google_query(query: str = "dhaka", max_images: int = 5) ) ] urllib.request.install_opener(opener) - path_name = f"query_{query.replace(' ', '_')}" - if not os.path.exists(path_name): - os.makedirs(path_name) + # Sanitise the query so it cannot escape the output directory. + # Replace any character that is not alphanumeric, space, hyphen, or + # underscore with an underscore, then collapse spaces. + safe_query = re.sub(r"[^\w\s-]", "_", query).replace(" ", "_") + output_dir = Path.cwd() / f"query_{safe_query}" + output_dir.mkdir(parents=True, exist_ok=True) + # Resolve to an absolute path and verify it stays inside cwd. + dest = (output_dir / f"original_size_img_{index}.jpg").resolve() + if not str(dest).startswith(str(Path.cwd().resolve())): + raise ValueError(f"Refusing to write outside working directory: {dest}") urllib.request.urlretrieve( # noqa: S310 - original_size_img, f"{path_name}/original_size_img_{index}.jpg" + original_size_img, dest ) return index diff --git a/web_programming/recaptcha_verification.py b/web_programming/recaptcha_verification.py index c3a53da0b3da..23c458c0fd54 100644 --- a/web_programming/recaptcha_verification.py +++ b/web_programming/recaptcha_verification.py @@ -39,6 +39,8 @@ # ] # /// +import os + import httpx try: @@ -49,8 +51,8 @@ def login_using_recaptcha(request): - # Enter your recaptcha secret key here - secret_key = "secretKey" # noqa: S105 + # Read the recaptcha secret key from the environment; never hard-code it. + secret_key = os.environ.get("RECAPTCHA_SECRET_KEY", "") url = "https://www.google.com/recaptcha/api/siteverify" # when method is not POST, direct user to login page