naeural-client 2.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.
Files changed (78) hide show
  1. naeural_client/__init__.py +13 -0
  2. naeural_client/_ver.py +13 -0
  3. naeural_client/base/__init__.py +6 -0
  4. naeural_client/base/distributed_custom_code_presets.py +44 -0
  5. naeural_client/base/generic_session.py +1763 -0
  6. naeural_client/base/instance.py +616 -0
  7. naeural_client/base/payload/__init__.py +1 -0
  8. naeural_client/base/payload/payload.py +66 -0
  9. naeural_client/base/pipeline.py +1499 -0
  10. naeural_client/base/plugin_template.py +5209 -0
  11. naeural_client/base/responses.py +209 -0
  12. naeural_client/base/transaction.py +157 -0
  13. naeural_client/base_decentra_object.py +143 -0
  14. naeural_client/bc/__init__.py +3 -0
  15. naeural_client/bc/base.py +1046 -0
  16. naeural_client/bc/chain.py +0 -0
  17. naeural_client/bc/ec.py +324 -0
  18. naeural_client/certs/__init__.py +0 -0
  19. naeural_client/certs/r9092118.ala.eu-central-1.emqxsl.com.crt +22 -0
  20. naeural_client/code_cheker/__init__.py +1 -0
  21. naeural_client/code_cheker/base.py +520 -0
  22. naeural_client/code_cheker/checker.py +294 -0
  23. naeural_client/comm/__init__.py +2 -0
  24. naeural_client/comm/amqp_wrapper.py +338 -0
  25. naeural_client/comm/mqtt_wrapper.py +539 -0
  26. naeural_client/const/README.md +3 -0
  27. naeural_client/const/__init__.py +9 -0
  28. naeural_client/const/base.py +101 -0
  29. naeural_client/const/comms.py +80 -0
  30. naeural_client/const/environment.py +26 -0
  31. naeural_client/const/formatter.py +7 -0
  32. naeural_client/const/heartbeat.py +111 -0
  33. naeural_client/const/misc.py +20 -0
  34. naeural_client/const/payload.py +190 -0
  35. naeural_client/default/__init__.py +1 -0
  36. naeural_client/default/instance/__init__.py +4 -0
  37. naeural_client/default/instance/chain_dist_custom_job_01_plugin.py +54 -0
  38. naeural_client/default/instance/custom_web_app_01_plugin.py +118 -0
  39. naeural_client/default/instance/net_mon_01_plugin.py +45 -0
  40. naeural_client/default/instance/view_scene_01_plugin.py +28 -0
  41. naeural_client/default/session/mqtt_session.py +72 -0
  42. naeural_client/io_formatter/__init__.py +2 -0
  43. naeural_client/io_formatter/base/__init__.py +1 -0
  44. naeural_client/io_formatter/base/base_formatter.py +80 -0
  45. naeural_client/io_formatter/default/__init__.py +3 -0
  46. naeural_client/io_formatter/default/a_dummy.py +51 -0
  47. naeural_client/io_formatter/default/aixp1.py +113 -0
  48. naeural_client/io_formatter/default/default.py +22 -0
  49. naeural_client/io_formatter/io_formatter_manager.py +96 -0
  50. naeural_client/logging/__init__.py +1 -0
  51. naeural_client/logging/base_logger.py +2056 -0
  52. naeural_client/logging/logger_mixins/__init__.py +12 -0
  53. naeural_client/logging/logger_mixins/class_instance_mixin.py +92 -0
  54. naeural_client/logging/logger_mixins/computer_vision_mixin.py +443 -0
  55. naeural_client/logging/logger_mixins/datetime_mixin.py +344 -0
  56. naeural_client/logging/logger_mixins/download_mixin.py +421 -0
  57. naeural_client/logging/logger_mixins/general_serialization_mixin.py +242 -0
  58. naeural_client/logging/logger_mixins/json_serialization_mixin.py +481 -0
  59. naeural_client/logging/logger_mixins/pickle_serialization_mixin.py +301 -0
  60. naeural_client/logging/logger_mixins/process_mixin.py +63 -0
  61. naeural_client/logging/logger_mixins/resource_size_mixin.py +81 -0
  62. naeural_client/logging/logger_mixins/timers_mixin.py +501 -0
  63. naeural_client/logging/logger_mixins/upload_mixin.py +260 -0
  64. naeural_client/logging/logger_mixins/utils_mixin.py +675 -0
  65. naeural_client/logging/small_logger.py +93 -0
  66. naeural_client/logging/tzlocal/__init__.py +20 -0
  67. naeural_client/logging/tzlocal/unix.py +231 -0
  68. naeural_client/logging/tzlocal/utils.py +113 -0
  69. naeural_client/logging/tzlocal/win32.py +151 -0
  70. naeural_client/logging/tzlocal/windows_tz.py +718 -0
  71. naeural_client/plugins_manager_mixin.py +273 -0
  72. naeural_client/utils/__init__.py +2 -0
  73. naeural_client/utils/comm_utils.py +44 -0
  74. naeural_client/utils/dotenv.py +75 -0
  75. naeural_client-2.0.0.dist-info/METADATA +365 -0
  76. naeural_client-2.0.0.dist-info/RECORD +78 -0
  77. naeural_client-2.0.0.dist-info/WHEEL +4 -0
  78. naeural_client-2.0.0.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,1046 @@
1
+ import os
2
+ import base64
3
+ import json
4
+ import binascii
5
+ import numpy as np
6
+ import datetime
7
+
8
+ from hashlib import sha256, md5
9
+ from threading import Lock
10
+ from copy import deepcopy
11
+
12
+
13
+ from cryptography.hazmat.primitives import serialization
14
+
15
+
16
+ class BCct:
17
+ SIGN = 'EE_SIGN'
18
+ SENDER = 'EE_SENDER'
19
+ HASH = 'EE_HASH'
20
+
21
+ ADDR_PREFIX_OLD = "aixp_"
22
+ ADDR_PREFIX = "0xai_"
23
+
24
+ K_PEM_FILE = 'PEM_FILE'
25
+ K_PASSWORD = 'PASSWORD'
26
+ K_PEM_LOCATION = 'PEM_LOCATION'
27
+
28
+ ERR_UNAVL_MSG = "Missing signature/sender data"
29
+ ERR_UNAVL = 1
30
+
31
+ ERR_SIGN_MSG = "Bad hash"
32
+ ERR_UNAVL = 1000
33
+
34
+ ERR_SIGN_MSG = "Bad signature"
35
+ ERR_UNAVL = 1001
36
+
37
+ AUTHORISED_ADDRS = 'authorized_addrs'
38
+
39
+ DEFAULT_INFO = '0xai handshake data'
40
+
41
+
42
+ class _DotDict(dict):
43
+ __getattr__ = dict.__getitem__
44
+ __setattr__ = dict.__setitem__
45
+ __delattr__ = dict.__delitem__
46
+
47
+
48
+ class VerifyMessage(_DotDict):
49
+ def __init__(self):
50
+ self.valid = False
51
+ self.message = None
52
+ self.sender = None
53
+
54
+
55
+ NON_DATA_FIELDS = [BCct.HASH, BCct.SIGN, BCct.SENDER]
56
+
57
+ def replace_nan_inf(data, inplace=False):
58
+ assert isinstance(data, (dict, list)), "Only dictionaries and lists are supported"
59
+ if inplace:
60
+ d = data
61
+ else:
62
+ d = deepcopy(data)
63
+ stack = [d]
64
+ while stack:
65
+ current = stack.pop()
66
+ for key, value in current.items():
67
+ if isinstance(value, dict):
68
+ stack.append(value)
69
+ elif isinstance(value, list):
70
+ for item in value:
71
+ if isinstance(item, dict):
72
+ stack.append(item)
73
+ elif isinstance(value, float) and (np.isnan(value) or np.isinf(value)):
74
+ current[key] = None
75
+ return d
76
+
77
+ class _SimpleJsonEncoder(json.JSONEncoder):
78
+ """
79
+ Used to help jsonify numpy arrays or lists that contain numpy data types.
80
+ """
81
+ def default(self, obj):
82
+ if isinstance(obj, np.integer):
83
+ return int(obj)
84
+ elif isinstance(obj, np.floating):
85
+ return float(obj)
86
+ elif isinstance(obj, np.ndarray):
87
+ return obj.tolist()
88
+ elif isinstance(obj, np.ndarray):
89
+ return obj.tolist()
90
+ elif isinstance(obj, datetime.datetime):
91
+ return obj.strftime("%Y-%m-%d %H:%M:%S")
92
+ else:
93
+ return super(_SimpleJsonEncoder, self).default(obj)
94
+
95
+ class _ComplexJsonEncoder(json.JSONEncoder):
96
+ def default(self, obj):
97
+ if isinstance(obj, np.integer):
98
+ return int(obj)
99
+ elif isinstance(obj, np.floating):
100
+ return float(obj)
101
+ elif isinstance(obj, np.ndarray):
102
+ return obj.tolist()
103
+ elif isinstance(obj, datetime.datetime):
104
+ return obj.strftime("%Y-%m-%d %H:%M:%S")
105
+ else:
106
+ return super(_ComplexJsonEncoder, self).default(obj)
107
+
108
+ def iterencode(self, o, _one_shot=False):
109
+ """Encode the given object and yield each string representation as available."""
110
+ if self.check_circular:
111
+ markers = {}
112
+ else:
113
+ markers = None
114
+ if self.ensure_ascii:
115
+ _encoder = json.encoder.encode_basestring_ascii
116
+ else:
117
+ _encoder = json.encoder.encode_basestring
118
+
119
+ def floatstr(o, allow_nan=self.allow_nan, _repr=float.__repr__, _inf=json.encoder.INFINITY, _neginf=-json.encoder.INFINITY):
120
+ if o != o: # Check for NaN
121
+ text = 'null'
122
+ elif o == _inf:
123
+ text = 'null'
124
+ elif o == _neginf:
125
+ text = 'null'
126
+ else:
127
+ return repr(o).rstrip('0').rstrip('.') if '.' in repr(o) else repr(o)
128
+
129
+ if not allow_nan:
130
+ raise ValueError("Out of range float values are not JSON compliant: " + repr(o))
131
+
132
+ return text
133
+
134
+ _iterencode = json.encoder._make_iterencode(
135
+ markers, self.default, _encoder, self.indent, floatstr,
136
+ self.key_separator, self.item_separator, self.sort_keys,
137
+ self.skipkeys, _one_shot
138
+ )
139
+ return _iterencode(o, 0)
140
+
141
+ ## RIPEMD160
142
+
143
+ # Message schedule indexes for the left path.
144
+ ML = [
145
+ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,
146
+ 7, 4, 13, 1, 10, 6, 15, 3, 12, 0, 9, 5, 2, 14, 11, 8,
147
+ 3, 10, 14, 4, 9, 15, 8, 1, 2, 7, 0, 6, 13, 11, 5, 12,
148
+ 1, 9, 11, 10, 0, 8, 12, 4, 13, 3, 7, 15, 14, 5, 6, 2,
149
+ 4, 0, 5, 9, 7, 12, 2, 10, 14, 1, 3, 8, 11, 6, 15, 13
150
+ ]
151
+
152
+ # Message schedule indexes for the right path.
153
+ MR = [
154
+ 5, 14, 7, 0, 9, 2, 11, 4, 13, 6, 15, 8, 1, 10, 3, 12,
155
+ 6, 11, 3, 7, 0, 13, 5, 10, 14, 15, 8, 12, 4, 9, 1, 2,
156
+ 15, 5, 1, 3, 7, 14, 6, 9, 11, 8, 12, 2, 10, 0, 4, 13,
157
+ 8, 6, 4, 1, 3, 11, 15, 0, 5, 12, 2, 13, 9, 7, 10, 14,
158
+ 12, 15, 10, 4, 1, 5, 8, 7, 6, 2, 13, 14, 0, 3, 9, 11
159
+ ]
160
+
161
+ # Rotation counts for the left path.
162
+ RL = [
163
+ 11, 14, 15, 12, 5, 8, 7, 9, 11, 13, 14, 15, 6, 7, 9, 8,
164
+ 7, 6, 8, 13, 11, 9, 7, 15, 7, 12, 15, 9, 11, 7, 13, 12,
165
+ 11, 13, 6, 7, 14, 9, 13, 15, 14, 8, 13, 6, 5, 12, 7, 5,
166
+ 11, 12, 14, 15, 14, 15, 9, 8, 9, 14, 5, 6, 8, 6, 5, 12,
167
+ 9, 15, 5, 11, 6, 8, 13, 12, 5, 12, 13, 14, 11, 8, 5, 6
168
+ ]
169
+
170
+ # Rotation counts for the right path.
171
+ RR = [
172
+ 8, 9, 9, 11, 13, 15, 15, 5, 7, 7, 8, 11, 14, 14, 12, 6,
173
+ 9, 13, 15, 7, 12, 8, 9, 11, 7, 7, 12, 7, 6, 15, 13, 11,
174
+ 9, 7, 15, 11, 8, 6, 6, 14, 12, 13, 5, 14, 13, 13, 7, 5,
175
+ 15, 5, 8, 11, 14, 14, 6, 14, 6, 9, 12, 9, 12, 5, 15, 8,
176
+ 8, 5, 12, 9, 12, 5, 14, 6, 8, 13, 6, 5, 15, 13, 11, 11
177
+ ]
178
+
179
+ # K constants for the left path.
180
+ KL = [0, 0x5a827999, 0x6ed9eba1, 0x8f1bbcdc, 0xa953fd4e]
181
+
182
+ # K constants for the right path.
183
+ KR = [0x50a28be6, 0x5c4dd124, 0x6d703ef3, 0x7a6d76e9, 0]
184
+
185
+
186
+ def fi(x, y, z, i):
187
+ """The f1, f2, f3, f4, and f5 functions from the specification."""
188
+ if i == 0:
189
+ return x ^ y ^ z
190
+ elif i == 1:
191
+ return (x & y) | (~x & z)
192
+ elif i == 2:
193
+ return (x | ~y) ^ z
194
+ elif i == 3:
195
+ return (x & z) | (y & ~z)
196
+ elif i == 4:
197
+ return x ^ (y | ~z)
198
+ else:
199
+ assert False
200
+
201
+
202
+ def rol(x, i):
203
+ """Rotate the bottom 32 bits of x left by i bits."""
204
+ return ((x << i) | ((x & 0xffffffff) >> (32 - i))) & 0xffffffff
205
+
206
+
207
+ def compress(h0, h1, h2, h3, h4, block):
208
+ """Compress state (h0, h1, h2, h3, h4) with block."""
209
+ # Left path variables.
210
+ al, bl, cl, dl, el = h0, h1, h2, h3, h4
211
+ # Right path variables.
212
+ ar, br, cr, dr, er = h0, h1, h2, h3, h4
213
+ # Message variables.
214
+ x = [int.from_bytes(block[4*i:4*(i+1)], 'little') for i in range(16)]
215
+
216
+ # Iterate over the 80 rounds of the compression.
217
+ for j in range(80):
218
+ rnd = j >> 4
219
+ # Perform left side of the transformation.
220
+ al = rol(al + fi(bl, cl, dl, rnd) + x[ML[j]] + KL[rnd], RL[j]) + el
221
+ al, bl, cl, dl, el = el, al, bl, rol(cl, 10), dl
222
+ # Perform right side of the transformation.
223
+ ar = rol(ar + fi(br, cr, dr, 4 - rnd) + x[MR[j]] + KR[rnd], RR[j]) + er
224
+ ar, br, cr, dr, er = er, ar, br, rol(cr, 10), dr
225
+
226
+ # Compose old state, left transform, and right transform into new state.
227
+ return h1 + cl + dr, h2 + dl + er, h3 + el + ar, h4 + al + br, h0 + bl + cr
228
+
229
+
230
+ def ripemd160(data):
231
+ """Compute the RIPEMD-160 hash of data."""
232
+ # Initialize state.
233
+ state = (0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476, 0xc3d2e1f0)
234
+ # Process full 64-byte blocks in the input.
235
+ for b in range(len(data) >> 6):
236
+ state = compress(*state, data[64*b:64*(b+1)])
237
+ # Construct final blocks (with padding and size).
238
+ pad = b"\x80" + b"\x00" * ((119 - len(data)) & 63)
239
+ fin = data[len(data) & ~63:] + pad + (8 * len(data)).to_bytes(8, 'little')
240
+ # Process final blocks.
241
+ for b in range(len(fin) >> 6):
242
+ state = compress(*state, fin[64*b:64*(b+1)])
243
+ # Produce output.
244
+ return b"".join((h & 0xffffffff).to_bytes(4, 'little') for h in state)
245
+
246
+ # END ## RIPEMD160
247
+
248
+ class BaseBlockEngine:
249
+ """
250
+ This multiton (multi-singleton via key) is the base workhorse of the private blockchain.
251
+
252
+ Parameters
253
+ ----------
254
+
255
+ name: str
256
+ the name of the engine. Used to create the private key file name.
257
+
258
+ config: dict
259
+ the configuration dict that contains the PEM_FILE, PASSWORD, PEM_LOCATION keys
260
+ for configuring the private key file access
261
+
262
+ log: Logger object
263
+ the Logger object
264
+
265
+ ensure_ascii_payloads: bool
266
+ flag that controls if the payloads are encoded as ascii or not. Default `False` for JS compatibility.
267
+
268
+ """
269
+ _lock: Lock = Lock()
270
+ __instances = {}
271
+
272
+ def __new__(cls, name, config, log, ensure_ascii_payloads=False, verbosity=1):
273
+ with cls._lock:
274
+ if name not in cls.__instances:
275
+ instance = super(BaseBlockEngine, cls).__new__(cls)
276
+ instance._build(
277
+ name=name, log=log, config=config,
278
+ ensure_ascii_payloads=ensure_ascii_payloads,
279
+ verbosity=verbosity,
280
+ )
281
+ cls.__instances[name] = instance
282
+ else:
283
+ instance = cls.__instances[name]
284
+ return instance
285
+
286
+ def _build(
287
+ self,
288
+ name,
289
+ config:dict,
290
+ log=None,
291
+ ensure_ascii_payloads=False,
292
+ verbosity=1,
293
+ ):
294
+
295
+ self.__name = name
296
+ assert log is not None, "Logger object was not provided!"
297
+
298
+ self.log = log
299
+ self.__private_key = None
300
+ self.__verbosity = verbosity
301
+ self.__public_key = None
302
+ self.__password = config.get(BCct.K_PASSWORD)
303
+ self.__config = config
304
+ self.__ensure_ascii_payloads = ensure_ascii_payloads
305
+
306
+ pem_name = config.get(BCct.K_PEM_FILE, '_pk.pem')
307
+ pem_folder = config.get(BCct.K_PEM_LOCATION, 'data')
308
+ pem_fn = os.path.join(log.get_target_folder(pem_folder), pem_name)
309
+ self.__pem_file = pem_fn
310
+
311
+ self._init()
312
+ return
313
+
314
+ def P(self, s, color=None, boxed=False, verbosity=1, **kwargs):
315
+ if verbosity > self.__verbosity:
316
+ return
317
+ if not boxed:
318
+ s = "<BC:{}> ".format(self.__name) + s
319
+ return self.log.P(
320
+ s,
321
+ color='g' if (color is None or color.lower() not in ['r', 'red', 'error']) else color,
322
+ boxed=boxed,
323
+ **kwargs
324
+ )
325
+
326
+
327
+ def _init(self):
328
+ self.P("Initializing Blockchain engine manager...", boxed=True, box_char='*', verbosity=1)
329
+
330
+ if True:
331
+ self.P("Initializing private blockchain:\n{}".format(
332
+ json.dumps(self.__config, indent=4)), verbosity=2
333
+ )
334
+ if self.__pem_file is not None:
335
+ try:
336
+ full_path = os.path.abspath(self.__pem_file)
337
+ self.P("Trying to load sk from {}".format(full_path), verbosity=1)
338
+ self.__private_key = self._text_to_sk(
339
+ source=self.__pem_file,
340
+ from_file=True,
341
+ password=self.__password,
342
+ )
343
+ self.P(" Loaded sk from {}".format(full_path), verbosity=1)
344
+ except:
345
+ self.P(" Failed to load sk from {}".format(full_path), color='r', verbosity=1)
346
+
347
+ if self.__private_key is None:
348
+ self.P("Creating new private key", verbosity=1)
349
+ self.__private_key = self._create_new_sk()
350
+ self._sk_to_text(
351
+ private_key=self.__private_key,
352
+ password=self.__password,
353
+ fn=self.__pem_file,
354
+ )
355
+ self.__public_key = self._get_pk(private_key=self.__private_key)
356
+ self.__address = self._pk_to_address(self.__public_key)
357
+ self.P("Current address: {}".format(self.address), boxed=True, verbosity=1)
358
+ self.P("Allowed list of senders: {}".format(self.allowed_list), verbosity=1)
359
+ return
360
+
361
+ @property
362
+ def private_key(self):
363
+ return self.__private_key
364
+
365
+
366
+ @staticmethod
367
+ def _compute_hash(data : bytes, method='SHA256'):
368
+ """
369
+ Computes the hash of a `bytes` data message
370
+
371
+ Parameters
372
+ ----------
373
+ data : bytes
374
+ the input message usually obtained from a bynary jsoned dict.
375
+
376
+ method : str, optional
377
+ the hash algo. The default is 'HASH160'.
378
+
379
+
380
+ Returns
381
+ -------
382
+ result : bytes, str
383
+ hash both in bin and text format.
384
+
385
+ """
386
+ result = None, None
387
+ method = method.upper()
388
+ assert method in ['HASH160', 'SHA256', 'MD5']
389
+
390
+ if method == 'MD5':
391
+ hash_obj = md5(data)
392
+ result = hash_obj.digest(), hash_obj.hexdigest()
393
+ elif method == 'SHA256':
394
+ hash_obj = sha256(data)
395
+ result = hash_obj.digest(), hash_obj.hexdigest()
396
+ elif method == 'HASH160':
397
+ hb_sha256 = sha256(data).digest()
398
+ hb_h160 = ripemd160(hb_sha256)
399
+ result = hb_h160, binascii.hexlify(hb_h160).decode()
400
+ return result
401
+
402
+
403
+ @staticmethod
404
+ def _binary_to_text(data : bytes, method='base64'):
405
+ """
406
+ Encodes a bytes message as text
407
+
408
+ Parameters
409
+ ----------
410
+ data : bytes
411
+ the binary data, usually a signature, hash, etc.
412
+
413
+ method : str, optional
414
+ the method - 'base64' or other. The default is 'base64'.
415
+
416
+
417
+ Returns
418
+ -------
419
+ txt : str
420
+ the base64 or hexlified text.
421
+
422
+ """
423
+ assert isinstance(data, bytes)
424
+ if method == 'base64':
425
+ txt = base64.urlsafe_b64encode(data).decode()
426
+ else:
427
+ txt = binascii.hexlify(data).decode()
428
+ return txt
429
+
430
+
431
+ @staticmethod
432
+ def _text_to_binary(text : str, method='base64'):
433
+ """
434
+ Convert from str/text to binary
435
+
436
+ Parameters
437
+ ----------
438
+ text : str
439
+ the message.
440
+
441
+ method : str, optional
442
+ the conversion method. The default is 'base64'.
443
+
444
+
445
+ Returns
446
+ -------
447
+ data : bytes
448
+ the decoded binary message.
449
+
450
+ """
451
+ assert isinstance(text, str), "Cannot convert non text to binary"
452
+ if method == 'base64':
453
+ data = base64.urlsafe_b64decode(text)
454
+ else:
455
+ data = binascii.unhexlify(text)
456
+ return data
457
+
458
+
459
+ @staticmethod
460
+ def _get_pk(private_key):
461
+ """
462
+ Simple wrapper to generate pk from sk
463
+
464
+
465
+ Returns
466
+ -------
467
+ public_key : pk
468
+
469
+ """
470
+ return private_key.public_key()
471
+
472
+
473
+ def _get_allowed_file(self):
474
+ """
475
+ Return the file path for the autorized addresses
476
+ """
477
+ folder = self.log.base_folder
478
+ path = os.path.join(folder, BCct.AUTHORISED_ADDRS)
479
+ return path
480
+
481
+ def _load_and_maybe_create_allowed(self):
482
+ fn = self._get_allowed_file()
483
+ lst_allowed = []
484
+ if os.path.isfile(fn):
485
+ with open(fn, 'rt') as fh:
486
+ lst_allowed = fh.readlines()
487
+ else:
488
+ full_path = os.path.abspath(fn)
489
+ self.P("WARNING: no `{}` file found. Creating empty one.".format(full_path), verbosity=1)
490
+ with open(fn, 'wt') as fh:
491
+ fh.write('\n')
492
+ lst_allowed = [x.strip() for x in lst_allowed]
493
+ lst_allowed = [x.split()[0] for x in lst_allowed if x != '']
494
+ lst_allowed = [self._remove_prefix(x) for x in lst_allowed if x != '']
495
+ return lst_allowed
496
+
497
+
498
+ def _remove_prefix(self, address):
499
+ """
500
+ Removes the prefix from the address
501
+
502
+ Parameters
503
+ ----------
504
+ address : str
505
+ the text address.
506
+
507
+ Returns
508
+ -------
509
+ address : str
510
+ the address without the prefix.
511
+ """
512
+ if address.startswith(BCct.ADDR_PREFIX):
513
+ address = address[len(BCct.ADDR_PREFIX):]
514
+ elif address.startswith(BCct.ADDR_PREFIX_OLD):
515
+ address = address[len(BCct.ADDR_PREFIX_OLD):]
516
+ return address
517
+
518
+ def _pk_to_address(self, public_key):
519
+ """
520
+ Given a pk object will return the simple text address.
521
+
522
+ OBS: Should be overwritten in particular implementations using X962
523
+
524
+
525
+ Parameters
526
+ ----------
527
+ public_key : pk
528
+ the pk object.
529
+
530
+ Returns
531
+ -------
532
+ text address
533
+
534
+ """
535
+ data = public_key.public_bytes(
536
+ encoding=serialization.Encoding.DER, # will encode the full pk information
537
+ format=serialization.PublicFormat.SubjectPublicKeyInfo, # used with DER
538
+ )
539
+ txt = BCct.ADDR_PREFIX + self._binary_to_text(data)
540
+ return txt
541
+
542
+
543
+ def _address_to_pk(self, address):
544
+ """
545
+ Given a address will return the pk object
546
+
547
+ OBS: Should be overwritten in particular implementations using X962
548
+
549
+
550
+ Parameters
551
+ ----------
552
+ address : str
553
+ the text address (pk).
554
+
555
+
556
+ Returns
557
+ -------
558
+ pk : pk
559
+ the pk object.
560
+
561
+ """
562
+ simple_address = self._remove_prefix(address)
563
+ bpublic_key = self._text_to_binary(simple_address)
564
+ # below works for DER / SubjectPublicKeyInfo
565
+ public_key = serialization.load_der_public_key(bpublic_key)
566
+ return public_key
567
+
568
+
569
+ def _text_to_sk(self, source, from_file=False, password=None):
570
+ """
571
+ Construct a EllipticCurvePrivateKey from a text sk
572
+
573
+ Parameters
574
+ ----------
575
+ source : str
576
+ the text secret key or the file name if `from_file == True`.
577
+
578
+ from_file: bool
579
+ flag that allows source to be a file
580
+
581
+
582
+ Returns
583
+ -------
584
+ sk
585
+
586
+ """
587
+ if from_file and os.path.isfile(source):
588
+ self.P("Reading SK from '{}'".format(source), color='g', verbosity=1)
589
+ with open(source, 'rt') as fh:
590
+ data = fh.read()
591
+ else:
592
+ data = source
593
+
594
+ bdata = data.encode()
595
+ if password:
596
+ pass_data = password.encode()
597
+ else:
598
+ pass_data = None
599
+ private_key = serialization.load_pem_private_key(bdata, pass_data)
600
+ return private_key
601
+
602
+ def _sk_to_text(self, private_key, password=None, fn=None):
603
+ """
604
+ Serialize a sk as text
605
+
606
+ Parameters
607
+ ----------
608
+ private_key : sk
609
+ the secret key object.
610
+
611
+ password: str
612
+ password to be used for sk serialization
613
+
614
+ fn: str:
615
+ text file where to save the pk
616
+
617
+ Returns
618
+ -------
619
+ the sk as text string
620
+
621
+ """
622
+ if password:
623
+ encryption_algorithm = serialization.BestAvailableEncryption(password.encode())
624
+ else:
625
+ encryption_algorithm = serialization.NoEncryption()
626
+
627
+ pem = private_key.private_bytes(
628
+ encoding=serialization.Encoding.PEM,
629
+ format=serialization.PrivateFormat.PKCS8,
630
+ encryption_algorithm=encryption_algorithm
631
+ )
632
+ str_pem = pem.decode()
633
+ if fn is not None:
634
+ full_path = os.path.abspath(fn)
635
+ self.P("Writing PEM-encoded key to {}".format(full_path), color='g', verbosity=2)
636
+ with open(fn, 'wt') as fh:
637
+ fh.write(str_pem)
638
+ return str_pem
639
+
640
+
641
+ def _dict_to_json(self, dct_data, replace_nan=True, inplace=True):
642
+ if replace_nan:
643
+ dct_safe_data = replace_nan_inf(dct_data, inplace=inplace)
644
+ else:
645
+ dct_safe_data = dct_data
646
+
647
+ dumps_config = dict(
648
+ sort_keys=True,
649
+ cls=_ComplexJsonEncoder,
650
+ separators=(',',':'),
651
+ ensure_ascii=self.__ensure_ascii_payloads,
652
+ )
653
+ str_data = json.dumps(dct_safe_data, **dumps_config)
654
+ return str_data
655
+
656
+ def _create_new_sk(self):
657
+ """
658
+ Simple wrapper to generated pk
659
+
660
+
661
+ Returns
662
+ -------
663
+ private_key : sk
664
+
665
+ """
666
+ raise NotImplementedError()
667
+
668
+
669
+
670
+ def _verify(self, public_key, signature : bytes, data : bytes):
671
+ """
672
+ Verifies a `pk` signature on a binary `data` package
673
+
674
+
675
+ Parameters
676
+ ----------
677
+ public_key : pk type
678
+ the pk object.
679
+
680
+ signature : bytes
681
+ the binary signature.
682
+
683
+ data : bytes
684
+ the binary message.
685
+
686
+
687
+ Returns
688
+ -------
689
+ result: _DotDict
690
+ contains `result.ok` and `result.message`
691
+
692
+ """
693
+ raise NotImplementedError()
694
+
695
+
696
+
697
+ def _sign(self, data : bytes, private_key, text=False):
698
+ """
699
+ Sign a binary message with Elliptic Curve
700
+
701
+
702
+ Parameters
703
+ ----------
704
+ data : bytes
705
+ the binary message.
706
+
707
+ private_key : pk
708
+ the private key object.
709
+
710
+ text : bool, optional
711
+ return the signature as text. The default is False.
712
+
713
+ Returns
714
+ -------
715
+ signature as text or binary
716
+
717
+ """
718
+ raise NotImplementedError()
719
+
720
+
721
+
722
+ #############################################################################
723
+ #### ####
724
+ #### Public functions ####
725
+ #### ####
726
+ #############################################################################
727
+
728
+ @property
729
+ def address(self):
730
+ """Returns the public address"""
731
+ return self.__address
732
+
733
+ @property
734
+ def address_no_prefix(self):
735
+ """Returns the public address without the prefix"""
736
+ return self._remove_prefix(self.address)
737
+
738
+ @property
739
+ def allowed_list(self):
740
+ """Returns the allowed command senders for the current node"""
741
+ return self._load_and_maybe_create_allowed()
742
+
743
+ @property
744
+ def whitelist(self):
745
+ """Returns the allowed command senders for the current node"""
746
+ return self.allowed_list
747
+
748
+
749
+ def maybe_remove_prefix(self, address):
750
+ """
751
+ Removes the prefix from the address
752
+
753
+ Parameters
754
+ ----------
755
+ address : str
756
+ the text address.
757
+
758
+ Returns
759
+ -------
760
+ address : str
761
+ the address without the prefix.
762
+ """
763
+ return self._remove_prefix(address)
764
+
765
+
766
+ def dict_digest(self, dct_data, return_str=True):
767
+ """Generates the hash of a dict object given as parameter"""
768
+ str_data = self._dict_to_json(dct_data, replace_nan=True)
769
+ bin_hex_hash, hex_hash = self._compute_hash(str_data.encode())
770
+ if return_str:
771
+ return hex_hash
772
+ else:
773
+ return bin_hex_hash
774
+
775
+
776
+ def save_sk(self, fn, password=None):
777
+ """
778
+ Saves the SK with or without password
779
+
780
+ Parameters
781
+ ----------
782
+ fn : str
783
+ SK file name.
784
+ password : str, optional
785
+ optional password. The default is None.
786
+
787
+ Returns
788
+ -------
789
+ fn : str
790
+ saved file name.
791
+
792
+ """
793
+ self.P("Serializing the private key...", verbosity=2)
794
+ _ = self._sk_to_text(
795
+ private_key=self.__private_key,
796
+ password=password,
797
+ fn=fn
798
+ )
799
+ return fn
800
+
801
+
802
+ def _generate_data_for_hash(self, dct_data, replace_nan=True):
803
+ """
804
+ Will convert the dict to json (removing the non-data fields) and return the json string.
805
+ The dict will be modified inplace to replace NaN and Inf with None.
806
+ """
807
+ assert isinstance(dct_data, dict), "Cannot compute hash on non-dict data"
808
+ dct_only_data = {k:dct_data[k] for k in dct_data if k not in NON_DATA_FIELDS}
809
+ str_data = self._dict_to_json(
810
+ dct_only_data,
811
+ replace_nan=replace_nan,
812
+ inplace=True # will replace inplace the np.nan and np.inf with None
813
+ )
814
+ return str_data
815
+
816
+
817
+ def compute_hash(self, dct_data, return_all=False, replace_nan=True):
818
+ """
819
+ Computes the hash of a dict object
820
+
821
+ Parameters
822
+ ----------
823
+ dct_data : dict
824
+ the input message as a dict.
825
+
826
+ return_all: bool, optional
827
+ if `True` will return the binary hash as well. Default `False`
828
+
829
+ replace_nan: bool, optional
830
+ will replace inplace `np.nan` and `np.inf` with `None` before hashing. Default `True`
831
+
832
+ Returns
833
+ -------
834
+ result : str or tuple(bytes, bytes, str) if `return_all` is `True`
835
+
836
+ """
837
+ str_data = self._generate_data_for_hash(dct_data, replace_nan=replace_nan)
838
+ bdata = bytes(str_data, 'utf-8')
839
+ bin_hexdigest, hexdigest = self._compute_hash(bdata)
840
+ if return_all:
841
+ result = bdata, bin_hexdigest, hexdigest
842
+ else:
843
+ result = hexdigest
844
+ return result
845
+
846
+
847
+ def sign(self, dct_data: dict, add_data=True, use_digest=True, replace_nan=True) -> str:
848
+ """
849
+ Generates the signature for a dict object.
850
+ Does not add the signature to the dict object
851
+
852
+
853
+ Parameters
854
+ ----------
855
+ dct_data : dict
856
+ the input message as a dict.
857
+
858
+ add_data: bool, optional
859
+ will add signature and address to the data dict (also digest if required). Default `True`
860
+
861
+ use_digest: bool, optional
862
+ will compute data hash and sign only on hash
863
+
864
+ replace_nan: bool, optional
865
+ will replace `np.nan` and `np.inf` with `None` before signing.
866
+
867
+ Returns
868
+ -------
869
+ text signature
870
+
871
+
872
+ IMPORTANT:
873
+ It is quite probable that the same sign(sk, hash) will generate different signatures
874
+
875
+ """
876
+ result = None
877
+ assert isinstance(dct_data, dict), "Cannot sign on non-dict data"
878
+
879
+ bdata, bin_hexdigest, hexdigest = self.compute_hash(
880
+ dct_data,
881
+ return_all=True,
882
+ replace_nan=replace_nan,
883
+ )
884
+ if use_digest:
885
+ bdata = bin_hexdigest # to-sign data is the hash
886
+ # finally sign either full or just hash
887
+ result = self._sign(data=bdata, private_key=self.__private_key, text=True)
888
+ if add_data:
889
+ # not populate dict
890
+ dct_data[BCct.SIGN] = result
891
+ dct_data[BCct.SENDER] = self.address
892
+ if use_digest:
893
+ dct_data[BCct.HASH] = hexdigest
894
+ return result
895
+
896
+
897
+
898
+ def verify(
899
+ self,
900
+ dct_data: dict,
901
+ signature: str=None,
902
+ sender_address: str=None,
903
+ return_full_info=True,
904
+ verify_allowed=False,
905
+ replace_nan=True,
906
+ log_hash_sign_fails=True,
907
+ ) -> bool:
908
+ """
909
+ Verifies the signature validity of a given text message
910
+
911
+ Parameters
912
+ ----------
913
+ dct_data : dict
914
+ dict object that needs to be verified against the signature.
915
+
916
+ signature : str, optional
917
+ the text encoded signature. Extracted from dict if missing
918
+
919
+ sender_address : str, optional
920
+ the text encoded public key. Extracted from dict if missing
921
+
922
+ return_full_info: bool, optional
923
+ if `True` will return more than `True/False` for signature verification
924
+
925
+ verify_allowed: bool, optional
926
+ if true will also check if the address is allowed by calling `check_allowed`
927
+
928
+ replace_nan: bool, optional
929
+ will replace `np.nan` and `np.inf` with `None` before verifying. Default `True`
930
+
931
+ log_hash_sign_fails: bool, optional
932
+ if `True` will log the verification failures for hash and signature issues. Default `True`
933
+
934
+
935
+ Returns
936
+ -------
937
+ bool / VerifyMessage
938
+ returns `True` if signature verifies else `False`. returns `VerifyMessage` if return_full_info
939
+
940
+ """
941
+ result = False
942
+
943
+ bdata_json, bin_hexdigest, hexdigest = self.compute_hash(
944
+ dct_data,
945
+ return_all=True,
946
+ replace_nan=replace_nan,
947
+ )
948
+
949
+ if signature is None:
950
+ signature = dct_data.get(BCct.SIGN)
951
+
952
+ if sender_address is None:
953
+ sender_address = dct_data.get(BCct.SENDER)
954
+
955
+ verify_msg = VerifyMessage()
956
+ verify_msg.sender = sender_address
957
+
958
+ received_digest = dct_data.get(BCct.HASH)
959
+ if received_digest:
960
+ # we need to verify hash and then verify signature on hash
961
+ if hexdigest != received_digest:
962
+ verify_msg.message = "Corrupted digest!"
963
+ verify_msg.valid = False
964
+ #endif hash failed
965
+ bdata = bin_hexdigest
966
+ else:
967
+ # normal signature on data
968
+ bdata = bdata_json
969
+ #endif has hash or not
970
+
971
+ if verify_msg.message is None:
972
+ try:
973
+ assert sender_address is not None, 'Sender address is NULL'
974
+ assert signature is not None, 'Signature is NULL'
975
+
976
+ bsignature = self._text_to_binary(signature)
977
+ pk = self._address_to_pk(sender_address)
978
+ verify_msg = self._verify(public_key=pk, signature=bsignature, data=bdata)
979
+ except Exception as exc:
980
+ verify_msg.message = str(exc)
981
+ verify_msg.valid = False
982
+ #endif check if signature failed already from digesting
983
+
984
+ verify_msg.sender = sender_address
985
+
986
+ if not verify_msg.valid:
987
+ if log_hash_sign_fails and signature is not None and sender_address is not None:
988
+ self.P("Signature failed on msg from {}: {}".format(
989
+ sender_address, verify_msg.message
990
+ ), color='r', verbosity=1,
991
+ )
992
+ elif verify_allowed and verify_msg.valid:
993
+ if not self.is_allowed(sender_address):
994
+ verify_msg.message = "Signature ok but address {} not in {}.".format(sender_address, BCct.AUTHORISED_ADDRS)
995
+ verify_msg.valid = False
996
+ #endif not allowed
997
+ #endif ok but authorization required
998
+
999
+ if return_full_info:
1000
+ result = verify_msg
1001
+ else:
1002
+ result = verify_msg.ok
1003
+ return result
1004
+
1005
+
1006
+ def is_allowed(self, sender_address: str):
1007
+ to_search_address = self._remove_prefix(sender_address)
1008
+ is_allowed = to_search_address in self.allowed_list or to_search_address == self._remove_prefix(self.address)
1009
+ return is_allowed
1010
+
1011
+
1012
+ def encrypt(self, data, destination):
1013
+ """
1014
+ Encrypts the data for a given destination
1015
+
1016
+ Parameters
1017
+ ----------
1018
+ data : dict
1019
+ the data to be encrypted.
1020
+
1021
+ destination : str
1022
+ the destination address.
1023
+
1024
+ Returns
1025
+ -------
1026
+ None.
1027
+
1028
+ """
1029
+ raise NotImplementedError()
1030
+
1031
+ def decrypt(self, data):
1032
+ """
1033
+ Decrypts the data
1034
+
1035
+ Parameters
1036
+ ----------
1037
+ data : dict
1038
+ the data to be decrypted.
1039
+
1040
+ Returns
1041
+ -------
1042
+ None.
1043
+
1044
+ """
1045
+ raise NotImplementedError()
1046
+