Ejemplo n.º 1
0
    def __init__(self, root_window, conf: ConfigParser):
        self._logger = logging.getLogger(__name__)
        self._logger.info("Starting curses CLI")

        # Global state init
        self._quit = False
        self._active_menu = _Menu("Empty menu", [])

        # Persistence
        try:
            db_file_path = conf.get("persistence", "db_file_path")
            self._db = SqliteStorage(db_file_path)
        except (NoSectionError, NoOptionError):
            self._logger.info("No configuration found for db file. Using default.")
            self._db = SqliteStorage()
        self._db.connect_thread()

        # Crypto init
        self._crypto_provider = ProviderNaCl()

        # Communication
        self._adapter = TcpUdpAdapter(self._crypto_provider)
        self._is_listening_passive = False
        self._is_listening_active = False

        # Root window init
        self._stdscr = root_window
        self._stdscr.clear()

        # Status init
        max_y, max_x = self._stdscr.getmaxyx()
        self._statusscr = self._stdscr.derwin(3, 45, 0, max_x - 45)
        self._status = "Inactive"
Ejemplo n.º 2
0
    def test_symmetric_encryption_and_decryption(self):
        provider = ProviderNaCl()

        test_string = """\
        an example string to encrypt
        including line breaks, etc

        also: exotic characters!
        åäö
        ㅏ매아내ㅐ
        """

        shared_secret = "a-secret-well-kept"

#        print("String before encryption")
#        print(test_string)

        encrypted_string = provider.encrypt_symmetric(test_string,
                                                      shared_secret)
#        print("String after encryption")
#        print(encrypted_string)

        decrypted_string = provider.decrypt_symmetric(encrypted_string,
                                                      shared_secret)
#        print("String after decryption")
#        print(decrypted_string)

        self.assertEqual(test_string, decrypted_string)
Ejemplo n.º 3
0
    def test_key_searialization(self):
        provider = ProviderNaCl()
        key = public.PrivateKey.generate()

        str_key = ProviderNaCl._serialize_private_key(key)
#        print("Stringified key is:", str_key)

        reconstituted_key = ProviderNaCl._deserialize_private_key(str_key)

        self.assertEqual(key, reconstituted_key)
Ejemplo n.º 4
0
    def test_asymmetric_encryption_and_decryption(self):
        # Test case specific setUp
        provider = ProviderNaCl()
        private_key_str = provider.generate_key()

        ## Trixy bit - Normally we have a serialized public key available (saved after
        ## rendezvoux) but here we have to retrieve one
        private_key = ProviderNaCl._deserialize_private_key(private_key_str)
        public_key = private_key.public_key
        public_key_str = ProviderNaCl._serialize_public_key(public_key)

        test_string = """\
        an example string to encrypt
        including line breaks, etc

        also: exotic characters!
        åäö
        ㅏ매아내ㅐ
        """

#        print("String before encryption")
#        print(test_string)

        encrypted_string = provider.encrypt(test_string, public_key_str)

#        print("String after encryption")
#        print(encrypted_string)
        decrypted_string = provider.decrypt(encrypted_string, private_key_str)
#        print("String after decryption")
#        print(decrypted_string)

        self.assertEqual(test_string, decrypted_string)
Ejemplo n.º 5
0
def _enqueue_generic_message(db: SqliteStorage, crypto: ProviderNaCl,
                             to_address: contact.Address, msg: EmailMessage) -> None:
    encrypted_contents = crypto.encrypt(msg.as_bytes(), to_address.public_key)

    message = UserMessage(
        to_address=to_address.address_id,
        send_time=datetime.datetime.now(),
        contents=encrypted_contents)

    db.store_message(message)
    db.mark_contact_address(to_address.address_id)  # Make sure not to re-use addresses
Ejemplo n.º 6
0
    def __init__(self, conf: ConfigParser):
        self._logger = logging.getLogger(__name__)

        # Set up persistence
        try:
            db_file_path = conf.get("persistence", "db_file_path")
            self._db = SqliteStorage(db_file_path)
        except (NoSectionError, NoOptionError):
            self._logger.info("No configuration found for db file. "
                              "Using default.")
            self._db = SqliteStorage()
        self._db.connect_thread()

        # Set up encryption
        self._crypto_provider = ProviderNaCl()

        # Set up networking
        self._adapter = TcpUdpAdapter(self._crypto_provider)
Ejemplo n.º 7
0
def parse_message(db: SqliteStorage, crypto: ProviderNaCl, message: StoredMessage):
    """
    This function will take a message and return the cleartext contents
    of the message as well as any protocol attachments contained in the
    message: Address pads and requests for pads
    """
    key_id, private_key = db.get_own_address_nacl_key(message.header_address)
    if not private_key:
        # We are trying to parse a message for which we have no key.
        # This is never going to work out well, better to exit early.
        return (None, None, None)

    cleartext = crypto.decrypt(message.contents, private_key)

    # Cleartext is supposed to be a MIME formatted message
    msg = BytesParser(policy=policy.default).parsebytes(cleartext)
    content = []
    address_pad = None
    address_pad_req = None

    for part in msg.walk():
        # Account for stuff we know will turn up - Specifically wrappers and protocol
        # control messages.
        # Please note that we do not currently support multiple address pads / requests in the
        # same message.
        if part.get_content_type() == 'application/json':
            if part['Content-Description'] == NodeIntercom.address_pad_request_description:
                address_pad_req = NodeIntercom.AddressPadRequest.deserialize(part.get_content())
            if part['Content-Description'] == NodeIntercom.address_pad_description:
                address_pad = NodeIntercom.AddressPad.deserialize(part.get_content())
        elif (part.get_content_maintype() == 'multipart' or
              part.get_content_maintype() == 'application'):
            continue
        else:
            content.append(part.get_content())

    msg_string = "From: {0}\nTo: {1}\n\n{2}".format(msg['from'], msg['to'], "\n".join(content))
    return (msg_string, address_pad_req, address_pad)
Ejemplo n.º 8
0
class CursesInterface:
    """This class encapsulates the different functions that the curses UI exposes"""

    @staticmethod
    def main_screen_turn_on(conf: ConfigParser):
        """
        If we want a a root screen to run on that happens to be the actual
        curses default screen, this is what we call. Naturally, there is only
        one curses root screen so multiple calls of this is a bad idea.
        """
        curses.wrapper(lambda scr: CursesInterface(scr, conf).run())


    def __init__(self, root_window, conf: ConfigParser):
        self._logger = logging.getLogger(__name__)
        self._logger.info("Starting curses CLI")

        # Global state init
        self._quit = False
        self._active_menu = _Menu("Empty menu", [])

        # Persistence
        try:
            db_file_path = conf.get("persistence", "db_file_path")
            self._db = SqliteStorage(db_file_path)
        except (NoSectionError, NoOptionError):
            self._logger.info("No configuration found for db file. Using default.")
            self._db = SqliteStorage()
        self._db.connect_thread()

        # Crypto init
        self._crypto_provider = ProviderNaCl()

        # Communication
        self._adapter = TcpUdpAdapter(self._crypto_provider)
        self._is_listening_passive = False
        self._is_listening_active = False

        # Root window init
        self._stdscr = root_window
        self._stdscr.clear()

        # Status init
        max_y, max_x = self._stdscr.getmaxyx()
        self._statusscr = self._stdscr.derwin(3, 45, 0, max_x - 45)
        self._status = "Inactive"


    def run(self):
        """
        This method will start the CLI (duh). Please notice that the CLI manages
        several threads, depending on what commands have been issued to it.
        """
        menu = self._get_root_menu()
        self._active_menu = menu

        self._redraw()

        while not self._quit:
            choice = self._stdscr.getkey()

            for item in self._active_menu.items:
                if item.key == choice:
                    item.command()

            self._redraw()

        if self._is_listening_passive:
            self._adapter.stop_register_announcements()

        # And if we happen to have any stray threads, they'll pop out here.
        self._logger.debug(str(threading.enumerate()))


    def _redraw(self):
        """
        Will cause the application to redraw itself to show all new state
        """
        self._stdscr.clear()
        self._draw_menu(self._stdscr, self._active_menu)
        self._display_state()
        self._stdscr.refresh()


    def _display_state(self):
        """
        This will refresh the status window, enabling the user to see what is
        going on in the application. Please note that the results will not be visible
        until containing window has been refreshed
        """
        self._statusscr.clear()
        self._statusscr.border()

        self._statusscr.addstr(1, 3, "Status: ")
        self._statusscr.addstr(self._status, curses.A_BOLD)


    def _draw_menu(self, scr, menu: _Menu, line: int=1):
        """
        This function will draw a meny in the given screen/window. If the menu
        is higher than the height of the window, bad stuff will happen.
        The menu will be drawn beginning at (1, 1) and extend one row per menu item
        plus one for the menu heading.
        """
        scr.addstr(line, 1, menu.title, curses.A_BOLD)

        for item in menu.items:
            line = line + 1
            scr.addstr(line, 3, item.key)
            scr.addstr(" - ")
            scr.addstr(item.text)


    def _draw_menu_horizontal(self, scr, menu: _Menu, line: int=1):
        """
        This function will draw a meny in the given screen/window. If the menu
        is wider than the width of the window, bad stuff will happen.
        The menu will be drawn beginning at (line, 1) and extend along that line
        """
        scr.addstr(line, 1, "{0}: ".format(menu.title), curses.A_BOLD)

        for item in menu.items:
            scr.addstr("{0}".format(item.text))
            scr.addstr("[{0}] ".format(item.key), curses.A_BOLD)


    def _show_text_dialog(self, prompt: str) -> str:
        """
        Shows a text input dialog window. Return value is a string that the user typed.
        """
        max_y, max_x = self._stdscr.getmaxyx()
        dialogscr = self._stdscr.derwin(3, 80, int(max_y / 2), int(max_x / 2) - 40)
        dialogscr.clear()
        dialogscr.border()

        dialogscr.addstr(1, 3, prompt)
        dialogscr.addstr(": ")

        dialogscr.refresh()

        curses.echo()
        result = dialogscr.getstr().decode("utf-8")
        curses.noecho()

        return result


    def _show_integer_dialog(self, prompt: str) -> int:
        result = None
        while type(result) is not int:
            text = self._show_text_dialog("{} (must be an integer)".format(prompt))
            try:
                result = int(text)
            except:
                pass

        return result


    def _show_selection_dialog(self, menu: _Menu, vertical: bool = True, text: str = None) -> object:
        """
        Shows a dialog window with a menu selection. Returns an object that the user selected.
        The text argument is shown above the menu - Used for instructions or content on
        which to select an action.

        PLEASE NOTE! The _Menu sent as argument must have commands attached to them that
        return the correct value for that menu item. This will usually mean that the command
        needs to be created over some closure.
        """
        max_y, max_x = self._stdscr.getmaxyx()

        top_padding = 1                       # Initial empty line looks good
        if not text:
            text_lines = []
            text_height = 0
        else:
            text_lines = text.splitlines()
            text_height = 1 + len(text_lines) # Allow for bottom padding of text

        if vertical:
            menu_height = 2 + len(menu.items) # One for title, one per item and one for padding
        else:
            menu_height = 2                   # One for menu, one for padding

        dialogscr_height = top_padding + text_height + menu_height

        dialogscr = self._stdscr.derwin(dialogscr_height, 80, int(max_y / 2), int(max_x / 2) - 40)
        dialogscr.clear()
        dialogscr.border()

        i = 1
        for line in text_lines:
            dialogscr.addstr(i, 3, line)
            i = i + 1

        # If we printed some text, we insert some padding between it and the menu
        if text_height > 0:
            i = i + 1

        if vertical:
            self._draw_menu(dialogscr, menu, i)
        else:
            self._draw_menu_horizontal(dialogscr, menu, i)

        dialogscr.refresh()

        result = None
        while not result:
            choice = self._stdscr.getkey()

            for item in menu.items:
                if item.key == choice:
                    result = item.command()

        return result


    def _show_confirmation_dialog(self, text: str) -> bool:
        """
        This method will present the user with a yes/no choice dialog window. If the
        user selects "yes", True will be returned, if the user selects "no", False will
        be returned.
        """
        max_y, max_x = self._stdscr.getmaxyx()

        text_lines = text.splitlines()
        dialogscr_height = 4 + len(text_lines)

        dialogscr = self._stdscr.derwin(dialogscr_height, 80, int(max_y / 2), int(max_x / 2) - 40)
        dialogscr.clear()
        dialogscr.border()

        i = 1
        for line in text_lines:
            dialogscr.addstr(i, 3, line)
            i = i + 1

        dialogscr.addstr(dialogscr_height - 2, 3, "[Y]es or [N]o")

        dialogscr.refresh()

        choice = None
        result = None
        while not choice:
            choice = self._stdscr.getkey()

            if choice.lower() == "y":
                result = True
            elif choice.lower() == "n":
                result = False
            else:
                choice = None

        return result


    def _show_information_dialog(self, text: str):
        """
        This method will present the user with a yes/no choice an informational dialog window.
        When the user presses any button, the dialog will be done.
        """
        max_y, max_x = self._stdscr.getmaxyx()

        text_lines = text.splitlines()
        dialogscr_height = 4 + len(text_lines)

        self._logger.debug("Text to display is: " + text)

        dialogscr = self._stdscr.derwin(dialogscr_height, 80, int(max_y / 2), int(max_x / 2) - 40)
        dialogscr.clear()
        dialogscr.border()

        i = 1
        for line in text_lines:
            dialogscr.addstr(i, 3, line)
            i = i + 1

        dialogscr.addstr(dialogscr_height - 2, 3, "Press [ANY] key to continue")

        dialogscr.refresh()

        self._stdscr.getkey()
        return


    def _command_failed(self, message):
        self._status = message
        self._active_menu = self._get_root_menu()
        self._redraw()


    ################################################################################
    #
    # Menus - Selection lists available without activating any specific command
    #
    ################################################################################

    def _get_root_menu(self) -> (str, list):
        """
        Returns the root menu for the "default" state of the application
        """
        items = []
        items.append(_MenuItem("1", "Generate new key", self.generate_key))
        items.append(_MenuItem("2", "Perform rendezvous", self.rendezvous))
        items.append(_MenuItem("3", "List contacts", self.list_contacts))
        items.append(_MenuItem("4", "Enqueue message", self.enqueue_message))
        items.append(_MenuItem("5", "Exchange messages", self.exchange_messages))
        items.append(_MenuItem("6", "Received messages", self.received_messages))
        items.append(_MenuItem("q", "Quit application", self.quit))

        return _Menu("Root menu:", items)


    ################################################################################
    #
    # Commands - Internal parts of these should eventually be generalized across
    #            different UI implementations
    #
    ################################################################################

    def reset(self):
        """
        Resets the status and root menu. For use when returning from sub-menus.
        """
        self._status = "Inactive"
        self._active_menu = self._get_root_menu()
        self._redraw()


    def quit(self):
        """
        Called in order to quit the application.
        """
        self._quit = True


    def generate_key(self):
        """
        This command initiates generation of a new private/public key pair for
        receiving messages. Generally speaking, only one key is necessary, but
        in order to avoid certain attacks new keys should be generated fairly often.
        """
        self._status = "Generating key"
        self._active_menu = _Menu("Operation in progress...", [])
        self._redraw()

        self._generate_new_key()

        self._status = "Key saved"
        self._active_menu = self._get_root_menu()
        self._redraw()


    def _generate_new_key(self) -> (str, str):
        secret_key, public_key = self._crypto_provider.generate_key()
        key_id = self._db.store_own_nacl_key(secret_key, public_key)
        return (key_id, public_key)


    def rendezvous(self):
        """
        Issuing this command will prompt for a shared secret - Once this has been
        suplied, the user is again prompted for when to start the rendezvous. When
        the user so indicates (on press of "Enter") the rendezvous sequence will
        start.
        TODO: On successful rendezvous, the user should be prompted for an alias for
        the new contact. Currently, the alias used is always what the foreign node
        gives which might not be what the user wants
        """
        # Set status
        self._status = "Rendezvous setup"
        self._active_menu = _Menu("Operation in progress...", [])
        self._redraw()

        # Get alias to send and secret shared with rendezvous partner
        alias = self._show_text_dialog("Alias to send")
        secret = self._show_text_dialog("Shared secret")

        info_success, key_id, rendezvous_info = self._rendezvous_produce_info(alias)

        # Use naive adapter to initiate rendezvous
        self._status = "Rendezvous started"
        self._redraw()

        success, foreign_info = self._adapter.rendezvous(secret, rendezvous_info)

        if success:
            self._status = "Rendezvous successful"
            self._rendezvous_save_info(key_id, rendezvous_info, foreign_info)
        else:
            self._status = "Rendezvous failed"

        self._active_menu = self._get_root_menu()
        self._redraw()


    def _rendezvous_produce_info(self, alias: str) -> (bool, str, RendezvousInfo):
        """
        This is a helper commad used to produce a rendezvous info object for sending
        to a foreign node.
        Return value is the ID of the key used to produce the rendezvous info as
        well as the info object itself.
        """
        # Retrieve a suitable public key to send
        (key_id, public_key) = self._db.get_least_used_own_nacl_key()

        # Produce address pad - We start with a block of 10 addresses
        address_pad = []
        for _ in range(10):
            adr = "ADR-" + str(uuid.uuid4())
            address_pad.append(adr)
        rendezvous_info = RendezvousInfo(alias=alias, address_pad=address_pad,
                                         public_key=public_key)

        return (True, key_id, rendezvous_info)


    def _rendezvous_save_info(self, key_id: str,
                              own_info: RendezvousInfo,
                              foreign_info: RendezvousInfo) -> bool:
        """
        This helper function will save rendezvous info on success - Returns a success
        report: True if successful, False otherwise.
        """
        # Only one key is to be imported at the moment. Not ideal, but
        # makes key usage a bit lower, saving some electrons for future generations
        contact_id = self._db.store_contact(foreign_info.alias, own_info.alias)
        contact_key_id = self._db.store_contact_nacl_key(foreign_info.public_key)

        for adr in foreign_info.address_pad:
            self._db.store_contact_address(contact_id, adr, contact_key_id)

        # Save the addresses that was sent to counterpart for future use
        for adr in own_info.address_pad:
            self._db.store_own_address(adr, contact_id, key_id)

        return True


    def list_contacts(self):
        """
        This command will list all available contacts under their local nickname.
        """
        self._status = "Listing contacts"
        contacts = self._db.get_contacts()

        menu_items = []
        i = 1
        for contact in contacts:
            menu_items.append(_MenuItem(str(i), contact.nickname,
                                        lambda c=contact: self._show_single_contact(c)))
            i = i + 1
        menu_items.append(_MenuItem("q", "Back to root menu", self.reset))

        self._active_menu = _Menu("Contacts: ", menu_items)
        self._redraw()


    def _show_single_contact(self, to_show: contact.Contact) -> None:
        self._redraw()

        def _no_action():
            return True

        menu_items = []
        menu_items.append(_MenuItem("s", "Send address pad to {}".format(to_show.nickname),
                                    lambda: to_show))
        menu_items.append(_MenuItem("q", "Quit",
                                    lambda: _no_action))
        action_menu = _Menu("Action on {}".format(to_show.nickname), menu_items)

        choice = self._show_selection_dialog(action_menu)
        if choice == True:
            return
        else:
            self._show_address_pad_send_sequence(to_show)


    def _show_address_pad_send_sequence(self, destination: contact.Contact) -> None:
        myself = contact.Contact() # May look odd: This is just a placeholder for the special case
        myself.nickname = "Myself"

        address_owner = self._show_address_owner_selection(myself)
        if address_owner == None:
            return
        if address_owner != myself:
            amount = self._show_address_amount_selection(address_owner)
        else:
            amount = self._show_address_amount_selection()

        # Be nice and let the user confirm or abort
        confirm_choices_message = "Is this ok?\nSend addresses to: {0}\nAddress owner: {1}\n" \
          "Number of addresses: {2}\n" \
          .format(destination.nickname, address_owner.nickname, amount)

        if not self._show_confirmation_dialog(confirm_choices_message):
            self._redraw()
            self._show_information_dialog("Aborting.")
            return
        self._redraw()

        # Special case: If the choice is to send local addresses, we'll need to generate new ones
        if address_owner == myself:
            msg = self._generate_own_address_pad_mime_message(destination, amount)
        # Else we'll need to collect some addresses and mark them as used before wrapping them up
        else:
            msg = self._generate_foreign_address_pad_mime_message(destination, address_owner, amount)

        address_data = self._db.get_address_pad_nacl(destination.contact_id)[0]
        self._enqueue_generic_message(address_data, msg)

        self._show_information_dialog("Message enqueued.")
        self._redraw()


    def _show_address_owner_selection(self, default: contact.Contact) -> contact.Contact:
        self._redraw()

        def _no_action():
            return True

        def _default():
            return default

        menu_items = []
        # Ugly special case. Yuck.
        menu_items.append(_MenuItem("m", "Myself", _default))
        i = 1
        contacts = self._db.get_contacts()
        for contact in contacts:
            menu_items.append(_MenuItem(str(i), contact.nickname, lambda c=contact: c))
            i += 1
        menu_items.append(_MenuItem("q", "Quit", lambda: _no_action))
        action_menu = _Menu("Send addresses belonging to", menu_items)

        choice = self._show_selection_dialog(action_menu)
        if choice == True:
            return None
        else:
            return choice


    def _show_address_amount_selection(self, address_owner: contact.Contact=None) -> int:
        self._redraw()

        if address_owner != None:
            available_addresses = len(self._db.get_address_pad_nacl(address_owner.contact_id))
            max_amount = min(available_addresses, 100)
        else:
            max_amount = 100

        amount = None
        while amount == None or max_amount < amount:
            amount = self._show_integer_dialog( \
                "Select number of addresses to include - Max {}".format(max_amount))
        return amount


    def enqueue_message(self):
        """
        This command will enqueue a message for exchange at the next convenient
        occasion. It will prompt the user for a contact to which to send the
        message and once one such has been chosen the user will be prompted for a
        file to send as message contents.
        In this version, only plain text files will be tested.
        """
        destination = self._select_destination()

        # If no destination is selected (bool value returned), we abort back to the main menu.
        if destination == True:
            return

        file_name = self._show_text_dialog("File to enqueue as a message")
        self._redraw()

        # Check that the file name given is a valid file
        if not os.path.isfile(file_name):
            self._show_information_dialog("File not found: {0}. Aborting.".format(file_name))
            self._redraw()
            return

        # Possibly ask if we should append request for more addresses
        remaining_address_count = self._db.get_unused_address_count(destination.contact_id)

        append_address_pad_message = "Should a request for more addresses be appended?\n" \
          "You currently have {0} unused addresses remaining.".format(remaining_address_count)
        is_sending_address_pad_req = self._show_confirmation_dialog(append_address_pad_message)
        self._redraw()

        # Check if we got everything right
        confirm_send_message = "Is this ok?\nTo: {0}\nFile name: {1}\nRequest address pad: {2}\n" \
          "".format(destination.nickname, file_name, is_sending_address_pad_req)
        self._redraw()

        if not self._show_confirmation_dialog(confirm_send_message):
            self._redraw()
            self._show_information_dialog("Aborting.")
            return
        self._redraw()

        # Finally set to do stuff!
        content_file = open(file_name)

        address_data = self._db.get_address_pad_nacl(destination.contact_id)[0]
        file_contents = content_file.read()

        # Cram stuff into a MIME message
        protocol_parts = []

        if is_sending_address_pad_req:
            protocol_parts.append(NodeIntercom.generate_address_pad_request(20))
        msg = _MIME.construct_top_level_mime_message(destination.nickname, destination.alias,
                                                     file_contents, protocol_parts)

        # Last step: Encrypt and plonk on the "out" tray
        self._enqueue_generic_message(address_data, msg)


    def _enqueue_generic_message(self, to_address: contact.Address, msg: EmailMessage) -> None:
        encrypted_contents = self._crypto_provider.encrypt(msg.as_bytes(), to_address.public_key)

        message = UserMessage(
            to_address=to_address.address_id,
            send_time=datetime.datetime.now(),
            contents=encrypted_contents)

        self._db.store_message(message)
        self._db.mark_contact_address(to_address.address_id) # Make sure not to re-use addresses



    def _select_destination(self):
        """
        Returns a contact of the users selection OR None if the user elects to skip this
        stage.
        """
        def _no_action():
            return True

        menu_items = []
        i = 1
        for contact in self._db.get_contacts():
            menu_items.append(_MenuItem(str(i), contact.nickname, lambda: contact))
            i = i + 1
        menu_items.append(_MenuItem("q", "Quit and go back to main menu", _no_action))

        destination_menu = _Menu("Select destination: ", menu_items)
        result = self._show_selection_dialog(destination_menu)
        self._redraw()

        return result


    def export_message(self):
        """
        This command will export the contents of a given message to a file. It will
        prompt the user for a message ID - Or at least the six initial characters of the
        message ID. If they are unique, the identified message will be decrypted
        and displayed.
        In this version, only plain text files will be tested.
        """
        pass


    def exchange_messages(self):
        """
        This command will try a generic exchange sequence consisting of a "dual
        attempt" with both passive and active message exchange methods. It'll
        run in active mode for 30 seconds after which it will stop.
        Concurrently, it'll start listening to other servers, which will continue
        until user interruption.
        """

        # Announce our presence and be prepared to respond to connection attempts
        self._status = "Exchanging messages"
        self._active_menu = _Menu("Operation in progress...", [])
        self._redraw()
        self._adapter.start_message_exchange_server(self._get_messages_to_send,
                                                    self._on_message_received)
        self._is_listening_active = True

        # Passively listen
        self._status = "Exchanging messages"
        self._active_menu = _Menu("Operation in progress...", [])
        self._redraw()
        self._adapter.start_register_announcements(self._on_server_announcement)
        self._is_listening_passive = True

        # And quit
        self._show_information_dialog("Press any key to quit message exchange")
        self._active_menu = _Menu("Aborting...", [])
        self._adapter.stop_register_announcements()
        self._adapter.stop_message_exchange_server()
        self._redraw()

        # Back to nornmal
        self._active_menu = self._get_root_menu()
        self._redraw()


    def _get_messages_to_send(self):
        self._db.connect_thread()
        stored_messages = self._db.get_messages_to_forward()
        user_messages = [UserMessage(m.header_address, m.header_sent_at, m.contents)
                             for m in
                             stored_messages]
        return user_messages


    def _on_server_announcement(self, *args):
        """Callback when a server announcement is picked up"""
        host, port = args
        self._db.connect_thread()

        user_messages = self._get_messages_to_send()

        self._logger.debug("Messages slated for exchange: " + str(user_messages))

        self._adapter.exchange_messages_with_server(
            (host, port),
            user_messages,
            self._on_message_received)

        # Once exchange is complete, we'll uptick the number of transfers for all
        # exchanged messages.

        self._db.disconnect_thread()


    def _on_message_received(self, *args):
        # We really expect only one "user message" to be returned per call. Anything else
        # is an error. Some strong typing would be in order. Python 3.4 says no.
        message = args[0]
        self._logger.info("Message received: {}".format(message))
        self._db.store_message(message)
        self._logger.info("Message stored.")


    def received_messages(self):
        """
        This command will list messages that match our own internally generated addresses -
        Generally this will mean that they are addressed to us/this node
        """
        self._status = "Listing received messages"
        messages = self._db.get_recieved_messages()

        def _show_message(m):
            self._show_message_cleartext(m.id, m.contents, m.header_address)
            self._redraw()

        menu_items = []
        i = 1
        for m in messages:
            menu_items.append(_MenuItem(str(i), m.id,
                                        lambda m=m: _show_message(m)))
            i = i + 1
        menu_items.append(_MenuItem("q", "Back to root menu", self.reset))

        self._active_menu = _Menu("Messages to me: ", menu_items)


    def _show_message_cleartext(self, message_id: str, ciphertext: str, address_id: str) -> None:
        key_id, private_key = self._db.get_own_address_nacl_key(address_id)
        cleartext = self._crypto_provider.decrypt(ciphertext, private_key)

        # Cleartext is supposed to be a MIME formatted message
        msg = BytesParser(policy=policy.default).parsebytes(cleartext)

        ## DEBUG
        self._logger.debug("Message id: {0} - Content: {1}".format(message_id, msg.as_string()))

        content = []
        address_pad = None
        address_pad_req = None

        for part in msg.walk():
            # Account for stuff we know will turn up - Specifically wrappers and protocol
            # control messages
            if part.get_content_type() == 'application/json':
                if part['Content-Description'] == NodeIntercom.address_pad_request_description:
                    address_pad_req = NodeIntercom.AddressPadRequest.deserialize(part.get_content())
                if part['Content-Description'] == NodeIntercom.address_pad_description:
                    address_pad = NodeIntercom.AddressPad.deserialize(part.get_content())
            elif part.get_content_maintype() == 'multipart' or \
              part.get_content_maintype() == 'application':
                continue
            else:
                content.append(part.get_content())

        msg_string = "From: {0}\nTo: {1}\n\n{2}".format(msg['from'], msg['to'], "\n".join(content))

        def _delete_message(message_id):
            self._logger.debug("Attempting to clean out message: {0}".format(message_id))
            self._db.clean_out_received_message(message_id)
            self.received_messages()  # Reload the messages menu
            return True

        def _no_action():
            return True

        # If the mail contained a address pad request or a message pad, show the alternatives
        if address_pad:
            import_prompt = "This message includes a block of addresses to {0}. " \
            "Import these?".format(address_pad.from_alias)
            should_import = self._show_confirmation_dialog(import_prompt)
            self._redraw()

            if should_import:
                self._import_address_pad(address_pad, msg['to'])

        if address_pad_req:
            request_prompt = "This message includes a request by {0} to send {1} additional " \
              "addresses. Enqueue addresses to {0}?".format(msg['from'], address_pad_req.pad_size)
            should_respond = self._show_confirmation_dialog(request_prompt)
            self._redraw()

            if should_respond:
                # Takes the "from" field and finds a contact from it, which is not ideal -
                # A "real" version of this would offer some kind of contact selection interaction
                # instead of trusting the from-header.
                destination = self._db.read_contact_from_nickname(msg['from'])
                # If the destination is not a known nickname, we'll just drop it - Again not ideal.
                if not destination:
                    return
                address_data = self._db.get_address_pad_nacl(destination.contact_id)[0]
                return_msg = self._generate_own_address_pad_mime_message(destination, 10)
                self._enqueue_generic_message(address_data, return_msg)


        # With the system messages done and dusted, let's show the defaul choices
        menu_items = []
        menu_items.append(_MenuItem("q", "Return", lambda: _no_action))
        menu_items.append(_MenuItem("d", "Delete", lambda: _delete_message(message_id)))
        action_menu = _Menu("Choice", menu_items)

        self._show_selection_dialog(action_menu, vertical=False, text=msg_string)


    def _import_address_pad(self, address_pad: NodeIntercom.AddressPad, own_alias: str) -> None:
        """
        This method will import foreign keys into our database in order for us to be able to send
        messages to the contact identified by the "owner" nickname in the pad - A "real" version of
        this would offer some kind of selection interaction instead of just using the nickname sent
        in the pad.
        """
        contact = self._db.read_contact_from_nickname(address_pad.from_alias)

        # If the destination is not a known nickname, we'll just create a new one - A better
        # interaction model would be good, but I want this shit done quick.
        if not contact:
            contact_id = self._db.store_contact(address_pad.from_alias, own_alias)
        else:
            contact_id = contact.contact_id

        for key_mapping in address_pad.key_mappings:
            contact_key_id = self._db.store_contact_nacl_key(key_mapping.key)
            for adr in key_mapping.address_list:
                self._db.store_contact_address(contact_id, adr, contact_key_id)


    def _generate_own_address_pad_mime_message(self, destination: contact.Contact,
                                               size: int) -> EmailMessage:
        address_pad_part = self._generate_own_address_pad(destination, size)
        msg = _MIME.construct_top_level_mime_message(destination.nickname,
                                                    destination.alias,
                                                    'Addresses to use when contacting me.',
                                                    [address_pad_part])
        return msg


    def _generate_own_address_pad(self, contact: contact.Contact, size: int) -> EmailMessage:
        """
        This method will generate an address pad MIME message. It takes the message
        recipients contact_id as argument as well as the intended size of the address pad.

        This implementation defaults the alias to send along with the address pad to be the
        same as the one registered for the contact - This is not mandated, but probably a
        reasonable guess at what a normal user would expect.
        """
        # Retrieve a suitable public key to send
        (key_id, public_key) = self._generate_new_key()

        # Produce address pad - We start with a block of addresses tied to a single key
        # This should probably be a double iterator to make new keys as we need them
        # (typically a new key per handful of addresses)

        address_list = []
        for _ in range(size):
            adr = "ADR-" + str(uuid.uuid4())
            address_list.append(adr)

        key_mapping = NodeIntercom.KeyMapping(public_key, address_list)

        # Persist the new addresses to the database, making sure we can read the messages
        # sent from the contact to us later.
        for adr in address_list:
            self._db.store_own_address(adr, contact.contact_id, key_id)

        # Remember: contact.alias is the name by which we are known to the contact
        return NodeIntercom.generate_address_pad(contact.alias, [key_mapping])


    def _generate_foreign_address_pad_mime_message(self, destination: contact.Contact,
                                                   address_owner: contact.Contact,
                                                   size: int) -> EmailMessage:
        address_pad_part = self._generate_foreign_address_pad(destination, address_owner, size)
        msg = _MIME.construct_top_level_mime_message(destination.nickname,
                                                     destination.alias,
                                                     'Addresses to use when contacting {}.'.\
                                                         format(address_owner.nickname),
                                                     [address_pad_part])
        return msg

    def _generate_foreign_address_pad(self, recipient: contact.Contact, \
                                      address_owner: contact.Contact, size: int) -> EmailMessage:
        """
        This method will generate an address pad MIME message containing addresses beloning to the
        address_owner - A contact that should be known to this node - the recipient is another
        contact to which the address pad should be sent
        """
        address_list = self._db.get_address_pad_nacl(address_owner.contact_id, size)

        mappings = {}
        for address in address_list:
            if not address.key_id in mappings:
                mappings[address.key_id] = NodeIntercom.KeyMapping(address.public_key, [])
            mappings[address.key_id].add_address(address.address_id)
            # Note that as we are exporting these addresses, we need to mark them as used in
            # order not to send addresses that might have been used / might be used in the future
            # by our contact.
            self._db.mark_contact_address(address.address)

        return NodeIntercom.generate_address_pad(address_owner.nickname, mappings.values())


    def enqueue_address_pad(self) -> None:
        """
        This command will enqueue an address pad of our own addresses OR a pad of addresses for
        a known contact that we want to forward to some third party (basically).
        """
        self.status = "Enqueueing address pad"

        menu_items = []
        i = 1
        menu_items.append(_MenuItem(str(i), "Myself", self.reset))
        i = i + 1
        contacts = self._db.get_contacts()
        for contact in contacts:
            menu_items.append(_MenuItem(str(i), contact.nickname, self.reset))
            i = i + 1
        menu_items.append(_MenuItem("q", "Back to root menu", self.reset))

        self._active_menu = _Menu("Send addresses belonging to: ", menu_items)
Ejemplo n.º 9
0
def generate_new_key(db: SqliteStorage, crypto: ProviderNaCl) -> (str, str):
    secret_key, public_key = crypto.generate_key()
    key_id = db.store_own_nacl_key(secret_key, public_key)
    return (key_id, public_key)