distributed-state-network 0.4.2__tar.gz → 0.5.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.
Files changed (33) hide show
  1. {distributed_state_network-0.4.2 → distributed_state_network-0.5.0}/PKG-INFO +1 -1
  2. {distributed_state_network-0.4.2 → distributed_state_network-0.5.0}/documentation/ds-node-config.md +2 -4
  3. {distributed_state_network-0.4.2 → distributed_state_network-0.5.0}/documentation/ds-node-server.md +8 -10
  4. {distributed_state_network-0.4.2 → distributed_state_network-0.5.0}/documentation/usage.md +0 -6
  5. {distributed_state_network-0.4.2 → distributed_state_network-0.5.0}/pyproject.toml +1 -1
  6. distributed_state_network-0.5.0/src/distributed_state_network/create_key.py +4 -0
  7. {distributed_state_network-0.4.2 → distributed_state_network-0.5.0}/src/distributed_state_network/dsnode.py +5 -12
  8. {distributed_state_network-0.4.2 → distributed_state_network-0.5.0}/src/distributed_state_network/handler.py +5 -7
  9. {distributed_state_network-0.4.2 → distributed_state_network-0.5.0}/src/distributed_state_network/objects/config.py +2 -2
  10. {distributed_state_network-0.4.2 → distributed_state_network-0.5.0}/src/distributed_state_network/util/key_manager.py +2 -6
  11. {distributed_state_network-0.4.2 → distributed_state_network-0.5.0}/technical.md +5 -3
  12. {distributed_state_network-0.4.2 → distributed_state_network-0.5.0}/tests/connections.py +2 -5
  13. {distributed_state_network-0.4.2 → distributed_state_network-0.5.0}/tests/test.py +5 -31
  14. distributed_state_network-0.4.2/src/distributed_state_network/create_key.py +0 -10
  15. {distributed_state_network-0.4.2 → distributed_state_network-0.5.0}/.github/workflows/publish.yml +0 -0
  16. {distributed_state_network-0.4.2 → distributed_state_network-0.5.0}/.gitignore +0 -0
  17. {distributed_state_network-0.4.2 → distributed_state_network-0.5.0}/LICENSE +0 -0
  18. {distributed_state_network-0.4.2 → distributed_state_network-0.5.0}/README.md +0 -0
  19. {distributed_state_network-0.4.2 → distributed_state_network-0.5.0}/coverage.sh +0 -0
  20. {distributed_state_network-0.4.2 → distributed_state_network-0.5.0}/documentation/ds-node.md +0 -0
  21. {distributed_state_network-0.4.2 → distributed_state_network-0.5.0}/documentation/protocol.md +0 -0
  22. {distributed_state_network-0.4.2 → distributed_state_network-0.5.0}/src/distributed_state_network/__init__.py +0 -0
  23. {distributed_state_network-0.4.2 → distributed_state_network-0.5.0}/src/distributed_state_network/objects/endpoint.py +0 -0
  24. {distributed_state_network-0.4.2 → distributed_state_network-0.5.0}/src/distributed_state_network/objects/hello_packet.py +0 -0
  25. {distributed_state_network-0.4.2 → distributed_state_network-0.5.0}/src/distributed_state_network/objects/msg_types.py +0 -0
  26. {distributed_state_network-0.4.2 → distributed_state_network-0.5.0}/src/distributed_state_network/objects/peers_packet.py +0 -0
  27. {distributed_state_network-0.4.2 → distributed_state_network-0.5.0}/src/distributed_state_network/objects/signed_packet.py +0 -0
  28. {distributed_state_network-0.4.2 → distributed_state_network-0.5.0}/src/distributed_state_network/objects/state_packet.py +0 -0
  29. {distributed_state_network-0.4.2 → distributed_state_network-0.5.0}/src/distributed_state_network/util/__init__.py +0 -0
  30. {distributed_state_network-0.4.2 → distributed_state_network-0.5.0}/src/distributed_state_network/util/aes.py +0 -0
  31. {distributed_state_network-0.4.2 → distributed_state_network-0.5.0}/src/distributed_state_network/util/byte_helper.py +0 -0
  32. {distributed_state_network-0.4.2 → distributed_state_network-0.5.0}/src/distributed_state_network/util/ecdsa.py +0 -0
  33. {distributed_state_network-0.4.2 → distributed_state_network-0.5.0}/src/distributed_state_network/util/https.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: distributed-state-network
3
- Version: 0.4.2
3
+ Version: 0.5.0
4
4
  Summary: A tool to distribute the state of a network device to other devices on the network
5
5
  Project-URL: Homepage, https://github.com/erinclemmer/distributed_state_network
6
6
  Project-URL: Issues, https://github.com/erinclemmer/distributed_state_network/issues
@@ -12,14 +12,14 @@ from distributed_state_network import DSNodeConfig
12
12
  class DSNodeConfig:
13
13
  node_id: str
14
14
  port: int
15
- aes_key_file: str
15
+ aes_key: str
16
16
  bootstrap_nodes: List[Endpoint]
17
17
  ```
18
18
 
19
19
  ### Attributes
20
20
  - **node_id** (`str`): Unique identifier for the node
21
21
  - **port** (`int`): Port number for the node to listen on (UDP)
22
- - **aes_key_file** (`str`): Path to the AES key file for encryption/decryption
22
+ - **aes_key** (`str`): Hexidecimal encoded AES key for network encryption
23
23
  - **bootstrap_nodes** (`List[Endpoint]`): List of initial nodes to connect to when joining the network
24
24
 
25
25
  **Note:** The node's public IP address is automatically detected by the bootstrap server during the initial handshake.
@@ -40,7 +40,6 @@ Creates a DSNodeConfig instance from a dictionary.
40
40
  config_dict = {
41
41
  "node_id": "node1",
42
42
  "port": 8000,
43
- "aes_key_file": "/path/to/key.aes",
44
43
  "bootstrap_nodes": [
45
44
  {"address": "127.0.0.1", "port": 8001}
46
45
  ]
@@ -53,7 +52,6 @@ config = DSNodeConfig.from_dict(config_dict)
53
52
  config_dict = {
54
53
  "node_id": "bootstrap",
55
54
  "port": 8000,
56
- "aes_key_file": "/path/to/key.aes",
57
55
  "bootstrap_nodes": [] # Empty for first node
58
56
  }
59
57
  config = DSNodeConfig.from_dict(config_dict)
@@ -59,7 +59,6 @@ server = DSNodeServer.start(config)
59
59
  bootstrap_config = DSNodeConfig(
60
60
  node_id="bootstrap",
61
61
  port=8000,
62
- aes_key_file="network.key",
63
62
  bootstrap_nodes=[]
64
63
  )
65
64
  bootstrap = DSNodeServer.start(bootstrap_config)
@@ -68,21 +67,20 @@ bootstrap = DSNodeServer.start(bootstrap_config)
68
67
  connector_config = DSNodeConfig(
69
68
  node_id="connector",
70
69
  port=8001,
71
- aes_key_file="network.key",
72
70
  bootstrap_nodes=[Endpoint("127.0.0.1", 8000)]
73
71
  )
74
72
  connector = DSNodeServer.start(connector_config)
75
73
  ```
76
74
 
77
- ### `generate_key(out_file_path: str) -> None`
78
- Generates a new AES key file for network encryption. All nodes in the same network must share the same AES key.
75
+ ### `generate_key() -> str`
76
+ Generates a new hexadecimal encoded AES key for network encryption. All nodes in the same network must share the same AES key.
79
77
 
80
78
  **Parameters:**
81
- - `out_file_path` (`str`): Path where the key file will be saved
79
+ - None
82
80
 
83
81
  **Example:**
84
82
  ```python
85
- DSNodeServer.generate_key("/path/to/network.key")
83
+ DSNodeServer.generate_key()
86
84
  ```
87
85
 
88
86
  ### Instance Methods
@@ -145,13 +143,13 @@ All shared state is protected with appropriate locks.
145
143
  from distributed_state_network import DSNodeServer, DSNodeConfig, Endpoint
146
144
 
147
145
  # Generate shared AES key (only once)
148
- DSNodeServer.generate_key("shared.key")
146
+ key = DSNodeServer.generate_key()
149
147
 
150
148
  # Start bootstrap node
151
149
  bootstrap = DSNodeServer.start(DSNodeConfig(
152
150
  node_id="bootstrap",
153
151
  port=8000,
154
- aes_key_file="shared.key",
152
+ aes_key=key,
155
153
  bootstrap_nodes=[]
156
154
  ))
157
155
 
@@ -159,14 +157,14 @@ bootstrap = DSNodeServer.start(DSNodeConfig(
159
157
  node1 = DSNodeServer.start(DSNodeConfig(
160
158
  node_id="node1",
161
159
  port=8001,
162
- aes_key_file="shared.key",
160
+ aes_key=key,
163
161
  bootstrap_nodes=[Endpoint("192.168.1.100", 8000)]
164
162
  ))
165
163
 
166
164
  node2 = DSNodeServer.start(DSNodeConfig(
167
165
  node_id="node2",
168
166
  port=8002,
169
- aes_key_file="shared.key",
167
+ aes_key=key,
170
168
  bootstrap_nodes=[Endpoint("192.168.1.100", 8000)]
171
169
  ))
172
170
 
@@ -9,7 +9,6 @@ from distributed_state_network import DSNodeServer, DSNodeConfig, Endpoint
9
9
  config = DSNodeConfig(
10
10
  node_id="node1",
11
11
  port=8000,
12
- aes_key_file="/path/to/shared.key",
13
12
  bootstrap_nodes=[] # Empty for first node
14
13
  )
15
14
 
@@ -38,7 +37,6 @@ server.stop()
38
37
  config2 = DSNodeConfig(
39
38
  node_id="node2",
40
39
  port=8001,
41
- aes_key_file="/path/to/shared.key", # Same key file as network
42
40
  bootstrap_nodes=[
43
41
  Endpoint("127.0.0.1", 8000) # Node 1's endpoint
44
42
  ]
@@ -65,7 +63,6 @@ custom_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
65
63
  config = DSNodeConfig(
66
64
  node_id="node3",
67
65
  port=8003, # Should match the bound port
68
- aes_key_file="/path/to/shared.key",
69
66
  bootstrap_nodes=[Endpoint("127.0.0.1", 8000)]
70
67
  )
71
68
 
@@ -83,7 +80,6 @@ def handle_disconnect():
83
80
  config = DSNodeConfig(
84
81
  node_id="node4",
85
82
  port=8004,
86
- aes_key_file="/path/to/shared.key",
87
83
  bootstrap_nodes=[Endpoint("127.0.0.1", 8000)]
88
84
  )
89
85
 
@@ -100,7 +96,6 @@ def handle_update():
100
96
  config = DSNodeConfig(
101
97
  node_id="node5",
102
98
  port=8005,
103
- aes_key_file="/path/to/shared.key",
104
99
  bootstrap_nodes=[Endpoint("127.0.0.1", 8000)]
105
100
  )
106
101
 
@@ -126,7 +121,6 @@ custom_sock.bind(("0.0.0.0", 8006))
126
121
  config = DSNodeConfig(
127
122
  node_id="node6",
128
123
  port=8006,
129
- aes_key_file="/path/to/shared.key",
130
124
  bootstrap_nodes=[
131
125
  Endpoint("127.0.0.1", 8000),
132
126
  Endpoint("127.0.0.1", 8001) # Multiple bootstrap options
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "distributed-state-network"
7
- version = "0.4.2"
7
+ version = "0.5.0"
8
8
  authors = [{name="Erin Clemmer", email="erin.c.clemmer@gmail.com"}]
9
9
  description = "A tool to distribute the state of a network device to other devices on the network"
10
10
  keywords = ["distributed", "networking"]
@@ -0,0 +1,4 @@
1
+ import sys
2
+ from util.aes import generate_aes_key
3
+
4
+ print("Key: " + generate_aes_key().hex())
@@ -49,7 +49,7 @@ class DSNode:
49
49
  self.version = version
50
50
  self.server = None # Reference to the Flask server
51
51
 
52
- self.cred_manager = CredentialManager(config.node_id)
52
+ self.cred_manager = CredentialManager("credentials", config.node_id)
53
53
  self.cred_manager.generate_keys()
54
54
 
55
55
  self.node_states = {
@@ -65,21 +65,14 @@ class DSNode:
65
65
  self.logger = logging.getLogger("DSN: " + config.node_id)
66
66
  self.disconnect_cb = disconnect_callback
67
67
  self.update_cb = update_callback
68
- if config.aes_key_file is not None and not os.path.exists(config.aes_key_file):
69
- raise Exception(f"Could not find aes key file in {config.aes_key_file}")
70
68
 
71
69
  threading.Thread(target=self.network_tick, daemon=True).start()
72
70
 
73
71
  def set_server(self, server):
74
- """Set reference to the Flask server"""
75
72
  self.server = server
76
73
 
77
- def get_aes_key(self):
78
- if not os.path.exists(self.config.aes_key_file):
79
- raise Exception("Could not find aes key file")
80
- with open(self.config.aes_key_file, 'r') as f:
81
- key_hex = f.read()
82
- return bytes.fromhex(key_hex)
74
+ def get_aes_key(self) -> bytes:
75
+ return bytes.fromhex(self.config.aes_key)
83
76
 
84
77
  def network_tick(self):
85
78
  time.sleep(TICK_INTERVAL)
@@ -113,7 +106,7 @@ class DSNode:
113
106
  try:
114
107
  # Prepend message type to payload
115
108
  data = bytes([msg_type]) + payload
116
- if self.config.aes_key_file is not None:
109
+ if self.config.aes_key is not None:
117
110
  data = self.encrypt_data(data)
118
111
 
119
112
  # Determine the URL path based on message type
@@ -137,7 +130,7 @@ class DSNode:
137
130
 
138
131
  response_data = response.content
139
132
  # Decrypt the response
140
- if self.config.aes_key_file is not None:
133
+ if self.config.aes_key is not None:
141
134
  response_data = self.decrypt_data(response_data)
142
135
 
143
136
  if len(response_data) < 1:
@@ -8,7 +8,7 @@ from distributed_state_network.objects.config import DSNodeConfig
8
8
  from distributed_state_network.util.aes import generate_aes_key
9
9
  from distributed_state_network.util import stop_thread
10
10
 
11
- VERSION = "0.4.2"
11
+ VERSION = "0.5.0"
12
12
  logging.basicConfig(level=logging.INFO)
13
13
 
14
14
  # Silence Flask and Werkzeug logging
@@ -66,7 +66,7 @@ class DSNodeServer:
66
66
  """Handle incoming HTTP request"""
67
67
  try:
68
68
  # Decrypt the data
69
- if self.config.aes_key_file is not None:
69
+ if self.config.aes_key is not None:
70
70
  data = self.node.decrypt_data(data)
71
71
 
72
72
  if len(data) < 1:
@@ -99,7 +99,7 @@ class DSNodeServer:
99
99
  if response_data is not None:
100
100
  # Prepend message type to response
101
101
  response_with_type = bytes([msg_type]) + response_data
102
- if self.config.aes_key_file is not None:
102
+ if self.config.aes_key is not None:
103
103
  response_with_type = self.node.encrypt_data(response_with_type)
104
104
  return Response(response_with_type, status=200, mimetype='application/octet-stream')
105
105
  else:
@@ -132,10 +132,8 @@ class DSNodeServer:
132
132
  self.node.logger.info(f'Started DSNode on HTTP port {port}')
133
133
 
134
134
  @staticmethod
135
- def generate_key(out_file_path: str):
136
- key = generate_aes_key()
137
- with open(out_file_path, 'w', encoding='utf-8') as f:
138
- f.write(key.hex())
135
+ def generate_key() -> str:
136
+ return generate_aes_key().hex()
139
137
 
140
138
  @staticmethod
141
139
  def start(
@@ -7,7 +7,7 @@ from distributed_state_network.objects.endpoint import Endpoint
7
7
  class DSNodeConfig:
8
8
  node_id: str
9
9
  port: int
10
- aes_key_file: str | None
10
+ aes_key: str | None
11
11
  bootstrap_nodes: List[Endpoint]
12
12
 
13
13
  @staticmethod
@@ -15,6 +15,6 @@ class DSNodeConfig:
15
15
  return DSNodeConfig(
16
16
  data["node_id"],
17
17
  data["port"],
18
- data["aes_key_file"] if "aes_key_file" in data else None,
18
+ data["aes_key"] if "aes_key" in data else None,
19
19
  [Endpoint.from_json(e) for e in data["bootstrap_nodes"]]
20
20
  )
@@ -77,10 +77,6 @@ class KeyManager:
77
77
  with open(f'{self.folder}/{self.node_id}/{self.node_id}.{self.private_extension}', 'wb') as f:
78
78
  f.write(key_bytes)
79
79
 
80
- class CertManager(KeyManager):
81
- def __init__(self, node_id: str):
82
- KeyManager.__init__(self, "HTTPS", node_id, "certs", "crt", "key", generate_cert)
83
-
84
80
  class CredentialManager(KeyManager):
85
- def __init__(self, node_id: str):
86
- KeyManager.__init__(self, "ECDSA", node_id, "credentials", "pub", "key", generate_key_pair)
81
+ def __init__(self, folder: str, node_id: str):
82
+ KeyManager.__init__(self, "ECDSA", node_id, folder, "pub", "key", generate_key_pair)
@@ -9,13 +9,13 @@ This is intended to be used as a middleware for another application rather than
9
9
  from distributed_state_network import DSNodeServer, DSNodeConfig
10
10
 
11
11
  # Write a new aes key to the current directory
12
- DSNodeServer.generate_key("test.key")
12
+ aes_key = DSNodeServer.generate_key()
13
13
 
14
14
  # Use the key to start a new network
15
15
  bootstrap = DSNodeServer(DSNodeConfig(
16
16
  node_id="bootstrap", # Network ID for the node
17
17
  port=8000, # Port to host the server on
18
- aes_key_file="test.key" # Key file for authentication to the network
18
+ aes_key=aes_key # Key file for authentication to the network
19
19
  ))
20
20
  ```
21
21
 
@@ -26,10 +26,12 @@ To connect another node to we will copy the AES key file to the new machine and
26
26
  ```python
27
27
  from distributed_state_network import DSNodeServer, DSNodeConfig
28
28
 
29
+ KEY = "..."
30
+
29
31
  connector = DSNodeServer(DSNodeConfig(
30
32
  node_id="connector", # New node ID
31
33
  port=8000, # Port to host the new server on
32
- aes_key_file= "test.key", # Key file that was copied from first machine
34
+ aes_key= KEY, # Key file that was copied from first machine
33
35
  bootstrap_nodes= [
34
36
  {
35
37
  # IP address of bootstrap node
@@ -8,10 +8,7 @@ sys.path.append(os.path.join(os.path.dirname(__file__), '../src'))
8
8
 
9
9
  from distributed_state_network import DSNodeServer, DSNodeConfig, Endpoint
10
10
 
11
- KEY_FILE = "network.key"
12
-
13
- if not os.path.exists(KEY_FILE):
14
- DSNodeServer.generate_key("network.key")
11
+ aes_key = DSNodeServer.generate_key()
15
12
 
16
13
  cb_test = 0
17
14
 
@@ -22,7 +19,7 @@ def start_node(node_id: str, bootstrap_port: int = None, disconnect_cb: Optional
22
19
  args = {
23
20
  "node_id": node_id,
24
21
  "port": current_port,
25
- "aes_key_file": KEY_FILE
22
+ "aes_key": aes_key
26
23
  }
27
24
  current_port += 1
28
25
  if bootstrap_port is not None:
@@ -14,16 +14,12 @@ from distributed_state_network import DSNodeServer, Endpoint, DSNodeConfig
14
14
  from distributed_state_network.objects.state_packet import StatePacket
15
15
  from distributed_state_network.objects.hello_packet import HelloPacket
16
16
 
17
- from distributed_state_network.util.key_manager import CertManager
18
17
  from distributed_state_network.util.aes import generate_aes_key
19
18
 
20
19
  current_port = 8000
21
20
  nodes = []
22
21
 
23
- key_file = "src/distributed_state_network/test.key"
24
-
25
- if not os.path.exists(key_file):
26
- DSNodeServer.generate_key(key_file)
22
+ aes_key = DSNodeServer.generate_key()
27
23
 
28
24
  def spawn_node(node_id: str, bootstrap_nodes: List[Dict] = []):
29
25
  global current_port
@@ -31,7 +27,7 @@ def spawn_node(node_id: str, bootstrap_nodes: List[Dict] = []):
31
27
  n = DSNodeServer.start(DSNodeConfig.from_dict({
32
28
  "node_id": node_id,
33
29
  "port": current_port,
34
- "aes_key_file": key_file,
30
+ "aes_key": aes_key,
35
31
  "bootstrap_nodes": bootstrap_nodes
36
32
  }))
37
33
  global nodes
@@ -264,7 +260,7 @@ class TestNode(unittest.TestCase):
264
260
  config_dict = {
265
261
  "node_id": "node",
266
262
  "port": 8000,
267
- "aes_key_file": "test.key",
263
+ "aes_key": "XXX",
268
264
  "bootstrap_nodes": [
269
265
  {
270
266
  "address": "127.0.0.1",
@@ -276,7 +272,7 @@ class TestNode(unittest.TestCase):
276
272
  config = DSNodeConfig.from_dict(config_dict)
277
273
  self.assertEqual(config_dict["node_id"], config.node_id)
278
274
  self.assertEqual(config_dict["port"], config.port)
279
- self.assertEqual(config_dict["aes_key_file"], config.aes_key_file)
275
+ self.assertEqual(config_dict["aes_key"], config.aes_key)
280
276
  self.assertTrue(len(config.bootstrap_nodes) > 0)
281
277
  self.assertEqual(config_dict["bootstrap_nodes"][0]["address"], config.bootstrap_nodes[0].address)
282
278
  self.assertEqual(config_dict["bootstrap_nodes"][0]["port"], config.bootstrap_nodes[0].port)
@@ -307,30 +303,8 @@ class TestNode(unittest.TestCase):
307
303
  print(e)
308
304
 
309
305
  def test_aes(self):
310
- test_key_file = 'delete_me.key'
311
- DSNodeServer.generate_key(test_key_file)
312
- with open(test_key_file, 'rb') as f:
313
- key = f.read()
306
+ key = DSNodeServer.generate_key()
314
307
  self.assertEqual(64, len(key))
315
- os.remove(test_key_file)
316
-
317
- def test_write_cert(self):
318
- if os.path.exists('certs'):
319
- shutil.rmtree('certs')
320
- cm = CertManager('test')
321
- cm.write_public('test', b'TEST')
322
- self.assertTrue(os.path.exists('certs/test/test.crt'))
323
- shutil.rmtree('certs')
324
-
325
- def test_read_public(self):
326
- if os.path.exists('certs'):
327
- shutil.rmtree('certs')
328
- cm = CertManager('test')
329
- try:
330
- cm.read_public('test')
331
- except Exception as e:
332
- self.assertEqual(e.args[0], 401)
333
- self.assertEqual(e.args[1], "Cannot find public ECDSA key for test")
334
308
 
335
309
  def test_authentication_reset(self):
336
310
  bootstrap = spawn_node("bootstrap")
@@ -1,10 +0,0 @@
1
- import sys
2
- from util.aes import generate_aes_key
3
-
4
- if len(sys.argv) < 2:
5
- raise Exception("Usage: python create_key.py [output_key_file_location]")
6
-
7
- location = sys.argv[1]
8
-
9
- with open(location, 'w') as f:
10
- f.write(generate_aes_key().hex())