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