A Python port of the Invisible Internet Project (I2P)
1"""EdDSA blinding primitives for I2P LS2 blinded destinations.
2
3Uses libsodium (via pynacl) for Ed25519 scalar and point operations.
4Required for EncryptedLeaseSet and blinded destination support.
5"""
6
7from __future__ import annotations
8
9import hashlib
10
11try:
12 from nacl.bindings import (
13 crypto_core_ed25519_scalar_reduce,
14 crypto_core_ed25519_scalar_add,
15 crypto_core_ed25519_add,
16 crypto_scalarmult_ed25519_base_noclamp,
17 )
18 HAS_NACL = True
19except ImportError:
20 HAS_NACL = False
21
22
23class EdDSABlinding:
24 """Static methods for Ed25519 blinding operations.
25
26 Blinding allows a destination to publish encrypted LeaseSets
27 where the published key is unrelated to the real signing key
28 without knowledge of the blinding factor (alpha).
29 """
30
31 @staticmethod
32 def _check_nacl() -> None:
33 if not HAS_NACL:
34 raise RuntimeError(
35 "pynacl with libsodium is required for EdDSA blinding"
36 )
37
38 @staticmethod
39 def seed_to_scalar(seed: bytes) -> bytes:
40 """Expand a 32-byte Ed25519 seed to a clamped scalar.
41
42 Ed25519 private keys in I2P are 32-byte seeds. To use them
43 with libsodium's scalar functions, we must expand via SHA-512
44 and clamp per RFC 8032.
45 """
46 expanded = hashlib.sha512(seed).digest()[:32]
47 scalar = bytearray(expanded)
48 scalar[0] &= 0xF8 # clear bottom 3 bits
49 scalar[31] &= 0x7F # clear top bit
50 scalar[31] |= 0x40 # set second-to-top bit
51 return bytes(scalar)
52
53 @staticmethod
54 def reduce(data_64: bytes) -> bytes:
55 """Reduce 64 bytes to a 32-byte Ed25519 scalar mod L."""
56 EdDSABlinding._check_nacl()
57 return crypto_core_ed25519_scalar_reduce(data_64)
58
59 @staticmethod
60 def blind_public(pub: bytes, alpha_pub: bytes) -> bytes:
61 """Add two Ed25519 points (public keys).
62
63 blinded_pub = pub + alpha_pub (point addition)
64 """
65 EdDSABlinding._check_nacl()
66 return crypto_core_ed25519_add(pub, alpha_pub)
67
68 @staticmethod
69 def blind_private(priv_scalar: bytes, alpha_scalar: bytes) -> bytes:
70 """Add two Ed25519 scalars.
71
72 blinded_priv = priv_scalar + alpha_scalar (mod L)
73 """
74 EdDSABlinding._check_nacl()
75 return crypto_core_ed25519_scalar_add(priv_scalar, alpha_scalar)
76
77 @staticmethod
78 def scalar_mult_base(scalar: bytes) -> bytes:
79 """Compute scalar * basepoint (no clamping).
80
81 Returns the 32-byte public key corresponding to the scalar.
82 """
83 EdDSABlinding._check_nacl()
84 return crypto_scalarmult_ed25519_base_noclamp(scalar)