def rxm_outgoing(queues: Dict[bytes, 'Queue'], settings: 'Settings', gateway: 'Gateway', unittest: bool = False) -> None: """Loop that outputs packets from queues to RxM. Commands (and local keys) from TxM to RxM have higher priority than messages and public keys from contacts. This prevents contact from doing DoS on RxM by filling queue with packets. """ rs = RSCodec(2 * settings.session_serial_error_correction) c_queue = queues[TXM_TO_RXM_QUEUE] m_queue = queues[RXM_OUTGOING_QUEUE] while True: try: time.sleep(0.01) while c_queue.qsize() != 0: packet = rs.encode(bytearray(c_queue.get())) gateway.write(packet) if m_queue.qsize() != 0: packet = rs.encode(bytearray(m_queue.get())) gateway.write(packet) if unittest: break except (EOFError, KeyboardInterrupt): pass
def test_prim_fcr_long(self) -> None: nn = 48 kk = 34 tt = nn - kk rs = RSCodec(tt, fcr=120, prim=0x187) hex_enc_msg = ('08faa123555555c000000354064432c0280e1b4d090cfc04' '887400000003500000000e1985ff9c6b33066ca9f43d12e8') strf = str enc_msg = bytearray.fromhex(strf(hex_enc_msg)) dec_msg = enc_msg[:kk] tem = rs.encode(dec_msg) self.assertEqual(enc_msg, tem, msg="encoded does not match expected") tdm, rtem = rs.decode(tem) self.assertEqual(tdm, dec_msg, msg="decoded does not match original") self.assertEqual(rtem, tem, msg="decoded mesecc does not match original") tem1 = bytearray(tem) num_errs = tt >> 1 for i in sample(range(nn), num_errs): tem1[i] ^= 0xff tdm, rtem = rs.decode(tem1) self.assertEqual(tdm, dec_msg, msg="decoded with errors does not match original") self.assertEqual(rtem, tem, msg="decoded mesecc with errors does not match original") tem1 = bytearray(tem) num_errs += 1 for i in sample(range(nn), num_errs): tem1[i] ^= 0xff self.assertRaises(ReedSolomonError, rs.decode, tem1)
def test_simple(self) -> None: rs = RSCodec() msg = bytearray("hello world " * 10, "latin1") enc = rs.encode(msg) dec, dec_enc = rs.decode(enc) self.assertEqual(dec, msg) self.assertEqual(dec_enc, enc)
def test_prim_fcr_basic(self) -> None: nn = 30 kk = 18 tt = nn - kk rs = RSCodec(tt, fcr=120, prim=0x187) hex_enc_msg = ('00faa123555555c000000354064432' 'c02800fe97c434e1ff5365cf8fafe4') strf = str enc_msg = bytearray.fromhex(strf(hex_enc_msg)) dec_msg = enc_msg[:kk] tem = rs.encode(dec_msg) self.assertEqual(enc_msg, tem, msg="encoded does not match expected") tdm, rtem = rs.decode(tem) self.assertEqual(tdm, dec_msg, msg="decoded does not match original") self.assertEqual(rtem, tem, msg="decoded mesecc does not match original") tem1 = bytearray(tem) # Clone a copy # Encoding and decoding intact message seem OK, so test errors num_errs = tt >> 1 # Inject tt/2 errors (expected to recover fully) for i in sample(range(nn), num_errs): # inject errors in random places tem1[i] ^= 0xff # flip all 8 bits tdm, _ = rs.decode(tem1) self.assertEqual(tdm, dec_msg, msg="decoded with errors does not match original") tem1 = bytearray(tem) # Clone another copy num_errs += 1 # Inject tt/2 + 1 errors (expected to fail and detect it) for i in sample(range(nn), num_errs): # Inject errors in random places tem1[i] ^= 0xff # Flip all 8 bits # If this fails, it means excessive errors not detected self.assertRaises(ReedSolomonError, rs.decode, tem1)
def calculate_race_condition_delay(settings: Union['Settings', 'NHSettings'], txm: bool = False) -> float: """Calculate NH race condition delay. This value is the max time it takes for NH to deliver command received from TxM all the way to RxM. :param settings: Settings object :param txm: When True, allocate time for command delivery from TxM to NH :return: Time to wait to prevent race condition """ rs = RSCodec(2 * settings.session_serial_error_correction) max_account_length = 254 max_message_length = PACKET_LENGTH + 2 * max_account_length command_length = 365*2 if txm else 365 max_bytes = (len(rs.encode(os.urandom(max_message_length))) + len(rs.encode(os.urandom(command_length)))) return (max_bytes * BAUDS_PER_BYTE) / settings.serial_baudrate
def transmit(packet: bytes, settings: 'Settings', gateway: 'Gateway') -> None: """Add Reed-Solomon erasure code and output packet via gateway.""" rs = RSCodec(2 * settings.session_ec_ratio) packet = rs.encode(packet) gateway.write(packet) if not settings.session_trickle: if settings.long_packet_rand_d: random_delay = random.SystemRandom().uniform(0, settings.max_val_for_rand_d) time.sleep(random_delay)
def test_receiver_loop(self) -> None: # Setup gateway = Gateway(local_test=False) rs = RSCodec(2 * gateway.settings.serial_error_correction) queues = { MESSAGE_DATAGRAM_HEADER: Queue(), FILE_DATAGRAM_HEADER: Queue(), COMMAND_DATAGRAM_HEADER: Queue(), LOCAL_KEY_DATAGRAM_HEADER: Queue() } all_q = dict(queues) all_q.update({GATEWAY_QUEUE: Queue()}) ts = datetime.now() ts_bytes = int_to_bytes(int(ts.strftime('%Y%m%d%H%M%S%f')[:-4])) for key in queues: packet = key + ts_bytes + bytes(ONION_SERVICE_PUBLIC_KEY_LENGTH) encoded = rs.encode(packet) broken_p = key + bytes.fromhex('df9005313af4136d') + bytes( ONION_SERVICE_PUBLIC_KEY_LENGTH) broken_p += rs.encode(b'a') def queue_delayer() -> None: """Place datagrams into queue after delay.""" time.sleep(0.01) all_q[GATEWAY_QUEUE].put( (datetime.now(), rs.encode(8 * b'1' + b'undecodable'))) all_q[GATEWAY_QUEUE].put((datetime.now(), broken_p)) all_q[GATEWAY_QUEUE].put((datetime.now(), encoded)) threading.Thread(target=queue_delayer).start() # Test self.assertIsNone(receiver_loop(all_q, gateway, unit_test=True)) time.sleep(0.01) self.assertEqual(queues[key].qsize(), 1) # Teardown tear_queue(queues[key])
def test_check(self) -> None: rs = RSCodec() msg = bytearray("hello world " * 10, "latin1") enc = rs.encode(msg) rmsg, renc = rs.decode(enc) self.assertEqual(rs.check(enc), [True]) self.assertEqual(rs.check(renc), [True]) for i in [27, -3, -9, 7, 0]: enc[i] = 99 rmsg, renc = rs.decode(enc) self.assertEqual(rs.check(enc), [False]) self.assertEqual(rs.check(renc), [True])
def test_long(self) -> None: rs = RSCodec() msg = bytearray("a" * 10000, "latin1") enc = rs.encode(msg) dec, dec_enc = rs.decode(enc) self.assertEqual(dec, msg) self.assertEqual(dec_enc, enc) enc2 = list(enc) enc2[177] = 99 enc2[2212] = 88 dec2, dec_enc2 = rs.decode(bytes(enc2)) self.assertEqual(dec2, msg) self.assertEqual(dec_enc2, enc)
def test_correction(self) -> None: rs = RSCodec() msg = bytearray("hello world " * 10, "latin1") enc = rs.encode(msg) rmsg, renc = rs.decode(enc) self.assertEqual(rmsg, msg) self.assertEqual(renc, enc) for i in [27, -3, -9, 7, 0]: enc[i] = 99 rmsg, renc = rs.decode(enc) self.assertEqual(rmsg, msg) enc[82] = 99 self.assertRaises(ReedSolomonError, rs.decode, enc)
def test_c_exp_12(self) -> None: rsc = RSCodec(12, c_exp=12) rsc2 = RSCodec(12, nsize=4095) self.assertEqual(rsc.c_exp, rsc2.c_exp) self.assertEqual(rsc.nsize, rsc2.nsize) mes = 'a'*(4095-12) mesecc = rsc.encode(mes) mesecc[2] = 1 mesecc[-1] = 1 rmes, rmesecc = rsc.decode(mesecc) self.assertEqual(rsc.check(mesecc), [False]) self.assertEqual(rsc.check(rmesecc), [True]) self.assertEqual([x for x in rmes], [ord(x) for x in mes])
def test_c_exp_9(self): rsc = RSCodec(12, c_exp=9) rsc2 = RSCodec(12, nsize=511) self.assertEqual(rsc.c_exp, rsc2.c_exp) self.assertEqual(rsc.nsize, rsc2.nsize) mes = 'a' * ((511 - 12) * 2) mesecc = rsc.encode(mes) mesecc[2] = 1 mesecc[-1] = 1 rmes, rmesecc = rsc.decode(mesecc) self.assertEqual(rsc.check(mesecc), [False, False]) self.assertEqual(rsc.check(rmesecc), [True, True]) self.assertEqual([x for x in rmes], [ord(x) for x in mes])
def rxm_outgoing(settings: 'Settings', q_to_rxm: 'Queue', gateway: 'Gateway') -> None: """Output packets from RxM-queue to RxM.""" rs = RSCodec(2 * settings.session_ec_ratio) while True: try: if q_to_rxm.empty(): time.sleep(0.001) continue from_q = q_to_rxm.get() packet = rs.encode(bytearray(from_q)) gateway.write(packet) except (EOFError, KeyboardInterrupt): pass
def test_multiple_rs_codec(self) -> None: """Test multiple RSCodec instances with different parameters.""" mes = 'A' * 30 rs_256 = RSCodec(102) rs_1024 = RSCodec(900, c_exp=10) bytearray(rs_1024.decode(rs_1024.encode(mes))[0]) rs_256.encode(mes) rs_1024.encode(mes) bytearray(rs_256.decode(rs_256.encode(mes))[0])
def update_delivery_time(self) -> None: """Calculate transmission time. Transmission time is based on average delays and settings. """ no_packets = self.count_number_of_packets() if self.settings.session_traffic_masking: avg_delay = self.settings.traffic_masking_static_delay + ( self.settings.traffic_masking_random_delay / 2) if self.settings.multi_packet_random_delay: avg_delay += (self.settings.max_duration_of_random_delay / 2) total_time = len(self.window) * no_packets * avg_delay total_time *= 2 # Accommodate command packets between file packets total_time += no_packets * TRAFFIC_MASKING_QUEUE_CHECK_DELAY else: # Determine total data to be transmitted over serial rs = RSCodec(2 * self.settings.session_serial_error_correction) total_data = 0 for c in self.window: data = os.urandom(PACKET_LENGTH) + c.rx_account.encode( ) + c.tx_account.encode() enc_data = rs.encode(data) total_data += no_packets * len(enc_data) # Determine time required to send all data total_time = 0.0 if self.settings.local_testing_mode: total_time += no_packets * LOCAL_TESTING_PACKET_DELAY else: total_bauds = total_data * BAUDS_PER_BYTE total_time += total_bauds / self.settings.session_serial_baudrate total_time += no_packets * self.settings.txm_inter_packet_delay if self.settings.multi_packet_random_delay: total_time += no_packets * ( self.settings.max_duration_of_random_delay / 2) # Update delivery time self.time_bytes = int_to_bytes(int(total_time)) self.time_print = str(datetime.timedelta(seconds=int(total_time)))
def calculate_race_condition_delay(serial_error_correction: int, serial_baudrate: int) -> float: """\ Calculate the delay required to prevent Relay Program race condition. When Transmitter Program outputs a command to exit or wipe data, Relay program will also receive a copy of the command. If the Relay Program acts on the command too early, the Receiver Program will not receive the exit/wipe command at all. This function calculates the delay Transmitter Program should wait before outputting command to the Relay Program, to ensure the Receiver Program has received its encrypted command. """ rs = RSCodec(2 * serial_error_correction) message_length = PACKET_LENGTH + ONION_ADDRESS_LENGTH enc_msg_length = len(rs.encode(os.urandom(message_length))) enc_cmd_length = len(rs.encode(os.urandom(COMMAND_LENGTH))) max_bytes = enc_msg_length + (2 * enc_cmd_length) return (max_bytes * BAUDS_PER_BYTE) / serial_baudrate
def transmit(packet: bytes, settings: 'Settings', gateway: 'Gateway', delay: bool = True) -> None: """Add Reed-Solomon erasure code and output packet via gateway. Note that random.SystemRandom() uses Kernel CSPRNG (/dev/urandom), not Python's weak RNG based on Mersenne Twister: https://docs.python.org/2/library/random.html#random.SystemRandom """ rs = RSCodec(2 * settings.session_serial_error_correction) packet = rs.encode(packet) gateway.write(packet) if settings.local_testing_mode: time.sleep(LOCAL_TESTING_PACKET_DELAY) if not settings.session_traffic_masking: if settings.multi_packet_random_delay and delay: random_delay = random.SystemRandom().uniform(0, settings.max_duration_of_random_delay) time.sleep(random_delay)
def test_receiver_loop(self): # Setup settings = Settings() rs = RSCodec(2 * settings.serial_error_correction) queues = { LOCAL_KEY_PACKET_HEADER: Queue(), PUBLIC_KEY_PACKET_HEADER: Queue(), MESSAGE_PACKET_HEADER: Queue(), COMMAND_PACKET_HEADER: Queue(), IMPORTED_FILE_HEADER: Queue() } all_q = dict(queues) all_q.update({GATEWAY_QUEUE: Queue()}) for key in queues: packet = key + bytes(KEY_LENGTH) encoded = rs.encode(packet) def queue_delayer(): time.sleep(0.1) all_q[GATEWAY_QUEUE].put(b'undecodable') all_q[GATEWAY_QUEUE].put(encoded) threading.Thread(target=queue_delayer).start() # Test self.assertIsNone(receiver_loop(all_q, settings, unittest=True)) time.sleep(0.1) self.assertEqual(queues[key].qsize(), 1) # Teardown while not queues[key].empty(): queues[key].get() time.sleep(0.1) queues[key].close()
def update_delivery_time(self) -> None: """Calculate transmission time. Transmission time is based on average delays and settings. """ packet_data = US_BYTE.join( [self.name, self.size, self.time_l, self.data]) if len(packet_data) < 255: no_packets = 1 else: packet_data = bytes(8) + packet_data packet_data = byte_padding(packet_data) no_packets = len(split_byte_string(packet_data, item_len=255)) no_recipients = len(self.window) if self.settings.session_trickle: avg_delay = self.settings.trickle_stat_delay + ( self.settings.trickle_rand_delay / 2) if self.settings.long_packet_rand_d: avg_delay += (self.settings.max_val_for_rand_d / 2) # Multiply by two as trickle sends a command packet between every file packet. total_time = 2 * no_recipients * no_packets * avg_delay # Add constant time queue load time total_time += no_packets * TRICKLE_QUEUE_CHECK_DELAY else: total_data = 0 rs = RSCodec(2 * self.settings.session_ec_ratio) static_data_len = ( 1 + 24 + 8 + 16 + 24 + 256 + 16 + 1 ) # header + nonce + harac-ct + tag + nonce + ass. p. ct + tag + US_BYTE for c in self.window.window_contacts: data_len = static_data_len + (len(c.rx_account.encode()) + len(c.tx_account.encode())) enc_data_len = len(rs.encode((os.urandom(data_len)))) total_data += (no_packets * enc_data_len) total_time = 0.0 if not self.settings.local_testing_mode: bauds_in_byte = 10 total_bauds = total_data * bauds_in_byte total_time += total_bauds / self.settings.session_if_speed total_time += no_packets * self.gateway.delay if self.settings.long_packet_rand_d: total_time += no_packets * (self.settings.max_val_for_rand_d / 2) delta_seconds = datetime.timedelta(seconds=int(total_time)) delivery_time = datetime.datetime(1, 1, 1) + delta_seconds # Format delivery time string if delivery_time.second == 0: self.time_s = '00s' self.time_l = b'00d 00h 00m 00s' return None time_l_str = '' self.time_s = '' for i in [(delivery_time.day - 1, 'd'), (delivery_time.hour, 'h'), (delivery_time.minute, 'm'), (delivery_time.second, 's')]: if i[0] > 0: self.time_s += str(i[0]).zfill(2) + f'{i[1]} ' time_l_str += str(i[0]).zfill(2) + f'{i[1]} ' self.time_s = self.time_s.strip(' ') time_l_str.strip() self.time_l = time_l_str.encode()
class TestTxMIncoming(unittest.TestCase): def setUp(self): self.settings = Settings() self.rs = RSCodec(2 * self.settings.serial_error_correction) self.o_urandom = os.urandom self.queues = {TXM_INCOMING_QUEUE: Queue(), RXM_OUTGOING_QUEUE: Queue(), TXM_TO_IM_QUEUE: Queue(), TXM_TO_NH_QUEUE: Queue(), TXM_TO_RXM_QUEUE: Queue(), NH_TO_IM_QUEUE: Queue(), EXIT_QUEUE: Queue()} def tearDown(self): os.urandom = self.o_urandom for key in self.queues: while not self.queues[key].empty(): self.queues[key].get() time.sleep(0.1) self.queues[key].close() for f in [8*'61', 8*'62']: with ignored(OSError): os.remove(f) def test_unencrypted_packet(self): # Setup packet = self.rs.encode(UNENCRYPTED_PACKET_HEADER + b'test') self.queues[TXM_INCOMING_QUEUE].put(640 * b'a') self.queues[TXM_INCOMING_QUEUE].put(packet) time.sleep(0.1) # Test self.assertIsNone(txm_incoming(self.queues, self.settings, unittest=True)) time.sleep(0.1) self.assertEqual(self.queues[TXM_TO_NH_QUEUE].qsize(), 1) def test_local_key_packet(self): # Setup packet = self.rs.encode(LOCAL_KEY_PACKET_HEADER + b'test') def queue_delayer(): time.sleep(0.1) self.queues[TXM_INCOMING_QUEUE].put(packet) threading.Thread(target=queue_delayer).start() # Test self.assertIsNone(txm_incoming(self.queues, self.settings, unittest=True)) time.sleep(0.1) self.assertEqual(self.queues[TXM_TO_RXM_QUEUE].qsize(), 1) def test_command_packet(self): # Setup packet = self.rs.encode(COMMAND_PACKET_HEADER + b'test') self.queues[TXM_INCOMING_QUEUE].put(packet) time.sleep(0.1) # Test self.assertIsNone(txm_incoming(self.queues, self.settings, unittest=True)) time.sleep(0.1) self.assertEqual(self.queues[TXM_TO_RXM_QUEUE].qsize(), 1) def test_message_packet(self): # Setup packet = self.rs.encode(MESSAGE_PACKET_HEADER + 344 * b'a' + b'*****@*****.**' + US_BYTE + b'*****@*****.**') self.queues[TXM_INCOMING_QUEUE].put(packet) time.sleep(0.1) # Test self.assertIsNone(txm_incoming(self.queues, self.settings, unittest=True)) time.sleep(0.1) self.assertEqual(self.queues[TXM_TO_IM_QUEUE].qsize(), 1) self.assertEqual(self.queues[RXM_OUTGOING_QUEUE].qsize(), 1) def test_public_key_packet(self): # Setup packet = self.rs.encode(PUBLIC_KEY_PACKET_HEADER + KEY_LENGTH * b'a' + b'*****@*****.**' + US_BYTE + b'*****@*****.**') self.queues[TXM_INCOMING_QUEUE].put(packet) time.sleep(0.1) # Test self.assertIsNone(txm_incoming(self.queues, self.settings, unittest=True)) time.sleep(0.1) self.assertEqual(self.queues[RXM_OUTGOING_QUEUE].qsize(), 1) self.assertEqual(self.queues[TXM_TO_IM_QUEUE].qsize(), 1) def test_exported_file_packet(self): # Setup open(8*'61', 'w+').close() packet = self.rs.encode(EXPORTED_FILE_HEADER + 500 * b'a') output_list = [8*b'a', 8*b'b'] gen = iter(output_list) os.urandom = lambda _: next(gen) self.queues[TXM_INCOMING_QUEUE].put(packet) time.sleep(0.1) # Test self.assertIsNone(txm_incoming(self.queues, self.settings, unittest=True)) self.assertTrue(os.path.isfile(8*'62'))
class Gateway(object): """\ Gateway object is a wrapper for interfaces that connect Source/Destination Computer with the Networked Computer. """ def __init__(self, operation: str, local_test: bool, dd_sockets: bool, qubes: bool, ) -> None: """Create a new Gateway object.""" self.settings = GatewaySettings(operation, local_test, dd_sockets, qubes) self.tx_serial = None # type: Optional[serial.Serial] self.rx_serial = None # type: Optional[serial.Serial] self.rx_socket = None # type: Optional[multiprocessing.connection.Connection] self.tx_socket = None # type: Optional[multiprocessing.connection.Connection] # Initialize Reed-Solomon erasure code handler self.rs = RSCodec(2 * self.settings.session_serial_error_correction) # Set True when the serial interface is initially found so that # further interface searches know to announce disconnection. self.init_found = False if self.settings.local_testing_mode: if self.settings.software_operation in [TX, NC]: self.client_establish_socket() if self.settings.software_operation in [NC, RX]: self.server_establish_socket() elif not self.settings.qubes: self.establish_serial() def establish_serial(self) -> None: """Create a new Serial object. By setting the Serial object's timeout to 0, the method `Serial().read_all()` will return 0..N bytes where N is the serial interface buffer size (496 bytes for FTDI FT232R for example). This is not enough for large packets. However, in this case, `read_all` will return a) immediately when the buffer is full b) if no bytes are received during the time it would take to transmit the next byte of the datagram. This type of behaviour allows us to read 0..N bytes from the serial interface at a time, and add them to a bytearray buffer. In our implementation below, if the receiver side stops receiving data when it calls `read_all`, it starts a timer that is evaluated with every subsequent call of `read_all` that returns an empty string. If the timer exceeds the `settings.rx_receive_timeout` value (twice the time it takes to send the next byte with given baud rate), the gateway object will return the received packet. The timeout timer is triggered intentionally by the transmitter side Gateway object, that after each transmission sleeps for `settings.tx_inter_packet_delay` seconds. This value is set to twice the length of `settings.rx_receive_timeout`, or four times the time it takes to send one byte with given baud rate. """ try: self.tx_serial = self.rx_serial = serial.Serial(self.search_serial_interface(), self.settings.session_serial_baudrate, timeout=0) except SerialException: raise CriticalError("SerialException. Ensure $USER is in the dialout group by restarting this computer.") def send_over_qrexec(self, packet: bytes) -> None: """Send packet content over the Qubes qrexec RPC. More information at https://www.qubes-os.org/doc/qrexec/ The packet is encoded with ASCII85 to ensure e.g. 0x0a byte is not interpreted as line feed by the RPC service. """ target_vm = QUBES_NET_VM_NAME if self.settings.software_operation == TX else QUBES_DST_VM_NAME dom0_policy = QUBES_SRC_NET_POLICY if self.settings.software_operation == TX else QUBES_NET_DST_POLICY subprocess.Popen(['/usr/bin/qrexec-client-vm', target_vm, dom0_policy], stdin=subprocess.PIPE, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL ).communicate(base64.b85encode(packet)) def write(self, orig_packet: bytes) -> None: """Add error correction data and output data via socket/serial interface. After outputting the packet via serial, sleep long enough to trigger the Rx-side timeout timer, or if local testing is enabled, add slight delay to simulate that introduced by the serial interface. """ packet = self.add_error_correction(orig_packet) if self.settings.local_testing_mode and self.tx_socket is not None: try: self.tx_socket.send(packet) time.sleep(LOCAL_TESTING_PACKET_DELAY) except BrokenPipeError: raise CriticalError("Relay IPC server disconnected.", exit_code=0) elif self.settings.qubes: self.send_over_qrexec(packet) elif self.tx_serial is not None: try: self.tx_serial.write(packet) self.tx_serial.flush() time.sleep(self.settings.tx_inter_packet_delay) except SerialException: self.establish_serial() self.write(orig_packet) def read_socket(self) -> bytes: """Read packet from socket interface.""" if self.rx_socket is None: raise CriticalError("Socket interface has not been initialized.") while True: try: packet = self.rx_socket.recv() # type: bytes return packet except KeyboardInterrupt: pass except EOFError: raise CriticalError("Relay IPC client disconnected.", exit_code=0) @staticmethod 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 read_serial(self) -> bytes: """Read packet from serial interface. Read 0..N bytes from serial interface, where N is the buffer size of the serial interface. Once `read_buffer` has data, and the interface hasn't returned data long enough for the timer to exceed the timeout value, return received data. """ if self.rx_serial is None: raise CriticalError("Serial interface has not been initialized.") while True: try: start_time = 0.0 read_buffer = bytearray() while True: read = self.rx_serial.read_all() if read: start_time = time.monotonic() read_buffer.extend(read) else: if read_buffer: delta = time.monotonic() - start_time if delta > self.settings.rx_receive_timeout: return bytes(read_buffer) else: time.sleep(0.0001) except (EOFError, KeyboardInterrupt): pass except (OSError, SerialException): self.establish_serial() def read(self, buffer_file_dir: str = '') -> bytes: """Read data via socket/serial interface.""" if self.settings.local_testing_mode: return self.read_socket() if self.settings.qubes: return self.read_qubes_buffer_file(buffer_file_dir) return self.read_serial() def add_error_correction(self, packet: bytes) -> bytes: """Add error correction to packet that will be output. If the error correction setting is set to 1 or higher, TFC adds Reed-Solomon erasure codes to detect and correct errors during transmission over the serial interface. For more information on Reed-Solomon, see https://en.wikipedia.org/wiki/Reed%E2%80%93Solomon_error_correction https://www.cs.cmu.edu/~guyb/realworld/reedsolomon/reed_solomon_codes.html If error correction is set to 0, errors are only detected. This is done by using a BLAKE2b based, 128-bit checksum. If Qubes is used, Reed-Solomon is not used as it only slows down data transfer. """ if self.settings.session_serial_error_correction and not self.settings.qubes: packet = self.rs.encode(packet) else: packet = packet + hashlib.blake2b(packet, digest_size=PACKET_CHECKSUM_LENGTH).digest() return packet def detect_errors(self, packet: bytes) -> bytes: """Handle received packet error detection and/or correction.""" if self.settings.qubes: try: packet = base64.b85decode(packet) except ValueError: raise SoftError("Error: Received packet had invalid Base85 encoding.") if self.settings.session_serial_error_correction and not self.settings.qubes: try: packet, _ = self.rs.decode(packet) return bytes(packet) except ReedSolomonError: raise SoftError("Error: Reed-Solomon failed to correct errors in the received packet.", bold=True) else: packet, checksum = separate_trailer(packet, PACKET_CHECKSUM_LENGTH) if hashlib.blake2b(packet, digest_size=PACKET_CHECKSUM_LENGTH).digest() != checksum: raise SoftError("Warning! Received packet had an invalid checksum.", bold=True) return packet 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.") # Local testing def server_establish_socket(self) -> None: """Initialize the receiver (IPC server). The multiprocessing connection during local test does not utilize authentication keys* because a MITM-attack against the connection requires endpoint compromise, and in such situation, MITM attack is not nearly as effective as key/screen logging or RAM dump. * https://docs.python.org/3/library/multiprocessing.html#authentication-keys Similar to the case of standard mode of operation, all sensitive data that passes through the socket/serial interface and Relay Program is encrypted. A MITM attack between the sockets could of course be used to e.g. inject public keys, but like with all key exchanges, that would only work if the user neglects fingerprint verification. Another reason why the authentication key is useless, is the key needs to be pre-shared. This means there's two ways to share it: 1) Hard-code the key to source file from where malware could read it. 2) Force the user to manually copy the PSK from one program to another. This would change the workflow that the local test configuration tries to simulate. To conclude, the local test configuration should never be used under a threat model where endpoint security is of importance. """ try: socket_number = RP_LISTEN_SOCKET if self.settings.software_operation == NC else DST_LISTEN_SOCKET listener = multiprocessing.connection.Listener((LOCALHOST, socket_number)) self.rx_socket = listener.accept() except KeyboardInterrupt: graceful_exit() 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()
class TestSRCIncoming(unittest.TestCase): def setUp(self): self.settings = Settings() self.unittest_dir = cd_unittest() self.gateway = Gateway() self.rs = RSCodec(2 * self.gateway.settings.serial_error_correction) self.ts = datetime.now() self.queues = gen_queue_dict() self.args = self.queues, self.gateway def tearDown(self): tear_queues(self.queues) cleanup(self.unittest_dir) def create_packet(self, packet: bytes): """Create Reed-Solomon encoded packet""" return self.rs.encode(packet) def test_unencrypted_datagram(self): # Setup packet = self.create_packet(UNENCRYPTED_DATAGRAM_HEADER + b'test') self.queues[GATEWAY_QUEUE].put((self.ts, 640 * b'a')) self.queues[GATEWAY_QUEUE].put((self.ts, packet)) # Test self.assertIsNone(src_incoming(*self.args, unittest=True)) self.assertEqual(self.queues[SRC_TO_RELAY_QUEUE].qsize(), 1) def test_local_key_datagram(self): # Setup packet = self.create_packet(LOCAL_KEY_DATAGRAM_HEADER + b'test') def queue_delayer(): """Place packet into queue after delay.""" time.sleep(0.01) self.queues[GATEWAY_QUEUE].put((self.ts, packet)) threading.Thread(target=queue_delayer).start() # Test self.assertIsNone(src_incoming(*self.args, unittest=True)) self.assertEqual(self.queues[DST_COMMAND_QUEUE].qsize(), 1) def test_command_datagram(self): # Setup packet = self.create_packet(COMMAND_DATAGRAM_HEADER + b'test') self.queues[GATEWAY_QUEUE].put((self.ts, packet)) # Test self.assertIsNone(src_incoming(*self.args, unittest=True)) self.assertEqual(self.queues[DST_COMMAND_QUEUE].qsize(), 1) def test_message_datagram(self): # Setup packet = self.create_packet(MESSAGE_DATAGRAM_HEADER + 344 * b'a' + nick_to_pub_key('bob')) self.queues[GATEWAY_QUEUE].put((self.ts, packet)) # Test self.assertIsNone(src_incoming(*self.args, unittest=True)) self.assertEqual(self.queues[M_TO_FLASK_QUEUE].qsize(), 1) self.assertEqual(self.queues[DST_MESSAGE_QUEUE].qsize(), 1) def test_public_key_datagram(self): # Setup packet = self.create_packet(PUBLIC_KEY_DATAGRAM_HEADER + nick_to_pub_key('bob') + TFC_PUBLIC_KEY_LENGTH * b'a') self.queues[GATEWAY_QUEUE].put((self.ts, packet)) # Test self.assertIsNone(src_incoming(*self.args, unittest=True)) self.assertEqual(self.queues[M_TO_FLASK_QUEUE].qsize(), 1) def test_file_datagram(self): # Setup packet = self.create_packet(FILE_DATAGRAM_HEADER + int_to_bytes(2) + nick_to_pub_key('Alice') + nick_to_pub_key('Bob') + 200 * b'a') self.queues[GATEWAY_QUEUE].put((self.ts, packet)) # Test self.assertIsNone(src_incoming(*self.args, unittest=True)) self.assertEqual(self.queues[DST_MESSAGE_QUEUE].qsize(), 0) self.assertEqual(self.queues[F_TO_FLASK_QUEUE].qsize(), 2) def test_group_invitation_datagram(self): # Setup packet = self.create_packet(GROUP_MSG_INVITE_HEADER + bytes(GROUP_ID_LENGTH) + nick_to_pub_key('Alice') + nick_to_pub_key('Bob')) self.queues[GATEWAY_QUEUE].put((self.ts, packet)) # Test self.assertIsNone(src_incoming(*self.args, unittest=True)) self.assertEqual(self.queues[DST_MESSAGE_QUEUE].qsize(), 0) self.assertEqual(self.queues[M_TO_FLASK_QUEUE].qsize(), 2) def test_group_join_datagram(self): # Setup packet = self.create_packet(GROUP_MSG_JOIN_HEADER + bytes(GROUP_ID_LENGTH) + nick_to_pub_key('Alice') + nick_to_pub_key('Bob')) self.queues[GATEWAY_QUEUE].put((self.ts, packet)) # Test self.assertIsNone(src_incoming(*self.args, unittest=True)) self.assertEqual(self.queues[DST_MESSAGE_QUEUE].qsize(), 0) self.assertEqual(self.queues[M_TO_FLASK_QUEUE].qsize(), 2) def test_group_add_datagram(self): # Setup packet = self.create_packet(GROUP_MSG_MEMBER_ADD_HEADER + bytes(GROUP_ID_LENGTH) + int_to_bytes(1) + nick_to_pub_key('Alice') + nick_to_pub_key('Bob')) self.queues[GATEWAY_QUEUE].put((self.ts, packet)) # Test self.assertIsNone(src_incoming(*self.args, unittest=True)) self.assertEqual(self.queues[DST_MESSAGE_QUEUE].qsize(), 0) self.assertEqual(self.queues[M_TO_FLASK_QUEUE].qsize(), 2) def test_group_remove_datagram(self): # Setup packet = self.create_packet(GROUP_MSG_MEMBER_REM_HEADER + bytes(GROUP_ID_LENGTH) + int_to_bytes(2) + nick_to_pub_key('Alice') + nick_to_pub_key('Bob')) self.queues[GATEWAY_QUEUE].put((self.ts, packet)) # Test self.assertIsNone(src_incoming(*self.args, unittest=True)) self.assertEqual(self.queues[DST_MESSAGE_QUEUE].qsize(), 0) self.assertEqual(self.queues[M_TO_FLASK_QUEUE].qsize(), 2) def test_group_exit_datagram(self): # Setup packet = self.create_packet(GROUP_MSG_EXIT_GROUP_HEADER + bytes(GROUP_ID_LENGTH) + nick_to_pub_key('Alice') + nick_to_pub_key('Bob')) self.queues[GATEWAY_QUEUE].put((self.ts, packet)) # Test self.assertIsNone(src_incoming(*self.args, unittest=True)) self.assertEqual(self.queues[DST_MESSAGE_QUEUE].qsize(), 0) self.assertEqual(self.queues[M_TO_FLASK_QUEUE].qsize(), 2)