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 shared_secret = url_token_private_key.exchange( X448PublicKey.from_public_bytes(public_key)) url_token = hashlib.blake2b(shared_secret, digest_size=URL_TOKEN_LENGTH).hexdigest() 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 change_setting(user_input: 'UserInput', window: 'TxWindow', contact_list: 'ContactList', group_list: 'GroupList', settings: 'Settings', queues: 'QueueDict', master_key: 'MasterKey', gateway: 'Gateway') -> None: """Change setting on Transmitter and Receiver Program.""" # Validate the KV-pair try: setting = user_input.plaintext.split()[1] except IndexError: raise SoftError("Error: No setting specified.", head_clear=True) if setting not in (settings.key_list + gateway.settings.key_list): raise SoftError(f"Error: Invalid setting '{setting}'.", head_clear=True) try: value = user_input.plaintext.split()[2] except IndexError: raise SoftError("Error: No value for setting specified.", head_clear=True) relay_settings = dict(serial_error_correction=UNENCRYPTED_EC_RATIO, serial_baudrate=UNENCRYPTED_BAUDRATE, allow_contact_requests=UNENCRYPTED_MANAGE_CONTACT_REQ ) # type: Dict[str, bytes] check_setting_change_conditions(setting, settings, relay_settings, master_key) change_setting_value(setting, value, relay_settings, queues, contact_list, group_list, settings, gateway) propagate_setting_effects(setting, queues, contact_list, group_list, settings, window)
def group_rename(cmd_data: bytes, ts: 'datetime', window_list: 'WindowList', contact_list: 'ContactList', group_list: 'GroupList') -> None: """Rename the group.""" group_id, new_name_bytes = separate_header(cmd_data, GROUP_ID_LENGTH) try: group = group_list.get_group_by_id(group_id) except StopIteration: raise SoftError( f"Error: No group with ID '{b58encode(group_id)}' found.") try: new_name = new_name_bytes.decode() except UnicodeError: raise SoftError( f"Error: New name for group '{group.name}' was invalid.") error_msg = validate_group_name(new_name, contact_list, group_list) if error_msg: raise SoftError(error_msg) old_name = group.name group.name = new_name group_list.store_groups() window = window_list.get_window(group.group_id) window.name = new_name message = f"Renamed group '{old_name}' to '{new_name}'." cmd_win = window_list.get_window(WIN_UID_COMMAND) cmd_win.add_new(ts, message, output=True)
def verify(window: 'TxWindow', contact_list: 'ContactList') -> None: """Verify fingerprints with contact.""" if window.type == WIN_TYPE_GROUP or window.contact is None: raise SoftError("Error: A group is selected.", head_clear=True) if window.contact.uses_psk(): raise SoftError("Pre-shared keys have no fingerprints.", head_clear=True) try: verified = verify_fingerprints(window.contact.tx_fingerprint, window.contact.rx_fingerprint) except (EOFError, KeyboardInterrupt): raise SoftError("Fingerprint verification aborted.", delay=1, head=2, tail_clear=True) status_hr, status = { True: ("Verified", KEX_STATUS_VERIFIED), False: ("Unverified", KEX_STATUS_UNVERIFIED) }[verified] window.contact.kex_status = status contact_list.store_contacts() m_print(f"Marked fingerprints with {window.name} as '{status_hr}'.", bold=True, tail_clear=True, delay=1, tail=1)
def validate_traffic_masking_delay(key: str, value: 'SettingType', contact_list: 'ContactList') -> None: """Validate setting value for traffic masking delays.""" if key in ["tm_static_delay", "tm_random_delay"]: for key_, name, min_setting in [ ("tm_static_delay", "static", TRAFFIC_MASKING_MIN_STATIC_DELAY), ("tm_random_delay", "random", TRAFFIC_MASKING_MIN_RANDOM_DELAY) ]: if key == key_ and value < min_setting: raise SoftError( f"Error: Can't set {name} delay lower than {min_setting}.", head_clear=True) if contact_list.settings.software_operation == TX: m_print([ "WARNING!", "Changing traffic masking delay can make your endpoint and traffic look unique!" ], bold=True, head=1, tail=1) if not yes("Proceed anyway?"): raise SoftError("Aborted traffic masking setting change.", head_clear=True) m_print("Traffic masking setting will change on restart.", head=1, tail=1)
def detect_errors(self, packet: bytes) -> bytes: """Handle received packet error detection and/or correction.""" if self.settings.qubes: try: packet = base64.b85decode(packet) except ValueError: raise SoftError( "Error: Received packet had invalid Base85 encoding.") if self.settings.session_serial_error_correction and not self.settings.qubes: try: packet, _ = self.rs.decode(packet) return bytes(packet) except ReedSolomonError: raise SoftError( "Error: Reed-Solomon failed to correct errors in the received packet.", bold=True) else: packet, checksum = separate_trailer(packet, PACKET_CHECKSUM_LENGTH) if hashlib.blake2b( packet, digest_size=PACKET_CHECKSUM_LENGTH).digest() != checksum: raise SoftError( "Warning! Received packet had an invalid checksum.", bold=True) return packet
def process_assembled_file( ts: 'datetime', # Timestamp last received packet payload: bytes, # File name and content onion_pub_key: bytes, # Onion Service pubkey of sender nick: str, # Nickname of sender settings: 'Settings', # Settings object window_list: 'WindowList', # WindowList object ) -> None: """Process received file assembly packets.""" try: file_name_b, file_data = payload.split(US_BYTE, 1) except ValueError: raise SoftError("Error: Received file had an invalid structure.") try: file_name = file_name_b.decode() except UnicodeError: raise SoftError("Error: Received file name had an invalid encoding.") if not file_name.isprintable() or not file_name or '/' in file_name: raise SoftError("Error: Received file had an invalid name.") file_ct, file_key = separate_trailer(file_data, SYMMETRIC_KEY_LENGTH) if len(file_key) != SYMMETRIC_KEY_LENGTH: raise SoftError("Error: Received file had an invalid key.") decrypt_and_store_file(ts, file_ct, file_key, file_name, onion_pub_key, nick, window_list, settings)
def validate_contact_public_key(tfc_public_key_contact: bytes) -> None: """This function validates the public key from contact. The validation takes into account key state and it will detect if the public key is zero, but it can't predict whether the shared key will be zero. Further validation of the public key is done by the `src.common.crypto` module. """ 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 SoftError("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 SoftError("Error: Zero public key", output=False)
def rxp_load_psk(window: 'TxWindow', contact_list: 'ContactList', settings: 'Settings', queues: 'QueueDict', ) -> None: """Send command to Receiver Program to load PSK for active contact.""" if settings.traffic_masking: raise SoftError("Error: Command is disabled during traffic masking.", head_clear=True) if window.type == WIN_TYPE_GROUP or window.contact is None: raise SoftError("Error: Group is selected.", head_clear=True) if not contact_list.get_contact_by_pub_key(window.uid).uses_psk(): raise SoftError(f"Error: The current key was exchanged with {ECDHE}.", head_clear=True) c_code = blake2b(window.uid, digest_size=CONFIRM_CODE_LENGTH) command = KEY_EX_PSK_RX + c_code + window.uid queue_command(command, settings, queues) while True: try: purp_code = ask_confirmation_code('Receiver') if purp_code == c_code.hex(): window.contact.kex_status = KEX_STATUS_HAS_RX_PSK contact_list.store_contacts() raise SoftError(f"Removed PSK reminder for {window.name}.", tail_clear=True, delay=1) m_print("Incorrect confirmation code.", head=1) print_on_previous_line(reps=4, delay=2) except (EOFError, KeyboardInterrupt): raise SoftError("PSK install verification aborted.", tail_clear=True, delay=1, head=2)
def change_nick(user_input: 'UserInput', window: 'TxWindow', contact_list: 'ContactList', group_list: 'GroupList', settings: 'Settings', queues: 'QueueDict') -> None: """Change nick of contact.""" try: nick = user_input.plaintext.split()[1] except IndexError: raise SoftError("Error: No nick specified.", head_clear=True) if window.type == WIN_TYPE_GROUP: group_rename(nick, window, contact_list, group_list, settings, queues) if window.contact is None: raise SoftError("Error: Window does not have contact.") onion_pub_key = window.contact.onion_pub_key error_msg = validate_nick(nick, (contact_list, group_list, onion_pub_key)) if error_msg: raise SoftError(error_msg, head_clear=True) window.contact.nick = nick window.name = nick contact_list.store_contacts() command = CH_NICKNAME + onion_pub_key + nick.encode() queue_command(command, settings, queues)
def ch_setting(cmd_data: bytes, ts: 'datetime', window_list: 'WindowList', contact_list: 'ContactList', group_list: 'GroupList', key_list: 'KeyList', settings: 'Settings', gateway: 'Gateway' ) -> None: """Change TFC setting.""" try: setting, value = [f.decode() for f in cmd_data.split(US_BYTE)] except ValueError: raise SoftError("Error: Received invalid setting data.") if setting in settings.key_list: settings.change_setting(setting, value, contact_list, group_list) elif setting in gateway.settings.key_list: gateway.settings.change_setting(setting, value) else: raise SoftError(f"Error: Invalid setting '{setting}'.") cmd_win = window_list.get_command_window() cmd_win.add_new(ts, f"Changed setting '{setting}' to '{value}'.", output=True) if setting == 'max_number_of_contacts': contact_list.store_contacts() key_list.store_keys() if setting in ['max_number_of_group_members', 'max_number_of_groups']: group_list.store_groups()
def process_file_key_message( assembled: bytes, # File decryption key onion_pub_key: bytes, # Onion address of associated contact origin: bytes, # Origin of file key packet (user / contact) contact_list: 'ContactList', # ContactList object file_keys: Dict[ bytes, bytes] # Dictionary of file identifiers and decryption keys ) -> str: """Process received file key delivery message.""" if origin == ORIGIN_USER_HEADER: raise SoftError("File key message from the user.", output=False) try: decoded = base64.b85decode(assembled) except ValueError: raise SoftError("Error: Received an invalid file key message.") ct_hash, file_key = separate_header(decoded, BLAKE2_DIGEST_LENGTH) if len(ct_hash) != BLAKE2_DIGEST_LENGTH or len( file_key) != SYMMETRIC_KEY_LENGTH: raise SoftError("Error: Received an invalid file key message.") file_keys[onion_pub_key + ct_hash] = file_key nick = contact_list.get_nick_by_pub_key(onion_pub_key) return nick
def determine_selector(selection: str, contact_list: 'ContactList', group_list: 'GroupList') -> bytes: """Determine selector (group ID or Onion Service public key).""" if selection in contact_list.contact_selectors(): selector = contact_list.get_contact_by_address_or_nick( selection).onion_pub_key elif selection in group_list.get_list_of_group_names(): selector = group_list.get_group(selection).group_id elif len(selection) == ONION_ADDRESS_LENGTH: if validate_onion_addr(selection): raise SoftError("Error: Invalid account.", head_clear=True) selector = onion_address_to_pub_key(selection) elif len(selection) == GROUP_ID_ENC_LENGTH: try: selector = b58decode(selection) except ValueError: raise SoftError("Error: Invalid group ID.", head_clear=True) else: raise SoftError("Error: Unknown selector.", head_clear=True) return selector
def new_file( ts: 'datetime', # Timestamp of received packet packet: bytes, # Sender of file and file ciphertext file_keys: Dict[bytes, bytes], # Dictionary for file decryption keys file_buf: Dict[bytes, Tuple['datetime', bytes]], # Dictionary for cached file ciphertexts contact_list: 'ContactList', # ContactList object window_list: 'WindowList', # WindowList object settings: 'Settings' # Settings object ) -> None: """Validate received file and process or cache it.""" onion_pub_key, _, file_ct = separate_headers( packet, [ONION_SERVICE_PUBLIC_KEY_LENGTH, ORIGIN_HEADER_LENGTH]) if not contact_list.has_pub_key(onion_pub_key): raise SoftError("File from an unknown account.", output=False) contact = contact_list.get_contact_by_pub_key(onion_pub_key) if not contact.file_reception: raise SoftError( f"Alert! Discarded file from {contact.nick} as file reception for them is disabled.", bold=True) k = onion_pub_key + blake2b(file_ct) # Dictionary key if k in file_keys: decryption_key = file_keys[k] process_file(ts, onion_pub_key, file_ct, decryption_key, contact_list, window_list, settings) file_keys.pop(k) else: file_buf[k] = (ts, file_ct)
def send_onion_service_key(contact_list: 'ContactList', settings: 'Settings', onion_service: 'OnionService', gateway: 'Gateway') -> None: """Resend Onion Service key to Relay Program on Networked Computer. This command is used in cases where Relay Program had to be restarted for some reason (e.g. due to system updates). """ try: if settings.traffic_masking: m_print([ "Warning!", "Exporting Onion Service data to Networked Computer ", "during traffic masking can reveal to an adversary ", "TFC is being used at the moment. You should only do ", "this if you've had to restart the Relay Program." ], bold=True, head=1, tail=1) if not yes("Proceed with the Onion Service data export?", abort=False): raise SoftError("Onion Service data export canceled.", tail_clear=True, delay=1, head=0) export_onion_service_data(contact_list, settings, onion_service, gateway) except (EOFError, KeyboardInterrupt): raise SoftError("Onion Service data export canceled.", tail_clear=True, delay=1, head=2)
def group_rename( new_name: str, window: 'TxWindow', contact_list: 'ContactList', group_list: 'GroupList', settings: 'Settings', queues: 'QueueDict', ) -> None: """Rename the active group.""" if window.type == WIN_TYPE_CONTACT or window.group is None: raise SoftError("Error: Selected window is not a group window.", head_clear=True) error_msg = validate_group_name(new_name, contact_list, group_list) if error_msg: raise SoftError(error_msg, head_clear=True) command = GROUP_RENAME + window.uid + new_name.encode() queue_command(command, settings, queues) old_name = window.group.name window.group.name = new_name group_list.store_groups() raise SoftError(f"Renamed group '{old_name}' to '{new_name}'.", delay=1, tail_clear=True)
def decrypt_and_store_file( ts: 'datetime', # Timestamp of received packet file_ct: bytes, # File ciphertext file_key: bytes, # File decryption key file_name: str, # Name of the file onion_pub_key: bytes, # Onion Service pubkey of sender nick: str, # Nickname of sender window_list: 'WindowList', # WindowList object settings: 'Settings' # Settings object ) -> None: """Decrypt and store file.""" try: file_pt = auth_and_decrypt(file_ct, file_key) except nacl.exceptions.CryptoError: raise SoftError("Error: Decryption of file data failed.") try: file_dc = decompress(file_pt, settings.max_decompress_size) except zlib.error: raise SoftError("Error: Decompression of file data failed.") file_dir = f'{DIR_RECV_FILES}{nick}/' final_name = store_unique(file_dc, file_dir, file_name) message = f"Stored file from {nick} as '{final_name}'." if settings.traffic_masking and window_list.active_win is not None: window = window_list.active_win else: window = window_list.get_window(onion_pub_key) window.add_new(ts, message, onion_pub_key, output=True, event_msg=True)
def group_add_member(group_name: str, purp_members: List['bytes'], contact_list: 'ContactList', group_list: 'GroupList', settings: 'Settings', queues: 'QueueDict', master_key: 'MasterKey', _: Optional[bytes] = None) -> None: """Add new member(s) to a specified group.""" if group_name not in group_list.get_list_of_group_names(): if not yes(f"Group {group_name} was not found. Create new group?", abort=False, head=1): raise SoftError("Group creation aborted.", head=0, delay=1, tail_clear=True) group_create(group_name, purp_members, contact_list, group_list, settings, queues, master_key) return None purp_pub_keys = set(purp_members) pub_keys = set(contact_list.get_list_of_pub_keys()) before_adding = set( group_list.get_group(group_name).get_list_of_member_pub_keys()) ok_pub_keys_set = set(pub_keys & purp_pub_keys) new_in_group_set = set(ok_pub_keys_set - before_adding) end_assembly = list(before_adding | new_in_group_set) rejected = list(purp_pub_keys - pub_keys) already_in_g = list(before_adding & purp_pub_keys) new_in_group = list(new_in_group_set) ok_pub_keys = list(ok_pub_keys_set) if len(end_assembly) > settings.max_number_of_group_members: raise SoftError( f"Error: TFC settings only allow {settings.max_number_of_group_members} members per group.", head_clear=True) group = group_list.get_group(group_name) group.add_members( [contact_list.get_contact_by_pub_key(k) for k in new_in_group]) command = GROUP_ADD + group.group_id + b''.join(ok_pub_keys) queue_command(command, settings, queues) group_management_print(ADDED_MEMBERS, new_in_group, contact_list, group_name) group_management_print(ALREADY_MEMBER, already_in_g, contact_list, group_name) group_management_print(UNKNOWN_ACCOUNTS, rejected, contact_list, group_name) if new_in_group: if yes("Publish the list of new members to involved?", abort=False): add_packet = (GROUP_MSG_MEMBER_ADD_HEADER + group.group_id + int_to_bytes(len(before_adding)) + b''.join(before_adding) + b''.join(new_in_group)) queue_to_nc(add_packet, queues[RELAY_PACKET_QUEUE])
def log_command(user_input: 'UserInput', window: 'TxWindow', contact_list: 'ContactList', group_list: 'GroupList', settings: 'Settings', queues: 'QueueDict', master_key: 'MasterKey') -> None: """Display message logs or export them to plaintext file on TCBs. Transmitter Program processes sent, Receiver Program sent and received, messages of all participants in the active window. Having the capability to export the log file from the encrypted database is a bad idea, but as it's required by the GDPR (https://gdpr-info.eu/art-20-gdpr/), it should be done as securely as possible. Therefore, before allowing export, TFC will ask for the master password to ensure no unauthorized user who gains momentary access to the system can the export logs from the database. """ cmd = user_input.plaintext.split()[0] export, header = dict(export=(True, LOG_EXPORT), history=(False, LOG_DISPLAY))[cmd] try: msg_to_load = int(user_input.plaintext.split()[1]) except ValueError: raise SoftError("Error: Invalid number of messages.", head_clear=True) except IndexError: msg_to_load = 0 try: command = header + int_to_bytes(msg_to_load) + window.uid except struct.error: raise SoftError("Error: Invalid number of messages.", head_clear=True) if export and not yes(f"Export logs for '{window.name}' in plaintext?", abort=False): raise SoftError("Log file export aborted.", tail_clear=True, head=0, delay=1) authenticated = master_key.authenticate_action( ) if settings.ask_password_for_log_access else True if authenticated: queue_command(command, settings, queues) access_logs(window, contact_list, group_list, settings, master_key, msg_to_load, export=export) if export: raise SoftError( f"Exported log file of {window.type} '{window.name}'.", head_clear=True)
def validate_serial_interface_value(self, key: str, json_dict: Any) -> None: """Validate the serial interface setting value.""" if not isinstance(json_dict[key], str): self.invalid_setting(key, json_dict) raise SoftError("Invalid value", output=False) if not any(json_dict[key] == f for f in os.listdir('/sys/class/tty')): self.invalid_setting(key, json_dict) raise SoftError("Invalid value", output=False)
def check_long_packet(self) -> None: """Check if the long packet has permission to be extended.""" if not self.long_active: self.add_masking_packet_to_log_file() raise SoftError("Missing start packet.", output=False) if self.type == FILE and not self.contact.file_reception: self.add_masking_packet_to_log_file(increase=len(self.assembly_pt_list) + 1) self.clear_assembly_packets() raise SoftError("Alert! File reception disabled mid-transfer.")
def process_group_message( ts: 'datetime', # Timestamp of group message assembled: bytes, # Group message and its headers onion_pub_key: bytes, # Onion address of associated contact origin: bytes, # Origin of group message (user / contact) whisper: bool, # When True, message is not logged. group_list: 'GroupList', # GroupList object window_list: 'WindowList' # WindowList object ) -> bool: """Process a group message.""" group_id, assembled = separate_header(assembled, GROUP_ID_LENGTH) if not group_list.has_group_id(group_id): raise SoftError("Error: Received message to an unknown group.", output=False) group = group_list.get_group_by_id(group_id) if not group.has_member(onion_pub_key): raise SoftError("Error: Account is not a member of the group.", output=False) group_msg_id, group_message = separate_header(assembled, GROUP_MSG_ID_LENGTH) try: group_message_str = group_message.decode() except UnicodeError: raise SoftError("Error: Received an invalid group message.") window = window_list.get_window(group.group_id) # All copies of group messages the user sends to members contain # the same message ID. This allows the Receiver Program to ignore # duplicates of outgoing messages sent by the user to each member. if origin == ORIGIN_USER_HEADER: if window.group_msg_id != group_msg_id: window.group_msg_id = group_msg_id window.add_new(ts, group_message_str, onion_pub_key, origin, output=True, whisper=whisper) elif origin == ORIGIN_CONTACT_HEADER: window.add_new(ts, group_message_str, onion_pub_key, origin, output=True, whisper=whisper) # Return the group's logging setting because it might be different # from the logging setting of the contact who sent group message. return group.log_messages
def test_function_return(self) -> None: error = SoftError('test message') self.assertEqual(error.message, 'test message') error = SoftError('test message', head_clear=True) self.assertEqual(error.message, 'test message') error = SoftError('test message', tail_clear=True) self.assertEqual(error.message, 'test message') error = SoftError('test message', window=RxWindow()) self.assertEqual(error.message, 'test message')
def queue_file(window: 'TxWindow', settings: 'Settings', queues: 'QueueDict') -> None: """Ask file path and load file data. In TFC there are two ways to send a file. For traffic masking, the file is loaded and sent inside normal messages using assembly packet headers dedicated for file transmission. This transmission is much slower, so the File object will determine metadata about the transmission's estimated transfer time, number of packets and the name and size of file. This information is inserted to the first assembly packet so that the recipient can observe the transmission progress from file transfer window. When traffic masking is disabled, file transmission is much faster as the file is only encrypted and transferred over serial once before the Relay Program multi-casts the ciphertext to each specified recipient. See the send_file docstring (below) for more details. """ path = ask_path_gui("Select file to send...", settings, get_file=True) if path.endswith( ('tx_contacts', 'tx_groups', 'tx_keys', 'tx_login_data', 'tx_settings', 'rx_contacts', 'rx_groups', 'rx_keys', 'rx_login_data', 'rx_settings', 'tx_serial_settings.json', 'nc_serial_settings.json', 'rx_serial_settings.json', 'tx_onion_db')): raise SoftError("Error: Can't send TFC database.", head_clear=True) if not settings.traffic_masking: send_file(path, settings, queues, window) return file = File(path, window, settings) assembly_packets = split_to_assembly_packets(file.plaintext, FILE) if settings.confirm_sent_files: try: if not yes( f"Send {file.name.decode()} ({file.size_hr}) to {window.type_print} {window.name} " f"({len(assembly_packets)} packets, time: {file.time_hr})?" ): raise SoftError("File selection aborted.", head_clear=True) except (EOFError, KeyboardInterrupt): raise SoftError("File selection aborted.", head_clear=True) queue_assembly_packets(assembly_packets, FILE, settings, queues, window, log_as_ph=True)
def process_message_packet( ts: 'datetime', # Timestamp of received message packet assembly_packet_ct: bytes, # Encrypted assembly packet window_list: 'WindowList', # WindowList object packet_list: 'PacketList', # PacketList object contact_list: 'ContactList', # ContactList object key_list: 'KeyList', # KeyList object group_list: 'GroupList', # GroupList object settings: 'Settings', # Settings object file_keys: Dict[bytes, bytes], # Dictionary of file decryption keys message_log: 'MessageLog', # MessageLog object ) -> None: """Process received message packet.""" command_window = window_list.get_command_window() onion_pub_key, origin, assembly_packet_ct = separate_headers( assembly_packet_ct, [ONION_SERVICE_PUBLIC_KEY_LENGTH, ORIGIN_HEADER_LENGTH]) if onion_pub_key == LOCAL_PUBKEY: raise SoftError("Warning! Received packet masqueraded as a command.", window=command_window) if origin not in [ORIGIN_USER_HEADER, ORIGIN_CONTACT_HEADER]: raise SoftError("Error: Received packet had an invalid origin-header.", window=command_window) assembly_packet = decrypt_assembly_packet(assembly_packet_ct, onion_pub_key, origin, window_list, contact_list, key_list) p_type = (FILE if assembly_packet[:ASSEMBLY_PACKET_HEADER_LENGTH].isupper() else MESSAGE) packet = packet_list.get_packet(onion_pub_key, origin, p_type) logging = contact_list.get_contact_by_pub_key(onion_pub_key).log_messages try: packet.add_packet(assembly_packet) except SoftError: log_masking_packets(onion_pub_key, origin, logging, settings, packet, message_log) raise log_masking_packets(onion_pub_key, origin, logging, settings, packet, message_log) if packet.is_complete: process_complete_message_packet(ts, onion_pub_key, p_type, origin, logging, packet, window_list, contact_list, group_list, settings, message_log, file_keys)
def remove_contact(user_input: 'UserInput', window: 'TxWindow', contact_list: 'ContactList', group_list: 'GroupList', settings: 'Settings', queues: 'QueueDict', master_key: 'MasterKey') -> None: """Remove contact from TFC.""" if settings.traffic_masking: raise SoftError("Error: Command is disabled during traffic masking.", head_clear=True) try: selection = user_input.plaintext.split()[1] except IndexError: raise SoftError("Error: No account specified.", head_clear=True) if not yes(f"Remove contact '{selection}'?", abort=False, head=1): raise SoftError("Removal of contact aborted.", head=0, delay=1, tail_clear=True) if selection in contact_list.contact_selectors(): onion_pub_key = contact_list.get_contact_by_address_or_nick( selection).onion_pub_key else: if validate_onion_addr(selection): raise SoftError("Error: Invalid selection.", head=0, delay=1, tail_clear=True) onion_pub_key = onion_address_to_pub_key(selection) receiver_command = CONTACT_REM + onion_pub_key queue_command(receiver_command, settings, queues) with ignored(SoftError): remove_logs(contact_list, group_list, settings, master_key, onion_pub_key) queues[KEY_MANAGEMENT_QUEUE].put((KDB_REMOVE_ENTRY_HEADER, onion_pub_key)) relay_command = UNENCRYPTED_DATAGRAM_HEADER + UNENCRYPTED_REM_CONTACT + onion_pub_key queue_to_nc(relay_command, queues[RELAY_PACKET_QUEUE]) target = determine_target(selection, onion_pub_key, contact_list) if any([g.remove_members([onion_pub_key]) for g in group_list]): m_print(f"Removed {target} from group(s).", tail=1) check_for_window_deselection(onion_pub_key, window, group_list)
def assemble_command_packet(self) -> bytes: """Assemble command packet.""" padded = b''.join([p[ASSEMBLY_PACKET_HEADER_LENGTH:] for p in self.assembly_pt_list]) payload = rm_padding_bytes(padded) if len(self.assembly_pt_list) > 1: payload, cmd_hash = separate_trailer(payload, BLAKE2_DIGEST_LENGTH) if blake2b(payload) != cmd_hash: raise SoftError("Error: Received an invalid command.") try: return decompress(payload, self.settings.max_decompress_size) except zlib.error: raise SoftError("Error: Decompression of command failed.")
def contact_setting(user_input: 'UserInput', window: 'TxWindow', contact_list: 'ContactList', group_list: 'GroupList', settings: 'Settings', queues: 'QueueDict') -> None: """\ Change logging, file reception, or notification setting of a group or (all) contact(s). """ try: parameters = user_input.plaintext.split() cmd_key = parameters[0] cmd_header = { LOGGING: CH_LOGGING, STORE: CH_FILE_RECV, NOTIFY: CH_NOTIFY }[cmd_key] setting, b_value = dict(on=(ENABLE, True), off=(DISABLE, False))[parameters[1]] except (IndexError, KeyError): raise SoftError("Error: Invalid command.", head_clear=True) # If second parameter 'all' is included, apply setting for all contacts and groups try: win_uid = b'' if parameters[2] == ALL: cmd_value = setting.upper() else: raise SoftError("Error: Invalid command.", head_clear=True) except IndexError: win_uid = window.uid cmd_value = setting + win_uid if win_uid: change_setting_for_selected_contact(cmd_key, b_value, window, contact_list, group_list) else: change_setting_for_all_contacts(cmd_key, b_value, contact_list, group_list) command = cmd_header + cmd_value if settings.traffic_masking and cmd_key == LOGGING: # Send `log_writer_loop` the new logging setting that is loaded # when the next noise packet is loaded from `noise_packet_loop`. queues[LOG_SETTING_QUEUE].put(b_value) window.update_log_messages() queue_command(command, settings, queues)
def group_rm_group(group_name: str, contact_list: 'ContactList', group_list: 'GroupList', settings: 'Settings', queues: 'QueueDict', master_key: 'MasterKey', _: Optional[bytes] = None) -> None: """Remove the group with its members.""" if not yes(f"Remove group '{group_name}'?", abort=False): raise SoftError("Group removal aborted.", head=0, delay=1, tail_clear=True) if group_name in group_list.get_list_of_group_names(): group_id = group_list.get_group(group_name).group_id else: try: group_id = b58decode(group_name) except ValueError: raise SoftError("Error: Invalid group name/ID.", head_clear=True) command = LOG_REMOVE + group_id queue_command(command, settings, queues) command = GROUP_DELETE + group_id queue_command(command, settings, queues) if group_list.has_group(group_name): with ignored(SoftError): remove_logs(contact_list, group_list, settings, master_key, group_id) else: raise SoftError(f"Transmitter has no group '{group_name}' to remove.") group = group_list.get_group(group_name) if not group.empty() and yes("Notify members about leaving the group?", abort=False): exit_packet = (GROUP_MSG_EXIT_GROUP_HEADER + group.group_id + b''.join(group.get_list_of_member_pub_keys())) queue_to_nc(exit_packet, queues[RELAY_PACKET_QUEUE]) group_list.remove_group_by_name(group_name) raise SoftError(f"Removed group '{group_name}'.", head=0, delay=1, tail_clear=True, bold=True)
def validate_contact_fingerprint(tx_fp: bytes, rx_fp: bytes) -> bytes: """Validate or skip validation of contact fingerprint. This function prompts the user to verify the fingerprint of the contact. If the user issues Ctrl+{C,D} command, this function will set the key exchange status as unverified. """ 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 SoftError("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, tail=1) kex_status = KEX_STATUS_UNVERIFIED return kex_status