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 store_unique( file_data: bytes, # File data to store file_dir: str, # Directory to store file file_name: str, # Name of the file. ) -> None: """Store file under a unique filename. Add trailing counter .# to ensure buffered packets are read in order. """ ensure_dir(file_dir) try: file_numbers = [ f[(len(file_name) + len('.')):] for f in os.listdir(file_dir) if f.startswith(file_name) ] file_numbers = [n for n in file_numbers if n.isdigit()] greatest_num = sorted(file_numbers, key=int)[-1] ctr = int(greatest_num) + 1 except IndexError: ctr = 0 with open(f"{file_dir}/{file_name}.{ctr}", 'wb+') as f: f.write(file_data) f.flush() os.fsync(f.fileno())
def change_log_db_key(previous_key: bytes, new_key: bytes, settings: 'Settings') -> None: """Re-encrypt log database with a new master key.""" ensure_dir(DIR_USER_DATA) file_name = f'{DIR_USER_DATA}{settings.software_operation}_logs' temp_name = f'{file_name}_temp' if not os.path.isfile(file_name): raise FunctionReturn("Error: Could not find log database.") if os.path.isfile(temp_name): os.remove(temp_name) f_old = open(file_name, 'rb') f_new = open(temp_name, 'ab+') for ct in iter(lambda: f_old.read(LOG_ENTRY_LENGTH), b''): pt = auth_and_decrypt(ct, key=previous_key, database=file_name) f_new.write(encrypt_and_sign(pt, key=new_key)) f_old.close() f_new.close() os.remove(file_name) os.rename(temp_name, file_name)
def test_invalid_type_is_replaced_with_default(self) -> None: # Setup ensure_dir(DIR_USER_DATA) with open(f"{DIR_USER_DATA}{TX}_serial_settings.json", 'w+') as f: f.write("""\ { "serial_baudrate": "115200", "serial_error_correction": "5", "use_serial_usb_adapter": "true", "built_in_serial_interface": true }""") # Test settings = GatewaySettings(operation=TX, local_test=True, dd_sockets=True, qubes=False) self.assertEqual(settings.serial_baudrate, 19200) self.assertEqual(settings.serial_error_correction, 5) self.assertEqual(settings.use_serial_usb_adapter, True) self.assertEqual(settings.built_in_serial_interface, 'ttyS0') with open(settings.file_name) as f: data = f.read() self.assertEqual(data, self.default_serialized)
def get_message(purp_url_token: str, queues: 'QueueDict', pub_key_dict: 'PubKeyDict', buf_key: bytes) -> str: """Send queued messages to contact.""" if not validate_url_token(purp_url_token, queues, pub_key_dict): return '' identified_onion_pub_key = pub_key_dict[purp_url_token] # Load outgoing messages for all contacts, # return the oldest message for contact sub_dir = hashlib.blake2b(identified_onion_pub_key, key=buf_key, digest_size=BLAKE2_DIGEST_LENGTH).hexdigest() buf_dir = f"{RELAY_BUFFER_OUTGOING_M_DIR}/{sub_dir}/" ensure_dir(buf_dir) packets = [] while len(os.listdir(buf_dir)) > 0: packet_ct, db = read_buffer_file(buf_dir, RELAY_BUFFER_OUTGOING_MESSAGE) packet = auth_and_decrypt(packet_ct, key=buf_key, database=f"{buf_dir}{db}") packets.append(packet.decode()) if packets: all_message_packets = '\n'.join(packets) return all_message_packets return ''
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 change_master_key(ts: 'datetime', window_list: 'WindowList', contact_list: 'ContactList', group_list: 'GroupList', key_list: 'KeyList', settings: 'Settings', master_key: 'MasterKey') -> None: """Derive new master key based on master password delivered by TxM.""" 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') key_list.store_keys() settings.store_settings() contact_list.store_contacts() group_list.store_groups() box_print("Master key successfully changed.", head=1) clear_screen(delay=1.5) local_win = window_list.get_window('local') local_win.print_new(ts, "Changed RxM master key.", print_=False)
def store_groups(self) -> None: """Write the list of groups to an encrypted database. This function will first generate a header that stores information about the group database content and padding at the moment of calling. Next, the function will serialize every Group object (including dummy groups) to form the constant length plaintext that will be encrypted and stored in the database. By default, TFC has a maximum number of 50 groups with 50 members. In addition, the group database stores the header that contains four 8-byte values. The database plaintext length with 50 groups, each with 50 members is 4*8 + 50*(1024 + 4 + 2*1 + 50*32) = 32 + 50*2630 = 131532 bytes. The ciphertext includes a 24-byte nonce and a 16-byte tag, so the size of the final database is 131572 bytes. """ pt_bytes = self._generate_group_db_header() pt_bytes += b''.join( [g.serialize_g() for g in (self.groups + self._dummy_groups())]) ct_bytes = encrypt_and_sign(pt_bytes, self.master_key.master_key) ensure_dir(DIR_USER_DATA) with open(self.file_name, 'wb+') as f: f.write(ct_bytes)
def store_settings(self) -> None: """Store settings to an encrypted database. The plaintext in the encrypted database is a constant length bytestring regardless of stored setting values. """ attribute_list = [self.__getattribute__(k) for k in self.key_list] bytes_lst = [] for a in attribute_list: if isinstance(a, bool): bytes_lst.append(bool_to_bytes(a)) elif isinstance(a, int): bytes_lst.append(int_to_bytes(a)) elif isinstance(a, float): bytes_lst.append(double_to_bytes(a)) else: raise CriticalError("Invalid attribute type in settings.") pt_bytes = b''.join(bytes_lst) ct_bytes = encrypt_and_sign(pt_bytes, self.master_key.master_key) ensure_dir(DIR_USER_DATA) with open(self.file_name, 'wb+') as f: f.write(ct_bytes)
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 __init__(self, local_testing: bool, dd_sockets: bool, operation=NH) -> None: # Fixed settings self.relay_to_im_client = True # False stops forwarding messages to IM client # Controllable settings self.serial_usb_adapter = True # False uses system's integrated serial interface self.disable_gui_dialog = False # True replaces Tkinter dialogs with CLI prompts self.serial_baudrate = 19200 # The speed of serial interface in bauds per second self.serial_error_correction = 5 # Number of byte errors serial datagrams can recover from self.software_operation = operation self.file_name = '{}{}_settings'.format(DIR_USER_DATA, operation) # Settings from launcher / CLI arguments self.local_testing_mode = local_testing self.data_diode_sockets = dd_sockets ensure_dir(DIR_USER_DATA) if os.path.isfile(self.file_name): self.load_settings() else: self.setup() self.store_settings() # Following settings change only when program is restarted self.session_serial_error_correction = self.serial_error_correction self.session_serial_baudrate = self.serial_baudrate self.race_condition_delay = calculate_race_condition_delay(self) self.receive_timeout, self.transmit_delay = calculate_serial_delays( self.session_serial_baudrate)
def get_file( purp_url_token: str, queues: 'QueueDict', pub_key_dict: 'PubKeyDict', buf_key: bytes, ) -> Any: """Send queued files to contact.""" if not validate_url_token(purp_url_token, queues, pub_key_dict): return '' identified_onion_pub_key = pub_key_dict[purp_url_token] sub_dir = hashlib.blake2b(identified_onion_pub_key, key=buf_key, digest_size=BLAKE2_DIGEST_LENGTH).hexdigest() buf_dir = f"{RELAY_BUFFER_OUTGOING_F_DIR}/{sub_dir}/" ensure_dir(buf_dir) if len(os.listdir(buf_dir)) > 0: packet_ct, db = read_buffer_file(buf_dir, RELAY_BUFFER_OUTGOING_FILE) packet = auth_and_decrypt(packet_ct, key=buf_key, database=f"{buf_dir}{db}") mem = BytesIO() mem.write(packet) mem.seek(0) return send_file(mem, mimetype="application/octet-stream") return ''
def re_encrypt(previous_key: bytes, new_key: bytes, settings: 'Settings') -> None: """Re-encrypt database with a new master key.""" ensure_dir(f'{DIR_USER_DATA}/') file_name = f'{DIR_USER_DATA}/{settings.software_operation}_logs' temp_name = f'{DIR_USER_DATA}/{settings.software_operation}_logs_temp' if not os.path.isfile(file_name): raise FunctionReturn(f"Error: Could not find '{file_name}'.") if os.path.isfile(temp_name): os.remove(temp_name) f_old = open(file_name, 'rb') f_new = open(temp_name, 'ab+') def read_entry(): """Read log entry.""" return f_old.read(1325) for ct_old in iter(read_entry, b''): pt_new = auth_and_decrypt(ct_old, key=previous_key) f_new.write(encrypt_and_sign(pt_new, key=new_key)) f_old.close() f_new.close() os.remove(file_name) os.rename(temp_name, file_name)
def test_qubes_read_file(self, *_: Any) -> None: # Setup ensure_dir(f"{QUBES_BUFFER_INCOMING_DIR}/") def packet_delayer() -> None: """Create packets one at a time.""" time.sleep(0.1) with open(f"{QUBES_BUFFER_INCOMING_DIR}/{QUBES_BUFFER_INCOMING_PACKET}.invalid", 'wb+') as fp: fp.write(base64.b85encode(b'data')) time.sleep(0.1) with open(f"{QUBES_BUFFER_INCOMING_DIR}/{QUBES_BUFFER_INCOMING_PACKET}.0", 'wb+') as fp: fp.write(base64.b85encode(b'data')) threading.Thread(target=packet_delayer).start() gateway = Gateway(operation=RX, local_test=False, dd_sockets=False, qubes=True) # Test self.assert_se("No packet was available.", gateway.read) time.sleep(0.3) self.assertIsInstance(gateway, Gateway) self.assertEqual(gateway.read(), b'data') # Test invalid packet content is handled with open(f"{QUBES_BUFFER_INCOMING_DIR}/{QUBES_BUFFER_INCOMING_PACKET}.1", 'wb+') as f: f.write(os.urandom(32)) self.assert_se("Error: Received packet had invalid Base85 encoding.", gateway.read)
def test_unknown_kv_pair_is_removed(self) -> None: # Setup ensure_dir(DIR_USER_DATA) with open(f"{DIR_USER_DATA}{TX}_serial_settings.json", 'w+') as f: f.write("""\ { "serial_baudrate": 19200, "serial_error_correction": 5, "use_serial_usb_adapter": true, "built_in_serial_interface": "ttyS0", "this_should_not_be_here": 1 }""") # Test settings = GatewaySettings(operation=TX, local_test=True, dd_sockets=True) self.assertEqual(settings.serial_baudrate, 19200) self.assertEqual(settings.serial_error_correction, 5) self.assertEqual(settings.use_serial_usb_adapter, True) self.assertEqual(settings.built_in_serial_interface, 'ttyS0') with open(settings.file_name) as f: data = f.read() self.assertEqual(data, self.default_serialized)
def new_master_key(self) -> None: """Create a new master key from salt and password. The number of rounds starts at 1 but is increased dynamically based on system performance. This allows more security on faster platforms without additional cost on key derivation time. """ password = MasterKey.new_password() salt = keygen() rounds = 1 phase("Deriving master key", head=2) while True: time_start = time.monotonic() master_key, memory = argon2_kdf(password, salt, rounds, local_testing=self.local_test) time_final = time.monotonic() - time_start if time_final > 3.0: self.master_key = master_key master_key_hash = hash_chain(master_key) ensure_dir(f'{DIR_USER_DATA}/') with open(self.file_name, 'wb+') as f: f.write(salt + master_key_hash + int_to_bytes(rounds) + int_to_bytes(memory)) phase('Done') break else: rounds *= 2
def read_qubes_buffer_file(buffer_file_dir: str = '') -> bytes: """Read packet from oldest buffer file.""" buffer_file_dir = buffer_file_dir if buffer_file_dir else BUFFER_FILE_DIR ensure_dir(f"{buffer_file_dir}/") while not any([f for f in os.listdir(buffer_file_dir) if f.startswith(BUFFER_FILE_NAME)]): time.sleep(0.001) tfc_buffer_file_numbers = [f[(len(BUFFER_FILE_NAME)+len('.')):] for f in os.listdir(buffer_file_dir) if f.startswith(BUFFER_FILE_NAME)] tfc_buffer_file_numbers = [n for n in tfc_buffer_file_numbers if n.isdigit()] tfc_buffer_files_in_order = [f"{BUFFER_FILE_NAME}.{n}" for n in sorted(tfc_buffer_file_numbers, key=int)] try: oldest_buffer_file = tfc_buffer_files_in_order[0] except IndexError: raise SoftError("No packet was available.", output=False) with open(f"{buffer_file_dir}/{oldest_buffer_file}", 'rb') as f: packet = f.read() try: packet = base64.b85decode(packet) except ValueError: raise SoftError("Error: Received packet had invalid Base85 encoding.") os.remove(f"{buffer_file_dir}/{oldest_buffer_file}") return packet
def setUp(self): self.unittest_dir = cd_unittest() self.msg = ("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean condimentum consectetur purus quis" " dapibus. Fusce venenatis lacus ut rhoncus faucibus. Cras sollicitudin commodo sapien, sed bibendu" "m velit maximus in. Aliquam ac metus risus. Sed cursus ornare luctus. Integer aliquet lectus id ma" "ssa blandit imperdiet. Ut sed massa eget quam facilisis rutrum. Mauris eget luctus nisl. Sed ut el" "it iaculis, faucibus lacus eget, sodales magna. Nunc sed commodo arcu. In hac habitasse platea dic" "tumst. Integer luctus aliquam justo, at vestibulum dolor iaculis ac. Etiam laoreet est eget odio r" "utrum, vel malesuada lorem rhoncus. Cras finibus in neque eu euismod. Nulla facilisi. Nunc nec ali" "quam quam, quis ullamcorper leo. Nunc egestas lectus eget est porttitor, in iaculis felis sceleris" "que. In sem elit, fringilla id viverra commodo, sagittis varius purus. Pellentesque rutrum loborti" "s neque a facilisis. Mauris id tortor placerat, aliquam dolor ac, venenatis arcu.") self.ts = datetime.now() self.master_key = MasterKey() self.settings = Settings(log_file_masking=True) self.file_name = f'{DIR_USER_DATA}{self.settings.software_operation}_logs' self.contact_list = ContactList(nicks=['Alice', 'Bob', 'Charlie', LOCAL_ID]) self.key_list = KeyList( nicks=['Alice', 'Bob', 'Charlie', LOCAL_ID]) self.group_list = GroupList( groups=['test_group']) self.packet_list = PacketList(contact_list=self.contact_list, settings=self.settings) self.window_list = WindowList(contact_list=self.contact_list, settings=self.settings, group_list=self.group_list, packet_list=self.packet_list) self.group_id = group_name_to_group_id('test_group') self.file_keys = dict() self.group_list.get_group('test_group').log_messages = True self.args = (self.window_list, self.packet_list, self.contact_list, self.key_list, self.group_list, self.settings, self.master_key, self.file_keys) ensure_dir(DIR_USER_DATA)
def store_onion_service_private_key(self) -> None: """Store Onion Service private key to an encrypted database.""" ct_bytes = encrypt_and_sign(self.onion_private_key, self.master_key.master_key) ensure_dir(DIR_USER_DATA) with open(self.file_name, 'wb+') as f: f.write(ct_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 store_settings(self) -> None: """Store persistent settings to file.""" setting_data = int_to_bytes(self.serial_iface_speed) setting_data += int_to_bytes(self.e_correction_ratio) setting_data += bool_to_bytes(self.serial_usb_adapter) setting_data += bool_to_bytes(self.disable_gui_dialog) ensure_dir('{}/'.format(DIR_USER_DATA)) open(self.file_name, 'wb+').write(setting_data)
def replace_log_db(settings: 'Settings') -> None: """Replace the log database with the temp file.""" ensure_dir(DIR_USER_DATA) file_name = f'{DIR_USER_DATA}{settings.software_operation}_logs' temp_name = file_name + TEMP_SUFFIX if os.path.isfile(temp_name): os.replace(temp_name, file_name)
def store_database(self, pt_bytes: bytes, replace: bool = True) -> None: """Encrypt and store data into database.""" ct_bytes = encrypt_and_sign(pt_bytes, self.database_key) ensure_dir(DIR_USER_DATA) self.ensure_temp_write(ct_bytes) if replace: self.replace_database()
def load_settings(self) -> None: """Load persistent settings from file.""" ensure_dir('{}/'.format(DIR_USER_DATA)) settings = open(self.file_name, 'rb').read() self.serial_iface_speed = bytes_to_int(settings[0:8]) self.e_correction_ratio = bytes_to_int(settings[8:16]) self.serial_usb_adapter = bytes_to_bool(settings[16:17]) self.disable_gui_dialog = bytes_to_bool(settings[17:18])
def __init__( self, master_key: 'MasterKey', # MasterKey object operation: str, # Operation mode of the program (Tx or Rx) local_test: bool, # Local testing setting from command-line argument qubes: bool = False # Qubes setting from command-line argument ) -> None: """Create a new Settings object. The settings below are defaults, and are only to be altered from within the program itself. Changes made to the default settings are stored in the encrypted settings database, from which they are loaded when the program starts. """ # Common settings self.disable_gui_dialog = False self.max_number_of_group_members = 50 self.max_number_of_groups = 50 self.max_number_of_contacts = 50 self.log_messages_by_default = False self.accept_files_by_default = False self.show_notifications_by_default = True self.log_file_masking = False self.ask_password_for_log_access = True # Transmitter settings self.nc_bypass_messages = False self.confirm_sent_files = True self.double_space_exits = False self.traffic_masking = False self.tm_static_delay = 2.0 self.tm_random_delay = 2.0 # Relay Settings self.allow_contact_requests = True # Receiver settings self.new_message_notify_preview = False self.new_message_notify_duration = 1.0 self.max_decompress_size = 100_000_000 self.master_key = master_key self.software_operation = operation self.local_testing_mode = local_test self.qubes = qubes self.file_name = f'{DIR_USER_DATA}{operation}_settings' self.database = TFCDatabase(self.file_name, master_key) self.all_keys = list(vars(self).keys()) self.key_list = self.all_keys[:self.all_keys.index('master_key')] self.defaults = {k: self.__dict__[k] for k in self.key_list} ensure_dir(DIR_USER_DATA) if os.path.isfile(self.file_name): self.load_settings() else: self.store_settings()
def store_contacts(self) -> None: """Write contacts to encrypted database.""" contacts = self.contacts + [self.dummy_contact] * (self.settings.max_number_of_contacts - len(self.contacts)) pt_bytes = b''.join([c.serialize_c() for c in contacts]) ct_bytes = encrypt_and_sign(pt_bytes, self.master_key.master_key) ensure_dir(DIR_USER_DATA) with open(self.file_name, 'wb+') as f: f.write(ct_bytes)
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 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 store_settings(self) -> None: """Store persistent settings to file.""" setting_data = int_to_bytes(self.serial_baudrate) setting_data += int_to_bytes(self.serial_error_correction) setting_data += bool_to_bytes(self.serial_usb_adapter) setting_data += bool_to_bytes(self.disable_gui_dialog) ensure_dir(DIR_USER_DATA) with open(self.file_name, 'wb+') as f: f.write(setting_data)
def store_database(self, pt_bytes: bytes, replace: bool = True) -> None: """Encrypt and store data into database.""" ct_bytes = encrypt_and_sign(pt_bytes, self.database_key) ensure_dir(DIR_USER_DATA) self.ensure_temp_write(ct_bytes) # Replace the original file with a temp file. (`os.replace` is atomic as per # POSIX requirements): https://docs.python.org/3/library/os.html#os.replace if replace: self.replace_database()