def test_determine_time_cost(self, *_: Any) -> None: master_key = MasterKey(self.operation, local_test=True) # Test1: Sentinel returns immediately if MAX_KEY_DERIVATION_TIME is exceeded time_cost, kd_time, _ = master_key.determine_time_cost("password", 8 * b'salt', memory_cost=512, parallelism=1) self.assertEqual(time_cost, 1) self.assertEqual(kd_time, 4.1) # Test2: Second key derivation time sentinel time_cost, kd_time, _ = master_key.determine_time_cost("password", 8 * b'salt', memory_cost=512, parallelism=1) self.assertEqual(time_cost, 2) self.assertEqual(kd_time, 4.1) # Test3: Complete binary search with search end time_cost, kd_time, _ = master_key.determine_time_cost("password", 8 * b'salt', memory_cost=512, parallelism=1) self.assertEqual(time_cost, 40) self.assertEqual(kd_time, 2.5)
def test_determine_memory_cost(self, *_: Any) -> None: master_key = MasterKey(self.operation, local_test=True) master_key.determine_memory_cost("password", 8 * b'salt', time_cost=1, memory_cost=1024, parallelism=1)
def test_master_key_generation_and_load(self, *_: Any) -> None: with self.assertRaises(SystemExit): MasterKey(self.operation, local_test=True) master_key = MasterKey(self.operation, local_test=True) self.assertIsInstance(master_key.master_key, bytes) self.assertEqual(os.path.getsize(self.file_name), MASTERKEY_DB_SIZE + BLAKE2_DIGEST_LENGTH) master_key2 = MasterKey(self.operation, local_test=True) self.assertIsInstance(master_key2.master_key, bytes) self.assertEqual(master_key.master_key, master_key2.master_key)
def test_master_key_generation_and_load(self): masterkey = MasterKey('ut', local_test=False) self.assertIsInstance(masterkey.master_key, bytes) os.path.isfile(f"{DIR_USER_DATA}ut_login_data") self.assertEqual( os.path.getsize(f"{DIR_USER_DATA}ut_login_data"), ARGON2_SALT_LEN + KEY_LENGTH + 3 * INTEGER_SETTING_LEN) masterkey = MasterKey('ut', local_test=False) self.assertIsInstance(masterkey.master_key, bytes)
def test_invalid_data_in_db_raises_critical_error(self, _): for delta in [-1, 1]: ensure_dir(DIR_USER_DATA) with open(self.file_name, 'wb+') as f: f.write(os.urandom(MASTERKEY_DB_SIZE + delta)) with self.assertRaises(SystemExit): _ = MasterKey(self.operation, local_test=False)
def new_psk(account: str, user: str, nick: str, contact_list: 'ContactList', settings: 'Settings', queues: Dict[bytes, 'Queue']) -> None: """Generate new pre-shared key for manual key delivery. :param account: The contact's account name (e.g. [email protected]) :param user: The user's account name (e.g. [email protected]) :param nick: Nick of contact :param contact_list: Contact list object :param settings: Settings object :param queues: Dictionary of multiprocessing queues :return: None """ try: tx_key = keygen() tx_hek = keygen() salt = keygen() password = MasterKey.new_password("password for PSK") phase("Deriving key encryption key", head=2) kek, _ = argon2_kdf(password, salt, rounds=16, memory=128000, parallelism=1) phase('Done') ct_tag = encrypt_and_sign(tx_key + tx_hek, key=kek) store_d = ask_path_gui(f"Select removable media for {nick}", settings) f_name = f"{store_d}/{user}.psk - Give to {account}" try: with open(f_name, 'wb+') as f: f.write(salt + ct_tag) except PermissionError: raise FunctionReturn( "Error: Did not have permission to write to directory.") packet = KEY_EX_PSK_TX_HEADER \ + tx_key \ + tx_hek \ + account.encode() + US_BYTE + nick.encode() queue_command(packet, settings, queues[COMMAND_PACKET_QUEUE]) contact_list.add_contact(account, user, nick, bytes(32), bytes(32), settings.log_msg_by_default, settings.store_file_default, settings.n_m_notify_privacy) queues[KEY_MANAGEMENT_QUEUE].put( ('ADD', account, tx_key, bytes(32), tx_hek, bytes(32))) box_print([f"Successfully added {nick}."], head=1) clear_screen(delay=1) except KeyboardInterrupt: raise FunctionReturn("PSK generation aborted.")
def test_load_master_key_with_invalid_data_raises_critical_error(self, _: Any) -> None: # Setup ensure_dir(DIR_USER_DATA) data = os.urandom(MASTERKEY_DB_SIZE + BLAKE2_DIGEST_LENGTH) with open(self.file_name, 'wb+') as f: f.write(data) # Test with self.assertRaises(SystemExit): _ = MasterKey(self.operation, local_test=False)
def test_invalid_data_in_db_raises_critical_error(self, _: Any) -> None: for delta in [-1, 1]: # Setup ensure_dir(DIR_USER_DATA) data = os.urandom(MASTERKEY_DB_SIZE + delta) data += blake2b(data) with open(self.file_name, 'wb+') as f: f.write(data) # Test with self.assertRaises(SystemExit): _ = MasterKey(self.operation, local_test=False)
def test_database_data_caching_and_storage_on_command(self, *_: Any): master_key = MasterKey(self.operation, local_test=True) master_key.new_master_key(replace=False) self.assertEqual(len(master_key.database_data), MASTERKEY_DB_SIZE) master_key.replace_database_data() self.assertIsNone(master_key.database_data) self.assertTrue(os.path.isfile(self.file_name))
def test_class(self): # Setup o_get_password = getpass.getpass getpass.getpass = lambda x: 'testpwd' # Test masterkey = MasterKey('ut', local_test=False) self.assertIsInstance(masterkey.master_key, bytes) os.path.isfile(f"{DIR_USER_DATA}/ut_login_data") self.assertEqual(os.path.getsize(f"{DIR_USER_DATA}/ut_login_data"), 32 + 32 + 8 + 8) cleanup() masterkey = MasterKey('ut', local_test=True) self.assertIsInstance(masterkey.master_key, bytes) masterkey = MasterKey('ut', local_test=True) self.assertIsInstance(masterkey.master_key, bytes) # Teardown getpass.getpass = o_get_password cleanup()
def decrypt_rx_psk(ct_tag: bytes, salt: bytes) -> bytes: """Get PSK password from user and decrypt Rx-PSK.""" while True: try: password = MasterKey.get_password("PSK password") phase("Deriving the key decryption key", head=2) kdk = argon2_kdf(password, salt, ARGON2_PSK_TIME_COST, ARGON2_PSK_MEMORY_COST, ARGON2_PSK_PARALLELISM) psk = auth_and_decrypt(ct_tag, kdk) phase(DONE) return psk 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 SoftError("PSK import aborted.", head=2, delay=1, tail_clear=True)
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 create_pre_shared_key(account: str, user: str, nick: str, contact_list: 'ContactList', settings: 'Settings', queues: Dict[bytes, 'Queue']) -> None: """Generate new pre-shared key for manual key delivery. :param account: The contact's account name (e.g. [email protected]) :param user: The user's account name (e.g. [email protected]) :param nick: Nick of contact :param contact_list: Contact list object :param settings: Settings object :param queues: Dictionary of multiprocessing queues :return: None """ try: tx_key = csprng() tx_hek = csprng() salt = csprng() password = MasterKey.new_password("password for PSK") phase("Deriving key encryption key", head=2) kek, _ = argon2_kdf(password, salt, parallelism=1) phase(DONE) ct_tag = encrypt_and_sign(tx_key + tx_hek, key=kek) while True: store_d = ask_path_gui(f"Select removable media for {nick}", settings) f_name = f"{store_d}/{user}.psk - Give to {account}" try: with open(f_name, 'wb+') as f: f.write(salt + ct_tag) break except PermissionError: c_print("Error: Did not have permission to write to directory.") time.sleep(0.5) continue packet = KEY_EX_PSK_TX_HEADER \ + tx_key \ + tx_hek \ + account.encode() + US_BYTE + nick.encode() queue_command(packet, settings, queues[COMMAND_PACKET_QUEUE]) contact_list.add_contact(account, user, nick, bytes(FINGERPRINT_LEN), bytes(FINGERPRINT_LEN), settings.log_messages_by_default, settings.accept_files_by_default, settings.show_notifications_by_default) queues[KEY_MANAGEMENT_QUEUE].put((KDB_ADD_ENTRY_HEADER, account, tx_key, csprng(), tx_hek, csprng())) box_print(f"Successfully added {nick}.", head=1) clear_screen(delay=1) except KeyboardInterrupt: raise FunctionReturn("PSK generation aborted.", delay=1, head=2, tail_clear=True)
def test_kd_binary_search(self, *_: Any) -> None: MasterKey(self.operation, local_test=True)
def main() -> None: """Derive master key, decrypt databases and initialize processes.""" os.chdir(sys.path[0]) init_entropy() operation, local_test, dd_sockets = process_arguments() clear_screen() c_print("TFC", head=1, tail=1) master_key = MasterKey(operation, local_test) settings = Settings(master_key, operation, local_test, dd_sockets) contact_list = ContactList(master_key, settings) key_list = KeyList(master_key, settings) group_list = GroupList(master_key, settings, contact_list) gateway = Gateway(settings) process_list = [] if settings.software_operation == 'tx': queues = { MESSAGE_PACKET_QUEUE: Queue(), FILE_PACKET_QUEUE: Queue(), COMMAND_PACKET_QUEUE: Queue(), LOG_PACKET_QUEUE: Queue(), NOISE_PACKET_QUEUE: Queue(), NOISE_COMMAND_QUEUE: Queue(), KEY_MANAGEMENT_QUEUE: Queue(), WINDOW_SELECT_QUEUE: Queue() } if settings.session_trickle: np_filler = Process(target=noise_process, args=(P_N_HEADER, queues[NOISE_PACKET_QUEUE], contact_list)) nc_filler = Process(target=noise_process, args=(C_N_HEADER, queues[NOISE_COMMAND_QUEUE])) process_list.extend([np_filler, nc_filler]) for p in [np_filler, nc_filler]: p.start() while any([ q.qsize() < 1000 for q in [queues[NOISE_PACKET_QUEUE], queues[NOISE_COMMAND_QUEUE]] ]): time.sleep(0.1) sender_process = Process(target=sender_loop, args=(settings, queues, gateway, key_list)) input_process = Process(target=tx_loop, args=(settings, queues, gateway, contact_list, group_list, master_key, sys.stdin.fileno())) log_process = Process(target=log_writer, args=(queues[LOG_PACKET_QUEUE], )) process_list.extend([sender_process, input_process, log_process]) for p in [sender_process, input_process, log_process]: p.start() elif settings.software_operation == 'rx': queues = { LOCAL_KEY_PACKET_HEADER: Queue(), PUBLIC_KEY_PACKET_HEADER: Queue(), MESSAGE_PACKET_HEADER: Queue(), COMMAND_PACKET_HEADER: Queue(), IMPORTED_FILE_CT_HEADER: Queue(), GATEWAY_QUEUE: Queue() } gateway_process = Process(target=gw_incoming, args=(gateway, queues[GATEWAY_QUEUE])) receiver_process = Process(target=receiver_loop, args=(settings, queues)) output_process = Process(target=rx_loop, args=(settings, queues, contact_list, key_list, group_list, master_key, sys.stdin.fileno())) process_list.extend( [gateway_process, receiver_process, output_process]) for p in [gateway_process, receiver_process, output_process]: p.start() while True: try: time.sleep(0.1) if not all([p.is_alive() for p in process_list]): for p in process_list: p.terminate() exit() except (EOFError, KeyboardInterrupt): pass
def test_password_generation(self) -> None: bit_strength, password = MasterKey.generate_master_password() self.assertIsInstance(bit_strength, int) self.assertIsInstance(password, str) self.assertGreaterEqual(bit_strength, PASSWORD_MIN_BIT_STRENGTH) self.assertEqual(len(password.split(' ')), 10)
def test_authenticate_action(self, *_: Any) -> None: master_key = MasterKey(self.operation, local_test=True) self.assert_se("Authentication aborted.", master_key.authenticate_action) self.assertTrue(master_key.authenticate_action())
def test_password_generation(self, *_: Any) -> None: master_key = MasterKey(self.operation, local_test=True) self.assertIsInstance(master_key.master_key, bytes)
def import_psk_rx_keys(cmd_data: bytes, ts: 'datetime', window_list: 'WindowList', contact_list: 'ContactList', key_list: 'KeyList', settings: 'Settings') -> None: """Import Rx-PSK of contact.""" account = cmd_data.decode() if not contact_list.has_contact(account): raise FunctionReturn(f"Error: Unknown account '{account}'") contact = contact_list.get_contact(account) psk_file = ask_path_gui(f"Select PSK for {contact.nick}", settings, get_file=True) with open(psk_file, 'rb') as f: psk_data = f.read() if len(psk_data) != PSK_FILE_SIZE: raise FunctionReturn("Error: Invalid PSK data in file.") salt = psk_data[:ARGON2_SALT_LEN] ct_tag = psk_data[ARGON2_SALT_LEN:] while True: try: password = MasterKey.get_password("PSK password") phase("Deriving key decryption key", head=2) kdk, _ = argon2_kdf(password, salt, parallelism=1) psk_pt = auth_and_decrypt(ct_tag, key=kdk, soft_e=True) phase(DONE) break except nacl.exceptions.CryptoError: print_on_previous_line() c_print("Invalid password. Try again.", head=1) print_on_previous_line(reps=5, delay=1.5) except KeyboardInterrupt: raise FunctionReturn("PSK import aborted.", head=2) rx_key = psk_pt[0:32] rx_hek = psk_pt[32:64] if any(k == bytes(KEY_LENGTH) for k in [rx_key, rx_hek]): raise FunctionReturn("Error: Received invalid keys from contact.") keyset = key_list.get_keyset(account) keyset.rx_key = rx_key keyset.rx_hek = rx_hek key_list.store_keys() # Pipes protects against shell injection. Source of command's parameter # is user's own RxM 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): box_print( f"Warning! Overwriting of PSK ({psk_file}) failed. Press <Enter> to continue.", manual_proceed=True) local_win = window_list.get_local_window() message = f"Added Rx-PSK for {contact.nick} ({account})." local_win.add_new(ts, message) box_print([ message, '', "Warning!", "Physically destroy the keyfile transmission ", "media to ensure that no data escapes RxM!" ], head=1, tail=1)
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 Argon2id 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, ARGON2_PSK_TIME_COST, ARGON2_PSK_MEMORY_COST, ARGON2_PSK_PARALLELISM) phase(DONE) ct_tag = encrypt_and_sign(tx_mk + tx_hk, key=kek) store_keys_on_removable_drive(ct_tag, salt, nick, onion_pub_key, onion_service, settings) deliver_contact_data(KEY_EX_PSK_TX, nick, onion_pub_key, tx_mk, csprng(), tx_hk, csprng(), queues, settings) 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 SoftError("PSK generation aborted.", tail_clear=True, delay=1, head=2)
def psk_import(cmd_data: bytes, ts: 'datetime', window_list: 'WindowList', contact_list: 'ContactList', key_list: 'KeyList', settings: 'Settings') -> None: """Import rx-PSK of contact.""" account = cmd_data.decode() if not contact_list.has_contact(account): raise FunctionReturn(f"Unknown account {account}.") contact = contact_list.get_contact(account) pskf = ask_path_gui(f"Select PSK for {contact.nick}", settings, get_file=True) with open(pskf, 'rb') as f: psk_data = f.read() if len( psk_data ) != 136: # Nonce (24) + Salt (32) + rx-key (32) + rx-hek (32) + tag (16) raise FunctionReturn("Invalid PSK data in file.") salt = psk_data[:32] ct_tag = psk_data[32:] while True: try: password = MasterKey.get_password("PSK password") phase("Deriving key decryption key", head=2) kdk, _ = argon2_kdf(password, salt, rounds=16, memory=128000, parallelism=1) psk_pt = auth_and_decrypt(ct_tag, key=kdk, soft_e=True) phase("Done") break except nacl.exceptions.CryptoError: print_on_previous_line() c_print("Invalid password. Try again.", head=1) print_on_previous_line(reps=5, delay=1.5) except KeyboardInterrupt: raise FunctionReturn("PSK import aborted.") rx_key = psk_pt[0:32] rx_hek = psk_pt[32:64] if rx_key == bytes(32) or rx_hek == bytes(32): raise FunctionReturn("Keys from contact are not valid.") keyset = key_list.get_keyset(account) keyset.rx_key = rx_key keyset.rx_hek = rx_hek key_list.store_keys() # Pipes protects against shell injection. Source of command # is trusted (user's own TxM) but it's still good practice. subprocess.Popen("shred -n 3 -z -u {}".format(pipes.quote(pskf)), shell=True).wait() if os.path.isfile(pskf): box_print(f"Warning! Overwriting of PSK ({pskf}) failed.") time.sleep(3) local_win = window_list.get_local_window() local_win.print_new(ts, f"Added Rx-PSK for {contact.nick} ({account})", print_=False) box_print([ f"Added Rx-PSK for {contact.nick}.", '', "Warning!", "Physically destroy the keyfile transmission ", "media to ensure that no data escapes RxM!" ], head=1, tail=1)
def main() -> None: """Load persistent data and launch the Transmitter/Receiver Program. This function decrypts user data from databases and launches processes for Transmitter or Receiver Program. It then monitors the EXIT_QUEUE for EXIT/WIPE signals and each process in case one of them dies. If you're reading this code to get the big picture on how TFC works, start by looking at the loop functions below, defined as the target for each process, from top to bottom: From `input_loop` process, you can see how the Transmitter Program processes a message or command from the user, creates assembly packets for a message/file/command, and how those are eventually pushed into a multiprocessing queue, from where they are loaded by the `sender_loop`. The `sender_loop` process encrypts outgoing assembly packets, and outputs the encrypted datagrams to the Networked Computer. The process also sends assembly packets to the `log_writer_loop`. The `log_writer_loop` process filters out non-message assembly packets and if logging for contact is enabled, stores the message assembly packet into an encrypted log database. The `noise_loop` processes are used to provide the `sender_loop` an interface identical to that of the `input_loop`. The `sender_loop` uses the interface to load noise packets/commands when traffic masking is enabled. Refer to the file `relay.py` to see how the Relay Program on Networked Computer manages datagrams between the network and Source/Destination Computer. In Receiver Program (also launched by this file), the `gateway_loop` process acts as a buffer for incoming datagrams. This buffer is consumed by the `receiver_loop` process that organizes datagrams loaded from the buffer into a set of queues depending on datagram type. Finally, the `output_loop` process loads and processes datagrams from the queues in the order of priority. """ working_dir = f'{os.getenv("HOME")}/{DIR_TFC}' ensure_dir(working_dir) os.chdir(working_dir) operation, local_test, data_diode_sockets = process_arguments() check_kernel_version() check_kernel_entropy() print_title(operation) master_key = MasterKey( operation, local_test) gateway = Gateway( operation, local_test, data_diode_sockets) settings = Settings( master_key, operation, local_test) contact_list = ContactList(master_key, settings) key_list = KeyList( master_key, settings) group_list = GroupList( master_key, settings, contact_list) if settings.software_operation == TX: onion_service = OnionService(master_key) queues = {MESSAGE_PACKET_QUEUE: Queue(), # Standard messages COMMAND_PACKET_QUEUE: Queue(), # Standard commands TM_MESSAGE_PACKET_QUEUE: Queue(), # Traffic masking messages TM_FILE_PACKET_QUEUE: Queue(), # Traffic masking files TM_COMMAND_PACKET_QUEUE: Queue(), # Traffic masking commands TM_NOISE_PACKET_QUEUE: Queue(), # Traffic masking noise packets TM_NOISE_COMMAND_QUEUE: Queue(), # Traffic masking noise commands RELAY_PACKET_QUEUE: Queue(), # Unencrypted datagrams to Networked Computer LOG_PACKET_QUEUE: Queue(), # `log_writer_loop` assembly packets to be logged LOG_SETTING_QUEUE: Queue(), # `log_writer_loop` logging state management between noise packets TRAFFIC_MASKING_QUEUE: Queue(), # `log_writer_loop` traffic masking setting management commands LOGFILE_MASKING_QUEUE: Queue(), # `log_writer_loop` logfile masking setting management commands KEY_MANAGEMENT_QUEUE: Queue(), # `sender_loop` key database management commands SENDER_MODE_QUEUE: Queue(), # `sender_loop` default/traffic masking mode switch commands WINDOW_SELECT_QUEUE: Queue(), # `sender_loop` window selection commands during traffic masking EXIT_QUEUE: Queue() # EXIT/WIPE signal from `input_loop` to `main` } # type: Dict[bytes, Queue] process_list = [Process(target=input_loop, args=(queues, settings, gateway, contact_list, group_list, master_key, onion_service, sys.stdin.fileno())), Process(target=sender_loop, args=(queues, settings, gateway, key_list)), Process(target=log_writer_loop, args=(queues, settings)), Process(target=noise_loop, args=(queues, contact_list)), Process(target=noise_loop, args=(queues,))] else: queues = {GATEWAY_QUEUE: Queue(), # Buffer for incoming datagrams LOCAL_KEY_DATAGRAM_HEADER: Queue(), # Local key datagrams MESSAGE_DATAGRAM_HEADER: Queue(), # Message datagrams FILE_DATAGRAM_HEADER: Queue(), # File datagrams COMMAND_DATAGRAM_HEADER: Queue(), # Command datagrams EXIT_QUEUE: Queue() # EXIT/WIPE signal from `output_loop` to `main` } process_list = [Process(target=gateway_loop, args=(queues, gateway)), Process(target=receiver_loop, args=(queues, gateway)), Process(target=output_loop, args=(queues, gateway, settings, contact_list, key_list, group_list, master_key, sys.stdin.fileno()))] for p in process_list: p.start() monitor_processes(process_list, settings.software_operation, queues)
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 Argon2id 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, ARGON2_PSK_TIME_COST, ARGON2_PSK_MEMORY_COST, ARGON2_PSK_PARALLELISM) 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 c_code = blake2b(onion_pub_key, digest_size=CONFIRM_CODE_LENGTH) command = (KEY_EX_PSK_TX + onion_pub_key + tx_mk + csprng() + tx_hk + csprng() + str_to_bytes(nick)) queue_command(command, settings, queues) while True: purp_code = ask_confirmation_code('Receiver') if purp_code == c_code.hex(): break elif purp_code == '': phase("Resending contact data", head=2) queue_command(command, settings, queues) phase(DONE) print_on_previous_line(reps=5) else: m_print("Incorrect confirmation code.", head=1) print_on_previous_line(reps=4, delay=2) 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 main() -> None: """Derive master key, decrypt databases and initialize processes.""" os.chdir(sys.path[0]) check_kernel_version() check_kernel_entropy() operation, local_test, dd_sockets = process_arguments() clear_screen() c_print(TFC, head=1, tail=1) master_key = MasterKey(operation, local_test) settings = Settings(master_key, operation, local_test, dd_sockets) contact_list = ContactList(master_key, settings) key_list = KeyList(master_key, settings) group_list = GroupList(master_key, settings, contact_list) gateway = Gateway(settings) if settings.software_operation == TX: queues = { MESSAGE_PACKET_QUEUE: Queue(), FILE_PACKET_QUEUE: Queue(), COMMAND_PACKET_QUEUE: Queue(), NH_PACKET_QUEUE: Queue(), LOG_PACKET_QUEUE: Queue(), EXIT_QUEUE: Queue(), NOISE_PACKET_QUEUE: Queue(), NOISE_COMMAND_QUEUE: Queue(), KEY_MANAGEMENT_QUEUE: Queue(), WINDOW_SELECT_QUEUE: Queue() } process_list = [ Process(target=input_loop, args=(queues, settings, gateway, contact_list, group_list, master_key, sys.stdin.fileno())), Process(target=sender_loop, args=(queues, settings, gateway, key_list)), Process(target=log_writer_loop, args=(queues, )) ] if settings.session_traffic_masking: process_list.extend([ Process(target=noise_loop, args=(P_N_HEADER, queues[NOISE_PACKET_QUEUE], contact_list)), Process(target=noise_loop, args=(C_N_HEADER, queues[NOISE_COMMAND_QUEUE])) ]) else: queues = { LOCAL_KEY_PACKET_HEADER: Queue(), PUBLIC_KEY_PACKET_HEADER: Queue(), MESSAGE_PACKET_HEADER: Queue(), COMMAND_PACKET_HEADER: Queue(), IMPORTED_FILE_HEADER: Queue(), EXIT_QUEUE: Queue(), GATEWAY_QUEUE: Queue() } process_list = [ Process(target=gateway_loop, args=(queues, gateway)), Process(target=receiver_loop, args=(queues, settings)), Process(target=output_loop, args=(queues, settings, contact_list, key_list, group_list, master_key, sys.stdin.fileno())) ] for p in process_list: p.start() while True: with ignored(EOFError, KeyboardInterrupt): time.sleep(0.1) if not all([p.is_alive() for p in process_list]): for p in process_list: p.terminate() sys.exit(1) if not queues[EXIT_QUEUE].empty(): command = queues[EXIT_QUEUE].get() for p in process_list: p.terminate() if command == WIPE: subprocess.Popen( f"find {DIR_USER_DATA} -name '{operation}*' -type f -exec shred -n 3 -z -u {{}} \;", shell=True).wait() os.system('poweroff') else: sys.exit(0)
def test_kd_binary_serach(self, *_): MasterKey(self.operation, local_test=True)