def process_received_packet(ts: 'datetime', ts_bytes: bytes, header: bytes, payload_bytes: bytes, onion_pub_key: bytes, short_addr: str, queues: 'QueueDict', gateway: 'Gateway') -> None: """Process received packet.""" if header == PUBLIC_KEY_DATAGRAM_HEADER: if len(payload_bytes) == TFC_PUBLIC_KEY_LENGTH: msg = f"Received public key from {short_addr} at {ts.strftime('%b %d - %H:%M:%S.%f')[:-4]}:" print_key(msg, payload_bytes, gateway.settings, public_key=True) queues[PUB_KEY_SEND_QUEUE].put((onion_pub_key, payload_bytes)) elif header == MESSAGE_DATAGRAM_HEADER: queues[DST_MESSAGE_QUEUE].put(header + ts_bytes + onion_pub_key + ORIGIN_CONTACT_HEADER + payload_bytes) rp_print(f"Message from contact {short_addr}", ts) elif header in [ GROUP_MSG_INVITE_HEADER, GROUP_MSG_JOIN_HEADER, GROUP_MSG_MEMBER_ADD_HEADER, GROUP_MSG_MEMBER_REM_HEADER, GROUP_MSG_EXIT_GROUP_HEADER ]: queues[GROUP_MSG_QUEUE].put((header, payload_bytes, short_addr)) else: rp_print(f"Received invalid packet from {short_addr}", ts, bold=True)
def src_incoming(queues: 'QueueDict', gateway: 'Gateway', unittest: bool = False ) -> None: """\ Redirect messages received from Source Computer to appropriate queues. """ packets_from_sc = queues[GATEWAY_QUEUE] packets_to_dc = queues[DST_MESSAGE_QUEUE] commands_to_dc = queues[DST_COMMAND_QUEUE] messages_to_flask = queues[M_TO_FLASK_QUEUE] files_to_flask = queues[F_TO_FLASK_QUEUE] commands_to_relay = queues[SRC_TO_RELAY_QUEUE] while True: with ignored(EOFError, KeyboardInterrupt): while packets_from_sc.qsize() == 0: time.sleep(0.01) ts, packet = packets_from_sc.get() # type: datetime, bytes ts_bytes = int_to_bytes(int(ts.strftime('%Y%m%d%H%M%S%f')[:-4])) try: packet = gateway.detect_errors(packet) except FunctionReturn: continue header, packet = separate_header(packet, DATAGRAM_HEADER_LENGTH) if header == UNENCRYPTED_DATAGRAM_HEADER: commands_to_relay.put(packet) elif header in [COMMAND_DATAGRAM_HEADER, LOCAL_KEY_DATAGRAM_HEADER]: commands_to_dc.put(header + ts_bytes + packet) p_type = 'Command ' if header == COMMAND_DATAGRAM_HEADER else 'Local key' rp_print(f"{p_type} to local Receiver", ts) elif header in [MESSAGE_DATAGRAM_HEADER, PUBLIC_KEY_DATAGRAM_HEADER]: onion_pub_key, payload = separate_header(packet, ONION_SERVICE_PUBLIC_KEY_LENGTH) packet_str = header.decode() + b85encode(payload) queue_to_flask(packet_str, onion_pub_key, messages_to_flask, ts, header) if header == MESSAGE_DATAGRAM_HEADER: packets_to_dc.put(header + ts_bytes + onion_pub_key + ORIGIN_USER_HEADER + payload) elif header == FILE_DATAGRAM_HEADER: no_contacts_b, payload = separate_header(packet, ENCODED_INTEGER_LENGTH) no_contacts = bytes_to_int(no_contacts_b) ser_accounts, file_ct = separate_header(payload, no_contacts * ONION_SERVICE_PUBLIC_KEY_LENGTH) pub_keys = split_byte_string(ser_accounts, item_len=ONION_SERVICE_PUBLIC_KEY_LENGTH) for onion_pub_key in pub_keys: queue_to_flask(file_ct, onion_pub_key, files_to_flask, ts, header) elif header in [GROUP_MSG_INVITE_HEADER, GROUP_MSG_JOIN_HEADER, GROUP_MSG_MEMBER_ADD_HEADER, GROUP_MSG_MEMBER_REM_HEADER, GROUP_MSG_EXIT_GROUP_HEADER]: process_group_management_message(ts, packet, header, messages_to_flask) if unittest: break
def connect(self, port: int) -> None: """Launch Tor as a subprocess. If TFC is running on top of Tails, do not launch a separate instance of Tor. """ if self.platform_is_tails(): self.controller = Controller.from_port(port=TOR_CONTROL_PORT) self.controller.authenticate() return None tor_data_directory = tempfile.TemporaryDirectory() tor_control_socket = os.path.join(tor_data_directory.name, 'control_socket') if not os.path.isfile('/usr/bin/tor'): raise CriticalError("Check that Tor is installed.") while True: try: self.tor_process = stem.process.launch_tor_with_config( config={'DataDirectory': tor_data_directory.name, 'SocksPort': str(port), 'ControlSocket': tor_control_socket, 'AvoidDiskWrites': '1', 'Log': 'notice stdout', 'GeoIPFile': '/usr/share/tor/geoip', 'GeoIPv6File ': '/usr/share/tor/geoip6'}, tor_cmd='/usr/bin/tor') break except OSError: pass # Tor timed out. Try again. start_ts = time.monotonic() self.controller = stem.control.Controller.from_socket_file(path=tor_control_socket) self.controller.authenticate() while True: time.sleep(0.1) try: response = self.controller.get_info("status/bootstrap-phase") except stem.SocketClosed: raise CriticalError("Tor socket closed.") res_parts = shlex.split(response) summary = res_parts[4].split('=')[1] if summary == 'Done': tor_version = self.controller.get_version().version_str.split(' (')[0] rp_print(f"Setup 70% - Tor {tor_version} is now running", bold=True) break if time.monotonic() - start_ts > 15: start_ts = time.monotonic() self.controller = stem.control.Controller.from_socket_file(path=tor_control_socket) self.controller.authenticate()
def process_command_datagram(ts: 'datetime', packet: bytes, header: bytes, queues: 'QueueDict') -> None: """Process command datagram.""" commands_to_dc = queues[DST_COMMAND_QUEUE] ts_bytes = int_to_bytes(int(ts.strftime("%Y%m%d%H%M%S%f")[:-4])) commands_to_dc.put(header + ts_bytes + packet) p_type = "Command " if header == COMMAND_DATAGRAM_HEADER else "Local key" rp_print(f"{p_type} to local Receiver", ts)
def remove_client_process(onion_pub_keys: List[bytes], proc_dict: Dict[bytes, Process]) -> None: """Remove client process.""" for onion_pub_key in onion_pub_keys: if onion_pub_key in proc_dict: process = proc_dict[onion_pub_key] # type: Process process.terminate() proc_dict.pop(onion_pub_key) rp_print(f"Removed {pub_key_to_short_address(onion_pub_key)}", bold=True)
def client_scheduler(queues: 'QueueDict', gateway: 'Gateway', url_token_private_key: X448PrivateKey, unittest: bool = False) -> None: """Manage `client` processes.""" proc_dict = dict() # type: Dict[bytes, Process] # Wait for Tor port from `onion_service` process. while True: with ignored(EOFError, KeyboardInterrupt): while queues[TOR_DATA_QUEUE].qsize() == 0: time.sleep(0.1) tor_port, onion_addr_user = queues[TOR_DATA_QUEUE].get() break while True: with ignored(EOFError, KeyboardInterrupt): while queues[CONTACT_KEY_QUEUE].qsize() == 0: time.sleep(0.1) command, ser_public_keys, is_existing_contact = queues[ CONTACT_KEY_QUEUE].get() onion_pub_keys = split_byte_string( ser_public_keys, ONION_SERVICE_PUBLIC_KEY_LENGTH) if command == RP_ADD_CONTACT_HEADER: for onion_pub_key in onion_pub_keys: if onion_pub_key not in proc_dict: onion_addr_user = '' if is_existing_contact else onion_addr_user proc_dict[onion_pub_key] = Process( target=client, args=(onion_pub_key, queues, url_token_private_key, tor_port, gateway, onion_addr_user)) proc_dict[onion_pub_key].start() elif command == RP_REMOVE_CONTACT_HEADER: for onion_pub_key in onion_pub_keys: if onion_pub_key in proc_dict: process = proc_dict[onion_pub_key] # type: Process process.terminate() proc_dict.pop(onion_pub_key) rp_print( f"Removed {pub_key_to_short_address(onion_pub_key)}", bold=True) if unittest and queues[UNITTEST_QUEUE].qsize() != 0: break
def client(onion_pub_key: bytes, queues: 'QueueDict', url_token_private_key: X448PrivateKey, tor_port: str, gateway: 'Gateway', onion_addr_user: str, unit_test: bool = False) -> None: """Load packets from contact's Onion Service.""" cached_pk = '' short_addr = pub_key_to_short_address(onion_pub_key) onion_addr = pub_key_to_onion_address(onion_pub_key) check_delay = RELAY_CLIENT_MIN_DELAY is_online = False session = requests.session() session.proxies = { 'http': f'socks5h://127.0.0.1:{tor_port}', 'https': f'socks5h://127.0.0.1:{tor_port}' } rp_print(f"Connecting to {short_addr}...", bold=True) # When Transmitter Program sends contact under UNENCRYPTED_ADD_EXISTING_CONTACT, this function # receives user's own Onion address: That way it knows to request the contact to add them: if onion_addr_user: send_contact_request(onion_addr, onion_addr_user, session) while True: with ignored(EOFError, KeyboardInterrupt, SoftError): time.sleep(check_delay) url_token_public_key_hex = load_url_token(onion_addr, session) is_online, check_delay = manage_contact_status( url_token_public_key_hex, check_delay, is_online, short_addr) if not is_online: continue url_token, cached_pk = update_url_token(url_token_private_key, url_token_public_key_hex, cached_pk, onion_pub_key, queues) get_data_loop(onion_addr, url_token, short_addr, onion_pub_key, queues, session, gateway) if unit_test: break
def connect(self, port: int) -> None: """Launch Tor as a subprocess. If TFC is running on top of Tails, do not launch a separate instance of Tor. """ if self.platform_is_tails(): self.controller = Controller.from_port(port=TOR_CONTROL_PORT) self.controller.authenticate() return None tor_data_directory = tempfile.TemporaryDirectory() tor_control_socket = os.path.join(tor_data_directory.name, 'control_socket') if not os.path.isfile('/usr/bin/tor'): raise CriticalError("Check that Tor is installed.") self.launch_tor_process(port, tor_control_socket, tor_data_directory) start_ts = time.monotonic() self.controller = stem.control.Controller.from_socket_file( path=tor_control_socket) self.controller.authenticate() while True: time.sleep(0.1) try: response = self.controller.get_info("status/bootstrap-phase") except stem.SocketClosed: raise CriticalError("Tor socket closed.") res_parts = shlex.split(response) summary = res_parts[4].split('=')[1] if summary == 'Done': tor_version = self.controller.get_version().version_str.split( ' (')[0] rp_print(f"Setup 70% - Tor {tor_version} is now running", bold=True) break if time.monotonic() - start_ts > 15: start_ts = time.monotonic() self.controller = stem.control.Controller.from_socket_file( path=tor_control_socket) self.controller.authenticate()
def check_for_files(url_token: str, onion_pub_key: bytes, onion_addr: str, short_addr: str, session: 'Session', queues: 'QueueDict') -> None: """See if a file is available from contact..""" try: file_data = session.get(f"http://{onion_addr}.onion/{url_token}/files", stream=True).content if file_data: ts = datetime.now() ts_bytes = int_to_bytes(int(ts.strftime("%Y%m%d%H%M%S%f")[:-4])) packet = FILE_DATAGRAM_HEADER + ts_bytes + onion_pub_key + ORIGIN_CONTACT_HEADER + file_data queues[DST_MESSAGE_QUEUE].put(packet) rp_print(f"File from contact {short_addr}", ts) except requests.exceptions.RequestException: pass
def onion_service(queues: Dict[bytes, 'Queue[Any]']) -> None: """Manage the Tor Onion Service and control Tor via stem.""" rp_print("Setup 0% - Waiting for Onion Service configuration...", bold=True) while queues[ONION_KEY_QUEUE].qsize() == 0: time.sleep(0.1) private_key, c_code = queues[ONION_KEY_QUEUE].get() # type: bytes, bytes public_key_user = bytes( nacl.signing.SigningKey(seed=private_key).verify_key) onion_addr_user = pub_key_to_onion_address(public_key_user) buffer_key = hashlib.blake2b(BUFFER_KEY, key=private_key, digest_size=SYMMETRIC_KEY_LENGTH).digest() try: rp_print("Setup 10% - Launching Tor...", bold=True) tor_port = get_available_port(1000, 65535) tor = Tor() tor.connect(tor_port) except (EOFError, KeyboardInterrupt): return if tor.controller is None: raise CriticalError("No Tor controller") try: rp_print("Setup 75% - Launching Onion Service...", bold=True) key_data = stem_compatible_ed25519_key_from_private_key(private_key) response = tor.controller.create_ephemeral_hidden_service( ports={80: 5000}, key_type='ED25519-V3', key_content=key_data, await_publication=True) rp_print("Setup 100% - Onion Service is now published.", bold=True) m_print([ "Your TFC account is:", onion_addr_user, '', f"Onion Service confirmation code (to Transmitter): {c_code.hex()}" ], box=True) # Allow the client to start looking for contacts at this point. queues[TOR_DATA_QUEUE].put((tor_port, onion_addr_user)) queues[USER_ACCOUNT_QUEUE].put(onion_addr_user) # Pass buffer key to related processes queues[TX_BUF_KEY_QUEUE].put(buffer_key) queues[RX_BUF_KEY_QUEUE].put(buffer_key) except (KeyboardInterrupt, stem.SocketClosed): tor.stop() return monitor_queues(tor, response, queues)
def queue_to_flask(packet: Union[bytes, str], onion_pub_key: bytes, flask_queue: 'Queue[Tuple[Union[bytes, str], bytes]]', ts: 'datetime', header: bytes) -> None: """Put packet to flask queue and print message.""" p_type = { MESSAGE_DATAGRAM_HEADER: 'Message ', PUBLIC_KEY_DATAGRAM_HEADER: 'Pub key ', FILE_DATAGRAM_HEADER: 'File ', GROUP_MSG_INVITE_HEADER: 'G invite ', GROUP_MSG_JOIN_HEADER: 'G join ', GROUP_MSG_MEMBER_ADD_HEADER: 'G add ', GROUP_MSG_MEMBER_REM_HEADER: 'G remove ', GROUP_MSG_EXIT_GROUP_HEADER: 'G exit ' }[header] flask_queue.put((packet, onion_pub_key)) rp_print(f"{p_type} to contact {pub_key_to_short_address(onion_pub_key)}", ts)
def manage_contact_status(ut_pubkey_hex: str, check_delay: float, is_online: bool, short_addr: str) -> Tuple[bool, float]: """Manage online status of contact based on availability of URL token's public key.""" if ut_pubkey_hex == "": if check_delay < RELAY_CLIENT_MAX_DELAY: check_delay *= 2 if check_delay > CLIENT_OFFLINE_THRESHOLD and is_online: is_online = False rp_print(f"{short_addr} is now offline", bold=True) else: check_delay = RELAY_CLIENT_MIN_DELAY if not is_online: is_online = True rp_print(f"{short_addr} is now online", bold=True) return is_online, check_delay
def buffer_to_flask(packet: Union[bytes, str], onion_pub_key: bytes, ts: 'datetime', header: bytes, buf_key: bytes, file: bool = False) -> None: """Buffer outgoing datagram for Flask and print message.""" p_type = { MESSAGE_DATAGRAM_HEADER: 'Message ', PUBLIC_KEY_DATAGRAM_HEADER: 'Pub key ', FILE_DATAGRAM_HEADER: 'File ', GROUP_MSG_INVITE_HEADER: 'G invite ', GROUP_MSG_JOIN_HEADER: 'G join ', GROUP_MSG_MEMBER_ADD_HEADER: 'G add ', GROUP_MSG_MEMBER_REM_HEADER: 'G remove ', GROUP_MSG_EXIT_GROUP_HEADER: 'G exit ' }[header] if buf_key is None: raise SoftError("Error: No buffer key available for packet buffering.") if isinstance(packet, str): packet = packet.encode() file_name = RELAY_BUFFER_OUTGOING_FILE if file else RELAY_BUFFER_OUTGOING_MESSAGE file_dir = RELAY_BUFFER_OUTGOING_F_DIR if file else RELAY_BUFFER_OUTGOING_M_DIR sub_dir = hashlib.blake2b(onion_pub_key, key=buf_key, digest_size=BLAKE2_DIGEST_LENGTH).hexdigest() enc_packet = encrypt_and_sign(packet, key=buf_key) store_unique(enc_packet, f"{file_dir}/{sub_dir}/", file_name) rp_print(f"{p_type} to contact {pub_key_to_short_address(onion_pub_key)}", ts)
def test_works_without_timestamp(self): self.assertIsNone(rp_print("testMessage"))
def client(onion_pub_key: bytes, queues: 'QueueDict', url_token_private_key: X448PrivateKey, tor_port: str, gateway: 'Gateway', onion_addr_user: str, unittest: bool = False) -> None: """Load packets from contact's Onion Service.""" url_token = '' cached_pk = '' short_addr = pub_key_to_short_address(onion_pub_key) onion_addr = pub_key_to_onion_address(onion_pub_key) check_delay = RELAY_CLIENT_MIN_DELAY is_online = False session = requests.session() session.proxies = { 'http': f'socks5h://127.0.0.1:{tor_port}', 'https': f'socks5h://127.0.0.1:{tor_port}' } rp_print(f"Connecting to {short_addr}...", bold=True) # When Transmitter Program sends contact under UNENCRYPTED_ADD_EXISTING_CONTACT, this function # receives user's own Onion address: That way it knows to request the contact to add them: if onion_addr_user: while True: try: reply = session.get( f'http://{onion_addr}.onion/contact_request/{onion_addr_user}', timeout=45).text if reply == "OK": break except requests.exceptions.RequestException: time.sleep(RELAY_CLIENT_MIN_DELAY) while True: with ignored(EOFError, KeyboardInterrupt): time.sleep(check_delay) # Obtain URL token # ---------------- # Load URL token public key from contact's Onion Service root domain try: url_token_public_key_hex = session.get( f'http://{onion_addr}.onion/', timeout=45).text except requests.exceptions.RequestException: url_token_public_key_hex = '' # Manage online status of contact based on availability of URL token's public key if url_token_public_key_hex == '': if check_delay < RELAY_CLIENT_MAX_DELAY: check_delay *= 2 if check_delay > CLIENT_OFFLINE_THRESHOLD and is_online: is_online = False rp_print(f"{short_addr} is now offline", bold=True) continue else: check_delay = RELAY_CLIENT_MIN_DELAY if not is_online: is_online = True rp_print(f"{short_addr} is now online", bold=True) # When contact's URL token public key changes, update URL token if url_token_public_key_hex != cached_pk: try: public_key = bytes.fromhex(url_token_public_key_hex) if len(public_key ) != TFC_PUBLIC_KEY_LENGTH or public_key == bytes( TFC_PUBLIC_KEY_LENGTH): raise ValueError shared_secret = url_token_private_key.exchange( X448PublicKey.from_public_bytes(public_key)) url_token = hashlib.blake2b( shared_secret, digest_size=SYMMETRIC_KEY_LENGTH).hexdigest() except (TypeError, ValueError): continue cached_pk = url_token_public_key_hex # Update client's URL token public key queues[URL_TOKEN_QUEUE].put( (onion_pub_key, url_token)) # Update Flask server's URL token for contact # Load TFC data with URL token # ---------------------------- get_data_loop(onion_addr, url_token, short_addr, onion_pub_key, queues, session, gateway) if unittest: break
def onion_service(queues: Dict[bytes, 'Queue[Any]']) -> None: """Manage the Tor Onion Service and control Tor via stem.""" rp_print("Setup 0% - Waiting for Onion Service configuration...", bold=True) while queues[ONION_KEY_QUEUE].qsize() == 0: time.sleep(0.1) private_key, c_code = queues[ONION_KEY_QUEUE].get() # type: bytes, bytes public_key_user = bytes(nacl.signing.SigningKey(seed=private_key).verify_key) onion_addr_user = pub_key_to_onion_address(public_key_user) try: rp_print("Setup 10% - Launching Tor...", bold=True) tor_port = get_available_port(1000, 65535) tor = Tor() tor.connect(tor_port) except (EOFError, KeyboardInterrupt): return if tor.controller is None: raise CriticalError("No Tor controller") try: rp_print("Setup 75% - Launching Onion Service...", bold=True) key_data = stem_compatible_ed25519_key_from_private_key(private_key) response = tor.controller.create_ephemeral_hidden_service(ports={80: 5000}, key_type='ED25519-V3', key_content=key_data, await_publication=True) rp_print("Setup 100% - Onion Service is now published.", bold=True) m_print(["Your TFC account is:", onion_addr_user, '', f"Onion Service confirmation code (to Transmitter): {c_code.hex()}"], box=True) # Allow the client to start looking for contacts at this point. queues[TOR_DATA_QUEUE].put((tor_port, onion_addr_user)) except (KeyboardInterrupt, stem.SocketClosed): tor.stop() return while True: try: time.sleep(0.1) if queues[ONION_KEY_QUEUE].qsize() > 0: _, c_code = queues[ONION_KEY_QUEUE].get() m_print(["Onion Service is already running.", '', f"Onion Service confirmation code (to Transmitter): {c_code.hex()}"], box=True) if queues[ONION_CLOSE_QUEUE].qsize() > 0: command = queues[ONION_CLOSE_QUEUE].get() if not tor.platform_is_tails() and command == EXIT: tor.controller.remove_hidden_service(response.service_id) tor.stop() queues[EXIT_QUEUE].put(command) time.sleep(5) break except (EOFError, KeyboardInterrupt): pass except stem.SocketClosed: tor.controller.remove_hidden_service(response.service_id) tor.stop() break
def get_data_loop(onion_addr: str, url_token: str, short_addr: str, onion_pub_key: bytes, queues: 'QueueDict', session: 'Session', gateway: 'Gateway') -> None: """Load TFC data from contact's Onion Service using valid URL token.""" while True: try: # See if a file is available try: file_data = session.get( f'http://{onion_addr}.onion/{url_token}/files', stream=True).content if file_data: ts = datetime.now() ts_bytes = int_to_bytes( int(ts.strftime('%Y%m%d%H%M%S%f')[:-4])) packet = FILE_DATAGRAM_HEADER + ts_bytes + onion_pub_key + ORIGIN_CONTACT_HEADER + file_data queues[DST_MESSAGE_QUEUE].put(packet) rp_print(f"File from contact {short_addr}", ts) except requests.exceptions.RequestException: pass # See if messages are available try: r = session.get( f'http://{onion_addr}.onion/{url_token}/messages', stream=True) except requests.exceptions.RequestException: return None for line in r.iter_lines( ): # Iterates over newline-separated datagrams if not line: continue try: header, payload = separate_header( line, DATAGRAM_HEADER_LENGTH) # type: bytes, bytes payload_bytes = base64.b85decode(payload) except (UnicodeError, ValueError): continue ts = datetime.now() ts_bytes = int_to_bytes(int( ts.strftime('%Y%m%d%H%M%S%f')[:-4])) if header == PUBLIC_KEY_DATAGRAM_HEADER: if len(payload_bytes) == TFC_PUBLIC_KEY_LENGTH: msg = f"Received public key from {short_addr} at {ts.strftime('%b %d - %H:%M:%S.%f')[:-4]}:" print_key(msg, payload_bytes, gateway.settings, public_key=True) elif header == MESSAGE_DATAGRAM_HEADER: queues[DST_MESSAGE_QUEUE].put(header + ts_bytes + onion_pub_key + ORIGIN_CONTACT_HEADER + payload_bytes) rp_print(f"Message from contact {short_addr}", ts) elif header in [ GROUP_MSG_INVITE_HEADER, GROUP_MSG_JOIN_HEADER, GROUP_MSG_MEMBER_ADD_HEADER, GROUP_MSG_MEMBER_REM_HEADER, GROUP_MSG_EXIT_GROUP_HEADER ]: queues[GROUP_MSG_QUEUE].put( (header, payload_bytes, short_addr)) else: rp_print(f"Received invalid packet from {short_addr}", ts, bold=True) except requests.exceptions.RequestException: break