Example #1
0
    def test_deriving_invalid_type_public_key_raises_critical_error(self):
        private_key = MagicMock(public_key=MagicMock(return_value=MagicMock(
            public_bytes=MagicMock(side_effect=[TFC_PUBLIC_KEY_LENGTH *
                                                'a']))))

        with self.assertRaises(SystemExit):
            X448.derive_public_key(private_key)
Example #2
0
    def test_flask_server(self) -> None:
        # Setup
        queues = gen_queue_dict()
        url_token_private_key = X448.generate_private_key()
        url_token_public_key = X448.derive_public_key(
            url_token_private_key).hex()
        url_token = 'a450987345098723459870234509827340598273405983274234098723490285'
        url_token_old = 'a450987345098723459870234509827340598273405983274234098723490286'
        url_token_invalid = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
        onion_pub_key = nick_to_pub_key('Alice')
        onion_address = nick_to_onion_address('Alice')
        packet1 = "packet1"
        packet2 = "packet2"
        packet3 = b"packet3"

        # Test
        app = flask_server(queues, url_token_public_key, unit_test=True)

        with app.test_client() as c:
            # Test root domain returns public key of server.
            resp = c.get('/')
            self.assertEqual(resp.data, url_token_public_key.encode())

            resp = c.get(f'/contact_request/{onion_address}')
            self.assertEqual(b'OK', resp.data)
            self.assertEqual(queues[CONTACT_REQ_QUEUE].qsize(), 1)

            # Test invalid URL token returns empty response
            resp = c.get(f'/{url_token_invalid}/messages/')
            self.assertEqual(b'', resp.data)
            resp = c.get(f'/{url_token_invalid}/files/')
            self.assertEqual(b'', resp.data)

        # Test valid URL token returns all queued messages
        queues[URL_TOKEN_QUEUE].put((onion_pub_key, url_token_old))
        queues[URL_TOKEN_QUEUE].put((onion_pub_key, url_token))
        queues[M_TO_FLASK_QUEUE].put((packet1, onion_pub_key))
        queues[M_TO_FLASK_QUEUE].put((packet2, onion_pub_key))
        queues[F_TO_FLASK_QUEUE].put((packet3, onion_pub_key))

        with app.test_client() as c:
            resp = c.get(f'/{url_token}/messages/')
            self.assertEqual(b'packet1\npacket2', resp.data)

        with app.test_client() as c:
            resp = c.get(f'/{url_token}/files/')
            self.assertEqual(b'packet3', resp.data)

        # Test valid URL token returns nothing as queues are empty
        with app.test_client() as c:
            resp = c.get(f'/{url_token}/messages/')
            self.assertEqual(b'', resp.data)

        with app.test_client() as c:
            resp = c.get(f'/{url_token}/files/')
            self.assertEqual(b'', resp.data)

        # Teardown
        tear_queues(queues)
Example #3
0
    def test_non_unique_subkeys_raise_critical_error(self) -> None:
        # Setup
        shared_key = os.urandom(SYMMETRIC_KEY_LENGTH)
        tx_public_key = os.urandom(TFC_PUBLIC_KEY_LENGTH)

        # Test
        with self.assertRaises(SystemExit):
            X448.derive_subkeys(shared_key, tx_public_key, tx_public_key)
Example #4
0
 def test_incorrect_public_key_length_raises_critical_error(self):
     sk = X448PrivateKey.generate()
     for key in [
             key_len * b'a' for key_len in range(1, 100)
             if key_len != TFC_PUBLIC_KEY_LENGTH
     ]:
         with self.assertRaises(SystemExit):
             X448.shared_key(sk, key)
Example #5
0
 def test_deriving_shared_secret_with_an_invalid_size_public_key_raises_critical_error(
         self) -> None:
     private_key = X448.generate_private_key()
     invalid_public_keys = [
         key_length * b'a'
         for key_length in (1, TFC_PUBLIC_KEY_LENGTH - 1,
                            TFC_PUBLIC_KEY_LENGTH + 1, 1000)
     ]
     for invalid_public_key in invalid_public_keys:
         with self.assertRaises(SystemExit):
             X448.shared_key(private_key, invalid_public_key)
Example #6
0
    def test_x448_with_the_official_test_vectors(self) -> None:
        sk_alice_ = X448PrivateKey.from_private_bytes(TestX448.sk_alice)
        sk_bob_ = X448PrivateKey.from_private_bytes(TestX448.sk_bob)

        self.assertEqual(X448.derive_public_key(sk_alice_), TestX448.pk_alice)
        self.assertEqual(X448.derive_public_key(sk_bob_), TestX448.pk_bob)

        shared_secret1 = X448.shared_key(sk_alice_, TestX448.pk_bob)
        shared_secret2 = X448.shared_key(sk_bob_, TestX448.pk_alice)

        self.assertEqual(shared_secret1, blake2b(TestX448.shared_secret))
        self.assertEqual(shared_secret2, blake2b(TestX448.shared_secret))
Example #7
0
def update_url_token(url_token_private_key: 'X448PrivateKey',
                     ut_pubkey_hex:         str,
                     cached_pk:             str,
                     onion_pub_key:         bytes,
                     queues:                'QueueDict'
                     ) -> Tuple[str, str]:
    """Update URL token for contact.

    When contact's URL token public key changes, update URL token.
    """
    if ut_pubkey_hex == cached_pk:
        raise SoftError("URL token public key has not changed.", output=False)

    try:
        public_key = bytes.fromhex(ut_pubkey_hex)

        if len(public_key) != TFC_PUBLIC_KEY_LENGTH or public_key == bytes(TFC_PUBLIC_KEY_LENGTH):
            raise ValueError

        url_token = X448.shared_key(url_token_private_key, public_key).hex()

        queues[URL_TOKEN_QUEUE].put((onion_pub_key, url_token))  # Update Flask server's URL token for contact

        return url_token, ut_pubkey_hex

    except (TypeError, ValueError):
        raise SoftError("URL token derivation failed.", output=False)
Example #8
0
    def test_client_scheduler(self):
        queues = gen_queue_dict()
        gateway = Gateway()
        server_private_key = X448.generate_private_key()

        def queue_delayer():
            """Place messages to queue one at a time."""
            time.sleep(0.1)
            queues[TOR_DATA_QUEUE].put(
                ('1234', nick_to_onion_address('Alice')))
            queues[CONTACT_KEY_QUEUE].put(
                (RP_ADD_CONTACT_HEADER,
                 b''.join([nick_to_pub_key('Alice'),
                           nick_to_pub_key('Bob')]), True))
            time.sleep(0.1)
            queues[CONTACT_KEY_QUEUE].put(
                (RP_REMOVE_CONTACT_HEADER,
                 b''.join([nick_to_pub_key('Alice'),
                           nick_to_pub_key('Bob')]), True))
            time.sleep(0.1)
            queues[UNITTEST_QUEUE].put(EXIT)
            time.sleep(0.1)
            queues[CONTACT_KEY_QUEUE].put((EXIT, EXIT, EXIT))

        threading.Thread(target=queue_delayer).start()

        self.assertIsNone(
            client_scheduler(queues,
                             gateway,
                             server_private_key,
                             unittest=True))
        tear_queues(queues)
Example #9
0
    def test_deriving_invalid_size_public_key_raises_critical_error(self):
        """
        The public key is already validated by the pyca/cryptography
        library[1], but assertive programming is a good practice, so
        this test ensures TFC also detects invalid public keys sizes
        from pyca/cryptography library.

         [1] https://github.com/pyca/cryptography/blob/master/src/cryptography/hazmat/backends/openssl/x448.py#L58
        """
        private_key = MagicMock(public_key=MagicMock(return_value=MagicMock(
            public_bytes=MagicMock(
                side_effect=[(TFC_PUBLIC_KEY_LENGTH - 1) *
                             b'a', (TFC_PUBLIC_KEY_LENGTH + 1) * b'a']))))
        with self.assertRaises(SystemExit):
            X448.derive_public_key(private_key)
        with self.assertRaises(SystemExit):
            X448.derive_public_key(private_key)
Example #10
0
    def test_x448_subkey_derivation(self) -> None:
        # Setup
        shared_key = os.urandom(SYMMETRIC_KEY_LENGTH)
        tx_public_key = os.urandom(TFC_PUBLIC_KEY_LENGTH)
        rx_public_key = os.urandom(TFC_PUBLIC_KEY_LENGTH)

        # Test
        key_set = X448.derive_subkeys(shared_key, tx_public_key, rx_public_key)

        # Test that correct number of keys were returned
        self.assertEqual(len(key_set), 6)

        # Test that each key is unique
        self.assertEqual(len(set(key_set)), 6)
Example #11
0
    def test_deriving_zero_shared_secret_raises_critical_error(self) -> None:
        """\
        Some experts such as JP Aumasson[1] and Thai Duong[2] have
        argued that X25519 public keys should be validated before use to
        prevent one party from having key control, i.e., being able to
        force the shared secret to a preselected value. This also
        applies to X448.
            It's not clear how this type of attack could be leveraged in
        the context of secure messaging where both the sender and the
        recipient desire confidentiality, and where easier ways to break
        the confidentiality of the conversation exist for both parties.
        However, there is
          a) no harm in doing the check and
          b) no need to trouble ourselves with whether TFC should ensure
             contributory behavior; the pyca/cryptography library
             already checks that the shared secret is not zero. This
             test merely verifies that the check takes place.

         [1] https://research.kudelskisecurity.com/2017/04/25/should-ecdh-keys-be-validated/
         [2] https://vnhacker.blogspot.com/2015/09/why-not-validating-curve25519-public.html
        """
        with self.assertRaises(SystemExit):
            X448.shared_key(X448.generate_private_key(),
                            bytes(TFC_PUBLIC_KEY_LENGTH))
Example #12
0
    def test_flask_server(self) -> None:
        # Setup
        queues = gen_queue_dict()
        url_token_private_key = X448.generate_private_key()
        url_token_public_key = X448.derive_public_key(
            url_token_private_key).hex()
        url_token = 'a450987345098723459870234509827340598273405983274234098723490285'
        url_token_old = 'a450987345098723459870234509827340598273405983274234098723490286'
        url_token_invalid = 'ääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääää'
        onion_pub_key = nick_to_pub_key('Alice')
        onion_address = nick_to_onion_address('Alice')
        packet1 = b"packet1"
        packet2 = b"packet2"
        packet3 = b"packet3"
        test_key = SYMMETRIC_KEY_LENGTH * b'a'

        sub_dir = hashlib.blake2b(
            onion_pub_key, key=test_key,
            digest_size=BLAKE2_DIGEST_LENGTH).hexdigest()

        buf_dir_m = f"{RELAY_BUFFER_OUTGOING_M_DIR}/{sub_dir}"
        buf_dir_f = f"{RELAY_BUFFER_OUTGOING_F_DIR}/{sub_dir}"

        ensure_dir(f"{buf_dir_m}/")
        ensure_dir(f"{buf_dir_f}/")

        packet_list = [packet1, packet2]

        for i, packet in enumerate(packet_list):
            TestFlaskServer.store_test_packet(
                packet, buf_dir_m, RELAY_BUFFER_OUTGOING_MESSAGE + f".{i}",
                test_key)

        TestFlaskServer.store_test_packet(packet3, buf_dir_f,
                                          RELAY_BUFFER_OUTGOING_FILE + '.0',
                                          test_key)

        def queue_delayer() -> None:
            """Place buffer key to queue after a delay."""
            time.sleep(0.1)
            queues[RX_BUF_KEY_QUEUE].put(test_key)

        threading.Thread(target=queue_delayer).start()

        # Test
        app = flask_server(queues, url_token_public_key, unit_test=True)

        # Test valid URL token returns all queued messages
        queues[URL_TOKEN_QUEUE].put((onion_pub_key, url_token_old))
        queues[URL_TOKEN_QUEUE].put((onion_pub_key, url_token))

        with app.test_client() as c:
            # Test root domain returns public key of server.
            resp = c.get('/')
            self.assertEqual(resp.data, url_token_public_key.encode())

            resp = c.get(f'/contact_request/{onion_address}')
            self.assertEqual(b'OK', resp.data)
            self.assertEqual(queues[CONTACT_REQ_QUEUE].qsize(), 1)

            # Test invalid URL token returns empty response
            resp = c.get(f'/{url_token_invalid}/messages/')
            self.assertEqual(b'', resp.data)
            resp = c.get(f'/{url_token_invalid}/files/')
            self.assertEqual(b'', resp.data)

        with app.test_client() as c:
            resp = c.get(f'/{url_token}/messages/')
            self.assertEqual(b'packet1\npacket2', resp.data)

        with app.test_client() as c:
            resp = c.get(f'/{url_token}/files/')
            self.assertEqual(b'packet3', resp.data)

        # Test valid URL token returns nothing as buffers are empty
        with app.test_client() as c:
            resp = c.get(f'/{url_token}/messages/')
            self.assertEqual(b'', resp.data)

        with app.test_client() as c:
            resp = c.get(f'/{url_token}/files/')
            self.assertEqual(b'', resp.data)

        # Teardown
        tear_queues(queues)
Example #13
0
 def test_private_key_generation(self):
     self.assertIsInstance(X448.generate_private_key(), X448PrivateKey)
Example #14
0
class TestClient(unittest.TestCase):

    url_token_private_key = X448.generate_private_key()
    url_token_public_key = X448.derive_public_key(url_token_private_key)
    url_token = X448.shared_key(url_token_private_key,
                                url_token_public_key).hex()

    class MockResponse(object):
        """Mock Response object."""
        def __init__(self, text):
            """Create new MockResponse object."""
            self.text = text
            self.content = text

    class MockSession(object):
        """Mock Session object."""
        def __init__(self):
            """Create new MockSession object."""
            self.proxies = dict()
            self.timeout = None
            self.url = None
            self.test_no = 0

        def get(self, url, timeout=0, stream=False):
            """Mock .get() method."""

            self.timeout = timeout

            # When we reach `get_data_loop` that loads stream, throw exception to close the test.
            if stream:
                (_ for _ in ()).throw(requests.exceptions.RequestException)

            if url.startswith(
                    "http://hpcrayuxhrcy2wtpfwgwjibderrvjll6azfr4tqat3eka2m2gbb55bid.onion/"
            ):

                if self.test_no == 0:
                    self.test_no += 1
                    (_ for _ in ()).throw(requests.exceptions.RequestException)

                if self.test_no == 1:
                    self.test_no += 1
                    return TestClient.MockResponse('OK')

                # Test function recovers from RequestException.
                if self.test_no == 2:
                    self.test_no += 1
                    (_ for _ in ()).throw(requests.exceptions.RequestException)

                # Test function recovers from invalid public key.
                if self.test_no == 3:
                    self.test_no += 1
                    return TestClient.MockResponse(
                        ((ONION_SERVICE_PUBLIC_KEY_LENGTH - 1) * b'a').hex())

                # Test client prints online/offline messages.
                elif self.test_no < 10:
                    self.test_no += 1
                    return TestClient.MockResponse('')

                # Test valid public key moves function to `get_data_loop`.
                elif self.test_no == 10:
                    self.test_no += 1
                    return TestClient.MockResponse(
                        TestClient.url_token_public_key.hex())

    @staticmethod
    def mock_session():
        """Return MockSession object."""
        return TestClient.MockSession()

    def setUp(self):
        self.o_session = requests.session
        self.queues = gen_queue_dict()
        requests.session = TestClient.mock_session

    def tearDown(self):
        requests.session = self.o_session
        tear_queues(self.queues)

    @mock.patch('time.sleep', return_value=None)
    def test_client(self, _):
        onion_pub_key = nick_to_pub_key('Alice')
        onion_address = nick_to_onion_address('Alice')
        tor_port = '1337'
        settings = Gateway()
        sk = TestClient.url_token_private_key
        self.assertIsNone(
            client(onion_pub_key,
                   self.queues,
                   sk,
                   tor_port,
                   settings,
                   onion_address,
                   unittest=True))
        self.assertEqual(self.queues[URL_TOKEN_QUEUE].get(),
                         (onion_pub_key, TestClient.url_token))
Example #15
0
def start_key_exchange(
    onion_pub_key: bytes,  # Public key of contact's v3 Onion Service
    nick: str,  # Contact's nickname
    contact_list: 'ContactList',  # ContactList object
    settings: 'Settings',  # Settings object
    queues: 'QueueDict'  # Dictionary of multiprocessing queues
) -> None:
    """Start X448 key exchange with the recipient.

    This function first creates the X448 key pair. It then outputs the
    public key to Relay Program on Networked Computer, that passes the
    public key to contact's Relay Program where it is displayed. When
    the contact's public key reaches the user's Relay Program, the user
    will manually type the key into their Transmitter Program.

    The X448 shared secret is used to create unidirectional message and
    header keys, that will be used in forward secret communication. This
    is followed by the fingerprint verification where the user manually
    authenticates the public key.

    Once the fingerprint has been accepted, this function will add the
    contact/key data to contact/key databases, and export that data to
    the Receiver Program on Destination Computer. The transmission is
    encrypted with the local key.

    ---

    TFC provides proactive security by making fingerprint verification
    part of the key exchange. This prevents the situation where the
    users don't know about the feature, and thus helps minimize the risk
    of MITM attack.

    The fingerprints can be skipped by pressing Ctrl+C. This feature is
    not advertised however, because verifying fingerprints the only
    strong way to be sure TFC is not under MITM attack. When
    verification is skipped, TFC marks the contact's X448 keys as
    "Unverified". The fingerprints can later be verified with the
    `/verify` command: answering `yes` to the question on whether the
    fingerprints match, marks the X448 keys as "Verified".

    Variable naming:
        tx = user's key     rx = contact's key    fp = fingerprint
        mk = message key    hk = header key
    """
    if not contact_list.has_pub_key(onion_pub_key):
        contact_list.add_contact(onion_pub_key, nick,
                                 bytes(FINGERPRINT_LENGTH),
                                 bytes(FINGERPRINT_LENGTH), KEX_STATUS_PENDING,
                                 settings.log_messages_by_default,
                                 settings.accept_files_by_default,
                                 settings.show_notifications_by_default)
    contact = contact_list.get_contact_by_pub_key(onion_pub_key)

    # Generate new private key or load cached private key
    if contact.tfc_private_key is None:
        tfc_private_key_user = X448.generate_private_key()
    else:
        tfc_private_key_user = contact.tfc_private_key

    try:
        tfc_public_key_user = X448.derive_public_key(tfc_private_key_user)
        kdk_hash = contact_list.get_contact_by_pub_key(
            LOCAL_PUBKEY).tx_fingerprint
        tfc_public_key_contact = exchange_public_keys(onion_pub_key,
                                                      tfc_public_key_user,
                                                      kdk_hash, contact,
                                                      settings, queues)

        validate_contact_public_key(tfc_public_key_contact)

        dh_shared_key = X448.shared_key(tfc_private_key_user,
                                        tfc_public_key_contact)

        tx_mk, rx_mk, tx_hk, rx_hk, tx_fp, rx_fp \
            = X448.derive_subkeys(dh_shared_key, tfc_public_key_user, tfc_public_key_contact)

        kex_status = validate_contact_fingerprint(tx_fp, rx_fp)

        deliver_contact_data(KEY_EX_ECDHE, nick, onion_pub_key, tx_mk, rx_mk,
                             tx_hk, rx_hk, queues, settings)

        # Store contact data into databases
        contact.tfc_private_key = None
        contact.tx_fingerprint = tx_fp
        contact.rx_fingerprint = rx_fp
        contact.kex_status = kex_status
        contact_list.store_contacts()

        queues[KEY_MANAGEMENT_QUEUE].put(
            (KDB_ADD_ENTRY_HEADER, onion_pub_key, tx_mk, csprng(), tx_hk,
             csprng()))

        m_print(f"Successfully added {nick}.",
                bold=True,
                tail_clear=True,
                delay=1,
                head=1)

    except (EOFError, KeyboardInterrupt):
        contact.tfc_private_key = tfc_private_key_user
        raise SoftError("Key exchange interrupted.",
                        tail_clear=True,
                        delay=1,
                        head=2)
Example #16
0
class TestGetDataLoop(unittest.TestCase):

    url_token_private_key_user = X448.generate_private_key()
    url_token_public_key_user = X448.derive_public_key(
        url_token_private_key_user)
    url_token_public_key_contact = X448.derive_public_key(
        X448.generate_private_key())
    url_token = X448.shared_key(url_token_private_key_user,
                                url_token_public_key_contact).hex()

    class MockResponse(object):
        """Mock Response object."""
        def __init__(self):
            self.test_no = 0

        def iter_lines(self):
            """Return data depending test number."""
            self.test_no += 1
            message = b''

            # Empty message
            if self.test_no == 1:
                pass

            # Invalid message
            elif self.test_no == 2:
                message = MESSAGE_DATAGRAM_HEADER + b'\x1f'

            # Valid message
            elif self.test_no == 3:
                message = MESSAGE_DATAGRAM_HEADER + base64.b85encode(
                    b'test') + b'\n'

            # Invalid public key
            elif self.test_no == 4:
                message = PUBLIC_KEY_DATAGRAM_HEADER + base64.b85encode(
                    (TFC_PUBLIC_KEY_LENGTH - 1) * b'\x01')

            # Valid public key
            elif self.test_no == 5:
                message = PUBLIC_KEY_DATAGRAM_HEADER + base64.b85encode(
                    TFC_PUBLIC_KEY_LENGTH * b'\x01')

            # Group management headers
            elif self.test_no == 6:
                message = GROUP_MSG_INVITE_HEADER

            elif self.test_no == 7:
                message = GROUP_MSG_JOIN_HEADER

            elif self.test_no == 8:
                message = GROUP_MSG_MEMBER_ADD_HEADER

            elif self.test_no == 9:
                message = GROUP_MSG_MEMBER_REM_HEADER

            elif self.test_no == 10:
                message = GROUP_MSG_EXIT_GROUP_HEADER

            # Invalid header
            elif self.test_no == 11:
                message = b'\x1f'

            # RequestException (no remaining data)
            elif self.test_no == 12:
                (_ for _ in ()).throw(requests.exceptions.RequestException)

            return message.split(b'\n')

    class MockFileResponse(object):
        """MockFileResponse object."""
        def __init__(self, content):
            self.content = content

    class Session(object):
        """Mock session object."""
        def __init__(self) -> None:
            """Create new Session object."""
            self.proxies = dict()
            self.timeout = None
            self.url = None
            self.stream = False
            self.test_no = 0
            self.response = TestGetDataLoop.MockResponse()
            self.url_token = TestGetDataLoop.url_token
            self.onion_url = 'http://aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaam2dqd.onion'

        def get(self, url: str, timeout: int = 0, stream: bool = False) -> Any:
            """Return data depending on what test is in question."""

            self.stream = stream
            self.timeout = timeout

            if url == f"{self.onion_url}/{self.url_token}/messages":

                # Test function recovers from RequestException.
                if self.test_no == 1:
                    self.test_no += 1
                    (_ for _ in ()).throw(requests.exceptions.RequestException)

                if self.test_no >= 2:
                    self.test_no += 1
                    return self.response

            elif url == f"{self.onion_url}/{self.url_token}/files":

                # Test file data is received
                if self.test_no == 0:
                    self.test_no += 1
                    return TestGetDataLoop.MockFileResponse(b'test')

                # Test function recovers from RequestException.
                if self.test_no > 1:
                    (_ for _ in ()).throw(requests.exceptions.RequestException)

    @staticmethod
    def mock_session() -> Session:
        """Return mock Session object."""
        return TestGetDataLoop.Session()

    def setUp(self):
        self.o_session = requests.session
        self.queues = gen_queue_dict()
        requests.session = TestGetDataLoop.mock_session

    def tearDown(self):
        requests.session = self.o_session
        tear_queues(self.queues)

    def test_get_data_loop(self):

        onion_pub_key = bytes(ONION_SERVICE_PUBLIC_KEY_LENGTH)
        settings = Gateway()
        onion_addr = pub_key_to_onion_address(
            bytes(ONION_SERVICE_PUBLIC_KEY_LENGTH))
        short_addr = pub_key_to_short_address(
            bytes(ONION_SERVICE_PUBLIC_KEY_LENGTH))
        url_token = TestGetDataLoop.url_token
        session = TestGetDataLoop.mock_session()

        self.assertIsNone(
            get_data_loop(onion_addr, url_token, short_addr, onion_pub_key,
                          self.queues, session, settings))

        self.assertIsNone(
            get_data_loop(onion_addr, url_token, short_addr, onion_pub_key,
                          self.queues, session, settings))

        self.assertEqual(self.queues[DST_MESSAGE_QUEUE].qsize(),
                         2)  # Message and file
        self.assertEqual(self.queues[GROUP_MSG_QUEUE].qsize(),
                         5)  # 5 group management messages
Example #17
0
 def test_zero_public_key_raises_critical_error(self):
     with self.assertRaises(SystemExit):
         X448.shared_key(X448PrivateKey.generate(),
                         bytes(TFC_PUBLIC_KEY_LENGTH))
Example #18
0
File: relay.py Project: dimwap/tfc
def main() -> None:
    """Load persistent settings and launch the Relay Program.

    This function loads settings from the settings database and launches
    processes for the Relay Program. It then monitors the EXIT_QUEUE for
    EXIT/WIPE signals and each process in case one of them dies.

    If you're reading this code to get the big picture on how TFC works,
    start by looking at `tfc.py` for Transmitter Program functionality.
    After you have reviewed the Transmitter Program's code, revisit the
    code of this program.

    The Relay Program operates multiple processes to enable real time IO
    between multiple data sources and destinations.

    Symbols:
        process_name    denotes the name of the process

        ─>, <─, ↑, ↓    denotes the direction of data passed from one
                        process to another

        (Description)   denotes the description of data passed from one
                        process to another

        ┈, ┊            denotes the link between a description and path
                        of data matching the description

        ▶|, |◀          denotes the gateways where the direction of data
                        flow is enforced with hardware data diodes


                                         Relay Program (Networked Computer)
                    ┏━━  ━━  ━━  ━━  ━━  ━━  ━━  ━━  ━━  ━━  ━━  ━━  ━━  ━━  ━━  ━━  ━━  ━━  ━━┓
                    ┃                  (1: Onion Service private key)                          ┃
                                       (2: Contact management commands)
                    ┃  ┌──────────────────────────────┬────────────────────────┬────────────┐  ┃
                       │                              │                        ↓┈2          │
                    ┃  │                    ┌─────> relay_command  ┌───> c_req_manager      │  ┃
                       │                    │                  │   │                        │
                    ┃  │     (Relay Program┈│   (Onion Service┈│   │                        │  ┃
                       │      commands)     │    public key)   │   │┈(In: Contact requests) │
                    ┃  │                    │                  ↓   │       ┊              1┈↓  ┃
      Source ───▶|─────(── gateway_loop ─> src_incoming ─> flask_server <─────> onion_service <───> client on contact's
    Computer        ┃  │                            │              ↑       ┊                   ┃     Networked Computer
                       │   (Local keys, commands,   │              │ (Out: msg/file/pubkey/
                    ┃  │    and copies of messages)┄│              │  group mgmt message)      ┃
                       │                            │              │
                    ┃  │                            ↓              │                           ┃
 Destination <──|◀─────(────────────────────── dst_outgoing        │
    Computer        ┃  │                    ┊       ↑              │                           ┃
                       ├──> g_msg_manager   ┊       │              │
                    ┃  │               ↑    ┊       │              │                           ┃
                       │        (Group┈│  (Incoming┈│  (URL token)┈│
                    ┃  │    management │  messages) │              │                           ┃
                       │     messages) │            │              │
                    ┃  ↓┈1,2           │            │              │                           ┃
                      client_scheduler │            │              │
                    ┃         └──> client ──────────┴──────────────┘                           ┃
                                     ↑
                    ┃                │                                                         ┃
                                     └───────────────────────────────────────────────────────────> flask_server on
                    ┃                                        ┊                                 ┃   contact's Networked
                                (In: message/file/public key/group management message              Computer
                    ┃            Out: contact request)                                         ┃
                    ┗━━  ━━  ━━  ━━  ━━  ━━  ━━  ━━  ━━  ━━  ━━  ━━  ━━  ━━  ━━  ━━  ━━  ━━  ━━┛


    The diagram above gives a rough overview of the structure of the
    Relay Program. The Relay Program acts as a protocol converter that
    reads datagrams from the Source Computer. Outgoing
    message/file/public key datagrams are made available in the user's
    Tor v3 Onion Service. Copies of sent message datagrams as well as
    datagrams from contacts' Onion Services are forwarded to the
    Destination Computer. The Relay-to-Relay encrypted datagrams from
    contacts such as contact requests, public keys and group management
    messages are displayed by the Relay Program.

    Outgoing message datagrams are loaded by contacts from the user's
    Flask web server. To request messages intended for them, each
    contact uses a contact-specific URL token to load the messages.
    The URL token is the X448 shared secret derived from the per-session
    ephemeral X448 values of the two conversing parties. The private
    value stays on the Relay Program -- the public value is obtained by
    connecting to the root domain of contact's Onion Service.
    """
    if platform_is_tails():
        working_dir = f'{os.getenv("HOME")}/{DIR_TAILS_PERS}{DIR_TFC}'
    else:
        working_dir = f'{os.getenv("HOME")}/{DIR_TFC}'

    ensure_dir(working_dir)
    os.chdir(working_dir)

    _, local_test, data_diode_sockets, qubes = process_arguments()

    gateway = Gateway(NC, local_test, data_diode_sockets, qubes)

    print_title(NC)

    url_token_private_key = X448.generate_private_key()
    url_token_public_key = X448.derive_public_key(url_token_private_key).hex()

    queues = \
        {GATEWAY_QUEUE:       Queue(),  # All     datagrams           from `gateway_loop`          to `src_incoming`
         DST_MESSAGE_QUEUE:   Queue(),  # Message datagrams           from `src_incoming`/`client` to `dst_outgoing`
         TX_BUF_KEY_QUEUE:    Queue(),  # Datagram buffer key         from `onion_service`         to `src_incoming`
         RX_BUF_KEY_QUEUE:    Queue(),  # Datagram buffer key         from `onion_service`         to `flask_server`
         SRC_TO_RELAY_QUEUE:  Queue(),  # Command datagrams           from `src_incoming`          to `relay_command`
         DST_COMMAND_QUEUE:   Queue(),  # Command datagrams           from `src_incoming`          to `dst_outgoing`
         CONTACT_MGMT_QUEUE:  Queue(),  # Contact management commands from `relay_command`         to `client_scheduler`
         C_REQ_STATE_QUEUE:   Queue(),  # Contact req. notify setting from `relay_command`         to `c_req_manager`
         URL_TOKEN_QUEUE:     Queue(),  # URL tokens                  from `client`                to `flask_server`
         GROUP_MSG_QUEUE:     Queue(),  # Group management messages   from `client`                to `g_msg_manager`
         CONTACT_REQ_QUEUE:   Queue(),  # Contact requests            from `flask_server`          to `c_req_manager`
         C_REQ_MGMT_QUEUE:    Queue(),  # Contact list management     from `relay_command`         to `c_req_manager`
         GROUP_MGMT_QUEUE:    Queue(),  # Contact list management     from `relay_command`         to `g_msg_manager`
         ONION_CLOSE_QUEUE:   Queue(),  # Onion Service close command from `relay_command`         to `onion_service`
         ONION_KEY_QUEUE:     Queue(),  # Onion Service private key   from `relay_command`         to `onion_service`
         TOR_DATA_QUEUE:      Queue(),  # Open port for Tor           from `onion_service`         to `client_scheduler`
         EXIT_QUEUE:          Queue(),  # EXIT/WIPE signal            from `relay_command`         to `main`
         ACCOUNT_CHECK_QUEUE: Queue(),  # Incorrectly typed accounts  from `src_incoming`          to `account_checker`
         ACCOUNT_SEND_QUEUE:  Queue(),  # Contact requests            from `flask_server`          to `account_checker`
         USER_ACCOUNT_QUEUE:  Queue(),  # User's public key           from `onion_service`         to `account_checker`
         PUB_KEY_CHECK_QUEUE: Queue(),  # Typed public keys           from `src_incoming`          to `pub_key_checker`
         PUB_KEY_SEND_QUEUE:  Queue(),  # Received public keys        from `client`                to `pub_key_checker`
         GUI_INPUT_QUEUE:     Queue()  # User inputs                 from `GUI prompt`            to `account_checker`
         }  # type: Dict[bytes, Queue[Any]]

    process_list = [
        Process(target=gateway_loop, args=(queues, gateway)),
        Process(target=src_incoming, args=(queues, gateway)),
        Process(target=dst_outgoing, args=(queues, gateway)),
        Process(target=client_scheduler,
                args=(queues, gateway, url_token_private_key)),
        Process(target=g_msg_manager, args=(queues, )),
        Process(target=c_req_manager, args=(queues, )),
        Process(target=flask_server, args=(queues, url_token_public_key)),
        Process(target=onion_service, args=(queues, )),
        Process(target=relay_command, args=(
            queues,
            gateway,
        )),
        Process(target=account_checker, args=(queues, sys.stdin.fileno())),
        Process(target=pub_key_checker, args=(queues, local_test))
    ]

    for p in process_list:
        p.start()

    monitor_processes(process_list, NC, queues)
Example #19
0
def start_key_exchange(
    onion_pub_key: bytes,  # Public key of contact's v3 Onion Service
    nick: str,  # Contact's nickname
    contact_list: 'ContactList',  # Contact list object
    settings: 'Settings',  # Settings object
    queues: 'QueueDict'  # Dictionary of multiprocessing queues
) -> None:
    """Start X448 key exchange with the recipient.

    This function first creates the X448 key pair. It then outputs the
    public key to Relay Program on Networked Computer, that passes the
    public key to contact's Relay Program. When contact's public key
    reaches the user's Relay Program, the user will manually copy the
    key into their Transmitter Program.

    The X448 shared secret is used to create unidirectional message and
    header keys, that will be used in forward secret communication. This
    is followed by the fingerprint verification where the user manually
    authenticates the public key.

    Once the fingerprint has been accepted, this function will add the
    contact/key data to contact/key databases, and export that data to
    the Receiver Program on Destination Computer. The transmission is
    encrypted with the local key.

    ---

    TFC provides proactive security by making fingerprint verification
    part of the key exchange. This prevents the situation where the
    users don't know about the feature, and thus helps minimize the risk
    of MITM attack.

    The fingerprints can be skipped by pressing Ctrl+C. This feature is
    not advertised however, because verifying fingerprints the only
    strong way to be sure TFC is not under MITM attack. When
    verification is skipped, TFC marks the contact's X448 keys as
    "Unverified". The fingerprints can later be verified with the
    `/verify` command: answering `yes` to the question on whether the
    fingerprints match, marks the X448 keys as "Verified".

    Variable naming:
        tx = user's key     rx = contact's key    fp = fingerprint
        mk = message key    hk = header key
    """
    if not contact_list.has_pub_key(onion_pub_key):
        contact_list.add_contact(onion_pub_key, nick,
                                 bytes(FINGERPRINT_LENGTH),
                                 bytes(FINGERPRINT_LENGTH), KEX_STATUS_PENDING,
                                 settings.log_messages_by_default,
                                 settings.accept_files_by_default,
                                 settings.show_notifications_by_default)
    contact = contact_list.get_contact_by_pub_key(onion_pub_key)

    # Generate new private key or load cached private key
    if contact.tfc_private_key is None:
        tfc_private_key_user = X448.generate_private_key()
    else:
        tfc_private_key_user = contact.tfc_private_key

    try:
        tfc_public_key_user = X448.derive_public_key(tfc_private_key_user)

        # Import public key of contact
        while True:
            public_key_packet = PUBLIC_KEY_DATAGRAM_HEADER + onion_pub_key + tfc_public_key_user
            queue_to_nc(public_key_packet, queues[RELAY_PACKET_QUEUE])

            tfc_public_key_contact = get_b58_key(B58_PUBLIC_KEY, settings,
                                                 contact.short_address)
            if tfc_public_key_contact != b'':
                break

        # Validate public key of contact
        if len(tfc_public_key_contact) != TFC_PUBLIC_KEY_LENGTH:
            m_print([
                "Warning!", "Received invalid size public key.",
                "Aborting key exchange for your safety."
            ],
                    bold=True,
                    tail=1)
            raise FunctionReturn("Error: Invalid public key length",
                                 output=False)

        if tfc_public_key_contact == bytes(TFC_PUBLIC_KEY_LENGTH):
            # The public key of contact is zero with negligible probability,
            # therefore we assume such key is malicious and attempts to set
            # the shared key to zero.
            m_print([
                "Warning!", "Received a malicious zero-public key.",
                "Aborting key exchange for your safety."
            ],
                    bold=True,
                    tail=1)
            raise FunctionReturn("Error: Zero public key", output=False)

        # Derive shared key
        dh_shared_key = X448.shared_key(tfc_private_key_user,
                                        tfc_public_key_contact)

        # Domain separate unidirectional keys from shared key by using public
        # keys as message and the context variable as personalization string.
        tx_mk = blake2b(tfc_public_key_contact,
                        dh_shared_key,
                        person=b'message_key',
                        digest_size=SYMMETRIC_KEY_LENGTH)
        rx_mk = blake2b(tfc_public_key_user,
                        dh_shared_key,
                        person=b'message_key',
                        digest_size=SYMMETRIC_KEY_LENGTH)
        tx_hk = blake2b(tfc_public_key_contact,
                        dh_shared_key,
                        person=b'header_key',
                        digest_size=SYMMETRIC_KEY_LENGTH)
        rx_hk = blake2b(tfc_public_key_user,
                        dh_shared_key,
                        person=b'header_key',
                        digest_size=SYMMETRIC_KEY_LENGTH)

        # Domain separate fingerprints of public keys by using the
        # shared secret as key and the context variable as
        # personalization string. This way entities who might monitor
        # fingerprint verification channel are unable to correlate
        # spoken values with public keys that they might see on RAM or
        # screen of Networked Computer: Public keys can not be derived
        # from the fingerprints due to preimage resistance of BLAKE2b,
        # and fingerprints can not be derived from public key without
        # the X448 shared secret. Using the context variable ensures
        # fingerprints are distinct from derived message and header keys.
        tx_fp = blake2b(tfc_public_key_user,
                        dh_shared_key,
                        person=b'fingerprint',
                        digest_size=FINGERPRINT_LENGTH)
        rx_fp = blake2b(tfc_public_key_contact,
                        dh_shared_key,
                        person=b'fingerprint',
                        digest_size=FINGERPRINT_LENGTH)

        # Verify fingerprints
        try:
            if not verify_fingerprints(tx_fp, rx_fp):
                m_print([
                    "Warning!", "Possible man-in-the-middle attack detected.",
                    "Aborting key exchange for your safety."
                ],
                        bold=True,
                        tail=1)
                raise FunctionReturn("Error: Fingerprint mismatch",
                                     delay=2.5,
                                     output=False)
            kex_status = KEX_STATUS_VERIFIED

        except (EOFError, KeyboardInterrupt):
            m_print([
                "Skipping fingerprint verification.", '', "Warning!",
                "Man-in-the-middle attacks can not be detected",
                "unless fingerprints are verified! To re-verify",
                "the contact, use the command '/verify'.", '',
                "Press <enter> to continue."
            ],
                    manual_proceed=True,
                    box=True,
                    head=2)
            kex_status = KEX_STATUS_UNVERIFIED

        # Send keys to the Receiver Program
        c_code = blake2b(onion_pub_key, digest_size=CONFIRM_CODE_LENGTH)
        command = (KEY_EX_ECDHE + onion_pub_key + tx_mk + rx_mk + tx_hk +
                   rx_hk + str_to_bytes(nick))

        queue_command(command, settings, queues)

        while True:
            purp_code = ask_confirmation_code('Receiver')
            if purp_code == c_code.hex():
                break

            elif purp_code == '':
                phase("Resending contact data", head=2)
                queue_command(command, settings, queues)
                phase(DONE)
                print_on_previous_line(reps=5)

            else:
                m_print("Incorrect confirmation code.", head=1)
                print_on_previous_line(reps=4, delay=2)

        # Store contact data into databases
        contact.tfc_private_key = None
        contact.tx_fingerprint = tx_fp
        contact.rx_fingerprint = rx_fp
        contact.kex_status = kex_status
        contact_list.store_contacts()

        queues[KEY_MANAGEMENT_QUEUE].put(
            (KDB_ADD_ENTRY_HEADER, onion_pub_key, tx_mk, csprng(), tx_hk,
             csprng()))

        m_print(f"Successfully added {nick}.",
                bold=True,
                tail_clear=True,
                delay=1,
                head=1)

    except (EOFError, KeyboardInterrupt):
        contact.tfc_private_key = tfc_private_key_user
        raise FunctionReturn("Key exchange interrupted.",
                             tail_clear=True,
                             delay=1,
                             head=2)
Example #20
0
 def test_generate_private_key_function_returns_private_key_object(
         self) -> None:
     self.assertIsInstance(X448.generate_private_key(), X448PrivateKey)
Example #21
0
 def test_x448_private_key_size(self) -> None:
     private_key_bytes = X448.generate_private_key().private_bytes(
         encoding=Encoding.Raw,
         format=PrivateFormat.Raw,
         encryption_algorithm=NoEncryption())
     self.assertEqual(len(private_key_bytes), TFC_PRIVATE_KEY_LENGTH)
Example #22
0
 def test_derive_public_key_returns_public_key_with_correct_type_and_size(
         self) -> None:
     private_key = X448.generate_private_key()
     public_key = X448.derive_public_key(private_key)
     self.assertIsInstance(public_key, bytes)
     self.assertEqual(len(public_key), TFC_PUBLIC_KEY_LENGTH)