/ LXMF / LXMessage.py
LXMessage.py
  1  import RNS
  2  import RNS.vendor.umsgpack as msgpack
  3  
  4  import os
  5  import time
  6  import base64
  7  import multiprocessing
  8  
  9  import LXMF.LXStamper as LXStamper
 10  from .LXMF import APP_NAME
 11  
 12  
 13  class LXMessage:
 14      GENERATING         = 0x00
 15      OUTBOUND           = 0x01
 16      SENDING            = 0x02
 17      SENT               = 0x04
 18      DELIVERED          = 0x08
 19      REJECTED           = 0xFD
 20      CANCELLED          = 0xFE
 21      FAILED             = 0xFF
 22      states             = [GENERATING, OUTBOUND, SENDING, SENT, DELIVERED, REJECTED, CANCELLED, FAILED]
 23  
 24      UNKNOWN            = 0x00
 25      PACKET             = 0x01
 26      RESOURCE           = 0x02
 27      representations    = [UNKNOWN, PACKET, RESOURCE]
 28  
 29      OPPORTUNISTIC      = 0x01
 30      DIRECT             = 0x02
 31      PROPAGATED         = 0x03
 32      PAPER              = 0x05
 33      valid_methods      = [OPPORTUNISTIC, DIRECT, PROPAGATED, PAPER]
 34  
 35      SOURCE_UNKNOWN     = 0x01
 36      SIGNATURE_INVALID  = 0x02
 37      unverified_reasons = [SOURCE_UNKNOWN, SIGNATURE_INVALID]
 38  
 39      DESTINATION_LENGTH = RNS.Identity.TRUNCATED_HASHLENGTH//8
 40      SIGNATURE_LENGTH   = RNS.Identity.SIGLENGTH//8
 41      TICKET_LENGTH      = RNS.Identity.TRUNCATED_HASHLENGTH//8
 42  
 43      # Default ticket expiry is 3 weeks, with an
 44      # additional grace period of 5 days, allowing
 45      # for timekeeping inaccuracies. Tickets will
 46      # automatically renew when there is less than
 47      # 14 days to expiry.
 48      TICKET_EXPIRY      = 21*24*60*60
 49      TICKET_GRACE       = 5*24*60*60
 50      TICKET_RENEW       = 14*24*60*60
 51      TICKET_INTERVAL    = 1*24*60*60
 52      COST_TICKET        = 0x100
 53  
 54      # LXMF overhead is 112 bytes per message:
 55      #   16  bytes for destination hash
 56      #   16  bytes for source hash
 57      #   64  bytes for Ed25519 signature
 58      #   8   bytes for timestamp
 59      #   8   bytes for msgpack structure
 60      TIMESTAMP_SIZE  = 8
 61      STRUCT_OVERHEAD = 8
 62      LXMF_OVERHEAD   = 2*DESTINATION_LENGTH + SIGNATURE_LENGTH + TIMESTAMP_SIZE + STRUCT_OVERHEAD
 63  
 64      # With an MTU of 500, the maximum amount of data
 65      # we can send in a single encrypted packet is
 66      # 391 bytes.
 67      ENCRYPTED_PACKET_MDU = RNS.Packet.ENCRYPTED_MDU + TIMESTAMP_SIZE
 68      
 69      # The max content length we can fit in LXMF message
 70      # inside a single RNS packet is the encrypted MDU, minus
 71      # the LXMF overhead. We can optimise a bit though, by
 72      # inferring the destination hash from the destination
 73      # field of the packet, therefore we also add the length
 74      # of a destination hash to the calculation. With default
 75      # RNS and LXMF parameters, the largest single-packet
 76      # LXMF message we can send is 295 bytes. If a message
 77      # is larger than that, a Reticulum link will be used.
 78      ENCRYPTED_PACKET_MAX_CONTENT = ENCRYPTED_PACKET_MDU - LXMF_OVERHEAD + DESTINATION_LENGTH
 79      
 80      # Links can carry a larger MDU, due to less overhead per
 81      # packet. The link MDU with default Reticulum parameters
 82      # is 431 bytes.
 83      LINK_PACKET_MDU = RNS.Link.MDU
 84  
 85      # Which means that we can deliver single-packet LXMF
 86      # messages with content of up to 319 bytes over a link.
 87      # If a message is larger than that, LXMF will sequence
 88      # and transfer it as a RNS resource over the link instead.
 89      LINK_PACKET_MAX_CONTENT = LINK_PACKET_MDU - LXMF_OVERHEAD
 90  
 91      # For plain packets without encryption, we can
 92      # fit up to 368 bytes of content.
 93      PLAIN_PACKET_MDU = RNS.Packet.PLAIN_MDU
 94      PLAIN_PACKET_MAX_CONTENT = PLAIN_PACKET_MDU - LXMF_OVERHEAD + DESTINATION_LENGTH
 95  
 96      # Descriptive strings regarding transport encryption
 97      ENCRYPTION_DESCRIPTION_AES = "AES-128"
 98      ENCRYPTION_DESCRIPTION_EC  = "Curve25519"
 99      ENCRYPTION_DESCRIPTION_UNENCRYPTED = "Unencrypted"
100  
101      # Constants for QR/URI encoding LXMs
102      URI_SCHEMA = "lxm"
103      QR_ERROR_CORRECTION = "ERROR_CORRECT_L"
104      QR_MAX_STORAGE = 2953
105      PAPER_MDU = ((QR_MAX_STORAGE-(len(URI_SCHEMA)+len("://")))*6)//8
106  
107      def __str__(self):
108          if self.hash != None:
109              return "<LXMessage "+RNS.hexrep(self.hash, delimit=False)+">"
110          else:
111              return "<LXMessage>"
112  
113      def __init__(self, destination, source, content = "", title = "", fields = None, desired_method = None, destination_hash = None, source_hash = None, stamp_cost=None, include_ticket=False):
114  
115          if isinstance(destination, RNS.Destination) or destination == None:
116              self.__destination    = destination
117              if destination != None:
118                  self.destination_hash = destination.hash
119              else:
120                  self.destination_hash = destination_hash
121          else:
122              raise ValueError("LXMessage initialised with invalid destination")
123  
124          if isinstance(source, RNS.Destination) or source == None:
125              self.__source    = source
126              if source != None:
127                  self.source_hash = source.hash
128              else:
129                  self.source_hash = source_hash
130          else:
131              raise ValueError("LXMessage initialised with invalid source")
132  
133          if title == None:
134              title = ""
135  
136          if type(title) == bytes:
137              self.set_title_from_bytes(title)
138          else:
139              self.set_title_from_string(title)
140  
141          if type(content) == bytes:
142              self.set_content_from_bytes(content)
143          else:
144              self.set_content_from_string(content)
145  
146          self.set_fields(fields)
147  
148          self.payload                 = None
149          self.timestamp               = None
150          self.signature               = None
151          self.hash                    = None
152          self.transient_id            = None
153          self.packed                  = None
154          self.state                   = LXMessage.GENERATING
155          self.method                  = LXMessage.UNKNOWN
156          self.progress                = 0.0
157          self.rssi                    = None
158          self.snr                     = None
159          self.q                       = None
160  
161          self.stamp                   = None
162          self.stamp_cost              = stamp_cost
163          self.stamp_value             = None
164          self.stamp_valid             = False
165          self.stamp_checked           = False
166          self.propagation_stamp       = None
167          self.propagation_stamp_value = None
168          self.propagation_stamp_valid = False
169          self.propagation_target_cost = None
170          self.defer_stamp             = True
171          self.defer_propagation_stamp = True
172          self.outbound_ticket         = None
173          self.include_ticket          = include_ticket
174  
175          self.propagation_packed      = None
176          self.paper_packed            = None
177  
178          self.incoming                = False
179          self.signature_validated     = False
180          self.unverified_reason       = None
181          self.ratchet_id              = None
182  
183          self.representation          = LXMessage.UNKNOWN
184          self.desired_method          = desired_method
185          self.delivery_attempts       = 0
186          self.transport_encrypted     = False
187          self.transport_encryption    = None
188          self.ratchet_id              = None
189          self.packet_representation   = None
190          self.resource_representation = None
191          self.__delivery_destination  = None
192          self.__delivery_callback     = None
193          self.__pn_encrypted_data     = None
194          self.failed_callback         = None
195          
196          self.deferred_stamp_generating = False
197  
198      def set_title_from_string(self, title_string):
199          self.title = title_string.encode("utf-8")
200  
201      def set_title_from_bytes(self, title_bytes):
202          self.title = title_bytes
203  
204      def title_as_string(self):
205          return self.title.decode("utf-8")
206  
207      def set_content_from_string(self, content_string):
208          self.content = content_string.encode("utf-8")
209  
210      def set_content_from_bytes(self, content_bytes):
211          self.content = content_bytes
212  
213      def content_as_string(self):
214          try:
215              return self.content.decode("utf-8")
216          except Exception as e:
217              RNS.log(f"{self} could not decode message content as string: {e}")
218              return None
219  
220      def set_fields(self, fields):
221          if isinstance(fields, dict) or fields == None:
222              self.fields = fields or {}
223          else:
224              raise ValueError("LXMessage property \"fields\" can only be dict or None")
225  
226      def get_fields(self):
227          return self.fields
228  
229      @property
230      def destination(self):
231          return self.__destination
232  
233      @destination.setter
234      def destination(self, destination):
235          self.set_destination(destination)
236  
237      def get_destination(self):
238          return self.destination
239  
240      def set_destination(self, destination):
241          if self.destination == None:
242              if isinstance(destination, RNS.Destination):
243                  self.__destination = destination
244              else:
245                  raise ValueError("Invalid destination set on LXMessage")
246          else:
247              raise ValueError("Cannot reassign destination on LXMessage")
248  
249      @property
250      def source(self):
251          return self.__source
252  
253      @source.setter
254      def source(self, source):
255          self.set_source(source)
256  
257      def get_source(self):
258          return self.source
259  
260      def set_source(self, source):
261          if self.source == None:
262              if isinstance(source, RNS.Destination):
263                  self.__source = source
264              else:
265                  raise ValueError("Invalid source set on LXMessage")
266          else:
267              raise ValueError("Cannot reassign source on LXMessage")
268  
269      def set_delivery_destination(self, delivery_destination):
270          self.__delivery_destination = delivery_destination
271  
272      def register_delivery_callback(self, callback):
273          self.__delivery_callback = callback
274  
275      def register_failed_callback(self, callback):
276          self.failed_callback = callback
277  
278      def validate_stamp(self, target_cost, tickets=None):
279          if tickets != None:
280              for ticket in tickets:
281                  try:
282                      if self.stamp == RNS.Identity.truncated_hash(ticket+self.message_id):
283                          RNS.log(f"Stamp on {self} validated by inbound ticket", RNS.LOG_DEBUG) # TODO: Remove at some point
284                          self.stamp_value = LXMessage.COST_TICKET
285                          return True
286                  except Exception as e:
287                      RNS.log(f"Error while validating ticket: {e}", RNS.LOG_ERROR)
288                      RNS.trace_exception(e)
289  
290          if self.stamp == None:
291              return False
292          else:
293              workblock = LXStamper.stamp_workblock(self.message_id)
294              if LXStamper.stamp_valid(self.stamp, target_cost, workblock):
295                  RNS.log(f"Stamp on {self} validated", RNS.LOG_DEBUG) # TODO: Remove at some point
296                  self.stamp_value = LXStamper.stamp_value(workblock, self.stamp)
297                  return True
298              else:
299                  return False
300  
301      def get_stamp(self, timeout=None):
302          # If an outbound ticket exists, use this for
303          # generating a valid stamp.
304          if self.outbound_ticket != None and type(self.outbound_ticket) == bytes and len(self.outbound_ticket) == LXMessage.TICKET_LENGTH:
305              generated_stamp = RNS.Identity.truncated_hash(self.outbound_ticket+self.message_id)
306              self.stamp_value = LXMessage.COST_TICKET
307              RNS.log(f"Generated stamp with outbound ticket {RNS.hexrep(self.outbound_ticket)} for {self}", RNS.LOG_DEBUG) # TODO: Remove at some point
308              return generated_stamp
309  
310          # If no stamp cost is required, we can just
311          # return immediately.
312          elif self.stamp_cost == None:
313              self.stamp_value = None
314              return None
315  
316          # If a stamp was already generated, return
317          # it immediately.
318          elif self.stamp != None:
319              return self.stamp
320  
321          # Otherwise, we will need to generate a
322          # valid stamp according to the cost that
323          # the receiver has specified.
324          else:
325              generated_stamp, value = LXStamper.generate_stamp(self.message_id, self.stamp_cost)
326              if generated_stamp:
327                  self.stamp_value = value
328                  self.stamp_valid = True
329                  return generated_stamp
330              
331              else:
332                  return None
333  
334      def get_propagation_stamp(self, target_cost, timeout=None):
335          # If a stamp was already generated, return
336          # it immediately.
337          if self.propagation_stamp != None:
338              return self.propagation_stamp
339  
340          # Otherwise, we will need to generate a
341          # valid stamp according to the cost that
342          # the propagation node has specified.
343          else:
344              self.propagation_target_cost = target_cost
345              if self.propagation_target_cost == None:
346                  raise ValueError("Cannot generate propagation stamp without configured target propagation cost")
347  
348              
349              if not self.transient_id: self.pack()
350              generated_stamp, value = LXStamper.generate_stamp(self.transient_id, target_cost, expand_rounds=LXStamper.WORKBLOCK_EXPAND_ROUNDS_PN)
351              if generated_stamp:
352                  self.propagation_stamp = generated_stamp
353                  self.propagation_stamp_value = value
354                  self.propagation_stamp_valid = True
355                  return generated_stamp
356              
357              else:
358                  return None
359  
360      def pack(self, payload_updated=False):
361          if not self.packed:
362              if self.timestamp == None: self.timestamp = time.time()
363  
364              self.propagation_packed = None
365              self.paper_packed = None
366  
367              self.payload = [self.timestamp, self.title, self.content, self.fields]
368  
369              hashed_part      = b""
370              hashed_part     += self.__destination.hash
371              hashed_part     += self.__source.hash
372              hashed_part     += msgpack.packb(self.payload)
373              self.hash        = RNS.Identity.full_hash(hashed_part)
374              self.message_id  = self.hash
375  
376              if not self.defer_stamp:
377                  self.stamp       = self.get_stamp()
378                  if self.stamp   != None: self.payload.append(self.stamp)
379  
380              signed_part      = b""
381              signed_part     += hashed_part
382              signed_part     += self.hash
383              self.signature   = self.__source.sign(signed_part)
384              self.signature_validated = True
385  
386              packed_payload   = msgpack.packb(self.payload)
387              self.packed      = b""
388              self.packed     += self.__destination.hash
389              self.packed     += self.__source.hash
390              self.packed     += self.signature
391              self.packed     += packed_payload
392              self.packed_size = len(self.packed)
393              content_size     = len(packed_payload)-LXMessage.TIMESTAMP_SIZE-LXMessage.STRUCT_OVERHEAD
394  
395              # If no desired delivery method has been defined,
396              # one will be chosen according to these rules:
397              if self.desired_method == None:
398                  self.desired_method = LXMessage.DIRECT
399              
400              # If opportunistic delivery was requested, check
401              # that message will fit within packet size limits
402              if self.desired_method == LXMessage.OPPORTUNISTIC:
403                  if self.__destination.type == RNS.Destination.SINGLE:
404                      if content_size > LXMessage.ENCRYPTED_PACKET_MAX_CONTENT:
405                          RNS.log(f"Opportunistic delivery was requested for {self}, but content of length {content_size} exceeds packet size limit. Falling back to link-based delivery.", RNS.LOG_DEBUG)
406                          self.desired_method = LXMessage.DIRECT
407  
408              # Set delivery parameters according to delivery method
409              if self.desired_method == LXMessage.OPPORTUNISTIC:
410                  if self.__destination.type == RNS.Destination.SINGLE:
411                      single_packet_content_limit = LXMessage.ENCRYPTED_PACKET_MAX_CONTENT
412                  elif self.__destination.type == RNS.Destination.PLAIN:
413                      single_packet_content_limit = LXMessage.PLAIN_PACKET_MAX_CONTENT
414  
415                  if content_size > single_packet_content_limit:
416                      raise TypeError(f"LXMessage desired opportunistic delivery method, but content of length {content_size} exceeds single-packet content limit of {single_packet_content_limit}.")
417                  else:
418                      self.method = LXMessage.OPPORTUNISTIC
419                      self.representation = LXMessage.PACKET
420                      self.__delivery_destination = self.__destination
421  
422              elif self.desired_method == LXMessage.DIRECT:
423                  single_packet_content_limit = LXMessage.LINK_PACKET_MAX_CONTENT
424                  if content_size <= single_packet_content_limit:
425                      self.method = self.desired_method
426                      self.representation = LXMessage.PACKET
427                  else:
428                      self.method = self.desired_method
429                      self.representation = LXMessage.RESOURCE
430  
431              elif self.desired_method == LXMessage.PROPAGATED:
432                  single_packet_content_limit = LXMessage.LINK_PACKET_MAX_CONTENT
433  
434                  if self.__pn_encrypted_data == None or payload_updated:
435                      self.__pn_encrypted_data = self.__destination.encrypt(self.packed[LXMessage.DESTINATION_LENGTH:])
436                      self.ratchet_id          = self.__destination.latest_ratchet_id
437  
438                  lxmf_data = self.packed[:LXMessage.DESTINATION_LENGTH]+self.__pn_encrypted_data
439                  self.transient_id = RNS.Identity.full_hash(lxmf_data)
440                  if self.propagation_stamp != None: lxmf_data += self.propagation_stamp
441                  self.propagation_packed = msgpack.packb([time.time(), [lxmf_data]])
442  
443                  content_size = len(self.propagation_packed)
444                  if content_size <= single_packet_content_limit:
445                      self.method = self.desired_method
446                      self.representation = LXMessage.PACKET
447                  else:
448                      self.method = self.desired_method
449                      self.representation = LXMessage.RESOURCE
450  
451              elif self.desired_method == LXMessage.PAPER:
452                  paper_content_limit = LXMessage.PAPER_MDU
453  
454                  encrypted_data = self.__destination.encrypt(self.packed[LXMessage.DESTINATION_LENGTH:])
455                  self.ratchet_id = self.__destination.latest_ratchet_id
456                  self.paper_packed = self.packed[:LXMessage.DESTINATION_LENGTH]+encrypted_data
457  
458                  content_size = len(self.paper_packed)
459                  if content_size <= paper_content_limit:
460                      self.method = self.desired_method
461                      self.representation = LXMessage.PAPER
462                  else:
463                      raise TypeError("LXMessage desired paper delivery method, but content exceeds paper message maximum size.")
464  
465          else:
466              raise ValueError("Attempt to re-pack LXMessage "+str(self)+" that was already packed")
467  
468      def send(self):
469          self.determine_transport_encryption()
470  
471          if self.method == LXMessage.OPPORTUNISTIC:
472              lxm_packet = self.__as_packet()
473              lxm_packet.send().set_delivery_callback(self.__mark_delivered)
474              self.progress = 0.50
475              self.ratchet_id = lxm_packet.ratchet_id
476              self.state = LXMessage.SENT
477          
478          elif self.method == LXMessage.DIRECT:
479              self.state = LXMessage.SENDING
480  
481              if self.representation == LXMessage.PACKET:
482                  lxm_packet = self.__as_packet()
483                  receipt = lxm_packet.send()
484                  self.ratchet_id = self.__delivery_destination.link_id
485                  if receipt:
486                      receipt.set_delivery_callback(self.__mark_delivered)
487                      receipt.set_timeout_callback(self.__link_packet_timed_out)
488                      self.progress = 0.50
489                  else:
490                      if self.__delivery_destination:
491                          self.__delivery_destination.teardown()
492  
493              elif self.representation == LXMessage.RESOURCE:
494                  self.resource_representation = self.__as_resource()
495                  self.ratchet_id = self.__delivery_destination.link_id
496                  self.progress = 0.10
497  
498          elif self.method == LXMessage.PROPAGATED:
499              self.state = LXMessage.SENDING
500  
501              if self.representation == LXMessage.PACKET:
502                  receipt = self.__as_packet().send()
503                  if receipt:
504                      receipt.set_delivery_callback(self.__mark_propagated)
505                      receipt.set_timeout_callback(self.__link_packet_timed_out)
506                      self.progress = 0.50
507                  else:
508                      self.__delivery_destination.teardown()
509  
510              elif self.representation == LXMessage.RESOURCE:
511                  self.resource_representation = self.__as_resource()
512                  self.progress = 0.10
513  
514  
515      def determine_transport_encryption(self):
516          # TODO: These descriptions are old and outdated.
517          # Update the transport encryption descriptions to
518          # account for ratchets and other changes.
519          if self.method == LXMessage.OPPORTUNISTIC:
520              if self.__destination.type == RNS.Destination.SINGLE:
521                  self.transport_encrypted = True
522                  self.transport_encryption = LXMessage.ENCRYPTION_DESCRIPTION_EC
523              elif self.__destination.type == RNS.Destination.GROUP:
524                  self.transport_encrypted = True
525                  self.transport_encryption = LXMessage.ENCRYPTION_DESCRIPTION_AES
526              else:
527                  self.transport_encrypted = False
528                  self.transport_encryption = LXMessage.ENCRYPTION_DESCRIPTION_UNENCRYPTED
529          elif self.method == LXMessage.DIRECT:
530              self.transport_encrypted = True
531              self.transport_encryption = LXMessage.ENCRYPTION_DESCRIPTION_EC
532          elif self.method == LXMessage.PROPAGATED:
533              if self.__destination.type == RNS.Destination.SINGLE:
534                  self.transport_encrypted = True
535                  self.transport_encryption = LXMessage.ENCRYPTION_DESCRIPTION_EC
536              elif self.__destination.type == RNS.Destination.GROUP:
537                  self.transport_encrypted = True
538                  self.transport_encryption = LXMessage.ENCRYPTION_DESCRIPTION_AES
539              else:
540                  self.transport_encrypted = False
541                  self.transport_encryption = LXMessage.ENCRYPTION_DESCRIPTION_UNENCRYPTED
542          elif self.method == LXMessage.PAPER:
543              if self.__destination.type == RNS.Destination.SINGLE:
544                  self.transport_encrypted = True
545                  self.transport_encryption = LXMessage.ENCRYPTION_DESCRIPTION_EC
546              elif self.__destination.type == RNS.Destination.GROUP:
547                  self.transport_encrypted = True
548                  self.transport_encryption = LXMessage.ENCRYPTION_DESCRIPTION_AES
549              else:
550                  self.transport_encrypted = False
551                  self.transport_encryption = LXMessage.ENCRYPTION_DESCRIPTION_UNENCRYPTED
552          else:
553              self.transport_encrypted = False
554              self.transport_encryption = LXMessage.ENCRYPTION_DESCRIPTION_UNENCRYPTED
555  
556      def __mark_delivered(self, receipt = None):
557          RNS.log("Received delivery notification for "+str(self), RNS.LOG_DEBUG)
558          self.state = LXMessage.DELIVERED
559          self.progress = 1.0
560  
561          if self.__delivery_callback != None and callable(self.__delivery_callback):
562              try:
563                  self.__delivery_callback(self)
564              except Exception as e:
565                      RNS.log("An error occurred in the external delivery callback for "+str(self), RNS.LOG_ERROR)
566                      RNS.trace_exception(e)
567  
568      def __mark_propagated(self, receipt = None):
569          RNS.log("Received propagation success notification for "+str(self), RNS.LOG_DEBUG)
570          self.state = LXMessage.SENT
571          self.progress = 1.0
572  
573          if self.__delivery_callback != None and callable(self.__delivery_callback):
574              try:
575                  self.__delivery_callback(self)
576              except Exception as e:
577                      RNS.log("An error occurred in the external delivery callback for "+str(self), RNS.LOG_ERROR)
578                      RNS.trace_exception(e)
579  
580      def __mark_paper_generated(self, receipt = None):
581          RNS.log("Paper message generation succeeded for "+str(self), RNS.LOG_DEBUG)
582          self.state = LXMessage.PAPER
583          self.progress = 1.0
584  
585          if self.__delivery_callback != None and callable(self.__delivery_callback):
586              try:
587                  self.__delivery_callback(self)
588              except Exception as e:
589                      RNS.log("An error occurred in the external delivery callback for "+str(self), RNS.LOG_ERROR)
590                      RNS.trace_exception(e)
591  
592      def __resource_concluded(self, resource):
593          if resource.status == RNS.Resource.COMPLETE:
594              self.__mark_delivered()
595          else:
596              if resource.status == RNS.Resource.REJECTED:
597                  self.state = LXMessage.REJECTED
598  
599              elif self.state != LXMessage.CANCELLED:
600                  resource.link.teardown()
601                  self.state = LXMessage.OUTBOUND
602  
603      def __propagation_resource_concluded(self, resource):
604          if resource.status == RNS.Resource.COMPLETE:
605              self.__mark_propagated()
606          else:
607              if self.state != LXMessage.CANCELLED:
608                  resource.link.teardown()
609                  self.state = LXMessage.OUTBOUND
610  
611      def __link_packet_timed_out(self, packet_receipt):
612          if self.state != LXMessage.CANCELLED:
613              if packet_receipt:
614                  packet_receipt.destination.teardown()
615          
616              self.state = LXMessage.OUTBOUND
617  
618      def __update_transfer_progress(self, resource):
619          self.progress = 0.10 + (resource.get_progress()*0.90)
620  
621      def __as_packet(self):
622          if not self.packed:
623              self.pack()
624  
625          if not self.__delivery_destination:
626              raise ValueError("Can't synthesize packet for LXMF message before delivery destination is known")
627  
628          if self.method == LXMessage.OPPORTUNISTIC:
629              return RNS.Packet(self.__delivery_destination, self.packed[LXMessage.DESTINATION_LENGTH:])
630          elif self.method == LXMessage.DIRECT:
631              return RNS.Packet(self.__delivery_destination, self.packed)
632          elif self.method == LXMessage.PROPAGATED:
633              return RNS.Packet(self.__delivery_destination, self.propagation_packed)
634  
635      def __as_resource(self):
636          if not self.packed:
637              self.pack()
638  
639          if not self.__delivery_destination:
640              raise ValueError("Can't synthesize resource for LXMF message before delivery destination is known")
641  
642          if not self.__delivery_destination.type == RNS.Destination.LINK:
643              raise TypeError("Tried to synthesize resource for LXMF message on a delivery destination that was not a link")
644  
645          if not self.__delivery_destination.status == RNS.Link.ACTIVE:
646              raise ConnectionError("Tried to synthesize resource for LXMF message on a link that was not active")
647  
648          if self.method == LXMessage.DIRECT:
649              return RNS.Resource(self.packed, self.__delivery_destination, callback = self.__resource_concluded, progress_callback = self.__update_transfer_progress)
650          elif self.method == LXMessage.PROPAGATED:
651              return RNS.Resource(self.propagation_packed, self.__delivery_destination, callback = self.__propagation_resource_concluded, progress_callback = self.__update_transfer_progress)
652          else:
653              return None
654  
655      def packed_container(self):
656          if not self.packed:
657              self.pack()
658  
659          container = {
660              "state": self.state,
661              "lxmf_bytes": self.packed,
662              "transport_encrypted": self.transport_encrypted,
663              "transport_encryption": self.transport_encryption,
664              "method": self.method
665          }
666  
667          return msgpack.packb(container)
668  
669  
670      def write_to_directory(self, directory_path):
671          file_name = RNS.hexrep(self.hash, delimit=False)
672          file_path = directory_path+"/"+file_name
673  
674          try:
675              file = open(file_path, "wb")
676              file.write(self.packed_container())
677              file.close()
678  
679              return file_path
680  
681          except Exception as e:
682              RNS.log("Error while writing LXMF message to file \""+str(file_path)+"\". The contained exception was: "+str(e), RNS.LOG_ERROR)
683              return None
684  
685      def as_uri(self, finalise=True):
686          if not self.packed:
687              self.pack()
688  
689          if self.desired_method == LXMessage.PAPER and self.paper_packed != None:
690              # Encode packed LXM with URL-safe base64 and remove padding
691              encoded_bytes = base64.urlsafe_b64encode(self.paper_packed)
692  
693              # Add protocol specifier and return
694              lxm_uri = LXMessage.URI_SCHEMA+"://"+encoded_bytes.decode("utf-8").replace("=","")
695  
696              if finalise:
697                  self.determine_transport_encryption()
698                  self.__mark_paper_generated()
699              
700              return lxm_uri
701  
702          else:
703              raise TypeError("Attempt to represent LXM with non-paper delivery method as URI")
704  
705      def as_qr(self):
706          if not self.packed:
707              self.pack()
708  
709          if self.desired_method == LXMessage.PAPER and self.paper_packed != None:
710              import importlib
711              if importlib.util.find_spec('qrcode') != None:
712                  import qrcode
713  
714                  qr = qrcode.make(
715                      error_correction = qrcode.constants.__dict__[LXMessage.QR_ERROR_CORRECTION],
716                      border = 1,
717                      data = self.as_uri(finalise=False),
718                  )
719  
720                  self.determine_transport_encryption()
721                  self.__mark_paper_generated()
722  
723                  return qr
724  
725              else:
726                  RNS.log("Generating QR-code representanions of LXMs requires the \"qrcode\" module to be installed.", RNS.LOG_CRITICAL)
727                  RNS.log("You can install it with the command: python3 -m pip install qrcode", RNS.LOG_CRITICAL)
728                  return None
729  
730          else:
731              raise TypeError("Attempt to represent LXM with non-paper delivery method as QR-code")
732  
733      @staticmethod
734      def unpack_from_bytes(lxmf_bytes, original_method = None):
735          destination_hash     = lxmf_bytes[:LXMessage.DESTINATION_LENGTH]
736          source_hash          = lxmf_bytes[LXMessage.DESTINATION_LENGTH:2*LXMessage.DESTINATION_LENGTH]
737          signature            = lxmf_bytes[2*LXMessage.DESTINATION_LENGTH:2*LXMessage.DESTINATION_LENGTH+LXMessage.SIGNATURE_LENGTH]
738          packed_payload       = lxmf_bytes[2*LXMessage.DESTINATION_LENGTH+LXMessage.SIGNATURE_LENGTH:]
739          unpacked_payload     = msgpack.unpackb(packed_payload)
740          
741          # Extract stamp from payload if included
742          if len(unpacked_payload) > 4:
743              stamp = unpacked_payload[4]
744              unpacked_payload = unpacked_payload[:4]
745              packed_payload = msgpack.packb(unpacked_payload)
746          else:
747              stamp = None
748  
749          hashed_part          = b"" + destination_hash + source_hash + packed_payload
750          message_hash         = RNS.Identity.full_hash(hashed_part)
751          signed_part          = b"" + hashed_part + message_hash
752          timestamp            = unpacked_payload[0]
753          title_bytes          = unpacked_payload[1]
754          content_bytes        = unpacked_payload[2]
755          fields               = unpacked_payload[3]
756  
757          destination_identity = RNS.Identity.recall(destination_hash)
758          if destination_identity != None:
759              destination = RNS.Destination(destination_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, APP_NAME, "delivery")
760          else:
761              destination = None
762          
763          source_identity = RNS.Identity.recall(source_hash)
764          if source_identity != None:
765              source = RNS.Destination(source_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, APP_NAME, "delivery")
766          else:
767              source = None
768  
769          message = LXMessage(
770              destination = destination,
771              source = source,
772              content = "",
773              title = "",
774              fields = fields,
775              destination_hash = destination_hash,
776              source_hash = source_hash,
777              desired_method = original_method)
778  
779          message.hash        = message_hash
780          message.message_id  = message.hash
781          message.signature   = signature
782          message.stamp       = stamp
783          message.incoming    = True
784          message.timestamp   = timestamp
785          message.packed      = lxmf_bytes
786          message.packed_size = len(lxmf_bytes)
787          message.set_title_from_bytes(title_bytes)
788          message.set_content_from_bytes(content_bytes)
789  
790          try:
791              if source:
792                  if source.identity.validate(signature, signed_part):
793                      message.signature_validated = True
794                  else:
795                      message.signature_validated = False
796                      message.unverified_reason = LXMessage.SIGNATURE_INVALID
797              else:
798                  signature_validated = False
799                  message.unverified_reason = LXMessage.SOURCE_UNKNOWN
800                  RNS.log("Unpacked LXMF message signature could not be validated, since source identity is unknown", RNS.LOG_DEBUG)
801          except Exception as e:
802              message.signature_validated = False
803              RNS.log("Error while validating LXMF message signature. The contained exception was: "+str(e), RNS.LOG_ERROR)
804  
805          return message
806          
807      @staticmethod
808      def unpack_from_file(lxmf_file_handle):
809          try:
810              container = msgpack.unpackb(lxmf_file_handle.read())
811              lxm = LXMessage.unpack_from_bytes(container["lxmf_bytes"])
812  
813              if "state" in container:
814                  lxm.state = container["state"]
815              if "transport_encrypted" in container:
816                  lxm.transport_encrypted = container["transport_encrypted"]
817              if "transport_encryption" in container:
818                  lxm.transport_encryption = container["transport_encryption"]
819              if "method" in container:
820                  lxm.method = container["method"]
821  
822              return lxm
823          except Exception as e:
824              RNS.log("Could not unpack LXMessage from file. The contained exception was: "+str(e), RNS.LOG_ERROR)
825              return None