def deliver_contact_data(header: bytes, # Key type (x448, PSK) nick: str, # Contact's nickname onion_pub_key: bytes, # Public key of contact's v3 Onion Service tx_mk: bytes, # Message key for outgoing messages rx_mk: bytes, # Message key for incoming messages tx_hk: bytes, # Header key for outgoing messages rx_hk: bytes, # Header key for incoming messages queues: 'QueueDict', # Dictionary of multiprocessing queues settings: 'Settings', # Settings object ) -> None: """Deliver contact data to Destination Computer.""" c_code = blake2b(onion_pub_key, digest_size=CONFIRM_CODE_LENGTH) command = (header + onion_pub_key + tx_mk + rx_mk + tx_hk + rx_hk + 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)
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 new_master_key(self) -> None: """Create a new master key from salt and password.""" password = MasterKey.new_password() salt = csprng() rounds = ARGON2_ROUNDS memory = ARGON2_MIN_MEMORY phase("Deriving master key", head=2) while True: time_start = time.monotonic() master_key, parallellism = argon2_kdf(password, salt, rounds, memory=memory, local_test=self.local_test) time_final = time.monotonic() - time_start if time_final > 3.0: self.master_key = master_key ensure_dir(f'{DIR_USER_DATA}/') with open(self.file_name, 'wb+') as f: f.write(salt + hash_chain(self.master_key) + int_to_bytes(rounds) + int_to_bytes(memory) + int_to_bytes(parallellism)) phase(DONE) break else: memory *= 2
def deliver_local_key(local_key_packet: bytes, kek: bytes, c_code: bytes, settings: 'Settings', queues: 'QueueDict' ) -> None: """Deliver encrypted local key to Destination Computer.""" nc_bypass_msg(NC_BYPASS_START, settings) queue_to_nc(local_key_packet, queues[RELAY_PACKET_QUEUE]) while True: print_key("Local key decryption key (to Receiver)", kek, settings) purp_code = ask_confirmation_code("Receiver") if purp_code == c_code.hex(): nc_bypass_msg(NC_BYPASS_STOP, settings) break elif purp_code == "": phase("Resending local key", head=2) queue_to_nc(local_key_packet, queues[RELAY_PACKET_QUEUE]) phase(DONE) print_on_previous_line(reps=(9 if settings.local_testing_mode else 10)) else: m_print(["Incorrect confirmation code. If Receiver did not receive", "the encrypted local key, resend it by pressing <Enter>."], head=1) print_on_previous_line(reps=(9 if settings.local_testing_mode else 10), delay=2)
def deliver_onion_service_data(relay_command: bytes, onion_service: 'OnionService', gateway: 'Gateway') -> None: """Send Onion Service data to Replay Program on Networked Computer.""" gateway.write(relay_command) while True: purp_code = ask_confirmation_code('Relay') if purp_code == onion_service.conf_code.hex(): onion_service.is_delivered = True onion_service.new_confirmation_code() break if purp_code == '': phase("Resending Onion Service data", head=2) gateway.write(relay_command) phase(DONE) print_on_previous_line(reps=5) else: m_print([ "Incorrect confirmation code. If Relay Program did not", "receive Onion Service data, resend it by pressing <Enter>." ], head=1) print_on_previous_line(reps=5, delay=2)
def client_establish_socket(self) -> None: """Initialize the transmitter (IPC client).""" try: target = RECEIVER if self.settings.software_operation == NC else RELAY phase(f"Connecting to {target}") while True: try: if self.settings.software_operation == TX: socket_number = SRC_DD_LISTEN_SOCKET if self.settings.data_diode_sockets else RP_LISTEN_SOCKET else: socket_number = DST_DD_LISTEN_SOCKET if self.settings.data_diode_sockets else DST_LISTEN_SOCKET try: self.tx_socket = multiprocessing.connection.Client((LOCALHOST, socket_number)) except ConnectionRefusedError: time.sleep(0.1) continue phase(DONE) break except socket.error: time.sleep(0.1) except KeyboardInterrupt: graceful_exit()
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 change_master_key(user_input: 'UserInput', contact_list: 'ContactList', group_list: 'GroupList', settings: 'Settings', queues: 'QueueDict', master_key: 'MasterKey', onion_service: 'OnionService') -> None: """Change the master key on Transmitter/Receiver Program.""" try: if settings.traffic_masking: raise FunctionReturn( "Error: Command is disabled during traffic masking.", head_clear=True) try: device = user_input.plaintext.split()[1].lower() except IndexError: raise FunctionReturn( f"Error: No target-system ('{TX}' or '{RX}') specified.", head_clear=True) if device not in [TX, RX]: raise FunctionReturn(f"Error: Invalid target system '{device}'.", head_clear=True) if device == RX: queue_command(CH_MASTER_KEY, settings, queues) return None old_master_key = master_key.master_key[:] new_master_key = master_key.master_key = master_key.new_master_key() phase("Re-encrypting databases") queues[KEY_MANAGEMENT_QUEUE].put( (KDB_CHANGE_MASTER_KEY_HEADER, master_key)) ensure_dir(DIR_USER_DATA) if os.path.isfile( f'{DIR_USER_DATA}{settings.software_operation}_logs'): change_log_db_key(old_master_key, new_master_key, settings) contact_list.store_contacts() group_list.store_groups() settings.store_settings() onion_service.store_onion_service_private_key() phase(DONE) m_print("Master key successfully changed.", bold=True, tail_clear=True, delay=1, head=1) except (EOFError, KeyboardInterrupt): raise FunctionReturn("Password change aborted.", tail_clear=True, delay=1, head=2)
def ch_master_key(ts: 'datetime', window_list: 'WindowList', contact_list: 'ContactList', group_list: 'GroupList', key_list: 'KeyList', settings: 'Settings', master_key: 'MasterKey') -> None: """Prompt the user for a new master password and derive a new master key from that.""" if not master_key.authenticate_action(): raise SoftError("Error: Invalid password.", tail_clear=True, delay=1, head=2) # Cache old master key to allow log file re-encryption. old_master_key = master_key.master_key[:] # Create new master key but do not store new master key data into any database. new_master_key = master_key.master_key = master_key.new_master_key( replace=False) phase("Re-encrypting databases") # Update encryption keys for databases contact_list.database.database_key = new_master_key key_list.database.database_key = new_master_key group_list.database.database_key = new_master_key settings.database.database_key = new_master_key # Create temp databases for each database, do not replace original. with ignored(SoftError): change_log_db_key(old_master_key, new_master_key, settings) contact_list.store_contacts(replace=False) key_list.store_keys(replace=False) group_list.store_groups(replace=False) settings.store_settings(replace=False) # At this point all temp files exist and they have been checked to be valid by the respective # temp file writing function. It's now time to create a temp file for the new master key # database. Once the temp master key database is created, the `replace_database_data()` method # will also run the atomic `os.replace()` command for the master key database. master_key.replace_database_data() # Next we do the atomic `os.replace()` for all other files too. replace_log_db(settings) contact_list.database.replace_database() key_list.database.replace_database() group_list.database.replace_database() settings.database.replace_database() phase(DONE) m_print("Master password successfully changed.", bold=True, tail_clear=True, delay=1, head=1) cmd_win = window_list.get_command_window() cmd_win.add_new(ts, "Changed Receiver master password.")
def new_master_key(self) -> bytes: """Create a new master key from password and salt. The generated master key depends on a 256-bit salt and the password entered by the user. Additional computational strength is added by the slow hash function (Argon2d). This method automatically tweaks the Argon2 memory parameter so that key derivation on used hardware takes at least three seconds. The more cores and the faster each core is, the more security a given password provides. The preimage resistance of BLAKE2b prevents derivation of master key from the stored hash, and Argon2d ensures brute force and dictionary attacks against the master password are painfully slow even with GPUs/ASICs/FPGAs, as long as the password is sufficiently strong. The salt does not need additional protection as the security it provides depends on the salt space in relation to the number of attacked targets (i.e. if two or more physically compromised systems happen to share the same salt, the attacker can speed up the attack against those systems with time-memory-trade-off attack). A 256-bit salt ensures that even in a group of 4.8*10^29 users, the probability that two users share the same salt is just 10^(-18).* * https://en.wikipedia.org/wiki/Birthday_attack """ password = MasterKey.new_password() salt = csprng(ARGON2_SALT_LENGTH) memory = ARGON2_MIN_MEMORY parallelism = multiprocessing.cpu_count() if self.local_test: parallelism = max(1, parallelism // 2) phase("Deriving master key", head=2) while True: time_start = time.monotonic() master_key = argon2_kdf(password, salt, ARGON2_ROUNDS, memory, parallelism) kd_time = time.monotonic() - time_start if kd_time < MIN_KEY_DERIVATION_TIME: memory *= 2 else: ensure_dir(DIR_USER_DATA) with open(self.file_name, 'wb+') as f: f.write(salt + blake2b(master_key) + int_to_bytes(memory) + int_to_bytes(parallelism)) phase(DONE) return master_key
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 determine_memory_cost( self, password: str, salt: bytes, time_cost: int, memory_cost: int, parallelism: int, ) -> Tuple[int, bytes]: """Determine suitable memory_cost value for Argon2id. If we reached this function, it means we found a `t+1` value for time_cost (explained in the `determine_time_cost` function). We therefore do a binary search on the amount of memory to use until we hit the desired key derivation time range. """ lower_bound = ARGON2_MIN_MEMORY_COST upper_bound = memory_cost while True: memory_cost = int(round((lower_bound + upper_bound) // 2, -3)) print_on_previous_line() phase(f"Trying memory cost {memory_cost} KiB") master_key, kd_time = self.timed_key_derivation( password, salt, time_cost, memory_cost, parallelism) phase(f"{kd_time:.1f}s", done=True) # If we found a suitable memory_cost value, we accept the key and the memory_cost. if MIN_KEY_DERIVATION_TIME <= kd_time <= MAX_KEY_DERIVATION_TIME: return memory_cost, master_key # The search might fail e.g. if external CPU load causes delay in key # derivation, which causes the search to continue into wrong branch. In # such a situation the search is restarted. The binary search is problematic # with tight key derivation time target ranges, so if the search keeps # restarting, increasing MAX_KEY_DERIVATION_TIME (and thus expanding the # range) will help finding suitable memory_cost value faster. Increasing # MAX_KEY_DERIVATION_TIME slightly affects security (positively) and user # experience (negatively). if memory_cost == lower_bound or memory_cost == upper_bound: lower_bound = ARGON2_MIN_MEMORY_COST upper_bound = self.get_available_memory() continue if kd_time < MIN_KEY_DERIVATION_TIME: lower_bound = memory_cost elif kd_time > MAX_KEY_DERIVATION_TIME: upper_bound = memory_cost
def client_establish_socket(self) -> None: """Establish IPC client.""" try: phase("Waiting for connection to NH", offset=11) while True: try: socket_number = 5000 if self.settings.data_diode_sockets else 5001 self.interface = multiprocessing.connection.Client( ('localhost', socket_number)) phase("Established", done=True) break except socket.error: time.sleep(0.1) except KeyboardInterrupt: graceful_exit()
def process_file( ts: 'datetime', # Timestamp of received_packet onion_pub_key: bytes, # Onion Service pubkey of sender file_ct: bytes, # File ciphertext file_key: bytes, # File decryption key contact_list: 'ContactList', # ContactList object window_list: 'WindowList', # WindowList object settings: 'Settings' # Settings object ) -> None: """Store file received from a contact.""" nick = contact_list.get_contact_by_pub_key(onion_pub_key).nick phase("Processing received file", head=1) try: file_pt = auth_and_decrypt(file_ct, file_key) except nacl.exceptions.CryptoError: raise FunctionReturn( f"Error: Decryption key for file from {nick} was invalid.") try: file_dc = decompress(file_pt, settings.max_decompress_size) except zlib.error: raise FunctionReturn(f"Error: Failed to decompress file from {nick}.") phase(DONE) print_on_previous_line(reps=2) try: file_name = bytes_to_str(file_dc[:PADDED_UTF32_STR_LENGTH]) except UnicodeError: raise FunctionReturn( f"Error: Name of file from {nick} had invalid encoding.") if not file_name.isprintable() or not file_name or '/' in file_name: raise FunctionReturn(f"Error: Name of file from {nick} was invalid.") f_data = file_dc[PADDED_UTF32_STR_LENGTH:] file_dir = f'{DIR_RECV_FILES}{nick}/' final_name = store_unique(f_data, 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 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_trickle: raise FunctionReturn("Command disabled during trickle connection.") try: device = user_input.plaintext.split()[1] except IndexError: raise FunctionReturn("No target system specified.") if device.lower() not in ['tx', 'txm', 'rx', 'rxm']: raise FunctionReturn("Invalid target system.") if device.lower() in ['rx', 'rxm']: queue_command(CHANGE_MASTER_K_HEADER, settings, queues[COMMAND_PACKET_QUEUE]) print('') return None old_master_key = master_key.master_key[:] master_key.new_master_key() new_master_key = master_key.master_key ensure_dir(f'{DIR_USER_DATA}/') file_name = f'{DIR_USER_DATA}/{settings.software_operation}_logs' if os.path.isfile(file_name): phase("Re-encrypting log-file") re_encrypt(old_master_key, new_master_key, settings) phase("Done") queues[KEY_MANAGEMENT_QUEUE].put(('KEY', master_key)) settings.store_settings() contact_list.store_contacts() group_list.store_groups() box_print("Master key successfully changed.", head=1) clear_screen(delay=1.5) except KeyboardInterrupt: raise FunctionReturn("Password change aborted.")
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 check_kernel_entropy() -> None: """Wait until the kernel CSPRNG is sufficiently seeded. Wait until the `entropy_avail` file states that kernel entropy pool has at least 512 bits of entropy. The waiting ensures the ChaCha20 CSPRNG is fully seeded (i.e., it has the maximum of 384 bits of entropy) when it generates keys. The same entropy threshold is used by the GETRANDOM syscall in random.c: #define CRNG_INIT_CNT_THRESH (2*CHACHA20_KEY_SIZE) For more information on the kernel CSPRNG threshold, see https://security.stackexchange.com/a/175771/123524 https://crypto.stackexchange.com/a/56377 """ message = "Waiting for kernel CSPRNG entropy pool to fill up" phase(message, head=1) ent_avail = 0 while ent_avail < ENTROPY_THRESHOLD: with ignored(EOFError, KeyboardInterrupt): with open('/proc/sys/kernel/random/entropy_avail') as f: ent_avail = int(f.read().strip()) m_print(f"{ent_avail}/{ENTROPY_THRESHOLD}") print_on_previous_line(delay=0.1) print_on_previous_line() phase(message) phase(DONE)
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 init_entropy() -> None: """Wait until Kernel CSPRNG is sufficiently seeded. Wait until entropy_avail file states that system has at least 512 bits of entropy. The headroom allows room for error in accuracy of entropy collector's entropy estimator; As long as input has at least 4 bits per byte of actual entropy, /dev/urandom will be sufficiently seeded when it is allowed to generate keys. """ clear_screen() phase("Waiting for Kernel CSPRNG random pool to fill up", head=1) ent_avail = 0 threshold = 512 while ent_avail < threshold: try: with open('/proc/sys/kernel/random/entropy_avail') as f: value = f.read() ent_avail = int(value.strip()) c_print("{}/{}".format(ent_avail, threshold)) print_on_previous_line(delay=0.01) except (KeyboardInterrupt, EOFError): pass print_on_previous_line() phase("Waiting for Kernel CSPRNG random pool to fill up") phase("Done")
def load_master_key(self) -> None: """Derive master key from password from stored values (salt, rounds, memory).""" ensure_dir(f'{DIR_USER_DATA}/') with open(self.file_name, 'rb') as f: data = f.read() salt = data[0:32] k_hash = data[32:64] rounds = bytes_to_int(data[64:72]) memory = bytes_to_int(data[72:80]) while True: password = MasterKey.get_password() phase("Deriving master key", head=2, offset=16) purp_key, _ = argon2_kdf(password, salt, rounds, memory, local_testing=self.local_test) if hash_chain(purp_key) == k_hash: phase("Password correct", done=True) self.master_key = purp_key clear_screen(delay=0.5) break else: phase("Invalid password", done=True) print_on_previous_line(reps=5, delay=1)
def search_serial_interface(self) -> str: """Search for serial interface.""" if self.settings.session_usb_iface: search_announced = False if not self.init_found: print_on_previous_line() phase("Searching for USB-to-serial interface") while True: time.sleep(0.1) for f in sorted(os.listdir('/dev')): if f.startswith('ttyUSB'): if self.init_found: time.sleep(1.5) phase('Found', done=True) if self.init_found: print_on_previous_line(reps=2) self.init_found = True return '/dev/{}'.format(f) else: if not search_announced: if self.init_found: phase( "Serial adapter disconnected. Waiting for interface", head=1) search_announced = True else: f = 'serial0' if 'Raspbian' in platform.platform() else 'ttyS0' if f in sorted(os.listdir('/dev/')): return '/dev/{}'.format(f) else: raise CriticalError("Error: /dev/{} was not found.".format(f))
def search_serial_interface(self) -> str: """Search for a serial interface.""" if self.settings.session_usb_serial_adapter: search_announced = False if not self.init_found: phase("Searching for USB-to-serial interface", offset=len('Found')) while True: for f in sorted(os.listdir('/dev/')): if f.startswith('ttyUSB'): if self.init_found: time.sleep(1) phase('Found', done=True) if self.init_found: print_on_previous_line(reps=2) self.init_found = True return f'/dev/{f}' time.sleep(0.1) if self.init_found and not search_announced: phase("Serial adapter disconnected. Waiting for interface", head=1, offset=len('Found')) search_announced = True else: if self.settings.built_in_serial_interface in sorted(os.listdir('/dev/')): return f'/dev/{self.settings.built_in_serial_interface}' raise CriticalError(f"Error: /dev/{self.settings.built_in_serial_interface} was not found.")
def ch_master_key(ts: 'datetime', window_list: 'WindowList', contact_list: 'ContactList', group_list: 'GroupList', key_list: 'KeyList', settings: 'Settings', master_key: 'MasterKey') -> None: """Prompt the user for a new master password and derive a new master key from that.""" try: old_master_key = master_key.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): change_log_db_key(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) m_print("Master password successfully changed.", bold=True, tail_clear=True, delay=1, head=1) local_win = window_list.get_local_window() local_win.add_new(ts, "Changed Receiver master password.") except (EOFError, KeyboardInterrupt): raise FunctionReturn("Password change aborted.", tail_clear=True, delay=1, head=2)
def process_imported_file(ts: 'datetime', packet: bytes, window_list: 'WindowList'): """Decrypt and store imported file.""" while True: try: print('') key = get_b58_key('imported_file') phase("Decrypting file", head=1) file_pt = auth_and_decrypt(packet[1:], key, soft_e=True) phase("Done") break except nacl.exceptions.CryptoError: c_print("Invalid decryption key. Try again.", head=2) print_on_previous_line(reps=6, delay=1.5) except KeyboardInterrupt: raise FunctionReturn("File import aborted.") try: phase("Decompressing file") file_dc = zlib.decompress(file_pt) phase("Done") except zlib.error: raise FunctionReturn("Decompression of file data failed.") try: f_name = bytes_to_str(file_dc[:1024]) except UnicodeError: raise FunctionReturn("Received file had an invalid name.") if not f_name.isprintable(): raise FunctionReturn("Received file had an invalid name.") f_data = file_dc[1024:] final_name = store_unique(f_data, DIR_IMPORTED, f_name) message = "Stored imported file to {}/{}".format(DIR_IMPORTED, final_name) box_print(message, head=1) local_win = window_list.get_local_window() local_win.print_new(ts, message, print_=False)
def load_master_key(self) -> None: """Derive master key from password and salt.""" with open(self.file_name, 'rb') as f: data = f.read() salt = data[0:32] key_hash = data[32:64] rounds = bytes_to_int(data[64:72]) memory = bytes_to_int(data[72:80]) parallelism = bytes_to_int(data[80:88]) while True: password = MasterKey.get_password() phase("Deriving master key", head=2, offset=16) purp_key, _ = argon2_kdf(password, salt, rounds, memory, parallelism) if hash_chain(purp_key) == key_hash: self.master_key = purp_key phase("Password correct", done=True) clear_screen(delay=0.5) break else: phase("Invalid password", done=True) print_on_previous_line(reps=5, delay=1)
def check_kernel_entropy() -> None: """Wait until Kernel CSPRNG is sufficiently seeded. Wait until entropy_avail file states that system has at least 512 bits of entropy. The headroom allows room for error in accuracy of entropy collector's entropy estimator; As long as input has at least 4 bits per byte of actual entropy, kernel CSPRNG will be sufficiently seeded when it generates 256-bit keys. """ clear_screen() phase("Waiting for Kernel CSPRNG entropy pool to fill up", head=1) ent_avail = 0 while ent_avail < ENTROPY_THRESHOLD: with ignored(EOFError, KeyboardInterrupt): with open('/proc/sys/kernel/random/entropy_avail') as f: value = f.read() ent_avail = int(value.strip()) c_print(f"{ent_avail}/{ENTROPY_THRESHOLD}") print_on_previous_line(delay=0.1) print_on_previous_line() phase("Waiting for Kernel CSPRNG entropy pool to fill up") phase(DONE)
def send_file(path: str, settings: 'Settings', queues: 'QueueDict', window: 'TxWindow' ) -> None: """Send file to window members in a single transmission. This is the default mode for file transmission, used when traffic masking is not enabled. The file is loaded and compressed before it is encrypted. The encrypted file is then exported to Networked Computer along with a list of Onion Service public keys (members in window) of all recipients to whom the Relay Program will multi-cast the file to. Once the file ciphertext has been exported, this function will multi-cast the file decryption key to each recipient inside an automated key delivery message that uses a special FILE_KEY_HEADER in place of standard PRIVATE_MESSAGE_HEADER. To know for which file ciphertext the key is for, an identifier must be added to the key delivery message. The identifier in this case is the BLAKE2b digest of the ciphertext itself. The reason of using the digest as the identifier is, it authenticates both the ciphertext and its origin. To understand this, consider the following attack scenario: Let the file ciphertext identifier be just a random 32-byte value "ID". 1) Alice sends Bob and Chuck (a malicious common peer) a file ciphertext and identifier CT|ID (where | denotes concatenation). 2) Chuck who has compromised Bob's Networked Computer interdicts the CT|ID from Alice. 3) Chuck decrypts CT in his end, makes edits to the plaintext PT to create PT'. 4) Chuck re-encrypts PT' with the same symmetric key to produce CT'. 5) Chuck re-uses the ID and produces CT'|ID. 6) Chuck uploads the CT'|ID to Bob's Networked Computer and replaces the interdicted CT|ID with it. 7) When Bob' Receiver Program receives the automated key delivery message from Alice, his Receiver program uses the bundled ID to identify the key is for CT'. 8) Bob's Receiver decrypts CT' using the newly received key and obtains Chuck's PT', that appears to come from Alice. Now, consider a situation where the ID is instead calculated ID = BLAKE2b(CT), if Chuck edits the PT, the CT' will by definition be different from CT, and the BLAKE2b digest will also be different. In order to make Bob decrypt CT', Chuck needs to also change the hash in Alice's key delivery message, which means Chuck needs to create an existential forgery of the TFC message. Since the Poly1305 tag prevents this, the calculated ID is enough to authenticate the ciphertext. If Chuck attempts to send their own key delivery message, Chuck's own Onion Service public key used to identify the TFC message key (decryption key for the key delivery message) will be permanently associated with the file hash, so if they inject a file CT, and Bob has decided to enable file reception for Chuck, the file CT will appear to come from Chuck, and not from Alice. From the perspective of Bob, it's as if Chuck had dropped Alice's file and sent him another file instead. """ from src.transmitter.windows import MockWindow # Avoid circular import if settings.traffic_masking: raise FunctionReturn("Error: Command is disabled during traffic masking.", head_clear=True) name = path.split('/')[-1] data = bytearray() data.extend(str_to_bytes(name)) if not os.path.isfile(path): raise FunctionReturn("Error: File not found.", head_clear=True) if os.path.getsize(path) == 0: raise FunctionReturn("Error: Target file is empty.", head_clear=True) phase("Reading data") with open(path, 'rb') as f: data.extend(f.read()) phase(DONE) print_on_previous_line(flush=True) phase("Compressing data") comp = bytes(zlib.compress(bytes(data), level=COMPRESSION_LEVEL)) phase(DONE) print_on_previous_line(flush=True) phase("Encrypting data") file_key = csprng() file_ct = encrypt_and_sign(comp, file_key) ct_hash = blake2b(file_ct) phase(DONE) print_on_previous_line(flush=True) phase("Exporting data") no_contacts = int_to_bytes(len(window)) ser_contacts = b''.join([c.onion_pub_key for c in window]) file_packet = FILE_DATAGRAM_HEADER + no_contacts + ser_contacts + file_ct queue_to_nc(file_packet, queues[RELAY_PACKET_QUEUE]) key_delivery_msg = base64.b85encode(ct_hash + file_key).decode() for contact in window: queue_message(user_input=UserInput(key_delivery_msg, MESSAGE), window =MockWindow(contact.onion_pub_key, [contact]), settings =settings, queues =queues, header =FILE_KEY_HEADER, log_as_ph =True) phase(DONE) print_on_previous_line(flush=True) m_print(f"Sent file '{name}' to {window.type_print} {window.name}.")
def test_phase(self, _): self.assertIsNone(phase('Entering phase')) self.assertIsNone(phase(DONE)) self.assertIsNone( phase('Starting phase', head=1, offset=len("Finished"))) self.assertIsNone(phase('Finished', done=True))
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 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)