neuronum 7.0.3__py3-none-any.whl → 8.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of neuronum might be problematic. Click here for more details.
- cli/main.py +144 -370
- neuronum/neuronum.py +385 -437
- neuronum-8.0.0.dist-info/METADATA +126 -0
- neuronum-8.0.0.dist-info/RECORD +10 -0
- neuronum-7.0.3.dist-info/METADATA +0 -171
- neuronum-7.0.3.dist-info/RECORD +0 -10
- {neuronum-7.0.3.dist-info → neuronum-8.0.0.dist-info}/WHEEL +0 -0
- {neuronum-7.0.3.dist-info → neuronum-8.0.0.dist-info}/entry_points.txt +0 -0
- {neuronum-7.0.3.dist-info → neuronum-8.0.0.dist-info}/licenses/LICENSE.md +0 -0
- {neuronum-7.0.3.dist-info → neuronum-8.0.0.dist-info}/top_level.txt +0 -0
neuronum/neuronum.py
CHANGED
|
@@ -1,17 +1,30 @@
|
|
|
1
1
|
import aiohttp
|
|
2
|
-
from typing import
|
|
3
|
-
import ssl
|
|
2
|
+
from typing import AsyncGenerator
|
|
4
3
|
import websockets
|
|
5
4
|
import json
|
|
6
5
|
import asyncio
|
|
6
|
+
import base64
|
|
7
|
+
import os
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from websockets.exceptions import ConnectionClosed
|
|
10
|
+
from cryptography.hazmat.primitives.asymmetric import ec
|
|
11
|
+
from cryptography.hazmat.primitives import serialization, hashes
|
|
12
|
+
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
|
13
|
+
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
|
|
14
|
+
from cryptography.hazmat.backends import default_backend
|
|
15
|
+
|
|
7
16
|
|
|
8
17
|
class Cell:
|
|
9
|
-
def __init__(self,
|
|
10
|
-
self.
|
|
11
|
-
self.
|
|
12
|
-
self.network = network
|
|
13
|
-
self.synapse = synapse
|
|
18
|
+
def __init__(self, private_key_path: str, public_key_path: str):
|
|
19
|
+
self.private_key_path = private_key_path
|
|
20
|
+
self.public_key_path = public_key_path
|
|
14
21
|
self.queue = asyncio.Queue()
|
|
22
|
+
self.host = self._load_host()
|
|
23
|
+
self.network = self._load_network()
|
|
24
|
+
self.synapse = self._load_synapse()
|
|
25
|
+
self.password = self._load_password()
|
|
26
|
+
self._private_key = self._load_private_key()
|
|
27
|
+
self._public_key = self._load_public_key()
|
|
15
28
|
|
|
16
29
|
|
|
17
30
|
def to_dict(self) -> dict:
|
|
@@ -21,186 +34,424 @@ class Cell:
|
|
|
21
34
|
"synapse": self.synapse
|
|
22
35
|
}
|
|
23
36
|
|
|
37
|
+
def _load_private_key(self):
|
|
38
|
+
try:
|
|
39
|
+
with open(self.private_key_path, "rb") as f:
|
|
40
|
+
private_key = serialization.load_pem_private_key(
|
|
41
|
+
f.read(),
|
|
42
|
+
password=None,
|
|
43
|
+
backend=default_backend()
|
|
44
|
+
)
|
|
45
|
+
print("Private key loaded successfully.")
|
|
46
|
+
return private_key
|
|
47
|
+
except FileNotFoundError:
|
|
48
|
+
print(f"Private key file not found at {self.private_key_path}.")
|
|
49
|
+
return None
|
|
50
|
+
|
|
51
|
+
def _load_host(self):
|
|
52
|
+
credentials_folder_path = Path.home() / ".neuronum"
|
|
53
|
+
env_path = credentials_folder_path / ".env"
|
|
54
|
+
|
|
55
|
+
env_data = {}
|
|
56
|
+
|
|
57
|
+
try:
|
|
58
|
+
with open(env_path, "r") as f:
|
|
59
|
+
for line in f:
|
|
60
|
+
key, value = line.strip().split("=")
|
|
61
|
+
env_data[key] = value
|
|
62
|
+
|
|
63
|
+
host = env_data.get("HOST", "")
|
|
64
|
+
return host
|
|
65
|
+
except FileNotFoundError:
|
|
66
|
+
print(f"Cell Host not found")
|
|
67
|
+
return None
|
|
68
|
+
|
|
24
69
|
|
|
25
|
-
def
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
async def stream(self, label: str, data: dict, stx: Optional[str] = None, retry_delay: int = 3):
|
|
30
|
-
context = ssl.create_default_context()
|
|
31
|
-
context.check_hostname = True
|
|
32
|
-
context.verify_mode = ssl.CERT_REQUIRED
|
|
33
|
-
|
|
34
|
-
while True:
|
|
35
|
-
try:
|
|
36
|
-
reader, writer = await asyncio.open_connection(self.network, 55555, ssl=context, server_hostname=self.network)
|
|
37
|
-
|
|
38
|
-
credentials = f"{self.host}\n{self.password}\n{self.synapse}\n{stx}\n"
|
|
39
|
-
writer.write(credentials.encode("utf-8"))
|
|
40
|
-
await writer.drain()
|
|
41
|
-
|
|
42
|
-
response = await reader.read(1024)
|
|
43
|
-
response_text = response.decode("utf-8").strip()
|
|
44
|
-
|
|
45
|
-
if "Authentication successful" not in response_text:
|
|
46
|
-
print("Authentication failed, retrying...")
|
|
47
|
-
writer.close()
|
|
48
|
-
await writer.wait_closed()
|
|
49
|
-
await asyncio.sleep(retry_delay)
|
|
50
|
-
continue
|
|
51
|
-
|
|
52
|
-
stream_payload = {
|
|
53
|
-
"label": label,
|
|
54
|
-
"data": data,
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
writer.write(json.dumps(stream_payload).encode("utf-8"))
|
|
58
|
-
await writer.drain()
|
|
59
|
-
|
|
60
|
-
response = await reader.read(1024)
|
|
61
|
-
response_text = response.decode("utf-8").strip()
|
|
62
|
-
|
|
63
|
-
if response_text == "Sent":
|
|
64
|
-
print(f"Success: {response_text} - {stream_payload}")
|
|
65
|
-
break
|
|
66
|
-
else:
|
|
67
|
-
print(f"Error sending: {stream_payload}")
|
|
68
|
-
|
|
69
|
-
except (ssl.SSLError, ConnectionError) as e:
|
|
70
|
-
print(f"Connection error: {e}, retrying...")
|
|
71
|
-
await asyncio.sleep(retry_delay)
|
|
72
|
-
|
|
73
|
-
except Exception as e:
|
|
74
|
-
print(f"Unexpected error: {e}, retrying...")
|
|
75
|
-
await asyncio.sleep(retry_delay)
|
|
70
|
+
def _load_password(self):
|
|
71
|
+
credentials_folder_path = Path.home() / ".neuronum"
|
|
72
|
+
env_path = credentials_folder_path / ".env"
|
|
76
73
|
|
|
77
|
-
|
|
78
|
-
if 'writer' in locals():
|
|
79
|
-
writer.close()
|
|
80
|
-
await writer.wait_closed()
|
|
74
|
+
env_data = {}
|
|
81
75
|
|
|
76
|
+
try:
|
|
77
|
+
with open(env_path, "r") as f:
|
|
78
|
+
for line in f:
|
|
79
|
+
key, value = line.strip().split("=")
|
|
80
|
+
env_data[key] = value
|
|
82
81
|
|
|
83
|
-
|
|
84
|
-
|
|
82
|
+
host = env_data.get("PASSWORD", "")
|
|
83
|
+
return host
|
|
84
|
+
except FileNotFoundError:
|
|
85
|
+
print(f"Cell Password not found")
|
|
86
|
+
return None
|
|
87
|
+
|
|
88
|
+
def _load_synapse(self):
|
|
89
|
+
credentials_folder_path = Path.home() / ".neuronum"
|
|
90
|
+
env_path = credentials_folder_path / ".env"
|
|
91
|
+
|
|
92
|
+
env_data = {}
|
|
93
|
+
|
|
94
|
+
try:
|
|
95
|
+
with open(env_path, "r") as f:
|
|
96
|
+
for line in f:
|
|
97
|
+
key, value = line.strip().split("=")
|
|
98
|
+
env_data[key] = value
|
|
99
|
+
|
|
100
|
+
host = env_data.get("SYNAPSE", "")
|
|
101
|
+
return host
|
|
102
|
+
except FileNotFoundError:
|
|
103
|
+
print(f"Cell Synapse not found")
|
|
104
|
+
return None
|
|
105
|
+
|
|
106
|
+
def _load_network(self):
|
|
107
|
+
credentials_folder_path = Path.home() / ".neuronum"
|
|
108
|
+
env_path = credentials_folder_path / ".env"
|
|
109
|
+
|
|
110
|
+
env_data = {}
|
|
111
|
+
|
|
112
|
+
try:
|
|
113
|
+
with open(env_path, "r") as f:
|
|
114
|
+
for line in f:
|
|
115
|
+
key, value = line.strip().split("=")
|
|
116
|
+
env_data[key] = value
|
|
117
|
+
|
|
118
|
+
host = env_data.get("NETWORK", "")
|
|
119
|
+
return host
|
|
120
|
+
except FileNotFoundError:
|
|
121
|
+
print(f"Cell Network not found")
|
|
122
|
+
return None
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _load_public_key(self):
|
|
126
|
+
try:
|
|
127
|
+
with open(self.public_key_path, "rb") as f:
|
|
128
|
+
public_key = serialization.load_pem_public_key(
|
|
129
|
+
f.read(),
|
|
130
|
+
backend=default_backend()
|
|
131
|
+
)
|
|
132
|
+
print("Public key loaded successfully.")
|
|
133
|
+
print(public_key)
|
|
134
|
+
return public_key
|
|
135
|
+
except FileNotFoundError:
|
|
136
|
+
print(f"Public key file not found at {self.public_key_path}. Deriving from private key.")
|
|
137
|
+
if self._private_key:
|
|
138
|
+
return self._private_key.public_key()
|
|
139
|
+
else:
|
|
140
|
+
return None
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def get_public_key_jwk(self):
|
|
144
|
+
public_key = self._load_public_key()
|
|
145
|
+
if not public_key:
|
|
146
|
+
print("Public key not loaded. Cannot generate JWK.")
|
|
147
|
+
return None
|
|
148
|
+
|
|
149
|
+
print("Public key loaded successfully.")
|
|
150
|
+
public_numbers = public_key.public_numbers()
|
|
151
|
+
|
|
152
|
+
x_bytes = public_numbers.x.to_bytes((public_numbers.x.bit_length() + 7) // 8, 'big')
|
|
153
|
+
y_bytes = public_numbers.y.to_bytes((public_numbers.y.bit_length() + 7) // 8, 'big')
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
"kty": "EC",
|
|
157
|
+
"crv": "P-256",
|
|
158
|
+
"x": base64.urlsafe_b64encode(x_bytes).rstrip(b'=').decode('utf-8'),
|
|
159
|
+
"y": base64.urlsafe_b64encode(y_bytes).rstrip(b'=').decode('utf-8')
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _decrypt_with_ecdh_aesgcm(self, ephemeral_public_key_bytes, nonce, ciphertext):
|
|
164
|
+
try:
|
|
165
|
+
ephemeral_public_key = ec.EllipticCurvePublicKey.from_encoded_point(
|
|
166
|
+
ec.SECP256R1(), ephemeral_public_key_bytes
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
shared_secret = self._private_key.exchange(ec.ECDH(), ephemeral_public_key)
|
|
170
|
+
|
|
171
|
+
derived_key = HKDF(
|
|
172
|
+
algorithm=hashes.SHA256(),
|
|
173
|
+
length=32,
|
|
174
|
+
salt=None,
|
|
175
|
+
info=b'handshake data'
|
|
176
|
+
).derive(shared_secret)
|
|
177
|
+
|
|
178
|
+
aesgcm = AESGCM(derived_key)
|
|
179
|
+
plaintext_bytes = aesgcm.decrypt(nonce, ciphertext, None)
|
|
180
|
+
return json.loads(plaintext_bytes.decode())
|
|
181
|
+
|
|
182
|
+
except Exception as e:
|
|
183
|
+
print(f"Decryption failed: {e}")
|
|
184
|
+
return None
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
async def sync(self, node_id: str) -> AsyncGenerator[str, None]:
|
|
188
|
+
full_url = f"wss://{self.network}/sync/{node_id}"
|
|
85
189
|
auth_payload = {
|
|
86
190
|
"host": self.host,
|
|
87
191
|
"password": self.password,
|
|
88
192
|
"synapse": self.synapse,
|
|
89
193
|
}
|
|
90
|
-
|
|
91
194
|
while True:
|
|
92
195
|
try:
|
|
93
196
|
async with websockets.connect(full_url) as ws:
|
|
94
197
|
await ws.send(json.dumps(auth_payload))
|
|
95
198
|
print("Connected to WebSocket.")
|
|
96
|
-
|
|
97
199
|
while True:
|
|
98
200
|
try:
|
|
99
201
|
raw_operation = await ws.recv()
|
|
100
202
|
operation = json.loads(raw_operation)
|
|
101
|
-
|
|
102
|
-
|
|
203
|
+
|
|
204
|
+
if "encrypted" in operation.get("data", {}):
|
|
205
|
+
encrypted_data = operation["data"]["encrypted"]
|
|
206
|
+
|
|
207
|
+
ephemeral_public_key_b64 = encrypted_data["ephemeralPublicKey"]
|
|
208
|
+
ephemeral_public_key_b64 += '=' * ((4 - len(ephemeral_public_key_b64) % 4) % 4)
|
|
209
|
+
ephemeral_public_key_bytes = base64.urlsafe_b64decode(ephemeral_public_key_b64)
|
|
210
|
+
|
|
211
|
+
nonce_b64 = encrypted_data["nonce"]
|
|
212
|
+
nonce_b64 += '=' * ((4 - len(nonce_b64) % 4) % 4)
|
|
213
|
+
nonce = base64.urlsafe_b64decode(nonce_b64)
|
|
214
|
+
|
|
215
|
+
ciphertext_b64 = encrypted_data["ciphertext"]
|
|
216
|
+
ciphertext_b64 += '=' * ((4 - len(ciphertext_b64) % 4) % 4)
|
|
217
|
+
ciphertext = base64.urlsafe_b64decode(ciphertext_b64)
|
|
218
|
+
|
|
219
|
+
decrypted_data = self._decrypt_with_ecdh_aesgcm(
|
|
220
|
+
ephemeral_public_key_bytes, nonce, ciphertext
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
if decrypted_data:
|
|
224
|
+
operation["data"].update(decrypted_data)
|
|
225
|
+
operation["data"].pop("encrypted")
|
|
226
|
+
yield operation
|
|
227
|
+
else:
|
|
228
|
+
print("Failed to decrypt incoming data. Skipping...")
|
|
229
|
+
else:
|
|
230
|
+
yield operation
|
|
103
231
|
except asyncio.TimeoutError:
|
|
104
232
|
print("No data received. Continuing...")
|
|
105
|
-
except
|
|
106
|
-
|
|
233
|
+
except ConnectionClosed as e:
|
|
234
|
+
if e.code == 1000:
|
|
235
|
+
print(f"WebSocket closed cleanly (code 1000). Reconnecting...")
|
|
236
|
+
else:
|
|
237
|
+
print(f"Connection closed with error code {e.code}: {e.reason}. Reconnecting...")
|
|
107
238
|
break
|
|
108
239
|
except Exception as e:
|
|
109
240
|
print(f"Unexpected error in recv loop: {e}")
|
|
110
241
|
break
|
|
111
|
-
|
|
112
242
|
except websockets.exceptions.WebSocketException as e:
|
|
113
243
|
print(f"WebSocket error occurred: {e}. Retrying in 5 seconds...")
|
|
114
244
|
except Exception as e:
|
|
115
245
|
print(f"General error occurred: {e}. Retrying in 5 seconds...")
|
|
116
|
-
|
|
117
246
|
await asyncio.sleep(3)
|
|
118
|
-
|
|
119
247
|
|
|
120
|
-
async def create_tx(self, descr: str, key_values: dict, stx: str, label: str, partners: list):
|
|
121
|
-
url = f"https://{self.network}/api/create_tx"
|
|
122
248
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
"stx": stx,
|
|
127
|
-
"label": label,
|
|
128
|
-
"partners": partners,
|
|
249
|
+
async def list_nodes(self):
|
|
250
|
+
full_url = f"https://{self.network}/api/list_nodes"
|
|
251
|
+
list_nodes_payload = {
|
|
129
252
|
"cell": self.to_dict()
|
|
130
253
|
}
|
|
131
|
-
|
|
132
254
|
async with aiohttp.ClientSession() as session:
|
|
133
255
|
try:
|
|
134
|
-
async with session.post(
|
|
256
|
+
async with session.post(full_url, json=list_nodes_payload) as response:
|
|
135
257
|
response.raise_for_status()
|
|
136
258
|
data = await response.json()
|
|
137
|
-
return data
|
|
138
|
-
|
|
259
|
+
return data.get("Nodes", [])
|
|
139
260
|
except aiohttp.ClientError as e:
|
|
140
261
|
print(f"Error sending request: {e}")
|
|
141
262
|
except Exception as e:
|
|
142
263
|
print(f"Unexpected error: {e}")
|
|
143
264
|
|
|
144
265
|
|
|
145
|
-
async def
|
|
146
|
-
url = f"https://{self.network}/api/
|
|
266
|
+
async def activate_tx(self, node_id: str, data: dict):
|
|
267
|
+
url = f"https://{self.network}/api/activate_tx/{node_id}"
|
|
268
|
+
|
|
269
|
+
nodes = await self.list_nodes()
|
|
147
270
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
"cell": self.to_dict()
|
|
151
|
-
}
|
|
271
|
+
target_public_key_pem = None
|
|
272
|
+
target_node = node_id
|
|
152
273
|
|
|
153
|
-
|
|
154
|
-
|
|
274
|
+
for node in nodes:
|
|
275
|
+
node_ids = node.get('config', {}).get('data_gateways', [])
|
|
276
|
+
for node_id_entry in node_ids:
|
|
277
|
+
if node_id_entry.get('node_id') == target_node:
|
|
278
|
+
target_public_key_pem = node.get('config', {}).get('public_key')
|
|
279
|
+
break
|
|
280
|
+
if target_public_key_pem:
|
|
281
|
+
break
|
|
282
|
+
|
|
283
|
+
if not target_public_key_pem:
|
|
284
|
+
print(f"Target node not found or public key is missing.")
|
|
285
|
+
return None
|
|
286
|
+
|
|
287
|
+
try:
|
|
288
|
+
print(target_public_key_pem)
|
|
289
|
+
partner_public_key_jwk = self._load_public_key_from_pem(target_public_key_pem)
|
|
290
|
+
if not partner_public_key_jwk:
|
|
291
|
+
print("Failed to convert public key to JWK format. Aborting.")
|
|
292
|
+
return None
|
|
293
|
+
|
|
294
|
+
partner_public_key = self._load_public_key_from_jwk(partner_public_key_jwk)
|
|
295
|
+
if not partner_public_key:
|
|
296
|
+
print("Failed to load partner's public key. Aborting.")
|
|
297
|
+
return None
|
|
298
|
+
|
|
299
|
+
data_to_encrypt = data.copy()
|
|
300
|
+
data_to_encrypt["publicKey"] = self.get_public_key_jwk()
|
|
301
|
+
|
|
302
|
+
encrypted_payload = self._encrypt_with_ecdh_aesgcm(partner_public_key, data_to_encrypt)
|
|
303
|
+
|
|
304
|
+
TX = {
|
|
305
|
+
"data": {
|
|
306
|
+
"encrypted": encrypted_payload
|
|
307
|
+
},
|
|
308
|
+
"cell": self.to_dict()
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
async with aiohttp.ClientSession() as session:
|
|
155
312
|
async with session.post(url, json=TX) as response:
|
|
313
|
+
if response.status == 504:
|
|
314
|
+
print(f"Gateway Timeout: No response from transmitter for txID: {node_id}")
|
|
315
|
+
return None
|
|
316
|
+
|
|
156
317
|
response.raise_for_status()
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
318
|
+
|
|
319
|
+
response_data = await response.json()
|
|
320
|
+
|
|
321
|
+
if "response" in response_data:
|
|
322
|
+
inner_response = response_data["response"]
|
|
323
|
+
|
|
324
|
+
if "ciphertext" in inner_response:
|
|
325
|
+
ephemeral_public_key_b64 = inner_response["ephemeralPublicKey"]
|
|
326
|
+
ephemeral_public_key_b64 += '=' * ((4 - len(ephemeral_public_key_b64) % 4) % 4)
|
|
327
|
+
ephemeral_public_key_bytes = base64.urlsafe_b64decode(ephemeral_public_key_b64)
|
|
328
|
+
|
|
329
|
+
nonce_b64 = inner_response["nonce"]
|
|
330
|
+
nonce_b64 += '=' * ((4 - len(nonce_b64) % 4) % 4)
|
|
331
|
+
nonce = base64.urlsafe_b64decode(nonce_b64)
|
|
332
|
+
|
|
333
|
+
ciphertext_b64 = inner_response["ciphertext"]
|
|
334
|
+
ciphertext_b64 += '=' * ((4 - len(ciphertext_b64) % 4) % 4)
|
|
335
|
+
ciphertext = base64.urlsafe_b64decode(ciphertext_b64)
|
|
336
|
+
|
|
337
|
+
decrypted_response = self._decrypt_with_ecdh_aesgcm(
|
|
338
|
+
ephemeral_public_key_bytes, nonce, ciphertext
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
if decrypted_response:
|
|
342
|
+
return decrypted_response
|
|
343
|
+
else:
|
|
344
|
+
print("Failed to decrypt server response.")
|
|
345
|
+
return None
|
|
346
|
+
else:
|
|
347
|
+
print("Server response was not encrypted as expected.")
|
|
348
|
+
return inner_response
|
|
160
349
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
350
|
+
else:
|
|
351
|
+
print("Unexpected response format.")
|
|
352
|
+
return response_data
|
|
353
|
+
|
|
354
|
+
except aiohttp.ClientResponseError as e:
|
|
355
|
+
print(f"HTTP Error: {e.status}, Message: {e.message}, URL: {e.request_info.url}")
|
|
356
|
+
return None
|
|
357
|
+
|
|
358
|
+
except aiohttp.ClientError as e:
|
|
359
|
+
print(f"Connection Error: {e}")
|
|
360
|
+
return None
|
|
361
|
+
|
|
362
|
+
except Exception as e:
|
|
363
|
+
print(f"Unexpected error: {e}")
|
|
364
|
+
return None
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def _load_public_key_from_jwk(self, jwk):
|
|
368
|
+
try:
|
|
369
|
+
print(jwk)
|
|
370
|
+
x = base64.urlsafe_b64decode(jwk['x'] + '==')
|
|
371
|
+
y = base64.urlsafe_b64decode(jwk['y'] + '==')
|
|
372
|
+
public_numbers = ec.EllipticCurvePublicNumbers(
|
|
373
|
+
int.from_bytes(x, 'big'),
|
|
374
|
+
int.from_bytes(y, 'big'),
|
|
375
|
+
ec.SECP256R1()
|
|
376
|
+
)
|
|
377
|
+
return public_numbers.public_key(default_backend())
|
|
378
|
+
except (KeyError, ValueError, TypeError) as e:
|
|
379
|
+
print(f"Error loading public key from JWK string: {e}")
|
|
380
|
+
return None
|
|
381
|
+
|
|
166
382
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
"
|
|
171
|
-
"
|
|
383
|
+
def _load_public_key_from_pem(self, pem_string: str):
|
|
384
|
+
try:
|
|
385
|
+
print(pem_string)
|
|
386
|
+
corrected_pem_string = pem_string.replace("-----BEGINPUBLICKEY-----", "-----BEGIN PUBLIC KEY-----")
|
|
387
|
+
corrected_pem_string = corrected_pem_string.replace("-----ENDPUBLICKEY-----", "-----END PUBLIC KEY-----")
|
|
388
|
+
|
|
389
|
+
public_key = serialization.load_pem_public_key(
|
|
390
|
+
corrected_pem_string.encode(),
|
|
391
|
+
backend=default_backend()
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
public_numbers = public_key.public_numbers()
|
|
395
|
+
x_bytes = public_numbers.x.to_bytes((public_numbers.x.bit_length() + 7) // 8, 'big')
|
|
396
|
+
y_bytes = public_numbers.y.to_bytes((public_numbers.y.bit_length() + 7) // 8, 'big')
|
|
397
|
+
return {
|
|
398
|
+
"kty": "EC",
|
|
399
|
+
"crv": "P-256",
|
|
400
|
+
"x": base64.urlsafe_b64encode(x_bytes).rstrip(b'=').decode('utf-8'),
|
|
401
|
+
"y": base64.urlsafe_b64encode(y_bytes).rstrip(b'=').decode('utf-8')
|
|
402
|
+
}
|
|
403
|
+
except Exception as e:
|
|
404
|
+
print(f"Error loading public key from PEM string: {e}")
|
|
405
|
+
return None
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
def _encrypt_with_ecdh_aesgcm(self, public_key, plaintext_dict):
|
|
409
|
+
ephemeral_private = ec.generate_private_key(ec.SECP256R1())
|
|
410
|
+
ephemeral_public = ephemeral_private.public_key()
|
|
411
|
+
shared_secret = ephemeral_private.exchange(ec.ECDH(), public_key)
|
|
412
|
+
derived_key = HKDF(
|
|
413
|
+
algorithm=hashes.SHA256(),
|
|
414
|
+
length=32,
|
|
415
|
+
salt=None,
|
|
416
|
+
info=b'handshake data'
|
|
417
|
+
).derive(shared_secret)
|
|
418
|
+
aesgcm = AESGCM(derived_key)
|
|
419
|
+
nonce = os.urandom(12)
|
|
420
|
+
plaintext_bytes = json.dumps(plaintext_dict).encode()
|
|
421
|
+
ciphertext = aesgcm.encrypt(nonce, plaintext_bytes, None)
|
|
422
|
+
ephemeral_public_bytes = ephemeral_public.public_bytes(
|
|
423
|
+
serialization.Encoding.X962,
|
|
424
|
+
serialization.PublicFormat.UncompressedPoint
|
|
425
|
+
)
|
|
426
|
+
return {
|
|
427
|
+
'ciphertext': base64.b64encode(ciphertext).decode(),
|
|
428
|
+
'nonce': base64.b64encode(nonce).decode(),
|
|
429
|
+
'ephemeralPublicKey': base64.b64encode(ephemeral_public_bytes).decode()
|
|
172
430
|
}
|
|
431
|
+
|
|
173
432
|
|
|
174
|
-
|
|
433
|
+
async def tx_response(self, transmitter_id: str, data: dict, client_public_key_str):
|
|
434
|
+
if isinstance(client_public_key_str, str):
|
|
175
435
|
try:
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
except Exception as e:
|
|
192
|
-
print(f"Unexpected error: {e}")
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
async def tx_response(self, txID: str, client: str, data: dict):
|
|
196
|
-
url = f"https://{self.network}/api/tx_response/{txID}"
|
|
197
|
-
|
|
436
|
+
client_public_key_jwk = json.loads(client_public_key_str)
|
|
437
|
+
except json.JSONDecodeError:
|
|
438
|
+
print("Failed to decode client public key from string. Aborting response.")
|
|
439
|
+
return
|
|
440
|
+
elif isinstance(client_public_key_str, dict):
|
|
441
|
+
client_public_key_jwk = client_public_key_str
|
|
442
|
+
else:
|
|
443
|
+
print("Invalid type for client public key. Expected str or dict. Aborting response.")
|
|
444
|
+
return
|
|
445
|
+
public_key = self._load_public_key_from_jwk(client_public_key_jwk)
|
|
446
|
+
if not public_key:
|
|
447
|
+
print("Failed to load public key. Aborting response.")
|
|
448
|
+
return
|
|
449
|
+
encrypted_payload = self._encrypt_with_ecdh_aesgcm(public_key, data)
|
|
450
|
+
url = f"https://{self.network}/api/tx_response/{transmitter_id}"
|
|
198
451
|
tx_response = {
|
|
199
|
-
"
|
|
200
|
-
"data": data,
|
|
452
|
+
"data": encrypted_payload,
|
|
201
453
|
"cell": self.to_dict()
|
|
202
454
|
}
|
|
203
|
-
|
|
204
455
|
async with aiohttp.ClientSession() as session:
|
|
205
456
|
try:
|
|
206
457
|
for _ in range(2):
|
|
@@ -208,312 +459,9 @@ class Cell:
|
|
|
208
459
|
response.raise_for_status()
|
|
209
460
|
data = await response.json()
|
|
210
461
|
print(data["message"])
|
|
211
|
-
|
|
212
|
-
except aiohttp.ClientError as e:
|
|
213
|
-
print(f"Error sending request: {e}")
|
|
214
|
-
except Exception as e:
|
|
215
|
-
print(f"Unexpected error: {e}")
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
async def create_ctx(self, descr: str, partners: list):
|
|
219
|
-
url = f"https://{self.network}/api/create_ctx"
|
|
220
|
-
|
|
221
|
-
CTX = {
|
|
222
|
-
"descr": descr,
|
|
223
|
-
"partners": partners,
|
|
224
|
-
"cell": self.to_dict()
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
async with aiohttp.ClientSession() as session:
|
|
228
|
-
try:
|
|
229
|
-
async with session.post(url, json=CTX) as response:
|
|
230
|
-
response.raise_for_status()
|
|
231
|
-
data = await response.json()
|
|
232
|
-
return data["ctxID"]
|
|
233
|
-
|
|
234
|
-
except aiohttp.ClientError as e:
|
|
235
|
-
print(f"Error sending request: {e}")
|
|
236
|
-
except Exception as e:
|
|
237
|
-
print(f"Unexpected error: {e}")
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
async def delete_ctx(self, ctxID: str):
|
|
241
|
-
url = f"https://{self.network}/api/delete_ctx"
|
|
242
|
-
|
|
243
|
-
CTX = {
|
|
244
|
-
"ctxID": ctxID,
|
|
245
|
-
"cell": self.to_dict()
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
async with aiohttp.ClientSession() as session:
|
|
249
|
-
try:
|
|
250
|
-
async with session.post(url, json=CTX) as response:
|
|
251
|
-
response.raise_for_status()
|
|
252
|
-
data = await response.json()
|
|
253
|
-
print(f"Response from Neuronum: {data}")
|
|
254
|
-
return data
|
|
255
|
-
|
|
256
|
-
except aiohttp.ClientError as e:
|
|
257
|
-
print(f"Error sending request: {e}")
|
|
258
|
-
except Exception as e:
|
|
259
|
-
print(f"Unexpected error: {e}")
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
async def create_stx(self, descr: str, partners: list):
|
|
263
|
-
url = f"https://{self.network}/api/create_stx"
|
|
264
|
-
|
|
265
|
-
STX = {
|
|
266
|
-
"descr": descr,
|
|
267
|
-
"partners": partners,
|
|
268
|
-
"cell": self.to_dict()
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
async with aiohttp.ClientSession() as session:
|
|
272
|
-
try:
|
|
273
|
-
async with session.post(url, json=STX) as response:
|
|
274
|
-
response.raise_for_status()
|
|
275
|
-
data = await response.json()
|
|
276
|
-
return data["stxID"]
|
|
277
|
-
|
|
278
|
-
except aiohttp.ClientError as e:
|
|
279
|
-
print(f"Error sending request: {e}")
|
|
280
|
-
except Exception as e:
|
|
281
|
-
print(f"Unexpected error: {e}")
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
async def delete_stx(self, stxID: str):
|
|
285
|
-
url = f"https://{self.network}/api/delete_stx"
|
|
286
|
-
|
|
287
|
-
STX = {
|
|
288
|
-
"stxID": stxID,
|
|
289
|
-
"cell": self.to_dict()
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
async with aiohttp.ClientSession() as session:
|
|
293
|
-
try:
|
|
294
|
-
async with session.post(url, json=STX) as response:
|
|
295
|
-
response.raise_for_status()
|
|
296
|
-
data = await response.json()
|
|
297
|
-
print(f"Response from Neuronum: {data}")
|
|
298
|
-
return data
|
|
299
|
-
|
|
300
|
-
except aiohttp.ClientError as e:
|
|
301
|
-
print(f"Error sending request: {e}")
|
|
302
|
-
except Exception as e:
|
|
303
|
-
print(f"Unexpected error: {e}")
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
async def list_cells(self):
|
|
307
|
-
full_url = f"https://{self.network}/api/list_cells"
|
|
308
|
-
|
|
309
|
-
list_cells_payload = {
|
|
310
|
-
"cell": self.to_dict()
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
async with aiohttp.ClientSession() as session:
|
|
314
|
-
try:
|
|
315
|
-
async with session.get(full_url, json=list_cells_payload) as response:
|
|
316
|
-
response.raise_for_status()
|
|
317
|
-
data = await response.json()
|
|
318
|
-
return data.get("Cells", [])
|
|
319
|
-
|
|
320
|
-
except aiohttp.ClientError as e:
|
|
321
|
-
print(f"Error sending request: {e}")
|
|
322
|
-
except Exception as e:
|
|
323
|
-
print(f"Unexpected error: {e}")
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
async def list_tx(self):
|
|
327
|
-
full_url = f"https://{self.network}/api/list_tx"
|
|
328
|
-
|
|
329
|
-
list_tx_payload = {
|
|
330
|
-
"cell": self.to_dict()
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
async with aiohttp.ClientSession() as session:
|
|
334
|
-
try:
|
|
335
|
-
async with session.get(full_url, json=list_tx_payload) as response:
|
|
336
|
-
response.raise_for_status()
|
|
337
|
-
data = await response.json()
|
|
338
|
-
return data.get("Transmitters", [])
|
|
339
|
-
|
|
340
462
|
except aiohttp.ClientError as e:
|
|
341
463
|
print(f"Error sending request: {e}")
|
|
342
464
|
except Exception as e:
|
|
343
465
|
print(f"Unexpected error: {e}")
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
async def list_ctx(self):
|
|
347
|
-
full_url = f"https://{self.network}/api/list_ctx"
|
|
348
|
-
|
|
349
|
-
list_ctx_payload = {
|
|
350
|
-
"cell": self.to_dict()
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
async with aiohttp.ClientSession() as session:
|
|
354
|
-
try:
|
|
355
|
-
async with session.get(full_url, json=list_ctx_payload) as response:
|
|
356
|
-
response.raise_for_status()
|
|
357
|
-
data = await response.json()
|
|
358
|
-
return data.get("Circuits", [])
|
|
359
|
-
|
|
360
|
-
except aiohttp.ClientError as e:
|
|
361
|
-
print(f"Error sending request: {e}")
|
|
362
|
-
except Exception as e:
|
|
363
|
-
print(f"Unexpected error: {e}")
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
async def list_stx(self):
|
|
367
|
-
full_url = f"https://{self.network}/api/list_stx"
|
|
368
|
-
|
|
369
|
-
list_stx_payload = {
|
|
370
|
-
"cell": self.to_dict()
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
async with aiohttp.ClientSession() as session:
|
|
374
|
-
try:
|
|
375
|
-
async with session.get(full_url, json=list_stx_payload) as response:
|
|
376
|
-
response.raise_for_status()
|
|
377
|
-
data = await response.json()
|
|
378
|
-
return data.get("Streams", [])
|
|
379
|
-
|
|
380
|
-
except aiohttp.ClientError as e:
|
|
381
|
-
print(f"Error sending request: {e}")
|
|
382
|
-
except Exception as e:
|
|
383
|
-
print(f"Unexpected error: {e}")
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
async def list_nodes(self):
|
|
387
|
-
full_url = f"https://{self.network}/api/list_nodes"
|
|
388
|
-
|
|
389
|
-
list_nodes_payload = {
|
|
390
|
-
"cell": self.to_dict()
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
async with aiohttp.ClientSession() as session:
|
|
394
|
-
try:
|
|
395
|
-
async with session.get(full_url, json=list_nodes_payload) as response:
|
|
396
|
-
response.raise_for_status()
|
|
397
|
-
data = await response.json()
|
|
398
|
-
return data.get("Nodes", [])
|
|
399
|
-
|
|
400
|
-
except aiohttp.ClientError as e:
|
|
401
|
-
print(f"Error sending request: {e}")
|
|
402
|
-
except Exception as e:
|
|
403
|
-
print(f"Unexpected error: {e}")
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
async def store(self, label: str, data: dict, ctx: Optional[str] = None):
|
|
407
|
-
full_url = f"https://{self.network}/api/store_in_ctx/{ctx}" if ctx else f"https://{self.network}/api/store"
|
|
408
|
-
|
|
409
|
-
store_payload = {
|
|
410
|
-
"label": label,
|
|
411
|
-
"data": data,
|
|
412
|
-
"cell": self.to_dict()
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
async with aiohttp.ClientSession() as session:
|
|
416
|
-
try:
|
|
417
|
-
async with session.post(full_url, json=store_payload) as response:
|
|
418
|
-
response.raise_for_status()
|
|
419
|
-
data = await response.json()
|
|
420
|
-
print(f"Response from Neuronum: {data}")
|
|
421
|
-
return data
|
|
422
|
-
|
|
423
|
-
except aiohttp.ClientError as e:
|
|
424
|
-
print(f"Error sending request: {e}")
|
|
425
|
-
except Exception as e:
|
|
426
|
-
print(f"Unexpected error: {e}")
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
async def load(self, label: str, ctx: Optional[str] = None):
|
|
430
|
-
full_url = f"https://{self.network}/api/load_from_ctx/{ctx}" if ctx else f"https://{self.network}/api/load"
|
|
431
|
-
|
|
432
|
-
load_payload = {
|
|
433
|
-
"label": label,
|
|
434
|
-
"cell": self.to_dict()
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
async with aiohttp.ClientSession() as session:
|
|
438
|
-
try:
|
|
439
|
-
async with session.post(full_url, json=load_payload) as response:
|
|
440
|
-
response.raise_for_status()
|
|
441
|
-
data = await response.json()
|
|
442
|
-
return data
|
|
443
|
-
|
|
444
|
-
except aiohttp.ClientError as e:
|
|
445
|
-
print(f"Error sending request: {e}")
|
|
446
|
-
except Exception as e:
|
|
447
|
-
print(f"Unexpected error: {e}")
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
async def delete(self, label: str, ctx: Optional[str] = None):
|
|
451
|
-
full_url = f"https://{self.network}/api/delete_from_ctx/{ctx}" if ctx else f"https://{self.network}/api/delete"
|
|
452
|
-
|
|
453
|
-
delete_payload = {
|
|
454
|
-
"label": label,
|
|
455
|
-
"cell": self.to_dict()
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
async with aiohttp.ClientSession() as session:
|
|
459
|
-
try:
|
|
460
|
-
async with session.post(full_url, json=delete_payload) as response:
|
|
461
|
-
response.raise_for_status()
|
|
462
|
-
data = await response.json()
|
|
463
|
-
print(f"Response from Neuronum: {data}")
|
|
464
|
-
return data
|
|
465
|
-
|
|
466
|
-
except aiohttp.ClientError as e:
|
|
467
|
-
print(f"Error sending request: {e}")
|
|
468
|
-
except Exception as e:
|
|
469
|
-
print(f"Unexpected error: {e}")
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
async def clear(self, ctx: Optional[str] = None):
|
|
473
|
-
full_url = f"https://{self.network}/api/clear_ctx/{ctx}" if ctx else f"https://{self.network}/api/clear"
|
|
474
|
-
|
|
475
|
-
clear_payload = {
|
|
476
|
-
"cell": self.to_dict()
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
async with aiohttp.ClientSession() as session:
|
|
480
|
-
try:
|
|
481
|
-
async with session.post(full_url, json=clear_payload) as response:
|
|
482
|
-
response.raise_for_status()
|
|
483
|
-
data = await response.json()
|
|
484
|
-
print(f"Response from Neuronum: {data}")
|
|
485
|
-
return data
|
|
486
|
-
|
|
487
|
-
except aiohttp.ClientError as e:
|
|
488
|
-
print(f"Error sending request: {e}")
|
|
489
|
-
except Exception as e:
|
|
490
|
-
print(f"Unexpected error: {e}")
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
async def notify(self, receiver: str, title: str, message: str):
|
|
494
|
-
full_url = f"https://{self.network}/api/notify"
|
|
495
|
-
|
|
496
|
-
notify_payload = {
|
|
497
|
-
"receiver": receiver,
|
|
498
|
-
"notification": {
|
|
499
|
-
"title": title,
|
|
500
|
-
"message": message
|
|
501
|
-
},
|
|
502
|
-
"cell": self.to_dict()
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
async with aiohttp.ClientSession() as session:
|
|
506
|
-
try:
|
|
507
|
-
async with session.post(full_url, json=notify_payload) as response:
|
|
508
|
-
response.raise_for_status()
|
|
509
|
-
data = await response.json()
|
|
510
|
-
print(f"Notification sent successfully: {data}")
|
|
511
|
-
return data
|
|
512
|
-
|
|
513
|
-
except aiohttp.ClientError as e:
|
|
514
|
-
print(f"HTTP error while sending notification: {e}")
|
|
515
|
-
except Exception as e:
|
|
516
|
-
print(f"Unexpected error: {e}")
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
__all__ = ['Cell']
|
|
466
|
+
|
|
467
|
+
__all__ = ['Cell']
|