tfc-mirror/src/transmitter/contact.py

359 lines
15 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 typing
from typing import Any, Dict
from src.common.db_logs import remove_logs
from src.common.encoding import onion_address_to_pub_key
from src.common.exceptions import SoftError
from src.common.input import box_input, yes
from src.common.misc import ignored, validate_key_exchange, validate_nick, validate_onion_addr
from src.common.output import m_print, print_on_previous_line
from src.common.statics import (ALL, CH_FILE_RECV, CH_LOGGING, CH_NICKNAME, CH_NOTIFY, CONTACT_REM, DISABLE, ECDHE,
ENABLE, KDB_REMOVE_ENTRY_HEADER, KEY_MANAGEMENT_QUEUE, LOGGING, LOG_SETTING_QUEUE,
NOTIFY, ONION_ADDRESS_LENGTH, PSK, RELAY_PACKET_QUEUE, STORE, TRUNC_ADDRESS_LENGTH,
UNENCRYPTED_ACCOUNT_CHECK, UNENCRYPTED_ADD_NEW_CONTACT, UNENCRYPTED_DATAGRAM_HEADER,
UNENCRYPTED_REM_CONTACT, WIN_TYPE_CONTACT, WIN_TYPE_GROUP)
from src.transmitter.commands_g import group_rename
from src.transmitter.key_exchanges import create_pre_shared_key, start_key_exchange
from src.transmitter.packet import queue_command, queue_to_nc
if typing.TYPE_CHECKING:
from multiprocessing import Queue
from src.common.db_contacts import ContactList
from src.common.db_groups import GroupList
from src.common.db_masterkey import MasterKey
from src.common.db_onion import OnionService
from src.common.db_settings import Settings
from src.transmitter.user_input import UserInput
from src.transmitter.windows import TxWindow
QueueDict = Dict[bytes, Queue[Any]]
def add_new_contact(contact_list: 'ContactList',
group_list: 'GroupList',
settings: 'Settings',
queues: 'QueueDict',
onion_service: 'OnionService'
) -> None:
"""Prompt for contact account details and initialize desired key exchange.
This function requests the minimum amount of data about the
recipient as possible. The TFC account of contact is the same as the
Onion URL of contact's v3 Tor Onion Service. Since the accounts are
random and hard to remember, the user has to choose a nickname for
their contact. Finally, the user must select the key exchange method:
ECDHE for convenience in a pre-quantum world, or PSK for situations
where physical key exchange is possible, and ciphertext must remain
secure even after sufficient QTMs are available to adversaries.
Before starting the key exchange, Transmitter Program exports the
public key of contact's Onion Service to Relay Program on their
Networked Computer so that a connection to the contact can be
established.
"""
try:
if settings.traffic_masking:
raise SoftError("Error: Command is disabled during traffic masking.", head_clear=True)
if len(contact_list) >= settings.max_number_of_contacts:
raise SoftError(f"Error: TFC settings only allow {settings.max_number_of_contacts} accounts.",
head_clear=True)
m_print("Add new contact", head=1, bold=True, head_clear=True)
m_print(["Your TFC account is",
onion_service.user_onion_address,
'', "Warning!",
"Anyone who knows this account",
"can see when your TFC is online"], box=True)
contact_address = get_onion_address_from_user(onion_service.user_onion_address, queues)
onion_pub_key = onion_address_to_pub_key(contact_address)
contact_nick = box_input("Contact nick",
expected_len=ONION_ADDRESS_LENGTH, # Limited to 255 but such long nick is unpractical.
validator=validate_nick,
validator_args=(contact_list, group_list, onion_pub_key)).strip()
key_exchange = box_input(f"Key exchange ([{ECDHE}],PSK) ",
default=ECDHE,
expected_len=28,
validator=validate_key_exchange).strip()
relay_command = UNENCRYPTED_DATAGRAM_HEADER + UNENCRYPTED_ADD_NEW_CONTACT + onion_pub_key
queue_to_nc(relay_command, queues[RELAY_PACKET_QUEUE])
if key_exchange.upper() in ECDHE:
start_key_exchange(onion_pub_key, contact_nick, contact_list, settings, queues)
elif key_exchange.upper() in PSK:
create_pre_shared_key(onion_pub_key, contact_nick, contact_list, settings, onion_service, queues)
except (EOFError, KeyboardInterrupt):
raise SoftError("Contact creation aborted.", head=2, delay=1, tail_clear=True)
def get_onion_address_from_user(onion_address_user: str, queues: 'QueueDict') -> str:
"""Get contact's Onion Address from user."""
while True:
onion_address_contact = box_input("Contact account", expected_len=ONION_ADDRESS_LENGTH)
error_msg = validate_onion_addr(onion_address_contact, onion_address_user)
if error_msg:
m_print(error_msg, head=1)
print_on_previous_line(reps=5, delay=1)
if error_msg not in ["Error: Invalid account length.",
"Error: Account must be in lower case.",
"Error: Can not add reserved account.",
"Error: Can not add own account."]:
relay_command = UNENCRYPTED_DATAGRAM_HEADER + UNENCRYPTED_ACCOUNT_CHECK + onion_address_contact.encode()
queue_to_nc(relay_command, queues[RELAY_PACKET_QUEUE])
continue
return onion_address_contact
def remove_contact(user_input: 'UserInput',
window: 'TxWindow',
contact_list: 'ContactList',
group_list: 'GroupList',
settings: 'Settings',
queues: 'QueueDict',
master_key: 'MasterKey'
) -> None:
"""Remove contact from TFC."""
if settings.traffic_masking:
raise SoftError("Error: Command is disabled during traffic masking.", head_clear=True)
try:
selection = user_input.plaintext.split()[1]
except IndexError:
raise SoftError("Error: No account specified.", head_clear=True)
if not yes(f"Remove contact '{selection}'?", abort=False, head=1):
raise SoftError("Removal of contact aborted.", head=0, delay=1, tail_clear=True)
if selection in contact_list.contact_selectors():
onion_pub_key = contact_list.get_contact_by_address_or_nick(selection).onion_pub_key
else:
if validate_onion_addr(selection):
raise SoftError("Error: Invalid selection.", head=0, delay=1, tail_clear=True)
onion_pub_key = onion_address_to_pub_key(selection)
receiver_command = CONTACT_REM + onion_pub_key
queue_command(receiver_command, settings, queues)
with ignored(SoftError):
remove_logs(contact_list, group_list, settings, master_key, onion_pub_key)
queues[KEY_MANAGEMENT_QUEUE].put((KDB_REMOVE_ENTRY_HEADER, onion_pub_key))
relay_command = UNENCRYPTED_DATAGRAM_HEADER + UNENCRYPTED_REM_CONTACT + onion_pub_key
queue_to_nc(relay_command, queues[RELAY_PACKET_QUEUE])
target = determine_target(selection, onion_pub_key, contact_list)
if any([g.remove_members([onion_pub_key]) for g in group_list]):
m_print(f"Removed {target} from group(s).", tail=1)
check_for_window_deselection(onion_pub_key, window, group_list)
def determine_target(selection: str,
onion_pub_key: bytes,
contact_list: 'ContactList'
) -> str:
"""Determine name of the target that will be removed."""
if onion_pub_key in contact_list.get_list_of_pub_keys():
contact = contact_list.get_contact_by_pub_key(onion_pub_key)
target = f"{contact.nick} ({contact.short_address})"
contact_list.remove_contact_by_pub_key(onion_pub_key)
m_print(f"Removed {target} from contacts.", head=1, tail=1)
else:
target = f"{selection[:TRUNC_ADDRESS_LENGTH]}"
m_print(f"Transmitter has no {target} to remove.", head=1, tail=1)
return target
def check_for_window_deselection(onion_pub_key: bytes,
window: 'TxWindow',
group_list: 'GroupList'
) -> None:
"""\
Check if the window should be deselected after contact is removed.
"""
if window.type == WIN_TYPE_CONTACT:
if onion_pub_key == window.uid:
window.deselect()
if window.type == WIN_TYPE_GROUP:
for c in window:
if c.onion_pub_key == onion_pub_key:
window.update_window(group_list)
# If the last member of the group is removed, deselect
# the group. Deselection is not done in
# `TxWindow.update_window()` because it would prevent
# selecting the empty group for group related commands
# such as notifications.
if not window.window_contacts:
window.deselect()
def change_nick(user_input: 'UserInput',
window: 'TxWindow',
contact_list: 'ContactList',
group_list: 'GroupList',
settings: 'Settings',
queues: 'QueueDict'
) -> None:
"""Change nick of contact."""
try:
nick = user_input.plaintext.split()[1]
except IndexError:
raise SoftError("Error: No nick specified.", head_clear=True)
if window.type == WIN_TYPE_GROUP:
group_rename(nick, window, contact_list, group_list, settings, queues)
if window.contact is None:
raise SoftError("Error: Window does not have contact.")
onion_pub_key = window.contact.onion_pub_key
error_msg = validate_nick(nick, (contact_list, group_list, onion_pub_key))
if error_msg:
raise SoftError(error_msg, head_clear=True)
window.contact.nick = nick
window.name = nick
contact_list.store_contacts()
command = CH_NICKNAME + onion_pub_key + nick.encode()
queue_command(command, settings, queues)
def contact_setting(user_input: 'UserInput',
window: 'TxWindow',
contact_list: 'ContactList',
group_list: 'GroupList',
settings: 'Settings',
queues: 'QueueDict'
) -> None:
"""\
Change logging, file reception, or notification setting of a group
or (all) contact(s).
"""
try:
parameters = user_input.plaintext.split()
cmd_key = parameters[0]
cmd_header = {LOGGING: CH_LOGGING,
STORE: CH_FILE_RECV,
NOTIFY: CH_NOTIFY}[cmd_key]
setting, b_value = dict(on=(ENABLE, True),
off=(DISABLE, False))[parameters[1]]
except (IndexError, KeyError):
raise SoftError("Error: Invalid command.", head_clear=True)
# If second parameter 'all' is included, apply setting for all contacts and groups
try:
win_uid = b''
if parameters[2] == ALL:
cmd_value = setting.upper()
else:
raise SoftError("Error: Invalid command.", head_clear=True)
except IndexError:
win_uid = window.uid
cmd_value = setting + win_uid
if win_uid:
change_setting_for_selected_contact(cmd_key, b_value, window, contact_list, group_list)
else:
change_setting_for_all_contacts(cmd_key, b_value, contact_list, group_list)
command = cmd_header + cmd_value
if settings.traffic_masking and cmd_key == LOGGING:
# Send `log_writer_loop` the new logging setting that is loaded
# when the next noise packet is loaded from `noise_packet_loop`.
queues[LOG_SETTING_QUEUE].put(b_value)
window.update_log_messages()
queue_command(command, settings, queues)
def change_setting_for_selected_contact(cmd_key: str,
b_value: bool,
window: 'TxWindow',
contact_list: 'ContactList',
group_list: 'GroupList'
) -> None:
"""Change setting for selected contact."""
if window.type == WIN_TYPE_CONTACT and window.contact is not None:
if cmd_key == LOGGING:
window.contact.log_messages = b_value
if cmd_key == STORE:
window.contact.file_reception = b_value
if cmd_key == NOTIFY:
window.contact.notifications = b_value
contact_list.store_contacts()
if window.type == WIN_TYPE_GROUP and window.group is not None:
if cmd_key == LOGGING:
window.group.log_messages = b_value
if cmd_key == STORE:
for c in window:
c.file_reception = b_value
if cmd_key == NOTIFY:
window.group.notifications = b_value
group_list.store_groups()
def change_setting_for_all_contacts(cmd_key: str,
b_value: bool,
contact_list: 'ContactList',
group_list: 'GroupList'
) -> None:
"""Change settings for all contacts."""
for contact in contact_list:
if cmd_key == LOGGING:
contact.log_messages = b_value
if cmd_key == STORE:
contact.file_reception = b_value
if cmd_key == NOTIFY:
contact.notifications = b_value
contact_list.store_contacts()
for group in group_list:
if cmd_key == LOGGING:
group.log_messages = b_value
if cmd_key == NOTIFY:
group.notifications = b_value
group_list.store_groups()