153 lines
5.3 KiB
Python
153 lines
5.3 KiB
Python
import random
|
|
import math
|
|
from typing import Optional
|
|
import sympy
|
|
from lightphe.models.Homomorphic import Homomorphic
|
|
from lightphe.commons.logger import Logger
|
|
|
|
logger = Logger(module="lightphe/cryptosystems/RSA.py")
|
|
|
|
|
|
class RSA(Homomorphic):
|
|
"""
|
|
RSA algorithm is partially homomorphic with respect to the multiplication
|
|
Ref: https://sefiks.com/2023/03/06/a-step-by-step-partially-homomorphic-encryption-example-with-rsa-in-python/
|
|
"""
|
|
|
|
REQUIRED_KEYS = {
|
|
"public_key": ["n", "e"],
|
|
"private_key": ["d"],
|
|
}
|
|
|
|
def __init__(
|
|
self,
|
|
keys: Optional[dict] = None,
|
|
key_size: Optional[int] = None,
|
|
encrypt_with_public=True,
|
|
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
|
|
encrypt_with_public (boolean): RSA has two keys: private (d) and public (e).
|
|
If you encrypt a message with smo's public, then just that person can decrypt it
|
|
with his private (secure message). Otherwise, if you encrypt it with your private,
|
|
one can decrypt it with your public (digital signatures).
|
|
Set this arg to True if you want to do encryption with public key e,
|
|
and do decryption with private key d.
|
|
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.plaintext_modulo = self.keys["public_key"]["n"]
|
|
self.ciphertext_modulo = self.keys["public_key"]["n"]
|
|
self.encrypt_with_public = encrypt_with_public
|
|
|
|
def generate_keys(
|
|
self,
|
|
key_size: int,
|
|
max_tries: int = 10000,
|
|
) -> dict:
|
|
"""
|
|
Generate public and private keys of RSA 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
|
|
"""
|
|
keys = {}
|
|
keys["private_key"] = {}
|
|
keys["public_key"] = {}
|
|
|
|
for _ in range(max_tries):
|
|
# picking a prime modulus p and q
|
|
p = sympy.randprime(2 ** (key_size // 2 - 300), 2 ** (key_size // 2) - 1)
|
|
q = sympy.randprime(2 ** (key_size // 2 - 300), 2 ** (key_size // 2) - 1)
|
|
|
|
if p == q:
|
|
continue
|
|
|
|
n = p * q
|
|
phi = (p - 1) * (q - 1)
|
|
|
|
# select public exponent e
|
|
for _ in range(1000): # try max 1000 random e
|
|
e = random.randint(2, phi - 1)
|
|
if math.gcd(e, phi) == 1:
|
|
d = pow(e, -1, phi)
|
|
return {
|
|
"public_key": {"n": n, "e": e},
|
|
"private_key": {"d": d},
|
|
}
|
|
raise ValueError(f"Failed to generate RSA keys after {max_tries} tries")
|
|
|
|
def generate_random_key(self) -> int:
|
|
"""
|
|
RSA does not require random key for encryption, still return one
|
|
Returns:
|
|
random key (int): one time random key for encryption
|
|
"""
|
|
return random.randint(1, self.keys["public_key"]["n"] - 1)
|
|
|
|
def encrypt(self, plaintext: int) -> int:
|
|
"""
|
|
Encrypt plain messages with RSA
|
|
Args:
|
|
plaintext (int): plain message
|
|
Returns:
|
|
ciphertext (int): ciphertext encrypted with RSA
|
|
"""
|
|
n = self.keys["public_key"]["n"]
|
|
|
|
if plaintext > n:
|
|
plaintext = plaintext % n
|
|
logger.debug(
|
|
f"RSA can encrypt messages [1, {n}]. "
|
|
f"Seems you exceeded this limit. New plaintext is {plaintext}"
|
|
)
|
|
|
|
if self.encrypt_with_public is True:
|
|
e = self.keys["public_key"]["e"]
|
|
c = pow(plaintext, e, n)
|
|
else:
|
|
d = self.keys["private_key"]["d"]
|
|
c = pow(plaintext, d, n)
|
|
|
|
return c
|
|
|
|
def decrypt(self, ciphertext: int) -> int:
|
|
"""
|
|
Decrypt ciphertexts with RSA
|
|
Args:
|
|
ciphertext (int): encrypted message
|
|
decrypt_with_private (int): RSA has two keys: private (d) and public (e).
|
|
If you encrypt a message with smo's public, then just that person can decrypt it
|
|
with his private (secure message). Otherwise, if you encrypt it with your private,
|
|
one can decrypt it with your public (digital signatures).
|
|
Set this arg to True if you want to do encryption with public key e,
|
|
and do decryption with private key d.
|
|
Returns:
|
|
plaintext (int): restored message
|
|
"""
|
|
n = self.keys["public_key"]["n"]
|
|
if self.encrypt_with_public is True:
|
|
d = self.keys["private_key"]["d"]
|
|
p = pow(ciphertext, d, n)
|
|
else:
|
|
e = self.keys["public_key"]["e"]
|
|
p = pow(ciphertext, e, n)
|
|
|
|
return p
|
|
|
|
def multiply(self, ciphertext1: int, ciphertext2: int) -> int:
|
|
"""
|
|
Perform homomorphic multiplication on encrypted data.
|
|
Result of this must be equal to E(m1 * m2)
|
|
"""
|
|
n = self.keys["public_key"]["n"]
|
|
return (ciphertext1 * ciphertext2) % n
|