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