def test_message_length(self): # Check that only 256-byte plaintext messages are ever allowed pub_key = nick_to_pub_key("Alice") for l in range(1, 256): with self.assertRaises(SystemExit): send_packet(self.key_list, self.gateway, self.l_queue, bytes(l), pub_key, True) for l in range(257, 300): with self.assertRaises(SystemExit): send_packet(self.key_list, self.gateway, self.l_queue, bytes(l), pub_key, True)
def process_command(queues: 'QueueDict', key_list: 'KeyList', gateway: 'Gateway') -> None: """Process command.""" c_queue = queues[COMMAND_PACKET_QUEUE] log_queue = queues[LOG_PACKET_QUEUE] if c_queue.qsize(): if key_list.has_local_keyset(): send_packet(key_list, gateway, log_queue, c_queue.get()) raise SoftError("Command processing complete.", output=False)
def process_buffered_messages(m_buffer: 'MessageBuffer', queues: 'QueueDict', key_list: 'KeyList', gateway: 'Gateway') -> None: """Process messages cached in `m_buffer`.""" log_queue = queues[LOG_PACKET_QUEUE] for onion_pub_key in m_buffer: if key_list.has_keyset(onion_pub_key) and m_buffer[onion_pub_key]: send_packet(key_list, gateway, log_queue, *m_buffer[onion_pub_key].pop(0)[:-1]) raise SoftError("Buffered message processing complete.", output=False)
def test_invalid_harac_raises_raises_struct_error(self): # Check that in the case where an internal error caused bytestring (possible key material) to end up in hash # ratchet value, the system raises some error that prevents the output of packet. In this case the, error comes # from the unsuccessful encoding of hash ratchet counter. for l in range(1, 33): key_list = KeyList() key_list.keysets = [create_keyset('Alice', tx_key=SYMMETRIC_KEY_LENGTH * b'\x02', tx_harac=l * b'k')] with self.assertRaises(struct.error): send_packet(key_list, self.gateway, self.l_queue, bytes(ASSEMBLY_PACKET_LENGTH), nick_to_pub_key("Alice"), True)
def process_new_message(m_buffer: 'MessageBuffer', queues: 'QueueDict', key_list: 'KeyList', gateway: 'Gateway') -> None: """Process new message in message queue.""" m_queue = queues[MESSAGE_PACKET_QUEUE] log_queue = queues[LOG_PACKET_QUEUE] if m_queue.qsize(): queue_data = m_queue.get( ) # type: Tuple[bytes, bytes, bool, bool, bytes] onion_pub_key = queue_data[1] if key_list.has_keyset(onion_pub_key): send_packet(key_list, gateway, log_queue, *queue_data[:-1]) else: m_buffer.setdefault(onion_pub_key, []).append(queue_data) raise SoftError("New message processing complete.", output=False)
def test_valid_message_packet(self): # Setup gateway = Gateway(serial_error_correction=5) key_list = KeyList(master_key=bytes(SYMMETRIC_KEY_LENGTH)) key_list.keysets = [create_keyset('Alice', tx_key=SYMMETRIC_KEY_LENGTH * b'\x02', tx_harac=8)] # Test self.assertIsNone(send_packet(key_list, gateway, self.l_queue, bytes(ASSEMBLY_PACKET_LENGTH), nick_to_pub_key("Alice"), True)) self.assertEqual(len(gateway.packets), 1) time.sleep(0.01) self.assertFalse(self.l_queue.empty())
def test_valid_command_packet(self): """Test that commands are output as they should. Since command packets have no trailer, and since only user's Receiver Program has local decryption key, encryption with any key recipient is not already in possession of does not compromise plaintext. """ # Setup key_list = KeyList(master_key=bytes(SYMMETRIC_KEY_LENGTH)) key_list.keysets = [create_keyset(LOCAL_ID)] # Test self.assertIsNone(send_packet(key_list, self.gateway, self.l_queue, bytes(ASSEMBLY_PACKET_LENGTH))) self.assertEqual(len(self.gateway.packets), 1) self.assertEqual(len(self.gateway.packets[0]), 345) self.assertEqual(self.l_queue.qsize(), 1)
def traffic_masking_loop( queues: 'QueueDict', settings: 'Settings', gateway: 'Gateway', key_list: 'KeyList', ) -> 'Settings': """Run Transmitter Program in traffic masking mode. The traffic masking loop loads assembly packets from a set of queues. As Python's multiprocessing lacks priority queues, several queues are prioritized based on their status. Files are only transmitted when messages are not being output: This is because file transmission is usually very slow and the user might need to send messages in the meantime. Command datagrams are output from Source Computer between each message datagram. The frequency in output allows commands to take effect as soon as possible but this unfortunately slows down message/file delivery by half. Each contact in the window is cycled in order. When this loop is active, making changes to the recipient list is prevented to protect the user from accidentally revealing the use of TFC. The traffic is masked the following way: If both m_queue and f_queue are empty, a noise assembly packet is loaded from np_queue. If no command packet is available in c_queue, a noise command packet is loaded from nc_queue. Both noise queues are filled by independent processes that ensure both noise queues always have packets to output. TFC does its best to hide the assembly packet loading times and encryption duration by using constant time context manager with CSPRNG spawned jitter, constant time queue status lookup and constant time XChaCha20 cipher. However, since TFC is written in a high-level language, it is impossible to guarantee Source Computer never reveals to Networked Computer when the user operates the Source Computer. """ ws_queue = queues[WINDOW_SELECT_QUEUE] m_queue = queues[TM_MESSAGE_PACKET_QUEUE] f_queue = queues[TM_FILE_PACKET_QUEUE] c_queue = queues[TM_COMMAND_PACKET_QUEUE] np_queue = queues[TM_NOISE_PACKET_QUEUE] nc_queue = queues[TM_NOISE_COMMAND_QUEUE] log_queue = queues[LOG_PACKET_QUEUE] sm_queue = queues[SENDER_MODE_QUEUE] while True: with ignored(EOFError, KeyboardInterrupt): while ws_queue.qsize() == 0: time.sleep(0.01) window_contacts = ws_queue.get() # Window selection command to Receiver Program. while c_queue.qsize() == 0: time.sleep(0.01) send_packet(key_list, gateway, log_queue, c_queue.get()) break while True: with ignored(EOFError, KeyboardInterrupt): # Load message/file assembly packet. with HideRunTime(settings, duration=TRAFFIC_MASKING_QUEUE_CHECK_DELAY): # Choosing element from list is constant time. # # First queue we evaluate: if m_queue has data Second to evaluate. If m_queue # in it, False is evaluated as 0, and we load has no data but f_queue has, the # the first nested list. At that point we load False is evaluated as 0 meaning # from m_queue regardless of f_queue state. f_queue (True as 1 and np_queue) # | | # v v queue = [[m_queue, m_queue], [f_queue, np_queue] ][m_queue.qsize() == 0][f_queue.qsize() == 0] # Regardless of queue, each .get() returns a tuple with identical # amount of data: 256 bytes long bytestring and two booleans. assembly_packet, log_messages, log_as_ph = queue.get( ) # type: bytes, bool, bool for c in window_contacts: # Message/file assembly packet to window contact. with HideRunTime(settings, delay_type=TRAFFIC_MASKING): send_packet(key_list, gateway, log_queue, assembly_packet, c.onion_pub_key, log_messages) # Send a command between each assembly packet for each contact. with HideRunTime(settings, delay_type=TRAFFIC_MASKING): # Choosing element from list is constant time. queue = [c_queue, nc_queue][c_queue.qsize() == 0] # Each loaded command and noise command is a 256 long bytestring. command = queue.get() # type: bytes send_packet(key_list, gateway, log_queue, command) exit_packet_check(queues, gateway) # If traffic masking has been disabled, wait until queued messages are sent before returning. if sm_queue.qsize() != 0 and all( q.qsize() == 0 for q in (m_queue, f_queue, c_queue)): settings = sm_queue.get() return settings
def standard_sender_loop(queues: 'QueueDict', gateway: 'Gateway', key_list: 'KeyList', m_buffer: Optional['Message_buffer'] = None ) -> Tuple['Settings', 'Message_buffer']: """Run Transmitter program in standard send mode. The standard sender loop loads assembly packets from a set of queues. As Python's multiprocessing lacks priority queues, several queues are prioritized based on their status: KEY_MANAGEMENT_QUEUE has the highest priority. This is to ensure the no queued message/command is encrypted with expired keyset. COMMAND_PACKET_QUEUE has the second highest priority, to ensure commands are issued swiftly to Receiver program. Some commands like screen clearing might need to be issued quickly. RELAY_PACKET_QUEUE has third highest priority. These are still commands but since Relay Program does not handle sensitive data, issuing commands to that devices does not take priority. Buffered messages have fourth highest priority. This ensures that if for whatever reason the keyset is removed, buffered messages do not get lost. Packets are loaded from the buffer in FIFO basis ensuring packets arrive to the recipient in order. MESSAGE_PACKET_QUEUE has fifth highest priority. Any buffered messages need to arrive earlier, thus new messages must be prioritized after the buffered ones. SENDER_MODE_QUEUE has sixth highest priority. This prevents outgoing packets from being left in the queues used by this loop. This queue returns up-to-date settings object for `sender_loop` parent loop, that in turn uses it to start `traffic_masking_loop`. Along with settings, this function returns the m_buffer status so that assembly packets that could not have been sent due to missing key can be output later, if the user resumes to standard_sender_loop and adds new keys for the contact. """ km_queue = queues[KEY_MANAGEMENT_QUEUE] c_queue = queues[COMMAND_PACKET_QUEUE] rp_queue = queues[RELAY_PACKET_QUEUE] m_queue = queues[MESSAGE_PACKET_QUEUE] sm_queue = queues[SENDER_MODE_QUEUE] log_queue = queues[LOG_PACKET_QUEUE] if m_buffer is None: m_buffer = dict() while True: with ignored(EOFError, KeyboardInterrupt): if km_queue.qsize() != 0: key_list.manage(*km_queue.get()) continue # Commands to Receiver if c_queue.qsize() != 0: if key_list.has_local_keyset(): send_packet(key_list, gateway, log_queue, c_queue.get()) continue # Commands/files to Networked Computer if rp_queue.qsize() != 0: packet = rp_queue.get() gateway.write(packet) command = packet[DATAGRAM_HEADER_LENGTH:] if command in [UNENCRYPTED_EXIT_COMMAND, UNENCRYPTED_WIPE_COMMAND]: time.sleep(gateway.settings.local_testing_mode * 0.1) time.sleep(gateway.settings.data_diode_sockets * 1.5) signal = WIPE if command == UNENCRYPTED_WIPE_COMMAND else EXIT queues[EXIT_QUEUE].put(signal) continue # Buffered messages for onion_pub_key in m_buffer: if key_list.has_keyset(onion_pub_key) and m_buffer[onion_pub_key]: send_packet(key_list, gateway, log_queue, *m_buffer[onion_pub_key].pop(0)[:-1]) continue # New messages if m_queue.qsize() != 0: queue_data = m_queue.get() # type: Tuple[bytes, bytes, bool, bool, bytes] onion_pub_key = queue_data[1] if key_list.has_keyset(onion_pub_key): send_packet(key_list, gateway, log_queue, *queue_data[:-1]) else: m_buffer.setdefault(onion_pub_key, []).append(queue_data) continue # If traffic masking has been enabled, switch send mode when all queues are empty. if sm_queue.qsize() != 0 and all(q.qsize() == 0 for q in (km_queue, c_queue, rp_queue, m_queue)): settings = sm_queue.get() return settings, m_buffer time.sleep(0.01)