def test_message_length(self): # Check that only 256-byte plaintext messages are ever allowed for l in range(1, 256): with self.assertRaises(SystemExit): send_packet(self.key_list, self.gateway, self.l_queue, bytes(l), self.settings, '*****@*****.**', '*****@*****.**', True) for l in range(257, 300): with self.assertRaises(SystemExit): send_packet(self.key_list, self.gateway, self.l_queue, bytes(l), self.settings, '*****@*****.**', '*****@*****.**', True)
def test_invalid_account_raises_stop_iteration(self): # Check that in case where internal error caused bytestring (possible key material) # to end up in account strings, System raises some error that prevents output of packet. # In this case the error comes from unsuccessful encoding of string (AttributeError) # or KeyList lookup error when bytes are used (StopIteration). These errors are not catched. with self.assertRaises(StopIteration): send_packet(self.key_list, self.gateway, self.l_queue, bytes(ASSEMBLY_PACKET_LEN), self.settings, b'*****@*****.**', '*****@*****.**', True) with self.assertRaises(AttributeError): send_packet(self.key_list, self.gateway, self.l_queue, bytes(ASSEMBLY_PACKET_LEN), self.settings, '*****@*****.**', b'*****@*****.**', True)
def test_invalid_harac_raises_raises_struct_error(self): # Check that in case where internal error caused bytestring (possible key material) # to end up in hash ratchet value, system raises some error that prevents output of packet. # In this case the error comes from unsuccessful encoding of hash ratchet counter. for l in range(1, 33): key_list = KeyList() key_list.keysets = [ create_keyset(tx_key=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_LEN), self.settings, '*****@*****.**', '*****@*****.**', True)
def test_message_length(self): # Setup key_list = KeyList() settings = Settings() gateway = Gateway() l_queue = Queue() # Check that only 256-byte plaintext messages are ever allowed for l in range(1, 256): with self.assertRaises(SystemExit): send_packet(bytes(l), key_list, settings, gateway, l_queue, '*****@*****.**', '*****@*****.**', True) for l in range(257, 300): with self.assertRaises(SystemExit): send_packet(bytes(l), key_list, settings, gateway, l_queue, '*****@*****.**', '*****@*****.**', True)
def test_invalid_account_crashes(self): # Setup settings = Settings() gateway = Gateway() l_queue = Queue() key_list = KeyList() key_list.keysets = [create_keyset('Alice')] # Check that in case where internal error caused bytestring (possible key material) # to end up in account strings, System raises some error that prevents output of packet. # In this case the error comes from unsuccessful encoding of string (AttributeError) # or KeyList lookup error when bytes are used (StopIteration). These errors are not catched. with self.assertRaises(StopIteration): send_packet(bytes(256), key_list, settings, gateway, l_queue, b'*****@*****.**', '*****@*****.**', True) with self.assertRaises(AttributeError): send_packet(bytes(256), key_list, settings, gateway, l_queue, '*****@*****.**', b'*****@*****.**', True)
def test_invalid_harac_crashes(self): # Setup settings = Settings() gateway = Gateway() l_queue = Queue() # Check that in case where internal error caused bytestring (possible key material) # to end up in hash ratchet value, system raises some error that prevents output of packet. # In this case the error comes from unsuccessful encoding of hash ratchet counter. for l in range(1, 32): key_list = KeyList() key_list.keysets = [ create_keyset(tx_hek=32 * b'\x01', tx_key=32 * b'\x02', tx_harac=l * b'k') ] with self.assertRaises(struct.error): send_packet(bytes(256), key_list, settings, gateway, l_queue, '*****@*****.**', '*****@*****.**', True)
def test_valid_message_packet(self): # Setup settings = Settings(long_packet_rand_d=True) gateway = Gateway() l_queue = Queue() key_list = KeyList(master_key=bytes(32)) key_list.keysets = [ create_keyset(tx_hek=32 * b'\x01', tx_key=32 * b'\x02', tx_harac=8) ] # Test self.assertIsNone( send_packet(bytes(256), key_list, settings, gateway, l_queue, '*****@*****.**', '*****@*****.**', True)) self.assertEqual(len(gateway.packets), 1) self.assertEqual(len(gateway.packets[0]), 396) time.sleep(0.2) self.assertFalse(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 RxM 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(KEY_LENGTH)) key_list.keysets = [create_keyset(LOCAL_ID)] # Test self.assertIsNone( send_packet(key_list, self.gateway, self.l_queue, bytes(ASSEMBLY_PACKET_LEN), self.settings)) time.sleep(0.1) self.assertEqual(len(self.gateway.packets), 1) self.assertEqual(len(self.gateway.packets[0]), 365) self.assertEqual(self.l_queue.qsize(), 1)
def test_valid_message_packet(self): # Setup settings = Settings(multi_packet_random_delay=True) gateway = Gateway() key_list = KeyList(master_key=bytes(KEY_LENGTH)) key_list.keysets = [ create_keyset(tx_key=KEY_LENGTH * b'\x02', tx_harac=8) ] # Test self.assertIsNone( send_packet(key_list, gateway, self.l_queue, bytes(ASSEMBLY_PACKET_LEN), settings, '*****@*****.**', '*****@*****.**', True)) self.assertEqual(len(gateway.packets), 1) self.assertEqual(len(gateway.packets[0]), 396) time.sleep(0.1) 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 RxM has local decryption key, encryption with any key recipient is not already in possession of does not compromise plaintext. """ # Setup settings = Settings() gateway = Gateway() l_queue = Queue() key_list = KeyList(master_key=bytes(32)) key_list.keysets = [create_keyset('local')] # Test self.assertIsNone( send_packet(bytes(256), key_list, settings, gateway, l_queue)) self.assertEqual(len(gateway.packets), 1) self.assertEqual(len(gateway.packets[0]), 365) time.sleep(0.2) self.assertTrue(l_queue.empty())
def sender_loop(settings: 'Settings', queues: Dict[bytes, 'Queue'], gateway: 'Gateway', key_list: 'KeyList') -> None: """Load assembly packets from queues based on their priority, encrypt and output them. Sender loop handles a set of queues. As Python's multiprocessing lacks priority queues, several queues are prioritized based on their status. In both trickle and non-trickle mode, file are only transmitted when no messages are being output. This is because file transmission is usually very slow and user might need to send messages in the meantime. In normal (non-trickle) mode commands take highest priority as they are not output all the time. In trickle mode commands are output between each output message packet. This allows commands to take effect as soon as possible but slows down message/file delivery by half. In trickle mode each contact in window is cycled in order. Making changes to recipient list during use is prevented to protect user from accidentally revealing use of TFC. In trickle mode, if no packets are available in either m_queue or f_queue, 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. TFC does it's best to hide the loading times and encryption duration by using constant time context manager and constant time queue status lookup, as well as constant time XSalsa20 cipher. """ m_queue = queues[MESSAGE_PACKET_QUEUE] f_queue = queues[FILE_PACKET_QUEUE] c_queue = queues[COMMAND_PACKET_QUEUE] l_queue = queues[LOG_PACKET_QUEUE] km_queue = queues[KEY_MANAGEMENT_QUEUE] np_queue = queues[NOISE_PACKET_QUEUE] nc_queue = queues[NOISE_COMMAND_QUEUE] ws_queue = queues[WINDOW_SELECT_QUEUE] m_buffer = [] # type: List[Tuple[bytes, Settings, str, str, bool, Window]] f_buffer = [] # type: List[Tuple[bytes, Settings, str, str, bool, Window]] if settings.session_trickle: while ws_queue.empty(): time.sleep(0.01) window = ws_queue.get() while True: try: with ConstantTime(settings, length=TRICKLE_QUEUE_CHECK_DELAY): queue = [[m_queue, m_queue], [f_queue, np_queue]][m_queue.empty()][f_queue.empty()] packet, log_dict = queue.get() for c in window: with ConstantTime(settings, d_type='trickle'): send_packet(packet, key_list, settings, gateway, l_queue, c.rx_account, c.tx_account, log_dict[c.rx_account]) with ConstantTime(settings, d_type='trickle'): queue = [c_queue, nc_queue][c_queue.empty()] command = queue.get() send_packet(command, key_list, settings, gateway, l_queue) except (EOFError, KeyboardInterrupt): pass else: while True: try: time.sleep(0.001) # Keylist database management packets have highest priority. if not km_queue.empty(): command, *params = km_queue.get() key_list.manage(command, *params) continue # packets from c_queue come only from local contact. Until keys for local contact # have been added, no command is loaded. Commands have second highest priority. if not c_queue.empty(): if key_list.has_local_key(): command, settings = c_queue.get() send_packet(command, key_list, settings, gateway, l_queue) continue # Iterate through buffer list that contains tuples of transmission information # loaded from m_queue in the order they were placed into the buffer. As soon as # keys are available, send packet. Restart the loop to prioritize keylist # management and command packets before going through the buffer list again. for i, params in enumerate(m_buffer): packet, settings, rx_account, tx_account, logging, window = params if key_list.has_keyset(rx_account): m_buffer.pop(i) send_packet(packet, key_list, settings, gateway, l_queue, rx_account, tx_account, logging) continue # Any new messages take priority only after the ones in buffer are sent. # If key is not on list, place the message packet into the buffer. if not m_queue.empty(): packet, settings, rx_account, tx_account, logging, window = m_queue.get( ) if key_list.has_keyset(rx_account): send_packet(packet, key_list, settings, gateway, l_queue, rx_account, tx_account, logging) else: m_buffer.append((packet, settings, rx_account, tx_account, logging, window)) continue # When no more messages can be processed, check if the # file buffer has packets that can be sent to contacts. for i, params in enumerate(f_buffer): packet, settings, rx_account, tx_account, logging, window = params if key_list.has_keyset(rx_account): f_buffer.pop(i) send_packet(packet, key_list, settings, gateway, l_queue, rx_account, tx_account, logging) continue # If file buffer is empty, check if new file packets are available. If there are and # contact has key, send file packet, otherwise place it into the file packet buffer. if not f_queue.empty(): packet, settings, rx_account, tx_account, logging, window = f_queue.get( ) if key_list.has_keyset(rx_account): send_packet(packet, key_list, settings, gateway, l_queue, rx_account, tx_account, logging) else: f_buffer.append((packet, settings, rx_account, tx_account, logging, window)) except (EOFError, KeyboardInterrupt): pass
def sender_loop(queues: Dict[bytes, 'Queue'], settings: 'Settings', gateway: 'Gateway', key_list: 'KeyList', unittest: bool = False) -> None: """Output packets from queues based on queue priority. 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. Whether or not traffic masking is enabled, files are only transmitted when no messages are being output. This is because file transmission is usually very slow and user might need to send messages in the meantime. When traffic masking is disabled, commands take highest priority as they are not output all the time. When traffic masking is enabled, commands are output between each output message packet. This allows commands to take effect as soon as possible but slows down message/file delivery by half. Each contact in window is cycled in order. Making changes to recipient list during use is prevented to protect user from accidentally revealing use of TFC. When traffic masking is enabled, if no packets are available in either m_queue or f_queue, 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. TFC does it's best to hide the loading times and encryption duration by using constant time context manager with CSPRNG spawned jitter, constant time queue status lookup, and constant time XSalsa20 cipher. However, since TFC is written with in a high-level language, it is impossible to guarantee TxM never reveals it's user-operation schedule to NH. """ m_queue = queues[MESSAGE_PACKET_QUEUE] f_queue = queues[FILE_PACKET_QUEUE] c_queue = queues[COMMAND_PACKET_QUEUE] n_queue = queues[NH_PACKET_QUEUE] l_queue = queues[LOG_PACKET_QUEUE] km_queue = queues[KEY_MANAGEMENT_QUEUE] np_queue = queues[NOISE_PACKET_QUEUE] nc_queue = queues[NOISE_COMMAND_QUEUE] ws_queue = queues[WINDOW_SELECT_QUEUE] m_buffer = dict( ) # type: Dict[str, List[Tuple[bytes, Settings, str, str, bool]]] f_buffer = dict( ) # type: Dict[str, List[Tuple[bytes, Settings, str, str, bool]]] if settings.session_traffic_masking: while ws_queue.qsize() == 0: time.sleep(0.01) window, log_messages = ws_queue.get() while True: with ignored(EOFError, KeyboardInterrupt): with ConstantTime(settings, length=TRAFFIC_MASKING_QUEUE_CHECK_DELAY): queue = [[m_queue, m_queue], [f_queue, np_queue] ][m_queue.qsize() == 0][f_queue.qsize() == 0] packet, lm, log_as_ph = queue.get() if lm is not None: # Ignores None sent by noise_packet_loop that does not alter log setting log_messages = lm for c in window: with ConstantTime(settings, d_type=TRAFFIC_MASKING): send_packet(key_list, gateway, l_queue, packet, settings, c.rx_account, c.tx_account, log_messages, log_as_ph) with ConstantTime(settings, d_type=TRAFFIC_MASKING): queue = [c_queue, nc_queue][c_queue.qsize() == 0] command, lm = queue.get() if lm is not None: # Log setting is only updated with 'logging' command log_messages = lm send_packet(key_list, gateway, l_queue, command, settings) if n_queue.qsize() != 0: packet, delay, settings = n_queue.get() transmit(packet, settings, gateway, delay) if packet[1:] == UNENCRYPTED_EXIT_COMMAND: queues[EXIT_QUEUE].put(EXIT) elif packet[1:] == UNENCRYPTED_WIPE_COMMAND: queues[EXIT_QUEUE].put(WIPE) if unittest: break else: while True: try: if km_queue.qsize() != 0: key_list.manage(*km_queue.get()) continue # Commands to RxM if c_queue.qsize() != 0: if key_list.has_local_key(): send_packet(key_list, gateway, l_queue, *c_queue.get()) continue # Commands/exported files to NH if n_queue.qsize() != 0: packet, delay, settings = n_queue.get() transmit(packet, settings, gateway, delay) if packet[1:] == UNENCRYPTED_EXIT_COMMAND: queues[EXIT_QUEUE].put(EXIT) elif packet[1:] == UNENCRYPTED_WIPE_COMMAND: queues[EXIT_QUEUE].put(WIPE) continue # Buffered messages for rx_account in m_buffer: if key_list.has_keyset( rx_account) and m_buffer[rx_account]: send_packet( key_list, gateway, l_queue, *m_buffer[rx_account].pop(0)[:-1] ) # Strip window UID as it's only used to cancel packets continue # New messages if m_queue.qsize() != 0: q_data = m_queue.get() rx_account = q_data[2] if key_list.has_keyset(rx_account): send_packet(key_list, gateway, l_queue, *q_data[:-1]) else: m_buffer.setdefault(rx_account, []).append(q_data) continue # Buffered files for rx_account in m_buffer: if key_list.has_keyset( rx_account) and f_buffer[rx_account]: send_packet(key_list, gateway, l_queue, *f_buffer[rx_account].pop(0)[:-1]) continue # New files if f_queue.qsize() != 0: q_data = f_queue.get() rx_account = q_data[2] if key_list.has_keyset(rx_account): send_packet(key_list, gateway, l_queue, *q_data[:-1]) else: f_buffer.setdefault(rx_account, []).append(q_data) if unittest and queues[UNITTEST_QUEUE].qsize() != 0: break time.sleep(0.01) except (EOFError, KeyboardInterrupt): pass