A Python port of the Invisible Internet Project (I2P)
1"""
2X25519 Elliptic Curve Diffie-Hellman key exchange.
3
4Port of net.i2p.crypto.x25519.X25519DH from I2P Java.
5Wraps Python's ``cryptography`` library (X25519PrivateKey / X25519PublicKey).
6
7All keys and shared secrets are raw 32-byte little-endian byte strings,
8matching the wire format used by I2P's ECIES_X25519.
9"""
10
11from __future__ import annotations
12
13from cryptography.hazmat.primitives.asymmetric.x25519 import (
14 X25519PrivateKey,
15 X25519PublicKey,
16)
17from cryptography.hazmat.primitives.serialization import (
18 Encoding,
19 PublicFormat,
20 PrivateFormat,
21 NoEncryption,
22)
23
24
25class X25519DH:
26 """
27 Static-method helper for X25519 ECDH, mirroring the Java
28 ``X25519DH`` utility class.
29
30 All byte strings are 32 bytes in length.
31 """
32
33 # Key length in bytes (matches EncType.ECIES_X25519)
34 KEY_LENGTH = 32
35
36 @staticmethod
37 def generate_keypair() -> tuple[bytes, bytes]:
38 """
39 Generate a new X25519 key pair.
40
41 Returns:
42 (private_key_bytes, public_key_bytes) -- both 32 bytes.
43 """
44 private_key = X25519PrivateKey.generate()
45 priv_bytes = private_key.private_bytes(
46 Encoding.Raw, PrivateFormat.Raw, NoEncryption()
47 )
48 pub_bytes = private_key.public_key().public_bytes(
49 Encoding.Raw, PublicFormat.Raw
50 )
51 return priv_bytes, pub_bytes
52
53 @staticmethod
54 def dh(private_key_bytes: bytes, public_key_bytes: bytes) -> bytes:
55 """
56 Perform an X25519 Diffie-Hellman exchange.
57
58 Args:
59 private_key_bytes: 32-byte private (scalar) key.
60 public_key_bytes: 32-byte public (u-coordinate) key.
61
62 Returns:
63 32-byte shared secret.
64
65 Raises:
66 ValueError: if either key is not exactly 32 bytes.
67 """
68 if len(private_key_bytes) != 32:
69 raise ValueError(
70 f"Private key must be 32 bytes, got {len(private_key_bytes)}"
71 )
72 if len(public_key_bytes) != 32:
73 raise ValueError(
74 f"Public key must be 32 bytes, got {len(public_key_bytes)}"
75 )
76
77 private_key = X25519PrivateKey.from_private_bytes(private_key_bytes)
78 public_key = X25519PublicKey.from_public_bytes(public_key_bytes)
79 return private_key.exchange(public_key)
80
81 @staticmethod
82 def public_from_private(private_key_bytes: bytes) -> bytes:
83 """
84 Derive the public key from a private key.
85
86 Args:
87 private_key_bytes: 32-byte private key.
88
89 Returns:
90 32-byte public key.
91
92 Raises:
93 ValueError: if the private key is not exactly 32 bytes.
94 """
95 if len(private_key_bytes) != 32:
96 raise ValueError(
97 f"Private key must be 32 bytes, got {len(private_key_bytes)}"
98 )
99
100 private_key = X25519PrivateKey.from_private_bytes(private_key_bytes)
101 return private_key.public_key().public_bytes(
102 Encoding.Raw, PublicFormat.Raw
103 )