vcon 0.1__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.
vcon/__init__.py ADDED
@@ -0,0 +1,1353 @@
1
+ """
2
+ Module for creating and modifying vCon conversation containers.
3
+ see https:/vcon.dev
4
+ """
5
+ # need future to reference Vcon type in Vcon methods
6
+ from __future__ import annotations
7
+ import importlib
8
+ import pkgutil
9
+ import typing
10
+ import sys
11
+ import os
12
+ import logging
13
+ import logging.config
14
+ import pythonjsonlogger.jsonlogger
15
+
16
+
17
+ def build_logger(name : str) -> logging.Logger:
18
+ logger = logging.getLogger(name)
19
+
20
+ log_config_filename = "./logging.conf"
21
+ if(os.path.isfile(log_config_filename)):
22
+ logging.config.fileConfig(log_config_filename)
23
+ #print("got logging config", file=sys.stderr)
24
+ else:
25
+ logger.setLevel(logging.DEBUG)
26
+
27
+ # Output to stdout WILL BREAK the Vcon CLI.
28
+ # MUST use stderr.
29
+ handler = logging.StreamHandler(sys.stderr)
30
+ handler.setLevel(logging.DEBUG)
31
+ formatter = pythonjsonlogger.jsonlogger.JsonFormatter( "%(timestamp)s %(levelname)s %(message)s ", timestamp=True)
32
+ handler.setFormatter(formatter)
33
+ logger.addHandler(handler)
34
+
35
+ return(logger)
36
+
37
+ logger = build_logger(__name__)
38
+
39
+ try:
40
+ import simplejson as json
41
+ dumps_options = {"ignore_nan" : True}
42
+ logger.info("using simplejson")
43
+ except Exception as import_error:
44
+ import json
45
+ dumps_options = {}
46
+ logger.info("using json")
47
+
48
+ import enum
49
+ import time
50
+ import hashlib
51
+ import inspect
52
+ import functools
53
+ import warnings
54
+ import datetime
55
+ import uuid6
56
+ import requests
57
+ import vcon.utils
58
+ import vcon.security
59
+ import vcon.filter_plugins
60
+ import jose.utils
61
+ import jose.jws
62
+ import jose.jwe
63
+
64
+ _LAST_V8_TIMESTAMP = None
65
+
66
+ for finder, module_name, is_package in pkgutil.iter_modules(vcon.filter_plugins.__path__, vcon.filter_plugins.__name__ + "."):
67
+ logger.info("plugin registration: {}".format(module_name))
68
+ importlib.import_module(module_name)
69
+
70
+ def deprecated(reason : str):
71
+ """
72
+ Decorator for marking and emmiting warnings on deprecated methods and classes
73
+ """
74
+
75
+ def decorator(func):
76
+ if inspect.isclass(func):
77
+ msg = "Call to deprecated class {{}} ({}).".format(reason)
78
+ else:
79
+ msg = "Call to deprecated function {{}} ({}).".format(reason)
80
+
81
+ @functools.wraps(func)
82
+ def new_func(*args, **kwargs):
83
+ warnings.simplefilter('always', DeprecationWarning)
84
+ warnings.warn(
85
+ msg.format(func.__name__),
86
+ category=DeprecationWarning,
87
+ stacklevel=2)
88
+ warnings.simplefilter('default', DeprecationWarning)
89
+ return func(*args, **kwargs)
90
+
91
+ return new_func
92
+
93
+ return decorator
94
+
95
+ class VconStates(enum.Enum):
96
+ """ Vcon states WRT signing and verification """
97
+ UNKNOWN = 0
98
+ UNSIGNED = 1
99
+ SIGNED = 2
100
+ UNVERIFIED = 3
101
+ VERIFIED = 4
102
+ ENCRYPTED = 5
103
+ DECRYPTED = 6
104
+
105
+
106
+ class UnsupportedVconVersion(Exception):
107
+ """ Thrown if vcon version string is not of set of versions supported by this package"""
108
+
109
+ class UnverifiedVcon(Exception):
110
+ """ Payload is signed, but not verified. Must be verified before reading data """
111
+
112
+ class InvalidVconState(Exception):
113
+ """ Vcon is in an invalid state for a given operation """
114
+
115
+ class InvalidVconJson(Exception):
116
+ """ JSON not valid for Vcon """
117
+
118
+ class InvalidVconHash(Exception):
119
+ """ Hash does not match the content/body """
120
+
121
+ class InvalidVconSignature(Exception):
122
+ """ Signature does not match the content"""
123
+
124
+
125
+ class VconAttribute:
126
+ """ descriptor base class for attributes in vcon """
127
+ def __init__(self, doc : str = None):
128
+ self._type_name = ""
129
+ self.name = None
130
+ if(doc is not None):
131
+ self.__doc__ = doc
132
+
133
+ def __set_name__(self, owner_class, name):
134
+ #print("defining new Vcon{}: {}".format(self._type_name, name))
135
+ self.name = name
136
+
137
+ def __get__(self, instance_object, class_type = None):
138
+ #print("getting: {} inst type: {} class type: {}".format(self.name, type(instance_object), type(class_type)))
139
+ # TODO: once signed, this should return a read only attribute
140
+ # This may be done by overloading the __get__ method in derived classes
141
+
142
+ if(instance_object._state in [VconStates.UNVERIFIED, VconStates.DECRYPTED]):
143
+ raise UnverifiedVcon("vCon is signed, but not verified. Call verify before reading data.")
144
+
145
+ if(instance_object._state in [VconStates.ENCRYPTED]):
146
+ raise UnverifiedVcon("vCon is encrypted. Call decrypt and verify before reading data.")
147
+
148
+ return(instance_object._vcon_dict.get(self.name, None))
149
+
150
+ def __set__(self, instance_object, value : str) -> None:
151
+ raise AttributeError("not allowed to replace {} {}".format(self.name, self._type_name))
152
+
153
+ class VconString(VconAttribute):
154
+ """ descriptor for String attributes in vcon """
155
+ def __init__(self, doc : str = None):
156
+ super().__init__(doc = doc)
157
+ self._type_name = "String"
158
+
159
+ class VconDict(VconAttribute):
160
+ """ descriptor for Lists of dicts in vcon """
161
+
162
+ def __init__(self, doc : str = None):
163
+ super().__init__(doc = doc)
164
+ self._type_name = "Dict"
165
+
166
+ class VconDictList(VconAttribute):
167
+ """ descriptor for Lists of dicts in vcon """
168
+
169
+ def __init__(self, doc : str = None):
170
+ super().__init__(doc = doc)
171
+ self._type_name = "DictList"
172
+
173
+ class VconPluginMethodType():
174
+ """ Class defining descriptor used to instantiate methods for the named filter plugins """
175
+ def __init__(self, filter_name, vcon_instance):
176
+ self.__function_name__ = filter_name
177
+ self.__self__ = vcon_instance
178
+ if(not isinstance(vcon_instance, vcon.Vcon)):
179
+ AttributeError("vcon_instance should be a Vcon not {}".format(type(vcon_instance)))
180
+
181
+ #print("added func: {} for obj: {} type{}".format(filter_name, vcon_instance, type(vcon_instance)))
182
+
183
+ def __call__(self, *args, **kwargs):
184
+ obj = self.__self__
185
+ #print("__call__ args: {}".format(args))
186
+ #print("__call__ kwargs: {}".format(kwargs))
187
+ #print("calling filter for {} create: {} num dialogs: {}".format(self.__function_name__, obj.created_at, len(obj.dialog)))
188
+ return(vcon.Vcon.filter(obj, self.__function_name__, **kwargs))
189
+
190
+ class VconPluginMethodProperty:
191
+ def __init__(self, plugin_name : str):
192
+ #print("VconPluginMethodProperty.__init__ {}".format(plugin_name))
193
+ self.plugin_name = plugin_name
194
+
195
+ def __get__(self, instance_object, class_type = None):
196
+ #print("__get__ on {}".format(self.plugin_name))
197
+ if(instance_object is None):
198
+ return(self)
199
+
200
+ return(VconPluginMethodType(self.plugin_name, instance_object))
201
+
202
+ class Vcon():
203
+ """
204
+ Constructor, Serializer and Deserializer for vCon conversation data container.
205
+
206
+ Attributes:
207
+ See Data descriptors under help(vcon.Vcon)
208
+
209
+ """
210
+
211
+ # Some commonly used MIME types for convenience
212
+ MIMETYPE_TEXT_PLAIN = "text/plain"
213
+ MIMETYPE_AUDIO_WAV = "audio/x-wav"
214
+ MIMETYPE_AUDIO_MP3 = "audio/x-mp3"
215
+ MIMETYPE_AUDIO_MP4 = "audio/x-mp4"
216
+ MIMETYPE_VIDEO_MP4 = "video/x-mp4"
217
+ MIMETYPE_VIDEO_OGG = "video/ogg"
218
+ MIMETYPE_MULTIPART = "multipart/mixed"
219
+
220
+ # Dict keys
221
+ VCON_VERSION = "vcon"
222
+ UUID = "uuid"
223
+ SUBJECT = "subject"
224
+ REDACTED = "redacted"
225
+ APPENDED = "appended"
226
+ GROUP = "group"
227
+ PARTIES = "parties"
228
+ DIALOG = "dialog"
229
+ ANALYSIS = "analysis"
230
+ ATTACHMENTS = "attachments"
231
+ CREATED_AT = "created_at"
232
+
233
+ PARTIES_OBJECT_STRING_PARAMETERS = ["tel", "stir", "mailto", "name", "validation", "gmlpos", "timezone", "role", "extension"]
234
+
235
+ vcon = VconString(doc = "vCon version string attribute")
236
+ uuid = VconString(doc = "vCon UUID string attribute")
237
+ created_at = VconString(doc = "vCon creation date string attribute")
238
+ subject = VconString(doc = "vCon subject string attribute")
239
+
240
+ redacted = VconDict(doc = "redacted Dict for reference or inclusion of less redacted signed or encrypted version of this vCon")
241
+ appended = VconDict(doc = "appended Dict for reference or includsion of signed or encrypted vCon to which this vCon appends data")
242
+
243
+ group = VconDictList(doc = "List of Dicts referencing or including other vCons to be aggregated by this vCon")
244
+ parties = VconDictList(doc = "List of Dicts, one for each party to this conversation")
245
+ dialog = VconDictList(doc = "List of Dicts referencing or including the capture of text, audio or video (original form of communication) segments for this conversation")
246
+ analysis = VconDictList(doc = "List of Dicts referencing or includeing analysis data for this conversation")
247
+ attachments = VconDictList(doc = "List of Dicts referencing or including ancillary documents to this conversation")
248
+
249
+ # TODO: work out states the vcon can be in. For example:
250
+ """
251
+ unsigned
252
+ signed
253
+ signed_unverified
254
+ signed_verified
255
+ encrypted
256
+ encrypted_unverified
257
+ decryppted_verified
258
+
259
+ Also are there failure cases for the above?
260
+
261
+ JSW (RFC7515) signing stored in:
262
+ _jsw_dict
263
+ {
264
+ payload
265
+ signatures
266
+ [
267
+ {
268
+ protected
269
+ header
270
+ signature
271
+ } [, ...]
272
+ ]
273
+ }
274
+
275
+
276
+ """
277
+
278
+ def __init__(self):
279
+ """ Constructor """
280
+ # Note: if you add new instance members/attributes, be sure to add its
281
+ # name to instance_attibutes in Vcon.attribute_exists.
282
+ # Register filter plugins as named instance methods
283
+ for plugin_name in vcon.filter_plugins.FilterPluginRegistry.get_names():
284
+ if(Vcon.attribute_exists(plugin_name) is not True):
285
+ setattr(vcon.Vcon, plugin_name, VconPluginMethodProperty(plugin_name))
286
+ logger.info("added Vcon.{}".format(plugin_name))
287
+ else:
288
+ existing_attr = getattr(vcon.Vcon, plugin_name)
289
+ if(issubclass(type(existing_attr), vcon.VconPluginMethodProperty)):
290
+ #print("Warning: Filter Plugin name: {} previsously added.".format(plugin_name))
291
+ pass
292
+ else:
293
+ logger.warning("Warning: Filter Plugin name: {} conflicts".format(plugin_name) +
294
+ " with existing instance or class attributes and is not directly callable." +
295
+ " Use Vcon.filter method to invoke it." +
296
+ " Better yet, change the name so that it does not conflict")
297
+
298
+ for plugin_type_name in vcon.filter_plugins.FilterPluginRegistry.get_types():
299
+ if(Vcon.attribute_exists(plugin_type_name) is not True):
300
+ setattr(vcon.Vcon, plugin_type_name, VconPluginMethodProperty(plugin_type_name))
301
+ logger.info("added Vcon.{}".format(plugin_type_name))
302
+ else:
303
+ existing_attr = getattr(vcon.Vcon, plugin_type_name)
304
+ if(issubclass(type(existing_attr), vcon.VconPluginMethodProperty)):
305
+ #print("Warning: Filter Plugin name: {} previsously added.".format(plugin_type_name))
306
+ pass
307
+ else:
308
+ logger.warning("Warning: Filter Plugin Type name: {} conflicts with existing".format(plugin_type_name) +
309
+ "instance or class attributes and is not directly callable." +
310
+ " Use Vcon.filter method to invoke it." +
311
+ " Better yet, change the name so that it does not conflict")
312
+
313
+ self._state = VconStates.UNSIGNED
314
+ self._jws_dict = None
315
+ self._jwe_dict = None
316
+
317
+ self._vcon_dict = {}
318
+ self._vcon_dict[Vcon.VCON_VERSION] = "0.0.1"
319
+ self._vcon_dict[Vcon.GROUP] = []
320
+ self._vcon_dict[Vcon.PARTIES] = []
321
+ self._vcon_dict[Vcon.DIALOG] = []
322
+ self._vcon_dict[Vcon.ANALYSIS] = []
323
+ self._vcon_dict[Vcon.ATTACHMENTS] = []
324
+ self._vcon_dict[Vcon.CREATED_AT] = vcon.utils.cannonize_date(datetime.datetime.utcnow())
325
+ self._vcon_dict[Vcon.REDACTED] = {}
326
+
327
+ def _attempting_modify(self) -> None:
328
+ if(self._state != VconStates.UNSIGNED):
329
+ raise InvalidVconState("Cannot modify Vcon unless current state is UNSIGNED. Current state: {}".format(self._state))
330
+
331
+ def __add_new_party(self, index : int) -> int:
332
+ """
333
+ check if a new party needs to be added to the list
334
+
335
+ Parameters:
336
+ index (int): -1 indicates adding a new party, positive numbers
337
+ throw AttributeError if the party with that index does not already exist
338
+
339
+ Returns:
340
+ party index in the list
341
+ """
342
+ self._attempting_modify()
343
+
344
+ party = index
345
+ if(party == -1):
346
+ self._vcon_dict[Vcon.PARTIES].append({})
347
+ party = len(self._vcon_dict[Vcon.PARTIES]) - 1
348
+
349
+ else:
350
+ if(not len(self._vcon_dict[Vcon.PARTIES]) > index):
351
+ raise AttributeError(
352
+ "index: {} > then party List length: {}. Use index of -1 to add one to the end.".format(
353
+ index, len(self._vcon_dict[Vcon.PARTIES])))
354
+
355
+ return(party)
356
+
357
+ def get_conversation_time(self) -> typing.Tuple[str, float]:
358
+ """
359
+ Get the start time and duration of the vcon
360
+
361
+ Parameters: none
362
+
363
+ Returns:
364
+ Tuple(str, float): RFC2822 format string start time and float duration in seconds
365
+ """
366
+ # TODO: loop through dialogs and find the oldest start time, calculate end time from
367
+ # duration, find the most recent end time and return the results
368
+
369
+ # TODO: Dialog recordings for mutiple parties will not show the start/join time for
370
+ # all of the parties, only the first to join. Requires analysis of recording to show
371
+ # when party speaks, but this may not be a good indicator of join time. Where as signalling
372
+ # has defininte joine time for each party, but is not captured in the vcon.
373
+ raise Exception("not implemented")
374
+
375
+ def set_party_parameter(self, parameter_name : str, parameter_value : str, party_index : int =-1) -> int:
376
+ """
377
+ Set the named parameter for the given party index. If the index is not provided,
378
+ add a new party to the vCon Parties Object array.
379
+
380
+ Parameters:
381
+ parameter_name (String) name of the Party Object parameter to be set.
382
+ Must beone of the following: ["tel", "stir", "mailto", "name", "validation", "gmlpos", "timezone"]
383
+ parameter_value (String) new value to set for the named parameter
384
+ party_index (int): index of party to set tel url on
385
+ (-1 indicates a new party should be added)
386
+
387
+ Returns:
388
+ int: if success, opsitive int index of party in list
389
+ """
390
+
391
+ self._attempting_modify()
392
+
393
+ if(parameter_name not in Vcon.PARTIES_OBJECT_STRING_PARAMETERS):
394
+ raise AttributeError(
395
+ "Not supported: setting of Parties Object parameter: {}. Must be one of the following: {}".
396
+ format(parameter_name, Vcon.PARTIES_OBJECT_STRING_PARAMETERS))
397
+
398
+ party_index = self.__add_new_party(party_index)
399
+
400
+ # TODO parameter specific validation
401
+ self._vcon_dict[Vcon.PARTIES][party_index][parameter_name] = parameter_value
402
+
403
+ return(party_index)
404
+
405
+ def add_party(self, party_dict: dict) -> int:
406
+ """
407
+ Add a new party to the vCon Parties Object array.
408
+
409
+ Parameters:
410
+ party_dict (dict) dict representing the parameter name and value pairs
411
+ Dict key must beone of the following: ["tel", "stir", "mailto", "name", "validation", "gmlpos", "timezone"]
412
+
413
+ Returns:
414
+ int: if success, positive int index of party in list
415
+ """
416
+ self._attempting_modify()
417
+ for key in party_dict.keys():
418
+ if(key not in Vcon.PARTIES_OBJECT_STRING_PARAMETERS):
419
+ raise AttributeError(f"Not supported: setting of Parties Object parameter: {key}. Must be one of the following: {Vcon.PARTIES_OBJECT_STRING_PARAMETERS}")
420
+ # TODO parameter specific validation
421
+ self._vcon_dict[Vcon.PARTIES].append(party_dict)
422
+ party_index = len(self._vcon_dict[Vcon.PARTIES]) - 1
423
+ return party_index
424
+
425
+
426
+ @deprecated("use Vcon.set_party_parameter")
427
+ def set_party_tel_url(self, tel_url : str, party_index : int =-1) -> int:
428
+ """
429
+ Set tel URL for a party.
430
+
431
+ Parameters:
432
+ tel_url
433
+ party_index (int): index of party to set tel url on
434
+ (-1 indicates a new party should be added)
435
+
436
+ Returns:
437
+ int: if success, opsitive int index of party in list
438
+ """
439
+
440
+ return(self.set_party_parameter("tel", tel_url, party_index))
441
+
442
+ def find_parties_by_parameter(self, parameter_name : str, parameter_value_substr : str) -> typing.List[int]:
443
+ """
444
+ Find the list of parties which have string parameters of the given name and value
445
+ which contains the given substring.
446
+
447
+ Parameters:
448
+ parameter_name (String) name of the Party Object parameter to be searched.
449
+ paramter_value_substr(String) substring to check if it is contained in the value of the given
450
+ parameter name
451
+
452
+ Returns:
453
+ List of indices into the parties object array for which the given parameter name's value
454
+ contains a match for the given substring.
455
+ """
456
+ found = []
457
+ for party_index, party in enumerate(self.parties):
458
+ value = party.get(parameter_name, "")
459
+ if(parameter_value_substr in value):
460
+ found.append(party_index)
461
+
462
+ return(found)
463
+
464
+ def add_dialog_inline_text(self, body : str,
465
+ start_time : typing.Union[str, int, float, datetime.datetime],
466
+ duration : typing.Union[int, float],
467
+ party : int,
468
+ mime_type : str,
469
+ file_name : str = None) -> int:
470
+ """
471
+ Add a dialog segment for a text chat or email thread.
472
+
473
+ Parameters:
474
+ body (str): bytes for the text communication (e.g. text or multipart MIME body).
475
+ start_time (str, int, float, datetime.datetime): Date, time of the start time the
476
+ sender started typing or if unavailable, the time it was sent.
477
+ String containing RFC 2822 or RFC3339 date time stamp or int/float
478
+ containing epoch time (since 1970) in seconds.
479
+ duration (int or float): duration in time the sender completed typing in seconds.
480
+ Should be zero if unknown.
481
+ party (int) index into parties object array as to which party sent the text communication.
482
+ mime_type (str): mime type of the body (usually MIMETYPE_TEXT_PLAIN or MIMETYPE_MULTIPART)
483
+ file_name (str): file name of the body if applicable (optional)
484
+
485
+ Returns:
486
+ Index of the new dialog in the Dialog Object array parameter.
487
+ """
488
+
489
+ self._attempting_modify()
490
+
491
+ new_dialog = {}
492
+ new_dialog['type'] = "text"
493
+ new_dialog['start'] = vcon.utils.cannonize_date(start_time)
494
+ new_dialog['duration'] = duration
495
+ new_dialog['parties'] = party
496
+ new_dialog['mimetype'] = mime_type
497
+ if(file_name is not None and len(file_name) > 0):
498
+ new_dialog['filename'] = file_name
499
+
500
+ new_dialog['encoding'] = "None"
501
+ new_dialog['body'] = body
502
+
503
+ if(self.dialog is None):
504
+ self._vcon_dict[Vcon.DIALOG] = []
505
+
506
+ self._vcon_dict[Vcon.DIALOG].append(new_dialog)
507
+
508
+ return(len(self.dialog))
509
+
510
+ def add_dialog_inline_recording(self, body : bytes,
511
+ start_time : typing.Union[str, int, float, datetime.datetime],
512
+ duration : typing.Union[int, float],
513
+ parties : typing.Union[int, typing.List[int], typing.List[typing.List[int]]],
514
+ mime_type : str,
515
+ file_name : str = None,
516
+ originator : typing.Union[int, None] = None) -> int:
517
+ """
518
+ Add a recording of a portion of the conversation, inline (base64 encoded) to the dialog.
519
+
520
+ Parameters:
521
+ body (bytes): bytes for the audio or video recording (e.g. wave or MP3 file).
522
+ start_time (str, int, float, datetime.datetime): Date, time of the start of
523
+ the recording.
524
+ string containing RFC 2822 or RFC3339 date time stamp or int/float
525
+ containing epoch time (since 1970) in seconds.
526
+ duration (int or float): duration of the recording in seconds
527
+ parties (int, List[int], List[List[int]]): party indices speaking in each
528
+ channel of the recording.
529
+ mime_type (str): mime type of the recording
530
+ file_name (str): file name of the recording (optional)
531
+ originator (int): by default the originator of the dialog is the first party listed in the parites array.
532
+ However , in some cases, it is difficult to arrange the recording channels with the originator
533
+ as the first party/channel. In these cases, the originator can be explicitly provided. The
534
+ value of the originator is the index into the Vcon.parties array of the party that originated
535
+ this dialog.
536
+
537
+ Returns:
538
+ Number of bytes read from body.
539
+ """
540
+ # TODO should return dialog index not byte count
541
+
542
+ # TODO: do we want to know the number of channels? e.g. to verify party list length
543
+
544
+ # TODO: should we validate the start time?
545
+
546
+ self._attempting_modify()
547
+
548
+ new_dialog = {}
549
+ new_dialog['type'] = "recording"
550
+ new_dialog['start'] = vcon.utils.cannonize_date(start_time)
551
+ new_dialog['duration'] = duration
552
+ new_dialog['parties'] = parties
553
+ new_dialog['mimetype'] = mime_type
554
+ if(file_name is not None and len(file_name) > 0):
555
+ new_dialog['filename'] = file_name
556
+
557
+ if(originator is not None and originator >= 0):
558
+ new_dialog['originator'] = originator
559
+
560
+ new_dialog['encoding'] = "base64url"
561
+ encoded_body = jose.utils.base64url_encode(body).decode('utf-8')
562
+ #print("encoded body type: {}".format(type(encoded_body)))
563
+ new_dialog['body'] = encoded_body
564
+
565
+ if(self.dialog is None):
566
+ self._vcon_dict[Vcon.DIALOG] = []
567
+
568
+ self._vcon_dict[Vcon.DIALOG].append(new_dialog)
569
+
570
+ return(len(body))
571
+
572
+ @deprecated("use Vcon.decode_dialog_inline_body")
573
+ def decode_dialog_inline_recording(self, dialog_index : int) -> bytes:
574
+ """ depricated use decode_dialog_inline_body """
575
+ return(self.decode_dialog_inline_body(dialog_index))
576
+
577
+ def decode_dialog_inline_body(self, dialog_index : int) -> typing.Union[str, bytes]:
578
+ """
579
+ Get the dialog recording at the given index, decoding it and returning the raw bytes.
580
+
581
+ Parameters:
582
+ dialog_index (int): index the the dialog in the dialog list, containing the inline recording
583
+
584
+ Returns:
585
+ (bytes): the bytes for the recording file
586
+ """
587
+ dialog = self.dialog[dialog_index]
588
+ if(dialog["type"] not in ["text", "recording"]):
589
+ raise AttributeError("dialog[{}] type: {} is not supported".format(dialog_index, dialog["type"]))
590
+ if(dialog.get("body") is None):
591
+ raise AttributeError("dialog[{}] does not contain an inline body/file".format(dialog_index))
592
+
593
+ encoding = dialog.get("encoding", "None").lower()
594
+ if(encoding == "base64url"):
595
+ # This is wrong. decode should take a string not bytes, but it fails without the bytes conversion
596
+ # this is a bug in jose.baseurl_decode
597
+ decoded_body = jose.utils.base64url_decode(bytes(dialog["body"], 'utf-8'))
598
+
599
+ # No encoding
600
+ elif(encoding == "none"):
601
+ decoded_body = dialog["body"]
602
+
603
+ else:
604
+ raise UnsupportedVconVersion("dialog[{}] body encoding: {} not supported".format(dialog_index, dialog["encoding"]))
605
+
606
+ return(decoded_body)
607
+
608
+ def add_dialog_external_recording(self, body : bytes,
609
+ start_time : typing.Union[str, int, float, datetime.datetime],
610
+ duration : typing.Union[int, float],
611
+ parties : typing.Union[int, typing.List[int], typing.List[typing.List[int]]],
612
+ external_url: str,
613
+ mime_type : str = None,
614
+ file_name : str = None,
615
+ sign_type : str = "SHA-512",
616
+ originator : typing.Union[int, None] = None) -> int:
617
+ """
618
+ Add a recording of a portion of the conversation, as a reference via the given
619
+ URL, to the dialog and generate a signature and key for the content. This
620
+ method has the limitation that the entire recording must be passed in in-memory.
621
+
622
+ Parameters:
623
+ body (bytes): bytes for the audio or video recording (e.g. wave or MP3 file).
624
+ start_time (str, int, float, datetime.datetime): Date, time of the start of
625
+ the recording.
626
+ string containing RFC 2822 or RFC 3339 date time stamp or int/float
627
+ containing epoch time (since 1970) in seconds.
628
+ duration (int or float): duration of the recording in seconds
629
+ parties (int, List[int], List[List[int]]): party indices speaking in each
630
+ channel of the recording.
631
+ external_url (string): https URL where the body is stored securely
632
+ mime_type (str): mime type of the recording (optional)
633
+ file_name (str): file name of the recording (optional)
634
+ sign_type (str): signature type to create for external signature
635
+ default= "SHA-512" use SHA 512 bit hash (RFC6234)
636
+ "LM-OTS" use Leighton-Micali One Time Signature (RFC8554
637
+ originator (int): by default the originator of the dialog is the first party listed in the parites array.
638
+ However , in some cases, it is difficult to arrange the recording channels with the originator
639
+ as the first party/channel. In these cases, the originator can be explicitly provided. The
640
+ value of the originator is the index into the Vcon.parties array of the party that originated
641
+ this dialog.
642
+
643
+ Returns:
644
+ Index to the added dialog
645
+ """
646
+ # TODO should return dialog index not byte count
647
+
648
+ # TODO: need a streaming/chunk version of this so that we don't have to have the whole file in memory.
649
+
650
+ self._attempting_modify()
651
+
652
+ new_dialog = {}
653
+ new_dialog['type'] = "recording"
654
+ new_dialog['start'] = vcon.utils.cannonize_date(start_time)
655
+ new_dialog['duration'] = duration
656
+ new_dialog['parties'] = parties
657
+ new_dialog['url'] = external_url
658
+ if(mime_type is not None):
659
+ new_dialog['mimetype'] = mime_type
660
+ if(file_name is not None):
661
+ new_dialog['filename'] = file_name
662
+ if(originator is not None and originator >= 0):
663
+ new_dialog['originator'] = originator
664
+
665
+
666
+ if (body):
667
+ if(sign_type == "LM-OTS"):
668
+ logger.warning("Warning: \"LM-OTS\" may be depricated")
669
+ key, signature = vcon.security.lm_one_time_signature(body)
670
+ new_dialog['key'] = key
671
+ new_dialog['signature'] = signature
672
+ new_dialog['alg'] = "LMOTS_SHA256_N32_W8"
673
+
674
+ elif(sign_type == "SHA-512"):
675
+ sig_hash = vcon.security.sha_512_hash(body)
676
+ new_dialog['signature'] = sig_hash
677
+ new_dialog['alg'] = "SHA-512"
678
+
679
+ else:
680
+ raise AttributeError("Unsupported signature type: {}. Please use \"SHA-512\" or \"LM-OTS\"".format(sign_type))
681
+
682
+ if(self.dialog is None):
683
+ self._vcon_dict[Vcon.DIALOG] = []
684
+
685
+ dialog_index = len(self.dialog)
686
+ self._vcon_dict[Vcon.DIALOG].append(new_dialog)
687
+
688
+ return(dialog_index)
689
+
690
+ def get_dialog_external_recording(self, dialog_index : int) -> bytes:
691
+ """
692
+ Get the externally referenced dialog recording via the dialog's url
693
+ and verify its integrity using the signature in the dialog object,
694
+ blocking on its return.
695
+
696
+ Parameters:
697
+ dialog_index (int) - index into the Vcon.dialog array indicating
698
+ which external recording is to be retrieved and verified.
699
+
700
+ Returns:
701
+ verified content/bytes for the recording
702
+ """
703
+ # Get body from URL using requests
704
+ url = self.dialog[dialog_index]["url"]
705
+ req = requests.get(url)
706
+ body = req.content
707
+
708
+ # verify the body
709
+ self.verify_dialog_external_recording(dialog_index, body)
710
+
711
+ return(body)
712
+
713
+ def verify_dialog_external_recording(self, dialog_index : int, body : bytes) -> None:
714
+ """
715
+ Verify the given body of the externally stored recording for the indicted dialog.
716
+ Using the signature and public key stored in the dialog, the content of the body
717
+ of the recording is verifyed.
718
+
719
+ Parameters:
720
+ dialog_index (int): index of the dialog to be verified
721
+
722
+ body (bytes): the contents of the recording which is stored external to this vCon
723
+
724
+ Returns: none
725
+
726
+ Raises exceptions if the signature and public key fail to verify the body.
727
+ """
728
+
729
+ dialog = self.dialog[dialog_index]
730
+
731
+ if(dialog['type'] != "recording"):
732
+ raise AttributeError("dialog[{}] is of type: {} not recording".format(dialog_index, dialog['type']))
733
+
734
+ if(len(dialog['signature']) < 1 ):
735
+ raise AttributeError("dialog[{}] signature: {} not set. Must be for LMOTS_SHA256_N32_W8".format(dialog_index, dialog['signature']))
736
+
737
+ if(dialog['alg'] == 'LMOTS_SHA256_N32_W8'):
738
+ if(len(dialog['key']) < 1 ):
739
+ raise AttributeError("dialog[{}] key: {} not set. Must be for LMOTS_SHA256_N32_W8".format(dialog_index, dialog['key']))
740
+
741
+ vcon.security.verify_lm_one_time_signature(body,
742
+ dialog['signature'],
743
+ dialog['key'])
744
+
745
+ elif(dialog['alg'] == 'SHA-512'):
746
+ sig_hash = vcon.security.sha_512_hash(body)
747
+ if( dialog['signature'] != sig_hash):
748
+ print("dialog[\"signature\"]: {} hash: {} size: {}".format(dialog['signature'], sig_hash, len(body)))
749
+ print("dialog: {}".format(json.dumps(dialog, indent=2)))
750
+ raise InvalidVconHash("SHA-512 hash in signature does not match the given body for dialog[{}]".format(dialog_index))
751
+
752
+ else:
753
+ raise AttributeError("dialog[{}] alg: {} not supported. Must be SHA-512 or LMOTS_SHA256_N32_W8".format(dialog_index, dialog['alg']))
754
+
755
+ def add_analysis_transcript(self,
756
+ dialog_index : int,
757
+ transcript : dict,
758
+ vendor : str,
759
+ vendor_schema : str = None,
760
+ analysis_type: str = "transcript",
761
+ encoding : str= "json"
762
+ ) -> None:
763
+ """
764
+ Add a transcript for the indicated dialog.
765
+
766
+ Parameters:
767
+ dialog_index (str): index to the dialog in the vCon dialog list that this trascript corresponds to.
768
+ vendor (str): string token for the vendor of the audio to text transcription service
769
+ vendor_schema (str): schema label for the transcription data. Used to identify data format of the transcription
770
+ for vendors that have more than one format or version.
771
+ """
772
+
773
+ self._attempting_modify()
774
+
775
+ analysis_element = {}
776
+ analysis_element["type"] = analysis_type
777
+ # TODO should validate dialog_index??
778
+ analysis_element["dialog"] = dialog_index
779
+ analysis_element["body"] = transcript
780
+ analysis_element["encoding"] = encoding
781
+ analysis_element["vendor"] = vendor
782
+ if(vendor_schema is not None):
783
+ analysis_element["vendor_schema"] = vendor_schema
784
+
785
+ if(self.analysis is None):
786
+ self._vcon_dict[Vcon.ANALYSIS] = []
787
+
788
+ self._vcon_dict[Vcon.ANALYSIS].append(analysis_element)
789
+
790
+ def add_analysis(self,
791
+ dialog_index : int,
792
+ analysis_type: str,
793
+ body : str = None,
794
+ vendor : str = "conserver",
795
+ vendor_schema : str = None,
796
+ encoding : str= "json"
797
+ ) -> None:
798
+ """
799
+ Add a generic analysis for the indicated dialog.
800
+
801
+ Parameters:
802
+ dialog_index (str): index to the dialog in the vCon dialog list that this trascript corresponds to.
803
+ vendor (str): string token for the vendor of the audio to text transcription service
804
+ vendor_schema (str): schema label for the transcription data. Used to identify data format of the transcription
805
+ for vendors that have more than one format or version.
806
+ """
807
+
808
+ self._attempting_modify()
809
+
810
+ analysis_element = {}
811
+ analysis_element["type"] = analysis_type
812
+ analysis_element["dialog"] = dialog_index
813
+ analysis_element["body"] = body
814
+ analysis_element["encoding"] = encoding
815
+ analysis_element["vendor"] = vendor
816
+ if(vendor_schema is not None):
817
+ analysis_element["vendor_schema"] = vendor_schema
818
+
819
+ if(self.analysis is None):
820
+ self._vcon_dict[Vcon.ANALYSIS] = []
821
+
822
+ self._vcon_dict[Vcon.ANALYSIS].append(analysis_element)
823
+
824
+ def dumps(self, signed = True) -> str:
825
+ """
826
+ Dump the vCon as a JSON string.
827
+
828
+ Parameters:
829
+
830
+ signed (Boolean): If the vCon is signed locally or verfied,
831
+ True: serialize the signed version
832
+ False: serialize the unsigned version
833
+
834
+ Returns:
835
+ String containing JSON representation of the vCon.
836
+ """
837
+
838
+ # TODO: Should it throw an acception if its not signed? Could have argument to
839
+ # not throw if it not signed.
840
+
841
+ if(self._state == VconStates.UNSIGNED):
842
+ if(self.uuid is None or len(self.uuid) < 1):
843
+ raise InvalidVconState("vCon has no UUID set. Use set_uuid method.")
844
+
845
+ return(json.dumps(self._vcon_dict, default=lambda o: o.__dict__, **dumps_options))
846
+
847
+ if(self._state in [VconStates.SIGNED, VconStates.UNVERIFIED, VconStates.VERIFIED]):
848
+ if(signed is False and self._state != VconStates.UNVERIFIED):
849
+ return(json.dumps(self._vcon_dict, default=lambda o: o.__dict__, **dumps_options))
850
+ return(json.dumps(self._jws_dict, default=lambda o: o.__dict__, **dumps_options))
851
+
852
+ if(self._state in [VconStates.ENCRYPTED, VconStates.DECRYPTED]):
853
+ if(signed is False):
854
+ raise AttributeError("not supported: unsigned JSON output for encrypted vCon")
855
+ return(json.dumps(self._jwe_dict, default=lambda o: o.__dict__, **dumps_options))
856
+
857
+ raise InvalidVconState("vCon state: {} is not valid for dumps".format(self._state))
858
+
859
+ def load(self, file_handle: typing.TextIO) -> None:
860
+ """
861
+ Load the Vcon JSON from the given file_handle and deserialize it.
862
+ see Vcon.loads for more details.
863
+ """
864
+ vcon_json_string = file_handle.read()
865
+ self.loads(vcon_json_string)
866
+
867
+ def loads(self, vcon_json : str) -> None:
868
+ """
869
+ Load the vCon from a JSON string.
870
+ Assumes that this vCon is an empty vCon as it is not cleared.
871
+
872
+ Decision as to what json form to be deserialized is:
873
+ 1) unsigned vcon must have a vcon and one or more of the following elements: parties, dialog, analysis, attachments
874
+ 2) JWS vCon must have a payload and signatures
875
+ 3) JWE vCon must have a cyphertext and recipients
876
+
877
+ Parameters:
878
+ vcon_json (str): string containing JSON representation of a vCon
879
+
880
+ Returns: none
881
+ """
882
+
883
+ #TODO: Should check unsafe stuff is not loaded
884
+
885
+ # TODO should use self._attempting_modify() ???
886
+ if(self._state != VconStates.UNSIGNED):
887
+ raise InvalidVconState("Cannot load Vcon unless current state is UNSIGNED. Current state: {}".format(self._state))
888
+
889
+ vcon_dict = json.loads(vcon_json)
890
+
891
+ # we need to check the format as to whether it is signed or
892
+ # not and deconstruct the loaded object.
893
+ # load differently based upon the contents of the JSON
894
+
895
+ # Signed vCon (JWS)
896
+ if(("payload" in vcon_dict) and
897
+ ("signatures" in vcon_dict)
898
+ ):
899
+ self._vcon_dict = {}
900
+
901
+ self._state = VconStates.UNVERIFIED
902
+ self._jws_dict = vcon_dict
903
+
904
+ # encrypted vCon (JWE)
905
+ elif(("cyphertext" in vcon_dict) and
906
+ ("recipients" in vcon_dict)
907
+ ):
908
+ self._vcon_dict = {}
909
+
910
+ self._state = VconStates.ENCRYPTED
911
+ self._jwe_dict = vcon_dict
912
+
913
+ # Unsigned vCon has to have vcon version and
914
+ elif((self.VCON_VERSION in vcon_dict) and (
915
+ # one of the following arrays
916
+ ('parties' in vcon_dict) or
917
+ ('dialog' in vcon_dict) or
918
+ ('analysis' in vcon_dict) or
919
+ ('attachments' in vcon_dict)
920
+ )):
921
+
922
+ # validate version
923
+ version_string = vcon_dict.get(self.VCON_VERSION, "not set")
924
+ if(version_string != "0.0.1"):
925
+ raise UnsupportedVconVersion("loads of JSON vcon version: \"{}\" not supported".format(version_string))
926
+
927
+ self._vcon_dict = self.migrate_0_0_1_vcon(vcon_dict)
928
+
929
+ # Unknown
930
+ else:
931
+ raise InvalidVconJson("Not recognized as a unsigned, signed or encrypted JSON vCon")
932
+
933
+
934
+ def sign(self, private_key_pem_file_name : str, cert_chain_pem_file_names : typing.List[str]) -> None:
935
+ """
936
+ Sign the vcon using the given private key from the give certificate chain.
937
+
938
+ Parameters:
939
+ cert_chain_pem_file_names (List{str]): file names for the pem format certicate chain for the
940
+ private key to use for signing. The cert/public key corresponding to the private key should be the
941
+ first cert. THe certificate authority root should be the last cert.
942
+
943
+ private_key_pem_file_name (str): the private key to use for signing the vcon.
944
+
945
+ Returns: none
946
+ """
947
+
948
+ if(self._state == VconStates.SIGNED):
949
+ raise InvalidVconState("Vcon was already signed.")
950
+
951
+ if(self._state != VconStates.UNSIGNED):
952
+ raise InvalidVconState("Vcon not in valid state to be signed: {}.".format(self._state))
953
+
954
+ if(self.uuid is None or len(self.uuid) < 1):
955
+ raise InvalidVconState("vCon has no UUID set. Use set_uuid method before signing.")
956
+
957
+ header, signing_jwk = vcon.security.build_signing_jwk_from_pem_files(private_key_pem_file_name, cert_chain_pem_file_names)
958
+
959
+ # dot separated JWS token. First part is the payload, second part is the signature (both base64url encoded)
960
+ jws_token = jose.jws.sign(self._vcon_dict, signing_jwk, headers=header, algorithm=signing_jwk["alg"])
961
+ #print(jws_token.split('.'))
962
+ protected_header, payload, signature = jws_token.split('.')
963
+ #print("decoded header: {}".format(jose.utils.base64url_decode(bytes(protected_header, 'utf-8'))))
964
+
965
+ jws_serialization = {}
966
+ jws_serialization['payload'] = payload
967
+ jws_serialization['signatures'] = []
968
+ first_sig = {}
969
+ first_sig['header'] = header
970
+ first_sig['signature'] = signature
971
+ first_sig['protected'] = protected_header
972
+ jws_serialization['signatures'].append(first_sig)
973
+
974
+ self._jws_dict = jws_serialization
975
+ self._state = VconStates.SIGNED
976
+
977
+ def verify(self, ca_cert_pem_file_names : typing.List[str]) -> None:
978
+ """
979
+ Verify the signed vCon and its certificate chain which should be issued by one of the given CAs
980
+
981
+ Parameters:
982
+ ca_cert_pem_file_names (List[str]): list of Certificate Authority certificate PEM file names
983
+ to verify the vCon's certificate chain.
984
+
985
+ Returns: none
986
+
987
+ Raises exceptions for invalid cert chaind, invalid cert dates or chain not issued by one
988
+ of the given CAs.
989
+
990
+ NOTE: DOES NOT CHECK REVOKATION LISTS!!!
991
+ """
992
+ if(self._state == VconStates.SIGNED):
993
+ raise InvalidVconState("Vcon was locally signed. No need to verify")
994
+
995
+ if(self._state == VconStates.VERIFIED):
996
+ raise InvalidVconState("Vcon was already verified")
997
+
998
+ if(self._state != VconStates.UNVERIFIED):
999
+ raise InvalidVconState("Vcon cannot be verified invalid state: {}")
1000
+
1001
+ if(self._jws_dict is None or
1002
+ ('signatures' not in self._jws_dict) or
1003
+ (len(self._jws_dict['signatures']) < 1) or
1004
+ ('signature' not in self._jws_dict['signatures'][0])
1005
+ ):
1006
+ raise InvalidVconState("Vcon JWS invalid")
1007
+
1008
+ # Load an array of CA certficate objects to use to verify acceptable cert chains
1009
+ ca_cert_object_list = []
1010
+ for ca in ca_cert_pem_file_names:
1011
+ ca_cert_object_list.append(vcon.security.load_pem_cert(ca)[0])
1012
+
1013
+ last_exception = Exception("Internal error in Vcon.verify this exception should never be thrown")
1014
+ chain_count = 0
1015
+ for signature in self._jws_dict['signatures']:
1016
+ if('header' in signature):
1017
+ if('x5c' in signature['header']):
1018
+ x5c = signature['header']['x5c']
1019
+ chain_count += 1
1020
+
1021
+ cert_chain_objects = vcon.security.der_to_certs(x5c)
1022
+
1023
+ # TODO: some of this should be move to the security submodule
1024
+ # e.g. the iterating over CAs
1025
+
1026
+ # TODO: need to do something a little smarter on the exception raise to
1027
+ # give a clue of the best/closest chain and CA that failed. Perhaps
1028
+ # even all of the failures.
1029
+
1030
+ try:
1031
+ vcon.security.verify_cert_chain(cert_chain_objects)
1032
+
1033
+ # We have a valid chain, check if its from one of the accepted CAs
1034
+ for ca_object in ca_cert_object_list:
1035
+ try:
1036
+ vcon.security.verify_cert(cert_chain_objects[len(cert_chain_objects) - 1], ca_object)
1037
+
1038
+ # IF we get here, we have a valid chain: cert_chain_objects issued from one of our accepted
1039
+ # CAs: ca_object.
1040
+ # The assumtion is that it is safe to trust this cert chain. So we
1041
+ # can use it to build a JWK and verify the signature.
1042
+ verification_jwk = {}
1043
+ verification_jwk["kty"] = "RSA"
1044
+ verification_jwk["use"] = "sig"
1045
+ verification_jwk["alg"] = signature['header']['alg']
1046
+ verification_jwk["e"] = jose.utils.base64url_encode(jose.utils.
1047
+ long_to_bytes(cert_chain_objects[0].public_key().public_numbers().e)).decode('utf-8')
1048
+ verification_jwk["n"] = jose.utils.base64url_encode(jose.utils.
1049
+ long_to_bytes(cert_chain_objects[0].public_key().public_numbers().n)).decode('utf-8')
1050
+
1051
+ jws_token = signature['protected'] + "." + self._jws_dict['payload'] + "." + signature['signature']
1052
+ verified_payload = jose.jws.verify(jws_token, verification_jwk, verification_jwk["alg"])
1053
+
1054
+ # If we get here, the payload was verified
1055
+ #print("verified payload: {}".format(verified_payload))
1056
+ #print("verified payload type: {}".format(type(verified_payload)))
1057
+ vcon_dict = json.loads(verified_payload.decode('utf-8'))
1058
+ self._vcon_dict = self.migrate_0_0_1_vcon(vcon_dict)
1059
+
1060
+ self._state = VconStates.VERIFIED
1061
+
1062
+ return(None)
1063
+
1064
+ # This valid chain, is not issued from the CA for this ca_objjwk
1065
+ except Exception as e:
1066
+ last_exception = e
1067
+ # Keep trying other CAs until we run out or succeed
1068
+
1069
+ # Invalid chain
1070
+ except Exception as e:
1071
+ last_exception = e
1072
+ # Keep trying other chains until we run out or succeed
1073
+
1074
+ if(chain_count == 0):
1075
+ raise InvalidVconSignature("None of the signatures contain the x5c chain, which this implementation currenlty requires.")
1076
+
1077
+ raise last_exception
1078
+
1079
+
1080
+ def encrypt(self, cert_pem_file_name : str) -> None:
1081
+ """
1082
+ encrypt a Signed vcon using the given public key from the give certificate.
1083
+
1084
+ vcon must be signed first.
1085
+
1086
+ Parameters:
1087
+ cert_pem_file_name (str): the public key/cert to use for encrypting the vcon.
1088
+
1089
+ Returns: none
1090
+ """
1091
+
1092
+ if(self._state not in [VconStates.SIGNED, VconStates.UNVERIFIED]):
1093
+ raise InvalidVconState("Vcon must be signed before it can be encrypted")
1094
+
1095
+ if(len(self._jws_dict) < 2):
1096
+ raise InvalidVconState("Vcon signature does not seem valid: {}".format(self._jws_dict))
1097
+
1098
+ # both of these work
1099
+ #encryption = "A256GCM"
1100
+ encryption = "A256CBC-HS512"
1101
+
1102
+ encryption_key = vcon.security.build_encryption_jwk_from_pem_file(cert_pem_file_name)
1103
+
1104
+ plaintext = json.dumps(self._jws_dict, **dumps_options)
1105
+
1106
+ jwe_compact_token = jose.jwe.encrypt(plaintext, encryption_key, encryption, encryption_key['alg']).decode('utf-8')
1107
+ jwe_complete_serialization = vcon.security.jwe_compact_token_to_complete_serialization(jwe_compact_token, enc = encryption, x5c = [])
1108
+ self._jwe_dict = jwe_complete_serialization
1109
+ self._state = VconStates.ENCRYPTED
1110
+
1111
+ def decrypt(self, private_key_pem_file_name : str, cert_pem_file_name : str) -> None:
1112
+ """
1113
+ Decrypt a vCon using private and public key file.
1114
+
1115
+ vCOn must be in encrypted state and will be in signed state after decryption.
1116
+
1117
+ Parameters:
1118
+ cert_pem_file_name (str): the public key/cert to use for decrypting the vcon.
1119
+
1120
+ private_key_pem_file_name (str): the private key to use for decrypting the vcon.
1121
+
1122
+ Returns: none
1123
+ """
1124
+
1125
+ if(self._state != VconStates.ENCRYPTED):
1126
+ raise InvalidVconState("Vcon is not encerypted")
1127
+
1128
+ if(len(self._jwe_dict) < 2):
1129
+ raise InvalidVconState("Vcon JWE does not seem valid: {}".format(self._jws_dict))
1130
+
1131
+ jwe_compact_token_reconstructed = vcon.security.jwe_complete_serialization_to_compact_token(self._jwe_dict)
1132
+
1133
+ (header, signing_key) = vcon.security.build_signing_jwk_from_pem_files(private_key_pem_file_name, [cert_pem_file_name])
1134
+ #signing_key['alg'] = encryption_key['alg']
1135
+
1136
+ plaintext_decrypted = jose.jwe.decrypt(jwe_compact_token_reconstructed, signing_key).decode('utf-8')
1137
+ # let loads figure out if this is an encrypted JWS vCon or just a vCon
1138
+ current_state = self._state
1139
+ # Fool loads into thinking this is a raw vCon and its safe to load. Save state incase we barf.
1140
+ self._state = VconStates.UNSIGNED
1141
+ try:
1142
+ self.loads(plaintext_decrypted)
1143
+
1144
+ except Exception as e:
1145
+ # restore state
1146
+ self._state = current_state
1147
+ raise e
1148
+
1149
+ def set_subject(self, subject: str) -> None:
1150
+ """
1151
+ Set the subject parameter of the vCon.
1152
+
1153
+ Parameters:
1154
+ subject - String value to assign to the vCon subject parameter.
1155
+
1156
+ Returns: None
1157
+ """
1158
+
1159
+ self._attempting_modify()
1160
+
1161
+ self._vcon_dict[Vcon.SUBJECT] = subject
1162
+
1163
+ def filter(self, filter_name: str, **options) -> Vcon:
1164
+ """
1165
+ Run this Vcon through the named filter plugin.
1166
+
1167
+ See vcon.filter_plugins.FilterPluginRegistry for the set of registered plugins.
1168
+
1169
+ Parameters:
1170
+ options (kwargs) - passed through to plugin. The key words are documented by
1171
+ the specified plugin.
1172
+
1173
+ Returns:
1174
+ the filter modified Vcon
1175
+ """
1176
+ self._attempting_modify()
1177
+
1178
+ try:
1179
+ plugin = vcon.filter_plugins.FilterPluginRegistry.get(filter_name)
1180
+ except vcon.filter_plugins.PluginFilterNotRegistered as fp_error:
1181
+ plugin = vcon.filter_plugins.FilterPluginRegistry.get_type_default_plugin(filter_name)
1182
+
1183
+ if(plugin is None):
1184
+ raise Exception("Vcon.filter plugin: {} not found".format(filter_name))
1185
+
1186
+ return(plugin.filter(self, **options))
1187
+
1188
+ def set_uuid(self, domain_name: str, replace: bool= False) -> str:
1189
+ """
1190
+ Generate a UUID for this vCon and set the parameter
1191
+
1192
+ Parameters:
1193
+ domain_name: a DNS domain name string, should generally be a fully qualified host
1194
+ name.
1195
+
1196
+ Returns:
1197
+ UUID version 8 string
1198
+ (vCon uuid parameter is also set)
1199
+
1200
+ """
1201
+
1202
+ self._attempting_modify()
1203
+
1204
+ if(self.uuid is not None and replace is False and len(self.uuid) > 0):
1205
+ raise AttributeError("uuid parameter already set")
1206
+
1207
+ uuid = self.uuid8_domain_name(domain_name)
1208
+
1209
+ self._vcon_dict[Vcon.UUID] = uuid
1210
+
1211
+ return(uuid)
1212
+
1213
+ @staticmethod
1214
+ def attribute_exists(name : str) -> bool:
1215
+ """
1216
+ Check if the given name is already used as a attribute or method on Vcon.
1217
+
1218
+ Parameters:
1219
+ name (str) - name to check if it is used.
1220
+
1221
+ Returns:
1222
+ True/False if name is used.
1223
+ """
1224
+ try:
1225
+ existing_attr = getattr(vcon.Vcon, name)
1226
+ #logger.warning("found Vcon attribute: {} {}".format(name, existing_attr))
1227
+ exists = True
1228
+
1229
+ except AttributeError as ex_err:
1230
+ if(str(ex_err).startswith("type object 'Vcon'")):
1231
+ exists = False
1232
+ else:
1233
+ # These are descriptors, which for some reason cannot
1234
+ # be got by getattr.
1235
+ logger.error(ex_err)
1236
+ exists = True
1237
+
1238
+ if(not exists):
1239
+ # The only programatic way to do this is to instantiate a Vcon, but this seemed a bit
1240
+ # heavy. So for now just testing a manually maintained list of attributes and blacklisted
1241
+ # token names.
1242
+ instance_attributes = ['_jwe_dict', '_jws_dict', '_state', '_vcon_dict', 'vcon', "Vcon", "filter_plugins", "security", "utils", "cli"]
1243
+ if(name in instance_attributes):
1244
+ exists = True
1245
+
1246
+ return(exists)
1247
+
1248
+ @staticmethod
1249
+ def uuid8_domain_name(domain_name: str) -> str:
1250
+ """
1251
+ Generate a version 8 (custom) UUID using the upper 62 bits of the SHA-1 hash
1252
+ for the given DNS domain name string for custom_c and generating
1253
+ custom_a and custom_b the same way as unix_ts_ms and rand_a respectively
1254
+ for UUID version 7 (per IETF I-D draft-peabody-dispatch-new-uuid-format-04).
1255
+
1256
+ Parameters:
1257
+ domain_name: a DNS domain name string, should generally be a fully qualified host
1258
+ name.
1259
+
1260
+ Returns:
1261
+ UUID version 8 string
1262
+ """
1263
+
1264
+ sha1_hasher = hashlib.sha1()
1265
+ sha1_hasher.update(bytes(domain_name, "utf-8"))
1266
+ dn_sha1 = sha1_hasher.digest()
1267
+
1268
+ hash_upper_64 = dn_sha1[0:8]
1269
+ int64 = int.from_bytes(hash_upper_64, byteorder="big")
1270
+
1271
+ uuid8_domain = Vcon.uuid8_time(int64)
1272
+
1273
+ return(uuid8_domain)
1274
+
1275
+ @staticmethod
1276
+ def uuid8_time(custom_c_62_bits: int) -> str:
1277
+ """
1278
+ Generate a version 8 (custom) UUID using the given custom_c and generating
1279
+ custom_a and custom_b the same way as unix_ts_ms and rand_a respectively
1280
+ for UUID version 7 (per IETF I-D draft-peabody-dispatch-new-uuid-format-04).
1281
+
1282
+ Parameters:
1283
+ custom_c_62_bits: the 62 bit value as an integer to be used for custom_b
1284
+ portion of UUID version 8.
1285
+
1286
+ Returns:
1287
+ UUID version 8 string
1288
+ """
1289
+ # This is partially from uuid6.uuid7 implementation:
1290
+ global _LAST_V8_TIMESTAMP
1291
+
1292
+ nanoseconds = time.time_ns()
1293
+ if _LAST_V8_TIMESTAMP is not None and nanoseconds <= _LAST_V8_TIMESTAMP:
1294
+ nanoseconds = _LAST_V8_TIMESTAMP + 1
1295
+ _last_v7_timestamp = nanoseconds
1296
+ timestamp_ms, timestamp_ns = divmod(nanoseconds, 10**6)
1297
+ subsec = uuid6._subsec_encode(timestamp_ns)
1298
+
1299
+ # This is not what is in the vCon I-D. It says random bits
1300
+ # not bits from the time stamp. May want to change this
1301
+ subsec_a = subsec >> 8
1302
+ uuid_int = (timestamp_ms & 0xFFFFFFFFFFFF) << 80
1303
+ uuid_int |= subsec_a << 64
1304
+ uuid_int |= custom_c_62_bits
1305
+
1306
+ # We lie about the version and then correct it afterwards
1307
+ uuid_str = str(uuid6.UUID(int=uuid_int, version=7))
1308
+ assert(uuid_str[14] == '7')
1309
+ uuid_str = uuid_str[:14] +'8' + uuid_str[15:]
1310
+
1311
+ return(uuid_str)
1312
+
1313
+ @staticmethod
1314
+ def migrate_0_0_1_vcon(old_vcon : dict) -> dict:
1315
+ """
1316
+ Migrate/translate an an older deprecated vCon to the current version.
1317
+
1318
+ Parameters:
1319
+ old_vcon old format 0.0.1 vCon
1320
+
1321
+ Returns:
1322
+ the modified old_vcon in the new format
1323
+ """
1324
+
1325
+ # Fix dates in older dialogs
1326
+ for index, dialog in enumerate(old_vcon.get("dialog", [])):
1327
+ if("start" in dialog):
1328
+ dialog['start'] = vcon.utils.cannonize_date(dialog['start'])
1329
+
1330
+ if("alg" in dialog):
1331
+ if( dialog['alg'] == "lm-ots"):
1332
+ dialog['alg'] = "LMOTS_SHA256_N32_W8"
1333
+ elif( dialog['alg'] in ["SHA-512", "LMOTS_SHA256_N32_W8"]):
1334
+ pass
1335
+ else:
1336
+ raise AttributeError("dialog[{}] alg: {} not supported. Must be SHA-512 or LMOTS_SHA256_N32_W8".format(index, dialog['alg']))
1337
+
1338
+ # Translate transcriptions to body for consistency with dialog and attachments
1339
+ for index, analysis in enumerate(old_vcon.get("analysis", [])):
1340
+ if(analysis['type'] == "transcript"):
1341
+ if("transcript" in analysis):
1342
+ analysis['body'] = analysis.pop('transcript')
1343
+
1344
+ if(isinstance(analysis['body'], dict)):
1345
+ analysis['encoding'] = "json"
1346
+ elif(isinstance(analysis['body'], str)):
1347
+ analysis['encoding'] = "none"
1348
+
1349
+ else:
1350
+ raise Exception("body type: {} in analysis[{}] not recognized".format(type(analysis['body']), index))
1351
+
1352
+ return(old_vcon)
1353
+