#!/usr/bin/env python3

from Cryptotools.Numbers.coprime import phi
from Cryptotools.Numbers.carmi import carmichael_lambda
from Cryptotools.Utils.utils import gcd
from Cryptotools.Numbers.primeNumber import getPrimeNumber
from sys import getsizeof

class RSAKey:
    """
        This class store the RSA key with the modulus associated
        The key is a tuple of the key and the modulus n

        Attributes:
            key: It's the exponent key, can be public or private
            modulus: It's the public modulus
            length: It's the key length
    """
    def __init__(self, key, modulus, length):
        """
            Contain the RSA Key. An object of RSAKey can be a public key or a private key

            Args:
                key (Integer): it's the exponent of the key
                modulus (Integer): it's the public modulus of the key
                length (Integer): length of the key
        """

        self._key     = key
        self._modulus = modulus
        self._length  = length

    @property
    def key(self):
        return self._key

    @property
    def modulus(self):
        return self._modulus

    @property
    def length(self):
        return self.length

class RSA:
    """
        This class generate public key based on RSA algorithm

        Attributes:
            p: it's the prime number for generating the modulus
            q: it's the second prime number for the modulus
            public: Object of the RSAKey which is the public key
            private: Object of the RSAKey for the private key
    """

    def __init__(self):
        """
            Build a RSA Key
        """
        self._p = None
        self._q = None
        self._public = None
        self._private = None

    def generateKeys(self, size=512):
        """
            This function generate both public and private keys
            Args:
                size: It's the size of the key and must be multiple of 64
        """
        # p and q must be coprime
        self._p = getPrimeNumber(size)
        self._q = getPrimeNumber(size)

        # compute n = pq
        n = self._p * self._q

        phin = (self._p - 1) * (self._q - 1)

        # e must be coprime with phi(n)
        # According to the FIPS 186-5, the public key exponent must be odd
        # and the minimum size is 65536 (Cf. Section 5.4 PKCS #1)
        # https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.186-5.pdf
        e = 65535 # Works
        for i in range(2, phin - 1):
            if gcd(phin, e) == 1:
                break
            e += 1
        # print(gcd(phin, e))
        self._public = RSAKey(e, n, getsizeof(e))

        # d is the reverse modulo of phi(n)
        # d = self._inverseModular(e, phin)
        d = pow(e, -1, phin) # Works in python 3.8
        self._private = RSAKey(d, n, getsizeof(d))

    @property
    def e(self):
        return self._public.key

    @property
    def d(self):
        return self._private.key

    @property
    def n(self):
        return self._public.modulus

    @property
    def p(self):
        return self._p

    @property
    def q(self):
        return self._q

    def encrypt(self, data) -> list:
        """
            This function return a list of data encrypted with the public key

            Args:
                data (str): it's the plaintext which need to be encrypted

            Returns:
                return a list of the data encrypted, each entries contains the value encoded
        """
        dataEncoded = self._str2bin(data)
        return list(
            pow(int(x), self._public.key, self._public.modulus)
            for x in dataEncoded
        )

    def decrypt(self, data) -> list:
        """
            This function return a list decrypted with the private key

            Args:
                data (str): It's the encrypted data which need to be decrypted

            Returns:
                Return the list of data uncrypted into plaintext
        """
        decrypted = list()
        for x in data:
            d = pow(x, self._private.key, self._private.modulus)
            decrypted.append(chr(d))
        return ''.join(decrypted)

    def _str2bin(self, data) -> list:
        """
            This function convert a string into the unicode value

            Args:
                data: the string which need to be converted

            Returns:
                Return a list of unicode values of data
        """
        return list(ord(x) for x in data)

    def _inverseModular(self, a, n):
        """
            This function compute the modular inverse for finding d, the decryption key

            Args:
                a (Integer): the base of the exponent
                n (Integer): the modulus
        """
        for b in range(1, n):
            #if pow(a, b, n) == 1:
            if (a * b) % n == 1:
                inv = b
                break
        return inv
