Skip to content

Commit 68767d1

Browse files
committed
feat: add crypto ed25519 support
1 parent 9c793d3 commit 68767d1

File tree

10 files changed

+932
-0
lines changed

10 files changed

+932
-0
lines changed

binding/python/py/synlink/crypto/__init__.py

Whitespace-only changes.
Lines changed: 288 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,288 @@
1+
from dataclasses import dataclass
2+
from typing import Optional, Tuple
3+
4+
import nacl.utils as utils
5+
from nacl.exceptions import BadSignatureError
6+
from nacl.public import PrivateKey as ImplPrivateKey
7+
8+
from nacl.signing import SigningKey, VerifyKey
9+
from nacl.encoding import HexEncoder
10+
from synlink.crypto.exception import CryptoException
11+
from synlink.crypto.kind import Kind
12+
from synlink.crypto.typing import PrivateKey as IPrivateKey
13+
from synlink.crypto.typing import PublicKey as IPublicKey
14+
from synlink.utils import _check_minimum_version
15+
16+
# Type hint for python lower then 3.11
17+
if _check_minimum_version(3, 11, 0):
18+
from typing import Self
19+
else:
20+
from typing_extensions import Self
21+
22+
23+
__all__ = ["PublicKey", "PrivateKey", "KeyPair", "create_new_ed25519_key_pair",
24+
"create_new_ed25519_key_pair_from_seed"]
25+
26+
27+
class PublicKey(IPublicKey):
28+
"""
29+
Represents an ED25519 public key, providing methods for verification
30+
and various conversions. This class wraps nacl.signing.VerifyKey.
31+
"""
32+
def __init__(self, impl: VerifyKey):
33+
"""
34+
Initializes a PublicKey object.
35+
36+
Args:
37+
impl: An instance of nacl.signing.VerifyKey.
38+
"""
39+
super().__init__()
40+
self._impl: VerifyKey = impl
41+
self._kind = Kind.ED25519
42+
43+
def get_kind(self) -> Kind:
44+
"""
45+
Returns the kind of this cryptographic key.
46+
"""
47+
return self._kind
48+
49+
def to_bytes(self) -> bytes:
50+
"""
51+
Converts the public key to its raw byte representation.
52+
"""
53+
return bytes(self._impl)
54+
55+
def __bytes__(self) -> bytes:
56+
"""
57+
Allows the PublicKey object to be converted to bytes using bytes().
58+
"""
59+
return self.to_bytes()
60+
61+
@classmethod
62+
def from_bytes(cls, data: bytes) -> Self:
63+
"""
64+
Creates a PublicKey object from its raw byte representation.
65+
66+
Args:
67+
data: The raw bytes of the public key (VerifyKey).
68+
69+
Returns:
70+
A new PublicKey instance.
71+
"""
72+
return cls(VerifyKey(data))
73+
74+
def verify(self, data: bytes, signature: bytes) -> bool:
75+
"""
76+
Verifies the signature against the signed message.
77+
78+
Args:
79+
data: The original message bytes that were signed.
80+
signature: The raw signature bytes.
81+
82+
Returns:
83+
True if the signature is valid, False otherwise.
84+
"""
85+
try:
86+
# Direct verification using the wrapped VerifyKey
87+
self._impl.verify(data, signature)
88+
return True
89+
except BadSignatureError:
90+
return False
91+
except Exception as e:
92+
# Catch other potential exceptions during verification
93+
# print(f"An unexpected error occurred during verification: {e}") # For debugging
94+
return False
95+
96+
97+
def __str__(self) -> str:
98+
"""
99+
Returns a human-readable string representation of the public key.
100+
"""
101+
output = self._impl \
102+
.encode(encoder=HexEncoder()) \
103+
.decode('utf-8')
104+
return f"PublicKey({output})"
105+
106+
def __repr__(self) -> str:
107+
"""
108+
Returns a detailed string representation for debugging.
109+
"""
110+
output = self._impl.encode(encoder=HexEncoder()).decode('utf-8')
111+
# Show a truncated hex representation for brevity in repr
112+
return f"<synlink.crypto.ed25519.PublicKey {output[:8]}...{output[-8:]}>"
113+
114+
115+
class PrivateKey(IPrivateKey):
116+
"""
117+
Represents an ED25519 private key, providing methods for signing
118+
and various conversions. This class wraps nacl.signing.SigningKey.
119+
"""
120+
def __init__(self, impl: SigningKey):
121+
"""
122+
Initializes a PrivateKey object.
123+
124+
Args:
125+
impl: An instance of nacl.signing.SigningKey.
126+
"""
127+
super().__init__()
128+
self._kind = Kind.ED25519
129+
self._impl: SigningKey = impl
130+
131+
def get_kind(self) -> Kind:
132+
"""
133+
Returns the kind of this cryptographic key.
134+
"""
135+
return self._kind
136+
137+
@classmethod
138+
def generate(cls) -> Self:
139+
"""
140+
Generates a new random ED25519 private key.
141+
142+
Returns:
143+
A new PrivateKey instance.
144+
"""
145+
impl = SigningKey.generate()
146+
return cls(impl)
147+
148+
@classmethod
149+
def from_seed(cls, seed: Optional[bytes] = None) -> Self:
150+
"""
151+
Generates an ED25519 private key from a 32-byte seed.
152+
153+
If no seed is provided, a random 32-byte seed will be generated.
154+
155+
Args:
156+
seed: An optional 32-byte binary sequence to use as a seed.
157+
158+
Returns:
159+
A new PrivateKey instance.
160+
161+
Raises:
162+
AssertionError: If the provided seed is not 32 bytes long.
163+
"""
164+
if seed is None:
165+
# Ensure random seed is exactly 32 bytes
166+
seed = utils.random(32)
167+
assert (
168+
len(seed) == 32
169+
), "PrivateKey seed must be a 32 bytes long binary sequence"
170+
impl = ImplPrivateKey.from_seed(seed)
171+
return cls(SigningKey(bytes(impl)))
172+
173+
@classmethod
174+
def from_bytes(cls, data: bytes) -> Self:
175+
"""
176+
Creates an ED25519 private key from its raw byte representation.
177+
178+
Args:
179+
data: The raw bytes of the private key (SigningKey).
180+
181+
Returns:
182+
A new PrivateKey instance.
183+
"""
184+
impl = SigningKey(data)
185+
return cls(impl)
186+
187+
def get_public_key(self) -> IPublicKey:
188+
"""
189+
Returns the public key corresponding to this private key.
190+
"""
191+
if not isinstance(self._impl, SigningKey):
192+
# Defensive check, though __init__ should ensure correct type.
193+
raise CryptoException("Invalid private key implementation.")
194+
# Return a PublicKey wrapping the VerifyKey derived from this SigningKey
195+
return PublicKey(self._impl.verify_key)
196+
197+
def sign(self, data: bytes) -> bytes:
198+
"""
199+
Signs data with the private key and returns the raw signature.
200+
201+
Args:
202+
data: The message bytes to sign.
203+
204+
Returns:
205+
The raw signature bytes.
206+
"""
207+
if not isinstance(self._impl, SigningKey):
208+
raise CryptoException("Invalid private key implementation.")
209+
# Direct signing using the wrapped SigningKey
210+
signed_message = self._impl.sign(data)
211+
return signed_message.message, signed_message.signature # Return only the raw signature bytes
212+
213+
def to_bytes(self) -> bytes:
214+
"""
215+
Converts the private key to its raw byte representation.
216+
"""
217+
return bytes(self._impl)
218+
219+
def __bytes__(self) -> bytes:
220+
"""
221+
Allows the PrivateKey object to be converted to bytes using bytes().
222+
"""
223+
return self.to_bytes()
224+
225+
def __str__(self) -> str:
226+
"""
227+
Returns a human-readable string representation of the private key.
228+
"""
229+
# For security, avoid showing private key material in str.
230+
return "PrivateKey(ED25519)"
231+
232+
def __repr__(self) -> str:
233+
"""
234+
Returns a detailed string representation for debugging.
235+
"""
236+
# For security, avoid showing private key material in repr.
237+
return "<synlink.crypto.ed25519.PrivateKey>"
238+
239+
240+
@dataclass(frozen=True, repr=False)
241+
class KeyPair(object):
242+
"""
243+
Represents an ED25519 key pair, containing both a private and public key.
244+
"""
245+
secret : PrivateKey
246+
public : PublicKey
247+
248+
def __bytes__(self) -> bytes:
249+
"""
250+
Returns the raw bytes of the secret (private) key.
251+
"""
252+
return bytes(self.secret)
253+
254+
def __repr__(self) -> str:
255+
"""
256+
Returns a detailed string representation for debugging.
257+
"""
258+
# Include the public key's repr for better context
259+
return f"<synlink.crypto.ed25519.KeyPair public={self.public!r}>"
260+
261+
262+
def create_new_ed25519_key_pair() -> KeyPair:
263+
"""
264+
Creates a new ED25519 key pair with randomly generated keys.
265+
266+
Returns:
267+
A new KeyPair instance.
268+
"""
269+
secret = PrivateKey.generate()
270+
public = secret.get_public_key()
271+
return KeyPair(secret, public)
272+
273+
274+
def create_new_ed25519_key_pair_from_seed(seed: Optional[bytes] = None) -> KeyPair:
275+
"""
276+
Creates a new ED25519 key pair from an optional 32-byte seed.
277+
278+
If no seed is provided, a random 32-byte seed will be generated.
279+
280+
Args:
281+
seed: An optional 32-byte binary sequence to use as a seed.
282+
283+
Returns:
284+
A new KeyPair instance.
285+
"""
286+
secret: PrivateKey = PrivateKey.from_seed(seed)
287+
public = secret.get_public_key()
288+
return KeyPair(secret, public)
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from synlink.error import SynlinkBaseException
2+
3+
4+
class CryptoException(SynlinkBaseException):
5+
pass
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
2+
import os
3+
import pathlib
4+
from cryptography.hazmat.primitives import serialization
5+
from synlink.crypto.typing import KeyPair, PrivateKey
6+
7+
from typing import Optional
8+
9+
HOME_DIR : str = pathlib.Path.home().__str__()
10+
SSH_DEFAULT_DIRECTORY = os.path.join(HOME_DIR, ".ssh")
11+
12+
def load_ssh_keys(ssh_dir: str = SSH_DEFAULT_DIRECTORY, key_name: str = "id_ed25519", password : Optional[str] = None) -> KeyPair:
13+
"""Load SSH key pair from filesystem.
14+
15+
Args:
16+
ssh_dir: Path to SSH directory (default: ~/.ssh)
17+
key_name: Base name of key files (default: id_rsa)
18+
19+
Returns:
20+
KeyPair containing the loaded public and private keys
21+
22+
Raises:
23+
FileNotFoundError: If key files don't exist
24+
ValueError: If keys are malformed or incompatible
25+
26+
Example:
27+
>>> keypair = load_ssh_keys(key_name="id_ed25519")
28+
"""
29+
file = os.path.join(ssh_dir, key_name),
30+
if not os.path.isfile(file):
31+
raise FileNotFoundError(f"{file} does not exist.")
32+
33+
with open(
34+
file,
35+
"rb",
36+
) as reader:
37+
buffer = serialization.load_ssh_private_key(
38+
reader.read(-1),
39+
password=password,
40+
)
41+
42+
43+
seceret : PrivateKey = PrivateKey.from_bytes(
44+
buffer.private_bytes_raw()
45+
)
46+
47+
public = seceret.get_public_key()
48+
return KeyPair(seceret=seceret, public=public)
49+
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
from enum import Enum
2+
from functools import cache
3+
4+
5+
class Kind(Enum):
6+
ED25519: int = 0
7+
8+
def to_string(self) -> str:
9+
"""return the name of the crypto kind."""
10+
return self.name
11+
12+
def to_bytes(self) -> bytes:
13+
return self.value.to_bytes(1, byteorder="little")
14+
15+
@classmethod
16+
def from_bytes(cls, data: bytes) -> "Kind":
17+
"""return kind from the byte(s)"""
18+
num = int().from_bytes(data, byteorder="little")
19+
if num > cls.max():
20+
raise ValueError(f"bytes shound not exceed {cls.max()}")
21+
elif num < cls.min():
22+
raise ValueError(f"bytes shound not be less then {cls.min()}")
23+
return cls(num)
24+
25+
@staticmethod
26+
def max() -> int:
27+
"""return the maximum possible enumeration number."""
28+
return 0
29+
30+
@staticmethod
31+
def min() -> int:
32+
"""return the maximum possible enumeration number."""
33+
return 0
34+
35+
36+
def __bytes__(self) -> bytes:
37+
return self.to_bytes()
38+
39+
def __str__(self) -> str:
40+
return self.to_string()
41+
42+
def __repr__(self) -> str:
43+
return f"<synlink.crypto.kind kind={self}>"

0 commit comments

Comments
 (0)