tfc-mirror/src/transmitter/key_exchanges.py

628 lines
29 KiB
Python

#!/usr/bin/env python3.7
# -*- coding: utf-8 -*-
"""
TFC - Onion-routed, endpoint secure messaging system
Copyright (C) 2013-2020 Markus Ottela
This file is part of TFC.
TFC is free software: you can redistribute it and/or modify it under the terms
of the GNU General Public License as published by the Free Software Foundation,
either version 3 of the License, or (at your option) any later version.
TFC is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with TFC. If not, see <https://www.gnu.org/licenses/>.
"""
import os
import time
import typing
from typing import Any, Dict
from src.common.crypto import argon2_kdf, blake2b, csprng, encrypt_and_sign, X448
from src.common.db_masterkey import MasterKey
from src.common.encoding import bool_to_bytes, int_to_bytes, pub_key_to_short_address, str_to_bytes, b58encode
from src.common.exceptions import SoftError
from src.common.input import ask_confirmation_code, get_b58_key, nc_bypass_msg, yes
from src.common.misc import reset_terminal, split_to_substrings
from src.common.output import m_print, phase, print_fingerprint, print_key, print_on_previous_line
from src.common.path import ask_path_gui
from src.common.statics import (ARGON2_PSK_MEMORY_COST, ARGON2_PSK_PARALLELISM, ARGON2_PSK_TIME_COST,
B58_PUBLIC_KEY, CONFIRM_CODE_LENGTH, DONE, ECDHE, FINGERPRINT_LENGTH,
KDB_ADD_ENTRY_HEADER, KEX_STATUS_HAS_RX_PSK, KEX_STATUS_LOCAL_KEY,
KEX_STATUS_NO_RX_PSK, KEX_STATUS_PENDING, KEX_STATUS_UNVERIFIED,
KEX_STATUS_VERIFIED, KEY_EX_ECDHE, KEY_EX_PSK_RX, KEY_EX_PSK_TX,
KEY_MANAGEMENT_QUEUE, LOCAL_KEY_DATAGRAM_HEADER, LOCAL_KEY_RDY, LOCAL_NICK,
LOCAL_PUBKEY, NC_BYPASS_START, NC_BYPASS_STOP, PUBLIC_KEY_DATAGRAM_HEADER,
RELAY_PACKET_QUEUE, TFC_PUBLIC_KEY_LENGTH, UNENCRYPTED_DATAGRAM_HEADER,
UNENCRYPTED_ONION_SERVICE_DATA, UNENCRYPTED_PUBKEY_CHECK, WIN_TYPE_GROUP, ENCODED_B58_KDK_LENGTH)
from src.transmitter.packet import queue_command, queue_to_nc
if typing.TYPE_CHECKING:
from multiprocessing import Queue
from src.common.db_contacts import Contact, ContactList
from src.common.db_onion import OnionService
from src.common.db_settings import Settings
from src.common.gateway import Gateway
from src.transmitter.windows import TxWindow
QueueDict = Dict[bytes, Queue[Any]]
def export_onion_service_data(contact_list: 'ContactList',
settings: 'Settings',
onion_service: 'OnionService',
gateway: 'Gateway'
) -> None:
"""\
Send the Tor Onion Service's private key and list of Onion Service
public keys of contacts to Relay Program on Networked Computer.
This private key is not intended to be used by the Transmitter
Program. Because the Networked Computer we are exporting it to
might not store data, we use the trusted Source Computer to generate
the private key and store it safely. The private key is needed by
Tor on Networked Computer to start the Onion Service.
Exporting this private key does not endanger message confidentiality
because TFC uses a separate key exchange with separate private key
to create the symmetric keys that protect the messages. That private
key is never exported to the Networked Computer.
Access to this key does not give any to user any information other
than the v3 Onion Address. However, if they have compromised Relay
Program to gain access to the key, they can see its public part
anyway.
This key is used by Tor to sign Diffie-Hellman public keys used when
clients of contacts establish a secure connection to the Onion
Service. This key can't be used to decrypt traffic retrospectively.
The worst possible case in the situation of key compromise is, the
key allows the attacker to start their own copy of the user's Onion
Service.
This does not allow impersonating as the user however, because the
attacker is not in possession of keys that allow them to create
valid ciphertexts. Even if they inject TFC public keys to conduct a
MITM attack, that attack will be detected during fingerprint
comparison.
In addition to the private key, the Onion Service data packet also
transmits the list of Onion Service public keys of existing and
pending contacts to the Relay Program, as well as the setting that
determines whether contact requests are allowed. Bundling all this
data in a single packet is great in the sense a single confirmation
code can be used to ensure that Relay Program has all the
information necessary to perform its duties.
"""
m_print("Onion Service setup", bold=True, head_clear=True, head=1, tail=1)
pending_contacts = b''.join(contact_list.get_list_of_pending_pub_keys())
existing_contacts = b''.join(contact_list.get_list_of_existing_pub_keys())
no_pending = int_to_bytes(len(contact_list.get_list_of_pending_pub_keys()))
contact_data = no_pending + pending_contacts + existing_contacts
relay_command = (UNENCRYPTED_DATAGRAM_HEADER
+ UNENCRYPTED_ONION_SERVICE_DATA
+ onion_service.onion_private_key
+ onion_service.conf_code
+ bool_to_bytes(settings.allow_contact_requests)
+ contact_data)
deliver_onion_service_data(relay_command, onion_service, gateway)
def deliver_onion_service_data(relay_command: bytes,
onion_service: 'OnionService',
gateway: 'Gateway'
) -> None:
"""Send Onion Service data to Replay Program on Networked Computer."""
gateway.write(relay_command)
while True:
purp_code = ask_confirmation_code('Relay')
if purp_code == onion_service.conf_code.hex():
onion_service.is_delivered = True
onion_service.new_confirmation_code()
break
if purp_code == '':
phase("Resending Onion Service data", head=2)
gateway.write(relay_command)
phase(DONE)
print_on_previous_line(reps=5)
else:
m_print(["Incorrect confirmation code. If Relay Program did not",
"receive Onion Service data, resend it by pressing <Enter>."], head=1)
print_on_previous_line(reps=5, delay=2)
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 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)
def deliver_local_key(local_key_packet: bytes,
kek: bytes,
c_code: bytes,
settings: 'Settings',
queues: 'QueueDict'
) -> None:
"""Deliver encrypted 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)
# ECDHE
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_keys(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)
def exchange_public_keys(onion_pub_key: bytes,
tfc_public_key_user: bytes,
kdk_hash: bytes,
contact: 'Contact',
settings: 'Settings',
queues: 'QueueDict',
) -> bytes:
"""Exchange public keys with contact.
This function outputs the user's public key and waits for user to
enter the public key of the contact. If the User presses <Enter>,
the function will resend the users' public key to contact.
"""
public_key_packet = PUBLIC_KEY_DATAGRAM_HEADER + onion_pub_key + tfc_public_key_user
queue_to_nc(public_key_packet, queues[RELAY_PACKET_QUEUE])
while True:
try:
tfc_public_key_contact = get_b58_key(B58_PUBLIC_KEY, settings, contact.short_address)
except ValueError as invalid_pub_key:
invalid_key = str(invalid_pub_key).encode()
# Do not send packet to Relay Program if the user has for some reason
# managed to embed the local key decryption key inside the public key.
substrings = split_to_substrings(invalid_key, ENCODED_B58_KDK_LENGTH)
safe_string = not any(blake2b(substring) == kdk_hash for substring in substrings)
if safe_string:
public_key_packet = (UNENCRYPTED_DATAGRAM_HEADER + UNENCRYPTED_PUBKEY_CHECK + onion_pub_key + invalid_key)
queue_to_nc(public_key_packet, queues[RELAY_PACKET_QUEUE])
continue
if tfc_public_key_contact == b'':
public_key_packet = PUBLIC_KEY_DATAGRAM_HEADER + onion_pub_key + tfc_public_key_user
queue_to_nc(public_key_packet, queues[RELAY_PACKET_QUEUE])
continue
return tfc_public_key_contact
def validate_contact_public_key(tfc_public_key_contact: bytes) -> None:
"""This function validates the public key from contact.
The validation takes into account key state and it will detect if
the public key is zero, but it can't predict whether the shared key
will be zero. Further validation of the public key is done by the
`src.common.crypto` module.
"""
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 SoftError("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 SoftError("Error: Zero public key", output=False)
def validate_contact_fingerprint(tx_fp: bytes, rx_fp: bytes) -> bytes:
"""Validate or skip validation of contact fingerprint.
This function prompts the user to verify the fingerprint of the contact.
If the user issues Ctrl+{C,D} command, this function will set the key
exchange status as unverified.
"""
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 SoftError("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, tail=1)
kex_status = KEX_STATUS_UNVERIFIED
return kex_status
def verify_fingerprints(tx_fp: bytes, # User's fingerprint
rx_fp: bytes # Contact's fingerprint
) -> bool: # True if fingerprints match, else False
"""\
Verify fingerprints over an authenticated out-of-band channel to
detect MITM attacks against TFC's key exchange.
MITM or man-in-the-middle attack is an attack against an inherent
problem in cryptography:
Cryptography is math, nothing more. During key exchange public keys
are just very large numbers. There is no way to tell by looking if a
number (received from an untrusted network / Networked Computer) is
the same number the contact generated.
Public key fingerprints are values designed to be compared by humans
either visually or audibly (or sometimes by using semi-automatic
means such as QR-codes). By comparing the fingerprint over an
authenticated channel it's possible to verify that the correct key
was received from the network.
"""
m_print("To verify received public key was not replaced by an attacker "
"call the contact over an end-to-end encrypted line, preferably Signal "
"(https://signal.org/). Make sure Signal's safety numbers have been "
"verified, and then verbally compare the key fingerprints below.",
head_clear=True, max_width=49, head=1, tail=1)
print_fingerprint(tx_fp, " Your fingerprint (you read) ")
print_fingerprint(rx_fp, "Purported fingerprint for contact (they read)")
return yes("Is the contact's fingerprint correct?")
def deliver_contact_data(header: bytes, # Key type (x448, PSK)
nick: str, # Contact's nickname
onion_pub_key: bytes, # Public key of contact's v3 Onion Service
tx_mk: bytes, # Message key for outgoing messages
rx_mk: bytes, # Message key for incoming messages
tx_hk: bytes, # Header key for outgoing messages
rx_hk: bytes, # Header key for incoming messages
queues: 'QueueDict', # Dictionary of multiprocessing queues
settings: 'Settings', # Settings object
) -> None:
"""Deliver contact data to Destination Computer."""
c_code = blake2b(onion_pub_key, digest_size=CONFIRM_CODE_LENGTH)
command = (header + 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)
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)
def store_keys_on_removable_drive(ct_tag: bytes, # Encrypted PSK
salt: bytes, # Salt for PSK decryption key derivation
nick: str, # Contact's nickname
onion_pub_key: bytes, # Public key of contact's v3 Onion Service
onion_service: 'OnionService', # OnionService object
settings: 'Settings', # Settings object
) -> None:
"""Store keys for contact on a removable media."""
while True:
trunc_addr = pub_key_to_short_address(onion_pub_key)
store_d = ask_path_gui(f"Select removable media for {nick}", settings)
f_name = f"{store_d}/{onion_service.user_short_address}.psk - Give to {trunc_addr}"
try:
with open(f_name, "wb+") as f:
f.write(salt + ct_tag)
f.flush()
os.fsync(f.fileno())
break
except PermissionError:
m_print("Error: Did not have permission to write to the directory.", delay=0.5)
continue
def rxp_load_psk(window: 'TxWindow',
contact_list: 'ContactList',
settings: 'Settings',
queues: 'QueueDict',
) -> None:
"""Send command to Receiver Program to load PSK for active contact."""
if settings.traffic_masking:
raise SoftError("Error: Command is disabled during traffic masking.", head_clear=True)
if window.type == WIN_TYPE_GROUP or window.contact is None:
raise SoftError("Error: Group is selected.", head_clear=True)
if not contact_list.get_contact_by_pub_key(window.uid).uses_psk():
raise SoftError(f"Error: The current key was exchanged with {ECDHE}.", head_clear=True)
c_code = blake2b(window.uid, digest_size=CONFIRM_CODE_LENGTH)
command = KEY_EX_PSK_RX + c_code + window.uid
queue_command(command, settings, queues)
while True:
try:
purp_code = ask_confirmation_code('Receiver')
if purp_code == c_code.hex():
window.contact.kex_status = KEX_STATUS_HAS_RX_PSK
contact_list.store_contacts()
raise SoftError(f"Removed PSK reminder for {window.name}.", tail_clear=True, delay=1)
m_print("Incorrect confirmation code.", head=1)
print_on_previous_line(reps=4, delay=2)
except (EOFError, KeyboardInterrupt):
raise SoftError("PSK verification aborted.", tail_clear=True, delay=1, head=2)