Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 7 additions & 8 deletions ciphers/base64_cipher.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@
>>> 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):
Expand All @@ -105,16 +105,15 @@

# 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):

Check failure on line 111 in ciphers/base64_cipher.py

View workflow job for this annotation

GitHub Actions / ruff

ruff (PLR5501)

ciphers/base64_cipher.py:110:5: PLR5501 Use `elif` instead of `else` then `if`, to reduce indentation help: Convert to `elif`
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
Expand Down
33 changes: 22 additions & 11 deletions ciphers/elgamal_key_generator.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import os
import random
import secrets
import sys

from . import cryptomath_module as cryptomath
Expand All @@ -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

Expand All @@ -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)
Expand Down
24 changes: 13 additions & 11 deletions ciphers/onepad_cipher.py
Original file line number Diff line number Diff line change
@@ -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):
...
Expand All @@ -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)
Expand All @@ -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], [])
Expand All @@ -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'
"""
Expand Down
51 changes: 47 additions & 4 deletions ciphers/rabin_miller.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,40 @@
# 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}")

Check failure on line 26 in ciphers/rabin_miller.py

View workflow job for this annotation

GitHub Actions / ruff

ruff (EM102)

ciphers/rabin_miller.py:26:26: EM102 Exception must not use an f-string literal, assign to variable first help: Assign to variable; remove f-string literal

s = num - 1
t = 0

while s % 2 == 0:
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
Expand All @@ -26,6 +48,16 @@


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

Expand Down Expand Up @@ -211,8 +243,19 @@


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

Expand Down
23 changes: 15 additions & 8 deletions ciphers/rsa_key_generator.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import os
import random
import secrets
import sys

from maths.greatest_common_divisor import gcd_by_iterative
Expand All @@ -15,20 +15,27 @@ 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)
n = p * q

# 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

Expand Down
36 changes: 24 additions & 12 deletions ciphers/xor_cipher.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,10 @@
"""

# 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}")

Check failure on line 59 in ciphers/xor_cipher.py

View workflow job for this annotation

GitHub Actions / ruff

ruff (EM102)

ciphers/xor_cipher.py:59:29: EM102 Exception must not use an f-string literal, assign to variable first help: Assign to variable; remove f-string literal
if not isinstance(content, str):
raise TypeError(f"content must be a str, not {type(content).__name__!r}")

Check failure on line 61 in ciphers/xor_cipher.py

View workflow job for this annotation

GitHub Actions / ruff

ruff (EM102)

ciphers/xor_cipher.py:61:29: EM102 Exception must not use an f-string literal, assign to variable first help: Assign to variable; remove f-string literal

key = key or self.__key or 1

Expand Down Expand Up @@ -90,8 +92,10 @@
"""

# 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}")

Check failure on line 96 in ciphers/xor_cipher.py

View workflow job for this annotation

GitHub Actions / ruff

ruff (EM102)

ciphers/xor_cipher.py:96:29: EM102 Exception must not use an f-string literal, assign to variable first help: Assign to variable; remove f-string literal
if not isinstance(content, str):
raise TypeError(f"content must be a str, not {type(content).__name__!r}")

Check failure on line 98 in ciphers/xor_cipher.py

View workflow job for this annotation

GitHub Actions / ruff

ruff (EM102)

ciphers/xor_cipher.py:98:29: EM102 Exception must not use an f-string literal, assign to variable first help: Assign to variable; remove f-string literal

key = key or self.__key or 1

Expand Down Expand Up @@ -125,8 +129,10 @@
"""

# 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}")

Check failure on line 133 in ciphers/xor_cipher.py

View workflow job for this annotation

GitHub Actions / ruff

ruff (EM102)

ciphers/xor_cipher.py:133:29: EM102 Exception must not use an f-string literal, assign to variable first help: Assign to variable; remove f-string literal
if not isinstance(content, str):
raise TypeError(f"content must be a str, not {type(content).__name__!r}")

Check failure on line 135 in ciphers/xor_cipher.py

View workflow job for this annotation

GitHub Actions / ruff

ruff (EM102)

ciphers/xor_cipher.py:135:29: EM102 Exception must not use an f-string literal, assign to variable first help: Assign to variable; remove f-string literal

key = key or self.__key or 1

Expand Down Expand Up @@ -166,8 +172,10 @@
"""

# 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}")

Check failure on line 176 in ciphers/xor_cipher.py

View workflow job for this annotation

GitHub Actions / ruff

ruff (EM102)

ciphers/xor_cipher.py:176:29: EM102 Exception must not use an f-string literal, assign to variable first help: Assign to variable; remove f-string literal
if not isinstance(content, str):
raise TypeError(f"content must be a str, not {type(content).__name__!r}")

Check failure on line 178 in ciphers/xor_cipher.py

View workflow job for this annotation

GitHub Actions / ruff

ruff (EM102)

ciphers/xor_cipher.py:178:29: EM102 Exception must not use an f-string literal, assign to variable first help: Assign to variable; remove f-string literal

key = key or self.__key or 1

Expand All @@ -192,8 +200,10 @@
"""

# 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
Expand All @@ -219,8 +229,10 @@
"""

# 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
Expand Down
Loading
Loading