Skip to content

Commit 8a3a7f3

Browse files
Update private/public keys API + fix Timestamp proto conversions
Signed-off-by: Alexander Shenshin <[email protected]>
1 parent 8c61874 commit 8a3a7f3

File tree

3 files changed

+106
-22
lines changed

3 files changed

+106
-22
lines changed

src/hedera_sdk_python/crypto/private_key.py

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ class PrivateKey:
99
Represents a private key that can be either Ed25519 or ECDSA (secp256k1).
1010
"""
1111

12-
def __init__(self, private_key):
12+
def __init__(self, private_key: ec.EllipticCurvePrivateKey | ed25519.Ed25519PrivateKey):
1313
"""
1414
Initializes a PrivateKey from a cryptography PrivateKey object.
1515
"""
@@ -56,26 +56,21 @@ def generate_ecdsa(cls):
5656
return cls(private_key)
5757

5858
@classmethod
59-
def from_string(cls, key_str):
59+
def from_bytes(cls, key_bytes: bytes):
6060
"""
61-
Load a private key from a hex-encoded string. For Ed25519, expects 32 bytes.
61+
Load a private key from bytes. For Ed25519, expects 32 bytes.
6262
For ECDSA (secp256k1), also expects 32 bytes (raw scalar).
63-
If it's DER-encoded, tries to parse and detect Ed25519 vs ECDSA.
63+
If the key is DER-encoded, tries to parse and detect Ed25519 vs ECDSA.
6464
6565
Args:
66-
key_str (str): The hex-encoded private key string.
66+
key_bytes (bytes): Private key bytes.
6767
6868
Returns:
6969
PrivateKey: A new instance of PrivateKey.
7070
7171
Raises:
7272
ValueError: If the key is invalid or unsupported.
7373
"""
74-
try:
75-
key_bytes = bytes.fromhex(key_str)
76-
except ValueError:
77-
raise ValueError("Invalid hex-encoded private key string.")
78-
7974
if len(key_bytes) == 32:
8075
try:
8176
ed_priv = ed25519.Ed25519PrivateKey.from_private_bytes(key_bytes)
@@ -104,6 +99,30 @@ def from_string(cls, key_str):
10499

105100
raise ValueError("Unsupported private key type.")
106101

102+
@classmethod
103+
def from_string(cls, key_str):
104+
"""
105+
Load a private key from a hex-encoded string. For Ed25519, expects 32 bytes.
106+
For ECDSA (secp256k1), also expects 32 bytes (raw scalar).
107+
If the key is DER-encoded, tries to parse and detect Ed25519 vs ECDSA.
108+
109+
Args:
110+
key_str (str): The hex-encoded private key string.
111+
112+
Returns:
113+
PrivateKey: A new instance of PrivateKey.
114+
115+
Raises:
116+
ValueError: If the key is invalid or unsupported.
117+
"""
118+
try:
119+
key_bytes = bytes.fromhex(key_str.removeprefix("0x"))
120+
except ValueError:
121+
raise ValueError("Invalid hex-encoded private key string.")
122+
123+
return cls.from_bytes(key_bytes)
124+
125+
107126
def sign(self, data: bytes) -> bytes:
108127
"""
109128
Signs the given data using this private key (Ed25519 or ECDSA).

src/hedera_sdk_python/crypto/public_key.py

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,76 @@ class PublicKey:
66
Represents a public key that can be either Ed25519 or ECDSA (secp256k1).
77
"""
88

9-
def __init__(self, public_key):
9+
def __init__(self, public_key: ec.EllipticCurvePublicKey | ed25519.Ed25519PublicKey):
1010
"""
1111
Initializes a PublicKey from a cryptography PublicKey object.
1212
"""
1313
self._public_key = public_key
1414

15+
@classmethod
16+
def from_bytes(cls, key_bytes: bytes):
17+
"""
18+
Load a public key from bytes.
19+
For Ed25519, expects 32 bytes. Raw bytes for ECDSA are not supported for now.
20+
If the key is DER-encoded, tries to parse and detect Ed25519 vs ECDSA.
21+
22+
Args:
23+
key_bytes (bytes): Public key bytes.
24+
25+
Returns:
26+
PublicKey: A new instance of PublicKey.
27+
28+
Raises:
29+
ValueError: If the key is invalid or unsupported.
30+
"""
31+
32+
if len(key_bytes) == 32:
33+
ed_public = ed25519.Ed25519PublicKey.from_public_bytes(key_bytes)
34+
return cls(ed_public)
35+
# TODO: Consider adding support for creating ECDSA public key instance from raw encoded point
36+
# Java SDK example: https://github.com/hiero-ledger/hiero-sdk-java/blob/main/sdk-java/src/main/java/org/hiero/sdk/java/PublicKeyECDSA.java#L46
37+
#
38+
# Potential approach for Python:
39+
# ec_public = ec.EllipticCurvePublicKey.from_encoded_point(ec.SECP256K1(), key_bytes)
40+
41+
try:
42+
public_key = serialization.load_der_public_key(key_bytes)
43+
except Exception as e:
44+
raise ValueError(f"Failed to load public key (DER): {e}")
45+
46+
if isinstance(public_key, ed25519.Ed25519PublicKey):
47+
return cls(public_key)
48+
49+
if isinstance(public_key, ec.EllipticCurvePublicKey):
50+
if not isinstance(public_key.curve, ec.SECP256K1):
51+
raise ValueError("Only secp256k1 ECDSA is supported.")
52+
return cls(public_key)
53+
54+
raise ValueError("Unsupported public key type.")
55+
56+
@classmethod
57+
def from_string(cls, key_str):
58+
"""
59+
Load a public key from a hex-encoded string.
60+
For Ed25519, expects 32 bytes. Raw bytes string for ECDSA is not supported for now.
61+
If the key is DER-encoded, tries to parse and detect Ed25519 vs ECDSA.
62+
63+
Args:
64+
key_str (str): The hex-encoded public key string.
65+
66+
Returns:
67+
PublicKey: A new instance of PublicKey.
68+
69+
Raises:
70+
ValueError: If the key is invalid or unsupported.
71+
"""
72+
try:
73+
key_bytes = bytes.fromhex(key_str.removeprefix("0x"))
74+
except ValueError:
75+
raise ValueError("Invalid hex-encoded public key string.")
76+
77+
return cls.from_bytes(key_bytes)
78+
1579
def verify(self, signature: bytes, data: bytes) -> None:
1680
"""
1781
Verifies a signature for the given data using this public key.

src/hedera_sdk_python/timestamp.py

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
from datetime import datetime, timedelta
1+
from datetime import datetime, timedelta, timezone
22
import random
33
import time
44

5+
from hedera_sdk_python.hapi.services.timestamp_pb2 import Timestamp as TimestampProto
6+
57

68
class Timestamp:
79
"""
@@ -72,7 +74,7 @@ def to_date(self) -> datetime:
7274
Returns:
7375
datetime: A `datetime` instance.
7476
"""
75-
return datetime.fromtimestamp(self.seconds) + timedelta(
77+
return datetime.fromtimestamp(self.seconds, tz=timezone.utc) + timedelta(
7678
microseconds=self.nanos // 1000
7779
)
7880

@@ -92,29 +94,28 @@ def plus_nanos(self, nanos: int) -> "Timestamp":
9294

9395
return Timestamp(new_seconds, new_nanos)
9496

95-
def to_protobuf(self) -> dict:
97+
def to_protobuf(self) -> TimestampProto:
9698
"""
97-
Convert the `Timestamp` to a protobuf-compatible dictionary.
99+
Convert the `Timestamp` to corresponding protobuf object.
98100
99101
Returns:
100-
dict: A dictionary representation of the `Timestamp`.
102+
dict: A protobuf representation of the `Timestamp`.
101103
"""
102-
return {"seconds": self.seconds, "nanos": self.nanos}
104+
return TimestampProto(seconds=self.seconds, nanos=self.nanos)
103105

104106
@staticmethod
105-
def from_protobuf(pb_obj) -> "Timestamp":
107+
def from_protobuf(pb_obj: TimestampProto) -> "Timestamp":
106108
"""
107109
Create a `Timestamp` from a protobuf object.
108110
109111
Args:
110-
pb_obj (dict): A protobuf-like dictionary with `seconds` and `nanos`.
112+
pb_obj (timestamp_pb2.Timestamp): A protobuf Timestamp object.
111113
112114
Returns:
113115
Timestamp: A `Timestamp` instance.
114116
"""
115-
seconds = pb_obj.get("seconds", 0)
116-
nanos = pb_obj.get("nanos", 0)
117-
return Timestamp(seconds, nanos)
117+
118+
return Timestamp(pb_obj.seconds, pb_obj.nanos)
118119

119120
def __str__(self) -> str:
120121
"""

0 commit comments

Comments
 (0)