neuronum 8.3.0__py3-none-any.whl → 9.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 +14 -22
- neuronum/neuronum.py +219 -193
- {neuronum-8.3.0.dist-info → neuronum-9.0.0.dist-info}/METADATA +1 -1
- neuronum-9.0.0.dist-info/RECORD +10 -0
- neuronum-8.3.0.dist-info/RECORD +0 -10
- {neuronum-8.3.0.dist-info → neuronum-9.0.0.dist-info}/WHEEL +0 -0
- {neuronum-8.3.0.dist-info → neuronum-9.0.0.dist-info}/entry_points.txt +0 -0
- {neuronum-8.3.0.dist-info → neuronum-9.0.0.dist-info}/licenses/LICENSE.md +0 -0
- {neuronum-8.3.0.dist-info → neuronum-9.0.0.dist-info}/top_level.txt +0 -0
cli/main.py
CHANGED
|
@@ -359,12 +359,12 @@ 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 = {{}}
|
|
366
366
|
|
|
367
|
-
if action == "ping_node":
|
|
367
|
+
if action == "ping_node" or action == "start_app":
|
|
368
368
|
|
|
369
369
|
html_content = template.render(client=client, ts=ts, data=action, transmitter_id=transmitter_id)
|
|
370
370
|
|
|
@@ -373,7 +373,10 @@ async def main():
|
|
|
373
373
|
"html": html_content
|
|
374
374
|
}}
|
|
375
375
|
|
|
376
|
-
|
|
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
|
""")
|
|
@@ -516,7 +519,6 @@ asyncio.run(main())
|
|
|
516
519
|
data: {{ "action": "ping_node" }},
|
|
517
520
|
nodePublicKey: '{pem_public_oneline}',
|
|
518
521
|
}};
|
|
519
|
-
|
|
520
522
|
if (window.parent) {{
|
|
521
523
|
window.parent.postMessage(messagePayload, '*');
|
|
522
524
|
}}
|
|
@@ -536,19 +538,9 @@ f"""{{
|
|
|
536
538
|
"version": "1.0.0",
|
|
537
539
|
"author": "{host}",
|
|
538
540
|
"audience": "private",
|
|
539
|
-
"logo": "https://neuronum.net/static/logo.png"
|
|
541
|
+
"logo": "https://neuronum.net/static/logo.png",
|
|
542
|
+
"node_id": "{node_id}"
|
|
540
543
|
}},
|
|
541
|
-
"data_gateways": [
|
|
542
|
-
{{
|
|
543
|
-
"node_id": "{node_id}",
|
|
544
|
-
"actions": [
|
|
545
|
-
{{
|
|
546
|
-
"action": "ping_node",
|
|
547
|
-
"info": "Ping Node"
|
|
548
|
-
}}
|
|
549
|
-
]
|
|
550
|
-
}}
|
|
551
|
-
],
|
|
552
544
|
"legals": {{
|
|
553
545
|
"terms": "https://url_to_your/terms",
|
|
554
546
|
"privacy_policy": "https://url_to_your/privacy_policy"
|
|
@@ -635,7 +627,7 @@ def check_node():
|
|
|
635
627
|
with open('config.json', 'r') as f:
|
|
636
628
|
data = json.load(f)
|
|
637
629
|
|
|
638
|
-
nodeID = data['
|
|
630
|
+
nodeID = data['app_metadata']['node_id']
|
|
639
631
|
|
|
640
632
|
except FileNotFoundError:
|
|
641
633
|
click.echo("Error: .env with credentials not found")
|
|
@@ -699,7 +691,7 @@ def restart_node(d):
|
|
|
699
691
|
with open('config.json', 'r') as f:
|
|
700
692
|
data = json.load(f)
|
|
701
693
|
|
|
702
|
-
nodeID = data['
|
|
694
|
+
nodeID = data['app_metadata']['node_id']
|
|
703
695
|
|
|
704
696
|
except FileNotFoundError:
|
|
705
697
|
print("Error: .env with credentials not found")
|
|
@@ -781,7 +773,7 @@ async def async_stop_node():
|
|
|
781
773
|
with open('config.json', 'r') as f:
|
|
782
774
|
data = json.load(f)
|
|
783
775
|
|
|
784
|
-
nodeID = data['
|
|
776
|
+
nodeID = data['app_metadata']['node_id']
|
|
785
777
|
|
|
786
778
|
except FileNotFoundError:
|
|
787
779
|
print("Error: .env with credentials not found")
|
|
@@ -863,7 +855,7 @@ async def async_update_node(env_data, config_data, audience: str, descr: str):
|
|
|
863
855
|
network = env_data.get("NETWORK", "")
|
|
864
856
|
synapse = env_data.get("SYNAPSE", "")
|
|
865
857
|
|
|
866
|
-
node_id = config_data.get("
|
|
858
|
+
node_id = config_data.get("app_metadata", [{}]).get("node_id", "")
|
|
867
859
|
|
|
868
860
|
with open("config.json", "r") as f:
|
|
869
861
|
config_file_content = f.read()
|
|
@@ -941,7 +933,7 @@ async def _async_update_node_at_start(env_data, config_data, audience, descr):
|
|
|
941
933
|
network = env_data.get("NETWORK", "")
|
|
942
934
|
synapse = env_data.get("SYNAPSE", "")
|
|
943
935
|
|
|
944
|
-
node_id = config_data.get("
|
|
936
|
+
node_id = config_data.get("app_metadata", [{}]).get("node_id", "")
|
|
945
937
|
|
|
946
938
|
try:
|
|
947
939
|
with open("config.json", "r") as f:
|
|
@@ -995,7 +987,7 @@ async def async_delete_node():
|
|
|
995
987
|
with open('config.json', 'r') as f:
|
|
996
988
|
data = json.load(f)
|
|
997
989
|
|
|
998
|
-
nodeID = data['
|
|
990
|
+
nodeID = data['app_metadata']['node_id']
|
|
999
991
|
|
|
1000
992
|
except FileNotFoundError:
|
|
1001
993
|
click.echo("Error: .env with credentials not found")
|
neuronum/neuronum.py
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import aiohttp
|
|
2
|
-
from typing import AsyncGenerator
|
|
2
|
+
from typing import Optional, AsyncGenerator, Union
|
|
3
3
|
import websockets
|
|
4
4
|
import json
|
|
5
5
|
import asyncio
|
|
6
6
|
import base64
|
|
7
7
|
import os
|
|
8
|
+
import ssl
|
|
8
9
|
from pathlib import Path
|
|
9
10
|
from websockets.exceptions import ConnectionClosed
|
|
10
11
|
from cryptography.hazmat.primitives.asymmetric import ec
|
|
@@ -13,17 +14,17 @@ from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
|
|
13
14
|
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
|
|
14
15
|
from cryptography.hazmat.backends import default_backend
|
|
15
16
|
|
|
16
|
-
|
|
17
17
|
class Node:
|
|
18
18
|
def __init__(self, id: str, private_key: str, public_key: str):
|
|
19
19
|
self.node_id = id
|
|
20
20
|
self.private_key_path = private_key
|
|
21
21
|
self.public_key_path = public_key
|
|
22
22
|
self.queue = asyncio.Queue()
|
|
23
|
-
self.
|
|
24
|
-
self.
|
|
25
|
-
self.
|
|
26
|
-
self.
|
|
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", "")
|
|
27
28
|
self._private_key = self._load_private_key()
|
|
28
29
|
self._public_key = self._load_public_key()
|
|
29
30
|
|
|
@@ -34,110 +35,39 @@ class Node:
|
|
|
34
35
|
"password": self.password,
|
|
35
36
|
"synapse": self.synapse
|
|
36
37
|
}
|
|
38
|
+
|
|
37
39
|
|
|
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):
|
|
40
|
+
def _load_env(self) -> dict:
|
|
91
41
|
credentials_folder_path = Path.home() / ".neuronum"
|
|
92
42
|
env_path = credentials_folder_path / ".env"
|
|
93
|
-
|
|
94
|
-
env_data = {}
|
|
95
|
-
|
|
43
|
+
env_data = {}
|
|
96
44
|
try:
|
|
97
45
|
with open(env_path, "r") as f:
|
|
98
46
|
for line in f:
|
|
99
47
|
key, value = line.strip().split("=")
|
|
100
48
|
env_data[key] = value
|
|
101
|
-
|
|
102
|
-
host = env_data.get("SYNAPSE", "")
|
|
103
|
-
return host
|
|
49
|
+
return env_data
|
|
104
50
|
except FileNotFoundError:
|
|
105
|
-
print(f"Cell
|
|
106
|
-
return
|
|
107
|
-
|
|
108
|
-
def _load_network(self):
|
|
109
|
-
credentials_folder_path = Path.home() / ".neuronum"
|
|
110
|
-
env_path = credentials_folder_path / ".env"
|
|
51
|
+
print(f"Cell credentials (.env) not found at {env_path}")
|
|
52
|
+
return {}
|
|
111
53
|
|
|
112
|
-
env_data = {}
|
|
113
54
|
|
|
55
|
+
def _load_private_key(self):
|
|
114
56
|
try:
|
|
115
|
-
with open(
|
|
116
|
-
|
|
117
|
-
key, value = line.strip().split("=")
|
|
118
|
-
env_data[key] = value
|
|
119
|
-
|
|
120
|
-
host = env_data.get("NETWORK", "")
|
|
121
|
-
return host
|
|
57
|
+
with open(self.private_key_path, "rb") as f:
|
|
58
|
+
return serialization.load_pem_private_key(f.read(), password=None, backend=default_backend())
|
|
122
59
|
except FileNotFoundError:
|
|
123
|
-
print(f"
|
|
60
|
+
print(f"Private key file not found at {self.private_key_path}.")
|
|
124
61
|
return None
|
|
125
62
|
|
|
126
63
|
|
|
127
64
|
def _load_public_key(self):
|
|
128
65
|
try:
|
|
129
66
|
with open(self.public_key_path, "rb") as f:
|
|
130
|
-
|
|
131
|
-
f.read(),
|
|
132
|
-
backend=default_backend()
|
|
133
|
-
)
|
|
134
|
-
return public_key
|
|
67
|
+
return serialization.load_pem_public_key(f.read(), backend=default_backend())
|
|
135
68
|
except FileNotFoundError:
|
|
136
|
-
print(f"Public key file not found
|
|
137
|
-
if self._private_key
|
|
138
|
-
return self._private_key.public_key()
|
|
139
|
-
else:
|
|
140
|
-
return None
|
|
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
|
|
141
71
|
|
|
142
72
|
|
|
143
73
|
def get_public_key_jwk(self):
|
|
@@ -145,44 +75,167 @@ class Node:
|
|
|
145
75
|
if not public_key:
|
|
146
76
|
print("Public key not loaded. Cannot generate JWK.")
|
|
147
77
|
return None
|
|
148
|
-
|
|
149
78
|
public_numbers = public_key.public_numbers()
|
|
150
|
-
|
|
151
79
|
x_bytes = public_numbers.x.to_bytes((public_numbers.x.bit_length() + 7) // 8, 'big')
|
|
152
80
|
y_bytes = public_numbers.y.to_bytes((public_numbers.y.bit_length() + 7) // 8, 'big')
|
|
153
|
-
|
|
154
81
|
return {
|
|
155
82
|
"kty": "EC",
|
|
156
83
|
"crv": "P-256",
|
|
157
84
|
"x": base64.urlsafe_b64encode(x_bytes).rstrip(b'=').decode('utf-8'),
|
|
158
85
|
"y": base64.urlsafe_b64encode(y_bytes).rstrip(b'=').decode('utf-8')
|
|
159
86
|
}
|
|
160
|
-
|
|
161
|
-
|
|
87
|
+
|
|
88
|
+
|
|
162
89
|
def _decrypt_with_ecdh_aesgcm(self, ephemeral_public_key_bytes, nonce, ciphertext):
|
|
163
90
|
try:
|
|
164
91
|
ephemeral_public_key = ec.EllipticCurvePublicKey.from_encoded_point(
|
|
165
92
|
ec.SECP256R1(), ephemeral_public_key_bytes
|
|
166
93
|
)
|
|
167
|
-
|
|
168
94
|
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
|
-
|
|
95
|
+
derived_key = HKDF(algorithm=hashes.SHA256(), length=32, salt=None, info=b'handshake data').derive(shared_secret)
|
|
177
96
|
aesgcm = AESGCM(derived_key)
|
|
178
97
|
plaintext_bytes = aesgcm.decrypt(nonce, ciphertext, None)
|
|
179
98
|
return json.loads(plaintext_bytes.decode())
|
|
180
|
-
|
|
181
99
|
except Exception as e:
|
|
182
100
|
print(f"Decryption failed: {e}")
|
|
183
101
|
return None
|
|
184
102
|
|
|
185
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
|
+
|
|
186
239
|
async def sync(self) -> AsyncGenerator[str, None]:
|
|
187
240
|
full_url = f"wss://{self.network}/sync/{self.node_id}"
|
|
188
241
|
auth_payload = {
|
|
@@ -245,94 +298,67 @@ class Node:
|
|
|
245
298
|
await asyncio.sleep(3)
|
|
246
299
|
|
|
247
300
|
|
|
248
|
-
async def
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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.")
|
|
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.")
|
|
312
311
|
return
|
|
313
|
-
|
|
314
|
-
|
|
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:
|
|
312
|
+
|
|
313
|
+
while True:
|
|
329
314
|
try:
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
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)
|
|
337
358
|
except Exception as e:
|
|
338
|
-
print(f"Unexpected error: {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()
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
cli/main.py,sha256=Ol3kyxh7odt03buze27PGCCaDS0iOax8nbTWYHvK1pA,31798
|
|
3
|
+
neuronum/__init__.py,sha256=qkAz6fpiS2KKnaKwPbS15y7lRCQV-XaWNTkVUarluPk,26
|
|
4
|
+
neuronum/neuronum.py,sha256=3Sway-04-Yb3PHcnbIVumL0kQMB__fiO5w9vDXbkWGk,16186
|
|
5
|
+
neuronum-9.0.0.dist-info/licenses/LICENSE.md,sha256=m7pw_FktMNCs4tcy2UXP3QQP2S_je28P1SepdYoo0Xo,1961
|
|
6
|
+
neuronum-9.0.0.dist-info/METADATA,sha256=UIovNeYOTcpaZz0FUhJvWQ5EqIu7dDvVpxgrxu8DTWM,3448
|
|
7
|
+
neuronum-9.0.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
8
|
+
neuronum-9.0.0.dist-info/entry_points.txt,sha256=XKYBcRNxGeJpZZkDPsa8HA_RaJ7Km_R_JaUq5T9Nk2U,42
|
|
9
|
+
neuronum-9.0.0.dist-info/top_level.txt,sha256=ru8Fr84cHm6oHr_DcJ8-uaq3RTiuCRFIr6AC8V0zPu4,13
|
|
10
|
+
neuronum-9.0.0.dist-info/RECORD,,
|
neuronum-8.3.0.dist-info/RECORD
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
-
cli/main.py,sha256=rsr2vt0qQjb0IX6V4-s5RkApZz5FPnoPuxuZXMtUlUM,31799
|
|
3
|
-
neuronum/__init__.py,sha256=qkAz6fpiS2KKnaKwPbS15y7lRCQV-XaWNTkVUarluPk,26
|
|
4
|
-
neuronum/neuronum.py,sha256=4iZjeQ1XTKPhUpojmpkYQiOaNZPZE6t7JUyhLPMOyuA,13183
|
|
5
|
-
neuronum-8.3.0.dist-info/licenses/LICENSE.md,sha256=m7pw_FktMNCs4tcy2UXP3QQP2S_je28P1SepdYoo0Xo,1961
|
|
6
|
-
neuronum-8.3.0.dist-info/METADATA,sha256=FXfumz1JVjicIaNHN4boBYy85m3FwuDkrrZCYbCyYhc,3448
|
|
7
|
-
neuronum-8.3.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
8
|
-
neuronum-8.3.0.dist-info/entry_points.txt,sha256=XKYBcRNxGeJpZZkDPsa8HA_RaJ7Km_R_JaUq5T9Nk2U,42
|
|
9
|
-
neuronum-8.3.0.dist-info/top_level.txt,sha256=ru8Fr84cHm6oHr_DcJ8-uaq3RTiuCRFIr6AC8V0zPu4,13
|
|
10
|
-
neuronum-8.3.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|