예제 #1
0
파일: test_crypto.py 프로젝트: dimwap/tfc
    def test_invalid_size_entropy_from_getrandom_raises_critical_error(
            self, mock_getrandom: MagicMock, mock_blake2b: MagicMock) -> None:
        with self.assertRaises(SystemExit):
            csprng()
        with self.assertRaises(SystemExit):
            csprng()

        mock_getrandom.assert_called_with(SYMMETRIC_KEY_LENGTH, flags=0)
        mock_blake2b.assert_not_called()
예제 #2
0
파일: test_crypto.py 프로젝트: AJMartel/tfc
    def test_travis_mock(self):
        # Setup
        o_environ = os.environ
        os.environ = dict(TRAVIS='true')

        # Test
        self.assertEqual(len(csprng()), KEY_LENGTH)
        self.assertIsInstance(csprng(), bytes)

        # Teardown
        os.environ = o_environ
예제 #3
0
def process_local_key(ts: 'datetime', packet: bytes, window_list: 'WindowList',
                      contact_list: 'ContactList', key_list: 'KeyList',
                      settings: 'Settings') -> None:
    """Decrypt local key packet and add local contact/keyset."""
    bootstrap = not key_list.has_local_key()

    try:
        while True:
            clear_screen()
            box_print("Received encrypted local key", tail=1)
            kdk = get_b58_key(B58_LOCAL_KEY, settings)

            try:
                pt = auth_and_decrypt(packet[1:], key=kdk, soft_e=True)
                break
            except nacl.exceptions.CryptoError:
                if bootstrap:
                    raise FunctionReturn(
                        "Error: Incorrect key decryption key.", delay=1.5)
                c_print("Incorrect key decryption key.", head=1)
                clear_screen(delay=1.5)

        key = pt[0:32]
        hek = pt[32:64]
        conf_code = pt[64:65]

        # Add local contact to contact list database
        contact_list.add_contact(LOCAL_ID, LOCAL_ID, LOCAL_ID,
                                 bytes(FINGERPRINT_LEN),
                                 bytes(FINGERPRINT_LEN), False, False, True)

        # Add local keyset to keyset database
        key_list.add_keyset(rx_account=LOCAL_ID,
                            tx_key=key,
                            rx_key=csprng(),
                            tx_hek=hek,
                            rx_hek=csprng())

        box_print(f"Confirmation code for TxM: {conf_code.hex()}", head=1)

        local_win = window_list.get_local_window()
        local_win.add_new(ts, "Added new local key.")

        if bootstrap:
            window_list.active_win = local_win

    except KeyboardInterrupt:
        raise FunctionReturn("Local key setup aborted.",
                             delay=1,
                             head=3,
                             tail_clear=True)
예제 #4
0
    def new_master_key(self) -> None:
        """Create a new master key from salt and password."""
        password = MasterKey.new_password()
        salt = csprng()
        rounds = ARGON2_ROUNDS
        memory = ARGON2_MIN_MEMORY

        phase("Deriving master key", head=2)
        while True:
            time_start = time.monotonic()
            master_key, parallellism = argon2_kdf(password,
                                                  salt,
                                                  rounds,
                                                  memory=memory,
                                                  local_test=self.local_test)
            time_final = time.monotonic() - time_start

            if time_final > 3.0:
                self.master_key = master_key
                ensure_dir(f'{DIR_USER_DATA}/')
                with open(self.file_name, 'wb+') as f:
                    f.write(salt + hash_chain(self.master_key) +
                            int_to_bytes(rounds) + int_to_bytes(memory) +
                            int_to_bytes(parallellism))
                phase(DONE)
                break
            else:
                memory *= 2
예제 #5
0
파일: test_crypto.py 프로젝트: dimwap/tfc
 def test_getrandom_called_with_correct_parameters_and_hashes_with_blake2b(
         self, mock_getrandom: MagicMock) -> None:
     key = csprng(XCHACHA20_NONCE_LENGTH)
     self.assertEqual(
         key, blake2b(self.mock_entropy,
                      digest_size=XCHACHA20_NONCE_LENGTH))
     mock_getrandom.assert_called_with(XCHACHA20_NONCE_LENGTH, flags=0)
예제 #6
0
def split_to_assembly_packets(payload: bytes, p_type: str) -> List[bytes]:
    """Split payload to assembly packets.

    Messages and commands are compressed to reduce transmission time.
    Files directed to this function during traffic masking have been
    compressed at an earlier point.

    If the compressed message cannot be sent over one packet, it is
    split into multiple assembly packets. Long messages are encrypted
    with an inner layer of XChaCha20-Poly1305 to provide sender based
    control over partially transmitted data. Regardless of packet size,
    files always have an inner layer of encryption, and it is added
    before the file data is passed to this function. Commands do not
    need sender-based control, so they are only delivered with a hash
    that makes integrity check easy.

    First assembly packet in file transmission is prepended with an
    8-byte packet counter header that tells the sender and receiver how
    many packets the file transmission requires.

    Each assembly packet is prepended with a header that tells the
    Receiver Program if the packet is a short (single packet)
    transmission or if it's the start packet, a continuation packet, or
    the last packet of a multi-packet transmission.
    """
    s_header = {MESSAGE: M_S_HEADER, FILE: F_S_HEADER, COMMAND: C_S_HEADER}[p_type]
    l_header = {MESSAGE: M_L_HEADER, FILE: F_L_HEADER, COMMAND: C_L_HEADER}[p_type]
    a_header = {MESSAGE: M_A_HEADER, FILE: F_A_HEADER, COMMAND: C_A_HEADER}[p_type]
    e_header = {MESSAGE: M_E_HEADER, FILE: F_E_HEADER, COMMAND: C_E_HEADER}[p_type]

    if p_type in [MESSAGE, COMMAND]:
        payload = zlib.compress(payload, level=COMPRESSION_LEVEL)

    if len(payload) < PADDING_LENGTH:
        padded      = byte_padding(payload)
        packet_list = [s_header + padded]

    else:
        if p_type == MESSAGE:
            msg_key = csprng()
            payload = encrypt_and_sign(payload, msg_key)
            payload += msg_key

        elif p_type == FILE:
            payload = bytes(FILE_PACKET_CTR_LENGTH) + payload

        elif p_type == COMMAND:
            payload += blake2b(payload)

        padded = byte_padding(payload)
        p_list = split_byte_string(padded, item_len=PADDING_LENGTH)

        if p_type == FILE:
            p_list[0] = int_to_bytes(len(p_list)) + p_list[0][FILE_PACKET_CTR_LENGTH:]

        packet_list = ([l_header + p_list[0]] +
                       [a_header + p for p in p_list[1:-1]] +
                       [e_header + p_list[-1]])

    return packet_list
예제 #7
0
파일: db_keys.py 프로젝트: xprog12/tfc
    def generate_dummy_keyset() -> 'KeySet':
        """Generate a dummy KeySet object.

        The dummy KeySet simplifies the code around the constant length
        serialization when the data is stored to, or read from the
        database.

        In case the dummy keyset would ever be loaded accidentally, it
        uses a set of random keys to prevent decryption by eavesdropper.
        """
        return KeySet(onion_pub_key=onion_address_to_pub_key(DUMMY_CONTACT),
                      tx_mk=csprng(),
                      rx_mk=csprng(),
                      tx_hk=csprng(),
                      rx_hk=csprng(),
                      tx_harac=INITIAL_HARAC,
                      rx_harac=INITIAL_HARAC,
                      store_keys=lambda: None)
예제 #8
0
def split_to_assembly_packets(payload: bytes, p_type: str) -> List[bytes]:
    """Split payload to assembly packets.

    Messages and commands are compressed to reduce transmission time.
    Files have been compressed at earlier phase, before B85 encoding.

    If the compressed message can not be sent over one packet, it is
    split into multiple assembly packets with headers. Long messages
    are encrypted with inner layer of XSalsa20-Poly1305 to provide
    sender based control over partially transmitted data. Regardless
    of packet size, files always have an inner layer of encryption,
    and it is added in earlier phase. Commands do not need
    sender-based control, so they are only delivered with hash that
    makes integrity check easy.

    First assembly packet in file transmission is prepended with 8-byte
    packet counter that tells sender and receiver how many packets the
    file transmission requires.
    """
    s_header = {MESSAGE: M_S_HEADER, FILE: F_S_HEADER, COMMAND: C_S_HEADER}[p_type]
    l_header = {MESSAGE: M_L_HEADER, FILE: F_L_HEADER, COMMAND: C_L_HEADER}[p_type]
    a_header = {MESSAGE: M_A_HEADER, FILE: F_A_HEADER, COMMAND: C_A_HEADER}[p_type]
    e_header = {MESSAGE: M_E_HEADER, FILE: F_E_HEADER, COMMAND: C_E_HEADER}[p_type]

    if p_type in [MESSAGE, COMMAND]:
        payload = zlib.compress(payload, level=COMPRESSION_LEVEL)

    if len(payload) < PADDING_LEN:
        padded      = byte_padding(payload)
        packet_list = [s_header + padded]

    else:
        if p_type == MESSAGE:
            msg_key = csprng()
            payload = encrypt_and_sign(payload, msg_key)
            payload += msg_key

        elif p_type == FILE:
            payload = bytes(FILE_PACKET_CTR_LEN) + payload

        elif p_type == COMMAND:
            payload += hash_chain(payload)

        padded = byte_padding(payload)
        p_list = split_byte_string(padded, item_len=PADDING_LEN)

        if p_type == FILE:
            p_list[0] = int_to_bytes(len(p_list)) + p_list[0][FILE_PACKET_CTR_LEN:]

        packet_list = ([l_header + p_list[0]] +
                       [a_header + p for p in p_list[1:-1]] +
                       [e_header + p_list[-1]])

    return packet_list
예제 #9
0
    def new_master_key(self) -> bytes:
        """Create a new master key from password and salt.

        The generated master key depends on a 256-bit salt and the
        password entered by the user. Additional computational strength
        is added by the slow hash function (Argon2d). This method
        automatically tweaks the Argon2 memory parameter so that key
        derivation on used hardware takes at least three seconds. The
        more cores and the faster each core is, the more security a
        given password provides.

        The preimage resistance of BLAKE2b prevents derivation of master
        key from the stored hash, and Argon2d ensures brute force and
        dictionary attacks against the master password are painfully
        slow even with GPUs/ASICs/FPGAs, as long as the password is
        sufficiently strong.

        The salt does not need additional protection as the security it
        provides depends on the salt space in relation to the number of
        attacked targets (i.e. if two or more physically compromised
        systems happen to share the same salt, the attacker can speed up
        the attack against those systems with time-memory-trade-off
        attack).

        A 256-bit salt ensures that even in a group of 4.8*10^29 users,
        the probability that two users share the same salt is just
        10^(-18).*
            * https://en.wikipedia.org/wiki/Birthday_attack
        """
        password = MasterKey.new_password()
        salt = csprng(ARGON2_SALT_LENGTH)
        memory = ARGON2_MIN_MEMORY

        parallelism = multiprocessing.cpu_count()
        if self.local_test:
            parallelism = max(1, parallelism // 2)

        phase("Deriving master key", head=2)
        while True:
            time_start = time.monotonic()
            master_key = argon2_kdf(password, salt, ARGON2_ROUNDS, memory,
                                    parallelism)
            kd_time = time.monotonic() - time_start

            if kd_time < MIN_KEY_DERIVATION_TIME:
                memory *= 2
            else:
                ensure_dir(DIR_USER_DATA)
                with open(self.file_name, 'wb+') as f:
                    f.write(salt + blake2b(master_key) + int_to_bytes(memory) +
                            int_to_bytes(parallelism))
                phase(DONE)
                return master_key
예제 #10
0
    def process_file_data(data: bytes) -> bytes:
        """Compress, encrypt and encode file data.

        Compress file to reduce data transmission time. Add an inner
        layer of encryption to provide sender-based control over partial
        transmission.
        """
        compressed = zlib.compress(data, level=COMPRESSION_LEVEL)
        file_key = csprng()
        processed = encrypt_and_sign(compressed, key=file_key)
        processed += file_key
        return processed
예제 #11
0
파일: files.py 프로젝트: AJMartel/tfc
    def process_file_data(self) -> None:
        """Compress, encrypt and encode file data.

        Compress file to reduce data transmission time. Add inner
        layer of encryption to provide sender-based control over
        partial transmission. Encode data with Base85. This prevents
        inner ciphertext from colliding with file header delimiters.
        """
        compressed = zlib.compress(self.data, level=COMPRESSION_LEVEL)

        file_key = csprng()
        encrypted = encrypt_and_sign(compressed, key=file_key)
        encrypted += file_key

        self.data = base64.b85encode(encrypted)
예제 #12
0
def export_file(settings: 'Settings', nh_queue: 'Queue') -> None:
    """Encrypt and export file to NH.

    This is a faster method to send large files. It is used together
    with file import (/fi) command that uploads ciphertext to RxM for
    RxM-side decryption. Key is generated automatically so that bad
    passwords selected by users do not affect security of ciphertexts.
    """
    if settings.session_traffic_masking:
        raise FunctionReturn(
            "Error: Command is disabled during traffic masking.")

    path = ask_path_gui("Select file to export...", settings, get_file=True)
    name = path.split('/')[-1]
    data = bytearray()
    data.extend(str_to_bytes(name))

    if not os.path.isfile(path):
        raise FunctionReturn("Error: File not found.")

    if os.path.getsize(path) == 0:
        raise FunctionReturn("Error: Target file is empty.")

    phase("Reading data")
    with open(path, 'rb') as f:
        data.extend(f.read())
    phase(DONE)

    phase("Compressing data")
    comp = bytes(zlib.compress(bytes(data), level=COMPRESSION_LEVEL))
    phase(DONE)

    phase("Encrypting data")
    file_key = csprng()
    file_ct = encrypt_and_sign(comp, key=file_key)
    phase(DONE)

    phase("Exporting data")
    queue_to_nh(EXPORTED_FILE_HEADER + file_ct, settings, nh_queue)
    phase(DONE)

    print_key(f"Decryption key for file '{name}':",
              file_key,
              settings,
              no_split=True,
              file_key=True)
예제 #13
0
파일: db_onion.py 프로젝트: todun/tfc
    def __init__(self, master_key: 'MasterKey') -> None:
        """Create a new OnionService object."""
        self.master_key = master_key
        self.file_name = f'{DIR_USER_DATA}{TX}_onion_db'
        self.is_delivered = False
        self.conf_code = csprng(CONFIRM_CODE_LENGTH)

        ensure_dir(DIR_USER_DATA)
        if os.path.isfile(self.file_name):
            self.onion_private_key = self.load_onion_service_private_key()
        else:
            self.onion_private_key = self.new_onion_service_private_key()
            self.store_onion_service_private_key()

        self.public_key = bytes(
            nacl.signing.SigningKey(seed=self.onion_private_key).verify_key)

        self.user_onion_address = pub_key_to_onion_address(self.public_key)
        self.user_short_address = pub_key_to_short_address(self.public_key)
예제 #14
0
파일: test_crypto.py 프로젝트: AJMartel/tfc
 def test_key_generation(self):
     self.assertEqual(len(csprng()), KEY_LENGTH)
     self.assertIsInstance(csprng(), bytes)
예제 #15
0
    def new_master_key(self, replace: bool = True) -> bytes:
        """Create a new master key from password and salt.

        The generated master key depends on a 256-bit salt and the
        password entered by the user. Additional computational strength
        is added by the slow hash function (Argon2id). The more cores
        and the faster each core is, and the more memory the system has,
        the more secure TFC data is under the same password.

        This method automatically tweaks the Argon2 time and memory cost
        parameters according to best practices as determined in

            https://tools.ietf.org/html/draft-irtf-cfrg-argon2-09#section-4

        1) For Argon2 type (y), Argon2id was selected because the
           adversary might be able to run arbitrary code on Destination
           Computer and thus perform a side-channel attack against the
           function.

        2) The maximum number of threads (h) is determined by the number
           available in the system. However, during local testing this
           number is reduced to half to allow simultaneous login to
           Transmitter and Receiver Program.

        3) The maximum amount of memory (m) is what the system has to
           offer. For hard-drive encryption purposes, the recommendation
           is 6GiB. TFC will use that amount (or even more) if available.
           However, on less powerful systems, it will settle for less.

        4) For key derivation time (x), the value is set to at least 3
           seconds, with the maximum being 4 seconds. The minimum value
           is the same as the recommendation for hard-drive encryption.

        5) The salt length is set to 256-bits which is double the
           recommended length. The salt size ensures that even in a
           group of 4.8*10^29 users, the probability that two users
           share the same salt is just 10^(-18).*
            * https://en.wikipedia.org/wiki/Birthday_attack

           The salt does not need additional protection as the security
           it provides depends on the salt space in relation to the
           number of attacked targets (i.e. if two or more physically
           compromised systems happen to share the same salt, the
           attacker can speed up the attack against those systems with
           time-memory-trade-off attack).

        6) The tag length isn't utilized. The result of the key
           derivation is the master encryption key itself, which is set
           to 32 bytes for use in XChaCha20-Poly1305.

        7) Memory wiping feature is not provided.

        To recognize the password is correct, the BLAKE2b hash of the
        master key is stored together with key derivation parameters
        into the login database.
            The preimage resistance of BLAKE2b prevents derivation of
        master key from the stored hash, and Argon2id ensures brute
        force and dictionary attacks against the master password are
        painfully slow even with GPUs/ASICs/FPGAs, as long as the
        password is sufficiently strong.
        """
        password = MasterKey.new_password()
        salt = csprng(ARGON2_SALT_LENGTH)

        # Determine the amount of memory used from the amount of free RAM in the system.
        memory_cost = self.get_available_memory()

        # Determine the number of threads to use
        parallelism = multiprocessing.cpu_count()
        if self.local_test:
            parallelism = max(ARGON2_MIN_PARALLELISM, parallelism // 2)

        # Determine time cost
        time_cost, kd_time, master_key = self.determine_time_cost(
            password, salt, memory_cost, parallelism)

        # Determine memory cost
        if kd_time > MAX_KEY_DERIVATION_TIME:
            memory_cost, master_key = self.determine_memory_cost(
                password, salt, time_cost, memory_cost, parallelism)

        # Store values to database
        database_data = (salt + blake2b(master_key) + int_to_bytes(time_cost) +
                         int_to_bytes(memory_cost) + int_to_bytes(parallelism))

        if replace:
            self.database.store_unencrypted_database(database_data)
        else:
            # When replacing the master key, the new master key needs to be generated before
            # databases are encrypted. However, storing the new master key shouldn't be done
            # before all new databases have been successfully written. We therefore just cache
            # the database data.
            self.database_data = database_data

        print_on_previous_line()
        phase("Deriving master key")
        phase(DONE, delay=1)

        return master_key
예제 #16
0
def process_local_key(ts: 'datetime', packet: bytes, window_list: 'WindowList',
                      contact_list: 'ContactList', key_list: 'KeyList',
                      settings: 'Settings', kdk_hashes: List[bytes],
                      packet_hashes: List[bytes], l_queue: 'Queue') -> None:
    """Decrypt local key packet and add local contact/keyset."""
    bootstrap = not key_list.has_local_keyset()
    plaintext = None

    try:
        packet_hash = blake2b(packet)

        # Check if the packet is an old one
        if packet_hash in packet_hashes:
            raise FunctionReturn("Error: Received old local key packet.",
                                 output=False)

        while True:
            m_print("Local key setup",
                    bold=True,
                    head_clear=True,
                    head=1,
                    tail=1)
            kdk = get_b58_key(B58_LOCAL_KEY, settings)
            kdk_hash = blake2b(kdk)

            try:
                plaintext = auth_and_decrypt(packet, kdk)
                break
            except nacl.exceptions.CryptoError:
                # Check if key was an old one
                if kdk_hash in kdk_hashes:
                    m_print("Error: Entered an old local key decryption key.",
                            delay=1)
                    continue

                # Check if the kdk was for a packet further ahead in the queue
                buffer = []  # type: List[Tuple[datetime, bytes]]
                while l_queue.qsize() > 0:
                    tup = l_queue.get()  # type: Tuple[datetime, bytes]
                    if tup not in buffer:
                        buffer.append(tup)

                for i, tup in enumerate(buffer):
                    try:
                        plaintext = auth_and_decrypt(tup[1], kdk)

                        # If we reach this point, decryption was successful.
                        for unexamined in buffer[i + 1:]:
                            l_queue.put(unexamined)
                        buffer = []
                        ts = tup[0]
                        break

                    except nacl.exceptions.CryptoError:
                        continue
                else:
                    # Finished the buffer without finding local key CT
                    # for the kdk. Maybe the kdk is from another session.
                    raise FunctionReturn(
                        "Error: Incorrect key decryption key.", delay=1)

            break

        # This catches PyCharm's weird claim that plaintext might be referenced before assignment
        if plaintext is None:  # pragma: no cover
            raise FunctionReturn("Error: Could not decrypt local key.")

        # Add local contact to contact list database
        contact_list.add_contact(LOCAL_PUBKEY,
                                 LOCAL_NICK, KEX_STATUS_LOCAL_KEY,
                                 bytes(FINGERPRINT_LENGTH),
                                 bytes(FINGERPRINT_LENGTH), False, False, True)

        tx_mk, tx_hk, c_code = separate_headers(plaintext,
                                                2 * [SYMMETRIC_KEY_LENGTH])

        # Add local keyset to keyset database
        key_list.add_keyset(onion_pub_key=LOCAL_PUBKEY,
                            tx_mk=tx_mk,
                            rx_mk=csprng(),
                            tx_hk=tx_hk,
                            rx_hk=csprng())

        # Cache hashes needed to recognize reissued local key packets and key decryption keys.
        packet_hashes.append(packet_hash)
        kdk_hashes.append(kdk_hash)

        # Prevent leak of KDK via terminal history / clipboard
        readline.clear_history()
        os.system(RESET)
        root = tkinter.Tk()
        root.withdraw()
        try:
            if root.clipboard_get() == b58encode(kdk):
                root.clipboard_clear()
        except tkinter.TclError:
            pass
        root.destroy()

        m_print([
            "Local key successfully installed.",
            f"Confirmation code (to Transmitter): {c_code.hex()}"
        ],
                box=True,
                head=1)

        local_win = window_list.get_local_window()
        local_win.add_new(ts, "Added new local key.")

        if bootstrap:
            window_list.active_win = local_win

    except (EOFError, KeyboardInterrupt):
        m_print("Local key setup aborted.",
                bold=True,
                tail_clear=True,
                delay=1,
                head=2)

        if window_list.active_win is not None and not bootstrap:
            window_list.active_win.redraw()

        raise FunctionReturn("Local key setup aborted.", output=False)
예제 #17
0
파일: key_exchanges.py 프로젝트: gtog/tfc
def start_key_exchange(
    onion_pub_key: bytes,  # Public key of contact's v3 Onion Service
    nick: str,  # Contact's nickname
    contact_list: 'ContactList',  # Contact list object
    settings: 'Settings',  # Settings object
    queues: 'QueueDict'  # Dictionary of multiprocessing queues
) -> None:
    """Start X448 key exchange with the recipient.

    This function first creates the X448 key pair. It then outputs the
    public key to Relay Program on Networked Computer, that passes the
    public key to contact's Relay Program. When contact's public key
    reaches the user's Relay Program, the user will manually copy the
    key into their Transmitter Program.

    The X448 shared secret is used to create unidirectional message and
    header keys, that will be used in forward secret communication. This
    is followed by the fingerprint verification where the user manually
    authenticates the public key.

    Once the fingerprint has been accepted, this function will add the
    contact/key data to contact/key databases, and export that data to
    the Receiver Program on Destination Computer. The transmission is
    encrypted with the local key.

    ---

    TFC provides proactive security by making fingerprint verification
    part of the key exchange. This prevents the situation where the
    users don't know about the feature, and thus helps minimize the risk
    of MITM attack.

    The fingerprints can be skipped by pressing Ctrl+C. This feature is
    not advertised however, because verifying fingerprints the only
    strong way to be sure TFC is not under MITM attack. When
    verification is skipped, TFC marks the contact's X448 keys as
    "Unverified". The fingerprints can later be verified with the
    `/verify` command: answering `yes` to the question on whether the
    fingerprints match, marks the X448 keys as "Verified".

    Variable naming:
        tx = user's key     rx = contact's key    fp = fingerprint
        mk = message key    hk = header key
    """
    if not contact_list.has_pub_key(onion_pub_key):
        contact_list.add_contact(onion_pub_key, nick,
                                 bytes(FINGERPRINT_LENGTH),
                                 bytes(FINGERPRINT_LENGTH), KEX_STATUS_PENDING,
                                 settings.log_messages_by_default,
                                 settings.accept_files_by_default,
                                 settings.show_notifications_by_default)
    contact = contact_list.get_contact_by_pub_key(onion_pub_key)

    # Generate new private key or load cached private key
    if contact.tfc_private_key is None:
        tfc_private_key_user = X448.generate_private_key()
    else:
        tfc_private_key_user = contact.tfc_private_key

    try:
        tfc_public_key_user = X448.derive_public_key(tfc_private_key_user)

        # Import public key of contact
        while True:
            public_key_packet = PUBLIC_KEY_DATAGRAM_HEADER + onion_pub_key + tfc_public_key_user
            queue_to_nc(public_key_packet, queues[RELAY_PACKET_QUEUE])

            tfc_public_key_contact = get_b58_key(B58_PUBLIC_KEY, settings,
                                                 contact.short_address)
            if tfc_public_key_contact != b'':
                break

        # Validate public key of contact
        if len(tfc_public_key_contact) != TFC_PUBLIC_KEY_LENGTH:
            m_print([
                "Warning!", "Received invalid size public key.",
                "Aborting key exchange for your safety."
            ],
                    bold=True,
                    tail=1)
            raise FunctionReturn("Error: Invalid public key length",
                                 output=False)

        if tfc_public_key_contact == bytes(TFC_PUBLIC_KEY_LENGTH):
            # The public key of contact is zero with negligible probability,
            # therefore we assume such key is malicious and attempts to set
            # the shared key to zero.
            m_print([
                "Warning!", "Received a malicious zero-public key.",
                "Aborting key exchange for your safety."
            ],
                    bold=True,
                    tail=1)
            raise FunctionReturn("Error: Zero public key", output=False)

        # Derive shared key
        dh_shared_key = X448.shared_key(tfc_private_key_user,
                                        tfc_public_key_contact)

        # Domain separate unidirectional keys from shared key by using public
        # keys as message and the context variable as personalization string.
        tx_mk = blake2b(tfc_public_key_contact,
                        dh_shared_key,
                        person=b'message_key',
                        digest_size=SYMMETRIC_KEY_LENGTH)
        rx_mk = blake2b(tfc_public_key_user,
                        dh_shared_key,
                        person=b'message_key',
                        digest_size=SYMMETRIC_KEY_LENGTH)
        tx_hk = blake2b(tfc_public_key_contact,
                        dh_shared_key,
                        person=b'header_key',
                        digest_size=SYMMETRIC_KEY_LENGTH)
        rx_hk = blake2b(tfc_public_key_user,
                        dh_shared_key,
                        person=b'header_key',
                        digest_size=SYMMETRIC_KEY_LENGTH)

        # Domain separate fingerprints of public keys by using the
        # shared secret as key and the context variable as
        # personalization string. This way entities who might monitor
        # fingerprint verification channel are unable to correlate
        # spoken values with public keys that they might see on RAM or
        # screen of Networked Computer: Public keys can not be derived
        # from the fingerprints due to preimage resistance of BLAKE2b,
        # and fingerprints can not be derived from public key without
        # the X448 shared secret. Using the context variable ensures
        # fingerprints are distinct from derived message and header keys.
        tx_fp = blake2b(tfc_public_key_user,
                        dh_shared_key,
                        person=b'fingerprint',
                        digest_size=FINGERPRINT_LENGTH)
        rx_fp = blake2b(tfc_public_key_contact,
                        dh_shared_key,
                        person=b'fingerprint',
                        digest_size=FINGERPRINT_LENGTH)

        # Verify fingerprints
        try:
            if not verify_fingerprints(tx_fp, rx_fp):
                m_print([
                    "Warning!", "Possible man-in-the-middle attack detected.",
                    "Aborting key exchange for your safety."
                ],
                        bold=True,
                        tail=1)
                raise FunctionReturn("Error: Fingerprint mismatch",
                                     delay=2.5,
                                     output=False)
            kex_status = KEX_STATUS_VERIFIED

        except (EOFError, KeyboardInterrupt):
            m_print([
                "Skipping fingerprint verification.", '', "Warning!",
                "Man-in-the-middle attacks can not be detected",
                "unless fingerprints are verified! To re-verify",
                "the contact, use the command '/verify'.", '',
                "Press <enter> to continue."
            ],
                    manual_proceed=True,
                    box=True,
                    head=2)
            kex_status = KEX_STATUS_UNVERIFIED

        # Send keys to the Receiver Program
        c_code = blake2b(onion_pub_key, digest_size=CONFIRM_CODE_LENGTH)
        command = (KEY_EX_ECDHE + onion_pub_key + tx_mk + rx_mk + tx_hk +
                   rx_hk + str_to_bytes(nick))

        queue_command(command, settings, queues)

        while True:
            purp_code = ask_confirmation_code('Receiver')
            if purp_code == c_code.hex():
                break

            elif purp_code == '':
                phase("Resending contact data", head=2)
                queue_command(command, settings, queues)
                phase(DONE)
                print_on_previous_line(reps=5)

            else:
                m_print("Incorrect confirmation code.", head=1)
                print_on_previous_line(reps=4, delay=2)

        # Store contact data into databases
        contact.tfc_private_key = None
        contact.tx_fingerprint = tx_fp
        contact.rx_fingerprint = rx_fp
        contact.kex_status = kex_status
        contact_list.store_contacts()

        queues[KEY_MANAGEMENT_QUEUE].put(
            (KDB_ADD_ENTRY_HEADER, onion_pub_key, tx_mk, csprng(), tx_hk,
             csprng()))

        m_print(f"Successfully added {nick}.",
                bold=True,
                tail_clear=True,
                delay=1,
                head=1)

    except (EOFError, KeyboardInterrupt):
        contact.tfc_private_key = tfc_private_key_user
        raise FunctionReturn("Key exchange interrupted.",
                             tail_clear=True,
                             delay=1,
                             head=2)
예제 #18
0
def send_file(path:     str,
              settings: 'Settings',
              queues:   'QueueDict',
              window:   'TxWindow'
              ) -> None:
    """Send file to window members in a single transmission.

    This is the default mode for file transmission, used when traffic
    masking is not enabled. The file is loaded and compressed before it
    is encrypted. The encrypted file is then exported to Networked
    Computer along with a list of Onion Service public keys (members in
    window) of all recipients to whom the Relay Program will multi-cast
    the file to.

    Once the file ciphertext has been exported, this function will
    multi-cast the file decryption key to each recipient inside an
    automated key delivery message that uses a special FILE_KEY_HEADER
    in place of standard PRIVATE_MESSAGE_HEADER. To know for which file
    ciphertext the key is for, an identifier must be added to the key
    delivery message. The identifier in this case is the BLAKE2b digest
    of the ciphertext itself. The reason of using the digest as the
    identifier is, it authenticates both the ciphertext and its origin.
    To understand this, consider the following attack scenario:

    Let the file ciphertext identifier be just a random 32-byte value "ID".

    1) Alice sends Bob and Chuck (a malicious common peer) a file
       ciphertext and identifier CT|ID (where | denotes concatenation).

    2) Chuck who has compromised Bob's Networked Computer interdicts the
       CT|ID from Alice.

    3) Chuck decrypts CT in his end, makes edits to the plaintext PT to
       create PT'.

    4) Chuck re-encrypts PT' with the same symmetric key to produce CT'.

    5) Chuck re-uses the ID and produces CT'|ID.

    6) Chuck uploads the CT'|ID to Bob's Networked Computer and replaces
       the interdicted CT|ID with it.

    7) When Bob' Receiver Program receives the automated key delivery
       message from Alice, his Receiver program uses the bundled ID to
       identify the key is for CT'.

    8) Bob's Receiver decrypts CT' using the newly received key and
       obtains Chuck's PT', that appears to come from Alice.

    Now, consider a situation where the ID is instead calculated
    ID = BLAKE2b(CT), if Chuck edits the PT, the CT' will by definition
    be different from CT, and the BLAKE2b digest will also be different.
    In order to make Bob decrypt CT', Chuck needs to also change the
    hash in Alice's key delivery message, which means Chuck needs to
    create an existential forgery of the TFC message. Since the Poly1305
    tag prevents this, the calculated ID is enough to authenticate the
    ciphertext.

    If Chuck attempts to send their own key delivery message, Chuck's
    own Onion Service public key used to identify the TFC message key
    (decryption key for the key delivery message) will be permanently
    associated with the file hash, so if they inject a file CT, and Bob
    has decided to enable file reception for Chuck, the file CT will
    appear to come from Chuck, and not from Alice. From the perspective
    of Bob, it's as if Chuck had dropped Alice's file and sent him
    another file instead.
    """
    from src.transmitter.windows import MockWindow  # Avoid circular import

    if settings.traffic_masking:
        raise FunctionReturn("Error: Command is disabled during traffic masking.", head_clear=True)

    name = path.split('/')[-1]
    data = bytearray()
    data.extend(str_to_bytes(name))

    if not os.path.isfile(path):
        raise FunctionReturn("Error: File not found.", head_clear=True)

    if os.path.getsize(path) == 0:
        raise FunctionReturn("Error: Target file is empty.", head_clear=True)

    phase("Reading data")
    with open(path, 'rb') as f:
        data.extend(f.read())
    phase(DONE)
    print_on_previous_line(flush=True)

    phase("Compressing data")
    comp = bytes(zlib.compress(bytes(data), level=COMPRESSION_LEVEL))
    phase(DONE)
    print_on_previous_line(flush=True)

    phase("Encrypting data")
    file_key = csprng()
    file_ct  = encrypt_and_sign(comp, file_key)
    ct_hash  = blake2b(file_ct)
    phase(DONE)
    print_on_previous_line(flush=True)

    phase("Exporting data")
    no_contacts  = int_to_bytes(len(window))
    ser_contacts = b''.join([c.onion_pub_key for c in window])
    file_packet  = FILE_DATAGRAM_HEADER + no_contacts + ser_contacts + file_ct
    queue_to_nc(file_packet, queues[RELAY_PACKET_QUEUE])

    key_delivery_msg = base64.b85encode(ct_hash + file_key).decode()
    for contact in window:
        queue_message(user_input=UserInput(key_delivery_msg, MESSAGE),
                      window    =MockWindow(contact.onion_pub_key, [contact]),
                      settings  =settings,
                      queues    =queues,
                      header    =FILE_KEY_HEADER,
                      log_as_ph =True)
    phase(DONE)
    print_on_previous_line(flush=True)
    m_print(f"Sent file '{name}' to {window.type_print} {window.name}.")
예제 #19
0
def start_key_exchange(
    onion_pub_key: bytes,  # Public key of contact's v3 Onion Service
    nick: str,  # Contact's nickname
    contact_list: 'ContactList',  # ContactList object
    settings: 'Settings',  # Settings object
    queues: 'QueueDict'  # Dictionary of multiprocessing queues
) -> None:
    """Start X448 key exchange with the recipient.

    This function first creates the X448 key pair. It then outputs the
    public key to Relay Program on Networked Computer, that passes the
    public key to contact's Relay Program where it is displayed. When
    the contact's public key reaches the user's Relay Program, the user
    will manually type the key into their Transmitter Program.

    The X448 shared secret is used to create unidirectional message and
    header keys, that will be used in forward secret communication. This
    is followed by the fingerprint verification where the user manually
    authenticates the public key.

    Once the fingerprint has been accepted, this function will add the
    contact/key data to contact/key databases, and export that data to
    the Receiver Program on Destination Computer. The transmission is
    encrypted with the local key.

    ---

    TFC provides proactive security by making fingerprint verification
    part of the key exchange. This prevents the situation where the
    users don't know about the feature, and thus helps minimize the risk
    of MITM attack.

    The fingerprints can be skipped by pressing Ctrl+C. This feature is
    not advertised however, because verifying fingerprints the only
    strong way to be sure TFC is not under MITM attack. When
    verification is skipped, TFC marks the contact's X448 keys as
    "Unverified". The fingerprints can later be verified with the
    `/verify` command: answering `yes` to the question on whether the
    fingerprints match, marks the X448 keys as "Verified".

    Variable naming:
        tx = user's key     rx = contact's key    fp = fingerprint
        mk = message key    hk = header key
    """
    if not contact_list.has_pub_key(onion_pub_key):
        contact_list.add_contact(onion_pub_key, nick,
                                 bytes(FINGERPRINT_LENGTH),
                                 bytes(FINGERPRINT_LENGTH), KEX_STATUS_PENDING,
                                 settings.log_messages_by_default,
                                 settings.accept_files_by_default,
                                 settings.show_notifications_by_default)
    contact = contact_list.get_contact_by_pub_key(onion_pub_key)

    # Generate new private key or load cached private key
    if contact.tfc_private_key is None:
        tfc_private_key_user = X448.generate_private_key()
    else:
        tfc_private_key_user = contact.tfc_private_key

    try:
        tfc_public_key_user = X448.derive_public_key(tfc_private_key_user)
        kdk_hash = contact_list.get_contact_by_pub_key(
            LOCAL_PUBKEY).tx_fingerprint
        tfc_public_key_contact = exchange_public_keys(onion_pub_key,
                                                      tfc_public_key_user,
                                                      kdk_hash, contact,
                                                      settings, queues)

        validate_contact_public_key(tfc_public_key_contact)

        dh_shared_key = X448.shared_key(tfc_private_key_user,
                                        tfc_public_key_contact)

        tx_mk, rx_mk, tx_hk, rx_hk, tx_fp, rx_fp \
            = X448.derive_subkeys(dh_shared_key, tfc_public_key_user, tfc_public_key_contact)

        kex_status = validate_contact_fingerprint(tx_fp, rx_fp)

        deliver_contact_data(KEY_EX_ECDHE, nick, onion_pub_key, tx_mk, rx_mk,
                             tx_hk, rx_hk, queues, settings)

        # Store contact data into databases
        contact.tfc_private_key = None
        contact.tx_fingerprint = tx_fp
        contact.rx_fingerprint = rx_fp
        contact.kex_status = kex_status
        contact_list.store_contacts()

        queues[KEY_MANAGEMENT_QUEUE].put(
            (KDB_ADD_ENTRY_HEADER, onion_pub_key, tx_mk, csprng(), tx_hk,
             csprng()))

        m_print(f"Successfully added {nick}.",
                bold=True,
                tail_clear=True,
                delay=1,
                head=1)

    except (EOFError, KeyboardInterrupt):
        contact.tfc_private_key = tfc_private_key_user
        raise SoftError("Key exchange interrupted.",
                        tail_clear=True,
                        delay=1,
                        head=2)
예제 #20
0
파일: db_onion.py 프로젝트: todun/tfc
 def new_confirmation_code(self) -> None:
     """Generate new confirmation code for Onion Service data."""
     self.conf_code = csprng(CONFIRM_CODE_LENGTH)
예제 #21
0
파일: key_exchanges.py 프로젝트: gtog/tfc
def new_local_key(contact_list: 'ContactList', settings: 'Settings',
                  queues: 'QueueDict') -> None:
    """Run local key exchange protocol.

    Local key encrypts commands and data sent from Source Computer to
    user's Destination Computer. The key is delivered to Destination
    Computer in packet encrypted with an ephemeral, symmetric, key
    encryption key.

    The check-summed Base58 format key decryption key is typed to
    Receiver Program manually. This prevents local key leak in following
    scenarios:

        1. CT is intercepted by an adversary on compromised Networked
           Computer, but no visual eavesdropping takes place.

        2. CT is not intercepted by an adversary on Networked Computer,
           but visual eavesdropping records key decryption key.

        3. CT is delivered from Source Computer to Destination Computer
           directly (bypassing compromised Networked Computer), and
           visual eavesdropping records key decryption key.

    Once the correct key decryption key is entered to Receiver Program,
    it will display the 2-hexadecimal confirmation code generated by
    the Transmitter Program. The code will be entered back to
    Transmitter Program to confirm the user has successfully delivered
    the key decryption key.

    The protocol is completed with Transmitter Program sending
    LOCAL_KEY_RDY signal to the Receiver Program, that then moves to
    wait for public keys from contact.
    """
    try:
        if settings.traffic_masking and contact_list.has_local_contact():
            raise FunctionReturn(
                "Error: Command is disabled during traffic masking.",
                head_clear=True)

        m_print("Local key setup", bold=True, head_clear=True, head=1, tail=1)

        if not contact_list.has_local_contact():
            time.sleep(0.5)

        key = csprng()
        hek = csprng()
        kek = csprng()
        c_code = os.urandom(CONFIRM_CODE_LENGTH)

        local_key_packet = LOCAL_KEY_DATAGRAM_HEADER + encrypt_and_sign(
            plaintext=key + hek + c_code, key=kek)

        # Deliver local key to Destination computer
        nc_bypass_msg(NC_BYPASS_START, settings)
        queue_to_nc(local_key_packet, queues[RELAY_PACKET_QUEUE])
        while True:
            print_key("Local key decryption key (to Receiver)", kek, settings)
            purp_code = ask_confirmation_code('Receiver')
            if purp_code == c_code.hex():
                nc_bypass_msg(NC_BYPASS_STOP, settings)
                break
            elif purp_code == '':
                phase("Resending local key", head=2)
                queue_to_nc(local_key_packet, queues[RELAY_PACKET_QUEUE])
                phase(DONE)
                print_on_previous_line(
                    reps=(9 if settings.local_testing_mode else 10))
            else:
                m_print([
                    "Incorrect confirmation code. If Receiver did not receive",
                    "the encrypted local key, resend it by pressing <Enter>."
                ],
                        head=1)
                print_on_previous_line(
                    reps=(9 if settings.local_testing_mode else 10), delay=2)

        # Add local contact to contact list database
        contact_list.add_contact(LOCAL_PUBKEY, LOCAL_NICK,
                                 bytes(FINGERPRINT_LENGTH),
                                 bytes(FINGERPRINT_LENGTH),
                                 KEX_STATUS_LOCAL_KEY, False, False, False)

        # Add local contact to keyset database
        queues[KEY_MANAGEMENT_QUEUE].put(
            (KDB_ADD_ENTRY_HEADER, LOCAL_PUBKEY, key, csprng(), hek, csprng()))

        # Notify Receiver that confirmation code was successfully entered
        queue_command(LOCAL_KEY_RDY, settings, queues)

        m_print("Successfully completed the local key exchange.",
                bold=True,
                tail_clear=True,
                delay=1,
                head=1)
        os.system(RESET)

    except (EOFError, KeyboardInterrupt):
        raise FunctionReturn("Local key setup aborted.",
                             tail_clear=True,
                             delay=1,
                             head=2)
예제 #22
0
파일: test_crypto.py 프로젝트: dimwap/tfc
 def test_exceeding_hash_function_max_digest_size_raises_critical_error(
         self) -> None:
     with self.assertRaises(SystemExit):
         csprng(BLAKE2_DIGEST_LENGTH_MAX + 1)
예제 #23
0
파일: test_crypto.py 프로젝트: dimwap/tfc
 def test_invalid_entropy_type_from_getrandom_raises_critical_error(
         self, _: Callable[..., None]) -> None:
     with self.assertRaises(SystemExit):
         csprng()
예제 #24
0
파일: test_crypto.py 프로젝트: dimwap/tfc
 def test_function_returns_key_of_specified_size(self) -> None:
     for key_size in range(BLAKE2_DIGEST_LENGTH_MIN,
                           BLAKE2_DIGEST_LENGTH_MAX + 1):
         key = csprng(key_size)
         self.assertEqual(len(key), key_size)
예제 #25
0
def new_local_key(contact_list: 'ContactList', settings: 'Settings',
                  queues: 'QueueDict') -> None:
    """Run local key exchange protocol.

    Local key encrypts commands and data sent from Source Computer to
    user's Destination Computer. The key is delivered to Destination
    Computer in packet encrypted with an ephemeral, symmetric, key
    encryption key.

    The check-summed Base58 format key decryption key is typed to
    Receiver Program manually. This prevents local key leak in following
    scenarios:

        1. CT is intercepted by an adversary on compromised Networked
           Computer, but no visual eavesdropping takes place.

        2. CT is not intercepted by an adversary on Networked Computer,
           but visual eavesdropping records key decryption key.

        3. CT is delivered from Source Computer to Destination Computer
           directly (bypassing compromised Networked Computer), and
           visual eavesdropping records key decryption key.

    Once the correct key decryption key is entered to Receiver Program,
    it will display the 2-hexadecimal confirmation code generated by
    the Transmitter Program. The code will be entered back to
    Transmitter Program to confirm the user has successfully delivered
    the key decryption key.

    The protocol is completed with Transmitter Program sending
    LOCAL_KEY_RDY signal to the Receiver Program, that then moves to
    wait for public keys from contact.
    """
    try:
        if settings.traffic_masking and contact_list.has_local_contact():
            raise SoftError(
                "Error: Command is disabled during traffic masking.",
                head_clear=True)

        m_print("Local key setup", bold=True, head_clear=True, head=1, tail=1)

        if not contact_list.has_local_contact():
            time.sleep(0.5)

        key = csprng()
        hek = csprng()
        kek = csprng()
        c_code = os.urandom(CONFIRM_CODE_LENGTH)

        local_key_packet = LOCAL_KEY_DATAGRAM_HEADER + encrypt_and_sign(
            plaintext=key + hek + c_code, key=kek)

        deliver_local_key(local_key_packet, kek, c_code, settings, queues)

        # Add local contact to contact list database
        contact_list.add_contact(LOCAL_PUBKEY, LOCAL_NICK,
                                 blake2b(b58encode(kek).encode()),
                                 bytes(FINGERPRINT_LENGTH),
                                 KEX_STATUS_LOCAL_KEY, False, False, False)

        # Add local contact to keyset database
        queues[KEY_MANAGEMENT_QUEUE].put(
            (KDB_ADD_ENTRY_HEADER, LOCAL_PUBKEY, key, csprng(), hek, csprng()))

        # Notify Receiver Program that confirmation code was successfully entered
        queue_command(LOCAL_KEY_RDY, settings, queues)

        m_print("Successfully completed the local key exchange.",
                bold=True,
                tail_clear=True,
                delay=1,
                head=1)
        reset_terminal()

    except (EOFError, KeyboardInterrupt):
        raise SoftError("Local key setup aborted.",
                        tail_clear=True,
                        delay=1,
                        head=2)
예제 #26
0
파일: db_onion.py 프로젝트: todun/tfc
 def new_onion_service_private_key() -> bytes:
     """Generate a new Onion Service private key and store it."""
     phase("Generate Tor OS key")
     onion_private_key = csprng(ONION_SERVICE_PRIVATE_KEY_LENGTH)
     phase(DONE)
     return onion_private_key
예제 #27
0
def create_pre_shared_key(
    onion_pub_key: bytes,  # Public key of contact's v3 Onion Service
    nick: str,  # Nick of contact
    contact_list: 'ContactList',  # Contact list object
    settings: 'Settings',  # Settings object
    onion_service: 'OnionService',  # OnionService object
    queues: 'QueueDict'  # Dictionary of multiprocessing queues
) -> None:
    """Generate a new pre-shared key for manual key delivery.

    Pre-shared keys offer a low-tech solution against the slowly
    emerging threat of quantum computers. PSKs are less convenient and
    not usable in every scenario, but until a quantum-safe key exchange
    algorithm with reasonably short keys is standardized, TFC can't
    provide a better alternative against quantum computers.

    The generated keys are protected by a key encryption key, derived
    from a 256-bit salt and a password (that is to be shared with the
    recipient) using Argon2id key derivation function.

    The encrypted message and header keys are stored together with salt
    on a removable media. This media must be a never-before-used device
    from sealed packaging. Re-using an old device might infect Source
    Computer, and the malware could either copy sensitive data on that
    removable media, or Source Computer might start transmitting the
    sensitive data covertly over the serial interface to malware on
    Networked Computer.

    Once the key has been exported to the clean drive, contact data and
    keys are exported to the Receiver Program on Destination computer.
    The transmission is encrypted with the local key.
    """
    try:
        tx_mk = csprng()
        tx_hk = csprng()
        salt = csprng()

        password = MasterKey.new_password("password for PSK")

        phase("Deriving key encryption key", head=2)
        kek = argon2_kdf(password, salt, ARGON2_PSK_TIME_COST,
                         ARGON2_PSK_MEMORY_COST, ARGON2_PSK_PARALLELISM)
        phase(DONE)

        ct_tag = encrypt_and_sign(tx_mk + tx_hk, key=kek)

        store_keys_on_removable_drive(ct_tag, salt, nick, onion_pub_key,
                                      onion_service, settings)

        deliver_contact_data(KEY_EX_PSK_TX, nick, onion_pub_key, tx_mk,
                             csprng(), tx_hk, csprng(), queues, settings)

        contact_list.add_contact(onion_pub_key, nick,
                                 bytes(FINGERPRINT_LENGTH),
                                 bytes(FINGERPRINT_LENGTH),
                                 KEX_STATUS_NO_RX_PSK,
                                 settings.log_messages_by_default,
                                 settings.accept_files_by_default,
                                 settings.show_notifications_by_default)

        queues[KEY_MANAGEMENT_QUEUE].put(
            (KDB_ADD_ENTRY_HEADER, onion_pub_key, tx_mk, csprng(), tx_hk,
             csprng()))

        m_print(f"Successfully added {nick}.",
                bold=True,
                tail_clear=True,
                delay=1,
                head=1)

    except (EOFError, KeyboardInterrupt):
        raise SoftError("PSK generation aborted.",
                        tail_clear=True,
                        delay=1,
                        head=2)
예제 #28
0
def process_local_key(ts: 'datetime', packet: bytes, window_list: 'WindowList',
                      contact_list: 'ContactList', key_list: 'KeyList',
                      settings: 'Settings', kdk_hashes: List[bytes],
                      packet_hashes: List[bytes],
                      l_queue: 'Queue[Tuple[datetime, bytes]]') -> None:
    """Decrypt local key packet and add local contact/keyset."""
    first_local_key = not key_list.has_local_keyset()

    try:
        if blake2b(packet) in packet_hashes:
            raise SoftError("Error: Received old local key packet.",
                            output=False)

        m_print("Local key setup", bold=True, head_clear=True, head=1, tail=1)

        ts, plaintext = decrypt_local_key(ts, packet, kdk_hashes,
                                          packet_hashes, settings, l_queue)

        # Add local contact to contact list database
        contact_list.add_contact(LOCAL_PUBKEY,
                                 LOCAL_NICK, KEX_STATUS_LOCAL_KEY,
                                 bytes(FINGERPRINT_LENGTH),
                                 bytes(FINGERPRINT_LENGTH), False, False, True)

        tx_mk, tx_hk, c_code = separate_headers(plaintext,
                                                2 * [SYMMETRIC_KEY_LENGTH])

        # Add local keyset to keyset database
        key_list.add_keyset(onion_pub_key=LOCAL_PUBKEY,
                            tx_mk=tx_mk,
                            rx_mk=csprng(),
                            tx_hk=tx_hk,
                            rx_hk=csprng())

        m_print([
            "Local key successfully installed.",
            f"Confirmation code (to Transmitter): {c_code.hex()}"
        ],
                box=True,
                head=1)

        cmd_win = window_list.get_command_window()

        if first_local_key:
            window_list.active_win = cmd_win

        raise SoftError("Added new local key.",
                        window=cmd_win,
                        ts=ts,
                        output=False)

    except (EOFError, KeyboardInterrupt):
        m_print("Local key setup aborted.",
                bold=True,
                tail_clear=True,
                delay=1,
                head=2)

        if window_list.active_win is not None and not first_local_key:
            window_list.active_win.redraw()

        raise SoftError("Local key setup aborted.", output=False)
예제 #29
0
def assembly_packet_creator(
                            # --- Payload creation ---

                            # Common settings
                            packet_type:        str,                        # Packet type (MESSAGE, FILE, or COMMAND, do not use tampered values)
                            payload:            Union[bytes, str] = None,   # Payload message content (Plaintext message (str), file data (bytes), or command (bytes))
                            inner_key:          bytes = None,               # Key for inner encryption layer
                            tamper_ciphertext:  bool  = False,              # When True, tampers with the inner layer of encryption to make it undecryptable

                            # Message packet parameters
                            message_header:     bytes = None,               # Message header (PRIVATE_MESSAGE_HEADER, GROUP_MESSAGE_HEADER, FILE_KEY_HEADER, or tamper byte)
                            tamper_plaintext:   bool  = False,              # When true, replaces plaintext with undecodable bytestring.
                            group_id:           bytes = None,               # When specified, creates message for group (4 byte random string)
                            group_msg_id:       bytes = None,               # The group message id (16 byte random string)
                            whisper_header:     bytes = b'\x00',            # Define whisper-header (b'\x00' for False, b'\x01' for True, others for tampering)

                            # File packet parameters
                            create_zip_bomb:    bool  = False,              # When True, creates large enough ciphertext to trigger zip bomb protection
                            tamper_compression: bool  = False,              # When True, tampers with compression to make decompression impossible
                            packet_time:        bytes = None,               # Allows overriding the 8-byte packet time header
                            packet_size:        bytes = None,               # Allows overriding the 8-byte packet size header
                            file_name:          bytes = None,               # Name of the file (allows e.g. injection of invalid file names)
                            omit_header_delim:  bool  = False,              # When True, omits the file_name<>file_data delimiter.

                            # --- Assembly packet splitting ---
                            s_header_override:  bytes = None,               # Allows overriding the `short packet` assembly packet header
                            l_header_override:  bytes = None,               # Allows overriding the `start of long packet` assembly packet header
                            a_header_override:  bytes = None,               # Allows overriding the `appended long packet` assembly packet header
                            e_header_override:  bytes = None,               # Allows overriding the `last packet of long packet` assembly packet header
                            tamper_cmd_hash:    bool  = False,              # When True, tampers with the command hash to make it undecryptable
                            no_padding:         bool  = False,              # When True, does not add padding to assembly packet.
                            split_length:       int   = PADDING_LENGTH,     # Allows configuring the length to which assembly packets are split

                            # --- Packet encryption ---
                            encrypt_packet:  bool     = False,              # When True, encrypts packet into set of datagrams starting with default key (32*b'\x01')
                            message_number:  int      = 0,                  # Determines the message key and harac for message
                            harac:           int      = INITIAL_HARAC,      # Allows choosing the hash ratchet counter for packet encryption
                            message_key:     bytes    = None,               # Allows choosing the message key to encrypt message with
                            header_key:      bytes    = None,               # Allows choosing the header key for hash ratchet encryption
                            tamper_harac:    bool     = False,              # When True, tampers with the MAC of encrypted harac
                            tamper_message:  bool     = False,              # When True, tampers with the MAC of encrypted messagae
                            onion_pub_key:   bytes    = b'',                # Defines the contact public key to use with datagram creation
                            origin_header:   bytes    = b'',                # Allows editing the origin header
                            ) -> List[bytes]:
    """Create assembly packet list and optionally encrypt it to create datagram list."""

    # ------------------------------------------------------------------------------------------------------------------
    # |                                                 Create payload                                                 |
    # ------------------------------------------------------------------------------------------------------------------

    if packet_type == MESSAGE:

        assert isinstance(payload, str)

        if message_header is None:
            if group_id is not None:
                group_msg_id_bytes = bytes(GROUP_MSG_ID_LENGTH) if group_msg_id is None else group_msg_id
                header = GROUP_MESSAGE_HEADER + group_id + group_msg_id_bytes
            else:
                header = PRIVATE_MESSAGE_HEADER
        else:
            header = message_header

        payload_bytes = UNDECODABLE_UNICODE if tamper_plaintext else payload.encode()

        payload = whisper_header + header + payload_bytes

    # ---

    elif packet_type == FILE:  # Create packets for traffic masking file transmission

        file_data_size  = 100_000_001 if create_zip_bomb else 10_000
        payload_bytes   = os.urandom(file_data_size) if payload is None else payload

        compressed      = zlib.compress(payload_bytes, level=COMPRESSION_LEVEL)
        compressed      = compressed if not tamper_compression else compressed[::-1]
        file_key_bytes  = os.urandom(SYMMETRIC_KEY_LENGTH) if inner_key is None else inner_key

        ciphertext      = encrypt_and_sign(compressed, key=file_key_bytes)
        ciphertext      = ciphertext if not tamper_ciphertext else ciphertext[::-1]
        ct_with_key     = ciphertext + file_key_bytes

        time_bytes      = int_to_bytes(2)              if packet_time is None   else packet_time
        size_bytes      = int_to_bytes(file_data_size) if packet_size is None   else packet_size
        file_name_bytes = b'test_file.txt'             if file_name   is None   else file_name
        delimiter       = US_BYTE                      if not omit_header_delim else b''

        payload   = time_bytes + size_bytes + file_name_bytes + delimiter + ct_with_key

    elif packet_type == COMMAND:
        payload = payload

    else:
        raise ValueError(f"Invalid packet type '{packet_type}'.")

    # ------------------------------------------------------------------------------------------------------------------
    # |                                       Split payload to assembly packets                                        |
    # ------------------------------------------------------------------------------------------------------------------

    s_header = {MESSAGE: M_S_HEADER, FILE: F_S_HEADER, COMMAND: C_S_HEADER}[packet_type]
    l_header = {MESSAGE: M_L_HEADER, FILE: F_L_HEADER, COMMAND: C_L_HEADER}[packet_type]
    a_header = {MESSAGE: M_A_HEADER, FILE: F_A_HEADER, COMMAND: C_A_HEADER}[packet_type]
    e_header = {MESSAGE: M_E_HEADER, FILE: F_E_HEADER, COMMAND: C_E_HEADER}[packet_type]

    s_header = s_header if s_header_override is None else s_header_override
    l_header = l_header if l_header_override is None else l_header_override
    a_header = a_header if a_header_override is None else a_header_override
    e_header = e_header if e_header_override is None else e_header_override

    if packet_type in [MESSAGE, COMMAND]:
        compressed = zlib.compress(payload, level=COMPRESSION_LEVEL)
        payload    = compressed if not tamper_compression else compressed[::-1]

    if len(payload) < PADDING_LENGTH:
        padded      = byte_padding(payload)
        packet_list = [s_header + padded]

    else:
        if packet_type == MESSAGE:
            msg_key  = csprng() if inner_key is None else inner_key
            payload  = encrypt_and_sign(payload, msg_key)
            payload  = payload if not tamper_ciphertext else payload[::-1]
            payload += msg_key

        elif packet_type == FILE:
            payload = bytes(FILE_PACKET_CTR_LENGTH) + payload

        elif packet_type == COMMAND:
            command_hash  = blake2b(payload)
            command_hash  = command_hash if not tamper_cmd_hash else command_hash[::-1]
            payload      += command_hash

        padded = payload if no_padding else byte_padding(payload)
        p_list = split_byte_string(padded, item_len=split_length)

        if packet_type == FILE:
            p_list[0] = int_to_bytes(len(p_list)) + p_list[0][FILE_PACKET_CTR_LENGTH:]

        packet_list = ([l_header + p_list[0]] +
                       [a_header + p for p in p_list[1:-1]] +
                       [e_header + p_list[-1]])

    if not encrypt_packet:
        return packet_list

    # ------------------------------------------------------------------------------------------------------------------
    # |                                  Encrypt assembly packets to create datagrams                                  |
    # ------------------------------------------------------------------------------------------------------------------

    message_key = SYMMETRIC_KEY_LENGTH * b'\x01' if message_key is None else message_key
    header_key  = SYMMETRIC_KEY_LENGTH * b'\x01' if header_key  is None else header_key

    for _ in range(message_number):
        message_key = blake2b(message_key + int_to_bytes(harac), digest_size=SYMMETRIC_KEY_LENGTH)
        harac      += 1

    assembly_ct_list = []

    for packet in packet_list:
        harac_in_bytes    = int_to_bytes(harac)
        encrypted_harac   = encrypt_and_sign(harac_in_bytes, header_key)
        encrypted_message = encrypt_and_sign(packet,         message_key)

        encrypted_harac   = encrypted_harac   if not tamper_harac   else tamper_last_byte(encrypted_harac)
        encrypted_message = encrypted_message if not tamper_message else tamper_last_byte(encrypted_message)

        encrypted_packet = onion_pub_key + origin_header + encrypted_harac + encrypted_message

        assembly_ct_list.append(encrypted_packet)

        message_key = blake2b(message_key + int_to_bytes(harac), digest_size=SYMMETRIC_KEY_LENGTH)
        harac += 1

    return assembly_ct_list
예제 #30
0
파일: test_crypto.py 프로젝트: dimwap/tfc
 def test_default_key_type_and_size(self) -> None:
     key = csprng()
     self.assertIsInstance(key, bytes)
     self.assertEqual(len(key), SYMMETRIC_KEY_LENGTH)