""" X25519 Elliptic Curve Diffie-Hellman key exchange. Port of net.i2p.crypto.x25519.X25519DH from I2P Java. Wraps Python's ``cryptography`` library (X25519PrivateKey / X25519PublicKey). All keys and shared secrets are raw 32-byte little-endian byte strings, matching the wire format used by I2P's ECIES_X25519. """ from __future__ import annotations from cryptography.hazmat.primitives.asymmetric.x25519 import ( X25519PrivateKey, X25519PublicKey, ) from cryptography.hazmat.primitives.serialization import ( Encoding, PublicFormat, PrivateFormat, NoEncryption, ) class X25519DH: """ Static-method helper for X25519 ECDH, mirroring the Java ``X25519DH`` utility class. All byte strings are 32 bytes in length. """ # Key length in bytes (matches EncType.ECIES_X25519) KEY_LENGTH = 32 @staticmethod def generate_keypair() -> tuple[bytes, bytes]: """ Generate a new X25519 key pair. Returns: (private_key_bytes, public_key_bytes) -- both 32 bytes. """ private_key = X25519PrivateKey.generate() priv_bytes = private_key.private_bytes( Encoding.Raw, PrivateFormat.Raw, NoEncryption() ) pub_bytes = private_key.public_key().public_bytes( Encoding.Raw, PublicFormat.Raw ) return priv_bytes, pub_bytes @staticmethod def dh(private_key_bytes: bytes, public_key_bytes: bytes) -> bytes: """ Perform an X25519 Diffie-Hellman exchange. Args: private_key_bytes: 32-byte private (scalar) key. public_key_bytes: 32-byte public (u-coordinate) key. Returns: 32-byte shared secret. Raises: ValueError: if either key is not exactly 32 bytes. """ if len(private_key_bytes) != 32: raise ValueError( f"Private key must be 32 bytes, got {len(private_key_bytes)}" ) if len(public_key_bytes) != 32: raise ValueError( f"Public key must be 32 bytes, got {len(public_key_bytes)}" ) private_key = X25519PrivateKey.from_private_bytes(private_key_bytes) public_key = X25519PublicKey.from_public_bytes(public_key_bytes) return private_key.exchange(public_key) @staticmethod def public_from_private(private_key_bytes: bytes) -> bytes: """ Derive the public key from a private key. Args: private_key_bytes: 32-byte private key. Returns: 32-byte public key. Raises: ValueError: if the private key is not exactly 32 bytes. """ if len(private_key_bytes) != 32: raise ValueError( f"Private key must be 32 bytes, got {len(private_key_bytes)}" ) private_key = X25519PrivateKey.from_private_bytes(private_key_bytes) return private_key.public_key().public_bytes( Encoding.Raw, PublicFormat.Raw )