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)
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)
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)
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)
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)
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))
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)
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)
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)
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)
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))
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)
def test_private_key_generation(self): self.assertIsInstance(X448.generate_private_key(), X448PrivateKey)
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))
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)
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
def test_zero_public_key_raises_critical_error(self): with self.assertRaises(SystemExit): X448.shared_key(X448PrivateKey.generate(), bytes(TFC_PUBLIC_KEY_LENGTH))
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)
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)
def test_generate_private_key_function_returns_private_key_object( self) -> None: self.assertIsInstance(X448.generate_private_key(), X448PrivateKey)
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)
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)