Exemple #1
0
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
Exemple #2
0
    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)
Exemple #3
0
 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)
Exemple #4
0
    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)
Exemple #5
0
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
Exemple #6
0
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)
Exemple #7
0
    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])
Exemple #8
0
 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])
Exemple #9
0
 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)
Exemple #10
0
 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)
Exemple #11
0
    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])
Exemple #12
0
    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])
Exemple #13
0
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
Exemple #14
0
 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])
Exemple #15
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)))
Exemple #16
0
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
Exemple #17
0
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)
Exemple #18
0
    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()
Exemple #19
0
    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()
Exemple #20
0
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'))
Exemple #21
0
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()
Exemple #22
0
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)