diff --git a/CHANGELOG.md b/CHANGELOG.md index d0bef58..5d380be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,14 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [v1.1.0] - 2024-04-22 + +### Added +- Added operations in Jacobian coordinates to improve calculation efficiency. + +### Changed +- Updated version to v1.1.0, making it the official stable version. + ## [v1.0.0] - 2024-04-22 ### Added diff --git a/README.md b/README.md index 00c97cb..a0d51b9 100755 --- a/README.md +++ b/README.md @@ -79,7 +79,7 @@ bob_shared_secret = bob.compute_shared_secret(alice.public_key) # alice_shared_secret should be equal to bob_shared_secret ``` -### MasseyOmura Key Exchange +### Massey-Omura Key Exchange ```python from ecutils.protocols import MasseyOmura diff --git a/src/ecutils/__init__.py b/src/ecutils/__init__.py index 5becc17..6849410 100755 --- a/src/ecutils/__init__.py +++ b/src/ecutils/__init__.py @@ -1 +1 @@ -__version__ = "1.0.0" +__version__ = "1.1.0" diff --git a/src/ecutils/algorithms.py b/src/ecutils/algorithms.py index e4fbe70..e83b1cf 100644 --- a/src/ecutils/algorithms.py +++ b/src/ecutils/algorithms.py @@ -1,12 +1,14 @@ +import multiprocessing from dataclasses import dataclass +from functools import lru_cache, partial from random import randint -from typing import Optional, Tuple +from typing import Tuple, Union from ecutils.core import EllipticCurve, Point from ecutils.curves import get as get_curve -@dataclass +@dataclass(frozen=True) class Koblitz: """A class implementing the Koblitz method for encoding and decoding messages using elliptic curves. @@ -19,89 +21,171 @@ class Koblitz: curve_name: str = "secp521r1" - def __post_init__(self) -> None: - """Initializes the internal curve representation once the class instance is created.""" - self._curve = None - @property + @lru_cache(maxsize=1024, typed=True) def curve(self) -> EllipticCurve: - """Lazy-loads and returns the elliptic curve used for encoding and decoding. + """Retrieves the elliptic curve associated with this `Koblitz` instance. - The elliptic curve object is initialized based on the curve name when this property is accessed - for the first time. + The elliptic curve object is initialized based on the `curve_name` attribute when this property is accessed + for the first time. Caching ensures efficient reuse across multiple operations. Returns: - EllipticCurve: An instance of `EllipticCurve` associated with the specified `curve_name`. + EllipticCurve: An instance of `EllipticCurve` representing the curve used for encoding and decoding messages. """ + return get_curve(self.curve_name) - if self._curve is None: - self._curve = get_curve(self.curve_name) - return self._curve + @lru_cache(maxsize=1024, typed=True) + def encode( + self, message: str, alphabet_size: int = 2**8, lengthy=False + ) -> Union[Tuple[Tuple[Point, int]], Tuple[Point, int]]: + """Encodes a textual message to a point on the elliptic curve using the Koblitz method. - def encode(self, message: str, alphabet_size: int = 2**8) -> Tuple[Point, int]: - """Encodes a textual message to a curve point using the Koblitz method. + This method efficiently converts a textual message (represented as a string) into a point + on the elliptic curve associated with this `Koblitz` instance. The Koblitz method leverages + the specified `alphabet_size` to map characters in the message to integers within a valid + range. Args: - message (str): The message to be encoded. Each character should be representable - within the specified `alphabet_size`. - alphabet_size (int): The size of the alphabet/character set to consider for encoding. - Common values are 2**8 for ASCII and 2**16 for Unicode, which correspond to - the number of values a single character can take. + message (str): The textual message to be encoded. Each character in the message should + be representable within the provided `alphabet_size`. Common choices for `alphabet_size` + include 2**8 for ASCII encoding and 2**16 for Unicode encoding, depending on the character + set used in the message. + alphabet_size (int, optional): The size of the alphabet/character set used in the message. + Defaults to 2**8 (256) for ASCII encoding. Higher values accommodate larger character sets. + lengthy (bool, optional): A flag indicating whether the message is lengthy or not. If True, the method + treats the `message` argument as a large message to be encoded in chunks. Defaults to False. Returns: - Tuple[Point, int]: A tuple with the encoded point on the elliptic curve and - an auxiliary value j used in the encoding process. + Union[Tuple[Point, int], Tuple[Tuple[Point, int]]]: + - If `lengthy` is False, a single tuple containing two elements is returned: + - The first element is a `Point` object representing the encoded point on the elliptic curve. + - The second element is an integer `j` that serves as an auxiliary value used during the + encoding process. + - If `lengthy` is True, a tuple of tuples is returned. Each inner tuple follows the same format + as the single tuple described above. """ - # Convert the string message to a single large integer - message_decimal = sum( - ord(char) * (alphabet_size**i) for i, char in enumerate(message) + if alphabet_size == 2**8: + size = 64 + else: + size = 32 + + # Encode a single message + if not lengthy: + # Convert the string message to a single large integer + message_decimal = sum( + ord(char) * (alphabet_size**i) for i, char in enumerate(message[:size]) + ) + + # Search for a valid curve point using the Koblitz method + d = 100 # Scaling factor + for j in range(1, d - 1): + x = (d * message_decimal + j) % self.curve.p + s = (x**3 + self.curve.a * x + self.curve.b) % self.curve.p + + # Check if 's' is a quadratic residue modulo 'p', meaning 'y' can be computed + if s == pow(s, (self.curve.p + 1) // 2, self.curve.p): + y = pow(s, (self.curve.p + 1) // 4, self.curve.p) + + # Verify that the computed point is on the curve + if self.curve.is_point_on_curve(Point(x, y)): + break + + return Point(x, y), j + + # Initialize a multiprocessing pool + pool = multiprocessing.Pool() + + # Execute the encode function in parallel using the pool + encoded_messages = pool.map( + partial(self.encode, alphabet_size=alphabet_size, lengthy=False), + [message[i : i + size] for i in range(0, len(message), size)], ) - # Search for a valid curve point using the Koblitz method - d = 100 - for j in range(1, d - 1): - x = (d * message_decimal + j) % self.curve.p - s = (x**3 + self.curve.a * x + self.curve.b) % self.curve.p - - # Check if 's' is a quadratic residue modulo 'p', meaning 'y' can be computed - if s == pow(s, (self.curve.p + 1) // 2, self.curve.p): - y = pow(s, (self.curve.p + 1) // 4, self.curve.p) - - # Verify that the computed point is on the curve - if self.curve.is_point_on_curve(Point(x, y)): - break + # Close the pool + pool.close() + pool.join() - return Point(x, y), j + return tuple(encoded_messages) - @staticmethod - def decode(point: Point, j: int, alphabet_size: int = 2**8) -> str: + @lru_cache(maxsize=1024, typed=True) + def decode( + self, + encoded: Union[Point, tuple[Tuple[Point, int]]], + j: int = 0, + alphabet_size: int = 2**8, + lengthy=False, + ) -> str: """Decodes a point on an elliptic curve to a textual message using the Koblitz method. + This method recovers the original textual message from a point on the elliptic curve + associated with this `Koblitz` class. The `decode` method leverages the Koblitz method and + the provided `j` value, which was obtained during the encoding process, to recover the message. + The specified `alphabet_size` is crucial for interpreting the integer values derived from the + curve point and mapping them back to characters in the message. + Args: - point (Point): The encoded point on the elliptic curve. - j (int): The auxiliary value 'j' used during the encoding process. - alphabet_size (int): The size of the alphabet/character set considered for decoding. + encoded (Point): The encoded point on the elliptic curve to be decoded, or a tuple of tuples + representing multiple encoded points if `lengthy` was True during encoding. + j (int): The auxiliary value 'j' that was generated during the encoding process and is + used to assist in the decoding process. Defaults to 0. + alphabet_size (int, optional): The size of the alphabet/character set used in the message. + Defaults to 2**8 (256) for ASCII encoding. Higher values accommodate larger character sets. + lengthy (bool, optional): A flag indicating whether the message was encoded in chunks. If True, the method + treats the `encoded` argument as a collection of encoded messages to be decoded individually. + Defaults to False. Returns: - str: The decoded textual message. + str: The decoded textual message that was originally encoded using the Koblitz method. + + Raises: + ValueError: If the provided point is not on the elliptic curve associated with this `Koblitz` instance. """ - # Calculate the original large integer from the point and 'j' - d = 100 - message_decimal = (point.x - j) // d + # Decode single point + if not lengthy and isinstance(encoded, Point): + # Calculate the original large integer from the point and 'j' + d = 100 # Assuming 'd' is a scaling factor used in encoding + message_decimal = (encoded.x - j) // d + + # Decompose the large integer into individual characters based on `alphabet_size` + characters = [] + while message_decimal != 0: + characters.append(chr(message_decimal % alphabet_size)) + message_decimal //= alphabet_size + + # Convert the list of characters into a string and return it + return "".join(characters) + + # Decode tuple of (Point, int) pairs + is_tuple_of_point_int = lambda instance: isinstance(instance, tuple) and all( + isinstance(elem, tuple) + and len(elem) == 2 + and isinstance(elem[0], Point) + and isinstance(elem[1], int) + for elem in instance + ) - # Decompose the large integer into individual characters based on `alphabet_size` characters = [] - while message_decimal != 0: - characters.append(chr(message_decimal % alphabet_size)) - message_decimal //= alphabet_size + if is_tuple_of_point_int(encoded): + + # Initialize a multiprocessing pool + pool = multiprocessing.Pool() + + # Execute the decode function in parallel using the pool + characters = pool.starmap( + partial(self.decode, alphabet_size=alphabet_size, lengthy=False), + [(i[0], i[1]) for i in encoded], + ) + + # Close the pool + pool.close() + pool.join() - # Convert the list of characters into a string and return it return "".join(characters) -@dataclass +@dataclass(frozen=True) class DigitalSignature: """Class to perform digital signature and verification using the ECDSA scheme. @@ -114,35 +198,57 @@ class DigitalSignature: private_key: int curve_name: str = "secp192k1" - public_key: Optional[Point] = None - def __post_init__(self) -> None: - """Initializes the DigitalSignature class and sets the public key.""" + @property + @lru_cache(maxsize=1024, typed=True) + def curve(self) -> EllipticCurve: + """Retrieves the elliptic curve associated with this `DigitalSignature` instance. + + The `curve_name` attribute is used to fetch the corresponding elliptic curve object. If the + curve object hasn't been retrieved yet, it is fetched from the `get_curve` function and + cached for efficient reuse within the instance. - self._curve = None - if self.public_key is None: - self.public_key = self.curve.multiply_point(self.private_key, self.curve.G) + Returns: + EllipticCurve: The elliptic curve object used for ECDSA operations. + """ + return get_curve(self.curve_name) @property - def curve(self) -> EllipticCurve: - """Retrieves the elliptic curve based on the curve_name, if not already set.""" + @lru_cache(maxsize=1024, typed=True) + def public_key(self) -> Point: + """Computes and returns the public key corresponding to the private key. - if self._curve is None: - self._curve = get_curve(self.curve_name) - return self._curve + This property leverages the `curve` property to access the elliptic curve and the + `multiply_point` method provided by the underlying elliptic curve library to + calculate the public key. The public key is derived by multiplying the generator + point (`G`) of the curve with the private key. - def generate_signature(self, message_hash: int) -> Tuple[int, int]: + Caching ensures efficient retrieval of the public key across multiple calls within the same instance. + + Returns: + Point: The public key point on the elliptic curve associated with this instance. """ - Generates an ECDSA signature for a given private key and message hash. + return self.curve.multiply_point(self.private_key, self.curve.G) + + @lru_cache(maxsize=1024, typed=True) + def generate_signature(self, message_hash: int) -> Tuple[int, int]: + """Generates an ECDSA signature for a given message hash using the private key. + + This method employs the Elliptic Curve Digital Signature Algorithm (ECDSA) to create a cryptographic + signature for the provided `message_hash`. The signature generation process utilizes the private key + associated with this `DigitalSignature` instance and a cryptographically secure random number `k` that + is chosen for each signature to ensure security. Args: - message_hash (int): The hash of the message to be signed. + message_hash (int): The hash of the message to be signed. The hash function used should + match the one used during message verification. Common hash functions include SHA-256 and SHA-384. Returns: - Tuple[int, int]: The ECDSA signature (r, s). + Tuple[int, int]: The ECDSA signature as a tuple containing two integers (r, s). The signature + can be used to verify the authenticity of the message and the signer's identity. - Note: - The random number k used in the signature process is chosen unpredictably for each signature. + Raises: + ValueError: If the provided `message_hash` is not of type `int`. """ (r, s) = (0, 0) @@ -155,31 +261,35 @@ def generate_signature(self, message_hash: int) -> Tuple[int, int]: ) % self.curve.n return r, s + @lru_cache(maxsize=1024, typed=True) def verify_signature( self, public_key: Point, message_hash: int, r: int, s: int ) -> bool: """ - Verifies the validity of an ECDSA signature against a public key and message hash. + Verifies the authenticity of an ECDSA signature against a public key and message hash. + + This method employs the Elliptic Curve Digital Signature Algorithm (ECDSA) to + verify the validity of a signature for a given `message_hash`. The verification process + involves the provided `public_key`, which is assumed to correspond to the signer's + private key, and the signature components `r` and `s`. Args: - public_key (Point): The public key corresponding to the signer's private key. - message_hash (int): The hash of the message that was signed. - r (int): The first component of the signature. - s (int): The second component of the signature. + public_key (Point): The public key associated with the signer. + message_hash (bytes): The hash of the message that was supposedly signed. The hash + function used should match the one used during message signing. Common hash functions + include SHA-256 and SHA-384. + r (int): The first component (r) of the ECDSA signature. + s (int): The second component (s) of the ECDSA signature. Returns: - bool: True if the signature is valid with respect to the given public key and message hash, False otherwise. + bool: True if the signature is valid with respect to the given public key and message hash, + False otherwise. A valid signature confirms that the message originated from the + entity with the corresponding private key and has not been tampered with. Raises: - ValueError: If r or s are not in the valid range [1, n-1], where n is the order of the curve. - - Examples: - >>> ds = DigitalSignature(private_key=123456) - >>> public_key = ds.public_key - >>> message_hash = hash('message') - >>> r, s = ds.generate_signature(message_hash) - >>> ds.verify_signature(public_key, message_hash, r, s) - True + ValueError: If r or s are not within the valid range [1, n-1], where n is the order (number + of elements) of the elliptic curve used for signature generation. This ensures the + mathematical integrity of the signature verification process. """ if not (1 <= r < self.curve.n and 1 <= s < self.curve.n): diff --git a/src/ecutils/core.py b/src/ecutils/core.py index 39bff8f..deb8660 100755 --- a/src/ecutils/core.py +++ b/src/ecutils/core.py @@ -1,8 +1,9 @@ from dataclasses import dataclass +from functools import lru_cache from typing import Optional -@dataclass +@dataclass(frozen=True) class Point: """Represents a point on an elliptic curve. @@ -15,9 +16,27 @@ class Point: y: Optional[int] = None +@dataclass(frozen=True) +class JacobianPoint: + """Represents a point on an elliptic curve in Jacobian coordinates. + + Attributes: + x (Optional[int]): The x-coordinate of the point. + y (Optional[int]): The y-coordinate of the point. + z (int): The additional coordinate for projective representation. + """ + + x: Optional[int] = None + y: Optional[int] = None + z: int = 1 + + class EllipticCurveOperations: """Implements mathematical operations for elliptic curves.""" + use_projective_coordinates: bool = True + + @lru_cache(maxsize=1024, typed=True) def add_points(self, p1: Point, p2: Point) -> Point: """Add two points on an elliptic curve. @@ -43,29 +62,48 @@ def add_points(self, p1: Point, p2: Point) -> Point: "Invalid input: One or both of the input points are not on the elliptic curve." ) + if self.use_projective_coordinates: + p1_jacobian = self.to_jacobian(p1) + p2_jacobian = self.to_jacobian(p2) + p3_jacobian = self.jacobian_add_points(p1_jacobian, p2_jacobian) + return self.to_affine(p3_jacobian) + if p1 == p2: - n = (3 * p1.x**2 + self.a) % self.p - d = (2 * p1.y) % self.p - try: - inv = pow(d, -1, self.p) - except ValueError: - return Point() # Point at infinity - s = (n * inv) % self.p - x_3 = (s**2 - p1.x - p1.x) % self.p - y_3 = (s * (p1.x - x_3) - p1.y) % self.p - return Point(x_3, y_3) - else: - n = (p2.y - p1.y) % self.p - d = (p2.x - p1.x) % self.p - try: - inv = pow(d, -1, self.p) - except ValueError: - return Point() # Point at infinity - s = (n * inv) % self.p - x_3 = (s**2 - p1.x - p2.x) % self.p - y_3 = (s * (p1.x - x_3) - p1.y) % self.p - return Point(x_3, y_3) + return self.double_point(p1) + n = (p2.y - p1.y) % self.p + d = (p2.x - p1.x) % self.p + try: + inv = pow(d, -1, self.p) + except ValueError: + return Point() # Point at infinity + s = (n * inv) % self.p + x_3 = (s**2 - p1.x - p2.x) % self.p + y_3 = (s * (p1.x - x_3) - p1.y) % self.p + return Point(x_3, y_3) + + @lru_cache(maxsize=1024, typed=True) + def double_point(self, p: Point) -> Point: + """Double a point on an elliptic curve.""" + if p.x is None or p.y is None: + return p + + if not self.is_point_on_curve(p): + raise ValueError( + "Invalid input: One or both of the input points are not on the elliptic curve." + ) + n = (3 * p.x**2 + self.a) % self.p + d = (2 * p.y) % self.p + try: + inv = pow(d, -1, self.p) + except ValueError: + return Point() # Point at infinity + s = (n * inv) % self.p + x_3 = (s**2 - p.x - p.x) % self.p + y_3 = (s * (p.x - x_3) - p.y) % self.p + return Point(x_3, y_3) + + @lru_cache(maxsize=1024, typed=True) def multiply_point(self, k: int, p: Point) -> Point: """Multiply a point on an elliptic curve by an integer scalar. @@ -83,6 +121,18 @@ def multiply_point(self, k: int, p: Point) -> Point: if k == 0 or k >= self.n: raise ValueError("k is not in the range 0 < k < n") + if self.use_projective_coordinates: + p_jacobian = self.to_jacobian(p) + q_jacobian = self.jacobian_multiply_point(k, p_jacobian) + p1 = self.to_affine(q_jacobian) + if p1.x is None or p1.y is None: + return p1 + if not self.is_point_on_curve(p1): + raise ValueError( + "Invalid input: One or both of the input points are not on the elliptic curve." + ) + return p1 + r = None num_bits = k.bit_length() @@ -95,7 +145,7 @@ def multiply_point(self, k: int, p: Point) -> Point: if r.x is None and r.y is None: r = p - r = self.add_points(r, r) + r = self.double_point(r) if (k >> i) & 1: if r.x is None and r.y is None: @@ -104,6 +154,104 @@ def multiply_point(self, k: int, p: Point) -> Point: r = self.add_points(r, p) return r + @lru_cache(maxsize=1024, typed=True) + def jacobian_add_points( + self, p1: JacobianPoint, p2: JacobianPoint + ) -> JacobianPoint: + """Add two points on an elliptic curve using Jacobian coordinates.""" + if p1.x is None or p1.y is None: + return p2 + if p2.x is None or p2.y is None: + return p1 + + z1z1 = p1.z * p1.z % self.p + z2z2 = p2.z * p2.z % self.p + u1 = p1.x * z2z2 % self.p + u2 = p2.x * z1z1 % self.p + s1 = p1.y * p2.z * z2z2 % self.p + s2 = p2.y * p1.z * z1z1 % self.p + + if u1 == u2: + if s1 != s2: + return JacobianPoint() # Point at infinity + return self.jacobian_double_point(p1) + + h = u2 - u1 + i = (2 * h) * (2 * h) % self.p + j = h * i % self.p + r = 2 * (s2 - s1) % self.p + v = u1 * i % self.p + x = (r * r - j - 2 * v) % self.p + y = (r * (v - x) - 2 * s1 * j) % self.p + z = ((p1.z + p2.z) * (p1.z + p2.z) - z1z1 - z2z2) * h % self.p + + return JacobianPoint(x, y, z) + + @lru_cache(maxsize=1024, typed=True) + def jacobian_double_point(self, p: JacobianPoint) -> JacobianPoint: + """Double a point on an elliptic curve using Jacobian coordinates.""" + if p.x is None or p.y is None: + return p + + if p.y == 0: + return JacobianPoint() # Point at infinity + + ysq = p.y * p.y % self.p + zsqr = p.z * p.z % self.p + s = (4 * p.x * ysq) % self.p + m = (3 * p.x * p.x + self.a * zsqr * zsqr) % self.p + nx = (m * m - 2 * s) % self.p + ny = (m * (s - nx) - 8 * ysq * ysq) % self.p + nz = (2 * p.y * p.z) % self.p + + return JacobianPoint(nx, ny, nz) + + @lru_cache(maxsize=1024, typed=True) + def jacobian_multiply_point(self, k: int, p: JacobianPoint) -> JacobianPoint: + """Multiply a point on an elliptic curve by an integer scalar using repeated addition.""" + if k == 0 or p.x is None or p.y is None: + return JacobianPoint() # Identity point + + result = JacobianPoint() # Initialize with the identity point + k_bin = bin(k)[2:] # Binary representation of k + for i in range(len(k_bin)): + if k_bin[-i - 1] == "1": + result = self.jacobian_add_points(result, p) + p = self.jacobian_double_point(p) + + return result + + @staticmethod + @lru_cache(maxsize=1024, typed=True) + def to_jacobian(point: Point) -> JacobianPoint: + """Converts a point from affine coordinates to Jacobian coordinates. + + Args: + point (Point): The point in affine coordinates. + + Returns: + JacobianPoint: The point in Jacobian coordinates. + """ + if point.x is None or point.y is None: + return JacobianPoint() + return JacobianPoint(point.x, point.y, 1) + + @lru_cache(maxsize=1024, typed=True) + def to_affine(self, point: JacobianPoint) -> Point: + """Converts a point from Jacobian coordinates to affine coordinates. + + Args: + point (JacobianPoint): The point in Jacobian coordinates. + + Returns: + Point: The point in affine coordinates. + """ + if point.x is None or point.y is None or point.z == 0: + return Point() + inv_z = pow(point.z, -1, self.p) + return Point((point.x * inv_z**2) % self.p, (point.y * inv_z**3) % self.p) + + @lru_cache(maxsize=1024, typed=True) def is_point_on_curve(self, p: Point) -> bool: """Check if a point lies on the elliptic curve. @@ -116,13 +264,17 @@ def is_point_on_curve(self, p: Point) -> bool: if p.x is None or p.y is None: return False + + if isinstance(p, JacobianPoint): + p = self.to_affine(p) + # The equation of the curve is y^2 = x^3 + ax + b. We check if the point satisfies this equation. left_side = p.y**2 % self.p right_side = (p.x**3 + self.a * p.x + self.b) % self.p return left_side == right_side -@dataclass +@dataclass(frozen=True) class EllipticCurve(EllipticCurveOperations): """Represents the parameters and operations of an elliptic curve. @@ -133,6 +285,7 @@ class EllipticCurve(EllipticCurveOperations): G (Point): The base point (generator) of the curve. n (int): The order of the base point. h (int): The cofactor. + use_projective_coordinates (bool): If True, Jacobian coordinates will be used in curve operations. """ p: int diff --git a/src/ecutils/curves.py b/src/ecutils/curves.py index 3a8dfc1..74b2fb6 100644 --- a/src/ecutils/curves.py +++ b/src/ecutils/curves.py @@ -1,3 +1,5 @@ +from functools import lru_cache + from ecutils.core import EllipticCurve, Point secp192k1 = EllipticCurve( @@ -99,6 +101,7 @@ ) +@lru_cache(maxsize=1024, typed=True) def get(name) -> EllipticCurve: """Retrieve an EllipticCurve instance by its standard name. diff --git a/src/ecutils/protocols.py b/src/ecutils/protocols.py index cf765af..5292425 100644 --- a/src/ecutils/protocols.py +++ b/src/ecutils/protocols.py @@ -1,39 +1,54 @@ from dataclasses import dataclass -from typing import Optional +from functools import lru_cache from ecutils.core import EllipticCurve, Point from ecutils.curves import get as get_curve -@dataclass +@dataclass(frozen=True) class DiffieHellman: """Class to perform Diffie-Hellman key exchange using elliptic curves. Attributes: private_key (int): The private key of the user. curve_name (str): Name of the elliptic curve to be used. Defaults to 'secp192k1'. - public_key (Optional[Point]): The calculated public key based on the private key and curve. """ private_key: int curve_name: str = "secp192k1" - public_key: Optional[Point] = None - def __post_init__(self) -> None: - """Initializes the Diffie-Hellman class and computes the public key if not provided.""" + @property + @lru_cache(maxsize=1024, typed=True) + def curve(self) -> EllipticCurve: + """Retrieves the elliptic curve associated with this `DiffieHellman` instance. + + The `curve_name` attribute is used to fetch the corresponding elliptic curve object. If the + curve object hasn't been retrieved yet, it is fetched from the `get_curve` function and + cached for efficient reuse within the instance. - self._curve = None - if self.public_key is None: - self.public_key = self.curve.multiply_point(self.private_key, self.curve.G) + Returns: + EllipticCurve: The elliptic curve object used for ECDSA operations. + """ + return get_curve(self.curve_name) @property - def curve(self) -> EllipticCurve: - """Gets the elliptic curve based on the given curve name, if not already set.""" + @lru_cache(maxsize=1024, typed=True) + def public_key(self) -> Point: + """Computes and returns the public key corresponding to the private key. + + This property leverages the `curve` property to access the elliptic curve and the + `multiply_point` method provided by the underlying elliptic curve library to + calculate the public key. The public key is derived by multiplying the generator + point (`G`) of the curve with the private key. + + Caching ensures efficient retrieval of the public key across multiple calls within the same instance. - if self._curve is None: - self._curve = get_curve(self.curve_name) - return self._curve + Returns: + Point: The public key point on the elliptic curve associated with this instance. + """ + return self.curve.multiply_point(self.private_key, self.curve.G) + @lru_cache(maxsize=1024, typed=True) def compute_shared_secret(self, other_public_key: Point) -> Point: """Computes the shared secret using the private key and the other party's public key. @@ -47,7 +62,7 @@ def compute_shared_secret(self, other_public_key: Point) -> Point: return self.curve.multiply_point(self.private_key, other_public_key) -@dataclass +@dataclass(frozen=True) class MasseyOmura: """Class to perform Massey-Omura key exchange using elliptic curves. @@ -59,29 +74,50 @@ class MasseyOmura: private_key: int curve_name: str = "secp192k1" - def __post_init__(self) -> None: - """Initializes the Massey-Omura class.""" + @property + @lru_cache(maxsize=1024, typed=True) + def curve(self) -> EllipticCurve: + """Retrieves the elliptic curve associated with this `MasseyOmura` instance. + + The `curve_name` attribute is used to fetch the corresponding elliptic curve object. If the + curve object hasn't been retrieved yet, it is fetched from the `get_curve` function and + cached for efficient reuse within the instance. - self._curve = None + Returns: + EllipticCurve: The elliptic curve object used for ECDSA operations. + """ + return get_curve(self.curve_name) @property - def curve(self) -> EllipticCurve: - """Gets the elliptic curve based on the given curve name, if not already set.""" + @lru_cache(maxsize=1024, typed=True) + def public_key(self) -> Point: + """Computes and returns the public key corresponding to the private key. + + This property leverages the `curve` property to access the elliptic curve and the + `multiply_point` method provided by the underlying elliptic curve library to + calculate the public key. The public key is derived by multiplying the generator + point (`G`) of the curve with the private key. + + Caching ensures efficient retrieval of the public key across multiple calls within the same instance. - if self._curve is None: - self._curve = get_curve(self.curve_name) - return self._curve + Returns: + Point: The public key point on the elliptic curve associated with this instance. + """ + return self.curve.multiply_point(self.private_key, self.curve.G) + @lru_cache(maxsize=1024, typed=True) def first_encryption_step(self, message: Point) -> Point: """Encrypts the message with the sender's private key.""" return self.curve.multiply_point(self.private_key, message) + @lru_cache(maxsize=1024, typed=True) def second_encryption_step(self, received_encrypted_message: Point) -> Point: """Applies the receiver's private key on the received encrypted message.""" return self.first_encryption_step(received_encrypted_message) + @lru_cache(maxsize=1024, typed=True) def partial_decryption_step(self, encrypted_message: Point) -> Point: """Partial decryption using the inverse of the sender's private key.""" diff --git a/src/ecutils/utils.py b/src/ecutils/utils.py new file mode 100644 index 0000000..18fde53 --- /dev/null +++ b/src/ecutils/utils.py @@ -0,0 +1,32 @@ +import hashlib +from functools import lru_cache + + +@lru_cache(maxsize=1024, typed=True) +def calculate_file_hash(file_name: str, block_size: int = 16384) -> int: + """Calculates the SHA-256 hash of a file efficiently. + + This function efficiently calculates the SHA-256 hash of a file, using a cache + to store previously calculated hashes and a block-wise reading approach to + optimize memory usage. + + Args: + file_name (str): The name of the file to calculate the hash for. + block_size (int, optional): The size of the data blocks to read from the + file in bytes. Defaults to 16384. + + Returns: + int: The SHA-256 hash of the file represented as an integer (base 16). + + Raises: + FileNotFoundError: If the specified file is not found. + """ + + try: + sha256_hash = hashlib.sha256() + with open(file_name, "rb") as f: + for block in iter(lambda: f.read(block_size), b""): + sha256_hash.update(block) + return int(sha256_hash.hexdigest(), 16) + except FileNotFoundError as e: + raise FileNotFoundError(f"File not found: {file_name}") from e diff --git a/tests/test_elliptic_curve_operations.py b/tests/test_elliptic_curve_operations.py index 53af62c..5f82d89 100644 --- a/tests/test_elliptic_curve_operations.py +++ b/tests/test_elliptic_curve_operations.py @@ -96,14 +96,16 @@ def test_addition_with_identity(self): def test_invalid_scalar_multiplication(self): """Test scalar multiplication with invalid scalar or point.""" - with self.assertRaises(ValueError): + with self.assertRaises(ValueError, msg="Test multiplying by scalar 0"): self.curve.multiply_point(0, self.point1) # Test multiplying by scalar 0 - with self.assertRaises(ValueError): + with self.assertRaises( + ValueError, msg="Test multiplying by scalar n (or larger)" + ): self.curve.multiply_point( self.curve.n, self.point1 ) # Test multiplying by scalar n (or larger) off_curve_point = Point(x=200, y=119) - with self.assertRaises(ValueError): + with self.assertRaises(ValueError, msg="Test with point not on the curve"): self.curve.multiply_point( 2, off_curve_point ) # Test with point not on the curve