TKK_E32232028/.venv/lib/python3.10/site-packages/lightphe/cryptosystems/GoldwasserMicali.py

227 lines
7.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# built-in dependencies
import random
from typing import Optional, List
import math
# 3rd party dependencies
import sympy
from sympy import jacobi_symbol
from tqdm import tqdm
# project dependencies
from lightphe.models.Homomorphic import Homomorphic
from lightphe.commons.logger import Logger
logger = Logger(module="lightphe/cryptosystems/GoldwasserMicali.py")
# pylint:disable=consider-using-enumerate
class GoldwasserMicali(Homomorphic):
"""
Goldwasser-Micali algorithm is homomorphic with respect to the Exclusively OR (XOR).
Ref: sefiks.com/2023/10/27/a-step-by-step-partially-homomorphic-encryption-example-with-goldwasser-micali-in-python/
"""
REQUIRED_KEYS = {
"public_key": ["n", "x"],
"private_key": ["p", "q"],
}
def __init__(
self,
keys: Optional[dict] = None,
key_size: Optional[int] = None,
max_tries: int = 10000,
):
"""
Args:
keys (dict): private - public key pair.
set this to None if you want to generate random keys.
key_size (int): key size in bits
max_tries (int): maximum attempts to generate keys
"""
self.keys = keys or self.generate_keys(
key_size=key_size or 1024, max_tries=max_tries
)
self.ciphertext_modulo = self.keys["public_key"]["n"]
# Plaintext can be any integer (even larger than n) because it is internally
# encrypted bit by bit. Internally, each bit uses modulo 2, but LightPHE expects
# integers, not bits. The original integer can be fully restored after decryption
# even if it is larger than n.
self.plaintext_modulo = self.keys["public_key"]["n"]
def generate_keys(self, key_size: int, max_tries: int = 10000) -> dict:
"""
Generate public and private keys of Goldwasser-Micali cryptosystem
Args:
key_size (int): key size in bits
max_tries (int): maximum number of tries to generate keys
Returns:
keys (dict): having private_key and public_key keys
"""
for attempt in tqdm(range(max_tries), disable=True):
# pick large random primes p and q
p = sympy.randprime(2 ** (key_size // 2 - 100), 2 ** (key_size // 2) - 1)
q = sympy.randprime(2 ** (key_size // 2 - 100), 2 ** (key_size // 2) - 1)
# to prevent factorizatin attacks, it is recommended that n should be
# several hundred bits or more
n = p * q
# find quadratic non-residue x
for _ in range(1000): # try max 1000 random x
x = random.randint(2, n - 1)
if math.gcd(x, n) != 1:
continue
if jacobi_symbol(x, p) == -1 and jacobi_symbol(x, q) == -1:
break
else:
# # If no suitable x is found after 1000 tries, discard the current pq pair and retry
continue
keys = {
"public_key": {"n": n, "x": x},
"private_key": {"p": p, "q": q},
}
logger.debug(
f"Goldwasser-Micali keys generated after {attempt+1} attempts, n bits: {n.bit_length()}"
)
return keys
raise RuntimeError(
f"Failed to generate Goldwasser-Micali keys after {max_tries} attempts."
"Please try to rerun."
)
def generate_random_key(self) -> int:
"""
Goldwasser-Micali requires to generate one-time random key that co-prime to n
Returns:
random key (int): one time random key for encryption
"""
n = self.keys["public_key"]["n"]
while True:
r = random.randint(1, n)
if math.gcd(r, n) == 1:
break
return r
def encrypt(self, plaintext: int, random_key: Optional[int] = None) -> List[int]:
"""
Encrypt a given plaintext for optionally given random key with Goldwasser-Micali
Args:
plaintext (int): message to encrypt
random_key (int): Goldwasser-Micali requires a random key
Random key will be generated automatically if you do not set this.
Returns:
ciphertext (int): encrypted message
"""
n = self.keys["public_key"]["n"]
x = self.keys["public_key"]["x"]
m_binary = bin(plaintext)[2:]
# number of bits
k = len(m_binary)
if random_key and len(random_key) != k:
raise ValueError(f"Random key must be length of {k}")
c = []
for i in range(0, k):
mi = int(m_binary[i])
if random_key:
ri = random_key[i]
else:
ri = self.generate_random_key()
ci = (pow(ri, 2, n) * pow(x, mi, n)) % n
c.append(ci)
return c
def decrypt(self, ciphertext: List[int]) -> int:
"""
Decrypt a given ciphertext with Goldwasser-Micali
Args:
ciphertext (int): encrypted message
Returns:
plaintext (int): restored message
"""
m_binaries = []
p = self.keys["private_key"]["p"]
q = self.keys["private_key"]["q"]
for i in ciphertext:
xp = i % p
xq = i % q
# reaches int limit for 3072-bit key
# if pow(xp, int((p - 1) / 2), p) == 1 and pow(xq, int((q - 1) / 2), q) == 1:
if (
pow(xp, int((p - 1) // 2), p) == 1
and pow(xq, int((q - 1) // 2), q) == 1
):
m_binaries.append("0")
else:
m_binaries.append("1")
m_binary = "".join(m_binaries)
return int(m_binary, 2)
def xor(self, ciphertext1: List[int], ciphertext2: List[int]) -> List[int]:
"""
Perform homomorphic xor on encrypted data.
Result of this must be equal to E(m1 ^ m2) = E(m1) ^ E(m2)
Encryption calculations are done in module n
Args:
ciphertext1 (list of int): 1st ciphertext created with Goldwasser-Micali
ciphertext2 (list of int): 2nd ciphertext created with Goldwasser-Micali
Returns:
ciphertext3 (list of int): 3rd ciphertext created with Goldwasser-Micali
"""
if len(ciphertext1) > len(ciphertext2):
ciphertext2 = [self.encrypt(0)[0]] * (
len(ciphertext1) - len(ciphertext2)
) + ciphertext2
elif len(ciphertext2) > len(ciphertext1):
ciphertext1 = [self.encrypt(0)[0]] * (
len(ciphertext2) - len(ciphertext1)
) + ciphertext1
assert len(ciphertext1) == len(ciphertext2)
ciphertext3 = []
for i in range(0, len(ciphertext1)):
c1 = ciphertext1[i]
c2 = ciphertext2[i]
ciphertext3.append((c1 * c2) % self.ciphertext_modulo)
return ciphertext3
def reencrypt(self, ciphertext: List[int]) -> List[int]:
"""
Re-encrypt a given ciphertext with Goldwasser-Micali
Args:
ciphertext (int): encrypted message
Returns:
ciphertext (int): re-encrypted message
"""
n = self.keys["public_key"]["n"]
c_reencrypted = []
for ci in ciphertext:
ri = self.generate_random_key()
ci_reencrypted = (pow(ri, 2, n) * ci) % n
c_reencrypted.append(ci_reencrypted)
return c_reencrypted