Exemplo n.º 1
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)
Exemplo n.º 2
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)
Exemplo n.º 3
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))
Exemplo n.º 4
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)
Exemplo n.º 5
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))
Exemplo n.º 6
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)
Exemplo n.º 7
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))
Exemplo n.º 8
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
Exemplo n.º 9
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)
Exemplo n.º 10
0
 def test_zero_public_key_raises_critical_error(self):
     with self.assertRaises(SystemExit):
         X448.shared_key(X448PrivateKey.generate(),
                         bytes(TFC_PUBLIC_KEY_LENGTH))