1import ConfigParser 2import errno 3import hmac 4import os 5import re 6import sqlite3 7import thread 8from hashlib import sha256 9from Queue import Queue 10from threading import Event, Lock, Thread 11 12import pyaxo 13import pyperclip 14import txtorcon 15from nacl.utils import random 16from nacl.exceptions import CryptoError 17from pyaxo import Axolotl, Keypair, a2b, b2a 18from twisted.internet import reactor 19from twisted.internet.defer import Deferred 20from twisted.internet.endpoints import connectProtocol 21from twisted.internet.endpoints import TCP4ClientEndpoint, TCP4ServerEndpoint 22from twisted.internet.protocol import Factory 23from twisted.protocols.basic import NetstringReceiver 24from txtorcon import TorClientEndpoint 25 26from . import elements 27from . import errors 28from . import notifications 29from . import packets 30from . import requests 31from . import untalk 32from .contact import Contact 33from .elements import RequestElement, UntalkElement, PresenceElement 34from .elements import MessageElement, AuthenticationElement 35from .ui import ConversationUi, PeerUi 36from .utils import Address 37from .smp import SMP 38 39 40APP_NAME = 'unMessage' 41 42USER_DIR = os.path.expanduser('~') 43APP_DIR = os.path.join(USER_DIR, '.config', APP_NAME) 44CONFIG_FILE = os.path.join(APP_DIR, '{}.cfg'.format(APP_NAME)) 45 46CONFIG = ConfigParser.ConfigParser() 47CONFIG.read(CONFIG_FILE) 48 49DATA_LENGTH = 1024 50TIMEOUT = 30 51 52HOST = '127.0.0.1' 53PORT = 50000 54 55TOR_SOCKS_PORT = 9054 56TOR_CONTROL_PORT = 9055 57 58 59class Peer(object): 60 state_created = 'created' 61 state_running = 'running' 62 state_stopped = 'stopped' 63 64 def __init__(self, name, ui=None): 65 if not name: 66 raise errors.InvalidNameError() 67 68 self._info = PeerInfo(port_local_server=PORT) 69 self._name = name 70 self._persistence = Persistence(dbname=self._path_peer_db, 71 dbpassphrase=None) 72 self._axolotl = None 73 self._conversations = dict() 74 self._inbound_requests = dict() 75 self._outbound_requests = dict() 76 self._element_parser = ElementParser(self) 77 78 self._tor = None 79 self._onion_service = None 80 self._port_tor_socks = TOR_SOCKS_PORT 81 self._port_tor_control = TOR_CONTROL_PORT 82 83 self._ip_local_server = HOST 84 self._local_mode = False 85 86 self._twisted_reactor = reactor 87 self._twisted_server_endpoint = None 88 self._twisted_factory = None 89 90 self._managers_conv = list() 91 92 self._presence_convs = list() 93 self._presence_event = Event() 94 95 self._event_stop = Event() 96 97 self._ui = ui or PeerUi() 98 99 self._state = Peer.state_created 100 101 @property 102 def _path_peer_dir(self): 103 return os.path.join(APP_DIR, self.name) 104 105 @property 106 def _path_peer_db(self): 107 return os.path.join(self._path_peer_dir, 'peer.db') 108 109 @property 110 def _path_axolotl_db(self): 111 return os.path.join(self._path_peer_dir, 'axolotl.db') 112 113 @property 114 def _path_tor_dir(self): 115 return os.path.join(self._path_peer_dir, 'tor') 116 117 @property 118 def _path_tor_data_dir(self): 119 return os.path.join(self._path_tor_dir, 'data') 120 121 @property 122 def _path_onion_service_dir(self): 123 return os.path.join(self._path_tor_dir, 'onion-service') 124 125 @property 126 def _contacts(self): 127 return self._info.contacts 128 129 @_contacts.setter 130 def _contacts(self, contacts): 131 self._info.contacts = contacts 132 133 @property 134 def name(self): 135 return self._info.name 136 137 @name.setter 138 def _name(self, name): 139 self._info.name = name 140 141 @property 142 def onion_service_key(self): 143 return self._info.onion_service_key 144 145 @onion_service_key.setter 146 def _onion_service_key(self, onion_service_key): 147 self._info.onion_service_key = onion_service_key 148 149 @property 150 def address(self): 151 try: 152 onion_domain = self._onion_service.hostname 153 except AttributeError: 154 onion_domain = 'hostname-not-found' 155 return Address(onion_domain, self._port_local_server) 156 157 @property 158 def port_local_server(self): 159 return self._info.port_local_server 160 161 @port_local_server.setter 162 def _port_local_server(self, port_local_server): 163 self._info.port_local_server = port_local_server 164 165 @property 166 def identity(self): 167 return '{}@{}:{}'.format(self.name, 168 self.address.host, self.address.port) 169 170 @property 171 def identity_keys(self): 172 return self._info.identity_keys 173 174 @identity_keys.setter 175 def _identity_keys(self, keys): 176 self._info.identity_keys = keys 177 178 @property 179 def contacts(self): 180 return self._contacts.values() 181 182 @property 183 def conversations(self): 184 return self._conversations.values() 185 186 @property 187 def inbound_requests(self): 188 return self._inbound_requests.values() 189 190 @property 191 def outbound_requests(self): 192 return self._outbound_requests.values() 193 194 @property 195 def is_running(self): 196 return self._state == Peer.state_running 197 198 def _create_peer_dir(self): 199 if not os.path.exists(self._path_peer_dir): 200 os.makedirs(self._path_peer_dir) 201 if not os.path.exists(self._path_tor_dir): 202 os.makedirs(self._path_tor_dir) 203 204 def _load_peer_info(self): 205 if os.path.exists(self._path_peer_db): 206 self._info = self._persistence.load_peer_info() 207 208 def _update_config(self): 209 if not CONFIG.has_section('unMessage'): 210 CONFIG.add_section('unMessage') 211 CONFIG.set('unMessage', 'ui', self._ui.__module__) 212 CONFIG.set('unMessage', 'name', self.name) 213 214 with open(CONFIG_FILE, 'w') as f: 215 CONFIG.write(f) 216 217 def _save_peer_info(self): 218 self._persistence.save_peer_info(self._info) 219 220 def _load_conversations(self): 221 """Load all existing conversations in the peer's database. 222 223 Return a dictionary mapping a contact's name to its respective 224 ``Conversation`` object. 225 """ 226 convs = dict() 227 for other_name in self._axolotl.get_other_names(): 228 axolotl = self._axolotl.load_conversation(other_name) 229 convs[other_name] = Conversation( 230 self, 231 self._contacts[other_name], 232 keys=ConversationKeys(axolotl.id_), 233 axolotl=axolotl) 234 return convs 235 236 def _send_presence(self, offline=False): 237 self._presence_convs = list() 238 self._presence_event = Event() 239 240 for c in self.conversations: 241 if c.contact.has_presence: 242 if offline and c.is_active: 243 self._presence_convs.append(c.contact.name) 244 self._send_element(c, PresenceElement.type_, 245 content=PresenceElement.status_offline) 246 elif not offline and not c.is_active: 247 self._send_element(c, PresenceElement.type_, 248 content=PresenceElement.status_online) 249 250 if self._presence_convs: 251 # wait until all conversations are notified 252 self._presence_event.wait() 253 254 def _add_intro_manager(self, connection): 255 manager = Introduction(self, connection) 256 self._managers_conv.append(manager) 257 manager.start() 258 return manager 259 260 def _connect(self, address, callback, errback): 261 if self._local_mode: 262 point = TCP4ClientEndpoint(self._twisted_reactor, 263 host=HOST, port=address.port) 264 265 else: 266 point = TorClientEndpoint(address.host, address.port, 267 socks_hostname=HOST, 268 socks_port=self._port_tor_socks) 269 270 def connect_from_thread(): 271 d = connectProtocol(point, 272 _ConversationProtocol(self._twisted_factory, 273 callback)) 274 d.addErrback(errback) 275 276 self._twisted_reactor.callFromThread(connect_from_thread) 277 278 def _create_request(self, contact): 279 """Create an ``OutboundRequest`` to be sent to a ``Contact``.""" 280 iv = random(packets.IV_LEN) 281 282 req = requests.OutboundRequest(Conversation(self, contact)) 283 req.request_keys = pyaxo.generate_keypair() 284 req.handshake_keys = pyaxo.generate_keypair() 285 req.ratchet_keys = pyaxo.generate_keypair() 286 287 shared_request_key = pyaxo.generate_dh(req.request_keys.priv, 288 contact.key) 289 req.conversation.request_keys = ConversationKeys(shared_request_key) 290 291 hs_packet = packets.HandshakePacket(self.identity, 292 b2a(self.identity_keys.pub), 293 b2a(req.handshake_keys.pub), 294 b2a(req.ratchet_keys.pub)) 295 enc_hs_packet = pyaxo.encrypt_symmetric( 296 req.conversation.request_keys.handshake_enc_key, 297 str(hs_packet)) 298 299 req.packet = packets.RequestPacket( 300 b2a(iv), 301 b2a(pyaxo.hash_(iv + contact.key + 302 req.conversation.request_keys.iv_hash_key)), 303 b2a(keyed_hash(req.conversation.request_keys.payload_hash_key, 304 enc_hs_packet)), 305 b2a(req.request_keys.pub), 306 b2a(enc_hs_packet)) 307 308 return req 309 310 def _process_request(self, data): 311 """Create a ``RequestPacket`` from the data received.""" 312 req_packet = packets.build_request_packet(data) 313 hs_packet = a2b(req_packet.handshake_packet) 314 315 shared_request_key = pyaxo.generate_dh(self.identity_keys.priv, 316 a2b(req_packet.request_key)) 317 request_keys = ConversationKeys(shared_request_key) 318 hs_packet_hash = keyed_hash(request_keys.payload_hash_key, hs_packet) 319 320 if hs_packet_hash == a2b(req_packet.handshake_packet_hash): 321 try: 322 dec_hs_packet = pyaxo.decrypt_symmetric( 323 request_keys.handshake_enc_key, 324 hs_packet) 325 except CryptoError: 326 e = errors.MalformedPacketError('request') 327 e.message += ' - decryption failed' 328 raise e 329 330 req_packet.handshake_packet = packets.build_handshake_packet( 331 dec_hs_packet) 332 333 contact = Contact(req_packet.handshake_packet.identity, 334 a2b(req_packet.handshake_packet.identity_key)) 335 conv = Conversation(self, contact, request_keys=request_keys) 336 337 return requests.InboundRequest( 338 conversation=conv, 339 packet=req_packet) 340 else: 341 raise errors.CorruptedPacketError() 342 343 def _init_conv(self, conv, 344 priv_handshake_key, other_handshake_key, 345 ratchet_keys=Keypair(None, None), other_ratchet_key=None, 346 mode=False): 347 # if mode: 348 # the peer is Alice: she does not need to provide her ratchet 349 # keys as they will be generated when she starts ratcheting, 350 # but in order to do that she needs Bob's ratchet key (provided 351 # by Bob in his ``RequestPacket``), which is passed to 352 # ``Axolotl.init_conversation`` as ``other_ratchet_key=`` 353 # else: 354 # the peer is Bob: he sent a request to Alice with a random 355 # ratchet key and for that reason the state has to be created 356 # using that same key pair 357 axolotl = self._axolotl.init_conversation( 358 other_name=conv.contact.name, 359 priv_identity_key=self.identity_keys.priv, 360 identity_key=self.identity_keys.pub, 361 priv_handshake_key=priv_handshake_key, 362 other_identity_key=conv.contact.key, 363 other_handshake_key=other_handshake_key, 364 priv_ratchet_key=ratchet_keys.priv, 365 ratchet_key=ratchet_keys.pub, 366 other_ratchet_key=other_ratchet_key, 367 mode=mode) 368 axolotl.save() 369 370 conv.axolotl = axolotl 371 conv.state = Conversation.state_conv 372 conv.keys = ConversationKeys(axolotl.id_) 373 374 self._contacts[conv.contact.name] = conv.contact 375 self._conversations[conv.contact.name] = conv 376 self._ui.notify_conv_established( 377 notifications.ConversationNotification( 378 conv, 379 title='Conversation established', 380 message='You can now chat with {}'.format(conv.contact.name))) 381 382 def _delete_conversation(self, conversation): 383 conversation.close() 384 conversation.axolotl.delete() 385 del self._contacts[conversation.contact.name] 386 del self._conversations[conversation.contact.name] 387 388 def _send_element(self, conv, type_, content, handshake_key=None): 389 """Create an ``ElementPacket`` and add it to the outbout packets queue. 390 391 TODO 392 - Size invariance should be handled here, before encryption by 393 ``_send_packet`` 394 - Split the element into multiple packets if needed 395 """ 396 packet = packets.ElementPacket(type_, payload=content) 397 conv.queue_out_packets.put([packet, conv, handshake_key]) 398 399 def _send_packet(self, packet, conversation, handshake_key=None): 400 """Encrypt an ``ElementPacket`` as a ``RegularPacket`` and send it. 401 402 Before proceding, make sure the conversation has a connection. Wrap the 403 element packet with the regular encrypted packet and send it. After 404 successfully transmitting it, process it and parse the element. 405 """ 406 def element_sent(): 407 element = self._process_element_packet( 408 packet, 409 conversation, 410 sender=self.name, 411 receiver=conversation.contact.name) 412 self._element_parser.parse(element, conversation) 413 414 def element_not_sent(failure): 415 # TODO handle remaining packets 416 conversation.close() 417 418 # TODO handle expected errors and display better messages 419 conversation.ui.notify_disconnect( 420 notifications.UnmessageNotification(str(failure))) 421 422 def send_with_manager(manager): 423 # at this point there is already an existing conversation between 424 # the two parties in the database, so a ``RegularPacket`` can be 425 # created with ``_encrypt`` 426 reg_packet = self._encrypt(packet, conversation, handshake_key) 427 428 # pack the ``RegularPacket`` into a ``str`` and send it 429 manager.send_data(str(reg_packet), 430 callback=element_sent, 431 errback=element_not_sent) 432 433 def connection_failed(failure): 434 if packet.type_ != PresenceElement.type_: 435 if failure.check(txtorcon.socks.HostUnreachableError, 436 txtorcon.socks.TtlExpiredError): 437 conversation.ui.notify_offline( 438 errors.OfflinePeerError( 439 title=failure.getErrorMessage(), 440 contact=conversation.contact.name)) 441 else: 442 conversation.ui.notify_error(errors.UnmessageError( 443 title='Conversation connection failed', 444 message=str(failure))) 445 446 def connect(connection_made): 447 self._connect(conversation.contact.address, 448 callback=connection_made, 449 errback=connection_failed) 450 451 if conversation.is_active: 452 if packet.type_ in elements.REGULAR_ELEMENT_TYPES: 453 send_with_manager(conversation) 454 else: 455 manager = conversation._get_manager(packet.type_) 456 if not manager.connection: 457 def connection_made(connection): 458 manager = conversation.add_connection(connection, 459 packet.type_) 460 send_with_manager(manager) 461 462 # the peer makes another connection to the other one to 463 # send this "special" element 464 connect(connection_made) 465 else: 466 send_with_manager(manager) 467 else: 468 def connection_made(connection): 469 conversation.set_active(connection, Conversation.state_conv) 470 send_with_manager(conversation) 471 472 # the peer connects to the other one to resume a conversation 473 connect(connection_made) 474 475 def _receive_packet(self, packet, connection, conversation): 476 """Decrypt a ``RegularPacket`` as an ``ElementPacket``. 477 478 Unwrap the element packet with decryption, process it and parse the 479 element. 480 """ 481 try: 482 regular_packet = self._decrypt(packet, conversation) 483 except (errors.MalformedPacketError, errors.CorruptedPacketError) as e: 484 e.title += ' caused by "{}"'.format(conversation.contact.name) 485 self.peer.notify_error(e) 486 else: 487 element = self._process_element_packet( 488 packet=regular_packet, 489 conversation=conversation, 490 sender=conversation.contact.name, 491 receiver=self.name) 492 self._element_parser.parse(element, conversation, connection) 493 494 def _process_element_packet(self, packet, conversation, sender, receiver): 495 with conversation.elements_lock: 496 try: 497 # get the ``Element`` that corresponds to the 498 # ``ElementPacket.id_`` in case it is one of the parts of an 499 # incomplete element 500 element = conversation.elements.pop(packet.id_) 501 except KeyError: 502 # create an ``Element`` as there are no incomplete elements 503 # with the respective ``ElementPacket.id_`` 504 element = elements.Element(sender, 505 receiver, 506 type_=packet.type_, 507 id_=packet.id_, 508 part_len=packet.part_len) 509 510 # add the part from the packet 511 element[packet.part_num] = packet.payload 512 513 if element.is_complete: 514 # the ``Element`` does not have to be stored as either it 515 # fitted in a single packet or all of its parts have been 516 # transmitted (the ``packet`` contained the last remaining 517 # part) 518 pass 519 else: 520 # store the ``Element`` in the incomplete elements ``dict`` as 521 # it has been split in multiple parts, yet to be transmitted 522 conversation.elements[element.id_] = element 523 return element 524 525 def _encrypt(self, packet, conversation, handshake_key=None): 526 """Encrypt an ``ElementPacket`` and return a ``RegularPacket``.""" 527 iv = random(packets.IV_LEN) 528 plaintext = str(packet) 529 if handshake_key: 530 keys = conversation.request_keys 531 handshake_key = pyaxo.encrypt_symmetric(keys.handshake_enc_key, 532 handshake_key) 533 else: 534 keys = conversation.keys 535 handshake_key = '' 536 537 ciphertext = conversation.axolotl.encrypt(plaintext) 538 conversation.axolotl.save() 539 540 return packets.RegularPacket( 541 b2a(iv), 542 b2a(pyaxo.hash_(iv + conversation.contact.key + keys.iv_hash_key)), 543 b2a(keyed_hash(keys.payload_hash_key, handshake_key + ciphertext)), 544 b2a(handshake_key), 545 b2a(ciphertext)) 546 547 def _decrypt(self, packet, conversation): 548 """Decrypt a ``RegularPacket`` and return an ``ElementPacket``.""" 549 ciphertext = a2b(packet.payload) 550 keys = conversation.keys or conversation.request_keys 551 payload_hash = keyed_hash(keys.payload_hash_key, 552 a2b(packet.handshake_key) + ciphertext) 553 554 if payload_hash == a2b(packet.payload_hash): 555 plaintext = conversation.axolotl.decrypt(ciphertext) 556 conversation.axolotl.save() 557 return packets.build_element_packet(plaintext) 558 else: 559 raise errors.CorruptedPacketError() 560 561 def _start_server(self, launch_tor): 562 self._ui.notify_bootstrap( 563 notifications.UnmessageNotification('Configuring local server')) 564 565 endpoint = TCP4ServerEndpoint(self._twisted_reactor, 566 self._port_local_server, 567 interface=self._ip_local_server) 568 self._twisted_server_endpoint = endpoint 569 570 d = Deferred() 571 572 def endpoint_listening(port): 573 self._ui.notify_bootstrap( 574 notifications.UnmessageNotification('Running local server')) 575 576 if self._local_mode: 577 d.callback(None) 578 else: 579 d_tor = self._start_tor(launch_tor) 580 d_tor.addCallbacks(d.callback, d.errback) 581 582 self._twisted_factory = _ConversationFactory( 583 peer=self, 584 connection_made=self._add_intro_manager) 585 586 d_server = endpoint.listen(self._twisted_factory) 587 d_server.addCallbacks(endpoint_listening, d.errback) 588 589 def run_reactor(): 590 self._ui.notify_bootstrap( 591 notifications.UnmessageNotification('Running reactor')) 592 593 # TODO improve the way the reactor is run 594 self._twisted_reactor.run(installSignalHandlers=0) 595 thread.start_new_thread(run_reactor, ()) 596 597 return d 598 599 def _start_tor(self, launch_tor): 600 d_tor = Deferred() 601 602 def finish(result): 603 self._ui.notify_bootstrap( 604 notifications.UnmessageNotification( 605 'Added Onion Service to Tor')) 606 607 d_tor.callback(result) 608 609 def add_onion(tor): 610 self._tor = tor 611 612 self._ui.notify_bootstrap( 613 notifications.UnmessageNotification( 614 'Controlling Tor process')) 615 616 self._ui.notify_bootstrap( 617 notifications.UnmessageNotification( 618 'Waiting for the Onion Service')) 619 620 onion_service_string = '{} {}:{}'.format(self._port_local_server, 621 self._ip_local_server, 622 self._port_local_server) 623 if self.onion_service_key: 624 self._onion_service = txtorcon.EphemeralHiddenService( 625 [onion_service_string], 626 self.onion_service_key) 627 d_onion = self._onion_service.add_to_tor(self._tor._protocol) 628 else: 629 def save_key(result): 630 self._onion_service_key = self._onion_service.private_key 631 632 self._onion_service = txtorcon.EphemeralHiddenService( 633 [onion_service_string]) 634 d_onion = self._onion_service.add_to_tor(self._tor._protocol) 635 d_onion.addCallback(save_key) 636 637 d_onion.addCallback(finish) 638 639 if launch_tor: 640 self._ui.notify_bootstrap( 641 notifications.UnmessageNotification('Launching Tor')) 642 643 def display_bootstrap_lines(prog, tag, summary): 644 self._ui.notify_bootstrap( 645 notifications.UnmessageNotification( 646 '{}%: {}'.format(prog, summary))) 647 648 d_process = txtorcon.launch( 649 self._twisted_reactor, 650 progress_updates=display_bootstrap_lines, 651 data_directory=self._path_tor_data_dir, 652 socks_port=self._port_tor_socks) 653 else: 654 self._ui.notify_bootstrap( 655 notifications.UnmessageNotification( 656 'Connecting to existing Tor')) 657 658 endpoint = TCP4ClientEndpoint(self._twisted_reactor, 659 HOST, 660 self._port_tor_control) 661 d_process = txtorcon.connect(self._twisted_reactor, endpoint) 662 663 d_process.addCallback(add_onion) 664 d_process.addErrback(d_tor.errback) 665 666 return d_tor 667 668 def _stop_tor(self): 669 if self._onion_service: 670 self._ui.notify( 671 notifications.UnmessageNotification( 672 'Removing Onion Service from Tor')) 673 674 e = Event() 675 676 def removed(result): 677 self._ui.notify( 678 notifications.UnmessageNotification( 679 'Removed Onion Service from Tor')) 680 e.set() 681 682 d = self._onion_service.remove_from_tor(self._tor._protocol) 683 d.addCallback(removed) 684 e.wait() 685 686 def _send_request(self, identity, key): 687 result = re.match(r'[^@]+@[^:]+(:(\d+))?$', identity) 688 port = result.group(2) 689 if not port: 690 identity += ':' + str(PORT) 691 692 try: 693 contact = Contact(identity, key) 694 except (errors.InvalidIdentityError, 695 errors.InvalidPublicKeyError) as e: 696 self._ui.notify_error(e) 697 else: 698 req = self._create_request(contact) 699 700 def connection_made(connection): 701 def request_sent(): 702 self._outbound_requests[contact.identity] = req 703 self._ui.notify_out_request( 704 notifications.ContactNotification( 705 contact, 706 title='Request sent', 707 message='{} has received your request'.format( 708 identity))) 709 710 def request_failed(failure): 711 # TODO handle expected errors and display better messages 712 self._ui.notify_error(errors.UnmessageError( 713 title='Request packet failed', 714 message=str(failure))) 715 716 conv = req.conversation 717 conv.start() 718 conv.set_active(connection, Conversation.state_out_req) 719 720 # pack the ``RequestPacket`` into a ``str`` and send it to the 721 # other peer 722 conv.send_data(str(req.packet), 723 callback=request_sent, 724 errback=request_failed) 725 726 def connection_failed(failure): 727 if failure.check(txtorcon.socks.HostUnreachableError, 728 txtorcon.socks.TtlExpiredError): 729 self._ui.notify_error(errors.OfflinePeerError( 730 title=failure.getErrorMessage(), 731 contact=contact.name, 732 is_request=True)) 733 else: 734 self._ui.notify_error(errors.UnmessageError( 735 title='Request connection failed', 736 message=str(failure))) 737 738 self._connect(contact.address, 739 callback=connection_made, 740 errback=connection_failed) 741 742 def _accept_request(self, request, new_name): 743 conv = request.conversation 744 745 if new_name: 746 address = re.match(r'[^@]+(@[^:]+:\d+)', conv.contact.identity) 747 conv.contact.identity = new_name + address.group(1) 748 749 handshake_keys = pyaxo.generate_keypair() 750 self._init_conv( 751 conv, 752 priv_handshake_key=handshake_keys.priv, 753 other_handshake_key=a2b( 754 request.packet.handshake_packet.handshake_key), 755 other_ratchet_key=a2b( 756 request.packet.handshake_packet.ratchet_key), 757 mode=True) 758 759 self._send_element(conv, 760 RequestElement.type_, 761 content=RequestElement.request_accepted, 762 handshake_key=handshake_keys.pub) 763 764 def _untalk(self, conversation, input_device=None, output_device=None): 765 if conversation.is_active: 766 if self._can_talk(conversation): 767 untalk_session = (conversation.untalk_session or 768 conversation.init_untalk()) 769 if untalk_session.is_talking: 770 conversation.stop_untalk() 771 else: 772 try: 773 untalk_session.configure(input_device, output_device) 774 except untalk.AudioDeviceNotFoundError as e: 775 conversation.remove_manager(untalk_session) 776 self._ui.notify_error(e) 777 else: 778 self._send_element( 779 conversation, 780 UntalkElement.type_, 781 content=b2a(untalk_session.handshake_keys.pub)) 782 else: 783 self._ui.notify_error(errors.UntalkError( 784 message='You can only make one voice conversation at a ' 785 'time')) 786 else: 787 # TODO automatically connect and send request 788 self._ui.notify_error(errors.UntalkError( 789 message='You must be connected to {} in order to start a ' 790 'conversation'.format(conversation.contact.name))) 791 792 def _can_talk(self, conversation): 793 for c in self.conversations: 794 try: 795 if c.untalk_session.is_talking and c is not conversation: 796 return False 797 except AttributeError: 798 continue 799 return True 800 801 def _send_message(self, conversation, plaintext): 802 self._send_element(conversation, 803 MessageElement.type_, 804 content=plaintext) 805 806 def _authenticate(self, conversation, secret): 807 auth_session = conversation.auth_session 808 if not auth_session or auth_session.is_waiting or \ 809 auth_session.is_authenticated is not None: 810 auth_session = conversation.init_auth() 811 # TODO maybe use locks or something to prevent advancing or restarting 812 # while the SMP is doing its math 813 self._send_element(conversation, 814 AuthenticationElement.type_, 815 content=auth_session.start( 816 conversation.keys.auth_secret_key + secret)) 817 818 def get_contact(self, name): 819 return self.get_conversation(name).contact 820 821 def get_conversation(self, name): 822 try: 823 return self._conversations[name] 824 except KeyError: 825 raise errors.UnknownContactError(name) 826 827 def copy_onion(self): 828 self.copy_to_clipboard(self.address.host) 829 830 def copy_identity(self): 831 self.copy_to_clipboard(self.identity) 832 833 def copy_key(self): 834 self.copy_to_clipboard(b2a(self.identity_keys.pub)) 835 836 def copy_peer(self): 837 self.copy_to_clipboard('{} {}'.format(self.identity, 838 b2a(self.identity_keys.pub))) 839 840 def copy_to_clipboard(self, data): 841 try: 842 pyperclip.copy(data) 843 except pyperclip.exceptions.PyperclipException: 844 self._ui.notify_error(errors.UnmessageError( 845 title='Clipboard error', 846 message='A copy/paste mechanism for your system could not be ' 847 'found')) 848 849 def start(self, local_server_ip=None, 850 local_server_port=None, 851 launch_tor=True, 852 tor_socks_port=None, 853 tor_control_port=None, 854 local_mode=False): 855 if local_mode: 856 launch_tor = False 857 self._local_mode = local_mode 858 self._ui.notify_bootstrap( 859 notifications.UnmessageNotification('Starting peer')) 860 861 self._create_peer_dir() 862 self._load_peer_info() 863 self._update_config() 864 865 if local_server_ip: 866 self._ip_local_server = local_server_ip 867 if local_server_port: 868 self._port_local_server = int(local_server_port) 869 if tor_socks_port: 870 self._port_tor_socks = int(tor_socks_port) 871 if tor_control_port: 872 self._port_tor_control = int(tor_control_port) 873 874 def peer_started(result): 875 self._ui.notify_bootstrap( 876 notifications.UnmessageNotification('Peer started')) 877 878 self._axolotl = Axolotl(name=self.name, 879 dbname=self._path_axolotl_db, 880 dbpassphrase=None, 881 nonthreaded_sql=False) 882 if not self.identity_keys: 883 self._identity_keys = pyaxo.generate_keypair() 884 885 self._conversations = self._load_conversations() 886 for c in self.conversations: 887 c.start() 888 889 self._state = Peer.state_running 890 891 self._send_presence() 892 893 # TODO maybe return something useful to the UI? 894 self._ui.notify_peer_started( 895 notifications.UnmessageNotification(title='Peer started', 896 message=str(result))) 897 898 def peer_failed(failure): 899 self._ui.notify_peer_failed(notifications.UnmessageNotification( 900 title='Peer failed', 901 message=failure.getErrorMessage())) 902 903 def errback(reason): 904 self._ui.notify_error(errors.UnmessageError(str(reason))) 905 906 d = self._start_server(launch_tor) 907 d.addCallbacks(peer_started, peer_failed) 908 d.addErrback(errback) 909 910 def stop(self): 911 self._save_peer_info() 912 913 self._send_presence(offline=True) 914 915 self._event_stop.set() 916 917 for c in self.conversations: 918 c.close() 919 920 self._stop_tor() 921 922 self._twisted_reactor.callFromThread(self._twisted_reactor.stop) 923 924 self._state = Peer.state_stopped 925 926 def send_request(self, identity, key): 927 try: 928 key_bytes = a2b(key) 929 except TypeError: 930 raise errors.InvalidPublicKeyError() 931 else: 932 t = Thread(target=self._send_request, args=(identity, key_bytes,)) 933 t.daemon = True 934 t.start() 935 936 def accept_request(self, identity, new_name=None): 937 request = self._inbound_requests.pop(identity) 938 939 t = Thread(target=self._accept_request, args=(request, new_name,)) 940 t.daemon = True 941 t.start() 942 943 def delete_conversation(self, name): 944 self._delete_conversation(self.get_conversation(name)) 945 946 def set_presence(self, name, enable=False): 947 contact = self.get_contact(name) 948 contact.has_presence = enable 949 950 def verify_contact(self, name, key): 951 contact = self.get_contact(name) 952 if contact.key == a2b(key): 953 contact.is_verified = True 954 else: 955 contact.is_verified = False 956 raise errors.VerificationError(name) 957 958 def get_audio_devices(self): 959 return untalk.get_audio_devices() 960 961 def untalk(self, name, input_device=None, output_device=None): 962 t = Thread(target=self._untalk, 963 args=(self.get_conversation(name), 964 input_device, output_device,)) 965 t.daemon = True 966 t.start() 967 968 def send_message(self, name, plaintext): 969 t = Thread(target=self._send_message, 970 args=(self.get_conversation(name), plaintext,)) 971 t.daemon = True 972 t.start() 973 974 def authenticate(self, name, secret): 975 t = Thread(target=self._authenticate, 976 args=(self.get_conversation(name), secret,)) 977 t.daemon = True 978 t.start() 979 980 981class Introduction(Thread): 982 def __init__(self, peer, connection): 983 super(Introduction, self).__init__() 984 self.daemon = True 985 986 self.queue_in_data = Queue() 987 988 self.peer = peer 989 self.connection = connection 990 991 self.connection.add_manager(self) 992 993 def run(self): 994 data, _ = self.queue_in_data.get() 995 try: 996 self.handle_introduction_data(data) 997 except (errors.MalformedPacketError, 998 errors.CorruptedPacketError, 999 errors.InvalidIdentityError, 1000 errors.InvalidPublicKeyError) as e: 1001 e.title += ' caused by an unknown peer' 1002 self.peer._ui.notify_error(e) 1003 self.connection.remove_manager() 1004 1005 def handle_introduction_data(self, data): 1006 packet = packets.build_intro_packet(data) 1007 1008 for conv in self.peer.conversations: 1009 keys = conv.keys or conv.request_keys 1010 iv_hash = pyaxo.hash_( 1011 a2b(packet.iv) + self.peer.identity_keys.pub + 1012 keys.iv_hash_key) 1013 if iv_hash == a2b(packet.iv_hash): 1014 # the database does have a conversation between the 1015 # users, so the current connection must be added to the 1016 # conversation, a manager must be started and then 1017 # receive the packet using the existing conversation 1018 if not conv.is_active: 1019 conv.set_active(self.connection, Conversation.state_conv) 1020 conv.queue_in_data.put([data, self.connection]) 1021 break 1022 else: 1023 # the database does not have a conversation between the 1024 # users, so a request must be created and the UI 1025 # notified 1026 req = self.peer._process_request(data) 1027 1028 conv = req.conversation 1029 conv.start() 1030 conv.set_active(self.connection, Conversation.state_in_req) 1031 1032 contact = req.conversation.contact 1033 self.peer._inbound_requests[contact.identity] = req 1034 self.peer._ui.notify_in_request( 1035 notifications.ContactNotification( 1036 contact, 1037 title='Request received', 1038 message='{} has sent you a ' 1039 'request'.format(contact.name))) 1040 1041 self.peer._managers_conv.remove(self) 1042 1043 def notify_disconnect(self): 1044 self.peer._ui.notify(notifications.UnmessageNotification( 1045 'An unknown peer has disconnected without sending any data')) 1046 1047 1048class Conversation(object): 1049 state_in_req = 'in_req' 1050 state_out_req = 'out_req' 1051 state_conv = 'conv' 1052 1053 def __init__(self, peer, contact, 1054 request_keys=None, keys=None, axolotl=None, connection=None): 1055 self.peer = peer 1056 self.ui = ConversationUi() 1057 1058 self.contact = contact 1059 self.request_keys = request_keys 1060 self.keys = keys 1061 self.axolotl = axolotl 1062 self.auth_session = None 1063 1064 self._managers = dict() 1065 self.connection = connection 1066 self.queue_in_data = Queue() 1067 self.queue_out_data = Queue() 1068 self.queue_in_packets = Queue() 1069 self.queue_out_packets = Queue() 1070 1071 self.elements = dict() 1072 self.elements_lock = Lock() 1073 1074 self.is_active = False 1075 1076 self.thread_in_data = Thread(target=self.check_in_data) 1077 self.thread_in_data.daemon = True 1078 self.thread_out_data = Thread(target=self.check_out_data) 1079 self.thread_out_data.daemon = True 1080 self.thread_in_packets = Thread(target=self.check_in_packets) 1081 self.thread_in_packets.daemon = True 1082 self.thread_out_packets = Thread(target=self.check_out_packets) 1083 self.thread_out_packets.daemon = True 1084 1085 def start(self): 1086 self.thread_in_data.start() 1087 self.thread_out_data.start() 1088 self.thread_in_packets.start() 1089 self.thread_out_packets.start() 1090 1091 @property 1092 def is_authenticated(self): 1093 try: 1094 return self.auth_session.is_authenticated 1095 except AttributeError: 1096 # the session has not been initialized 1097 return None 1098 1099 @property 1100 def untalk_session(self): 1101 return self._get_manager(elements.UntalkElement.type_) 1102 1103 @untalk_session.setter 1104 def _untalk_session(self, manager): 1105 self._set_manager(manager, elements.UntalkElement.type_) 1106 1107 def _get_manager(self, type_): 1108 try: 1109 return self._managers[type_] 1110 except KeyError: 1111 return None 1112 1113 def _set_manager(self, manager, type_): 1114 self._managers[type_] = manager 1115 1116 def remove_manager(self, manager): 1117 manager.stop() 1118 del self._managers[manager.type_] 1119 1120 def check_in_data(self): 1121 while True: 1122 data, connection = self.queue_in_data.get() 1123 try: 1124 method = getattr(self, 'handle_{}_data'.format(self.state)) 1125 method(data, connection) 1126 except AttributeError: 1127 # the state does not have a "handle" method, which currently is 1128 # state_in_req because it should be waiting for the request to 1129 # be accepted (by the user) and meanwhile no more data should 1130 # be received from the other party 1131 # TODO maybe disconnect instead of ignoring the data 1132 pass 1133 except (errors.MalformedPacketError, 1134 errors.CorruptedPacketError) as e: 1135 e.title += ' caused by "{}"'.format(self.contact.name) 1136 self.peer._ui.notify_error(e) 1137 1138 def check_out_data(self): 1139 while True: 1140 data, callback, errback = self.queue_out_data.get() 1141 try: 1142 self.connection.send(data) 1143 except Exception as e: 1144 errback(errors.UnmessageError(title=type(e), 1145 message=e.message)) 1146 else: 1147 callback() 1148 1149 def check_in_packets(self): 1150 while True: 1151 args = self.queue_in_packets.get() 1152 self.peer._receive_packet(*args) 1153 1154 def check_out_packets(self): 1155 while True: 1156 args = self.queue_out_packets.get() 1157 self.peer._send_packet(*args) 1158 1159 def send_data(self, data, callback, errback): 1160 self.queue_out_data.put([data, callback, errback]) 1161 1162 def handle_conv_data(self, data, connection): 1163 packet = packets.build_regular_packet(data) 1164 self.queue_in_packets.put([packet, connection, self]) 1165 1166 def handle_out_req_data(self, data, connection): 1167 packet = packets.build_reply_packet(data) 1168 req = self.peer._outbound_requests[self.contact.identity] 1169 enc_handshake_key = a2b(packet.handshake_key) 1170 1171 payload_hash = keyed_hash( 1172 req.conversation.request_keys.payload_hash_key, 1173 enc_handshake_key + a2b(packet.payload)) 1174 1175 if payload_hash == a2b(packet.payload_hash): 1176 # the reply packet provides a handshake key, making it possible 1177 # to do a Triple Diffie-Hellman handshake and create an Axolotl 1178 # state 1179 try: 1180 handshake_key = pyaxo.decrypt_symmetric( 1181 req.conversation.request_keys.handshake_enc_key, 1182 enc_handshake_key) 1183 except CryptoError: 1184 e = errors.MalformedPacketError('reply') 1185 e.message += ' - decryption failed' 1186 raise e 1187 1188 self.peer._init_conv( 1189 self, 1190 priv_handshake_key=req.handshake_keys.priv, 1191 other_handshake_key=handshake_key, 1192 ratchet_keys=req.ratchet_keys) 1193 else: 1194 # TODO maybe disconnect instead of ignoring the data 1195 pass 1196 1197 def set_active(self, connection, state): 1198 connection.add_manager(self) 1199 self.connection = connection 1200 self.state = state 1201 self.is_active = True 1202 1203 def add_connection(self, connection, type_): 1204 manager = self._get_manager(type_) 1205 manager.connection = connection 1206 connection.add_manager(manager, type_) 1207 return manager 1208 1209 def close(self): 1210 for m in self._managers.values(): 1211 self.remove_manager(m) 1212 if self.connection: 1213 self.connection.remove_manager() 1214 self.connection = None 1215 self.auth_session = None 1216 self.is_active = False 1217 1218 def notify_disconnect(self): 1219 if self.is_active: 1220 self.ui.notify_disconnect( 1221 notifications.UnmessageNotification( 1222 '{} has disconnected'.format(self.contact.name))) 1223 self.connection = None 1224 self.close() 1225 1226 def init_untalk(self, connection=None, other_handshake_key=None): 1227 self._untalk_session = untalk.UntalkSession(self, other_handshake_key) 1228 if connection: 1229 self.add_connection(connection, elements.UntalkElement.type_) 1230 return self.untalk_session 1231 1232 def start_untalk(self, other_handshake_key=None): 1233 self.untalk_session.start(other_handshake_key) 1234 1235 def stop_untalk(self): 1236 self.remove_manager(self.untalk_session) 1237 1238 def init_auth(self, buffer_=None): 1239 self.auth_session = AuthSession(buffer_) 1240 return self.auth_session 1241 1242 1243class ConversationKeys: 1244 handshake_enc_salt = b'\x00' 1245 1246 iv_hash_salt = b'\x01' 1247 payload_hash_salt = b'\x02' 1248 1249 auth_secret_salt = b'\x03' 1250 1251 def __init__(self, key): 1252 self.key = key 1253 1254 self.handshake_enc_key = pyaxo.kdf(key, self.handshake_enc_salt) 1255 1256 self.iv_hash_key = pyaxo.kdf(key, self.iv_hash_salt) 1257 self.payload_hash_key = pyaxo.kdf(key, self.payload_hash_salt) 1258 1259 self.auth_secret_key = pyaxo.kdf(key, self.auth_secret_salt) 1260 1261 1262class AuthSession: 1263 def __init__(self, buffer_=None): 1264 self.smp = None 1265 self.buffer_ = buffer_ 1266 if self.buffer_: 1267 # start from step 2 as the initial buffer was received from the 1268 # other party, who started the session 1269 self.step = 2 1270 else: 1271 # start from step 1 as the initial buffer still has to be sent to 1272 # the other party, who will advance the session 1273 self.step = 1 1274 1275 @property 1276 def is_authenticated(self): 1277 if self.step > 5: 1278 # the session is complete 1279 return self.smp.match 1280 else: 1281 # the session has not started or is incomplete 1282 return None 1283 1284 @property 1285 def is_waiting(self): 1286 # the session is waiting for the other party to initialize theirs by 1287 # performing step 2 1288 return self.step == 3 1289 1290 def start(self, secret): 1291 self.smp = SMP(secret) 1292 return self.advance(self.buffer_) 1293 1294 def advance(self, buffer_): 1295 if self.step == 1: 1296 next_buffer = self.smp.step1() 1297 else: 1298 step_method = getattr(self.smp, 'step' + str(self.step)) 1299 next_buffer = step_method(a2b(buffer_)) 1300 # skip the next step because it will be performed by the other party 1301 self.step += 2 1302 try: 1303 return b2a(next_buffer) + '\n' 1304 except TypeError: 1305 return None 1306 1307 1308class ElementParser: 1309 def __init__(self, peer): 1310 self.peer = peer 1311 1312 def _parse_untalk_element(self, element, conversation, connection=None): 1313 message = None 1314 if conversation.untalk_session: 1315 if element.sender == self.peer.name: 1316 if (conversation.untalk_session.state == 1317 untalk.UntalkSession.state_sent): 1318 message = 'voice conversation request sent to {}' 1319 else: 1320 # this peer has accepted the request 1321 conversation.start_untalk() 1322 elif (conversation.untalk_session.state == 1323 untalk.UntalkSession.state_sent): 1324 # the other peer has accepted the request 1325 conversation.start_untalk( 1326 other_handshake_key=a2b(str(element))) 1327 elif element.receiver == self.peer.name: 1328 message = '{} wishes to start a voice conversation' 1329 conversation.init_untalk(connection, 1330 other_handshake_key=a2b(str(element))) 1331 1332 if message: 1333 conversation.ui.notify( 1334 notifications.UntalkNotification( 1335 message.format(conversation.contact.name))) 1336 1337 def _parse_pres_element(self, element, conversation, connection=None): 1338 if str(element) == PresenceElement.status_online: 1339 conversation.ui.notify_online( 1340 notifications.UnmessageNotification( 1341 '{} is online'.format(conversation.contact.name))) 1342 elif str(element) == PresenceElement.status_offline: 1343 if element.sender == self.peer.name: 1344 # remove the name from the list of pending presence packets and 1345 # set the event if it was the last one 1346 self.peer._presence_convs.remove(element.receiver) 1347 if not self.peer._presence_convs: 1348 self.peer._presence_event.set() 1349 else: 1350 conversation.close() 1351 conversation.ui.notify_offline( 1352 notifications.UnmessageNotification( 1353 '{} is offline'.format(conversation.contact.name))) 1354 1355 def _parse_msg_element(self, element, conversation, connection=None): 1356 conversation.ui.notify_message( 1357 notifications.ElementNotification(element)) 1358 1359 def _parse_auth_element(self, element, conversation, connection=None): 1360 if element.sender == self.peer.name: 1361 if conversation.auth_session.is_waiting: 1362 conversation.ui.notify_out_authentication( 1363 notifications.UnmessageNotification( 1364 title='Authentication started', 1365 message='Waiting for {} to advance'.format( 1366 conversation.contact.name))) 1367 else: 1368 buffer_ = str(element) 1369 try: 1370 next_buffer = conversation.auth_session.advance(buffer_) 1371 except AttributeError: 1372 conversation.init_auth(buffer_) 1373 conversation.ui.notify_in_authentication( 1374 notifications.UnmessageNotification( 1375 title='Authentication started', 1376 message='{} wishes to authenticate '.format( 1377 conversation.contact.name))) 1378 else: 1379 if next_buffer: 1380 self.peer._send_element(conversation, 1381 type_=AuthenticationElement.type_, 1382 content=next_buffer) 1383 if conversation.is_authenticated is None: 1384 # the authentication is not complete as buffers are still being 1385 # exchanged 1386 pass 1387 else: 1388 if conversation.is_authenticated: 1389 title = 'Authentication successful' 1390 message = 'Your conversation with {} is authenticated!' 1391 else: 1392 title = 'Authentication failed' 1393 message = 'Your conversation with {} is NOT authenticated!' 1394 conversation.ui.notify_finished_authentication( 1395 notifications.UnmessageNotification( 1396 title=title, 1397 message=message.format( 1398 conversation.contact.name))) 1399 1400 def parse(self, element, conversation, connection=None): 1401 if element.is_complete: 1402 # it can be parsed as all parts have been added to the ``Element`` 1403 # or it is composed of a single part 1404 try: 1405 method = getattr(self, 1406 '_parse_{}_element'.format(element.type_)) 1407 except AttributeError: 1408 # TODO handle elements with unknown types 1409 pass 1410 else: 1411 method(element, conversation, connection) 1412 else: 1413 # the ``Element`` has parts yet to be transmitted (sent/received) 1414 pass 1415 1416 1417class _ConversationFactory(Factory): 1418 def __init__(self, peer, connection_made): 1419 self.peer = peer 1420 self.connection_made = connection_made 1421 1422 def buildProtocol(self, addr): 1423 return _ConversationProtocol(self, self.connection_made) 1424 1425 def notify_error(self, error): 1426 self.peer._ui.notify_error(error) 1427 1428 1429class _ConversationProtocol(NetstringReceiver): 1430 type_regular = 'reg' 1431 type_untalk = elements.UntalkElement.type_ 1432 1433 def __init__(self, factory, connection_made): 1434 self.factory = factory 1435 self.connection_made = connection_made 1436 self.manager = None 1437 self.type_ = None 1438 1439 def connectionMade(self): 1440 self.connection_made(self) 1441 1442 def add_manager(self, manager, type_=None): 1443 self.manager = manager 1444 self.type_ = type_ or _ConversationProtocol.type_regular 1445 1446 def remove_manager(self): 1447 self.manager = None 1448 self.transport.loseConnection() 1449 1450 def connectionLost(self, reason): 1451 if self.manager: 1452 # the other party disconnected cleanly without sending a presence 1453 # element or the connection was actually lost 1454 # TODO check the different reasons and act accordingly? 1455 # TODO consider a connection that never had a manager? 1456 self.manager.notify_disconnect() 1457 1458 def stringReceived(self, string): 1459 try: 1460 if self.type_ == _ConversationProtocol.type_regular: 1461 self.manager.queue_in_data.put([string, self]) 1462 elif self.type_ == _ConversationProtocol.type_untalk: 1463 self.manager.receive_data(string) 1464 else: 1465 self.factory.notify_error(errors.UnmessageError( 1466 title='Connection of unknown type', 1467 message=str(self.type_))) 1468 except AttributeError: 1469 self.factory.notify_error( 1470 errors.TransportError( 1471 message='Packet received without a manager')) 1472 1473 def send(self, string): 1474 self.sendString(string) 1475 1476 1477class PeerInfo: 1478 def __init__(self, name=None, port_local_server=None, identity_keys=None, 1479 onion_service_key=None, contacts=None): 1480 self.name = name 1481 self.port_local_server = port_local_server 1482 self.identity_keys = identity_keys 1483 self.onion_service_key = onion_service_key 1484 self.contacts = contacts or dict() 1485 1486 1487class Persistence: 1488 def __init__(self, dbname, dbpassphrase): 1489 self.dbname = dbname 1490 self.dbpassphrase = dbpassphrase 1491 self.db = self._open_db() 1492 1493 def _open_db(self): 1494 db = sqlite3.connect(':memory:', check_same_thread=False) 1495 db.row_factory = sqlite3.Row 1496 1497 with db: 1498 try: 1499 with open(self.dbname, 'r') as f: 1500 sql = f.read() 1501 db.cursor().executescript(sql) 1502 except IOError as e: 1503 if e.errno == errno.ENOENT: 1504 self._create_db(db) 1505 else: 1506 raise 1507 return db 1508 1509 def _create_db(self, db): 1510 db.execute(''' 1511 CREATE TABLE IF NOT EXISTS 1512 peer ( 1513 name TEXT, 1514 port_local_server INTEGER, 1515 priv_identity_key TEXT, 1516 pub_identity_key TEXT, 1517 onion_service_key TEXT)''') 1518 db.execute(''' 1519 CREATE UNIQUE INDEX IF NOT EXISTS 1520 peer_name 1521 ON 1522 peer (name)''') 1523 db.execute(''' 1524 CREATE TABLE IF NOT EXISTS 1525 contacts ( 1526 identity TEXT, 1527 key TEXT, 1528 is_verified INTEGER, 1529 has_presence INTEGER)''') 1530 db.execute(''' 1531 CREATE UNIQUE INDEX IF NOT EXISTS 1532 contact_identity 1533 ON 1534 contacts (identity)''') 1535 1536 def _write_db(self): 1537 with self.db as db: 1538 sql = bytes('\n'.join(db.iterdump())) 1539 with open(self.dbname, 'w') as f: 1540 f.write(sql) 1541 1542 def load_peer_info(self): 1543 with self.db as db: 1544 cur = db.cursor() 1545 cur.execute(''' 1546 SELECT 1547 * 1548 FROM 1549 peer''') 1550 row = cur.fetchone() 1551 if row: 1552 onion_service_key = row['onion_service_key'] 1553 identity_keys = Keypair(a2b(row['priv_identity_key']), 1554 a2b(row['pub_identity_key'])) 1555 port_local_server = int(row['port_local_server']) 1556 name = str(row['name']) 1557 else: 1558 onion_service_key = None 1559 identity_keys = None 1560 port_local_server = None 1561 name = None 1562 1563 with self.db as db: 1564 rows = db.execute(''' 1565 SELECT 1566 * 1567 FROM 1568 contacts''') 1569 contacts = dict() 1570 for row in rows: 1571 c = Contact(str(row['identity']), 1572 a2b(row['key']), 1573 bool(row['is_verified']), 1574 bool(row['has_presence'])) 1575 contacts[c.name] = c 1576 1577 return PeerInfo(name, port_local_server, identity_keys, 1578 onion_service_key, contacts) 1579 1580 def save_peer_info(self, peer_info): 1581 with self.db as db: 1582 db.execute(''' 1583 DELETE FROM 1584 peer''') 1585 if peer_info.identity_keys: 1586 db.execute(''' 1587 INSERT INTO 1588 peer ( 1589 name, 1590 port_local_server, 1591 priv_identity_key, 1592 pub_identity_key, 1593 onion_service_key) 1594 VALUES (?, ?, ?, ?, ?)''', ( 1595 peer_info.name, 1596 peer_info.port_local_server, 1597 b2a(peer_info.identity_keys.priv), 1598 b2a(peer_info.identity_keys.pub), 1599 peer_info.onion_service_key)) 1600 db.execute(''' 1601 DELETE FROM 1602 contacts''') 1603 for c in peer_info.contacts.values(): 1604 db.execute(''' 1605 INSERT INTO 1606 contacts ( 1607 identity, 1608 key, 1609 is_verified, 1610 has_presence) 1611 VALUES (?, ?, ?, ?)''', ( 1612 c.identity, 1613 b2a(c.key), 1614 int(c.is_verified), 1615 int(c.has_presence))) 1616 1617 self._write_db() 1618 1619 1620def keyed_hash(key, data): 1621 return hmac.new(key, data, sha256).digest() 1622