neuronum 8.4.0__tar.gz → 9.0.0__tar.gz

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.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: neuronum
3
- Version: 8.4.0
3
+ Version: 9.0.0
4
4
  Summary: The E2E Web Engine
5
5
  Home-page: https://neuronum.net
6
6
  Author: Neuronum Cybernetics
@@ -359,7 +359,7 @@ async def main():
359
359
  data = transmitter.get("data")
360
360
  transmitter_id = transmitter.get("transmitter_id")
361
361
  client = transmitter.get("operator")
362
- client_public_key = data.get("publicKey")
362
+ client_public_key = data.get("publicKey", {{}})
363
363
  action = data.get("action")
364
364
 
365
365
  response_data = {{}}
@@ -373,7 +373,10 @@ async def main():
373
373
  "html": html_content
374
374
  }}
375
375
 
376
- await node.tx_response(transmitter_id, response_data, client_public_key)
376
+ if not client_public_key:
377
+ await node.tx_response(transmitter_id, response_data, encrypted=False)
378
+ else:
379
+ await node.tx_response(transmitter_id, response_data, client_public_key)
377
380
 
378
381
  asyncio.run(main())
379
382
  """)
@@ -0,0 +1,364 @@
1
+ import aiohttp
2
+ from typing import Optional, AsyncGenerator, Union
3
+ import websockets
4
+ import json
5
+ import asyncio
6
+ import base64
7
+ import os
8
+ import ssl
9
+ from pathlib import Path
10
+ from websockets.exceptions import ConnectionClosed
11
+ from cryptography.hazmat.primitives.asymmetric import ec
12
+ from cryptography.hazmat.primitives import serialization, hashes
13
+ from cryptography.hazmat.primitives.ciphers.aead import AESGCM
14
+ from cryptography.hazmat.primitives.kdf.hkdf import HKDF
15
+ from cryptography.hazmat.backends import default_backend
16
+
17
+ class Node:
18
+ def __init__(self, id: str, private_key: str, public_key: str):
19
+ self.node_id = id
20
+ self.private_key_path = private_key
21
+ self.public_key_path = public_key
22
+ self.queue = asyncio.Queue()
23
+ self.env = self._load_env()
24
+ self.host = self.env.get("HOST", "")
25
+ self.network = self.env.get("NETWORK", "")
26
+ self.synapse = self.env.get("SYNAPSE", "")
27
+ self.password = self.env.get("PASSWORD", "")
28
+ self._private_key = self._load_private_key()
29
+ self._public_key = self._load_public_key()
30
+
31
+
32
+ def to_dict(self) -> dict:
33
+ return {
34
+ "host": self.host,
35
+ "password": self.password,
36
+ "synapse": self.synapse
37
+ }
38
+
39
+
40
+ def _load_env(self) -> dict:
41
+ credentials_folder_path = Path.home() / ".neuronum"
42
+ env_path = credentials_folder_path / ".env"
43
+ env_data = {}
44
+ try:
45
+ with open(env_path, "r") as f:
46
+ for line in f:
47
+ key, value = line.strip().split("=")
48
+ env_data[key] = value
49
+ return env_data
50
+ except FileNotFoundError:
51
+ print(f"Cell credentials (.env) not found at {env_path}")
52
+ return {}
53
+
54
+
55
+ def _load_private_key(self):
56
+ try:
57
+ with open(self.private_key_path, "rb") as f:
58
+ return serialization.load_pem_private_key(f.read(), password=None, backend=default_backend())
59
+ except FileNotFoundError:
60
+ print(f"Private key file not found at {self.private_key_path}.")
61
+ return None
62
+
63
+
64
+ def _load_public_key(self):
65
+ try:
66
+ with open(self.public_key_path, "rb") as f:
67
+ return serialization.load_pem_public_key(f.read(), backend=default_backend())
68
+ except FileNotFoundError:
69
+ print(f"Public key file not found. Deriving from private key.")
70
+ return self._private_key.public_key() if self._private_key else None
71
+
72
+
73
+ def get_public_key_jwk(self):
74
+ public_key = self._load_public_key()
75
+ if not public_key:
76
+ print("Public key not loaded. Cannot generate JWK.")
77
+ return None
78
+ public_numbers = public_key.public_numbers()
79
+ x_bytes = public_numbers.x.to_bytes((public_numbers.x.bit_length() + 7) // 8, 'big')
80
+ y_bytes = public_numbers.y.to_bytes((public_numbers.y.bit_length() + 7) // 8, 'big')
81
+ return {
82
+ "kty": "EC",
83
+ "crv": "P-256",
84
+ "x": base64.urlsafe_b64encode(x_bytes).rstrip(b'=').decode('utf-8'),
85
+ "y": base64.urlsafe_b64encode(y_bytes).rstrip(b'=').decode('utf-8')
86
+ }
87
+
88
+
89
+ def _decrypt_with_ecdh_aesgcm(self, ephemeral_public_key_bytes, nonce, ciphertext):
90
+ try:
91
+ ephemeral_public_key = ec.EllipticCurvePublicKey.from_encoded_point(
92
+ ec.SECP256R1(), ephemeral_public_key_bytes
93
+ )
94
+ shared_secret = self._private_key.exchange(ec.ECDH(), ephemeral_public_key)
95
+ derived_key = HKDF(algorithm=hashes.SHA256(), length=32, salt=None, info=b'handshake data').derive(shared_secret)
96
+ aesgcm = AESGCM(derived_key)
97
+ plaintext_bytes = aesgcm.decrypt(nonce, ciphertext, None)
98
+ return json.loads(plaintext_bytes.decode())
99
+ except Exception as e:
100
+ print(f"Decryption failed: {e}")
101
+ return None
102
+
103
+
104
+ def _encrypt_with_ecdh_aesgcm(self, public_key, plaintext_dict):
105
+ ephemeral_private = ec.generate_private_key(ec.SECP256R1())
106
+ shared_secret = ephemeral_private.exchange(ec.ECDH(), public_key)
107
+ derived_key = HKDF(algorithm=hashes.SHA256(), length=32, salt=None, info=b'handshake data').derive(shared_secret)
108
+ aesgcm = AESGCM(derived_key)
109
+ nonce = os.urandom(12)
110
+ plaintext_bytes = json.dumps(plaintext_dict).encode()
111
+ ciphertext = aesgcm.encrypt(nonce, plaintext_bytes, None)
112
+ ephemeral_public_bytes = ephemeral_private.public_key().public_bytes(
113
+ serialization.Encoding.X962, serialization.PublicFormat.UncompressedPoint
114
+ )
115
+ return {
116
+ 'ciphertext': base64.urlsafe_b64encode(ciphertext).rstrip(b'=').decode(),
117
+ 'nonce': base64.urlsafe_b64encode(nonce).rstrip(b'=').decode(),
118
+ 'ephemeralPublicKey': base64.urlsafe_b64encode(ephemeral_public_bytes).rstrip(b'=').decode()
119
+ }
120
+
121
+
122
+ def _load_public_key_from_jwk(self, jwk):
123
+ try:
124
+ x_bytes = base64.urlsafe_b64decode(jwk['x'] + '==')
125
+ y_bytes = base64.urlsafe_b64decode(jwk['y'] + '==')
126
+ public_numbers = ec.EllipticCurvePublicNumbers(
127
+ int.from_bytes(x_bytes, 'big'),
128
+ int.from_bytes(y_bytes, 'big'),
129
+ ec.SECP256R1()
130
+ )
131
+ return public_numbers.public_key(default_backend())
132
+ except Exception as e:
133
+ print(f"Error loading public key from JWK: {e}")
134
+ return None
135
+
136
+
137
+ def _load_public_key_from_pem(self, pem_string: str):
138
+ try:
139
+ corrected_pem = pem_string.replace("-----BEGINPUBLICKEY-----", "-----BEGIN PUBLIC KEY-----") \
140
+ .replace("-----ENDPUBLICKEY-----", "-----END PUBLIC KEY-----")
141
+ public_key = serialization.load_pem_public_key(corrected_pem.encode(), backend=default_backend())
142
+ return public_key
143
+ except Exception as e:
144
+ print(f"Error loading public key from PEM: {e}")
145
+ return None
146
+
147
+
148
+ async def _get_target_node_public_key(self, node_id: str):
149
+ nodes = await self.list_nodes()
150
+ for node in nodes:
151
+ app_metadata = node.get('config', {}).get('app_metadata', {})
152
+ if app_metadata.get('node_id') == node_id:
153
+ pem = node.get('config', {}).get('public_key')
154
+ if not pem:
155
+ print(f"Public key missing for node: {node_id}")
156
+ return None
157
+ public_key = self._load_public_key_from_pem(pem)
158
+ if not public_key:
159
+ return None
160
+ return public_key
161
+ print(f"Target node not found: {node_id}")
162
+ return None
163
+
164
+
165
+ async def _post_request(self, url, payload):
166
+ async with aiohttp.ClientSession() as session:
167
+ try:
168
+ async with session.post(url, json=payload) as response:
169
+ response.raise_for_status()
170
+ return await response.json()
171
+ except aiohttp.ClientError as e:
172
+ print(f"HTTP Error: {e.status}, URL: {url}")
173
+ except Exception as e:
174
+ print(f"Unexpected error: {e}")
175
+ return None
176
+
177
+
178
+ async def list_nodes(self):
179
+ full_url = f"https://{self.network}/api/list_nodes"
180
+ payload = {"cell": self.to_dict()}
181
+ data = await self._post_request(full_url, payload)
182
+ return data.get("Nodes", []) if data else []
183
+
184
+
185
+ async def tx_response(self, transmitter_id: str, data: dict, client_public_key_str: Optional[Union[str, dict]] = None, encrypted: Optional[bool] = True):
186
+ url = f"https://{self.network}/api/tx_response/{transmitter_id}"
187
+
188
+ if encrypted:
189
+ if not client_public_key_str:
190
+ print("Error: client_public_key_str is required for encrypted responses.")
191
+ return
192
+
193
+ public_key_jwk = json.loads(client_public_key_str) if isinstance(client_public_key_str, str) else client_public_key_str
194
+ public_key = self._load_public_key_from_jwk(public_key_jwk)
195
+ if not public_key:
196
+ return
197
+
198
+ encrypted_payload = self._encrypt_with_ecdh_aesgcm(public_key, data)
199
+ payload = {"data": encrypted_payload, "cell": self.to_dict()}
200
+ else:
201
+ payload = {"data": data, "cell": self.to_dict()}
202
+
203
+ await self._post_request(url, payload)
204
+
205
+
206
+ async def activate_tx(self, node_id: str, data: dict, encrypted: Optional[bool] = True):
207
+ url = f"https://{self.network}/api/activate_tx/{node_id}"
208
+ payload = {"cell": self.to_dict()}
209
+
210
+ if encrypted:
211
+ public_key = await self._get_target_node_public_key(node_id)
212
+ if not public_key: return None
213
+ data_to_encrypt = data.copy()
214
+ data_to_encrypt["publicKey"] = self.get_public_key_jwk()
215
+ encrypted_payload = self._encrypt_with_ecdh_aesgcm(public_key, data_to_encrypt)
216
+ payload["data"] = {"encrypted": encrypted_payload}
217
+ else:
218
+ payload["data"] = data
219
+
220
+ response_data = await self._post_request(url, payload)
221
+
222
+ if encrypted:
223
+ if not response_data or "response" not in response_data:
224
+ print("Unexpected or missing response.")
225
+ return response_data
226
+ inner_response = response_data["response"]
227
+ if "ciphertext" in inner_response:
228
+ ephemeral_public_key_bytes = base64.urlsafe_b64decode(inner_response["ephemeralPublicKey"] + '==')
229
+ nonce = base64.urlsafe_b64decode(inner_response["nonce"] + '==')
230
+ ciphertext = base64.urlsafe_b64decode(inner_response["ciphertext"] + '==')
231
+ return self._decrypt_with_ecdh_aesgcm(ephemeral_public_key_bytes, nonce, ciphertext)
232
+ else:
233
+ print("Server response was not encrypted as expected.")
234
+ return inner_response
235
+ else:
236
+ return response_data
237
+
238
+
239
+ async def sync(self) -> AsyncGenerator[str, None]:
240
+ full_url = f"wss://{self.network}/sync/{self.node_id}"
241
+ auth_payload = {
242
+ "host": self.host,
243
+ "password": self.password,
244
+ "synapse": self.synapse,
245
+ }
246
+ while True:
247
+ try:
248
+ async with websockets.connect(full_url) as ws:
249
+ await ws.send(json.dumps(auth_payload))
250
+ print("Node syncing...")
251
+ while True:
252
+ try:
253
+ raw_operation = await ws.recv()
254
+ operation = json.loads(raw_operation)
255
+
256
+ if "encrypted" in operation.get("data", {}):
257
+ encrypted_data = operation["data"]["encrypted"]
258
+
259
+ ephemeral_public_key_b64 = encrypted_data["ephemeralPublicKey"]
260
+ ephemeral_public_key_b64 += '=' * ((4 - len(ephemeral_public_key_b64) % 4) % 4)
261
+ ephemeral_public_key_bytes = base64.urlsafe_b64decode(ephemeral_public_key_b64)
262
+
263
+ nonce_b64 = encrypted_data["nonce"]
264
+ nonce_b64 += '=' * ((4 - len(nonce_b64) % 4) % 4)
265
+ nonce = base64.urlsafe_b64decode(nonce_b64)
266
+
267
+ ciphertext_b64 = encrypted_data["ciphertext"]
268
+ ciphertext_b64 += '=' * ((4 - len(ciphertext_b64) % 4) % 4)
269
+ ciphertext = base64.urlsafe_b64decode(ciphertext_b64)
270
+
271
+ decrypted_data = self._decrypt_with_ecdh_aesgcm(
272
+ ephemeral_public_key_bytes, nonce, ciphertext
273
+ )
274
+
275
+ if decrypted_data:
276
+ operation["data"].update(decrypted_data)
277
+ operation["data"].pop("encrypted")
278
+ yield operation
279
+ else:
280
+ print("Failed to decrypt incoming data. Skipping...")
281
+ else:
282
+ yield operation
283
+ except asyncio.TimeoutError:
284
+ print("No data received. Continuing...")
285
+ except ConnectionClosed as e:
286
+ if e.code == 1000:
287
+ print(f"WebSocket closed cleanly (code 1000). Reconnecting...")
288
+ else:
289
+ print(f"Connection closed with error code {e.code}: {e.reason}. Reconnecting...")
290
+ break
291
+ except Exception as e:
292
+ print(f"Unexpected error in recv loop: {e}")
293
+ break
294
+ except websockets.exceptions.WebSocketException as e:
295
+ print(f"WebSocket error occurred: {e}. Retrying in 5 seconds...")
296
+ except Exception as e:
297
+ print(f"General error occurred: {e}. Retrying in 5 seconds...")
298
+ await asyncio.sleep(3)
299
+
300
+
301
+ async def stream(self, label: str, data: dict, node_id: str = None, encrypted: Optional[bool] = True, retry_delay: int = 3):
302
+ context = ssl.create_default_context()
303
+ context.check_hostname = True
304
+ context.verify_mode = ssl.CERT_REQUIRED
305
+
306
+ target_node_public_key = None
307
+ if encrypted:
308
+ target_node_public_key = await self._get_target_node_public_key(node_id)
309
+ if not target_node_public_key:
310
+ print("Failed to get target node's public key. Cannot stream data.")
311
+ return
312
+
313
+ while True:
314
+ try:
315
+ reader, writer = await asyncio.open_connection(self.network, 55555, ssl=context, server_hostname=self.network)
316
+
317
+ credentials = f"{self.host}\n{self.password}\n{self.synapse}\n{node_id}\n"
318
+ writer.write(credentials.encode("utf-8"))
319
+ await writer.drain()
320
+
321
+ response = await reader.read(1024)
322
+ response_text = response.decode("utf-8").strip()
323
+
324
+ if "Authentication successful" not in response_text:
325
+ print("Authentication failed, retrying...")
326
+ writer.close()
327
+ await writer.wait_closed()
328
+ await asyncio.sleep(retry_delay)
329
+ continue
330
+
331
+ stream_payload = {
332
+ "label": label,
333
+ "data": data.copy()
334
+ }
335
+
336
+ if encrypted:
337
+ data_to_encrypt = data.copy()
338
+ data_to_encrypt["publicKey"] = self.get_public_key_jwk()
339
+
340
+ encrypted_payload = self._encrypt_with_ecdh_aesgcm(target_node_public_key, data_to_encrypt)
341
+ stream_payload["data"] = {"encrypted": encrypted_payload}
342
+
343
+ writer.write(json.dumps(stream_payload).encode("utf-8"))
344
+ await writer.drain()
345
+
346
+ response = await reader.read(1024)
347
+ response_text = response.decode("utf-8").strip()
348
+
349
+ if response_text == "Sent":
350
+ print(f"Success: {response_text} - {stream_payload}")
351
+ break
352
+ else:
353
+ print(f"Error sending: {stream_payload}")
354
+
355
+ except (ssl.SSLError, ConnectionError) as e:
356
+ print(f"Connection error: {e}, retrying...")
357
+ await asyncio.sleep(retry_delay)
358
+ except Exception as e:
359
+ print(f"Unexpected error: {e}, retrying...")
360
+ await asyncio.sleep(retry_delay)
361
+ finally:
362
+ if 'writer' in locals():
363
+ writer.close()
364
+ await writer.wait_closed()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: neuronum
3
- Version: 8.4.0
3
+ Version: 9.0.0
4
4
  Summary: The E2E Web Engine
5
5
  Home-page: https://neuronum.net
6
6
  Author: Neuronum Cybernetics
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
2
2
 
3
3
  setup(
4
4
  name='neuronum',
5
- version='8.4.0',
5
+ version='9.0.0',
6
6
  author='Neuronum Cybernetics',
7
7
  author_email='welcome@neuronum.net',
8
8
  description='The E2E Web Engine',
@@ -1,338 +0,0 @@
1
- import aiohttp
2
- from typing import AsyncGenerator
3
- import websockets
4
- import json
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
-
16
-
17
- class Node:
18
- def __init__(self, id: str, private_key: str, public_key: str):
19
- self.node_id = id
20
- self.private_key_path = private_key
21
- self.public_key_path = public_key
22
- self.queue = asyncio.Queue()
23
- self.host = self._load_host()
24
- self.network = self._load_network()
25
- self.synapse = self._load_synapse()
26
- self.password = self._load_password()
27
- self._private_key = self._load_private_key()
28
- self._public_key = self._load_public_key()
29
-
30
-
31
- def to_dict(self) -> dict:
32
- return {
33
- "host": self.host,
34
- "password": self.password,
35
- "synapse": self.synapse
36
- }
37
-
38
-
39
- def _load_private_key(self):
40
- try:
41
- with open(self.private_key_path, "rb") as f:
42
- private_key = serialization.load_pem_private_key(
43
- f.read(),
44
- password=None,
45
- backend=default_backend()
46
- )
47
- return private_key
48
- except FileNotFoundError:
49
- print(f"Private key file not found at {self.private_key_path}.")
50
- return None
51
-
52
-
53
- def _load_host(self):
54
- credentials_folder_path = Path.home() / ".neuronum"
55
- env_path = credentials_folder_path / ".env"
56
-
57
- env_data = {}
58
-
59
- try:
60
- with open(env_path, "r") as f:
61
- for line in f:
62
- key, value = line.strip().split("=")
63
- env_data[key] = value
64
-
65
- host = env_data.get("HOST", "")
66
- return host
67
- except FileNotFoundError:
68
- print(f"Cell Host not found")
69
- return None
70
-
71
-
72
- def _load_password(self):
73
- credentials_folder_path = Path.home() / ".neuronum"
74
- env_path = credentials_folder_path / ".env"
75
-
76
- env_data = {}
77
-
78
- try:
79
- with open(env_path, "r") as f:
80
- for line in f:
81
- key, value = line.strip().split("=")
82
- env_data[key] = value
83
-
84
- host = env_data.get("PASSWORD", "")
85
- return host
86
- except FileNotFoundError:
87
- print(f"Cell Password not found")
88
- return None
89
-
90
- def _load_synapse(self):
91
- credentials_folder_path = Path.home() / ".neuronum"
92
- env_path = credentials_folder_path / ".env"
93
-
94
- env_data = {}
95
-
96
- try:
97
- with open(env_path, "r") as f:
98
- for line in f:
99
- key, value = line.strip().split("=")
100
- env_data[key] = value
101
-
102
- host = env_data.get("SYNAPSE", "")
103
- return host
104
- except FileNotFoundError:
105
- print(f"Cell Synapse not found")
106
- return None
107
-
108
- def _load_network(self):
109
- credentials_folder_path = Path.home() / ".neuronum"
110
- env_path = credentials_folder_path / ".env"
111
-
112
- env_data = {}
113
-
114
- try:
115
- with open(env_path, "r") as f:
116
- for line in f:
117
- key, value = line.strip().split("=")
118
- env_data[key] = value
119
-
120
- host = env_data.get("NETWORK", "")
121
- return host
122
- except FileNotFoundError:
123
- print(f"Cell Network not found")
124
- return None
125
-
126
-
127
- def _load_public_key(self):
128
- try:
129
- with open(self.public_key_path, "rb") as f:
130
- public_key = serialization.load_pem_public_key(
131
- f.read(),
132
- backend=default_backend()
133
- )
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
- public_numbers = public_key.public_numbers()
150
-
151
- x_bytes = public_numbers.x.to_bytes((public_numbers.x.bit_length() + 7) // 8, 'big')
152
- y_bytes = public_numbers.y.to_bytes((public_numbers.y.bit_length() + 7) // 8, 'big')
153
-
154
- return {
155
- "kty": "EC",
156
- "crv": "P-256",
157
- "x": base64.urlsafe_b64encode(x_bytes).rstrip(b'=').decode('utf-8'),
158
- "y": base64.urlsafe_b64encode(y_bytes).rstrip(b'=').decode('utf-8')
159
- }
160
-
161
-
162
- def _decrypt_with_ecdh_aesgcm(self, ephemeral_public_key_bytes, nonce, ciphertext):
163
- try:
164
- ephemeral_public_key = ec.EllipticCurvePublicKey.from_encoded_point(
165
- ec.SECP256R1(), ephemeral_public_key_bytes
166
- )
167
-
168
- shared_secret = self._private_key.exchange(ec.ECDH(), ephemeral_public_key)
169
-
170
- derived_key = HKDF(
171
- algorithm=hashes.SHA256(),
172
- length=32,
173
- salt=None,
174
- info=b'handshake data'
175
- ).derive(shared_secret)
176
-
177
- aesgcm = AESGCM(derived_key)
178
- plaintext_bytes = aesgcm.decrypt(nonce, ciphertext, None)
179
- return json.loads(plaintext_bytes.decode())
180
-
181
- except Exception as e:
182
- print(f"Decryption failed: {e}")
183
- return None
184
-
185
-
186
- async def sync(self) -> AsyncGenerator[str, None]:
187
- full_url = f"wss://{self.network}/sync/{self.node_id}"
188
- auth_payload = {
189
- "host": self.host,
190
- "password": self.password,
191
- "synapse": self.synapse,
192
- }
193
- while True:
194
- try:
195
- async with websockets.connect(full_url) as ws:
196
- await ws.send(json.dumps(auth_payload))
197
- print("Node syncing...")
198
- while True:
199
- try:
200
- raw_operation = await ws.recv()
201
- operation = json.loads(raw_operation)
202
-
203
- if "encrypted" in operation.get("data", {}):
204
- encrypted_data = operation["data"]["encrypted"]
205
-
206
- ephemeral_public_key_b64 = encrypted_data["ephemeralPublicKey"]
207
- ephemeral_public_key_b64 += '=' * ((4 - len(ephemeral_public_key_b64) % 4) % 4)
208
- ephemeral_public_key_bytes = base64.urlsafe_b64decode(ephemeral_public_key_b64)
209
-
210
- nonce_b64 = encrypted_data["nonce"]
211
- nonce_b64 += '=' * ((4 - len(nonce_b64) % 4) % 4)
212
- nonce = base64.urlsafe_b64decode(nonce_b64)
213
-
214
- ciphertext_b64 = encrypted_data["ciphertext"]
215
- ciphertext_b64 += '=' * ((4 - len(ciphertext_b64) % 4) % 4)
216
- ciphertext = base64.urlsafe_b64decode(ciphertext_b64)
217
-
218
- decrypted_data = self._decrypt_with_ecdh_aesgcm(
219
- ephemeral_public_key_bytes, nonce, ciphertext
220
- )
221
-
222
- if decrypted_data:
223
- operation["data"].update(decrypted_data)
224
- operation["data"].pop("encrypted")
225
- yield operation
226
- else:
227
- print("Failed to decrypt incoming data. Skipping...")
228
- else:
229
- yield operation
230
- except asyncio.TimeoutError:
231
- print("No data received. Continuing...")
232
- except ConnectionClosed as e:
233
- if e.code == 1000:
234
- print(f"WebSocket closed cleanly (code 1000). Reconnecting...")
235
- else:
236
- print(f"Connection closed with error code {e.code}: {e.reason}. Reconnecting...")
237
- break
238
- except Exception as e:
239
- print(f"Unexpected error in recv loop: {e}")
240
- break
241
- except websockets.exceptions.WebSocketException as e:
242
- print(f"WebSocket error occurred: {e}. Retrying in 5 seconds...")
243
- except Exception as e:
244
- print(f"General error occurred: {e}. Retrying in 5 seconds...")
245
- await asyncio.sleep(3)
246
-
247
-
248
- async def list_nodes(self):
249
- full_url = f"https://{self.network}/api/list_nodes"
250
- list_nodes_payload = {
251
- "cell": self.to_dict()
252
- }
253
- async with aiohttp.ClientSession() as session:
254
- try:
255
- async with session.post(full_url, json=list_nodes_payload) as response:
256
- response.raise_for_status()
257
- data = await response.json()
258
- return data.get("Nodes", [])
259
- except aiohttp.ClientError as e:
260
- print(f"Error sending request: {e}")
261
- except Exception as e:
262
- print(f"Unexpected error: {e}")
263
-
264
-
265
- def _load_public_key_from_jwk(self, jwk):
266
- try:
267
- print(jwk)
268
- x = base64.urlsafe_b64decode(jwk['x'] + '==')
269
- y = base64.urlsafe_b64decode(jwk['y'] + '==')
270
- public_numbers = ec.EllipticCurvePublicNumbers(
271
- int.from_bytes(x, 'big'),
272
- int.from_bytes(y, 'big'),
273
- ec.SECP256R1()
274
- )
275
- return public_numbers.public_key(default_backend())
276
- except (KeyError, ValueError, TypeError) as e:
277
- print(f"Error loading public key from JWK string: {e}")
278
- return None
279
-
280
-
281
- def _encrypt_with_ecdh_aesgcm(self, public_key, plaintext_dict):
282
- ephemeral_private = ec.generate_private_key(ec.SECP256R1())
283
- ephemeral_public = ephemeral_private.public_key()
284
- shared_secret = ephemeral_private.exchange(ec.ECDH(), public_key)
285
- derived_key = HKDF(
286
- algorithm=hashes.SHA256(),
287
- length=32,
288
- salt=None,
289
- info=b'handshake data'
290
- ).derive(shared_secret)
291
- aesgcm = AESGCM(derived_key)
292
- nonce = os.urandom(12)
293
- plaintext_bytes = json.dumps(plaintext_dict).encode()
294
- ciphertext = aesgcm.encrypt(nonce, plaintext_bytes, None)
295
- ephemeral_public_bytes = ephemeral_public.public_bytes(
296
- serialization.Encoding.X962,
297
- serialization.PublicFormat.UncompressedPoint
298
- )
299
- return {
300
- 'ciphertext': base64.b64encode(ciphertext).decode(),
301
- 'nonce': base64.b64encode(nonce).decode(),
302
- 'ephemeralPublicKey': base64.b64encode(ephemeral_public_bytes).decode()
303
- }
304
-
305
-
306
- async def tx_response(self, transmitter_id: str, data: dict, client_public_key_str):
307
- if isinstance(client_public_key_str, str):
308
- try:
309
- client_public_key_jwk = json.loads(client_public_key_str)
310
- except json.JSONDecodeError:
311
- print("Failed to decode client public key from string. Aborting response.")
312
- return
313
- elif isinstance(client_public_key_str, dict):
314
- client_public_key_jwk = client_public_key_str
315
- else:
316
- print("Invalid type for client public key. Expected str or dict. Aborting response.")
317
- return
318
- public_key = self._load_public_key_from_jwk(client_public_key_jwk)
319
- if not public_key:
320
- print("Failed to load public key. Aborting response.")
321
- return
322
- encrypted_payload = self._encrypt_with_ecdh_aesgcm(public_key, data)
323
- url = f"https://{self.network}/api/tx_response/{transmitter_id}"
324
- tx_response = {
325
- "data": encrypted_payload,
326
- "cell": self.to_dict()
327
- }
328
- async with aiohttp.ClientSession() as session:
329
- try:
330
- for _ in range(2):
331
- async with session.post(url, json=tx_response) as response:
332
- response.raise_for_status()
333
- data = await response.json()
334
- print(data["message"])
335
- except aiohttp.ClientError as e:
336
- print(f"Error sending request: {e}")
337
- except Exception as e:
338
- print(f"Unexpected error: {e}")
File without changes
File without changes
File without changes
File without changes
File without changes