def clear_screens(user_input: 'UserInput', window: 'TxWindow', settings: 'Settings', queues: Dict[bytes, 'Queue']) -> None: """Clear/reset TxM, RxM and NH screens. Only send unencrypted command to NH if traffic masking is disabled and if some related IM account can be bound to active window. Since reset command removes ephemeral message log on RxM, TxM decides the window to reset (in case e.g. previous window selection command packet dropped and active window state is inconsistent between TxM/RxM). """ cmd = user_input.plaintext.split()[0] command = CLEAR_SCREEN_HEADER if cmd == CLEAR else RESET_SCREEN_HEADER + window.uid.encode( ) queue_command(command, settings, queues[COMMAND_PACKET_QUEUE]) clear_screen() if not settings.session_traffic_masking and window.imc_name is not None: im_window = window.imc_name.encode() pt_cmd = UNENCRYPTED_SCREEN_CLEAR if cmd == CLEAR else UNENCRYPTED_SCREEN_RESET packet = UNENCRYPTED_PACKET_HEADER + pt_cmd + im_window queue_to_nh(packet, settings, queues[NH_PACKET_QUEUE]) if cmd == RESET: os.system('reset')
def select_tx_window( self, settings: 'Settings', # Settings object queues: 'QueueDict', # Dictionary of Queues onion_service: 'OnionService', # OnionService object gateway: 'Gateway', # Gateway object selection: Optional[str] = None, # Selector for window cmd: bool = False # True when `/msg` command is used to switch window ) -> None: """Select specified window or ask the user to specify one.""" if selection is None: selection = self.select_recipient() if selection in self.group_list.get_list_of_group_names(): self.select_group(selection, cmd, settings) elif selection in self.contact_list.contact_selectors(): self.select_contact(selection, cmd, queues, settings) elif selection.startswith("/"): self.window_selection_command(selection, settings, queues, onion_service, gateway) else: raise SoftError("Error: No contact/group was found.") if settings.traffic_masking: queues[WINDOW_SELECT_QUEUE].put(self.window_contacts) packet = WIN_SELECT + self.uid queue_command(packet, settings, queues) clear_screen()
def load_master_key(self) -> bytes: """Derive the master key from password and salt. Load the salt, hash, and key derivation settings from the login database. Derive the purported master key from the salt and entered password. If the BLAKE2b hash of derived master key matches the hash in the login database, accept the derived master key. """ database_data = self.database.load_database() if len(database_data) != MASTERKEY_DB_SIZE: raise CriticalError(f"Invalid {self.file_name} database size.") salt, key_hash, time_bytes, memory_bytes, parallelism_bytes \ = separate_headers(database_data, [ARGON2_SALT_LENGTH, BLAKE2_DIGEST_LENGTH, ENCODED_INTEGER_LENGTH, ENCODED_INTEGER_LENGTH]) time_cost = bytes_to_int(time_bytes) memory_cost = bytes_to_int(memory_bytes) parallelism = bytes_to_int(parallelism_bytes) while True: password = MasterKey.get_password() phase("Deriving master key", head=2, offset=len("Password correct")) purp_key = argon2_kdf(password, salt, time_cost, memory_cost, parallelism) if blake2b(purp_key) == key_hash: phase("Password correct", done=True, delay=1) clear_screen() return purp_key phase("Invalid password", done=True, delay=1) print_on_previous_line(reps=5)
def change_master_key(ts: 'datetime', window_list: 'WindowList', contact_list: 'ContactList', group_list: 'GroupList', key_list: 'KeyList', settings: 'Settings', master_key: 'MasterKey') -> None: """Prompt user for new master password and derive new master key from that.""" try: old_master_key = master_key.master_key[:] master_key.new_master_key() phase("Re-encrypting databases") ensure_dir(DIR_USER_DATA) file_name = f'{DIR_USER_DATA}{settings.software_operation}_logs' if os.path.isfile(file_name): re_encrypt(old_master_key, master_key.master_key, settings) key_list.store_keys() settings.store_settings() contact_list.store_contacts() group_list.store_groups() phase(DONE) box_print("Master key successfully changed.", head=1) clear_screen(delay=1.5) local_win = window_list.get_window(LOCAL_ID) local_win.add_new(ts, "Changed RxM master key.") except KeyboardInterrupt: raise FunctionReturn("Password change aborted.", delay=1, head=3, tail_clear=True)
def add_psk_tx_keys(cmd_data: bytes, ts: 'datetime', window_list: 'WindowList', contact_list: 'ContactList', key_list: 'KeyList', settings: 'Settings', pubkey_buf: Dict[str, bytes]) -> None: """Add contact and Tx-PSKs.""" tx_key = cmd_data[0:32] tx_hek = cmd_data[32:64] account, nick = [f.decode() for f in cmd_data[64:].split(US_BYTE)] contact_list.add_contact(account, DUMMY_USER, nick, bytes(FINGERPRINT_LEN), bytes(FINGERPRINT_LEN), settings.log_messages_by_default, settings.accept_files_by_default, settings.show_notifications_by_default) # The Rx-side keys are set as null-byte strings to indicate they have not # been added yet. This does not allow existential forgeries as # decrypt_assembly_packet does not allow use of zero-keys for decryption. key_list.add_keyset(account, tx_key=tx_key, rx_key=bytes(KEY_LENGTH), tx_hek=tx_hek, rx_hek=bytes(KEY_LENGTH)) pubkey_buf.pop(account, None) message = f"Added Tx-PSK for {nick} ({account})." local_win = window_list.get_window(LOCAL_ID) local_win.add_new(ts, message) box_print(message) clear_screen(delay=1)
def add_x25519_keys(packet: bytes, ts: 'datetime', window_list: 'WindowList', contact_list: 'ContactList', key_list: 'KeyList', settings: 'Settings', pubkey_buf: Dict[str, bytes]) -> None: """Add contact and their X25519 keys.""" tx_key = packet[0:32] tx_hek = packet[32:64] rx_key = packet[64:96] rx_hek = packet[96:128] account, nick = [f.decode() for f in packet[128:].split(US_BYTE)] contact_list.add_contact(account, DUMMY_USER, nick, bytes(FINGERPRINT_LEN), bytes(FINGERPRINT_LEN), settings.log_messages_by_default, settings.accept_files_by_default, settings.show_notifications_by_default) key_list.add_keyset(account, tx_key, rx_key, tx_hek, rx_hek) pubkey_buf.pop(account, None) message = f"Added X25519 keys for {nick} ({account})." local_win = window_list.get_window(LOCAL_ID) local_win.add_new(ts, message) box_print(message) clear_screen(delay=1)
def print_about() -> None: """Print URLs that direct to TFC's project site and documentation.""" clear_screen() print(f"\n Tinfoil Chat {VERSION}\n\n" " Website: https://github.com/maqp/tfc/\n" " Wikipage: https://github.com/maqp/tfc/wiki\n" " White paper: https://cs.helsinki.fi/u/oottela/tfc.pdf\n")
def print_contacts(self) -> None: """Print list of contacts.""" # Columns c1 = ['Contact'] c2 = ['Logging'] c3 = ['Notify'] c4 = ['Files '] c5 = ['Key Ex'] c6 = ['Account'] for c in self.get_list_of_contacts(): c1.append(c.nick) c2.append('Yes' if c.log_messages else 'No') c3.append('Yes' if c.notifications else 'No') c4.append('Accept' if c.file_reception else 'Reject') c5.append('PSK' if c.tx_fingerprint == bytes(FINGERPRINT_LEN) else 'X25519') c6.append(c.rx_account) lst = [] for nick, log_setting, notify_setting, file_rcv_setting, key_exchange, account in zip(c1, c2, c3, c4, c5, c6): lst.append('{0:{1}} {2:{3}} {4:{5}} {6:{7}} {8:{9}} {10}'.format( nick, max(len(v) for v in c1) + CONTACT_LIST_INDENT, log_setting, max(len(v) for v in c2) + CONTACT_LIST_INDENT, notify_setting, max(len(v) for v in c3) + CONTACT_LIST_INDENT, file_rcv_setting, max(len(v) for v in c4) + CONTACT_LIST_INDENT, key_exchange, max(len(v) for v in c5) + CONTACT_LIST_INDENT, account, max(len(v) for v in c6) + CONTACT_LIST_INDENT)) lst.insert(1, get_terminal_width() * '─') clear_screen() print('\n' + '\n'.join(lst) + '\n\n')
def process_public_key(ts: 'datetime', packet: bytes, window_list: 'WindowList', settings: 'Settings', pubkey_buf: Dict[str, bytes]) -> None: """Display contact's public key and add it to buffer.""" pub_key = packet[1:33] origin = packet[33:34] try: account = packet[34:].decode() except UnicodeError: raise FunctionReturn( "Error! Account for received public key had invalid encoding.") if origin not in [ORIGIN_CONTACT_HEADER, ORIGIN_USER_HEADER]: raise FunctionReturn( "Error! Received public key had an invalid origin header.") if origin == ORIGIN_CONTACT_HEADER: pubkey_buf[account] = pub_key print_key(f"Received public key from {account}:", pub_key, settings) local_win = window_list.get_local_window() pub_key_b58 = ' '.join( split_string(b58encode(pub_key), item_len=(51 if settings.local_testing_mode else 3))) local_win.add_new( ts, f"Received public key from {account}: {pub_key_b58}") elif origin == ORIGIN_USER_HEADER and account in pubkey_buf: clear_screen() print_key(f"Public key for {account}:", pubkey_buf[account], settings)
def get_b58_key(key_type: str, # The type of Base58 key to be entered settings: 'Settings', # Settings object short_address: str = '' # The contact's short Onion address ) -> bytes: # The Base58 decoded key """Ask the user to input a Base58 encoded key.""" if key_type == B58_PUBLIC_KEY: clear_screen() m_print(f"{ECDHE} key exchange", head=1, tail=1, bold=True) m_print("If needed, resend your public key to the contact by pressing <Enter>", tail=1) box_msg = f"Enter public key of {short_address} (from Relay)" elif key_type == B58_LOCAL_KEY: box_msg = "Enter local key decryption key (from Transmitter)" else: raise CriticalError("Invalid key type") while True: rx_pk = box_input(box_msg, key_type=key_type, guide=not (settings.local_testing_mode or settings.qubes)) rx_pk = ''.join(rx_pk.split()) if key_type == B58_PUBLIC_KEY and rx_pk == '': return rx_pk.encode() try: return b58decode(rx_pk, public_key=(key_type == B58_PUBLIC_KEY)) except ValueError: m_print("Checksum error - Check that the entered key is correct.") print_on_previous_line(reps=(4 if settings.local_testing_mode else 5), delay=1) if key_type == B58_PUBLIC_KEY and len(rx_pk) == ENCODED_B58_PUB_KEY_LENGTH: raise ValueError(rx_pk)
def print_logs(message_list: List[MsgTuple], export: bool, msg_to_load: int, window: Union['TxWindow', 'RxWindow'], contact_list: 'ContactList', group_list: 'GroupList', settings: 'Settings' ) -> None: """Print list of logged messages to screen or export them to file.""" terminal_width = get_terminal_width() system, m_dir = {TX: ("Transmitter", "sent to"), RX: ("Receiver", "to/from")}[settings.software_operation] f_name = open(f"{system} - Plaintext log ({window.name})", 'w+') if export else sys.stdout subset = '' if msg_to_load == 0 else f"{msg_to_load} most recent " title = textwrap.fill(f"Log file of {subset}message(s) {m_dir} {window.type} {window.name}", terminal_width) packet_list = PacketList(settings, contact_list) log_window = RxWindow(window.uid, contact_list, group_list, settings, packet_list) log_window.is_active = True log_window.message_log = message_list if message_list: if not export: clear_screen() print(title, file=f_name) print(terminal_width * '═', file=f_name) log_window.redraw( file=f_name) print("<End of log file>\n", file=f_name) else: raise FunctionReturn(f"No logged messages for {window.type} '{window.name}'.", head_clear=True) if export: f_name.close()
def ensure_im_connection() -> None: """\ Check that nh.py has connection to Pidgin before launching other processes. """ phase("Waiting for enabled account in Pidgin", offset=1) while True: try: bus = dbus.SessionBus(private=True) obj = bus.get_object("im.pidgin.purple.PurpleService", "/im/pidgin/purple/PurpleObject") purple = dbus.Interface(obj, "im.pidgin.purple.PurpleInterface") while not purple.PurpleAccountsGetAllActive(): time.sleep(0.01) phase('OK', done=True) accounts = [] for a in purple.PurpleAccountsGetAllActive(): accounts.append(purple.PurpleAccountGetUsername(a)[:-1]) just_len = len(max(accounts, key=len)) justified = ["Active accounts in Pidgin:"] + [ "* {}".format(a.ljust(just_len)) for a in accounts ] box_print(justified, head=1, tail=1) return None except (IndexError, dbus.exceptions.DBusException): continue except (EOFError, KeyboardInterrupt): clear_screen() exit()
def clear_screens(user_input: 'UserInput', window: 'TxWindow', settings: 'Settings', queues: 'QueueDict') -> None: """Clear/reset screen of Source, Destination, and Networked Computer. Only send an unencrypted command to Networked Computer if traffic masking is disabled. With clear command, sending only the command header is enough. However, as reset command removes the ephemeral message log on Receiver Program, Transmitter Program must define the window to reset (in case, e.g., previous window selection command packet dropped, and active window state is inconsistent between the TCB programs). """ clear = user_input.plaintext.split()[0] == CLEAR command = CLEAR_SCREEN if clear else RESET_SCREEN + window.uid queue_command(command, settings, queues) clear_screen() if not settings.traffic_masking: pt_cmd = UNENCRYPTED_SCREEN_CLEAR if clear else UNENCRYPTED_SCREEN_RESET packet = UNENCRYPTED_DATAGRAM_HEADER + pt_cmd queue_to_nc(packet, queues[RELAY_PACKET_QUEUE]) if not clear: readline.clear_history() reset_terminal()
def process_arguments() -> Tuple[str, int, int]: """Load simulator settings from the command line argument.""" try: argv = str(sys.argv[1]) input_socket, output_socket = { SCNCLR: (SRC_DD_LISTEN_SOCKET, RP_LISTEN_SOCKET), SCNCRL: (SRC_DD_LISTEN_SOCKET, RP_LISTEN_SOCKET), NCDCLR: (DST_DD_LISTEN_SOCKET, DST_LISTEN_SOCKET), NCDCRL: (DST_DD_LISTEN_SOCKET, DST_LISTEN_SOCKET) }[argv] return argv, input_socket, output_socket except (IndexError, KeyError): clear_screen() print( f"\nUsage: python3.7 dd.py [OPTION]\n\n" f"\nMandatory arguments" f"\n Argument Simulate data diode between..." f"\n {SCNCLR} Source Computer and Networked Computer (left to right)" f"\n {SCNCRL} Source Computer and Networked Computer (right to left)" f"\n {NCDCLR} Networked Computer and Destination Computer (left to right)" f"\n {NCDCRL} Networked Computer and Destination Computer (right to left)" ) sys.exit(1)
def wipe(settings: 'Settings', queues: Dict[bytes, 'Queue']) -> None: """Reset terminals, wipe all user data from TxM/RxM/NH and power off systems. No effective RAM overwriting tool currently exists, so as long as TxM/RxM use FDE and DDR3 memory, recovery of user data becomes impossible very fast: https://www1.cs.fau.de/filepool/projects/coldboot/fares_coldboot.pdf """ if not yes("Wipe all user data and power off systems?"): raise FunctionReturn("Wipe command aborted.") clear_screen() for q in [COMMAND_PACKET_QUEUE, NH_PACKET_QUEUE]: while queues[q].qsize() != 0: queues[q].get() queue_command(WIPE_USER_DATA_HEADER, settings, queues[COMMAND_PACKET_QUEUE]) if not settings.session_traffic_masking: if settings.local_testing_mode: time.sleep(0.8) if settings.data_diode_sockets: time.sleep(2.2) else: time.sleep(settings.race_condition_delay) queue_to_nh(UNENCRYPTED_PACKET_HEADER + UNENCRYPTED_WIPE_COMMAND, settings, queues[NH_PACKET_QUEUE]) os.system('reset')
def animate(argv: str) -> None: """Animate the data diode.""" animation_length = 16 for i in range(animation_length): clear_screen() draw_frame(argv, 'Data flow', high=(i % 2 == 0)) time.sleep(0.04) clear_screen() draw_frame(argv, 'Idle', high=False)
def animate(argv: str) -> None: """Animate the data diode transmission indicator.""" for i in range(DD_ANIMATION_LENGTH): clear_screen() draw_frame(argv, DATA_FLOW, high=(i % 2 == 0)) time.sleep(0.04) clear_screen() draw_frame(argv, IDLE)
def setup(self) -> None: """Prompt user to enter initial settings.""" clear_screen() if not self.local_testing_mode: if self.software_operation == TX: self.txm_usb_serial_adapter = yes( "Does TxM use USB-to-serial/TTL adapter?", head=1, tail=1) else: self.rxm_usb_serial_adapter = yes( "Does RxM use USB-to-serial/TTL adapter?", head=1, tail=1)
def graceful_exit(message: str = '', clear: bool = True, exit_code: int = 0) -> None: """Display a message and exit TFC.""" if clear: clear_screen() if message: print('\n' + message) print("\nExiting TFC.\n") sys.exit(exit_code)
def print_contacts(self) -> None: """Print the list of contacts. Neatly printed contact list allows easy contact management: It allows the user to check active logging, file reception and notification settings, as well as what key exchange was used and what is the state of that key exchange. The contact list also shows and what the account displayed by the Relay Program corresponds to what nick etc. """ # Initialize columns c1 = ['Contact'] c2 = ['Account'] c3 = ['Logging'] c4 = ['Notify'] c5 = ['Files '] c6 = ['Key Ex'] # Key exchange status dictionary kex_dict = { KEX_STATUS_PENDING: f"{ECDHE} (Pending)", KEX_STATUS_UNVERIFIED: f"{ECDHE} (Unverified)", KEX_STATUS_VERIFIED: f"{ECDHE} (Verified)", KEX_STATUS_NO_RX_PSK: f"{PSK} (No contact key)", KEX_STATUS_HAS_RX_PSK: PSK } # Populate columns with contact data for c in self.get_list_of_contacts(): c1.append(c.nick) c2.append(c.short_address) c3.append('Yes' if c.log_messages else 'No') c4.append('Yes' if c.notifications else 'No') c5.append('Accept' if c.file_reception else 'Reject') c6.append(kex_dict[c.kex_status]) # Calculate column widths c1w, c2w, c3w, c4w, c5w, = [ max(len(v) for v in column) + CONTACT_LIST_INDENT for column in [c1, c2, c3, c4, c5] ] # Align columns by adding whitespace between fields of each line lines = [ f'{f1:{c1w}}{f2:{c2w}}{f3:{c3w}}{f4:{c4w}}{f5:{c5w}}{f6}' for f1, f2, f3, f4, f5, f6 in zip(c1, c2, c3, c4, c5, c6) ] # Add a terminal-wide line between the column names and the data lines.insert(1, get_terminal_width() * '─') # Print the contact list clear_screen() print('\n' + '\n'.join(lines) + '\n\n')
def graceful_exit(message: str = '', # Exit message to print clear: bool = True, # When False, does not clear screen before printing message exit_code: int = 0 # Value returned to parent process ) -> None: """Display a message and exit TFC.""" if clear: clear_screen() if message: print('\n' + message) print(f"\nExiting {TFC}.\n") sys.exit(exit_code)
def local_key_installed(ts: 'datetime', window_list: 'WindowList', contact_list: 'ContactList') -> None: """Clear local key bootstrap process from screen.""" message = "Successfully completed local key exchange." local_win = window_list.get_window(LOCAL_ID) local_win.add_new(ts, message) box_print(message) clear_screen(delay=1) if not contact_list.has_contacts(): c_print("Waiting for new contacts", head=1, tail=1)
def select_tx_window(self, settings: 'Settings', queues: Dict[bytes, 'Queue'], selection: str = None, cmd: bool = False) -> None: """Select specified window or ask the user to specify one.""" if selection is None: self.contact_list.print_contacts() self.group_list.print_groups() selection = input("Select recipient: ").strip() if selection in self.group_list.get_list_of_group_names(): if cmd and settings.session_traffic_masking and selection != self.uid: raise FunctionReturn("Error: Can't change window during traffic masking.") self.group = self.group_list.get_group(selection) self.window_contacts = self.group.members self.name = self.group.name self.uid = self.name self.log_messages = self.group.log_messages self.type = WIN_TYPE_GROUP self.type_print = 'group' if self.window_contacts: self.imc_name = self.window_contacts[0].rx_account elif selection in self.contact_list.contact_selectors(): if cmd and settings.session_traffic_masking: contact = self.contact_list.get_contact(selection) if contact.rx_account != self.uid: raise FunctionReturn("Error: Can't change window during traffic masking.") self.contact = self.contact_list.get_contact(selection) self.window_contacts = [self.contact] self.name = self.contact.nick self.uid = self.contact.rx_account self.imc_name = self.contact.rx_account self.log_messages = self.contact.log_messages self.type = WIN_TYPE_CONTACT self.type_print = 'contact' else: raise FunctionReturn("Error: No contact/group was found.") if settings.session_traffic_masking and not cmd: queues[WINDOW_SELECT_QUEUE].put((self.window_contacts, self.log_messages)) packet = WINDOW_SELECT_HEADER + self.uid.encode() queue_command(packet, settings, queues[COMMAND_PACKET_QUEUE]) clear_screen()
def process_local_key(ts: 'datetime', packet: bytes, window_list: 'WindowList', contact_list: 'ContactList', key_list: 'KeyList', settings: 'Settings') -> None: """Decrypt local key packet and add local contact/keyset.""" bootstrap = not key_list.has_local_key() try: while True: clear_screen() box_print("Received encrypted local key", tail=1) kdk = get_b58_key(B58_LOCAL_KEY, settings) try: pt = auth_and_decrypt(packet[1:], key=kdk, soft_e=True) break except nacl.exceptions.CryptoError: if bootstrap: raise FunctionReturn( "Error: Incorrect key decryption key.", delay=1.5) c_print("Incorrect key decryption key.", head=1) clear_screen(delay=1.5) key = pt[0:32] hek = pt[32:64] conf_code = pt[64:65] # Add local contact to contact list database contact_list.add_contact(LOCAL_ID, LOCAL_ID, LOCAL_ID, bytes(FINGERPRINT_LEN), bytes(FINGERPRINT_LEN), False, False, True) # Add local keyset to keyset database key_list.add_keyset(rx_account=LOCAL_ID, tx_key=key, rx_key=csprng(), tx_hek=hek, rx_hek=csprng()) box_print(f"Confirmation code for TxM: {conf_code.hex()}", head=1) local_win = window_list.get_local_window() local_win.add_new(ts, "Added new local key.") if bootstrap: window_list.active_win = local_win except KeyboardInterrupt: raise FunctionReturn("Local key setup aborted.", delay=1, head=3, tail_clear=True)
def change_master_key(user_input: 'UserInput', contact_list: 'ContactList', group_list: 'GroupList', settings: 'Settings', queues: Dict[bytes, 'Queue'], master_key: 'MasterKey') -> None: """Change master key on TxM/RxM.""" try: if settings.session_traffic_masking: raise FunctionReturn( "Error: Command is disabled during traffic masking.") try: device = user_input.plaintext.split()[1].lower() except IndexError: raise FunctionReturn("Error: No target system specified.") if device not in [TX, RX]: raise FunctionReturn("Error: Invalid target system.") if device == RX: queue_command(CHANGE_MASTER_K_HEADER, settings, queues[COMMAND_PACKET_QUEUE]) return None old_master_key = master_key.master_key[:] master_key.new_master_key() new_master_key = master_key.master_key phase("Re-encrypting databases") queues[KEY_MANAGEMENT_QUEUE].put( (KDB_CHANGE_MASTER_KEY_HEADER, master_key)) ensure_dir(DIR_USER_DATA) file_name = f'{DIR_USER_DATA}{settings.software_operation}_logs' if os.path.isfile(file_name): re_encrypt(old_master_key, new_master_key, settings) settings.store_settings() contact_list.store_contacts() group_list.store_groups() phase(DONE) box_print("Master key successfully changed.", head=1) clear_screen(delay=1.5) except KeyboardInterrupt: raise FunctionReturn("Password change aborted.", delay=1, head=3, tail_clear=True)
def redraw(self, file=None) -> None: """Print all messages received to window.""" self.unread_messages = 0 if file is None: clear_screen() if self.message_log: self.previous_msg_ts = self.message_log[0][0] self.create_handle_dict(self.message_log) for msg_tuple in self.message_log: self.print(msg_tuple, file) else: c_print(f"This window for {self.name} is currently empty.", head=1, tail=1)
def get_b58_key(key_type: str, settings: 'Settings') -> bytes: """Ask user to input Base58 encoded public key from RxM. For file keys, use testnet address format instead to prevent file injected via import from accidentally being decrypted with public key from adversary. """ if key_type == B58_PUB_KEY: clear_screen() c_print("Import public key from RxM", head=1, tail=1) c_print("WARNING") message_printer( "Outside specific requests TxM (this computer) " "makes, you must never copy any data from " "NH/RxM to TxM. Doing so could infect TxM, that " "could then later covertly transmit private " "keys/messages to attacker.", head=1, tail=1) message_printer("You can resend your public key by typing 'resend'", tail=1) box_msg = "Enter contact's public key from RxM" elif key_type == B58_LOCAL_KEY: box_msg = "Enter local key decryption key from TxM" elif key_type == B58_FILE_KEY: box_msg = "Enter file decryption key" else: raise CriticalError("Invalid key type") while True: if settings.local_testing_mode or key_type == B58_FILE_KEY: pub_key = box_input(box_msg, expected_len=51) small = True else: pub_key = box_input(box_msg, expected_len=65, key_input=True) small = False pub_key = ''.join(pub_key.split()) if key_type == B58_PUB_KEY and pub_key == RESEND: return pub_key.encode() try: return b58decode(pub_key, file_key=(key_type == B58_FILE_KEY)) except ValueError: c_print("Checksum error - Check that entered key is correct.", head=1) print_on_previous_line(reps=5 if small else 6, delay=1.5)
def show_fingerprints(window: 'TxWindow') -> None: """Print domain separated fingerprints of public keys on TxM. Comparison of fingerprints over authenticated channel can be used to verify users are not under man-in-the-middle attack. """ if window.type == WIN_TYPE_GROUP: raise FunctionReturn('Group is selected.') if window.contact.tx_fingerprint == bytes(FINGERPRINT_LEN): raise FunctionReturn(f"Pre-shared keys have no fingerprints.") clear_screen() print_fingerprint(window.contact.tx_fingerprint, " Your fingerprint (you read) ") print_fingerprint(window.contact.rx_fingerprint, "Contact's fingerprint (they read)") print('')
def select_group(self, selection: str, cmd: bool, settings: 'Settings') -> None: """Select group.""" if cmd and settings.traffic_masking and selection != self.name: raise SoftError( "Error: Can't change window during traffic masking.", head_clear=True) self.contact = None self.group = self.group_list.get_group(selection) self.window_contacts = self.group.members self.name = self.group.name self.uid = self.group.group_id self.group_id = self.group.group_id self.log_messages = self.group.log_messages self.type = WIN_TYPE_GROUP self.type_print = "group" clear_screen()
def verify_fingerprints(tx_fp: bytes, rx_fp: bytes) -> bool: """\ Verify fingerprints over out-of-band channel to detect MITM attacks against TFC's key exchange. :param tx_fp: User's fingerprint :param rx_fp: Contact's fingerprint :return: True if fingerprints match, else False """ clear_screen() message_printer("To verify received public key was not replaced by attacker in network, " "call the contact over end-to-end encrypted line, preferably Signal " "(https://signal.org/). Make sure Signal's safety numbers have been " "verified, and then verbally compare the key fingerprints below.", head=1, tail=1) print_fingerprint(tx_fp, " Your fingerprint (you read) ") print_fingerprint(rx_fp, "Purported fingerprint for contact (they read)") return yes("Is the contact's fingerprint correct?")