806 lines
39 KiB
Python
Executable File
806 lines
39 KiB
Python
Executable File
#!/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 readline
|
|
import struct
|
|
import textwrap
|
|
import time
|
|
import typing
|
|
|
|
from multiprocessing import Queue
|
|
from typing import Any, Dict, List, Tuple, Union
|
|
|
|
from src.common.db_logs import access_logs, change_log_db_key, remove_logs, replace_log_db
|
|
from src.common.db_keys import KeyList
|
|
from src.common.encoding import b58decode, b58encode, bool_to_bytes, int_to_bytes, onion_address_to_pub_key
|
|
from src.common.exceptions import CriticalError, SoftError
|
|
from src.common.input import yes
|
|
from src.common.misc import get_terminal_width, ignored, reset_terminal, validate_onion_addr
|
|
from src.common.output import clear_screen, m_print, phase, print_on_previous_line
|
|
from src.common.statics import (CH_MASTER_KEY, CH_SETTING, CLEAR, CLEAR_SCREEN, COMMAND_PACKET_QUEUE, DONE,
|
|
EXIT_PROGRAM, GROUP_ID_ENC_LENGTH, KDB_HALT_ACK_HEADER, KDB_M_KEY_CHANGE_HALT_HEADER,
|
|
KDB_UPDATE_SIZE_HEADER, KEX_STATUS_UNVERIFIED, KEX_STATUS_VERIFIED,
|
|
KEY_MANAGEMENT_QUEUE, KEY_MGMT_ACK_QUEUE, LOCAL_TESTING_PACKET_DELAY,
|
|
LOGFILE_MASKING_QUEUE, LOG_DISPLAY, LOG_EXPORT, LOG_REMOVE, MESSAGE,
|
|
ONION_ADDRESS_LENGTH, RELAY_PACKET_QUEUE, RESET_SCREEN, RX, SENDER_MODE_QUEUE,
|
|
TRAFFIC_MASKING_QUEUE, TX, UNENCRYPTED_BAUDRATE, UNENCRYPTED_DATAGRAM_HEADER,
|
|
UNENCRYPTED_EC_RATIO, UNENCRYPTED_EXIT_COMMAND, UNENCRYPTED_MANAGE_CONTACT_REQ,
|
|
UNENCRYPTED_SCREEN_CLEAR, UNENCRYPTED_SCREEN_RESET, UNENCRYPTED_WIPE_COMMAND,
|
|
US_BYTE, VERSION, WIN_ACTIVITY, WIN_SELECT, WIN_TYPE_GROUP, WIN_UID_COMMAND,
|
|
WIN_UID_FILE, WIPE_USR_DATA)
|
|
|
|
from src.transmitter.commands_g import process_group_command
|
|
from src.transmitter.contact import add_new_contact, change_nick, contact_setting, remove_contact
|
|
from src.transmitter.key_exchanges import export_onion_service_data, new_local_key, rxp_load_psk, verify_fingerprints
|
|
from src.transmitter.packet import cancel_packet, queue_command, queue_message, queue_to_nc
|
|
from src.transmitter.user_input import UserInput
|
|
from src.transmitter.windows import select_window
|
|
|
|
if typing.TYPE_CHECKING:
|
|
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.common.gateway import Gateway
|
|
from src.transmitter.windows import TxWindow
|
|
QueueDict = Dict[bytes, Queue[Any]]
|
|
|
|
|
|
def process_command(user_input: 'UserInput',
|
|
window: 'TxWindow',
|
|
contact_list: 'ContactList',
|
|
group_list: 'GroupList',
|
|
settings: 'Settings',
|
|
queues: 'QueueDict',
|
|
master_key: 'MasterKey',
|
|
onion_service: 'OnionService',
|
|
gateway: 'Gateway'
|
|
) -> None:
|
|
"""\
|
|
Select function based on the first keyword of the
|
|
issued command, and pass relevant parameters to it.
|
|
"""
|
|
# Command Function to run ( Parameters )
|
|
# -----------------------------------------------------------------------------------------------------------------------------------------
|
|
d = {'about': (print_about, ),
|
|
'add': (add_new_contact, contact_list, group_list, settings, queues, onion_service ),
|
|
'cf': (cancel_packet, user_input, window, settings, queues ),
|
|
'cm': (cancel_packet, user_input, window, settings, queues ),
|
|
'clear': (clear_screens, user_input, window, settings, queues ),
|
|
'cmd': (rxp_show_sys_win, user_input, window, settings, queues ),
|
|
'connect': (send_onion_service_key, contact_list, settings, onion_service, gateway),
|
|
'exit': (exit_tfc, settings, queues, gateway),
|
|
'export': (log_command, user_input, window, contact_list, group_list, settings, queues, master_key ),
|
|
'fw': (rxp_show_sys_win, user_input, window, settings, queues ),
|
|
'group': (process_group_command, user_input, contact_list, group_list, settings, queues, master_key ),
|
|
'help': (print_help, settings ),
|
|
'history': (log_command, user_input, window, contact_list, group_list, settings, queues, master_key ),
|
|
'localkey': (new_local_key, contact_list, settings, queues, ),
|
|
'logging': (contact_setting, user_input, window, contact_list, group_list, settings, queues ),
|
|
'msg': (select_window, user_input, window, settings, queues, onion_service, gateway),
|
|
'names': (print_recipients, contact_list, group_list, ),
|
|
'nick': (change_nick, user_input, window, contact_list, group_list, settings, queues ),
|
|
'notify': (contact_setting, user_input, window, contact_list, group_list, settings, queues ),
|
|
'passwd': (change_master_key, user_input, contact_list, group_list, settings, queues, master_key, onion_service ),
|
|
'psk': (rxp_load_psk, window, contact_list, settings, queues ),
|
|
'reset': (clear_screens, user_input, window, settings, queues ),
|
|
'rm': (remove_contact, user_input, window, contact_list, group_list, settings, queues, master_key ),
|
|
'rmlogs': (remove_log, user_input, contact_list, group_list, settings, queues, master_key ),
|
|
'set': (change_setting, user_input, window, contact_list, group_list, settings, queues, master_key, gateway),
|
|
'settings': (print_settings, settings, gateway),
|
|
'store': (contact_setting, user_input, window, contact_list, group_list, settings, queues ),
|
|
'unread': (rxp_display_unread, settings, queues ),
|
|
'verify': (verify, window, contact_list ),
|
|
'whisper': (whisper, user_input, window, settings, queues ),
|
|
'whois': (whois, user_input, contact_list, group_list ),
|
|
'wipe': (wipe, settings, queues, gateway)
|
|
} # type: Dict[str, Any]
|
|
|
|
try:
|
|
cmd_key = user_input.plaintext.split()[0]
|
|
except (IndexError, UnboundLocalError):
|
|
raise SoftError("Error: Invalid command.", head_clear=True)
|
|
|
|
try:
|
|
from_dict = d[cmd_key]
|
|
except KeyError:
|
|
raise SoftError(f"Error: Invalid command '{cmd_key}'.", head_clear=True)
|
|
|
|
func = from_dict[0]
|
|
parameters = from_dict[1:]
|
|
func(*parameters)
|
|
|
|
|
|
def print_about() -> None:
|
|
"""Print URLs that direct to TFC's project site and documentation."""
|
|
clear_screen()
|
|
print(f"\n Tinfoil Chat {VERSION}\n\n"
|
|
" Website: https://github.com/maqp/tfc/\n"
|
|
" Wikipage: https://github.com/maqp/tfc/wiki\n")
|
|
|
|
|
|
def clear_screens(user_input: 'UserInput',
|
|
window: 'TxWindow',
|
|
settings: 'Settings',
|
|
queues: 'QueueDict'
|
|
) -> None:
|
|
"""Clear/reset screen of Source, Destination, and Networked Computer.
|
|
|
|
Only send an unencrypted command to Networked Computer if traffic
|
|
masking is disabled.
|
|
|
|
With clear command, sending only the command header is enough.
|
|
However, as reset command removes the ephemeral message log on
|
|
Receiver Program, Transmitter Program must define the window to
|
|
reset (in case, e.g., previous window selection command packet
|
|
dropped, and active window state is inconsistent between the
|
|
TCB programs).
|
|
"""
|
|
clear = user_input.plaintext.split()[0] == CLEAR
|
|
|
|
command = CLEAR_SCREEN if clear else RESET_SCREEN + window.uid
|
|
queue_command(command, settings, queues)
|
|
|
|
clear_screen()
|
|
|
|
if not settings.traffic_masking:
|
|
pt_cmd = UNENCRYPTED_SCREEN_CLEAR if clear else UNENCRYPTED_SCREEN_RESET
|
|
packet = UNENCRYPTED_DATAGRAM_HEADER + pt_cmd
|
|
queue_to_nc(packet, queues[RELAY_PACKET_QUEUE])
|
|
|
|
if not clear:
|
|
readline.clear_history()
|
|
reset_terminal()
|
|
|
|
|
|
def rxp_show_sys_win(user_input: 'UserInput',
|
|
window: 'TxWindow',
|
|
settings: 'Settings',
|
|
queues: 'QueueDict',
|
|
) -> None:
|
|
"""\
|
|
Display a system window on Receiver Program until the user presses
|
|
Enter.
|
|
|
|
Receiver Program has a dedicated window, WIN_UID_LOCAL, for system
|
|
messages that shows information about received commands, status
|
|
messages etc.
|
|
|
|
Receiver Program also has another window, WIN_UID_FILE, that shows
|
|
progress of file transmission from contacts that have traffic
|
|
masking enabled.
|
|
"""
|
|
cmd = user_input.plaintext.split()[0]
|
|
win_uid = dict(cmd=WIN_UID_COMMAND, fw=WIN_UID_FILE)[cmd]
|
|
|
|
command = WIN_SELECT + win_uid
|
|
queue_command(command, settings, queues)
|
|
|
|
try:
|
|
m_print(f"<Enter> returns Receiver to {window.name}'s window", manual_proceed=True, box=True)
|
|
except (EOFError, KeyboardInterrupt):
|
|
pass
|
|
|
|
print_on_previous_line(reps=4, flush=True)
|
|
|
|
command = WIN_SELECT + window.uid
|
|
queue_command(command, settings, queues)
|
|
|
|
|
|
def exit_tfc(settings: 'Settings',
|
|
queues: 'QueueDict',
|
|
gateway: 'Gateway'
|
|
) -> None:
|
|
"""Exit TFC on all three computers.
|
|
|
|
To exit TFC as fast as possible, this function starts by clearing
|
|
all command queues before sending the exit command to Receiver
|
|
Program. It then sends an unencrypted exit command to Relay Program
|
|
on Networked Computer. As the `sender_loop` process loads the
|
|
unencrypted exit command from queue, it detects the user's
|
|
intention, and after outputting the packet, sends the EXIT signal to
|
|
Transmitter Program's main() method that's running the
|
|
`monitor_processes` loop. Upon receiving the EXIT signal,
|
|
`monitor_processes` kills all Transmitter Program's processes and
|
|
exits the program.
|
|
|
|
During local testing, this function adds some delays to prevent TFC
|
|
programs from dying when sockets disconnect.
|
|
"""
|
|
for q in [COMMAND_PACKET_QUEUE, RELAY_PACKET_QUEUE]:
|
|
while queues[q].qsize() > 0:
|
|
queues[q].get()
|
|
|
|
queue_command(EXIT_PROGRAM, settings, queues)
|
|
|
|
if not settings.traffic_masking:
|
|
if settings.local_testing_mode:
|
|
time.sleep(LOCAL_TESTING_PACKET_DELAY)
|
|
time.sleep(gateway.settings.data_diode_sockets * 1.5)
|
|
else:
|
|
time.sleep(gateway.settings.race_condition_delay)
|
|
|
|
relay_command = UNENCRYPTED_DATAGRAM_HEADER + UNENCRYPTED_EXIT_COMMAND
|
|
queue_to_nc(relay_command, queues[RELAY_PACKET_QUEUE])
|
|
|
|
|
|
def log_command(user_input: 'UserInput',
|
|
window: 'TxWindow',
|
|
contact_list: 'ContactList',
|
|
group_list: 'GroupList',
|
|
settings: 'Settings',
|
|
queues: 'QueueDict',
|
|
master_key: 'MasterKey'
|
|
) -> None:
|
|
"""Display message logs or export them to plaintext file on TCBs.
|
|
|
|
Transmitter Program processes sent, Receiver Program sent and
|
|
received, messages of all participants in the active window.
|
|
|
|
Having the capability to export the log file from the encrypted
|
|
database is a bad idea, but as it's required by the GDPR
|
|
(https://gdpr-info.eu/art-20-gdpr/), it should be done as securely
|
|
as possible.
|
|
|
|
Therefore, before allowing export, TFC will ask for the master
|
|
password to ensure no unauthorized user who gains momentary
|
|
access to the system can the export logs from the database.
|
|
"""
|
|
cmd = user_input.plaintext.split()[0]
|
|
export, header = dict(export =(True, LOG_EXPORT),
|
|
history=(False, LOG_DISPLAY))[cmd]
|
|
|
|
try:
|
|
msg_to_load = int(user_input.plaintext.split()[1])
|
|
except ValueError:
|
|
raise SoftError("Error: Invalid number of messages.", head_clear=True)
|
|
except IndexError:
|
|
msg_to_load = 0
|
|
|
|
try:
|
|
command = header + int_to_bytes(msg_to_load) + window.uid
|
|
except struct.error:
|
|
raise SoftError("Error: Invalid number of messages.", head_clear=True)
|
|
|
|
if export and not yes(f"Export logs for '{window.name}' in plaintext?", abort=False):
|
|
raise SoftError("Log file export aborted.", tail_clear=True, head=0, delay=1)
|
|
|
|
authenticated = master_key.authenticate_action() if settings.ask_password_for_log_access else True
|
|
|
|
if authenticated:
|
|
queue_command(command, settings, queues)
|
|
access_logs(window, contact_list, group_list, settings, master_key, msg_to_load, export=export)
|
|
|
|
if export:
|
|
raise SoftError(f"Exported log file of {window.type} '{window.name}'.", head_clear=True)
|
|
|
|
|
|
def send_onion_service_key(contact_list: 'ContactList',
|
|
settings: 'Settings',
|
|
onion_service: 'OnionService',
|
|
gateway: 'Gateway'
|
|
) -> None:
|
|
"""Resend Onion Service key to Relay Program on Networked Computer.
|
|
|
|
This command is used in cases where Relay Program had to be
|
|
restarted for some reason (e.g. due to system updates).
|
|
"""
|
|
try:
|
|
if settings.traffic_masking:
|
|
m_print(["Warning!",
|
|
"Exporting Onion Service data to Networked Computer ",
|
|
"during traffic masking can reveal to an adversary ",
|
|
"TFC is being used at the moment. You should only do ",
|
|
"this if you've had to restart the Relay Program."], bold=True, head=1, tail=1)
|
|
if not yes("Proceed with the Onion Service data export?", abort=False):
|
|
raise SoftError("Onion Service data export canceled.", tail_clear=True, delay=1, head=0)
|
|
|
|
export_onion_service_data(contact_list, settings, onion_service, gateway)
|
|
except (EOFError, KeyboardInterrupt):
|
|
raise SoftError("Onion Service data export canceled.", tail_clear=True, delay=1, head=2)
|
|
|
|
|
|
def print_help(settings: 'Settings') -> None:
|
|
"""Print the list of commands."""
|
|
|
|
def help_printer(tuple_list: List[Union[Tuple[str, str, bool]]]) -> None:
|
|
"""Print list of commands and their descriptions.
|
|
|
|
Style in which commands are printed depends on terminal width.
|
|
Depending on whether traffic masking is enabled, some commands
|
|
are either displayed or hidden.
|
|
"""
|
|
len_longest_command = max(len(t[0]) for t in tuple_list) + 1 # Add one for spacing
|
|
wrapper = textwrap.TextWrapper(width=max(1, terminal_width - len_longest_command))
|
|
|
|
for help_cmd, description, display in tuple_list:
|
|
if not display:
|
|
continue
|
|
|
|
desc_lines = wrapper.fill(description).split('\n')
|
|
desc_indent = (len_longest_command - len(help_cmd)) * ' '
|
|
|
|
print(help_cmd + desc_indent + desc_lines[0])
|
|
|
|
# Print wrapped description lines with indent
|
|
if len(desc_lines) > 1:
|
|
for line in desc_lines[1:]:
|
|
print(len_longest_command * ' ' + line)
|
|
print('')
|
|
|
|
# ------------------------------------------------------------------------------------------------------------------
|
|
|
|
y_tm = settings.traffic_masking
|
|
n_tm = not y_tm
|
|
|
|
common_commands = [("/about", "Show links to project resources", True),
|
|
("/add", "Add new contact", n_tm),
|
|
("/cf", "Cancel file transmission to active contact/group", y_tm),
|
|
("/cm", "Cancel message transmission to active contact/group", True),
|
|
("/clear, ' '", "Clear TFC screens", True),
|
|
("/cmd, '//'", "Display command window on Receiver", True),
|
|
("/connect", "Resend Onion Service data to Relay", True),
|
|
("/exit", "Exit TFC on all three computers", True),
|
|
("/export (n)", "Export (n) messages from recipient's log file", True),
|
|
("/file", "Send file to active contact/group", True),
|
|
("/fw", "Display file reception window on Receiver", y_tm),
|
|
("/help", "Display this list of commands", True),
|
|
("/history (n)", "Print (n) messages from recipient's log file", True),
|
|
("/localkey", "Generate new local key pair", n_tm),
|
|
("/logging {on,off}(' all')", "Change message log setting (for all contacts)", True),
|
|
("/msg {A,N,G}", "Change recipient to Account, Nick, or Group", n_tm),
|
|
("/names", "List contacts and groups", True),
|
|
("/nick N", "Change nickname of active recipient/group to N", True),
|
|
("/notify {on,off} (' all')", "Change notification settings (for all contacts)", True),
|
|
("/passwd {tx,rx}", "Change master password on target system", n_tm),
|
|
("/psk", "Open PSK import dialog on Receiver", n_tm),
|
|
("/reset", "Reset ephemeral session log for active window", True),
|
|
("/rm {A,N}", "Remove contact specified by account A or nick N", n_tm),
|
|
("/rmlogs {A,N}", "Remove log entries for account A or nick N", True),
|
|
("/set S V", "Change setting S to value V", True),
|
|
("/settings", "List setting names, values and descriptions", True),
|
|
("/store {on,off} (' all')", "Change file reception (for all contacts)", True),
|
|
("/unread, ' '", "List windows with unread messages on Receiver", True),
|
|
("/verify", "Verify fingerprints with active contact", True),
|
|
("/whisper M", "Send message M, asking it not to be logged", True),
|
|
("/whois {A,N}", "Check which A corresponds to N or vice versa", True),
|
|
("/wipe", "Wipe all TFC user data and power off systems", True),
|
|
("Shift + PgUp/PgDn", "Scroll terminal up/down", True)]
|
|
|
|
group_commands = [("/group create G A₁..Aₙ", "Create group G and add accounts A₁..Aₙ", n_tm),
|
|
("/group join ID G A₁..Aₙ", "Join group ID, call it G and add accounts A₁..Aₙ", n_tm),
|
|
("/group add G A₁..Aₙ", "Add accounts A₁..Aₙ to group G", n_tm),
|
|
("/group rm G A₁..Aₙ", "Remove accounts A₁..Aₙ from group G", n_tm),
|
|
("/group rm G", "Remove group G", n_tm)]
|
|
|
|
terminal_width = get_terminal_width()
|
|
|
|
clear_screen()
|
|
|
|
print(textwrap.fill("List of commands:", width=terminal_width))
|
|
print('')
|
|
help_printer(common_commands)
|
|
print(terminal_width * '─')
|
|
|
|
if settings.traffic_masking:
|
|
print('')
|
|
else:
|
|
print(textwrap.fill("Group management:", width=terminal_width))
|
|
print('')
|
|
help_printer(group_commands)
|
|
print(terminal_width * '─' + '\n')
|
|
|
|
|
|
def print_recipients(contact_list: 'ContactList', group_list: 'GroupList') -> None:
|
|
"""Print the list of contacts and groups."""
|
|
contact_list.print_contacts()
|
|
group_list.print_groups()
|
|
|
|
|
|
def change_master_key(user_input: 'UserInput',
|
|
contact_list: 'ContactList',
|
|
group_list: 'GroupList',
|
|
settings: 'Settings',
|
|
queues: 'QueueDict',
|
|
master_key: 'MasterKey',
|
|
onion_service: 'OnionService'
|
|
) -> None:
|
|
"""Change the master key on Transmitter/Receiver Program."""
|
|
if settings.traffic_masking:
|
|
raise SoftError("Error: Command is disabled during traffic masking.", head_clear=True)
|
|
|
|
try:
|
|
device = user_input.plaintext.split()[1].lower()
|
|
except IndexError:
|
|
raise SoftError(f"Error: No target-system ('{TX}' or '{RX}') specified.", head_clear=True)
|
|
|
|
if device not in [TX, RX]:
|
|
raise SoftError(f"Error: Invalid target system '{device}'.", head_clear=True)
|
|
|
|
if device == RX:
|
|
queue_command(CH_MASTER_KEY, settings, queues)
|
|
return None
|
|
|
|
authenticated = master_key.authenticate_action()
|
|
|
|
if authenticated:
|
|
# Halt `sender_loop` for the duration of database re-encryption.
|
|
queues[KEY_MANAGEMENT_QUEUE].put((KDB_M_KEY_CHANGE_HALT_HEADER,))
|
|
wait_for_key_db_halt(queues)
|
|
|
|
# Load old key_list from database file as it's not used on input_loop side.
|
|
key_list = KeyList(master_key, settings)
|
|
|
|
# Cache old master key to allow log file re-encryption.
|
|
old_master_key = master_key.master_key[:]
|
|
|
|
# Create new master key but do not store new master key data into any database.
|
|
new_master_key = master_key.master_key = master_key.new_master_key(replace=False)
|
|
phase("Re-encrypting databases")
|
|
|
|
# Update encryption keys for databases
|
|
contact_list.database.database_key = new_master_key
|
|
key_list.database.database_key = new_master_key
|
|
group_list.database.database_key = new_master_key
|
|
settings.database.database_key = new_master_key
|
|
onion_service.database.database_key = new_master_key
|
|
|
|
# Create temp databases for each database, do not replace original.
|
|
with ignored(SoftError):
|
|
change_log_db_key(old_master_key, new_master_key, settings)
|
|
contact_list.store_contacts(replace=False)
|
|
key_list.store_keys(replace=False)
|
|
group_list.store_groups(replace=False)
|
|
settings.store_settings(replace=False)
|
|
onion_service.store_onion_service_private_key(replace=False)
|
|
|
|
# At this point all temp files exist and they have been checked to be valid by the respective
|
|
# temp file writing function. It's now time to create a temp file for the new master key
|
|
# database. Once the temp master key database is created, the `replace_database_data()` method
|
|
# will also run the atomic `os.replace()` command for the master key database.
|
|
master_key.replace_database_data()
|
|
|
|
# Next we do the atomic `os.replace()` for all other files too.
|
|
replace_log_db(settings)
|
|
contact_list.database.replace_database()
|
|
key_list.database.replace_database()
|
|
group_list.database.replace_database()
|
|
settings.database.replace_database()
|
|
onion_service.database.replace_database()
|
|
|
|
# Now all databases have been updated. It's time to let
|
|
# the key database know what the new master key is.
|
|
queues[KEY_MANAGEMENT_QUEUE].put(new_master_key)
|
|
|
|
wait_for_key_db_ack(new_master_key, queues)
|
|
|
|
phase(DONE)
|
|
m_print("Master key successfully changed.", bold=True, tail_clear=True, delay=1, head=1)
|
|
|
|
|
|
def wait_for_key_db_halt(queues: 'QueueDict') -> None:
|
|
"""Wait for the key database to acknowledge it has halted output of packets."""
|
|
while not queues[KEY_MGMT_ACK_QUEUE].qsize():
|
|
time.sleep(0.001)
|
|
if queues[KEY_MGMT_ACK_QUEUE].get() != KDB_HALT_ACK_HEADER:
|
|
raise SoftError("Error: Key database returned wrong signal.")
|
|
|
|
|
|
def wait_for_key_db_ack(new_master_key: bytes, queues: 'QueueDict') -> None:
|
|
"""Wait for the key database to acknowledge it has replaced the master key."""
|
|
while not queues[KEY_MGMT_ACK_QUEUE].qsize():
|
|
time.sleep(0.001)
|
|
if queues[KEY_MGMT_ACK_QUEUE].get() != new_master_key:
|
|
raise CriticalError("Key database failed to install new master key.")
|
|
|
|
|
|
def remove_log(user_input: 'UserInput',
|
|
contact_list: 'ContactList',
|
|
group_list: 'GroupList',
|
|
settings: 'Settings',
|
|
queues: 'QueueDict',
|
|
master_key: 'MasterKey'
|
|
) -> None:
|
|
"""Remove log entries for contact or group."""
|
|
try:
|
|
selection = user_input.plaintext.split()[1]
|
|
except IndexError:
|
|
raise SoftError("Error: No contact/group specified.", head_clear=True)
|
|
|
|
if not yes(f"Remove logs for {selection}?", abort=False, head=1):
|
|
raise SoftError("Log file removal aborted.", tail_clear=True, delay=1, head=0)
|
|
|
|
selector = determine_selector(selection, contact_list, group_list)
|
|
|
|
# Remove logs that match the selector
|
|
command = LOG_REMOVE + selector
|
|
queue_command(command, settings, queues)
|
|
|
|
remove_logs(contact_list, group_list, settings, master_key, selector)
|
|
|
|
|
|
def determine_selector(selection: str,
|
|
contact_list: 'ContactList',
|
|
group_list: 'GroupList'
|
|
) -> bytes:
|
|
"""Determine selector (group ID or Onion Service public key)."""
|
|
if selection in contact_list.contact_selectors():
|
|
selector = contact_list.get_contact_by_address_or_nick(selection).onion_pub_key
|
|
|
|
elif selection in group_list.get_list_of_group_names():
|
|
selector = group_list.get_group(selection).group_id
|
|
|
|
elif len(selection) == ONION_ADDRESS_LENGTH:
|
|
if validate_onion_addr(selection):
|
|
raise SoftError("Error: Invalid account.", head_clear=True)
|
|
selector = onion_address_to_pub_key(selection)
|
|
|
|
elif len(selection) == GROUP_ID_ENC_LENGTH:
|
|
try:
|
|
selector = b58decode(selection)
|
|
except ValueError:
|
|
raise SoftError("Error: Invalid group ID.", head_clear=True)
|
|
|
|
else:
|
|
raise SoftError("Error: Unknown selector.", head_clear=True)
|
|
|
|
return selector
|
|
|
|
|
|
def change_setting(user_input: 'UserInput',
|
|
window: 'TxWindow',
|
|
contact_list: 'ContactList',
|
|
group_list: 'GroupList',
|
|
settings: 'Settings',
|
|
queues: 'QueueDict',
|
|
master_key: 'MasterKey',
|
|
gateway: 'Gateway'
|
|
) -> None:
|
|
"""Change setting on Transmitter and Receiver Program."""
|
|
# Validate the KV-pair
|
|
try:
|
|
setting = user_input.plaintext.split()[1]
|
|
except IndexError:
|
|
raise SoftError("Error: No setting specified.", head_clear=True)
|
|
|
|
if setting not in (settings.key_list + gateway.settings.key_list):
|
|
raise SoftError(f"Error: Invalid setting '{setting}'.", head_clear=True)
|
|
|
|
try:
|
|
value = user_input.plaintext.split()[2]
|
|
except IndexError:
|
|
raise SoftError("Error: No value for setting specified.", head_clear=True)
|
|
|
|
relay_settings = dict(serial_error_correction=UNENCRYPTED_EC_RATIO,
|
|
serial_baudrate =UNENCRYPTED_BAUDRATE,
|
|
allow_contact_requests =UNENCRYPTED_MANAGE_CONTACT_REQ) # type: Dict[str, bytes]
|
|
|
|
check_setting_change_conditions(setting, settings, relay_settings, master_key)
|
|
|
|
change_setting_value(setting, value, relay_settings, queues, contact_list, group_list, settings, gateway)
|
|
|
|
propagate_setting_effects(setting, queues, contact_list, group_list, settings, window)
|
|
|
|
|
|
def check_setting_change_conditions(setting: str,
|
|
settings: 'Settings',
|
|
relay_settings: Dict[str, bytes],
|
|
master_key: 'MasterKey'
|
|
) -> None:
|
|
"""Check if the setting can be changed."""
|
|
if settings.traffic_masking and (setting in relay_settings or setting == "max_number_of_contacts"):
|
|
raise SoftError("Error: Can't change this setting during traffic masking.", head_clear=True)
|
|
|
|
if setting in ["use_serial_usb_adapter", "built_in_serial_interface"]:
|
|
raise SoftError("Error: Serial interface setting can only be changed manually.", head_clear=True)
|
|
|
|
if setting == "ask_password_for_log_access":
|
|
if not master_key.authenticate_action():
|
|
raise SoftError("Error: No permission to change setting.", head_clear=True)
|
|
|
|
|
|
def change_setting_value(setting: str,
|
|
value: str,
|
|
relay_settings: Dict[str, bytes],
|
|
queues: 'QueueDict',
|
|
contact_list: 'ContactList',
|
|
group_list: 'GroupList',
|
|
settings: 'Settings',
|
|
gateway: 'Gateway'
|
|
) -> None:
|
|
"""Change setting value in setting databases."""
|
|
if setting in gateway.settings.key_list:
|
|
gateway.settings.change_setting(setting, value)
|
|
else:
|
|
settings.change_setting(setting, value, contact_list, group_list)
|
|
|
|
receiver_command = CH_SETTING + setting.encode() + US_BYTE + value.encode()
|
|
|
|
queue_command(receiver_command, settings, queues)
|
|
|
|
if setting in relay_settings:
|
|
if setting == 'allow_contact_requests':
|
|
value = bool_to_bytes(settings.allow_contact_requests).decode()
|
|
relay_command = UNENCRYPTED_DATAGRAM_HEADER + relay_settings[setting] + value.encode()
|
|
queue_to_nc(relay_command, queues[RELAY_PACKET_QUEUE])
|
|
|
|
|
|
def propagate_setting_effects(setting: str,
|
|
queues: 'QueueDict',
|
|
contact_list: 'ContactList',
|
|
group_list: 'GroupList',
|
|
settings: 'Settings',
|
|
window: 'TxWindow'
|
|
) -> None:
|
|
"""Propagate the effects of the setting."""
|
|
if setting == "max_number_of_contacts":
|
|
contact_list.store_contacts()
|
|
queues[KEY_MANAGEMENT_QUEUE].put((KDB_UPDATE_SIZE_HEADER, settings))
|
|
|
|
if setting in ['max_number_of_group_members', 'max_number_of_groups']:
|
|
group_list.store_groups()
|
|
|
|
if setting == 'traffic_masking':
|
|
queues[SENDER_MODE_QUEUE].put(settings)
|
|
queues[TRAFFIC_MASKING_QUEUE].put(settings.traffic_masking)
|
|
window.deselect()
|
|
|
|
if setting == 'log_file_masking':
|
|
queues[LOGFILE_MASKING_QUEUE].put(settings.log_file_masking)
|
|
|
|
|
|
def print_settings(settings: 'Settings', gateway: 'Gateway') -> None:
|
|
"""Print settings and gateway settings."""
|
|
settings.print_settings()
|
|
gateway.settings.print_settings()
|
|
|
|
|
|
def rxp_display_unread(settings: 'Settings', queues: 'QueueDict') -> None:
|
|
"""\
|
|
Display the list of windows that contain unread messages on Receiver
|
|
Program.
|
|
"""
|
|
queue_command(WIN_ACTIVITY, settings, queues)
|
|
|
|
|
|
def verify(window: 'TxWindow', contact_list: 'ContactList') -> None:
|
|
"""Verify fingerprints with contact."""
|
|
if window.type == WIN_TYPE_GROUP or window.contact is None:
|
|
raise SoftError("Error: A group is selected.", head_clear=True)
|
|
|
|
if window.contact.uses_psk():
|
|
raise SoftError("Pre-shared keys have no fingerprints.", head_clear=True)
|
|
|
|
try:
|
|
verified = verify_fingerprints(window.contact.tx_fingerprint,
|
|
window.contact.rx_fingerprint)
|
|
except (EOFError, KeyboardInterrupt):
|
|
raise SoftError("Fingerprint verification aborted.", delay=1, head=2, tail_clear=True)
|
|
|
|
status_hr, status = {True: ("Verified", KEX_STATUS_VERIFIED),
|
|
False: ("Unverified", KEX_STATUS_UNVERIFIED)}[verified]
|
|
|
|
window.contact.kex_status = status
|
|
contact_list.store_contacts()
|
|
m_print(f"Marked fingerprints with {window.name} as '{status_hr}'.", bold=True, tail_clear=True, delay=1, tail=1)
|
|
|
|
|
|
def whisper(user_input: 'UserInput',
|
|
window: 'TxWindow',
|
|
settings: 'Settings',
|
|
queues: 'QueueDict',
|
|
) -> None:
|
|
"""\
|
|
Send a message to the contact that overrides their enabled logging
|
|
setting for that message.
|
|
|
|
The functionality of this feature is impossible to enforce, but if
|
|
the recipient can be trusted and they do not modify their client,
|
|
this feature can be used to send the message off-the-record.
|
|
"""
|
|
try:
|
|
message = user_input.plaintext.strip().split(' ', 1)[1]
|
|
except IndexError:
|
|
raise SoftError("Error: No whisper message specified.", head_clear=True)
|
|
|
|
queue_message(user_input=UserInput(message, MESSAGE),
|
|
window=window,
|
|
settings=settings,
|
|
queues=queues,
|
|
whisper=True,
|
|
log_as_ph=True)
|
|
|
|
|
|
def whois(user_input: 'UserInput',
|
|
contact_list: 'ContactList',
|
|
group_list: 'GroupList'
|
|
) -> None:
|
|
"""Do a lookup for a contact or group selector."""
|
|
try:
|
|
selector = user_input.plaintext.split()[1]
|
|
except IndexError:
|
|
raise SoftError("Error: No account or nick specified.", head_clear=True)
|
|
|
|
# Contacts
|
|
if selector in contact_list.get_list_of_addresses():
|
|
m_print([f"Nick of '{selector}' is ",
|
|
f"{contact_list.get_contact_by_address_or_nick(selector).nick}"], bold=True)
|
|
|
|
elif selector in contact_list.get_list_of_nicks():
|
|
m_print([f"Account of '{selector}' is",
|
|
f"{contact_list.get_contact_by_address_or_nick(selector).onion_address}"], bold=True)
|
|
|
|
# Groups
|
|
elif selector in group_list.get_list_of_group_names():
|
|
m_print([f"Group ID of group '{selector}' is",
|
|
f"{b58encode(group_list.get_group(selector).group_id)}"], bold=True)
|
|
|
|
elif selector in group_list.get_list_of_hr_group_ids():
|
|
m_print([f"Name of group with ID '{selector}' is",
|
|
f"{group_list.get_group_by_id(b58decode(selector)).name}"], bold=True)
|
|
|
|
else:
|
|
raise SoftError("Error: Unknown selector.", head_clear=True)
|
|
|
|
|
|
def wipe(settings: 'Settings',
|
|
queues: 'QueueDict',
|
|
gateway: 'Gateway'
|
|
) -> None:
|
|
"""\
|
|
Reset terminals, wipe all TFC user data from Source, Networked, and
|
|
Destination Computer, and power all three systems off.
|
|
|
|
The purpose of the wipe command is to provide additional protection
|
|
against physical attackers, e.g. in situation where a dissident gets
|
|
a knock on their door. By overwriting and deleting user data the
|
|
program prevents access to encrypted databases. Additional security
|
|
should be sought with full disk encryption (FDE).
|
|
|
|
Unfortunately, no effective tool for overwriting RAM currently exists.
|
|
However, as long as Source and Destination Computers use FDE and
|
|
DDR3 memory, recovery of sensitive data becomes impossible very fast:
|
|
https://www1.cs.fau.de/filepool/projects/coldboot/fares_coldboot.pdf
|
|
"""
|
|
if not yes("Wipe all user data and power off systems?", abort=False):
|
|
raise SoftError("Wipe command aborted.", head_clear=True)
|
|
|
|
clear_screen()
|
|
|
|
for q in [COMMAND_PACKET_QUEUE, RELAY_PACKET_QUEUE]:
|
|
while queues[q].qsize() != 0:
|
|
queues[q].get()
|
|
|
|
queue_command(WIPE_USR_DATA, settings, queues)
|
|
|
|
if not settings.traffic_masking:
|
|
if settings.local_testing_mode:
|
|
time.sleep(0.8)
|
|
time.sleep(gateway.settings.data_diode_sockets * 2.2)
|
|
else:
|
|
time.sleep(gateway.settings.race_condition_delay)
|
|
|
|
relay_command = UNENCRYPTED_DATAGRAM_HEADER + UNENCRYPTED_WIPE_COMMAND
|
|
queue_to_nc(relay_command, queues[RELAY_PACKET_QUEUE])
|
|
|
|
reset_terminal()
|