예제 #1
0
    def connect(self, port: int) -> None:
        """Launch Tor as a subprocess.

        If TFC is running on top of Tails, do not launch a separate
        instance of Tor.
        """
        if self.platform_is_tails():
            self.controller = Controller.from_port(port=TOR_CONTROL_PORT)
            self.controller.authenticate()
            return None

        tor_data_directory = tempfile.TemporaryDirectory()
        tor_control_socket = os.path.join(tor_data_directory.name, 'control_socket')

        if not os.path.isfile('/usr/bin/tor'):
            raise CriticalError("Check that Tor is installed.")

        while True:
            try:
                self.tor_process = stem.process.launch_tor_with_config(
                    config={'DataDirectory':   tor_data_directory.name,
                            'SocksPort':       str(port),
                            'ControlSocket':   tor_control_socket,
                            'AvoidDiskWrites': '1',
                            'Log':             'notice stdout',
                            'GeoIPFile':       '/usr/share/tor/geoip',
                            'GeoIPv6File ':    '/usr/share/tor/geoip6'},
                    tor_cmd='/usr/bin/tor')
                break

            except OSError:
                pass  # Tor timed out. Try again.

        start_ts = time.monotonic()
        self.controller = stem.control.Controller.from_socket_file(path=tor_control_socket)
        self.controller.authenticate()

        while True:
            time.sleep(0.1)

            try:
                response = self.controller.get_info("status/bootstrap-phase")
            except stem.SocketClosed:
                raise CriticalError("Tor socket closed.")

            res_parts = shlex.split(response)
            summary   = res_parts[4].split('=')[1]

            if summary == 'Done':
                tor_version = self.controller.get_version().version_str.split(' (')[0]
                rp_print(f"Setup  70% - Tor {tor_version} is now running", bold=True)
                break

            if time.monotonic() - start_ts > 15:
                start_ts = time.monotonic()
                self.controller = stem.control.Controller.from_socket_file(path=tor_control_socket)
                self.controller.authenticate()
예제 #2
0
    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)
예제 #3
0
파일: crypto.py 프로젝트: todun/tfc
    def derive_public_key(private_key: 'X448PrivateKey') -> bytes:
        """Derive public key from an X448 private key."""
        public_key = private_key.public_key().public_bytes(
            encoding=Encoding.Raw, format=PublicFormat.Raw)  # type: bytes

        if not isinstance(public_key, bytes):
            raise CriticalError(
                f"Generated an invalid type ({type(public_key)}) public key.")

        if len(public_key) != TFC_PUBLIC_KEY_LENGTH:
            raise CriticalError(
                f"Generated an invalid size public key from private key ({len(public_key)} bytes)."
            )

        return public_key
예제 #4
0
    def connect(self, port: int) -> None:
        """Launch Tor as a subprocess.

        If TFC is running on top of Tails, do not launch a separate
        instance of Tor.
        """
        if self.platform_is_tails():
            self.controller = Controller.from_port(port=TOR_CONTROL_PORT)
            self.controller.authenticate()
            return None

        tor_data_directory = tempfile.TemporaryDirectory()
        tor_control_socket = os.path.join(tor_data_directory.name,
                                          'control_socket')

        if not os.path.isfile('/usr/bin/tor'):
            raise CriticalError("Check that Tor is installed.")

        self.launch_tor_process(port, tor_control_socket, tor_data_directory)

        start_ts = time.monotonic()
        self.controller = stem.control.Controller.from_socket_file(
            path=tor_control_socket)
        self.controller.authenticate()

        while True:
            time.sleep(0.1)

            try:
                response = self.controller.get_info("status/bootstrap-phase")
            except stem.SocketClosed:
                raise CriticalError("Tor socket closed.")

            res_parts = shlex.split(response)
            summary = res_parts[4].split('=')[1]

            if summary == 'Done':
                tor_version = self.controller.get_version().version_str.split(
                    ' (')[0]
                rp_print(f"Setup  70% - Tor {tor_version} is now running",
                         bold=True)
                break

            if time.monotonic() - start_ts > 15:
                start_ts = time.monotonic()
                self.controller = stem.control.Controller.from_socket_file(
                    path=tor_control_socket)
                self.controller.authenticate()
예제 #5
0
    def change_setting(self, key: str, value_str: str,
                       contact_list: 'ContactList',
                       group_list: 'GroupList') -> None:
        """Parse, update and store new setting value."""
        attribute = self.__getattribute__(key)

        try:
            if isinstance(attribute, bool):
                value_ = value_str.lower()
                if value_ not in ['true', 'false']:
                    raise ValueError
                value = (value_ == 'true')  # type: Union[bool, int, float]

            elif isinstance(attribute, int):
                value = int(value_str)
                if value < 0 or value > 2**64 - 1:
                    raise ValueError

            elif isinstance(attribute, float):
                value = float(value_str)
                if value < 0.0:
                    raise ValueError
            else:
                raise CriticalError("Invalid attribute type in settings.")

        except ValueError:
            raise FunctionReturn(f"Error: Invalid value '{value_str}'")

        self.validate_key_value_pair(key, value, contact_list, group_list)

        setattr(self, key, value)
        self.store_settings()
예제 #6
0
    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.")
예제 #7
0
    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.")
예제 #8
0
파일: db_settings.py 프로젝트: savg110/tfc
    def load_settings(self) -> None:
        """Load settings from the encrypted database."""
        pt_bytes = self.database.load_database()

        # Update settings based on plaintext byte string content
        for key in self.key_list:

            attribute = self.__getattribute__(key)

            if isinstance(attribute, bool):
                value    = bytes_to_bool(pt_bytes[0])  # type: Union[bool, int, float]
                pt_bytes = pt_bytes[ENCODED_BOOLEAN_LENGTH:]

            elif isinstance(attribute, int):
                value    = bytes_to_int(pt_bytes[:ENCODED_INTEGER_LENGTH])
                pt_bytes = pt_bytes[ENCODED_INTEGER_LENGTH:]

            elif isinstance(attribute, float):
                value    = bytes_to_double(pt_bytes[:ENCODED_FLOAT_LENGTH])
                pt_bytes = pt_bytes[ENCODED_FLOAT_LENGTH:]

            else:
                raise CriticalError("Invalid data type in settings default values.")

            setattr(self, key, value)
예제 #9
0
    def load_master_key(self) -> bytes:
        """Derive the master key from password and salt.

        Load the salt, hash, and key derivation settings from the login
        database. Derive the purported master key from the salt and
        entered password. If the BLAKE2b hash of derived master key
        matches the hash in the login database, accept the derived
        master key.
        """
        database_data = self.database.load_database()

        if len(database_data) != MASTERKEY_DB_SIZE:
            raise CriticalError(f"Invalid {self.file_name} database size.")

        salt, key_hash, time_bytes, memory_bytes, parallelism_bytes \
            = separate_headers(database_data, [ARGON2_SALT_LENGTH, BLAKE2_DIGEST_LENGTH,
                                               ENCODED_INTEGER_LENGTH, ENCODED_INTEGER_LENGTH])

        time_cost   = bytes_to_int(time_bytes)
        memory_cost = bytes_to_int(memory_bytes)
        parallelism = bytes_to_int(parallelism_bytes)

        while True:
            password = MasterKey.get_password()
            phase("Deriving master key", head=2, offset=len("Password correct"))
            purp_key = argon2_kdf(password, salt, time_cost, memory_cost, parallelism)

            if blake2b(purp_key) == key_hash:
                phase("Password correct", done=True, delay=1)
                clear_screen()
                return purp_key

            phase("Invalid password", done=True, delay=1)
            print_on_previous_line(reps=5)
예제 #10
0
    def search_serial_interface(self) -> str:
        """Search for serial interface."""
        if self.settings.serial_usb_adapter:
            search_announced = False

            if not self.init_found:
                print_on_previous_line()
                phase("Searching for USB-to-serial interface")

            while True:
                time.sleep(0.1)
                for f in sorted(os.listdir('/dev')):
                    if f.startswith('ttyUSB'):
                        if self.init_found:
                            time.sleep(1.5)
                        phase('Found', done=True)
                        if self.init_found:
                            print_on_previous_line(reps=2)
                        self.init_found = True
                        return '/dev/{}'.format(f)
                else:
                    if not search_announced:
                        if self.init_found:
                            phase(
                                "Serial adapter disconnected. Waiting for interface",
                                head=1)
                        search_announced = True

        else:
            f = 'ttyS0'
            if f in sorted(os.listdir('/dev/')):
                return '/dev/{}'.format(f)
            raise CriticalError("Error: /dev/{} was not found.".format(f))
예제 #11
0
파일: database.py 프로젝트: xprog12/tfc
    def load_database(self) -> bytes:
        """Load data from database.

        This function first checks if a temporary file exists from
        previous session. The integrity of the temporary file is
        verified with a BLAKE2b-based checksum before the database is
        replaced.

        The function then reads the up-to-date database content.
        """
        if os.path.isfile(self.database_temp):
            if self.verify_file(self.database_temp):
                os.replace(self.database_temp, self.database_name)
            else:
                # If temp file failed integrity check, the file is most likely corrupt,
                # so we delete it and continue using the old file to ensure atomicity.
                os.remove(self.database_temp)

        with open(self.database_name, 'rb') as f:
            database_data = f.read()

        database_data, digest = separate_trailer(database_data,
                                                 BLAKE2_DIGEST_LENGTH)

        if blake2b(database_data) != digest:
            raise CriticalError(
                f"Invalid data in login database {self.database_name}")

        return database_data
예제 #12
0
    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)
예제 #13
0
def get_b58_key(key_type:      str,         # The type of Base58 key to be entered
                settings:      'Settings',  # Settings object
                short_address: str = ''     # The contact's short Onion address
                ) -> bytes:                 # The Base58 decoded key
    """Ask the user to input a Base58 encoded key."""
    if key_type == B58_PUBLIC_KEY:
        clear_screen()
        m_print(f"{ECDHE} key exchange", head=1, tail=1, bold=True)
        m_print("If needed, resend your public key to the contact by pressing <Enter>", tail=1)

        box_msg = f"Enter public key of {short_address} (from Relay)"
    elif key_type == B58_LOCAL_KEY:
        box_msg = "Enter local key decryption key (from Transmitter)"
    else:
        raise CriticalError("Invalid key type")

    while True:
        rx_pk = box_input(box_msg, key_type=key_type, guide=not (settings.local_testing_mode or settings.qubes))
        rx_pk = ''.join(rx_pk.split())

        if key_type == B58_PUBLIC_KEY and rx_pk == '':
            return rx_pk.encode()

        try:
            return b58decode(rx_pk, public_key=(key_type == B58_PUBLIC_KEY))
        except ValueError:
            m_print("Checksum error - Check that the entered key is correct.")
            print_on_previous_line(reps=(4 if settings.local_testing_mode else 5), delay=1)

            if key_type == B58_PUBLIC_KEY and len(rx_pk) == ENCODED_B58_PUB_KEY_LENGTH:
                raise ValueError(rx_pk)
예제 #14
0
파일: db_keys.py 프로젝트: xprog12/tfc
    def _load_keys(self) -> None:
        """Load KeySets from the encrypted database.

        This function first reads and decrypts the database content. It
        then splits the plaintext into a list of 176-byte blocks. Each
        block contains the serialized data of one KeySet. Next, the
        function will remove from the list all dummy KeySets (that start
        with the `dummy_id` byte string). The function will then
        populate the `self.keysets` list with KeySet objects, the data
        of which is sliced and decoded from the dummy-free blocks.
        """
        pt_bytes = self.database.load_database()
        blocks = split_byte_string(pt_bytes, item_len=KEYSET_LENGTH)
        df_blocks = [b for b in blocks if not b.startswith(self.dummy_id)]

        for block in df_blocks:
            if len(block) != KEYSET_LENGTH:
                raise CriticalError("Invalid data in key database.")

            onion_pub_key, tx_mk, rx_mk, tx_hk, rx_hk, tx_harac_bytes, rx_harac_bytes \
                = separate_headers(block, [ONION_SERVICE_PUBLIC_KEY_LENGTH] + 4*[SYMMETRIC_KEY_LENGTH] + [HARAC_LENGTH])

            self.keysets.append(
                KeySet(onion_pub_key=onion_pub_key,
                       tx_mk=tx_mk,
                       rx_mk=rx_mk,
                       tx_hk=tx_hk,
                       rx_hk=rx_hk,
                       tx_harac=bytes_to_int(tx_harac_bytes),
                       rx_harac=bytes_to_int(rx_harac_bytes),
                       store_keys=self.store_keys))
예제 #15
0
    def load_settings(self) -> None:
        """Load settings from the encrypted database."""
        with open(self.file_name, 'rb') as f:
            ct_bytes = f.read()

        pt_bytes = auth_and_decrypt(ct_bytes,
                                    self.master_key.master_key,
                                    database=self.file_name)

        # Update settings based on plaintext byte string content
        for key in self.key_list:

            attribute = self.__getattribute__(key)

            if isinstance(attribute, bool):
                value = bytes_to_bool(
                    pt_bytes[0])  # type: Union[bool, int, float]
                pt_bytes = pt_bytes[ENCODED_BOOLEAN_LENGTH:]

            elif isinstance(attribute, int):
                value = bytes_to_int(pt_bytes[:ENCODED_INTEGER_LENGTH])
                pt_bytes = pt_bytes[ENCODED_INTEGER_LENGTH:]

            elif isinstance(attribute, float):
                value = bytes_to_double(pt_bytes[:ENCODED_FLOAT_LENGTH])
                pt_bytes = pt_bytes[ENCODED_FLOAT_LENGTH:]

            else:
                raise CriticalError(
                    "Invalid data type in settings default values.")

            setattr(self, key, value)
예제 #16
0
파일: crypto.py 프로젝트: gtog/tfc
def byte_padding(
    bytestring: bytes  # Bytestring to be padded
) -> bytes:  # Padded bytestring
    """Pad bytestring to next 255 bytes.

    TFC adds padding to messages it outputs. The padding ensures each
    assembly packet has a constant length. When traffic masking is
    disabled, because of padding the packet length reveals only the
    maximum length of the compressed message.

    When traffic masking is enabled, the padding contributes to traffic
    flow confidentiality: During traffic masking, TFC will output a
    constant stream of padded packets at constant intervals that hides
    metadata about message length (i.e., the adversary won't be able to
    distinguish when transmission of packet or series of packets starts
    and stops), as well as the type (message/file) of transferred data.

    TFC uses PKCS #7 padding scheme described in RFC 2315 and RFC 5652:
        https://tools.ietf.org/html/rfc2315#section-10.3
        https://tools.ietf.org/html/rfc5652#section-6.3

    For a better explanation, see
        https://en.wikipedia.org/wiki/Padding_(cryptography)#PKCS#5_and_PKCS#7
    """
    padder = padding.PKCS7(PADDING_LENGTH * BITS_PER_BYTE).padder()
    bytestring = padder.update(bytestring)
    bytestring += padder.finalize()

    if len(bytestring) % PADDING_LENGTH != 0:  # pragma: no cover
        raise CriticalError("Invalid padding length.")

    return bytestring
예제 #17
0
def stem_compatible_ed25519_key_from_private_key(private_key: bytes) -> str:
    """Tor's custom encoding format for v3 Onion Service private keys.

    This code is based on Tor's testing code at
        https://github.com/torproject/tor/blob/8e84968ffbf6d284e8a877ddcde6ded40b3f5681/src/test/ed25519_exts_ref.py#L48
    """
    b = 256

    def bit(h: bytes, i: int) -> int:
        """\
        Output (i % 8 + 1) right-most bit of (i // 8) right-most byte
        of the digest.
        """
        return (h[i // 8] >> (i % 8)) & 1

    def encode_int(y: int) -> bytes:
        """Encode integer to 32-byte bytestring (little-endian format)."""
        bits = [(y >> i) & 1 for i in range(b)]
        return b''.join([bytes([(sum([bits[i * 8 + j] << j for j in range(8)]))]) for i in range(b // 8)])

    def expand_private_key(sk: bytes) -> bytes:
        """Expand private key to base64 blob."""
        h = hashlib.sha512(sk).digest()
        a = 2 ** (b - 2) + sum(2 ** i * bit(h, i) for i in range(3, b - 2))
        k = b''.join([bytes([h[i]]) for i in range(b // 8, b // 4)])

        return encode_int(a) + k

    if len(private_key) != ONION_SERVICE_PRIVATE_KEY_LENGTH:
        raise CriticalError("Onion Service private key had an invalid length.")

    expanded_private_key = expand_private_key(private_key)

    return base64.b64encode(expanded_private_key).decode()
예제 #18
0
    def change_setting(self, key: str, value_str: str) -> None:
        """Parse, update and store new setting value."""
        attribute = self.__getattribute__(key)
        try:
            if isinstance(attribute, bool):
                value = dict(
                    true=True,
                    false=False)[value_str.lower()]  # type: Union[bool, int]

            elif isinstance(attribute, int):
                value = int(value_str)
                if value < 0 or value > MAX_INT:
                    raise ValueError

            else:
                raise CriticalError("Invalid attribute type in settings.")

        except (KeyError, ValueError):
            raise SoftError(f"Error: Invalid setting value '{value_str}'.",
                            delay=1,
                            tail_clear=True)

        self.validate_key_value_pair(key, value)

        setattr(self, key, value)
        self.store_settings()
예제 #19
0
    def read(self) -> bytes:
        """Read data via socket/serial interface."""
        if self.settings.local_testing_mode:
            while True:
                try:
                    return self.interface.recv()
                except KeyboardInterrupt:
                    pass
                except EOFError:
                    raise CriticalError("IPC client disconnected.")
        else:
            while True:
                try:
                    start_time = 0.0
                    read_buffer = bytearray()
                    while True:
                        read = self.interface.read(1000)
                        if read:
                            start_time = time.monotonic()
                            read_buffer.extend(read)
                        else:
                            if read_buffer:
                                delta = time.monotonic() - start_time
                                if delta > self.settings.rxm_receive_timeout:
                                    return bytes(read_buffer)
                            else:
                                time.sleep(0.001)

                except KeyboardInterrupt:
                    pass
                except SerialException:
                    self.establish_serial()
                    self.read()
예제 #20
0
파일: db_settings.py 프로젝트: savg110/tfc
    def change_setting(self,
                       key:          str,  # Name of the setting
                       value_str:    str,  # Value of the setting
                       contact_list: 'ContactList',
                       group_list:   'GroupList'
                       ) -> None:
        """Parse, update and store new setting value."""
        attribute = self.__getattribute__(key)

        try:
            if isinstance(attribute, bool):
                value = dict(true=True, false=False)[value_str.lower()]  # type: Union[bool, int, float]

            elif isinstance(attribute, int):
                value = int(value_str)
                if value < 0 or value > MAX_INT:
                    raise ValueError

            elif isinstance(attribute, float):
                value = float(value_str)
                if value < 0.0:
                    raise ValueError

            else:
                raise CriticalError("Invalid attribute type in settings.")

        except (KeyError, ValueError):
            raise SoftError(f"Error: Invalid setting value '{value_str}'.", head_clear=True)

        self.validate_key_value_pair(key, value, contact_list, group_list)

        setattr(self, key, value)
        self.store_settings()
예제 #21
0
    def store_settings(self) -> None:
        """Store settings to an encrypted database.

        The plaintext in the encrypted database is a constant
        length bytestring regardless of stored setting values.
        """
        attribute_list = [self.__getattribute__(k) for k in self.key_list]

        bytes_lst = []
        for a in attribute_list:
            if isinstance(a, bool):
                bytes_lst.append(bool_to_bytes(a))
            elif isinstance(a, int):
                bytes_lst.append(int_to_bytes(a))
            elif isinstance(a, float):
                bytes_lst.append(double_to_bytes(a))
            else:
                raise CriticalError("Invalid attribute type in settings.")

        pt_bytes = b''.join(bytes_lst)
        ct_bytes = encrypt_and_sign(pt_bytes, self.master_key.master_key)

        ensure_dir(DIR_USER_DATA)
        with open(self.file_name, 'wb+') as f:
            f.write(ct_bytes)
예제 #22
0
파일: packet.py 프로젝트: gtog/tfc
def send_packet(
    key_list: 'KeyList',  # Key list object
    gateway: 'Gateway',  # Gateway object
    log_queue: 'Queue',  # Multiprocessing queue for logged messages
    assembly_packet: bytes,  # Padded plaintext assembly packet
    onion_pub_key: Optional[
        bytes] = None,  # Recipient v3 Onion Service address
    log_messages: Optional[
        bool] = None,  # When True, log the message assembly packet
    log_as_ph: Optional[
        bool] = None  # When True, log assembly packet as placeholder data
) -> None:
    """Encrypt and send assembly packet.

    The assembly packets are encrypted using a symmetric message key.
    TFC provides forward secrecy via a hash ratchet, meaning previous
    message key is replaced by it's BLAKE2b hash. The preimage
    resistance of the hash function prevents retrospective decryption of
    ciphertexts in cases of physical compromise.

    The hash ratchet state (the number of times initial message key has
    been passed through BLAKE2b) is delivered to recipient inside the
    hash ratchet counter. This counter is encrypted with a static
    symmetric key called the header key.

    The encrypted assembly packet and encrypted harac are prepended with
    datagram headers that tell if the encrypted assembly packet is a
    command or a message. Packets with MESSAGE_DATAGRAM_HEADER also
    contain a second header, which is the public key of the recipient's
    Onion Service. This allows the ciphertext to be requested from Relay
    Program's server by the correct contact.

    Once the encrypted_packet has been output, the hash ratchet advances
    to the next state, and the assembly packet is pushed to log_queue,
    which is read by the `log_writer_loop` process (that can be found
    at src.common.db_logs). This approach prevents IO delays caused by
    `input_loop` reading the log file from affecting the `sender_loop`
    process, which could reveal schedule information under traffic
    masking mode.
    """
    if len(assembly_packet) != ASSEMBLY_PACKET_LENGTH:
        raise CriticalError("Invalid assembly packet PT length.")

    if onion_pub_key is None:
        keyset = key_list.get_keyset(LOCAL_PUBKEY)
        header = COMMAND_DATAGRAM_HEADER
    else:
        keyset = key_list.get_keyset(onion_pub_key)
        header = MESSAGE_DATAGRAM_HEADER + onion_pub_key

    harac_in_bytes = int_to_bytes(keyset.tx_harac)
    encrypted_harac = encrypt_and_sign(harac_in_bytes, keyset.tx_hk)
    encrypted_message = encrypt_and_sign(assembly_packet, keyset.tx_mk)
    encrypted_packet = header + encrypted_harac + encrypted_message
    gateway.write(encrypted_packet)

    keyset.rotate_tx_mk()

    log_queue.put((onion_pub_key, assembly_packet, log_messages, log_as_ph,
                   key_list.master_key))
예제 #23
0
    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()
예제 #24
0
    def load_onion_service_private_key(self) -> bytes:
        """Load the Onion Service private key from the encrypted database."""
        onion_private_key = self.database.load_database()

        if len(onion_private_key) != ONION_SERVICE_PRIVATE_KEY_LENGTH:
            raise CriticalError("Invalid Onion Service private key length.")

        return onion_private_key
예제 #25
0
파일: crypto.py 프로젝트: gtog/tfc
def argon2_kdf(
    password: str,  # Password to derive the key from
    salt: bytes,  # Salt to derive the key from
    time_cost: int = ARGON2_PSK_TIME_COST,  # Number of iterations
    memory_cost:
    int = ARGON2_PSK_MEMORY_COST,  # Amount of memory to use (in bytes)
    parallelism: int = 2  # Number of threads to use
) -> bytes:  # The derived key
    """Derive an encryption key from password and salt using Argon2d.

    Argon2 is a key derivation function (KDF) designed by Alex Biryukov,
    Daniel Dinu, and Dmitry Khovratovich from the University of
    Luxembourg. The algorithm is the winner of the 2015 Password Hashing
    Competition (PHC).

    For more details, see
        https://password-hashing.net/
        https://github.com/P-H-C/phc-winner-argon2/blob/master/argon2-specs.pdf
        https://en.wikipedia.org/wiki/Argon2

    The purpose of the KDF is to stretch a password into a 256-bit key.
    Argon2 features a slow, memory-hard hash function that consumes
    computational resources of an attacker that attempts a dictionary
    or a brute force attack. The accompanied 256-bit salt prevents
    rainbow-table attacks, forcing each attack to take place against an
    individual (physically compromised) TFC-endpoint, or PSK
    transmission media.

    The used Argon2 version is Argon2d that uses data-dependent memory
    access, which maximizes security against time-memory trade-off
    (TMTO) attacks at the risk of side-channel attacks. The IETF
    recommends using Argon2id (that is side-channel resistant and almost
    as secure as Argon2d against TMTO attacks) **except** when there is
    a reason to prefer Argon2d (or Argon2i). The reason TFC uses Argon2d
    is key derivation only takes place on Source and Destination
    Computer. As these computers are connected to the Networked Computer
    only via a data diode, they do not leak any information via
    side-channels to the adversary. The expected attacks are against
    physically compromised data storage devices where the encrypted data
    is at rest. In such situation, Argon2d is the most secure option.

    The correctness of the Argon2d implementation* is tested by TFC unit
    tests. The testing is done in limited scope by using an official KAT.

    * https://github.com/P-H-C/phc-winner-argon2
      https://github.com/hynek/argon2_cffi
    """
    if len(salt) != ARGON2_SALT_LENGTH:
        raise CriticalError("Invalid salt length.")

    key = argon2.low_level.hash_secret_raw(secret=password.encode(),
                                           salt=salt,
                                           time_cost=time_cost,
                                           memory_cost=memory_cost,
                                           parallelism=parallelism,
                                           hash_len=SYMMETRIC_KEY_LENGTH,
                                           type=argon2.Type.D)  # type: bytes
    return key
예제 #26
0
def unicode_padding(string: str) -> str:
    """Pad Unicode string to 255 chars.

    Database fields are padded with Unicode chars and then encoded
    with UTF-32 to hide the metadata about plaintext field length.
    """
    from src.common.exceptions import CriticalError

    if len(string) >= PADDING_LENGTH:
        raise CriticalError("Invalid input size.")

    length = PADDING_LENGTH - (len(string) % PADDING_LENGTH)
    string += length * chr(length)

    if len(string) != PADDING_LENGTH:  # pragma: no cover
        raise CriticalError("Invalid padded string size.")

    return string
예제 #27
0
 def establish_serial(self) -> None:
     """Create a new Serial object."""
     try:
         serial_nh = self.search_serial_interface()
         self.interface = serial.Serial(
             serial_nh, self.settings.session_serial_baudrate, timeout=0)
     except SerialException:
         raise CriticalError(
             "SerialException. Ensure $USER is in the dialout group.")
예제 #28
0
def check_kernel_version() -> None:
    """Check that the Linux kernel version is at least 4.8.

    This check ensures that TFC only runs on Linux kernels that use
    the new ChaCha20 based CSPRNG: https://lkml.org/lkml/2016/7/25/43
    """
    major_v, minor_v = [int(i) for i in os.uname()[2].split('.')[:2]]

    if major_v < 4 or (major_v == 4 and minor_v < 8):
        raise CriticalError("Insecure kernel CSPRNG version detected.")
예제 #29
0
    def get_local_ip_addr() -> str:
        """Get local IP address of the system."""
        s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        try:
            s.connect(('192.0.0.8', 1027))
        except socket.error:
            raise CriticalError("Socket error")
        ip_address = s.getsockname()[0]  # type: str

        return ip_address
예제 #30
0
파일: db_keys.py 프로젝트: AJMartel/tfc
 def manage(self, command: str, *params: Any) -> None:
     """Manage keyset database based on data received from km_queue."""
     if command == KDB_ADD_ENTRY_HEADER:
         self.add_keyset(*params)
     elif command == KDB_REMOVE_ENTRY_HEADER:
         self.remove_keyset(*params)
     elif command == KDB_CHANGE_MASTER_KEY_HEADER:
         self.change_master_key(*params)
     else:
         raise CriticalError("Invalid KeyList management command.")