def test_pub_key_to_short_addr(self): self.assertEqual( len( pub_key_to_short_address( bytes(ONION_SERVICE_PUBLIC_KEY_LENGTH))), TRUNC_ADDRESS_LENGTH) self.assertIsInstance( pub_key_to_short_address(bytes(ONION_SERVICE_PUBLIC_KEY_LENGTH)), str)
def store_keys_on_removable_drive( ct_tag: bytes, # Encrypted PSK salt: bytes, # Salt for PSK decryption key derivation nick: str, # Contact's nickname onion_pub_key: bytes, # Public key of contact's v3 Onion Service onion_service: 'OnionService', # OnionService object settings: 'Settings', # Settings object ) -> None: """Store keys for contact on a removable media.""" trunc_addr = pub_key_to_short_address(onion_pub_key) while True: store_d = ask_path_gui(f"Select removable media for {nick}", settings) f_name = f"{store_d}/{onion_service.user_short_address}.psk - Give to {trunc_addr}" try: with open(f_name, "wb+") as f: f.write(salt + ct_tag) f.flush() os.fsync(f.fileno()) break except PermissionError: m_print( "Error: Did not have permission to write to the directory.", delay=0.5) continue
def contact_rem(onion_pub_key: bytes, ts: 'datetime', window_list: 'WindowList', contact_list: 'ContactList', group_list: 'GroupList', key_list: 'KeyList', settings: 'Settings', master_key: 'MasterKey') -> None: """Remove contact from Receiver Program.""" key_list.remove_keyset(onion_pub_key) window_list.remove_window(onion_pub_key) short_addr = pub_key_to_short_address(onion_pub_key) try: contact = contact_list.get_contact_by_pub_key(onion_pub_key) except StopIteration: raise FunctionReturn( f"Receiver has no account '{short_addr}' to remove.") nick = contact.nick in_group = any([g.remove_members([onion_pub_key]) for g in group_list]) contact_list.remove_contact_by_pub_key(onion_pub_key) message = f"Removed {nick} ({short_addr}) from contacts{' and groups' if in_group else ''}." m_print(message, bold=True, head=1, tail=1) local_win = window_list.get_local_window() local_win.add_new(ts, message) remove_logs(contact_list, group_list, settings, master_key, onion_pub_key)
def update_handle_dict(self, pub_key: bytes) -> None: """Update handle for public key in `handle_dict`.""" if self.contact_list.has_pub_key(pub_key): self.handle_dict[pub_key] = self.contact_list.get_nick_by_pub_key( pub_key) else: self.handle_dict[pub_key] = pub_key_to_short_address(pub_key)
def ch_nick(cmd_data: bytes, ts: 'datetime', window_list: 'WindowList', contact_list: 'ContactList') -> None: """Change nickname of contact.""" onion_pub_key, nick_bytes = separate_header( cmd_data, header_length=ONION_SERVICE_PUBLIC_KEY_LENGTH) nick = nick_bytes.decode() short_addr = pub_key_to_short_address(onion_pub_key) try: contact = contact_list.get_contact_by_pub_key(onion_pub_key) except StopIteration: raise FunctionReturn( f"Error: Receiver has no contact '{short_addr}' to rename.") contact.nick = nick contact_list.store_contacts() window = window_list.get_window(onion_pub_key) window.name = nick window.handle_dict[onion_pub_key] = nick if window.type == WIN_TYPE_CONTACT: window.redraw() cmd_win = window_list.get_local_window() cmd_win.add_new(ts, f"Changed {short_addr} nick to '{nick}'.", output=True)
def __init__(self, onion_pub_key: bytes, nick: str, tx_fingerprint: bytes, rx_fingerprint: bytes, kex_status: bytes, log_messages: bool, file_reception: bool, notifications: bool ) -> None: """Create a new Contact object. `self.short_address` is a truncated version of the account used to identify TFC account in printed messages. """ self.onion_pub_key = onion_pub_key self.nick = nick self.tx_fingerprint = tx_fingerprint self.rx_fingerprint = rx_fingerprint self.kex_status = kex_status self.log_messages = log_messages self.file_reception = file_reception self.notifications = notifications self.onion_address = pub_key_to_onion_address(self.onion_pub_key) self.short_address = pub_key_to_short_address(self.onion_pub_key) self.tfc_private_key = None # type: Optional[X448PrivateKey]
def __init__(self, **kwargs: Any) -> None: """Create new OnionService mock object.""" self.onion_private_key = ONION_SERVICE_PRIVATE_KEY_LENGTH*b'a' self.conf_code = b'a' self.public_key = bytes(nacl.signing.SigningKey(seed=self.onion_private_key).verify_key) self.user_onion_address = pub_key_to_onion_address(self.public_key) self.user_short_address = pub_key_to_short_address(self.public_key) self.is_delivered = False for key, value in kwargs.items(): setattr(self, key, value)
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 __init__(self, master_key: 'MasterKey') -> None: """Create a new OnionService object.""" self.master_key = master_key self.file_name = f'{DIR_USER_DATA}{TX}_onion_db' self.is_delivered = False self.conf_code = csprng(CONFIRM_CODE_LENGTH) ensure_dir(DIR_USER_DATA) if os.path.isfile(self.file_name): self.onion_private_key = self.load_onion_service_private_key() else: self.onion_private_key = self.new_onion_service_private_key() self.store_onion_service_private_key() self.public_key = bytes( nacl.signing.SigningKey(seed=self.onion_private_key).verify_key) self.user_onion_address = pub_key_to_onion_address(self.public_key) self.user_short_address = pub_key_to_short_address(self.public_key)
def key_ex_psk_rx(packet: bytes, ts: 'datetime', window_list: 'WindowList', contact_list: 'ContactList', key_list: 'KeyList', settings: 'Settings') -> None: """Import Rx-PSK of contact.""" c_code, onion_pub_key = separate_header(packet, CONFIRM_CODE_LENGTH) short_addr = pub_key_to_short_address(onion_pub_key) if not contact_list.has_pub_key(onion_pub_key): raise FunctionReturn(f"Error: Unknown account '{short_addr}'.", head_clear=True) contact = contact_list.get_contact_by_pub_key(onion_pub_key) psk_file = ask_path_gui(f"Select PSK for {contact.nick} ({short_addr})", settings, get_file=True) try: with open(psk_file, 'rb') as f: psk_data = f.read() except PermissionError: raise FunctionReturn("Error: No read permission for the PSK file.") if len(psk_data) != PSK_FILE_SIZE: raise FunctionReturn("Error: The PSK data in the file was invalid.", head_clear=True) salt, ct_tag = separate_header(psk_data, ARGON2_SALT_LENGTH) while True: try: password = MasterKey.get_password("PSK password") phase("Deriving the key decryption key", head=2) kdk = argon2_kdf(password, salt, time_cost=ARGON2_PSK_TIME_COST, memory_cost=ARGON2_PSK_MEMORY_COST) psk = auth_and_decrypt(ct_tag, kdk) phase(DONE) break except nacl.exceptions.CryptoError: print_on_previous_line() m_print("Invalid password. Try again.", head=1) print_on_previous_line(reps=5, delay=1) except (EOFError, KeyboardInterrupt): raise FunctionReturn("PSK import aborted.", head=2, delay=1, tail_clear=True) rx_mk, rx_hk = separate_header(psk, SYMMETRIC_KEY_LENGTH) if any(k == bytes(SYMMETRIC_KEY_LENGTH) for k in [rx_mk, rx_hk]): raise FunctionReturn("Error: Received invalid keys from contact.", head_clear=True) keyset = key_list.get_keyset(onion_pub_key) keyset.rx_mk = rx_mk keyset.rx_hk = rx_hk key_list.store_keys() contact.kex_status = KEX_STATUS_HAS_RX_PSK contact_list.store_contacts() # Pipes protects against shell injection. Source of command's parameter is # the program itself, and therefore trusted, but it's still good practice. subprocess.Popen(f"shred -n 3 -z -u {pipes.quote(psk_file)}", shell=True).wait() if os.path.isfile(psk_file): m_print( f"Warning! Overwriting of PSK ({psk_file}) failed. Press <Enter> to continue.", manual_proceed=True, box=True) message = f"Added Rx-side PSK for {contact.nick} ({short_addr})." local_win = window_list.get_local_window() local_win.add_new(ts, message) m_print([ message, '', "Warning!", "Physically destroy the keyfile transmission media ", "to ensure it does not steal data from this computer!", '', f"Confirmation code (to Transmitter): {c_code.hex()}" ], box=True, head=1, tail=1)
def key_ex_psk_rx(packet: bytes, ts: 'datetime', window_list: 'WindowList', contact_list: 'ContactList', key_list: 'KeyList', settings: 'Settings') -> None: """Import Rx-PSK of contact.""" c_code, onion_pub_key = separate_header(packet, CONFIRM_CODE_LENGTH) short_addr = pub_key_to_short_address(onion_pub_key) if not contact_list.has_pub_key(onion_pub_key): raise SoftError(f"Error: Unknown account '{short_addr}'.", head_clear=True) contact = contact_list.get_contact_by_pub_key(onion_pub_key) psk_file = ask_path_gui(f"Select PSK for {contact.nick} ({short_addr})", settings, get_file=True) try: with open(psk_file, 'rb') as f: psk_data = f.read() except PermissionError: raise SoftError("Error: No read permission for the PSK file.") if len(psk_data) != PSK_FILE_SIZE: raise SoftError("Error: The PSK data in the file was invalid.", head_clear=True) salt, ct_tag = separate_header(psk_data, ARGON2_SALT_LENGTH) psk = decrypt_rx_psk(ct_tag, salt) rx_mk, rx_hk = separate_header(psk, SYMMETRIC_KEY_LENGTH) if any(k == bytes(SYMMETRIC_KEY_LENGTH) for k in [rx_mk, rx_hk]): raise SoftError("Error: Received invalid keys from contact.", head_clear=True) keyset = key_list.get_keyset(onion_pub_key) keyset.rx_mk = rx_mk keyset.rx_hk = rx_hk key_list.store_keys() contact.kex_status = KEX_STATUS_HAS_RX_PSK contact_list.store_contacts() # Pipes protects against shell injection. Source of command's parameter is # the program itself, and therefore trusted, but it's still good practice. subprocess.Popen(f"shred -n 3 -z -u {pipes.quote(psk_file)}", shell=True).wait() if os.path.isfile(psk_file): m_print( f"Warning! Overwriting of PSK ({psk_file}) failed. Press <Enter> to continue.", manual_proceed=True, box=True) message = f"Added Rx-side PSK for {contact.nick} ({short_addr})." cmd_win = window_list.get_command_window() cmd_win.add_new(ts, message) m_print([ message, '', "Warning!", "Physically destroy the keyfile transmission media ", "to ensure it does not steal data from this computer!", '', f"Confirmation code (to Transmitter): {c_code.hex()}" ], box=True, head=1, tail=1)
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 create_pre_shared_key( onion_pub_key: bytes, # Public key of contact's v3 Onion Service nick: str, # Nick of contact contact_list: 'ContactList', # Contact list object settings: 'Settings', # Settings object onion_service: 'OnionService', # OnionService object queues: 'QueueDict' # Dictionary of multiprocessing queues ) -> None: """Generate a new pre-shared key for manual key delivery. Pre-shared keys offer a low-tech solution against the slowly emerging threat of quantum computers. PSKs are less convenient and not usable in every scenario, but until a quantum-safe key exchange algorithm with reasonably short keys is standardized, TFC can't provide a better alternative against quantum computers. The generated keys are protected by a key encryption key, derived from a 256-bit salt and a password (that is to be shared with the recipient) using Argon2d key derivation function. The encrypted message and header keys are stored together with salt on a removable media. This media must be a never-before-used device from sealed packaging. Re-using an old device might infect Source Computer, and the malware could either copy sensitive data on that removable media, or Source Computer might start transmitting the sensitive data covertly over the serial interface to malware on Networked Computer. Once the key has been exported to the clean drive, contact data and keys are exported to the Receiver Program on Destination computer. The transmission is encrypted with the local key. """ try: tx_mk = csprng() tx_hk = csprng() salt = csprng() password = MasterKey.new_password("password for PSK") phase("Deriving key encryption key", head=2) kek = argon2_kdf(password, salt, time_cost=ARGON2_PSK_TIME_COST, memory_cost=ARGON2_PSK_MEMORY_COST) phase(DONE) ct_tag = encrypt_and_sign(tx_mk + tx_hk, key=kek) while True: trunc_addr = pub_key_to_short_address(onion_pub_key) store_d = ask_path_gui(f"Select removable media for {nick}", settings) f_name = f"{store_d}/{onion_service.user_short_address}.psk - Give to {trunc_addr}" try: with open(f_name, 'wb+') as f: f.write(salt + ct_tag) break except PermissionError: m_print( "Error: Did not have permission to write to the directory.", delay=0.5) continue command = (KEY_EX_PSK_TX + onion_pub_key + tx_mk + csprng() + tx_hk + csprng() + str_to_bytes(nick)) queue_command(command, settings, queues) contact_list.add_contact(onion_pub_key, nick, bytes(FINGERPRINT_LENGTH), bytes(FINGERPRINT_LENGTH), KEX_STATUS_NO_RX_PSK, settings.log_messages_by_default, settings.accept_files_by_default, settings.show_notifications_by_default) 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): raise FunctionReturn("PSK generation aborted.", tail_clear=True, delay=1, head=2)
def remove_logs(contact_list: 'ContactList', group_list: 'GroupList', settings: 'Settings', master_key: 'MasterKey', selector: bytes) -> None: """\ Remove log entries for selector (public key of an account/group ID). If the selector is a public key, all messages (both the private conversation and any associated group messages) sent to and received from the associated contact are removed. If the selector is a group ID, only messages for the group matching that group ID are removed. """ ensure_dir(DIR_USER_DATA) file_name = f'{DIR_USER_DATA}{settings.software_operation}_logs' temp_name = file_name + TEMP_SUFFIX packet_list = PacketList(settings, contact_list) entries_to_keep = [] # type: List[bytes] removed = False contact = len(selector) == ONION_SERVICE_PUBLIC_KEY_LENGTH check_log_file_exists(file_name) message_log = MessageLog(file_name, master_key.master_key) for log_entry in message_log: onion_pub_key, _, origin, assembly_packet = separate_headers( log_entry, [ ONION_SERVICE_PUBLIC_KEY_LENGTH, TIMESTAMP_LENGTH, ORIGIN_HEADER_LENGTH ]) if contact: if onion_pub_key == selector: removed = True else: entries_to_keep.append(log_entry) else: # Group packet = packet_list.get_packet(onion_pub_key, origin, MESSAGE, log_access=True) try: packet.add_packet(assembly_packet, log_entry) except SoftError: continue if not packet.is_complete: continue removed = check_packet_fate(entries_to_keep, packet, removed, selector) message_log.close_database() message_log_temp = MessageLog(temp_name, master_key.master_key) for log_entry in entries_to_keep: message_log_temp.insert_log_entry(log_entry) message_log_temp.close_database() os.replace(temp_name, file_name) try: name = contact_list.get_nick_by_pub_key( selector) if contact else group_list.get_group_by_id(selector).name except StopIteration: name = pub_key_to_short_address(selector) if contact else b58encode( selector) action = "Removed" if removed else "Found no" win_type = "contact" if contact else "group" raise SoftError(f"{action} log entries for {win_type} '{name}'.")
def remove_logs(contact_list: 'ContactList', group_list: 'GroupList', settings: 'Settings', master_key: 'MasterKey', selector: bytes) -> None: """\ Remove log entries for selector (public key of an account/group ID). If the selector is a public key, all messages (both the private conversation and any associated group messages) sent to and received from the associated contact are removed. If the selector is a group ID, only messages for group determined by that group ID are removed. """ ensure_dir(DIR_USER_DATA) file_name = f'{DIR_USER_DATA}{settings.software_operation}_logs' temp_name = f'{file_name}_temp' log_file = get_logfile(file_name) packet_list = PacketList(settings, contact_list) ct_to_keep = [] # type: List[bytes] removed = False contact = len(selector) == ONION_SERVICE_PUBLIC_KEY_LENGTH for ct in iter(lambda: log_file.read(LOG_ENTRY_LENGTH), b''): plaintext = auth_and_decrypt(ct, master_key.master_key, database=file_name) onion_pub_key, _, origin, assembly_packet = separate_headers( plaintext, [ ONION_SERVICE_PUBLIC_KEY_LENGTH, TIMESTAMP_LENGTH, ORIGIN_HEADER_LENGTH ]) if contact: if onion_pub_key == selector: removed = True else: ct_to_keep.append(ct) else: # Group packet = packet_list.get_packet(onion_pub_key, origin, MESSAGE, log_access=True) try: packet.add_packet(assembly_packet, ct) except FunctionReturn: continue if not packet.is_complete: continue _, header, message = separate_headers( packet.assemble_message_packet(), [WHISPER_FIELD_LENGTH, MESSAGE_HEADER_LENGTH]) if header == PRIVATE_MESSAGE_HEADER: ct_to_keep.extend(packet.log_ct_list) packet.clear_assembly_packets() elif header == GROUP_MESSAGE_HEADER: group_id, _ = separate_header(message, GROUP_ID_LENGTH) if group_id == selector: removed = True else: ct_to_keep.extend(packet.log_ct_list) packet.clear_assembly_packets() log_file.close() if os.path.isfile(temp_name): os.remove(temp_name) with open(temp_name, 'wb+') as f: if ct_to_keep: f.write(b''.join(ct_to_keep)) os.remove(file_name) os.rename(temp_name, file_name) try: name = contact_list.get_contact_by_pub_key(selector).nick \ if contact else group_list.get_group_by_id(selector).name except StopIteration: name = pub_key_to_short_address(selector) \ if contact else b58encode(selector) action = "Removed" if removed else "Found no" win_type = "contact" if contact else "group" raise FunctionReturn(f"{action} log entries for {win_type} '{name}'.")